iooiau.net

自作カーソルに影を付ける

Windows 2000 以降ではマウスカーソルにこのようにシステムのカーソル影が付くようになりましたが、プログラム内で自作のカーソルを使用した場合はこのように自作のカーソル影が付きません。
そこで、自作のカーソルにも影を付ける方法を紹介します。
ただし予め断っておきますが、実質的な意味はほとんどありませんし、あまり行儀のいい方法ではありません。

あるウィンドウに自作カーソルを使用する場合、通常 LoadCursor 関数などを使用してカーソルを読み込むか、CreateCursor 関数などを使用してカーソルを作成して、SetCursor 関数で設定します。
しかし、この方法ではカーソルに影が付いてくれません。 恐らく Windows からは各プログラムのカーソルに影を付けていいのかいけないのか分からないので、互換性のためにそうなっているのでしょう。

ではカーソルに影を付けたい場合はどうすればいいでしょうか。 SetCursorShadow といった API 関数が用意されていればいいのですが、そのようなものはありません。
そこで SetSystemCursor という API 関数を使用します。
この関数はその名の通りシステムのカーソルを変更する関数ですが、SetCursor と違いこの関数でカーソルを設定した場合、カーソルに影が付きます。
この関数での変更は一時的なもので、再びこの関数を呼ぶか、SystemParametersInfo 関数に SPI_SETCURSORS を渡して呼ぶか、Windows を再起動するとカーソルは元に戻ります。

SetSystemCursor を使用してカーソルを変更するコードは以下のようになります。


#define OEMRESOURCE   // OCR_??? の定義を利用するためにはこの定義が必要です
#include <windows.h>  // いつものヘッダ
#include <tchar.h>

static HINSTANCE hInst;         // プログラムのインスタンスハンドルです
static HCURSOR hcurOld = NULL;  // 元のカーソルを保持する変数です

// カーソルを設定する関数です
BOOL SetMyCursor(void)
{
    HCURSOR hcur;

    // 自作のカーソルをリソースから読み込んで複製します
    hcur = CopyCursor(LoadCursor(hInst, TEXT("IDC_MYCURSOR")));
    if (hcur != NULL) {

        // 現在のカーソルを取得して複製します
        hcurOld = CopyCursor(LoadCursor(NULL, IDC_ARROW));
        if (hcurOld != NULL) {

            // カーソルを自作のものに変えます
            if (SetSystemCursor(hcur, OCR_NORMAL))
                return TRUE;

            // 変えられなかったのでリソースを開放します
            DestroyCursor(hcurOld);
            hcurOld = NULL;
        }
        DestroyCursor(hcur);
    }

    return FALSE;
}

// カーソルを元に戻す関数です
void RestoreCursor(void)
{
    // カーソルを変更していた場合は元に戻します
    if (hcurOld != NULL) {
        SetSystemCursor(hcurOld, OCR_NORMAL);
        hcurOld = NULL;
    }
}

まず LoadCursor を使用してリソースからカーソルを読み込んでいます。 次に読み込んだカーソルを CopyCursor を使用して複製しています。
なぜ複製するかと言うと、SetSystemCursor で設定したカーソルはシステムが所有することになるので、プログラムの側のカーソルとは別のものにしなければならないからです。
次に LoadCursor を使用して、今度は現在のカーソルを取得して複製しています。
こうすることでカーソルを復元できるようにしているのですが、複製しているのは SetSystemCursor でカーソルを設定した段階で、前のカーソルが破棄されてしまうためです。

しかしシステムのカーソルを変更するのですから、単純に使っただけでは当然他のプログラムのカーソルまで変わってしまうことになります。
自分のウィンドウ内にカーソルが入ってきたら自作カーソルに変え、ウィンドウからカーソルが出た時にカーソルを元に戻す必要があります。
その為には、ウィンドウプロシージャで WM_MOUSEMOVE と WM_MOUSELEAVE を処理します。 具体的には以下のようなコードになります。


