EternalWindows
コントロールパネル / リソースの取得

今回は、cplファイルをアドレス空間にロードして、 CPlAppletにCPLメッセージを送りたいと思います。 このような作業は、explorer.exeやrundll32.exeが行っていることであり、 CPlAppletの内部構造を把握しているならば、通常のアプリケーションも行うことができます。 具体的にはどうするかですが、これはCPlAppletを呼ばれる側のことを 考慮すれば察しがつくでしょう。

前々節ではCPlAppletにコードを書きましたが、 そのときには、最初にCPL_INITからCPL_INQUIREまでが送られ、 アイコンをクリックしたらCPL_DBLCLKが送られるなどということを 考慮してコードを書きました。 実は、CPlAppletを呼び出す側となってもこの理屈は変わらないのです。 つまり、CPL_INITからCPL_INQUIREまでのメッセージをCPlAppletに送り、 アイコンをクリックしたらCPL_DBLCLKを送ればよいのです。

今回のプログラムは、WM_CREATEにてCPL_INITからCPL_INQUIREまでのメッセージを CplAppletに送り、CPLINFO構造体を初期化します。 WM_PAINTでは、この構造体の各リソースの識別子から実際にリソースを取得し、 描画を行っています。 また、ウインドウでマウスの左ボタンが押されたときには、 それをアイコンのクリックと仮定し、CPL_DBLCLKを送ります。

#include <windows.h>
#include <cpl.h>

typedef LONG (APIENTRY *LPFNCPLAPPLET)(HWND, UINT, LPARAM, LPARAM);

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR      szAppName[] = TEXT("sample");
	HWND       hwnd;
	MSG        msg;
	WNDCLASSEX wc;

	wc.cbSize        = sizeof(WNDCLASSEX);
	wc.style         = 0;
	wc.lpfnWndProc   = WindowProc;
	wc.cbClsExtra    = 0;
	wc.cbWndExtra    = 0;
	wc.hInstance     = hinst;
	wc.hIcon         = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	wc.hCursor       = (HCURSOR)LoadImage(NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0, LR_SHARED);
	wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	wc.lpszMenuName  = NULL;
	wc.lpszClassName = szAppName;
	wc.hIconSm       = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	
	if (RegisterClassEx(&wc) == 0)
		return 0;

	hwnd = CreateWindowEx(0, szAppName, szAppName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hinst, NULL);
	if (hwnd == NULL)
		return 0;

	ShowWindow(hwnd, nCmdShow);
	UpdateWindow(hwnd);
	
	while (GetMessage(&msg, NULL, 0, 0) > 0) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return (int)msg.wParam;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	static HMODULE       hcpl = NULL;
	static CPLINFO       cplInfo = {0};
	static LPFNCPLAPPLET lpfnCPlApplet = NULL;

	switch (uMsg) {

	case WM_CREATE: {
		TCHAR szFileName[256];

		GetWindowsDirectory(szFileName, sizeof(szFileName));
		lstrcat(szFileName, TEXT("\\system32\\inetcpl.cpl"));

		hcpl = LoadLibrary(szFileName);
		if (hcpl == NULL)
			return -1;

		lpfnCPlApplet = (LPFNCPLAPPLET)GetProcAddress(hcpl, "CPlApplet");
		if (lpfnCPlApplet == NULL)
			return -1;

		if (lpfnCPlApplet(hwnd, CPL_INIT, 0, 0) == 0)
			return -1;

		lpfnCPlApplet(hwnd, CPL_GETCOUNT, 0, 0);

		lpfnCPlApplet(hwnd, CPL_INQUIRE, 0, (LPARAM)&cplInfo);

		return 0;
	}

	case WM_PAINT: {
		HDC         hdc;
		HICON       hicon;
		TCHAR       szBuf[256]; 
		PAINTSTRUCT ps;

		hdc = BeginPaint(hwnd, &ps);

		hicon = (HICON)LoadImage(hcpl, MAKEINTRESOURCE(cplInfo.idIcon), IMAGE_ICON, 0, 0, LR_SHARED);
		DrawIcon(hdc, 30, 30, hicon);

		LoadString(hcpl, cplInfo.idName, szBuf, sizeof(szBuf));
		TextOut(hdc, 30, 80, szBuf, lstrlen(szBuf));

		LoadString(hcpl, cplInfo.idInfo, szBuf, sizeof(szBuf));
		TextOut(hdc, 30, 130, szBuf, lstrlen(szBuf));

		EndPaint(hwnd, &ps);

		return 0;
	}

	case WM_LBUTTONDOWN:
		lpfnCPlApplet(hwnd, CPL_DBLCLK, 0, cplInfo.lData);
		lpfnCPlApplet(hwnd, CPL_STOP, 0, cplInfo.lData);
		return 0;

	case WM_DESTROY:
		if (hcpl != NULL) {
			if (lpfnCPlApplet != NULL)
				lpfnCPlApplet(hwnd, CPL_EXIT, 0, 0);
			FreeLibrary(hcpl);
		}

		PostQuitMessage(0);

		return 0;

	default:
		break;

	}

	return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

