2013年12月23日月曜日

FireMonkey の TWebBrowser を Win/Mac で使えるようにする!

Delphi Advent Calendar 2013 12/23 の記事です。

なんだか、結構需要があるっぽい FireMonkey 用の TWebBrowser を作りました!というお話です。

FireMonkey で複数 Platform に対応するコントロールを作るためには「共通の要素を Interface 化する」という作業が必要になります。
そして、それぞれの Platform で、その Interface を実装してやります。
今回は、iOS / Android 用の TWebBrowser が継承しているのと同じ ICustomBrowser を継承して、Win/Mac 用の TWebBrowser を作りました。

また、FireMonkey のファイル名は、FMX.コントロール名.Platform.pas とする慣習があります。
ということで、今回は
  • FMX.WebBrowser.Win.pas
  • FMX.WebBrowser.Mac.pas
という名前にしました。

また、元々の TWebBrowser は iOS / Android 用なので Parent を設定するという処理がありません。
Parent が設定されたら ICustomBrowser を継承した WebBrowser を表示する必要があるので、それらを処理する TWebBrowserEx というコンポーネントも作りました。

また、もう一つ FireMonkey の慣習があって、各プラットフォーム用の Interface は、implementation 部に書き、interface 部には、それを登録する関数だけ宣言する、というモノがあります。
あまり好きでは無いのですが、今回は、それに習って Win/Mac 用のユニットには RegisterWebBrowserService と UnregisterWebBrowserService という関数だけ用意しました。

unit FMX.WebBrowser.Win;
interface
procedure RegisterWebBrowserService;
procedure UnRegisterWebBrowserService;
implementation

unit FMX.WebBrowser.Mac;
interface
procedure RegisterWebBrowserService;
procedure UnRegisterWebBrowserService;
implementation

この関数を呼ぶと、TPlatformServices に Win/Mac 用の WebBrowser コントロールを「生成するクラスのインスタンス」が登録される仕組みです。
ここで、WebBrowser コントロールのインスタンスを登録してはいけません!そうすると1個しかインスタンスが作れませんからね!
WebBrowser コントロールを生成するクラスのインスタンスを登録しておけば、そのインスタンスを取り出して、いくらでも WebBrowser を生成できます。

登録したインスタンスは TPlatformServices.Current.SupportsPlatformService メソッドで取り出せます。

RegisterWebBrowserService は initialization で呼ぶようにします。

initialization
RegisterWebBrowserService;
end.

これで、それぞれの Platform 用のユニットを読み込むと、Platform に適したコンポーネントが使えるようになります。
もちろん、それぞれの Platform 用のユニットは IFDEF を使って、どれを読み込むか制御します。
今回は、Win/Mac 用なので、下記のように制御しました。

uses
System.Rtti
{$IFDEF MSWINDOWS}
, FMX.WebBrowser.Win
{$ENDIF}
{$IFDEF MACOS}
, FMX.WebBrowser.Mac
{$ENDIF}
;

これで、Win/Mac 用の WebBrowser を使えるようになりました。
しかし、WebBrowserEx には、WebBrowser を生成しているコードはありません!
生成するコードは、親の親である TCustomWebBrowser の Create で生成されています。
TPlatformServices に登録してあるので、こちらでは何もせずに、正しいインスタンスが生成されます。すばらしい!

ちなみに、軽く各 Platform の実装を説明すると、

Windows 用は、VCL の TWebBrowser を使っています。
IWebBrowserX を取り出したりとか、めんどくさいから!!
FireMonkey でも別に VCL のコントロールも使えてしまうのです。
FireMonkey は TForm だけが Window Handle を持っています。
Windows 用 WebBrowser は、VCL なので、独自に WindowHandle を持っています。
そのため、VCL の TWebBrowser より 上に FireMonkey のコントロールを載せることはできません!
今回は、TForm の指定された(Parent で)場所にコントロールが載っているように見せています。
単純に、Parent の Left,Top,Right,Bottom に合致するように TWebBrowser を作っているだけです。

