...ing logging 4.0

はてなブログに移行しました。D言語の話とかいろいろ。

通知領域アイコンの矩形座標を得る(高 DPI 対応)

dfl.notifyicon の残件だったイベント関係に対応しようとしたら、 通知領域のアイコンをキーボードで選択したときに、 そのアイコンの座標にコンテキストメニューを出す必要があって、 その座標を取得するのが大変だった。 マウスでクリックしたときは GetCursorPos() で簡単に取得できるけどその方法が使えないし、 この PC は表示スケールが 150% なのにその倍率を取得しようとしても 140% になってしまう。

ネットで検索しても、おそらく SetProcessDpiAwarenessContext() とかを使って、 DPI Aware なアプリにする前提になっているもよう。 ただ、DPI Aware なアプリは、バルーン通知が表示されないという仕様らしい。 どうして・・・。 バルーン通知が表示されなくなる理由に気づくのにかなり時間がかかった。

DPI Aware なアプリには結局できないけど、 下のコードなら、表示スケールを変えても正しい座標に変換して取得できる。 その座標にコンテキストメニューを表示すればよい。

記事冒頭のスクリーンショットは、キーボードでアイコンを選択した様子。

///
Rect getNotifyIconRect(HWND notifyIconControlHandle, ushort notifyIconID)
{
    import dfl.drawing;

    NOTIFYICONIDENTIFIER nii;
    nii.cbSize = nii.sizeof;
    nii.guidItem = GUID(); // GUID_NULL
    nii.hWnd = notifyIconControlHandle;
    nii.uID = notifyIconID;

    RECT iconRect;
    Shell_NotifyIconGetRect(&nii, &iconRect);

    // Graphics g = Graphics.getScreen();
    // g.drawRectangle(new Pen(Color.red, 3), Rect(&iconRect));

    POINT topLeft = POINT(iconRect.left, iconRect.top);
    POINT bottomRight = POINT(iconRect.right, iconRect.bottom);
    PhysicalToLogicalPointForPerMonitorDPI(null, &topLeft);
    PhysicalToLogicalPointForPerMonitorDPI(null, &bottomRight);
    ClientToScreen(notifiIconControlHandle, &topLeft);
    ClientToScreen(notifiIconControlHandle, &bottomRight);

    return Rect(topLeft.x, topLeft.y, bottomRight.x, bottomRight.y);
}

ネットに丁度いい記事が見つからないので書いておく。 Windows API に置き換えるのは簡単にできると思う。

NOTIFYICONIDENTIFIER の hWnd に与えるウィンドウハンドルについてはここを参照。

https://learn.microsoft.com/ja-jp/windows/win32/api/shellapi/ns-shellapi-notifyicondataa

hWnd

型: HWND

通知領域のアイコンに関連付けられた通知を受信するウィンドウへのハンドル。

それにしても、DPI Aware 関係は比較的新しい API を使わないといけないので、軒並み定義されてないから、ほとんど自分で定義しないといけない。