実行結果は、次のようになります。

上図は、CPLINFO構造体のidIcon, IdName, IdInfoの各識別子から、 実際のリソースを取得して描画したものです。 これらを実現できるのは、WM_CREATEでの正しい初期化の効果です。

GetWindowsDirectory(szFileName, sizeof(szFileName));
lstrcat(szFileName, TEXT("\\system32\\inetcpl.cpl"));

hcpl = LoadLibrary(szFileName);
if (hcpl == NULL)
	return -1;

lpfnCPlApplet = (LPFNCPLAPPLET)GetProcAddress(hcpl, "CPlApplet");
if (lpfnCPlApplet == NULL)
	return -1;

とにもかくにも、まずはcplファイルをロードしなければなりません。 ここでのロードの対象は、inetcpl.cplにしていますが勿論、自由に決めて構いません。 LoadLibraryで、c:\\WINDOWS\\system32\\inetcpl.cpl とせずに、 GetWindowsDirectoryをわざわざ呼び出しているのは、 WindowsDirectoryというものが必ずしも c:\\WINDOWS とは限らないためです。 lpfnCPlAppletの型は、CPlAppletの関数ポインタです。 この変数とhcplは、他のメッセージでも参照されるため静的に宣言されています。

if (lpfnCPlApplet(hwnd, CPL_INIT, 0, 0) == 0)
	return -1;

lpfnCPlApplet(hwnd, CPL_GETCOUNT, 0, 0);

lpfnCPlApplet(hwnd, CPL_INQUIRE, 0, (LPARAM)&cplInfo);

CPLメッセージを順にCPlAppletに送ります。 送る順番は、必ず上記のようにするべきです。 また、CPL_INITで0が返ったときには、初期化が失敗したことを意味していたことを 思い出してください。 CPL_GETCOUNTではダイアログの数が返りますが、 今回は複数のダイアログの起動は考慮していないので、戻り値は受け取りませんでした。 CPL_INQUIREにて初期化したcplInfoは、WM_PAINTにて参照されます。 そのため、この変数も静的に宣言されています。 次のコードは、WM_PAINTの一部です。

hicon = (HICON)LoadImage(hcpl, MAKEINTRESOURCE(cplInfo.idIcon), IMAGE_ICON, 0, 0, LR_SHARED);
DrawIcon(hdc, 30, 30, hicon);

LoadString(hcpl, cplInfo.idName, szBuf, sizeof(szBuf));
TextOut(hdc, 30, 80, szBuf, lstrlen(szBuf));

LoadString(hcpl, cplInfo.idInfo, szBuf, sizeof(szBuf));
TextOut(hdc, 30, 130, szBuf, lstrlen(szBuf));

idIconメンバは、アイコンの識別子です。 これをMAKEINTRESOURCE経由でLoadImageに指定すれば、 実際のアイコンを取得できます。 第1引数に、cplファイルをロードしたアドレスを指定するのがポイントです。 文字列の取得は、LoadStringで可能です。 ここでも、第1引数はcplファイルをロードしたアドレスを指定します。