OS X 用も基本的には同じ仕組みです。
ただ、こちらは VCL の TWebBrowser などないので、自分で WebView を実装しました…超大変だった…Obj-C から Delphi 用のファイルを作るのが
これができたら、あとは実直に WebView を作るだけです。
で、結構はまってる人が居るみたいですが、StackOverflow に提示してあるコードは間違っています
なぜか setHostWindow を使っていますが、これはレシーバ用の Host を決めるためのもので、Parent を設定するモノではありません!
実際には、下記のように addSubView を使って親を設定します。

procedure TMacWebBrowserService.UpdateContentFromControl;
var
View: NSView;
Bounds: TRectF;
begin
if (FWebView <> nil) then begin
if
(FWebControl <> nil)
and not (csDesigning in FWebControl.ComponentState)
and (FForm <> nil)
then begin
Bounds := GetBounds;
View := WindowHandleToPlatform(FForm.Handle).View;
View.addSubview(FWebView);
if (SameValue(Bounds.Width, 0)) or (SameValue(Bounds.Height, 0)) then
FWebView.setHidden(True)
else begin
FWebView.setFrame(GetNSBounds);
FWebView.setHidden(not FWebControl.ParentedVisible);
end;
end
else
FWebView.setHidden(True);
end;
end;

これで、表示されるようになります。

実際に使うと、こんな感じ!






