Technologies of Dolphin

Dolphin技術情報
 Dolphinで使われている技術の一部を公開いたします。

 Delphiでタブブラウザを作っている方、あるいはこれから作りたいと思っている方の支援が目的ですが、ある程度Delphi言語やCOMに関する知識・経験があるのを前提に書いています。特にCOMの操作はタブブラウザを作成する上で必ず必要になります。分からない方は、難しいとは思いますが覚えて損は無い技術ですので勉強してみてください。

[参考資料]
MSDN 「Programming and Reusing the Browser Overviews and Tutorials」
MSDN 「WebBrowser Control (Internet Explorer - WebBrowser)」
Prog/Win32/IEコンポーネント - ZeroDivision
開発言語の選択
(Windows上で動作する)タブブラウザの開発言語として、主に以下の3つが挙げられます。

C++
 最もスタンダードな選択でしょう。実行速度の速さ、資料の多さ、Windowsとの相性の良さは他を圧倒します。また、MSDNに書かれているIEに関する情報も、多くはCやC++のコードで提供されています。
 欠点としては比較的コード量が多くなる傾向があり、特に開発が進んである程度の規模になったときにメンテがしづらくなりがちです。タブブラウザは個人で作るソフトとしては規模が大きいため、開発者にはある程度の設計能力が要求されます。
C#
 .NET環境で動作させたい(マネージドコードで書きたい)のであれば、ベストな選択肢の一つとなるでしょう。すなわち、マネージドコードによる安定した動作と次期Windowsへの将来性が期待できます。
 欠点としては、.NET Frameworkを介して動作させるために実行速度(特に起動の速さ)が犠牲になります。ユーザーにもランタイムのインストールを強いることになるでしょう。また新しい言語であるため資料がほとんど無く、フリーで公開されているコンポーネントなどの資産もまだまだ少ないのが現状です。
Delphi
 Borland社が提供する言語で、DolphinはこのDelphi言語で書かれています。「Delphi6 Personal」がBorland社から無料で提供されており、開発のための投資が少ないのが利点です。また、C++には及ばないものの実行速度は速く、RAD環境のおかげでコーディング量も比較的少なくて済みます。またコンパイルが異様に速いのも開発者にとってはありがたいところ。
 欠点としてはC++に比べるとやはり資料が少ないため、調べ物に時間が掛かります。また、C++で提供されることの多いIEの情報を、Delphiにいちいち翻訳する手間もかかります。VCLを使うと実行ファイルが非常に大きくなるのも、ネット環境によっては苦しいところでしょう。

 どの言語も一長一短です。どれが自分に合うかを考え、慎重に決めるべきです。あとから変更するのは大きな労力が必要になるからです。このページではDelphiを選択したものとしてサンプルコードを提供します。
IEコンポーネント使用時の注意点
 DelphiからIEコンポーネント(TWebBrowser)を使う際には、いくつかの注意点があります。バグのため(C++との言語仕様の差異が原因?)、そのままではまともに動作してくれません。これらは自分で修正する必要があります。

「コピー」の動作不良
 以下のコードをユニットの最後部に書く必要があります。

initialization
 OleInitialize(nil);

finalization
 OleUninitialize;
Enterキーが効かない
 IEコンポーネントの仕様上、単純にキーメッセージを転送して修正することはできません。これはIE内でメッセージを処理するハンドルが隠されていて、見ることができないためです。(=メッセージの送り先が分からない)
 これを修正するにはフォームにApplicationEventsコンポーネントを追加し、そのOnMessageイベント内に以下のようなコードを書いてやる必要があります。

2005/08/21追記*
Spy++を使って調べてみたところ、どうやらTWebBrowserの子ウインドウである「Internet Explorer_Server」がキーボードメッセージを処理しているようです。これに直接メッセージを送っても良いかもしれません。


(例)
//TMainFormにApplicationEventsを配置したとします。
//WebBrowser1.: TWebBrowser;
procedure TMainForm.AppMessages(var Msg: tagMSG; var Handled: Boolean);
var
 FOleInPlaceActiveObject: IOleInPlaceActiveObject;
 Re: HRESULT;