case WM_LBUTTONDOWN:
	lpfnCPlApplet(hwnd, CPL_DBLCLK, 0, cplInfo.lData);
	lpfnCPlApplet(hwnd, CPL_STOP, 0, cplInfo.lData);
	return 0;

WM_LBUTTONDOWNではCPL_DBLCLKを送り、ダイアログの起動を促します。 本来は、アイコンのクリック時ということになっていますが、 メッセージを受け取った側としては、それは重要なことではないでしょう。 第4引数には、CPlAppletが内部で初期化したアプリケーション定義値を 忘れずに指定しなければなりません。 CPL_DBLCLKから制御が返ったらば、それはダイアログが閉じられたということですから、 後始末処理の機会を与えるべく、CPL_STOPを送ります。

if (hcpl != NULL) {
	if (lpfnCPlApplet != NULL)
		lpfnCPlApplet(hwnd, CPL_EXIT, 0, 0);
	FreeLibrary(hcpl);
}

これは特に述べることはないでしょう。 CPL_EXITは、FreeLibraryの呼び出し前に送ります。

cplファイルの扱われ方

実際にCPLメッセージを送る側になってみると、各メッセージの役割が見えてくることもあり、 CPlAppletのコーディングにもプラスとなる知識が得られると思われます。 今回はいわば、explorer.exeやrundll32.exeが行っていることを真似てみたわけですが、 これら2つのプロセスもCPLメッセージをCPlAppletに送っていると思われます。 ただ、その送り方を調べたところ少し興味深いところがあったため、 長文にはなりますが、以下にその経緯と筆者の想像を記したいと思います。

まず、explorer.exeはコントロールパネルを始めて開くとき、 レジストリとsysytem32フォルダを走査し、見つかったcplファイルをロードします。 そして、CPL_INITからCPL_INQUIREまでのメッセージを送り、CPLINFO構造体を初期後、 アイコン及び文字列、そしてcplファイル名を内部データベースに登録します。 これにより、2回目以降のコントロールパネルの起動は、 各cplファイルの文字列、アイコンを維持しているため、 cplファイルを実際にロードせずに、素早くリソースを表示できます。 しかしながら、explorer.exeは2回目以降のコントロールパネルの起動でも、 cplファイルを走査するようになっています。 たとえば、cplファイルが削除されたならば、いくらアイコンを登録しているかたといって、 それを表示するにはいけませんから、データベースに登録しているcplファイルが 今もなお存在するかを確かめなければなりません。 存在しない場合は、登録は解除されます。 もし、cplファイルを更新したのにアイコンが反映されないようであれば、 一度cplファイルを削除してコントロールパネルを起動し、 その後に新しいcplファイルを配置すればよいでしょう。 このようにすれば、次にコントロールパネルを起動したときには、 explorer.exeはそのcplファイルを登録します。

explorer.exeは、ユーザーがアイコンをクリックしたときに対応するcplファイルを求め、 runll32.exeを起動すると共にcplファイルをコマンドラインとして指定します。 runll32.exeは受け取ったcplファイルをロードし、 CPL_INITからCPL_DBLCLKまでのメッセージを続けて送ります。 CPL_DBLCLKまでが送られるのは、不思議なことのように思えますが、 コントロールパネルにてユーザーがアイコンをクリックしたので当然といえば当然です。 結局、CPL_DBLCLKを送るタイミングというというのは、 CPL_INQUIREが既に処理されていれば、いつでもよいということなのです。 ダイアログが表示されたとき、タスクマネージャなどを表示してみると rundll32.exeが存在することが分かりますが、場合によっては存在しないこともあります。 実はここには巧妙な手段が使われており、実際にはrundll.exeは起動されたのですが、 CPlAppletがCPL_DBLCLK時に別プロセスを起動して直ぐに制御を返したため、 rundll.exeも終了したということなのです。 つまり、ダイアログの表示を専用のプロセスに委ねたということです。 このようにすれば、cplファイルにはダイアログの起動に関するコードを含めずにすむため、 コードが非常に見やすくなり、目的がはっきりとします。



戻る