2.3.3 インターフェイスの実装
はじめに
最初に予告しましたとおり、インターフェイスの実装は Delphiにて行います。しかし、インターフェイスの実装はどの言語を使用しても似たような形になるはずですので別の言語で開発しておられる方にもそれなりに参考になるものと思います。
順番としましては、まずこの手のインプロセスActiveXサーバーではお決まりの形となっていますライブラリソースを解説し、次にコンテキストメニューハンドラの核であるIContextMenuインターフェイスの実装を解説します。
なお、コードに関する注釈は最低限のものとなっておりますので、不明な点は前章の各インターフェイスの解説を参照するなり関連書籍・資料を参照するなりして下さい。
2.3.3.1 ライブラリソース
ライブラリソースはきわめて単純なものです。インプロセスActiveXサーバーを開発する場合は常にいわゆる「お決まりの型」にするだけでよいです。
注意しなければならないことは、下のコード中にも見られるようにインプロセスActiveXサーバーは必ず
1. DllGetClassObject
2. DllCanUnloadNow
3. DllRegisterServer
4. DllUnregisterServer
の4つの関数をエクスポートしなくてはならない、という点だけです。
以下にライブラリソースを示します。
library EZSHLEXT;
uses
SysUtils,
Classes,
ComServ,
EZShlObj in 'EZShlObj.pas';
exports
{Inprocess ActiveX Server がエクスポートしなければならないお約束の関数}
DllGetClassObject,
DllCanUnloadNow,
DllRegisterServer,
DllUnregisterServer;
begin
end.
|
2.3.3.2 IContextMenuインターフェイスの実装
コンテキストメニューハンドラはIContextMenuインターフェイスを実装することで実現されます。
ここで、そのIContextMenuを実装するクラスはTComObjectから派生させたクラスで、クラス名をTEZShellObjectクラスとすることとします。
今回の例に限らず、拡張シェルオブジェクトはTComObjectクラスから派生させるのが便利です。TComObjectはIUnknownインターフェイス、ISupportErrorInfoインターフェイスを既に実装しており、クラスファクトリによるインスタンス化・アグリゲーション・OLE例外処理・デュアルインターフェースで使うsafecall呼び出し規約、をサポートしています。
前章までで述べたように、コンテキストメニューハンドラはその初期化をIContextMenuを通じては行いません。初期化はIShellExtInitインターフェイスを通じて行われます。
したがって、最終的にはTEZShellObjectクラスは「TComObject、IContextMenu、IShellExtInit」から継承しなくてはならない、ということが分かります。(ObjectPASCALはクラスの多重継承を認めてはいませんが、インターフェイスの多重継承は認めている点に注意)
まずは最初にTEZShellObjectクラスの宣言部を示し、追加されたプライベートフィールドに関する解説をすることにします。
TEZShellObject = class(TComObject,IShellExtInit,IContextMenu)
private
Files: TStrings; //選択されたファイルを格納する
DdeClientConv: TDdeClientConv; //DDE通信用
procedure InitFields; //各内部フィールドを初期化する
procedure SendFileNames(WindowClass, CommandParams, DdeServerName: string); //EZSECRETを起動し、ファイル名を送る
public
destructor Destroy; override; //各内部フィールドを掃除する
//IShellExtInit
function Initialize(pidlFolder: PItemIDList; lpdobj: IDataObject;
hKeyProgID: HKEY): HResult; stdcall;
//IContextMenu
function QueryContextMenu(Menu: HMENU;
indexMenu, idCmdFirst, idCmdLast, uFlags: UINT): HResult; stdcall;
function InvokeCommand(var lpici: TCMInvokeCommandInfo): HResult; stdcall;
function GetCommandString(idCmd, uType: UINT; pwReserved: PUINT;
pszName: LPSTR; cchMax: UINT): HResult; stdcall;
end;
|
内部フィールド | 用 途 |
Files | ユーザーによって選択されたファイル名を格納するTStringsオブジェクト |
DdeClientConv | EZSECRET.EXEにファイル名を渡すのに使用されるTDdeClientConvオブジェクト 注意:ここではDDE通信を用いてファイル名を渡すことにしたが、別にDDE通信でなくても要はファイル名をEZSECRETに渡せれば他の手段でも何ら問題は無い。 |
次に、追加されたプライベートメソッドについて、実装部とその解説を示します。
念のためここで断っておきますが、別に拡張シェルオブジェクトを作成する時には常に上のTEZShellObjectのようなプライベートメソッドなりプライベートフィールドなりを追加する必要があるというわけではありません。
ただし、IContextMenuインターフェイスやIShellPropSheetExインターフェイスのような別のインターフェイスを通じて初期化されるタイプの拡張シェルオブジェクトは、上でのFilesフィールドのような初期化用のフィールドを持つ必要が結局は出てくることになりますが...
{各内部フィールドを初期化する
~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
procedure TEZShellObject.InitFields;
var
i: integer;
rt: TRect;
tmpBmp: TBitmap;
begin
Files := TStringList.Create;
DdeClientConv := TDdeClientConv.Create( Nil);
with DdeClientConv do
begin
ConnectMode := ddeAutomatic;
FormatChars := FALSE;
end;
end;
{EZSECRETを起動し、ファイル名を送る
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[引数]
WindowClass:ファイル名の送り先ウィンドウクラス(例:TEncodeForm)
CommandParams:EZSECRETを起動する際に使用するコマンドパラメータ(例:/e)
DdeServerName:ファイル名を送る際のリンク先DDEサーバー名(例:DdeEncdServer)
}
procedure TEZShellObject.SendFileNames(WindowClass, CommandParams, DdeServerName: string);
var
hEXE: UINT;
begin
if FindWindow( PChar(WindowClass), Nil) = 0 then
begin
hEXE := WinExec( PChar( 'EZSECRET.EXE ' + CommandParams), SW_SHOW);
if hEXE < 32 then
begin
MessageDlg( 'Easy Secret を起動できませんでした', mtWarning, [mbYES], 0);
Exit;
end;
end;
while FindWindow( PChar(WindowClass), Nil) = 0 do Sleep(50); //Windowが生成されるまで待つ
if not DdeClientConv.SetLink('EZSECRET', DdeServerName) then Exit;
if not DdeClientConv.ExecuteMacroLines( Files, False) then Exit;
end;
|
メソッド | 解 説 |
InitFields |
Files、DdeClientConvオブジェクトを生成する。 このメソッドをどこで呼出せばよいかが重要である。Createをオーバーライドしてその中で呼出しても意味がない。なぜなら、クラスファクトリから生成された場合、Createは経由されないからである。 したがって、ここではこのメソッドをIShellExtInit.Initialize中で呼出すことにした。そうすればこのメソッドはコンテキストメニューハンドラが初期化される際必ず呼出されるからである。 |
SendFileNames |
EZSECRET.EXEにFilesに格納されたファイル名を渡す。 ここではDDE通信を使用しているが、上でも述べたように、別の手段を用いても何ら問題は無い。例えば、ウィンドウメッセージを送りメモリマップドファイルを経由してファイル名を送るのも良いだろう。 各引数についてはコード中のコメント参照のこと。 |
以下に、IShellExtInitインターフェイス、IContextMenuインターフェイスに関して、各メソッドの実装部とその解説をまとめて示します。
これでTEZShellObjectは完成です。後は、ユニットのInitialization部とFinalization部のみとなります。最初に断りましたとおり、EZSECRET.EXEの方は具体的なファイルの暗号化・復号化をうけもっているだけなのでここでは割愛します。
なお、Destroyメソッドの実装部を載せてませんが、これは単にInitFieldsメソッドで生成したオブジェクトを破棄しているだけなので特に掲載する必要もないものと思いあえてそうしています。万が一わからないという方がおられましたらメールでも下さい。
//IShellExtInit
{拡張シェルの初期化
~~~~~~~~~~~~~~~~~~
選択されたファイルをFilesに格納する。
[引数]
pidlFolder:拡張コンテキストメニュー、拡張プロパティシートに対しては親フォルダ、
非デフォルトドラッグアンドドロップに対してはターゲットフォルダを示
すTItemIDList構造体へのポインタ
lpdobj:1つ以上の選択された(ドロップされた)項目を返すIDataObjectオブジェクト
へのポインタ
hKeyProgID:拡張コンテキストメニュー、拡張プロパティシートに対してはフォーカス
のあるファイル項目のレジストリキー(ファイルクラス)、非デフォルト
ドラッグアンドドロップに対してはターゲットフォルダのレジストリキー
(ファイルクラス)を示す
[返値]
関数が成功すれば、NOERRORを返します。失敗した場合はOLEで定義されたエラーコード
を返します。
}
function TEZShellObject.Initialize(pidlFolder: PItemIDList; lpdobj: IDataObject;
hKeyProgID: HKEY): HResult;
var
StgMedium: TStgMedium;
FormatEtc: TFormatEtc;
i, n: integer;
Buffer: array[0..1024] of char;
begin
Result := E_FAIL;
if lpdObj = Nil then Exit; //引数チェック
InitFields; //内部フィールドの初期化
{データ形式の指定}
with FormatEtc do
begin
cfFormat := CF_HDROP; //ファイルリスト
ptd := Nil;
dwAspect := DVASPECT_CONTENT; //データはコンテナ内部への埋め込み
lindex := -1; //全てのデータを対象
tymed := TYMED_HGLOBAL; //媒体にグローバルメモリハンドル
end;
{選択されたファイル名の取得}
Result := lpdObj.GetData( FormatEtc, StgMedium);
if Failed( Result) then Exit;
n := DragQueryFile( StgMedium.hGlobal, $FFFFFFFF, Nil, 0); //ファイル数の取得
for i := 0 to n - 1 do
begin
ZeroMemory( @Buffer, SizeOf(Buffer));
DragQueryFile( StgMedium.hGlobal, i, Buffer, SizeOf(Buffer));
if FileExists( string( Buffer)) then Files.Add( string( Buffer));
end;
{Resultの設定 & StgMediumの開放}
if n > 0 then
Result := NOERROR;
ReleaseStgMedium( StgMedium);
end;
//IContextMenu
{コンテキストメニューを追加する
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[引数]
Menu:メニューハンドル
indexMenu:最初のメニュー項目の挿入位置(0ベース)
idCmdFirst:挿入したメニュー項目IDの下限
idCmdLast:挿入したメニュー項目IDの上限
uFlags:起動された状況を示すフラグ(詳細はSDK参照)
[返値]
関数が成功すれば、HRESULTを返します。ただし、そのCODEメンバ(low word)は追加
したメニュー項目のIDの最大値(idCmdFirst - 1)からのオフセットを表しています。
}
function TEZShellObject.QueryContextMenu(Menu: HMENU;
indexMenu, idCmdFirst, idCmdLast, uFlags: UINT): HResult;
var
i: integer;
idEncd, idDecd, idProp: UINT;
begin
Result := 0;
if (uFlags and CMF_NORMAL) <> CMF_NORMAL then Exit;
idEncd := idCmdFirst;
idDecd := idCmdFirst + 1;
idProp := idCmdFirst + 2;
InsertMenu( Menu, indexMenu, MF_SEPARATOR or MF_BYPOSITION, 0, Nil);
InsertMenu( Menu, indexMenu + 1, MF_STRING or MF_BYPOSITION, idEncd, 'ファイルの暗号化...');
Result := Result + 1; //increment Result
for i := 0 to Files.Count - 1 do
if LowerCase( ExtractFileExt( Files.Strings[i])) <> '.sct' then Exit;
InsertMenu( Menu, indexMenu + 2, MF_STRING or MF_BYPOSITION, idDecd, 'ファイルの復号化...');
InsertMenu( Menu, indexMenu + 3, MF_STRING or MF_BYPOSITION, idProp, '暗号化時情報...');
Result := Result + 2; //increment Result
end;
{追加したメニューを実行する
~~~~~~~~~~~~~~~~~~~~~~~~~~
QueryContextMenuで追加されたメニューがクリックされた時に呼出されるので、メニ
ューを実行する。
[引数]
lpici:選択されたコマンドに関する情報を格納しているTCMInvokeCommandInfo構造体
[返値]
関数が成功すれば、NOERRORを返します。失敗した場合はOLEで定義されたエラーコー
ドを返します
}
function TEZShellObject.InvokeCommand(var lpici: TCMInvokeCommandInfo): HResult;
begin
Result := E_FAIL;
{アプリケーション呼出しの場合はEXit}
if HiWord( Integer( lpici.lpVerb)) <> 0 then Exit;
{メニューIDのオフセット値は 0(暗号化)、1(復元)、2(暗号化時情報)のみ}
if LoWord( Integer( lpici.lpVerb)) > 2 then
begin
Result := E_INVALIDARG;
Exit;
end;
if LoWord( Integer( lpici.lpVerb)) = 0 then
SendFileNames( 'TEncodeForm', '/e', 'DdeEncdServer') //暗号化
else if LoWord( Integer( lpici.lpVerb)) = 1 then
SendFileNames( 'TDecodeForm', '/d', 'DdeDecdServer') //復元
else
SendFileNames( 'TProptyForm', '/p', 'DdePropServer'); //暗号化時情報
Result := NOERROR;
end;
{Explorer のステータスバーに表示する文字列を指定する
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[引数]
idCmd:選択されているメニュー項目IDのidCmdFirstからのオフセット
uType:返値の種類を指定するパラメータ(詳細は SDK参照)
pwReserved:予約済
pszName:返値を格納するバッファーのアドレス
cchMax:上記バッファのサイズ
[返値]
関数が成功すれば、NOERRORを返します。失敗した場合はOLEで定義されたエラーコー
ドを返します。
}
function TEZShellObject.GetCommandString(idCmd, uType: UINT; pwReserved: PUINT;
pszName: LPSTR; cchMax: UINT): HResult;
begin
Result := E_INVALIDARG;
if idCmd > 1 then Exit;
if idCmd = 0 then
begin
{暗号化}
StrLCopy( pszName, 'ファイルを暗号化します', cchMax - 1);
end
else if idCmd = 1 then
begin
{復元}
StrLCopy( pszName, '暗号化ファイルを復元します', cchMax - 1);
end
else
begin
{暗号化時情報}
StrLCopy( pszName, '暗号化ファイルの情報を表示します', cchMax - 1);
end;
Result := NOERROR;
end;
|
メソッド | 解 説 |
Initialize |
ここでコンテキストメニューハンドラの初期化を行う。 最も重要な作業は、ユーザーに選択されたファイルをFilesオブジェクトに記録することである。 シェルから渡されたIDataObjectオブジェクトの取り扱いがややこしいが、選択ファイル名を取得したいだけであれば常に上のコードに掲載の手順でファイル名を取得すればよい。 IDataObjectオブジェクトから得たTStgMedium構造体の開放タイミングは拡張シェルオブジェクトにしかわからないので、拡張シェルオブジェクトの実装部内で必ずその開放をしなくてはならない点に注意。勿論、そういう意味でInitializeメソッド内で開放しなければならないわけではない。 引数に関しては上記コード中のコメントを参照のこと。 |
QueryContextMenu |
登録した拡張子のファイルをユーザーが選択した上で右クリックしたとき、つまり選択されたファイルに関するコンテキストメニューをシェルが表示しなくてはならないとき、シェルがこのメソッドを呼出し何か追加するべきメニューがあるかどうかを問合せてくる。 メニューを追加する必要がある場合は上のコードのようにInsertMenuしてやればよい。ただし、どこに追加してもよいというわけではない。引数のindexMenu、idCmdFirstにしたがったものにする必要がある。 引数に関しては上記コード中のコメントを参照のこと。 |
InvokeCommand |
QueryContextMenuで追加したメニューをユーザーが選択したときに呼出される。 上のコードのように選択されたメニューのメニューIDオフセット値を調べてその後の処理を行うとよい。 引数に関しては上記コード中のコメント及び前章での解説を参照のこと。 |
GetCommandString |
ユーザーがファイルを右クリックしてでたコンテキストメニューの上をマウスカーソルが移動している最中、ビューの左下にそのマウスカーソルの下にあるメニューの動作概要が表示されるが、QueryContextMenuで追加したメニューに関するその文字列をシェルが問合せてくる際に呼出される。 これもメニューIDオフセット値を調べて処理を行うとよい。 引数に関しては上記コード中のコメントを参照のこと。 |
最後にTEZShellObjectを宣言・定義しているユニットのInitialization部及びFinalization部を掲載します。
Initialization部では、COMライブラリの初期化とクラスファクトリの生成を行っています。
このように拡張シェルオブジェクトは「お決まりの型」としてInitialization部でクラスファクトリを生成しておくのがよいでしょう。
クラスファクトリをサーバーのロード時に自動的に使用可能にするためには、クラスファクトリはその関連付けられたサーバーを含むユニットの initializationセクションでインスタンス化しておかなければならないためです。
Finalization部では、COMライブラリの後始末をしています。
実際にEZSHLEXT.DLLをコンテキストメニューハンドラに登録して使用してみると、シェルの拡張はユーザーにアピールするところが意外に大きいことに気付きます。研究すればいろいろと面白いことが出来るようになると思います。
正直言うと僕自身まだシェルをいじった経験はほとんどありません。(コンテキストメニューハンドラとコピーフックハンドラとプロパティシートハンドラのみ経験...)
皆さんの開発事例等から何か面白い応用例等がありましたら教えて下さい。(最後はお願いになってしまいましたね ^-^;)
initialization
CoInitialize( Nil);
TComObjectFactory.Create( ComServer
,TEZShellObject
,CLSID_EZShellObject
,''
,'Easy Secret Ver. 1.00'
,ciMultiInstance);
finalization
CoUninitialize;
end.
|
|