begin
 if Msg.message = WM_KEYDOWN then
 begin
  //WebBrowserにメッセージが送られてきたかを判定
  if IsChild(WebBrowser1.Handle, Msg.hwnd) then
  begin
   FOleInPlaceActiveObject :=
   WebBrowser1.ControlInterface as IOleInPlaceActiveObject;

   //メッセージをアクティブなWebBrowser(の子ウインド)に転送
   Re := FOleInPlaceActiveObject.TranslateAccelerator(Msg);

   if Re = S_OK then
    Handled := True
   else
    Handled := False;
  end
 end
end

 この例ではEnterキー以外も全部処理していますが、経験上、Enter以外にも動作の怪しいキーがあるのでこのほうが無難だと思います。
TWebBrowserのメソッド
 頻繁に使うメソッドは数が限られています。使い方も簡単なので、まずはメソッドを使って色々と実験してみると良いでしょう。

Navigate(URL: WideString)
 指定したURLに移動します。同様のメソッドにNavigate2メソッドがありますが、こちらはURLの型がOleVariantになっており、PIDLを指定することも可能です。通常はNavigateメソッドを使えば問題ないでしょう。
GoBack、GoForward
 それぞれ、「一つ戻る」「一つ進む」機能を実現します。複数ページ戻るにはTWebBrowserに実装されているメソッドだけでは実現できません。(単純にこれらのメソッドをループ内で必要回数呼び出すことでも実現できますが、正常動作しないことがあるためオススメしません)
GoHome
 IEで設定した「ホームページ」に移動します。
Refresh
 現在表示しているページを「更新」します。同様の機能を提供するメソッドとしてRefresh2がありますが、こちらは引数によって更新のレベルを指定できます。
第1引数 動作
REFRESH_NORMAL (0) 通常の更新
REFRESH_IFEXPIRED (1) 簡易的な更新
REFRESH_COMPLETELY (3) 完全な更新
※( )内は定数の値
Stop
 移動しようとしている場合、それを「中止」します。
ExecWB
 後述します。
TWebBrowserのイベント
イベントはたくさんありますが、必要なものから順に実装していけばよいでしょう。ここでは各イベントの簡単な解説をしていきます。

OnBeforeNavigate2
 別のURLに移動しようとすると、まずこのイベントが発生します。このイベントを利用して、POSTデータも取得できます。また、引数CancelをTrueにすることによってページの移動を中止させることができます。
 フレームが使われたページでは、各フレームごとにこのイベントが発生します。引数pDispがTWebBrowserのApplicationプロパティと一致する場合、そのページは最上位のフレーム(Topのフレーム)であると判定できます。
OnNavigateComplete2
 OnBeforeNavigate2に続いて発生するイベントです。URLへのナビゲートが完了したことを示しますが、HTMLのレンダリングが終了したかどうかは保証されません。OnBeforeNavigate2と同様、フレームが使われたページでは、各フレームごとにこのイベントが発生します。
OnDocumentComplete
 ドキュメントの読み込みが完了し、レンダリングも完了した際に発生します。フレームが使われたページでは、フレームが使われたページでは、各フレームごとにこのイベントが発生しますが、完了した順に発生するため最上位のフレームは一番最後にこのイベントが発生します。
OnCommandStateChange
 「戻る」「進む」が実行できるかどうか、その状態が変化したときに発生します。・・・とドキュメントには書かれていますが、実際に状態が変化したときにだけ起こるわけではないようです。すなわち、「状態が変化する可能性があるとき」に起こります。どちらがどのような状態に変化したかは、引数CommandとEnableの値によって知ることができます。
引数Command 動作
CSC_NAVIGATEBACK 「戻る」の実行可能状態
CSC_NAVIGATEFORWARD 「進む」の実行可能状態

実際の使用例
http://support.microsoft.com/default.aspx?scid=%2Fisapi%2Fgomscom%2Easp%3Ftarget%3D%2Fjapan%2Fsupport%2Fkb%2Farticles%2Fjp163%2F2%2F82%2Easp&LN=JA
OnNewWindow2
 リンクのターゲット指定などによって、新しいウインドウ上にナビゲートされる際に発生するイベントです。引数ppDispにTWebBrowserのIDispatchを指定することで、指定したTWebBrowser上にナビゲートすることができます。(何も指定しない場合は新たにInternetExplorerが起動します。)また、引数CancelをTrueにすると新しいウインドウではなく現在のウインドウ上にナビゲートします。