28 件のコメント:

  1. このようなものを公開していただき、大変重宝し、また感謝しています。
    ですが一点、問題に遭遇しています。
    FWebBrowser.LoadFromStrings(content, '');
    のように使用すると、例外が発生し、何も表示されません。
    何かわかりますでしょうか ?

    0 CoreFoundation 0x980cb471 __raiseError + 193\
    1 libobjc.A.dylib 0x99c37091 objc_exception_throw + 162\
    2 CoreFoundation 0x980cfcb3 -[NSObject(NSObject) doesNotRecognizeSelector:] + 275\
    3 CoreFoundation 0x9801b5c2 ___forwarding___ + 1010\
    4 CoreFoundation 0x9801b1ae _CF_forwarding_prep_0 + 14\
    5 Project1 0x002cc033 TMethodImplementationIntercept + 1879535\
    6 Project1 0x0051885f TMethodImplementationIntercept + 4290075\
    7 Project1 0x00512826 TMethodImplementationIntercept + 4265442\
    8 Project1 0x003512c6 TMethodImplementationIntercept + 2424962\
    9 Project1 0x00232ca9 TMethodImplementationIntercept + 1251941\
    10 Project1 0x004d3667 TMethodImplementationIntercept + 4006947\
    11 Project1 0x0042fc59 TMethodImplementationIntercept + 3336725\
    12 Project1 0x0042fe5b TMethodImplementationIntercept + 3337239\
    13 Project1 0x00435fa4 TMethodImplementationIntercept + 3362144\
    14 Project1 0x002cbcea TMethodImplementationIntercept + 1878694\
    15 AppKit 0x90931ebb -[NSWindow sendEvent:] + 719\
    16 AppKit 0x908d08fd -[NSApplication sendEvent:] + 4034\
    17 AppKit 0x907151fc -[NSApplication run] + 823\
    18 Project1 0x002cc033 TMethodImplementationIntercept + 1879535\
    19 Project1 0x00430952 TMethodImplementationIntercept + 3340046\
    20 Project1 0x0051a987 TMethodImplementationIntercept + 4298563\
    21 ??? 0xbe00ef00 0x0 + 3187732224\

    返信削除
  2. すみません、LoadFromStrings は XE6 から新たに追加された Interface Method なので、検証が不十分でした。
    FMX.WebBrowser.Mac.pas の 208 行目 loadHTMLStrings の次の行に

    FWebView.setFrame(CGRectFromRect(RectF(0, 0, 1, 1)));

    を追加したらどうなるでしょうか?

    返信削除
  3. 変化ありませんね。
    すみません。最初の投稿で、最も手がかりになる例外メッセージの先頭行を書き漏らしていました。
    *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[WebFrame loadHTMLString:URL:]: unrecognized selector sent to instance 0x5863830'

    返信削除
    返信
    1. ありがとうございます。なるほど。loadHTMLString の引数がおかしいということですね。今すぐには直せませんが、近いうちに修正します。

      削除
    2. この問題、修正しました。
      お試し下さい。

      削除
    3. 直りました。ありがとうございます。
      お礼が遅れたことをお詫び申し上げます。

      削除
  4. hi.. i need to make a program with webbowser inside and this program need to go in win and mac.... i see this topic but i dont understand if i can use this.. and i dont understand how i can use this..

    返信削除
    返信
    1. hi. I can't speak English.
      but It is very easy to use TWebBrowserEx.

      And Souce on GitHub.
      Please, see below URL.

      https://github.com/freeonterminate/delphi/tree/master/TWebBrowser

      Ex.
      procedure TForm1.FormCreate(Sender: TObject);
      begin
      FWebBrowserEx := TWebBrowserEx.Create(Self);
      FWebBrowserEx.SetBounds(0, 0, 400, 400);
      FWebBrowserEx.Parent := Self;

      FWebBrowserEx.URL := 'http://google.com';
      end;

      If good, please introduce by a blog etc.
      Since I cannot speak English.

      削除
    2. If you are hurrying, please contact me twitter.
      my twitter account @pik

      削除
  5. 日本語をよくず、Google翻訳で上げます。ご理解願います。
    まず、あなたの労苦に感謝します。
    XE7でエラーがナドラグヨ。
    XE7も可能にお願い求めることができる。

    返信削除
    返信
    1. TWebBrowserEx をご使用頂きありがとうございます。
      XE7 対応は、まだしていなかったので、これから修正します。
      ご連絡ありがとうございます!

      削除
    2. XE7 に対応しました。
      お試し下さい。

      削除
    3. The Applicaiton made with TWebBrowserEx, The Application has 2-Focus System.
      1.FireMonkey Focus.
      2.NSWindow Focus.
      But FireMonkey handling NSWindow Focus.
      So, TWebBrowserEx can't handling Focus.
      I'm thinking for solving to this Problem
      Please wait...

      削除
  6. Hi, thank you for this OS X compatible firemonkey WebView!
    There is a problem, mouse hover does not work.
    If I go over a link with mouse, it does not underline the link.
    What can be the problem? Do you know a solution?

    返信削除
    返信
    1. The Applicaiton made with TWebBrowserEx, The Application has 2-Focus System.
      1.FireMonkey Focus.
      2.NSWindow Focus.
      But FireMonkey handling NSWindow Focus.
      So, TWebBrowserEx can't handling Focus.
      I'm thinking for solving to this Problem
      Please wait...

      削除
  7. not yet. The focus problem are still unclear in Windows. but after excuting alert function of Javascript, the problem seems to be disappeared just once.

    返信削除
  8. Try it! Focus between FMX.TEdit and TWebbrowserEx

    返信削除
    返信
    1. OK...
      I confirmed the phenomenon...
      Please Wait.

      削除
    2. こんにちは TWebbrowserExを提供されて とても感謝しています ありがとう
      but the focus problem are still occured between FMX.TEdit and TWebbrowserEx in Windows.....
      First I used mouse to focus on TWebbrowserEx then focus on TEdit but keyboard is not working on TEdit, please check it ありがとうございます

      削除
  9. はじめまして。C++Builderで動かそうとして苦労してます。リンカに「idoc.objが見つからない」と怒られるところまでたどり着きました。今まで動作している例はあるのでしょうか?

    返信削除
    返信
    1. idoc.pas は source\internet\idoc.pas にあるので、そこをインクルードパスに含めてください。

      削除
  10. Can you tell me how to get the html code of the current page in your component?

    返信削除
    返信
    1. Added New property HTMLSource !
      Please try it.

      削除