Tutorial 23: Tray Icon

このチュートリアルではアイコンをシステムトレイに格納し、 どうやってポップアップメニュを作成、使用するかを学ぶ。
 ソース   実行結果 

Theory:

システムトレイはタスクバーにある矩形領域で、そこにはアイコンがいくつかあるはずだ。 普通はそこに時計アイコンがあるだろう(私はないけど=p)。 そこには自分のプログラムでも格納することができる。 やり方は以下のような手順を踏めばよい。

  1. 以下に説明するNOTIFYICONDATA構造体をセットする。
    • cbSize
      この構造体のサイズ。
    • hwnd
      ウィンドウハンドル。マウスイベントがトレイアイコン上で起こった時に、このウィンドウに通知が来る。
    • uID
      アイコンを識別する定数。この値は自由に決められる。複数のアイコンを使用したければ、どのトレイアイコンがマウスの通知を受けたかをチェックすればよい。
    • uFlags
      この構造体のどのメンバを有効にするかのフラグを指定する。
      • NIF_ICON:hIconを有効にする
      • NIF_MESSAGE:uCallbackMessageを有効にする
      • NIF_TIP:szTipを有効にする
    • uCallbackMessage
      トレイアイコンにマウスイベントが発生したときに、hwndで指定されるウィンドウへWindowsに送信してもらうメッセージ。このメッセージは自分で指定する。
    • hIcon
      システムトレイに格納したいアイコンのハンドル
    • szTip
      マウスカーソルがトレイアイコン上にあるときにツールチップ文字列として使用される最大64バイトの文字列(システムDLLが5.0か、それ以降なら最大128バイトまで)
  2. shell32.incで定義されているShell_NotifyIcon関数をCALLする。

    Shell_NotifyIcon PROTO dwMessage:DWORD ,pnid:DWORD

    • dwMessageはシェルへ送信するメッセージのタイプを指定する
      • NIM_ADD:ステータスエリアにアイコンを追加する
      • NIM_DELETE:ステータスエリアからアイコンを削除する
      • NIM_MODIFY:ステータスエリアのアイコンを変更する
    • pnid
      適当な値をセットしたNOTIFYICONDATA構造体へのポインタ

これが全てだ。しかしたいていは、アイコンを格納するだけで満足できない。 トレイアイコンに発生するマウスイベントに対しての処理も必要である。 なので、NOTIFYICONDATA構造体のメンバであるuCallbackMessageに記述されるメッセージに対する処理を行うことになる。 このメッセージでは wParam と lParam に以下のような値がセットされる。

しかしながらこれだけではなく、ほとんどのトレイアイコンはユーザが右クリックしたらメニューがポップアップされるようになっている。 ポップアップメニューを作成することによりこの機能を実装し、TrackPopupMenu関数をCALLしてそのメニューを表示させる。手順は以下だ。

  1. CreatePopupMenu関数をCALLしてポップアップメニューを作成する。この関数は空のメニューを作成し、成功すればメニューハンドルを返す。
  2. AppendMenu関数、もしくはInsertMenu関数かInsertMenuItem関数を使用してメニューアイテムを追加する。
  3. マウスカーソルがあるときにポップアップメニューを表示したければ、GetCursorPos関数をCALLしてマウスカーソルのスクリーン座標値を取得し、GetCursorPos関数をCALLしてメニューを表示させる。
    ユーザがそのポップアップメニューからメニューアイテムを選択すれば、Windowsは通常のメニューセクションと同じようにWM_COMMANDメッセージをウィンドウプロシージャに送信する。