OnPrivacyImpactedStateChange
クッキーの受信を制限した際に発生します。引数bImpactedを調べることで、その状況を調べることが可能です。引数bImpactedがTrueの場合はクッキーの受信が制限されたことが示されます。
OnProgressChange
 ページの移動の進行度が変更になったときに発生します。以下の式で進行度を「%」で取得できます。

Round(引数Progress / 引数ProgressMax * 100)
OnSetSecureLockIcon
 暗号化の状況が変更になった際に発生します。引数SecureLockIconの値を調べることで、現在の暗号化の状況を知ることができます。

引数SecureLockIcon 暗号レベル
0 暗号化なし
1 混在
2 不明
3 40ビット
4 56ビット
5 Fortezza
6 128ビット
OnStatusTextChange
 ステータスバー上に表示されるテキストが変更された際に発生します。引数Textにステータスバーのテキストが入っています。
OnTitleChange
 ページのタイトルが変更された際に発生します。引数Textにページのタイトルが入っています。
OnWindowClosing
 JavaScriptなどによってTWebBrowserが閉じられた際に発生します。引数CancelをTrueに設定することで、TWebBrowserが閉じられるのを中止することができます。また、引数IsChildWindowによって対象のTWebBrowserが「子ウインドウ」として作られたものかを調べることができます。
IE互換の「お気に入り」「履歴」について
 IE互換のお気に入りに「お気に入りを追加」したり、「お気に入りの整理」をする方法などを解説します。

お気に入りを追加
IShellUIHelper.AddFavoriteを使います。以下はお気に入りを追加するコードの例です。

(例)
procedure TMainForm.AddFavorites(Sender: TObject);
const
 CLSID_ShellUIHelper: TGUID = '{64AB4BB7-111E-11D1-8F79-00C04FC2FBE1}';
var
 FShellUIHelper: IShellUIHelper;
 BookMarkURL: WideString;
 BookMarkName: OleVariant;
begin
 FShellUIHelper := CreateComObject(CLSID_SHELLUIHELPER) as IShellUIHelper;

 with WebBrowser1 do
 begin
  if Document <> nil then
  begin
   BookMarkURL := LocationURL;
   BookMarkName := LocationName;
   if BookMarkURL <> '' then
    FShellUIHelper.AddFavorite(BookMarkURL, BookMarkName);

  end;
 end;
end;
お気に入りを整理
IShellUIHelper.ShowBrowserUIを使います。以下はお気に入りを整理するコードの例です。

(例)
procedure TMainForm.ArrangeFavorites(Sender: TObject);
var
 FShellUIHelper: IShellUIHelper;
 pvarIn: OleVariant;
begin
 FShellUIHelper := CreateComObject(CLSID_SHELLUIHELPER) as IShellUIHelper;
 FShellUIHelper.ShowBrowserUI('OrganizeFavorites', pvarIn);
end;
お気に入りツリービュー
 サイドパネルのお気に入りツリービューは、Ver.0.40beta2からIEで使われるものと同じタイプのものに変更しましたレジストリを触らないと正常動作しないため、Ver.0.40beta3で元に戻しました。これはDelphiとDHTMLの技術を組み合わせて実現しています。要するに、このツリービューの実体は「TWebBrowser」であるということです。この例ではDHTMLのコードを示します。
 また、clsid:55136805-B2DE-11D1-B9F2-00A0C98BC547というのはIShellNameSpaceのことです。MSDNにも詳しい情報が載っていないインターフェイスですので、分からないことがあったらまず自分で実験してみましょう。

(例)
<HTML><HEAD>
<TITLE></TITLE>
<!-- Dolphin Favorites Viewing System Ver.1.00 -->
<STYLE>.info {background: MENU; color: WINDOWTEXT; margin: 0;}</STYLE>
<SCRIPT LANGUAGE="VBScript">
Sub window_onload()
 sns.EnumOptions = 32 or 64 'ファイルとフォルダを表示