// ウィンドウプロシージャです
LRESULT CALLBACK WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    // カーソルを変更したかどうかを保持する変数です
    static BOOL fChangeCursor = FALSE;

    switch (uMsg) {
    case WM_MOUSEMOVE:
        // カーソルがウィンドウのクライアント領域内で移動した
        if (!fChangeCursor) {
            // カーソルがウィンドウの外に出た時に、WM_MOUSELEAVE で知らせてもらえるようにします
            TRACKMOUSEEVENT tme;
            tme.cbSize    = sizeof(tme);
            tme.dwFlags   = TME_LEAVE;
            tme.hwndTrack = hwnd;
            if (TrackMouseEvent(&tme)) {
                SetMyCursor();
                fChangeCursor = TRUE;
            }
        }
        return 0;

    case WM_MOUSELEAVE:
        // カーソルがウィンドウ外に出たので元に戻します
        RestoreCursor();
        fChangeCursor = FALSE;
        return 0;

    case WM_DESTROY:
        // 念のために終了時にもカーソルを元に戻す処理を入れておきます
        RestoreCursor();

        // プログラムを終了させます
        PostQuitMessage(0);
        return 0;
    }
    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

// エントリポイントです
int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                       LPTSTR pszCmdLine, int nCmdShow)
{
    // ウィンドウクラス名です
    static const LPCTSTR pszWindowClass = TEXT("MyCursor Sample Window");

    // インスタンスハンドルを保持します
    hInst = hInstance;

    // ウィンドウクラスを登録します
    WNDCLASS wc;
    wc.style         = 0;
    wc.lpfnWndProc   = WndProc;
    wc.cbClsExtra    = 0;
    wc.cbWndExtra    = 0;
    wc.hInstance     = hInst;
    wc.hIcon         = NULL;
    wc.hCursor       = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)(COLOR_3DFACE+1);
    wc.lpszMenuName  = NULL;
    wc.lpszClassName = pszWindowClass;
    RegisterClass(&wc);

    // ウィンドウを作成します
    HWND hwnd = CreateWindowEx(0, pszWindowClass, TEXT("Sample"),
                               WS_OVERLAPPEDWINDOW,
                               CW_USEDEFAULT, CW_USEDEFAULT, 256, 256,
                               NULL, NULL, hInst, NULL);
    if (hwnd == NULL) {
        MessageBox(NULL, TEXT("ウィンドウが作成できません。"), NULL, MB_OK | MB_ICONSTOP);
        return 0;
    }
    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);

    // メッセージループです
    MSG msg;
    BOOL Result;
    while ((Result = GetMessage(&msg, NULL, 0, 0)) != 0 && Result != -1) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return msg.wParam;
}

実行結果は以下のようになり、ちゃんと影が付いています。
実行結果

これでカーソルに影を付けることができました。
しかし、プログラムが異常終了した場合にカーソルが元に戻らない可能性があります。 それが気になるのであれば、異常終了時にカーソルを元に戻す処理を追加します。
以下のように記述すればいいでしょう。


// 例外発生時に呼ばれる関数です
LONG WINAPI ExceptionFilter(EXCEPTION_POINTERS *ExceptionInfo)
{
    // カーソルを元に戻します
    SystemParametersInfo(SPI_SETCURSORS, 0, NULL, 0);
    return EXCEPTION_CONTINUE_SEARCH;
}

// エントリポイントです
int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                       LPTSTR pszCmdLine, int nCmdShow)
{
    // 例外ハンドラを登録します
    SetUnhandledExceptionFilter(ExceptionFilter);

    // 以下は前のコードと同じです

先に少し触れましたが、SystemParametersInfo 関数に SPI_SETCURSORS を指定して呼び出すと、カーソルがシステムで設定されているものに戻ります。
先に示した SetMyCursor では古いカーソルを複製しておいて RestoreCursor で元に戻す処理を記述していますが、実はこれを使えばそんなことをしなくてもいい訳です。

さて、ここまでカーソルに影を付ける方法を紹介しましたが、はっきり言ってただカーソルに影を付けるだけのためにこんなコードを書くのは無駄です(とはいえ、人間は無駄なことが大好きなのですが)
しかも、システムのカーソルを変更するという行儀の悪いことを行っています。 ですので通常はこんなことはしない方がいいのですが、「どうしてもカーソルに影がないと気が済まない!」という方は(もし居れば)やってみてください。