※  トレイアイコンにポップアップメニューを使用するときは、2つのやっかいな問題に気をつけなければならない

  1. ポップアップメニューが表示されたとき、メニュー以外のどこかの場所をクリックしてもすぐにはそのポップアップメニューは消えない。これは、ポップアップメニューから通知を受け取るウィンドウはフォアグラウンドでなければならないからだ。なのでこれを回避するために、SetForegroundWindow関数をCALLする。
  2. SetForegroundWindow関数をCALLした後、ポップアップメニューが表示される一番最初の動作は大丈夫だが、次回、ポップアップメニューが表示されたときは、すぐに閉じてしまう。これは「意図的」な動作であるとMSDNには記述されており、そのうち必要となるトレイアイコンのオーナーであるプログラムへのタスクスイッチが必要である。
    このタスクスイッチを強制的に発生させたければ、ウィンドウへなんらかのメッセージを送信すればよい。その際、PostMessage関数を使用しなければならない。SendMessage関数ではないことに気をつけること。

Example:

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\shell32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\shell32.lib

WM_SHELLNOTIFY equ WM_USER+5
IDI_TRAY equ 0
IDM_RESTORE equ 1000
IDM_EXIT equ 1010
WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD

.data
ClassName db "TrayIconWinClass",0
AppName   db "TrayIcon Demo",0
RestoreString db "&Restore",0
ExitString  db "E&xit Program",0

.data?
hInstance dd ?
note NOTIFYICONDATA <>
hPopupMenu dd ?

.code
start:
   invoke GetModuleHandle, NULL
   mov   hInstance,eax
   invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
   invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
   LOCAL wc:WNDCLASSEX
   LOCAL msg:MSG
   LOCAL hwnd:HWND
   mov  wc.cbSize,SIZEOF WNDCLASSEX
   mov  wc.style, CS_HREDRAW or CS_VREDRAW or CS_DBLCLKS
   mov  wc.lpfnWndProc, OFFSET WndProc
   mov  wc.cbClsExtra,NULL
   mov  wc.cbWndExtra,NULL
   push hInst
   pop  wc.hInstance
   mov  wc.hbrBackground,COLOR_APPWORKSPACE
   mov  wc.lpszMenuName,NULL
   mov  wc.lpszClassName,OFFSET ClassName
   invoke LoadIcon,NULL,IDI_APPLICATION
   mov  wc.hIcon,eax
   mov  wc.hIconSm,eax
   invoke LoadCursor,NULL,IDC_ARROW
   mov  wc.hCursor,eax
   invoke RegisterClassEx, addr wc
   invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPED+WS_CAPTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEBOX+WS_VISIBLE,CW_USEDEFAULT,\
          CW_USEDEFAULT,350,200,NULL,NULL,\
          hInst,NULL
   mov  hwnd,eax
   .while TRUE
       invoke GetMessage, ADDR msg,NULL,0,0
       .BREAK .IF (!eax)
       invoke TranslateMessage, ADDR msg
       invoke DispatchMessage, ADDR msg
   .endw
   mov eax,msg.wParam
   ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
   LOCAL pt:POINT
   .if uMsg==WM_CREATE
       invoke CreatePopupMenu
       mov hPopupMenu,eax
       invoke AppendMenu,hPopupMenu,MF_STRING,IDM_RESTORE,addr RestoreString
       invoke AppendMenu,hPopupMenu,MF_STRING,IDM_EXIT,addr ExitString
   .elseif uMsg==WM_DESTROY
       invoke DestroyMenu,hPopupMenu
       invoke PostQuitMessage,NULL
   .elseif uMsg==WM_SIZE
       .if wParam==SIZE_MINIMIZED
           mov note.cbSize,sizeof NOTIFYICONDATA
           push hWnd
           pop note.hwnd
           mov note.uID,IDI_TRAY
           mov note.uFlags,NIF_ICON+NIF_MESSAGE+NIF_TIP
           mov note.uCallbackMessage,WM_SHELLNOTIFY
           invoke LoadIcon,NULL,IDI_WINLOGO
           mov note.hIcon,eax
           invoke lstrcpy,addr note.szTip,addr AppName
           invoke ShowWindow,hWnd,SW_HIDE
           invoke Shell_NotifyIcon,NIM_ADD,addr note
       .endif
   .elseif uMsg==WM_COMMAND
       .if lParam==0
           invoke Shell_NotifyIcon,NIM_DELETE,addr note
           mov eax,wParam
           .if ax==IDM_RESTORE
               invoke ShowWindow,hWnd,SW_RESTORE
           .else
               invoke DestroyWindow,hWnd
           .endif
       .endif
   .elseif uMsg==WM_SHELLNOTIFY
       .if wParam==IDI_TRAY
           .if lParam==WM_RBUTTONDOWN
               invoke GetCursorPos,addr pt
               invoke SetForegroundWindow,hWnd
               invoke TrackPopupMenu,hPopupMenu,TPM_RIGHTALIGN,pt.x,pt.y,NULL,hWnd,NULL
               invoke PostMessage,hWnd,WM_NULL,0,0
           .elseif lParam==WM_LBUTTONDBLCLK
               invoke SendMessage,hWnd,WM_COMMAND,IDM_RESTORE,0
           .endif
       .endif
   .else
       invoke DefWindowProc,hWnd,uMsg,wParam,lParam
       ret
   .endif
   xor eax,eax
   ret