End Sub
'strName:表示名 strUrl:URL
Sub OnSelectionChange(cItems, hItem, strName, strUrl, cVisits, strDate, fAvailableOffline)
 Window.Status = strName
 Window.Document.Title = strURL
End Sub
</SCRIPT>
<SCRIPT FOR="sns" EVENT="FavoritesSelectionChange(cItems, hItem, strName, strUrl, cVisits, strDate, fAvailableOffline)">
 Call OnSelectionChange(cItems, hItem, strName, strUrl, cVisits, strDate, fAvailableOffline)
</SCRIPT>
</HEAD>
<BODY SCROLL="no" CLASS="info">
 <OBJECT ID="sns"
  STYLE="background:window; height=100%; width=100%;"
  CLASSID="clsid:55136805-B2DE-11D1-B9F2-00A0C98BC547">
 </OBJECT>
</BODY></HTML>
履歴ツリービュー
 上記のお気に入りツリービューと動作原理はほぼ同じです。ただし、PARAMタグを使って初期化しておく必要があります。

(例)
<HTML><HEAD>
<TITLE></TITLE>
<!-- Dolphin History Viewing System Ver.1.00 -->
<STYLE>.info {background: MENU; color: WINDOWTEXT; margin: 0;}</STYLE>
<SCRIPT LANGUAGE="VBScript">
Sub window_onload()
 sns.EnumOptions = 32 or 64 'ファイルとフォルダを表示
 sns.setRoot "この中に履歴のフォルダを指定。Delphiを使っても良いしDHTMLで調べても良いでしょう。"
End Sub
'strName:表示名 strUrl:URL
Sub OnSelectionChange(cItems, hItem, strName, strUrl, cVisits, strDate, fAvailableOffline)
 Window.Status = strName
 Window.Document.Title = strURL
End Sub
</SCRIPT>
<SCRIPT FOR="sns" EVENT="FavoritesSelectionChange(cItems, hItem, strName, strUrl, cVisits, strDate, fAvailableOffline)">
 Call OnSelectionChange(cItems, hItem, strName, strUrl, cVisits, strDate, fAvailableOffline)
</SCRIPT>
</HEAD>
<BODY SCROLL="no" CLASS="info">
 <OBJECT ID="sns"
  STYLE="background:window; height=100%; width=100%;"
  CLASSID="clsid:55136805-B2DE-11D1-B9F2-00A0C98BC547">
  <PARAM NAME="EnumOptions" VALUE="0"></PARAM>
 </OBJECT>
</BODY></HTML>
お気に入りのインポート/エクスポート
 お気に入りのインポートとエクスポートを行ないます。ただし、WindowsXPのサービスパック2以降では使えなくなりました。

(例)インポート
procedure TMainForm.ImportBookmark;
var
  FShellUIHelper: IShellUIHelper;
begin
  FShellUIHelper := CreateComObject(CLASS_ShellUIHelper) as IShellUIHelper;
  try
   if Assigned(FShellUIHelper) then
    FShellUIHelper.ImportExportFavorites(True, ''); 
    //↑「True」を「False」に変えるとエクスポートできる
  finally
  end;
end;
TWebBrowserのTips
その他、ブラウザを作成する上で有用な情報です。

IEコンポーネントの初期化
 「about:blank」にナビゲートしてやることで初期化します。DocumentCompleteまで待ったほうが確実かもしれませんが、Dolphinでは特に待たず、そのまま処理を続けています。
 もし待つ場合は、DocumentCompleteイベントを使用してください。ループを仕掛けてBusyプロパティで調べる方法を使うと、後々「ポップアップ抑止機能」とコンフリクトする可能性があります。(while busy do 〜 なんて待ってるときにTWebBrowserがFreeされたら・・・その後は推して知るべしorz)
ブラウザに表示される文字の大きさを取得する
procedure TMainForm.GetCharSize;
var
 CharSize: Integer; //文字の大きさ
 rc, CharZoom: OleVariant;
