tmytのらくがき

個人の日記レベルです

Part.1 Hello World!

やる。と言ってしまったからには後には引けないので第1回。どこにでもあるTodayでのHello Worldです。
今回できあがるのはこんなのです。

TodayはDLLとして実装しなければいけないので、とりあえずDllMainを書きます。

// DLL エントリポイント
int DllMain(HANDLE hinstDll, DWORD dwReason, LPVOID lpvReserved)
{
    switch (dwReason) {
        case DLL_PROCESS_ATTACH:
            // DLL_THREAD_ATTACH/DLL_THREAD_DETACH が不要なので
            DisableThreadLibraryCalls((HMODULE)hinstDll);
            // プロセスにアタッチされたときにウィンドウクラスを登録する
            g_hinst = (HINSTANCE)hinstDll;
            if (! registerClass())
                return FALSE;
            return TRUE;
        case DLL_PROCESS_DETACH:
            // プロセスからデタッチされたときにクラスを削除する
            unregisterClass();
            return TRUE;
    }
    return FALSE;
}

いたって普通なDllMainです。
TodayアプリはTodayウィンドウの子ウィンドウとして実装されているので、DLL_PROCESS_ATTACHでTodayに表示するウィンドウのウィンドウクラスを登録しています。DLL_PROCESS_DETACHではクラスの登録を解除しています。
以下はregisterClass(), unregisterClass() それぞれの中身です。

// クラスを登録
BOOL registerClass()
{
    WNDCLASS wc = {0};
    wc.lpfnWndProc   = (WNDPROC)WndProc;
    wc.hInstance     = g_hinst;
    wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
    wc.lpszClassName = szClassName;
    return RegisterClass(&wc) != INVALID_ATOM;
}

// クラスを削除
BOOL unregisterClass()
{
    return UnregisterClass(szClassName, g_hinst);
}

さて、TodayはDLLとして実装されシステムにロードされ、表示されます。そのためには当然関数をエクスポートしなければいけません。ここまでのコードではウィンドウクラスの登録/解除しかしていません。
Todayアプリでは序数240, 241 で関数をエクスポートします。
序数240ではTodayアプリのウィンドウを生成します。241ではオプションダイアログのダイアログプロシージャを実装します。
序数240の関数は今回は次のようにしてみました。

// 序数240の関数
extern "C" HWND APIENTRY InitializeCustomItem(TODAYLISTITEM *ptlistItem, HWND hwndParent)
{
    // fEnableがFALSEならNULLを返さなければならない
    if (!ptlistItem->fEnabled)
        return NULL;

    // hwndParentの子ウィンドウとして自身を作成
    HWND hWnd = CreateWindow(szClassName, szClassName, WS_CHILD,
         0, 0, 0, 0, hwndParent, NULL, g_hinst, NULL);
    ShowWindow(hWnd, SW_SHOW);
    return hWnd;
}

コード中のコメントにもあるようにfEnableがFALSEの時、関数はNULLを返さなければなりません。
fEnableはコントロールパネルのTodayアイテムの設定の有効/無効と同じです。
fEnableがTRUEの場合は引数で渡されたウィンドウハンドルの子ウィンドウとしてウィンドウを生成します。
関数の戻り値として、生成したウィンドウのウィンドウハンドルを返します。

そして、最後にウィンドウプロシージャです。
今回はHello Worldだけなので簡単です。

// ウィンドウプロシージャ
LRESULT WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uMsg) {
        // システムが2秒に1回送ってきます。
        // 高さの変更はここで。
        // TRUEを返すとシステムはウィンドウの高さを変更します
        case WM_TODAYCUSTOM_QUERYREFRESHCACHE:
            {
                TODAYLISTITEM* ptlistItem = (TODAYLISTITEM*)wParam;    
                if (ptlistItem->cyp == 0) {
                    ptlistItem->cyp = DRA::SCALEY(20);
                    return TRUE;
                }
                return FALSE;
            }

        // Todayを描画するときに送られます。
        // 普通のWindowsプログラムとおなじ
        case WM_PAINT:
            {
                PAINTSTRUCT ps;
                HDC hdc = BeginPaint(hWnd, &ps);

                // Todayの背景を描画
                TODAYDRAWWATERMARKINFO dwi = {0};
                GetClientRect(hWnd, &dwi.rc);
                dwi.hdc = (HDC)wParam;
                dwi.hwnd = hWnd;
                SendMessage(GetParent(hWnd), TODAYM_DRAWWATERMARK, 0, (LPARAM)&dwi);
                
                // このあたりで描画を。
                static const wchar_t str[] = L"Hello World!";
                RECT rc;
                GetClientRect(hWnd, &rc);
                DrawText(hdc, str, wcslen(str), &rc, 0);

                EndPaint(hWnd, &ps);
                return TRUE;
            }
    }

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