WndProc endp

end start

Analysis:

プログラムはウィンドウを表示し、最小化ボタンを押したとき、ウィンドウを隠しシステムトレイにアイコンを格納する。 そのアイコンをダブルクリックしたとき、プログラムはウィンドウを復元させシステムトレイからアイコンを削除する。 右クリックしたとき、ポップアップメニューを表示し、プログラムを終了するかウィンドウを復元するかを選択できる。

.if uMsg==WM_CREATE
     invoke CreatePopupMenu
     mov hPopupMenu,eax
     invoke AppendMenu,hPopupMenu,MF_STRING,IDM_RESTORE,addr RestoreString
     invoke AppendMenu,hPopupMenu,MF_STRING,IDM_EXIT,addr ExitString

メインウィンドウが作成されたとき、ポップアップメニューを作成し2つのメニューアイテムを追加する。 AppendMenu関数はこのようになっている。

AppendMenu PROTO hMenu:DWORD, uFlags:DWORD, uIDNewItem:DWORD, lpNewItem:DWORD

ポップアップメニューが作成し終えたら、メインウィンドウはユーザが最小化ボタンを押すまで待ち続ける。
ウィンドウが最小化されれば、wParamがSIZE_MINIMIZEDにセットされ、WM_SIZEメッセージを受け取る。

.elseif uMsg==WM_SIZE
    .if wParam==SIZE_MINIMIZED
        mov note.cbSize,sizeof NOTIFYICONDATA
        push hWnd
        pop note.hwnd
        mov note.uID,IDI_TRAY
        mov note.uFlags,NIF_ICON+NIF_MESSAGE+NIF_TIP
        mov note.uCallbackMessage,WM_SHELLNOTIFY
        invoke LoadIcon,NULL,IDI_WINLOGO
        mov note.hIcon,eax
        invoke lstrcpy,addr note.szTip,addr AppName
        invoke ShowWindow,hWnd,SW_HIDE
        invoke Shell_NotifyIcon,NIM_ADD,addr note
    .endif

このメッセージを受け取ったときにNOTIFYICONDATA構造体をセットする。 IDI_TRAYはソースコードの最初に定義されている、単なる定数である。この値は何でも好きな値をセットできる。 今回の例ではトレイアイコンが1つしかないので重要ではないのだが、 システムトレイに複数のアイコンを格納する場合はそれぞれのアイコンにユニークな値を割り振らなければならない。

そして、アイコン、メッセージ、ツールチップテキストを使用するので、uFlagsに全てのフラグをセットする。 WM_SHELLNOTIFYは WM_USER+5 と定義された単なるメッセージである。 値自体はユニークでありさえすれば重要ではない。 ここではトレイアイコンとしてwinlogoアイコンを使用するが、どんなアイコンでも使用できる。 LoadIcon関数でリソースからアイコンをロードし、戻り値をhIconにセットする。 最後に、マウスがアイコンに重なったときに表示させるszTipに文字列をセットする。