begin
 if WebBrowsers.Count <> 0 then
 begin
  with WebBrowser1 do
  begin
   if AnsiPos('http://', LocationURL) <> 0 then //念のためHTMLに限定してます。
    if Assigned(Document) then
    begin
     ExecWB(OLECMDID_ZOOM, OLECMDEXECOPT_DONTPROMPTUSER,
     rc, CharZoom);
     CharSize := Integer(CharZoom);
    end;
  end;
 end;
end;
ExecWBメソッドの使い方
 このメソッドは引数を変えることでいろいろな動作をしてくれるので非常に便利で、その機能も実用的なものがそろっています。

[execWBメソッドの定義]
 http://msdn.microsoft.com/library/default.asp?url=/workshop/browser/webbrowser/reference/ifaces/iwebbrowser2/execwb.asp

動作しないものやIEが異常終了するもの、バグを含むものがあります。Dolphinで動作確認されているのは以下のものだけです。ほかの引数を使う際は注意してください。
第1引数 第2引数 動作
OLECMDID_SAVEAS (4) OLECMDEXECOPT_PROMPTUSER (1) 名前をつけて保存
OLECMDID_PRINTPREVIEW (7) OLECMDEXECOPT_PROMPTUSER (1) 印刷プレビュー
OLECMDID_PRINT (6) OLECMDEXECOPT_PROMPTUSER (1) 印刷
OLECMDID_PAGESETUP (8) OLECMDEXECOPT_PROMPTUSER (1) プリンタ設定
OLECMDID_PROPERTIES (10) OLECMDEXECOPT_PROMPTUSER (1) ページのプロパティ
※( )内は定数の値

(例/名前をつけて保存)
with WebBrowser1 do
 if Document <> nil then
   ExecWB(OLECMDID_SAVEAS, OLECMDEXECOPT_PROMPTUSER);

 コピーや貼り付けなどの編集機能も、このメソッドを使って実装できますが、いくつかバグがあるためオススメしません(ページに書かれている内容を「切り取り」できてしまう、JAVA製のチャットツール上などでは全く動作しない)。代替手段として(あまりスマートではありませんが)、例えば貼り付けならCtrl+Vキーを押したことにして動作させると良いでしょう(keybd_event APIを使う)。その際メインメニューのショートカットが機能して無限ループに陥らないよう注意。
検索ウインドウやソース、インターネットオプションの表示
 いずれもIOleCommandTargetのExecメソッドで実現できます。第2引数を変えることで、どれを表示するか決めることができます。

[execメソッドの定義]
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/com/htm/oin_oc_33j7.asp
第2引数 動作
HTMLID_FIND (1) 検索
HTMLID_VIEWSOURCE (2) ソース
HTMLID_OPTIONS (3) インターネットオプション
※( )内は定数の値

(例/検索ウインドウの表示)
procedure TMainForm.ShowSearchDialog;
const
 CGID_WebBrowser: TGUID = '{ED016940-BD5B-11cf-BA4E-00C04FD70816}';
var
 CmdTarget : IOleCommandTarget;
 vaIn, vaOut: OleVariant;
 PtrGUID: PGUID;
begin
 with WebBrowser1 do
 begin
  New(PtrGUID);
  try
   PtrGUID^ := CGID_WebBrowser;
   if Assigned(Document) and (not Busy) then
   begin
    if Document.QueryInterface(IOleCommandTarget, CmdTarget) = S_OK then
    begin
     try
      CmdTarget.Exec(PtrGUID, HTMLID_FIND, 0, vaIn, vaOut);
     finally
      CmdTarget._Release;
     end;
    end;
   end;
  finally
   Dispose(PtrGUID);
  end;
 end;
end;
戻る・進むの履歴取得
 MicroSoftはこの履歴のことを「トラベルログ(Travel Log)」と呼んでいるようです。このトラベルログを取得するにはインターフェイスをいくつも取得する必要があります。ここでは例として、トラベルログを取得するメソッドを提示します。