ウィンドウプロシージャではToday特有のメッセージをいくつか処理する必要があるだけで、それ以外は普通のWindowsアプリケーションと同じです。
Hello World!するだけのTodayなので、Today用のメッセージは1つしか処理してません。
WM_TODAYCUSTOM_QUERYREFRESHCACHEメッセージはTodayが初期化されるとき、または2秒毎にシステムが送信してきます。Todayの高さを変更するときはこのメッセージのWPARAMとして送信されるTODAYLISTITEM構造体へのポインタのcypメンバに値をセットしてTRUEを返します。
このメッセージに対しTRUEが返るとシステムはアイテムの高さを変更します。
このコードでは、初回ではcypメンバには0が入っているので、cypメンバの値をしらべ0ならばTodayの高さを20ドットに変更しています。0でない場合、高さの変更は不要なのでただFALSEを返しているだけです。
DRA::SCALEY(20)は、システムの解像度に従って値をスケーリングしてくれるMSのライブラリです。VS2005あたりからついているようです。このコードではAdesなどのVGAデバイスでは値を40に、S11HTなどのQVGAなデバイスでは値を20に。といった風にスケーリングしてくれます。

WM_PAINTではたいしたことはしていません。親ウィンドウに対しTODAYM_DRAWWATERMARKメッセージを送信している以外は、普通のWindowsプログラムでウィンドウに対しHello Worldを描画しているのと変わりません。
TODAYM_DRAWWATERMARKメッセージは、Todayアイテムの位置にマッチするような背景をシステムに描画してもらうメッセージです。LPARAMにTODAYDRAWWATERMARKINFO構造体へのポインタをセットしてメッセージを投げると背景を描画してくれます。
TODAYDRAWWATERMARKINFO構造体のメンバはそれぞれ、描画先デバイスコンテキストハンドル、描画範囲、自分自身のウィンドウハンドルです。
この先は普通にDrawTextしてるだけなので省きます。

順番が逆な気もしますが、このコードのヘッダ周りです。プロトタイプ宣言は省いてます。

#include <windows.h>
#include <todaycmn.h>
#include <DeviceResolutionAware.h>

// 定数宣言
const wchar_t szClassName[] = L"MinimalToday";

// グローバル変数
HINSTANCE g_hinst;

内容はたいしたことないです。windowsヘッダ、Today用メッセージなどが定義されたヘッダ、DRA名前空間を使用するためのヘッダです。
あとはクラス名の宣言と、DLLのインスタンスハンドルを保存するための変数宣言です。

ついでに序数でエクスポートするためのDEFファイルの内容です。

LIBRARY    "MinimalToday"
EXPORTS
    InitializeCustomItem @ 240 NONAME

以上、Todayを実装するコードでした。こいつをコンパイルすればTodayDLLとしては完成です。
ですがこのままではシステムはロードしてくれないし、Todayの設定にも出てきません。とりあえずレジストリ書かないと。なのです。
以下のキーと値を書けばTodayの設定に出てきて、Todayに表示可能になります。

[HKEY_LOCAL_MACHINE\\Software\\Microsoft\\Today\\Items\\Hello World]
"DLL"="\\Program Files\\HelloWorld\\HelloWorld.dll"
"Type"=dword:00000004

中身ですが、キー名のHello Worldのところは好きな文字列で大丈夫です。ここがTodayの設定画面に表示される名前になります。
DLLはみての通りTodayを実装したDLLファイルへのパスです。置いた場所に応じて適宜変更してください。
最後、TypeはDWORDで4固定です。5とか6とかも実はありますが、省きます。


以上、Todayの作り方 第1回でした。
今回つくったTodayはSelectability = 0なので選択できません。Selectabilityを1とか2とかにすると選択できるようになりますが、選択されたときの処理をしないといけないので次回以降で。
Todayに情報を表示するだけのものであればこんな感じに簡単に作れます。
システムではもう少したくさんのメッセージを送ってくれているのでそれの解説などを次回やっていこうかな。と思います。
では今回はここまで。

サンプルコードをダウンロードできます。必要な方はお使いください。このコードは無許可で再利用可能です。
Download: TodaySample_1.zip