「最小化することによりトレイアイコンが表示される」というイリュージョンを完成させるためにメインウィンドウを画する。 次に、システムトレイにアイコンを追加するためにNIM_ADDメッセージ付きでShell_NotifyIcon関数をCALLする。

これにより、メインウィンドウは隠れて、アイコンがシステムトレイに格納される。 もしマウスカーソルをアイコンに重ねれば、szTipにセットした文字列を表示したツールチップが現れるだろう。 次に、ダブルクリックすれば、メインウィンドウは再び表示され、トレイアイコンは消え去る。

.elseif uMsg==WM_SHELLNOTIFY
    .if wParam==IDI_TRAY
        .if lParam==WM_RBUTTONDOWN
            invoke GetCursorPos,addr pt
            invoke SetForegroundWindow,hWnd
            invoke TrackPopupMenu,hPopupMenu,TPM_RIGHTALIGN,pt.x,pt.y,NULL,hWnd,NULL
            invoke PostMessage,hWnd,WM_NULL,0,0
        .elseif lParam==WM_LBUTTONDBLCLK
            invoke SendMessage,hWnd,WM_COMMAND,IDM_RESTORE,0
        .endif
    .endif

マウスイベントがトレイアイコン上で発生すれば、 作成したウィンドウにuCallbackMessageにセットしたWM_SHELLNOTIFYメッセージが送信される。 このメッセージを受け取ったら、wParamにトレイアイコンのID、lParamは実際のマウスメッセージになるということを思い出そう。 上のコードでは、まずこのメッセージがトレイアイコンから送信されたものかどうかをチェックする。 もしそうなら、マウスメッセージをチェックし、今回の例ではマウスの右クリックとダブルクリックしか必要ないので、 WM_RBUTTONDOWNとWM_LBUTTONDBLCLKメッセージの場合だけに対して処理を行う。

マウスメッセージがWM_RBUTTONDOWNだったら、GetCursorPos関数をCALLしてマウスカーソルのスクリーン座標値を取得する。 関数からの戻り値は、マウスカーソルのスクリーン座標値がセットされたPOINT構造体となっている。 スクリーン座標値とは、ウィンドウは関係なく、完全に画面の座標値である。 例えば、解像度が640*480だったら、一番右下の座標値は (639,479) となる。 このスクリーン座標値を各々のウィンドウの座標値に変換するには、ScreenToClient関数を使用する。

しかしながら、今回の目的は、TrackPopupMenu関数をCALLしてマウスカーソルの場所にポップアップメニューを表示することなので、 別にウィンドウ座標値に変換する必要はなく、直接GetCursorPos関数でセットされたスクリーン座標値を使用すればよいのである。

TrackPopupMenu PROTO hMenu:DWORD, uFlags:DWORD, x:DWORD, y:DWORD, nReserved:DWORD, hWnd:DWORD, prcRect:DWORD

ユーザがトレイアイコン上でダブルクリックした場合、 ポップアップメニューの「Restore」メニューアイテムをユーザがセレクトしたのと同じ動作を行うために、 IDM_RESTOREで指定されたウィンドウにWM_COMMANDメッセージを送信する。 その後、メインウィンドウが復元され、システムトレイからアイコンが削除される。 ダブルクリックメッセージを受け取るために、 メインウィンドウはCS_DBLCLKSスタイルになっていなければならない。

invoke Shell_NotifyIcon,NIM_DELETE,addr note
mov eax,wParam
.if ax==IDM_RESTORE
    invoke ShowWindow,hWnd,SW_RESTORE
.else
    invoke DestroyWindow,hWnd
.endif

ユーザがRestoreメニューアイテムを選択したら、 再度、Shell_NotifyIcon関数をCALLしてトレイアイコンを削除するのだが、 今度はNIM_DELETEをセットする。 次に、メインウィンドウをもともとの状態で復元しする。 ユーザがExitメニューアイテムを選択したら、トレイアイコンはもちろん削除するが、 DestroyWindow関数をCALLしてメインウィンドウも削除する。


[戻る]