[使用する定数]
インターフェイスのGUID
 IID_ITravelLogEntry: TGUID = '{7EBFDD87-AD18-11d3-A4C5-00C04F72D6B8}';
 IID_IEnumTravelLogEntry: TGUID = '{7EBFDD85-AD18-11d3-A4C5-00C04F72D6B8}';
 IID_ITravelLogStg: TGUID = '{7EBFDD80-AD18-11d3-A4C5-00C04F72D6B8}';
 SID_STravelLogCursor: TGUID = '{7EBFDD80-AD18-11d3-A4C5-00C04F72D6B8}';

インターフェイスで使用する定数
 TLEF_RELATIVE_INCLUDE_CURRENT = $00000001;
 TLEF_RELATIVE_BACK = $00000010;
 TLEF_RELATIVE_FORE = $00000020;
 TLEF_INCLUDE_UNINVOKEABLE = $00000040;
 TLEF_ABSOLUTE = $00000031;

(例)
{*************************************************************************
[引数]
BackOrForward
 0:[戻る]の履歴の取得
 1:[進む]の履歴の取得
NumHistory
 取得する履歴の数を指定
HistoryList
 このTStringListに履歴のタイトル(ページのタイトルと同じ)を格納して返す
*************************************************************************}
procedure TMainForm.GetHistoryList(BackOrForward: Integer; NumHistory: Cardinal;
HistoryList: TStrings);
var
 i: Integer;
 ISP: IServiceProvider;
 FTravelLogStg: ITravelLogStg;
 FEnumTravelLogEntry: IEnumTravelLogEntry;
 FTravelLogEntry: ITravelLogEntry;
 Fetched: Cardinal;
 Count: DWord;
 pTitle: PWideChar;
begin
 //HistoryListの初期化
 if Assigned(HistoryList) then HistoryList.Clear;

 if Failed(Application.QueryInterface(IServiceprovider, ISP)) then exit;

 if Failed(ISP.QueryService(SID_STravelLogCursor, IID_ITravelLogStg, FTravelLogStg))
 then exit;

 if BackOrForward = 0 then //[戻る]
  FTravelLogStg.GetCount(TLEF_RELATIVE_BACK, Count)
 else if BackOrForward = 1 then //[進む]
  FTravelLogStg.GetCount(TLEF_RELATIVE_FORE, Count);

 if Count > NumHistory then Count := NumHistory;

 if BackOrForward = 0 then //[戻る]
  FTravelLogStg.EnumEntries(TLEF_RELATIVE_BACK, FEnumTravelLogEntry)
 else if BackOrForward = 1 then //[進む]
  FTravelLogStg.EnumEntries(TLEF_RELATIVE_FORE, FEnumTravelLogEntry);

 for i := 1 to Count do
 begin
  if FEnumTravelLogEntry.next(1, FTravelLogEntry, Fetched) = S_OK then
  begin
   FTravelLogEntry.GetTitle(pTitle);
   if Assigned(HistoryList) then
    HistoryList.Add(String(pTitle));
  end;
 end;
end;
複数ページに渡って「戻る」「進む」
 複数ページに渡って戻ったり進んだりするには、トラベルログの取得で使ったインターフェイスを利用します。単純にGoBackメソッドやGoForwardメソッドを繰り返し呼ぶより確実な動作が期待できます。(おそらくIEも同様の方法で複数ページ戻ったり進んだりしているものと思われます)

(例)
{*************************************************************************
[引数]
 [戻る]のとき:NavIndex < 0
 [進む]のとき:NavIndex > 0
 例えばNavIndexに3を入れると、3ページ分進む。
*************************************************************************}
procedure TMainForm.NavigateGoBackForwardTo(NavIndex: Integer);
var
 ISP: IServiceProvider;
 FTravelLogStg: ITravelLogStg;
 FTravelLogEntry: ITravelLogEntry;
begin
 if Assigned(WebBrowser1.Document) then
 begin
  if Failed(Application.QueryInterface(IServiceprovider, ISP)) then exit;

  if Failed(ISP.QueryService(
   SID_STravelLogCursor, IID_ITravelLogStg, FTravelLogStg)) then exit;

  if FTravelLogStg.GetRelativeEntry(
   NavIndex, FTravelLogEntry) <> S_OK then exit;

  FTravelLogStg.Travelto(FTravelLogEntry);
 end;
end;