2007. 2. 16. 14:11
DevX Win32 Assembly Tutorial 23: Tray Icon

Tutorial 23: Tray Icon

이 번장에서는 아이콘을 시스템 트레이에  어떻게 저장하는지와, 어떻게 팝업메뉴를 작성하는지에 대해서 설명한다.
 소스    실행 결과  

Theory:

시스템 트레이는 작업표시줄에 존재하는 사각영역으로서, 몇개의 아이콘이 이미 존재하고 있을것이다. 이미 시계 아이콘과 같은것이 있을 것이다. 이 부분에는 자신만의 프로그램을 사용할 수 있다.이것은 다음과 같은 순서를 따르면 된다.

  1. 다음에 설명하는 NOTIFYICONDATA 구조체를 설정한다.
    • cbSize
      구조체의 크기.
    • hwnd
      윈도우 핸들. 마우스 이벤트가 트레이 아이콘에서 발생할때, 이 윈도우로 통지신호를 보낸다.
    • uID
      아이콘을 구별하는 정수값. 이 값은 자유롭게 설정할 수 있다. 여러개의 아이콘을 사용하고 싶다면, 어떤 트레이 아이콘이 마우스통지 신호를 받았는지를 체크하면 된다.
    • uFlags
      유효하게 할 멤버의 플래그를 지정한다.
      • NIF_ICON:hIcon를 유효하게 한다
      • NIF_MESSAGE:uCallbackMessage를 유효하게 한다
      • NIF_TIP:szTip를 유효하게 한다
    • uCallbackMessage
      트레이 아이콘에 마우스 이벤트가 발생했을 때, hwnd로 지정된 윈도우에 전송할 메세지. 이 메세지는 직접 생성한다.
    • hIcon
      시스템 트레이에 저장하고 싶은 아이콘의 핸들
    • szTip
      마우스 커서가 트레이 아이콘에 있을 때 툴팁 형태로 표시되는 문자열로서 최대 64바이트의 문자열(시스템 DLL이 5.0이라면 최대 128바이트)
  2. shell32.inc에 정의되어 있는 Shell_NotifyIcon 함수를 호출한다.

    Shell_NotifyIcon PROTO dwMessage:DWORD , pnid:DWORD

    • dwMessage는 해당 윈도우로 전송하는 메세지의 타입을 지정한다
      • NIM_ADD:트레이 영역에 아이콘을 추가한다
      • NIM_DELETE:트레이 영역의 아이콘을 삭제한다
      • NIM_MODIFY:트레이 영역의 아이콘을 변경한다
    • pnid
      적절한 값을 설정 한 NOTIFYICONDATA 구조체의 포인터

이것이 전부다. 하지만, 아이콘을 표시하는 것만으론 만족할 수 없을 것이다. 트레이 아이콘에서 발생되는 마우스 이벤트에 대한 처리도 필요하다. 그래서, NOTIFYICONDATA 구조체의 멤버인 uCallbackMessage에서 설정된 메세지에 대한 처리를 하게 된다. 이 메세지는 wParam 와 lParam 에 다음과 같은 값이 설정 된다.

  • wParam는 아이콘의 ID. 이것은 NOTIFYICONDATA 구조체의 uID 멤버에 설정한 값과 같다.
  • lParam의 하위워드는 마우스 메세지이다. 예를 들어, 유저가 아이콘에서 오른쪽버튼을 클릭하면, lParam은 WM_RBUTTONDOWN이 된다.

대부분의 경우 트레이 아이콘에서 유저가 오른쪽버튼을 클릭하면 팝업메뉴가 나타나게 되어 있다. 팝업메뉴를 생성하고, TrackPopupMenu 함수를 호출해서 해당 팝업메뉴를 화면에 출력한다. 순서는 다음과 같다.

  1. CreatePopupMenu 함수를 호출해서 pop-up menu를 생성한다. 이 함수는 빈메뉴를 생성하고, 메뉴의 핸들을 반환한다.
  2. AppendMenu 함수, InsertMenu 함수, InsertMenuItem 함수중의 하나를 사용해서 메뉴 아이템을 추가한다.
  3. 마우스 커서로 pop-up menu를 표시하고 싶다면, GetCursorPos 함수를 호출해서 마우스 커서의 화면 좌표값을 얻고, GetCursorPos 함수를 호출해서 메뉴를 화면에 표시한다.
    유저가 pop-up menu로부터 메뉴를 선택하면, Windows는 보통의 메뉴와 같이 WM_COMMAND 메세지를 윈도우 프로시저에 전송한다.

※  트레이 아이콘에서 pop-up menu를 사용할 때는, 2가지의 귀찮은 문제를 조심해야 한다

  1. pop-up menu가 표시될때, 메뉴밖의 영역을 클릭해도 pop-up menu는 사라지지 않는다. 이것은, pop-up menu로부터 통지신호를 받는 윈도우는 fore-ground여야만 하기 때문이다. 그래서 이것을 피하기 위해서, SetForegroundWindow 함수를 호출한다.
  2. SetForegroundWindow 함수를 호출한 후, pop-up menu가 처음으로 표시될때는 아무런 문제가 없지만, 두번째부터 pop-up menu가 표시될때는, 바로 닫혀버리게 된다. 이것은 「의도적」인 동작이라고 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:

