基本的にはWindows Mobileサンプルプログラムとプロジェクト作成方法は変わりませんが、アプリケーションの種類をDLLにしておきましょう。
シンボルは後で、defファイルを使ってエクスポートするので、チェックはいらないです。
これで、DllMainのみのソースが作成されますので、ここに必要なものを追加していきましょう。
Todayプラグインは、シェルからロードされる時に呼び出される関数「InitializeCustomItem」と、設定画面を作る場合は、その設定ダイアログ用のプロシージャ「CustomItemOptionsDlgProc」を、定義して外部から参照できるようにする必要があります。
まだ設定画面を作るかどうかはわかりませんが、一応、両方定義しておきましょう。
まずはソース(DllMainの下にでも)に、以下を追加します。
HWND APIENTRY InitializeCustomItem( TODAYLISTITEM *ptli, HWND hwndParent ) { return NULL; } BOOL APIENTRY CustomItemOptionsDlgProc( HWND hDlg, UINT message, UINT wParam, LONG lParam ) { return FALSE; }
TODAYLISTITEMの定義が、デフォルトでは参照できないので、以下のincludeも追加しておきましょう。
プリコンパイルヘッダ(stdafx.h)の一番下のTODO以下でいいと思います。
// TODO: プログラムに必要な追加ヘッダーをここで参照してください。 #include <todaycmn.h>
InitializeCustomItemは、これから、today画面に表示するWindowを作成して、そのWindowハンドルを返すことになります。
CustomItemOptionsDlgProcは、設定ダイアログを作った場合、そのダイアログのメッセージ処理を書きます。
ダイアログプロシージャは、メッセージを処理した場合にTRUEを返すので、とりあえず今はFALSEで。
次に、追加した関数を、DLLの外から見えるようにしてやる必要があります。
しかも、これらの関数は、外からコールする際の関数の番号(序数)を240番と241番という値に固定してやらなければなりません。
このために、defファイルを使用したエクスポートの設定をしてやります。
まずは、ソリューションエクスプローラの追加したい箇所で右クリックして、追加→新しい項目を選びます。
ソースにでも追加しておきますかね。
で、コードのモジュール定義ファイルを選んで、適当に名前を入力します。
出来たdefファイルには、ライブラリ名だけが記述されていますので、EXPORTS指定をして、先程の2関数を序数指定で追加します。
LIBRARY "todaytest" EXPORTS InitializeCustomItem @240 CustomItemOptionsDlgProc @241
上記手順で作れば、自動的にプロジェクトのプロパティに、モジュール定義ファイルとして設定されていると思いますので、確認してみてください。
手動でファイルを追加した場合は、モジュール定義ファイルの項目に、追加したファイルを設定してやる必要があります。
この状態でビルドしてやれば、dllとexp(エクスポートファイル)と、インポートライブラリ(lib)ファイルが生成されるはずです。
とはいえ、シェルはダイナミックにdllをロードして、序数指定で関数をコールするので、このインポートライブラリを使う人はいませんが。
InitializeCustomItemで、表示するWindowを作成するために、まずはウィンドウクラスを登録する必要があります。
登録は普通のWin32プログラムとほとんど変わらないのですが、DLLでの登録なので、シェルがDLLを呼び出してる間だけ、ウィンドウクラスが有効である必要があります。
InitializeCustomItem内でウィンドウクラスの登録を行なおうとすると、初回はいいのですが、何らかのタイミングで一旦todayプラグインのWindowをつくり直す契機があった場合、シェルはInitializeCustomItemで作ったWindowを一旦Destroyして、再度InitializeCustomItemをコールし、ウィンドウの再作成をすることがあるという作りらしいので、2回目以降にコールされたとき、既にウィンドウクラスが登録されている状態になってしまいます。
これを防ぐために、DllMainで、プロセスにアタッチされた時に、ウィンドウクラスの登録やリソースをロードし、プロセスからデタッチされた時に、これらを解放するという仕組みが、正攻法のようです。
ウィンドウクラスに登録する名前とかは、定数でもいいんですけど、リソースのストリングテーブルとかに持ってたほうがいいかもしれませんね。
どうせ後で、他のことにもリソースは使いそうなので、リソースファイルを追加しておきますか。
というわけで、先ほどと同じように、リソースに追加します。
で、リソースのリソースファイルを選んで、適当に名前を入力します。
リソースを追加すると、リソースのIDが記述されるresource.hがプロジェクトに追加されます。
使用するソースでは、こいつをincludeしてやる必要がありますので、DllMainの前の、他のincludeの後に追加しておきます。
#include "resource.h"
ちなみに、DllMainの前にincludeされているwindows.hとcommctrl.hですが、この記述はstdafx.hの方に移してしまっていいと思います。
というか、windows.hは既にstdafx.h内に記述されているので、消してしまってもいいです。
基本的に変更されることがないヘッダファイルは、プリコンパイルヘッダ(stdafx.h)中に記述して、そうでないヘッダファイルは、実際に使用するファイルがincludeするようにするといいでしょう。
次に、リソースファイルを作ったので、ここにストリングテーブルを追加します。
右クリックでリソースの追加を選んで……。
String Tableを選択して、新規作成。
作成できたら、ウィンドウクラス名を登録するIDと文字列を入力します。
ストリングテーブルの他に、ウィンドウクラスには、そのウィンドウクラスを指定して作成したWindowのメッセージを処理する、ウィンドウプロシージャが必要です。
とりあえず、何もしない(デフォルトの処理のみを行う)ウィンドウプロシージャも作っておきましょう。
LRESULT CALLBACK WndProc( HWND hwnd, UINT uiMsg, WPARAM wParam, LPARAM lParam ) { return ::DefWindowProc( hwnd, uiMsg, wParam, lParam ); }
これで、下準備は出来ました。
適当なクラスでも作って、インスタンスハンドルを保持したり、ストリングテーブルから文字列を読み込む処理のラッパー等の処理を作成することにします。
というわけで、出来たクラスと使用側のDllMainがこんな感じでしょうか。
まだ動かして試すところまでいっていないので、ちゃんと動くかどうかわかりませんが……。
使用APIはLoadStringとRegisterClassとUnregisterClassです。
LoadStringは、WindowsMobile版では、文字列バッファにNULLを設定すると、戻り値としてリソース直のポインタが返ってくるらしいですが、単語の境界がよくわからなかったので、普通の使い方してます。
RegisterClassは、通常のWin32プログラムとは違って、結構意味をなさないメンバがいそうです。
まぁ、必要なものだけ設定という感じで。
InitializeCustomItemが呼ばれたときにWindowを作成しますが、まずは引数を見てみますか。
HWND APIENTRY InitializeCustomItem( TODAYLISTITEM *ptli, HWND hwndParent );
hwndParentの方はまぁ、親Windowのハンドルということで、Today画面自体かなんかのウィンドウハンドルが渡されてくるわけですね。
自分が作成するWindowは、これの子Windowとして作成すると。
リモートスパイを立ち上げて、Window構成を見てみると、DesktopWindowの下に2つくらいかまして、TodayプラグインのWindowがあるように見えますね。
しかし、Todayプラグインは時計を含めて4つ表示しているはずなんだけど、時計らしきものがないですね……。
Todayプラグインの情報が入っているレジストリの場所「HKEY_LOCAL_MACHINE\Software\Microsoft\Today\Items」を見ても、時計らしいものがないので、別扱いなのかな。
もう一つの引数は、TODAYLISTITEMという構造体のポインタですね。
この構造体は、以下のようなメンバとなっているらしいですが、InitializeCustomItemに渡される時には、szName、tlit、dwOrder、fEnabled、fOptions、grfFlagsが埋められていて、fEnabledの値がFALSEの時には、NULLを返さないとならないらしいです。
埋められているってのは、基本的にはレジストリに書かれている値が入ってくるという意味っぽいですね。
レジストリ内容については、また後日。
typedef struct _TODAYLISTITEM { TCHAR szName[MAX_ITEMNAME]; TODAYLISTITEMTYPE tlit; DWORD dwOrder; DWORD cyp; BOOL fEnabled; BOOL fOptions; DWORD grfFlags; TCHAR szDLLPath[MAX_PATH]; HINSTANCE hinstDLL; HWND hwndCustom; BOOL fSizeOnDraw; BYTE * prgbCachedData; DWORD cbCachedData; } TODAYLISTITEMTYPE;
fEnabledがFALSEだったらNULLを返し、そうでない場合は、適当にウィンドウを管理するクラスでも作って、ウィンドウを作成しましょう。
ウィンドウ作成は、以下のような感じでしょうか。
m_hwnd = ::CreateWindow( lpCApp->getClassName(), lpCApp->getClassName(), WS_VISIBLE | WS_CHILD, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, hwndParent, NULL, lpCApp->getInstance(), ( LPVOID )this );
ウィンドウタイトルはとりあえずクラス名と同じにしておくとして、シェルのTodayウィンドウの子ウィンドウとして、それぞれのアイテムウィンドウを作成するため、WS_CHILDのスタイル指定となっています。
位置とサイズが全部CW_USEDEFAULTになっていますが、ウィンドウ作成後、位置、サイズはシェルによって調整されるようなので、とりあえずはこれで問題ないようです。
(高さだけは、後で書くWM_TODAYCUSTOM_QUERYREFRESHCACHEメッセージ処理時に設定するようですが)
あとは、パラメータとしてウィンドウ管理クラスのポインタを渡しているので、WM_CREATEでSetWindowLong(GWL_USERDATA)を使用してウィンドウにクラスを関連付けておき、WM_DESTROY時にクラスをdeleteすればいいでしょうかね。
ウィンドウの管理クラスポインタをグローバルにすると、上位のシェル動作次第ですが、WM_DESTROY前に再度InitializeCustomItemが呼ばれたりすると困るので、ウィンドウに紐付けておくのがよさそうです。
デフォルトのウィンドウメッセージは、<windowsx.h>をincludeしておいて、メッセージをハンドルするのが見やすくていい感じです
。
stdafx.hの、windows.hのincludeの後にでも追加しておくといいでしょう。
あらかじめ、windowsx.h内の定義に従って、WM_CREATEとWM_DESTROY用の関数を用意しておいて……。
BOOL Today_OnCreate( HWND hwnd, LPCREATESTRUCT lpCreateStruct ); // WM_CREATE処理 void Today_OnDestroy( HWND hwnd ); // WM_DESTROY処理
ウィンドウプロシージャの記述を、以下のように。
LRESULT CALLBACK WndProc( HWND hwnd, UINT uiMsg, WPARAM wParam, LPARAM lParam ) { switch( uiMsg ){ HANDLE_MSG( hwnd, WM_CREATE, Today_OnCreate ); HANDLE_MSG( hwnd, WM_DESTROY, Today_OnDestroy ); } return ::DefWindowProc( hwnd, uiMsg, wParam, lParam ); }
実際の処理は、これでいいかな……?
BOOL Today_OnCreate( HWND hwnd, LPCREATESTRUCT lpCreateStruct ) { LPCTodayWnd lpCTodayWnd = ( LPCTodayWnd )lpCreateStruct->lpCreateParams; // Todayウィンドウクラス if( NULL == lpCTodayWnd ){ return FALSE; } if( !::SetWindowLong( hwnd, GWL_USERDATA, ( LONG )lpCTodayWnd )){ return FALSE; } return TRUE; } void Today_OnDestroy( HWND hwnd ) { LPCTodayWnd lpCTodayWnd = NULL; // Todayウィンドウクラス lpCTodayWnd = ( LPCTodayWnd )::GetWindowLong( hwnd, GWL_USERDATA ); if( NULL != lpCTodayWnd ){ ::SetWindowLong( hwnd, GWL_USERDATA, NULL ); delete lpCTodayWnd; lpCTodayWnd = NULL; } }
さて、作ったウィンドウを表示させるには、あと一つやらなければならないことがあります。
上でちょっと書きましたが、このアイテムを表示する高さを、WM_TODAYCUSTOM_QUERYREFRESHCACHEというメッセージを処理することによって、シェルに教えてやらなければなりません。
WM_TODAYCUSTOM_QUERYREFRESHCACHE wParam = (WPARAM) lptli; lParam = (LPARAM) res;
このメッセージは、WPARAMとして、TODAYLISTITEM構造体のポインタを受け取ります。
InitializeCustomItemの時に出てきたやつですね。
このメンバの中に、cypというものがあり、これはアイテムのピクセルでの高さを意味しています。
この値の初期値は0ですので、0であればアイテムの高さを変えてやり、高さを変更した場合は、TRUEを返すようにします。
初期値でない場合にも、アイテムの高さを変更する必要があれば、値を変更してTRUEを返すようにします。
高さを変更しない場合には、FALSEを返せばいいようです。
あと、WindowsMobileSDKのTodayプラグインサンプルを見ると、シェルAPIの準備が出来ているかどうかというのを、WaitForSingleObjectでチェックしているようです。
これについては、情報が少なく、どういう意図で行われているかというのがよくわからなかったのですが、TRUEを返すとシェルのAPIを呼んで何かをするから、シェルの準備が出来ていない時にTRUEを返してしまうと、待たされたり正しく処理が行われなかったりするかもというぐらいの推測しか出来ないです……。
では実際のソース。
まず、WM_TODAYCUSTOM_QUERYREFRESHCACHEメッセージをハンドリングできるように、stdafx.hにでも、マクロを追加しておきましょう。
このあたりのメッセージは、windowsx.hでは定義されていないので。
/* BOOL Cls_OnTCQueryRefreshCache(HWND hwnd, TODAYLISTITEM* lptli) */ #define HANDLE_WM_TODAYCUSTOM_QUERYREFRESHCACHE(hwnd, wParam, lParam, fn) \ ((fn)((hwnd), (TODAYLISTITEM*)(wParam))) #define FORWARD_WM_TODAYCUSTOM_QUERYREFRESHCACHE(hwnd, lptli, fn) \ (BOOL)(DWORD)(fn)((hwnd), WM_TODAYCUSTOM_QUERYREFRESHCACHE, (WPARAM)(TODAYLISTITEM*)(lptli), 0L)
ソースの方にプロトタイプも追加して……。
BOOL Today_OnTCQueryRefreshCache( HWND hwnd, TODAYLISTITEM* lptli ); // WM_TODAYCUSTOM_QUERYREFRESHCACHE処理
ウィンドウプロシージャにメッセージのハンドリングを追加して。
LRESULT CALLBACK WndProc( HWND hwnd, UINT uiMsg, WPARAM wParam, LPARAM lParam ) { switch( uiMsg ){ HANDLE_MSG( hwnd, WM_CREATE, Today_OnCreate ); HANDLE_MSG( hwnd, WM_DESTROY, Today_OnDestroy ); HANDLE_MSG( hwnd, WM_TODAYCUSTOM_QUERYREFRESHCACHE, Today_OnTCQueryRefreshCache ); } return ::DefWindowProc( hwnd, uiMsg, wParam, lParam ); }
実際の処理は以下のように。
BOOL Today_OnTCQueryRefreshCache( HWND hwnd, TODAYLISTITEM* lptli ) { if(( NULL == lptli ) || ( WAIT_TIMEOUT == ::WaitForSingleObject( SHELL_API_READY_EVENT, 0 ))){ return FALSE; } if( 0 == lptli->cyp ){ lptli->cyp = 40; return TRUE; } return FALSE; }
今回は暫定的に、アイテムの高さを40ピクセルに指定しています。
ここまでのソースは、配置の説明の後につけておきます。
さて、作ったソフトは、実行する前にインストールの必要があります。
なのでまず、インストール用のCABファイルを作成しましょう。
一度インストールしてしまった後は、基本的にはdllを差し替えるだけで、デバッグが出来ます。
というわけで、ソリューションに新規プロジェクトを追加します。
「セットアップと配置」から、「スマートデバイスCABプロジェクト」を選択します。
プロジェクトを選択した状態で、プロパティウィンドウを開き、ManufacturerとProductNameを設定します。
プロジェクトの右クリックメニューからのプロパティも開いて、出力ファイル名も必要があれば変更しておきましょう。
Debug構成だけじゃなく、Release構成も、変更が必要であれば忘れずに。
そして、作成したDLLを、CABに含めるように設定します。
プロジェクトの右クリックメニューから、「追加」の「プロジェクト出力」を選択します。
DLL出力プロジェクトの、プライマリ出力を選択します。
これで、出力されたDLLがCABに含まれます。
プライマリ出力は、「アプリケーションフォルダ」内に配置されたと思いますが、「アプリケーションフォルダ」は「%InstallDir%」という関連付けがされており、これは「\Program Files\製品名(ProductName)」の場所を指しています。
他に、「プログラムファイルフォルダ」というのがファイルシステム上に表示されていると思いますが、これは「%CE1%」という関連付けがされており、「\Program Files」の場所を指しています。
他に、マクロ文字列は以下のような場所を指しているようですので、覚えておくと後々役に立つかもしれません。
%CE1% \Program Files %CE2% \Windows %CE4% \Windows\StartUp %CE5% \My Documents %CE8% \Program Files\Games %CE11% \Windows\Start Menu\Programs %CE14% \Windows\Start Menu\Programs\Games %CE15% \Windows\Fonts %CE17% \Windows\Start Menu
そうそう、このままではスマートデバイスCABプロジェクトのビルドが行われませんので、ソリューションから「構成マネージャ」を選択し、ビルドのチェックを入れておきましょう。
こちらも、Debug構成だけじゃなく、Release構成も忘れずに。
最後に、インストール時に登録するレジストリの設定をしましょう。
プロジェクトから、レジストリを開きます。
HKEY_LOCAL_MACHINE以下にキーを追加していき、「HKEY_LOCAL_MACHINE\Software\Microsoft\Today\Items\製品名(ProductName)」まで作りましょう。
そしてこの中に、以下の値を設定する必要があります。
Type | DWORD | このアイテムのタイプを指定します |
Enabled | DWORD | このアイテムの有効/無効を指定します |
Options | DWORD | このアイテムが設定ダイアログを持つかどうかを指定します |
DLL | 文字列 | DLLが格納されている位置を指定します |
Selectability | DWORD | このアイテムの選択方式を指定します |
Typeは通常、4(カスタムプラグイン)を指定しておけばよいです。
0はオーナー情報、1は予定、2はメール、3は仕事を表すようですが、この値を変更する事により、どういう影響があるのかはよくわかってないです……。
Enabledは、インストール直後に表示させたくなければ0にしておくといいでしょう。
ユーザーが設定からチェックを入れれば表示されるようになります。
Optionsは、設定ダイアログを持つ場合は1にする必要があります。
今回はまだ、CustomItemOptionsDlgProcの中身を実装していないので、0にしておきましょう。
DLLは、そのまんま、DLLのパスを指定します。
今回の例では、「%InstallDir%\todaytest.dll」を指定すれば良いです。
Selectabilityは、そのアイテムを選択できるかどうか、選択できるようにした場合、どういう動作をさせるかという設定です。
このあたりの動作はまだ決めていないので、今回は0を指定しておきましょう。
というわけで、ここまでできたらビルドしてみましょう。
CABファイルが作成されているはずです。
出来ていたら、ActiveSync経由で実機の適当な場所に置き、実機のエクスプローラから開いてインストールしてみましょう。
インストールが終了したら、ActiveSyncのエクスプローラからでも、指定した場所にちゃんとインストールされているか確認しておきましょう。
実機のエクスプローラだと、デフォではdllが表示されない上に、拡張子もでてなくて、ちゃんと入ってるのかどうかよくわからなかったです……。
確認できたら、レジストリにも正しく登録されているか、リモートレジストリエディタで見てみましょう。
ここまでできたら、実際にプラグインを動かせますが、毎回ソースを修正するたびにいちいちCABファイルを作成してインストールを繰り返すのは、とても手間です。
なので、インストール先のdllを、VCから直接書き換えられるようにしましょう。
DLLのプロジェクトのプロパティを開き、構成プロパティの「配置」を選択します。
そして、配置デバイスを「Windows Mobile 6 Professional Device」にします。
プログラムをビルド後、メニューからソリューションの配置(プロジェクトの配置でもいいと思います)を行うと、ActiveSync経由でdllが上書きされます。
(おそらく、使用中だとダメだと思うので、書き換え時はTodayプラグインのチェックを外して無効にしておいてくださいね)
以上で、配置については終了です。
ここまでのソリューション、プロジェクトとソースは、こんな感じで。
ウィンドウクラスの時のソースとはちょっと変わってるところもあります。
これを実際に動かしてみると、表示は下図のようになります。
黒く表示されている部分が、今回作成したアイテムになります。
黒いのは、ウィンドウクラスの設定時に、GetStockObject( BLACK_BRUSH )で、背景を黒ブラシでクリアするようにしているためです。
WM_ERASEBKGNDでクリアされた背景が表示されている状態です。
今後は、このメッセージを処理して、壁紙を透かして表示する等の対応を行っていくことになります。
基本的には、上記の配置を使って、以下のような順番でデバッグをしていきます。
プラグイン無効状態→配置→プロセス(shell32.exe)にアタッチ→プラグインを有効に→デバッグ→プラグインを無効に