이 프로그램은 윈도우를 표시한 후, 최소화 버튼을 누르면, 윈도우를 숨기고 시스템 트레이에 아이콘을 저장한다. 아이콘을 더블 클릭 하면, 프로그램은 윈도우를 복원시키고 시스템 트레이에서 아이콘을 제거한다. 오른쪽 버튼을 클릭하면, pop-up menu를 표시하고, 프로그램을 종료할지 또는 윈도우를 복원하는지 등을 선택할 수 있다.

.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

메인 윈도우가 생성될때, pop-up menu를 생성해서 2개의 메뉴를 추가한다. AppendMenu 함수가 사용되고 다음과 같다.

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

  • hMenu
    아이템을 추가하려는 메뉴의 핸들
  • uFlags
    Windows에 추가하는 아이템이 비트맵이나 문자열일 경우 유효,무효,비활성등의 상태를 지정하기 위한 플래그로서, 자세한 것은 Win32API 레퍼런스를 참고하기 바란다. 예제에서는, MF_STRING를 사용해서, 메뉴 아이템이 문자열임을 지정하고 있다.
  • uIDNewItem
    메뉴 아이템의 ID. 메뉴 아이템을 나타내기 위해 사용되는 유저가 정의할 수 있는 값.
  • lpNewItem
    uFlags에 어떤 내용이 설정된지에 따라 내용이 틀려지지만, 메뉴 아이템의 내용을 지정한다. 예제에서는 MF_STRING를 지정했으므로, lpNewItem은 pop-up menu에 표시되는 문자열의 포인터이다.

pop-up menu가 생성되면, 메인 윈도우는 유저가 최소화 버튼을 누를 때까지 계속 대기한다.
최소화 버튼이 눌려지고, 윈도우가 최소화되면, 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는 소스코드의 처음에 정의되어 있는 단순한 정수이다. 이 값은 어떤 값이라도 상관없다.예제에서는 트레이 아이콘이 한개 밖에 없기 때문에 별로 중요하지 않지만, 시스템 트레이에 여러개의 아이콘을 저장하는 경우에는 각각의 아이콘을 구별하는 값이 저장되게 된다.

그리고, 아이콘, 메세지, 툴팁 텍스트를 사용하므로, uFlags에 모든 플래그를 설정한다. WM_SHELLNOTIFY 메세지는 WM_USER+5 라고 정의된 단순한 형태의 메세지이다. 값 자체는 특이하지도 않고 중요하지도 않다. 예제에서는 트레이 아이콘으로서 winlogo 아이콘을 사용했지만, 어떠 아이콘이라도 사용할 수 있다. LoadIcon 함수를 사용해서 리소스로부터 아이콘을 로드하고, 반환값을 hIcon에 설정한다. 끝으로, 마우스가 아이콘과 겹쳐질때 표시되는 szTip에 문자열을 설정 한다.(툴팁문자열)

「최소화하면 시스템트레이 아이콘으로 표시」되는 처리를 하기 위해서 메인 윈도우를 숨긴다. 다음으로, 시스템 트레이에 아이콘을 추가하기 위해서 NIM_ADD 메세지를 설정해서 Shell_NotifyIcon 함수를 호출한다.

이로인해, 메인 윈도우는 숨겨지고, 아이콘이 시스템 트레이에 저장된다. 만약 마우스 커서를 아이콘에 가져다 대면, 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 함수를 호출해서 마우스 커서의 화면 좌표값을 얻는다. 이때 반환값은, 마우스 커서의 화면 좌표값이 설정되어 있는 POINT 구조체이다. 화면 좌표값이란, 윈도우와 관계없는, 완전한 화면영역의 좌표값이다. 예를 들어, 해상도가640*480라면, 우하단의 좌표값은 (639,479)가 된다. 이 화면 좌표값을 각 각의 윈도우의 좌표값으로 변경할려면 , ScreenToClient 함수를 사용한다.

하지만, 이번장의 목적은, TrackPopupMenu 함수를 호출해서 마우스 커서의 위치에 pop-up menu를 표시하는 것이므로, 별도로 윈도우 좌표값으로 변환할 필요는 없고, 직접 GetCursorPos 함수로 설정된 화면 좌표값을 사용하면 된다.

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

  • hMenu
    표시시키고자 하는 pop-up menu의 핸들
  • uFlags
    함수의 옵션을 지정한다. 이 플래그로 인해 마우스가 눌렸을 때의 좌표값을, pop-up menu를 어떤 위치에 표시하는지의 관계를 지정한다.
    예제에서는, TPM_RIGHTALIGN를 사용해서, pop-up menu의 표시위치를 클릭한 x좌표에 pop-up menu의 좌상단이 되도록 하고 있다.
  • x and y
    메뉴가 표시될 화면 좌표값을 지정한다
  • nReserved
    NULL로 설정해야 한다
  • hWnd
    메뉴로부터 메세지를 받게되는 윈도우의 핸들
  • prcRect
    메뉴를 제거하지 않고 클릭할 되도록 하는 화면상의 영역.보통 이값은 NULL로 설정하며 유저가 pop-up menu 외의 영역을 클릭했을 때 메뉴가 제거된다.

유저가 트레이 아이콘을 더블 클릭 했다면, pop-up menu의 「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 함수를 호출해서 트레이 아이콘을 제거하지만, 이번에는 NIM_DELETE를 설정한다. 다음으로, 메인 윈도우를 원래상태로 복원한다. 유저가 Exit 메뉴를 선택하면, 트레이 아이콘은 물론 제거해야 하고, DestroyWindow 함수를 호출해서 메인 윈도우 또한 제거한다.


Posted by openserver