2019年7月10日水曜日

キャプチャソフト開発 その2 タスクトレイに常駐するモックを作る

ブラウザやメモ帳のような普通のプログラムとは違い、今回作りたいキャプチャソフトはタスクトレイ常駐型のプログラムである。

タスクトレイは上三角印を押すと表示される。


本ページでは以下の機能の確認を行う。
  • 不可視のウィンドウを作成する
  • タスクトレイにプログラムのアイコンを表示する
  • アイコンを右クリックでメニューを表示し、項目が選択されたことを知る

実装にあたり、主に以下のページを参考にした。



ソースファイル
#include <stdio.h>
#include <strsafe.h>
#include <wchar.h>
#include <windows.h>
#include <windowsx.h>
#include "./resource.h"
#include "./util.h"

namespace {
constexpr wchar_t WINDOW_NAME[] = L"Template window";
constexpr wchar_t CLASS_NAME[] = L"Template class";
constexpr int TASKTRAY_ICONID = 1;
}  // namespace

BOOL Cls_OnCreate(HWND hwnd, LPCREATESTRUCT lpCreateStruct) {
  (void)hwnd;
  (void)lpCreateStruct;

  HINSTANCE hInstance = (HINSTANCE)GetWindowLong(hwnd, GWL_HINSTANCE);

  // Add task tray icon.
  NOTIFYICONDATA nid;
  ZeroMemory(&nid, sizeof(nid));
  nid.cbSize = sizeof(nid);
  nid.hWnd = hwnd;
  nid.uID = TASKTRAY_ICONID;
  nid.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP;
  nid.uCallbackMessage = WM_TASKTRAY;
  nid.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_ICON1));
  StringCchCopy(nid.szTip, ARRAYSIZE(nid.szTip), L"Task tray test");

  if (Shell_NotifyIcon(NIM_ADD, &nid) != TRUE) {
#ifdef DEBUG
    fwprintf(stderr, L"Failed to add an icon on task tray!\n");
#endif
    return FALSE;
  }
  return TRUE;
}

void Cls_OnDestroy(HWND hwnd) {
  (void)hwnd;

  // Remove task tray icon.
  NOTIFYICONDATA nid;
  ZeroMemory(&nid, sizeof(nid));
  nid.cbSize = sizeof(nid);
  nid.hWnd = hwnd;
  nid.uID = TASKTRAY_ICONID;
  nid.uFlags = 0;
  Shell_NotifyIcon(NIM_DELETE, &nid);

  PostQuitMessage(0);
}

void Cls_OnClose(HWND hwnd) { DestroyWindow(hwnd); }

void Cls_OnCommand(HWND hwnd, int id, HWND hWndCtl, UINT codeNotify) {
  (void)hwnd;
  (void)id;
  (void)hWndCtl;
  (void)codeNotify;

  switch (id) {
    case IDM_FOLDER:
#ifdef DEBUG
      fwprintf(stderr, L"Folder is clicked\n");
#endif
      break;
    case IDM_QUIT:
#ifdef DEBUG
      fwprintf(stderr, L"Quit is clicked\n");
#endif
      break;
    default:
      break;
  }
}

void Cls_OnTaskTray(HWND hwnd, UINT id, UINT uMsg) {
  (void)hwnd;

  if (id != TASKTRAY_ICONID) {
    return;
  }
  switch (uMsg) {
    case WM_RBUTTONDOWN: {
      // Display menu when right button is clicked on task tray icon.
      POINT point;
      GetCursorPos(&point);
      HINSTANCE hInstance = (HINSTANCE)GetWindowLong(hwnd, GWL_HINSTANCE);
      HMENU hMenu = LoadMenu(hInstance, MAKEINTRESOURCE(IDR_MENU1));
      HMENU hSubMenu = GetSubMenu(hMenu, 0);
      SetForegroundWindow(hwnd);
      TrackPopupMenu(hSubMenu, TPM_LEFTALIGN | TPM_BOTTOMALIGN, point.x,
                     point.y, 0, hwnd, NULL);
      DestroyMenu(hMenu);
      PostMessage(hwnd, WM_NULL, 0, 0);
    } break;
    default:
      break;
  }
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam,
                         LPARAM lParam) {
  switch (message) {
    HANDLE_MSG(hwnd, WM_CREATE, Cls_OnCreate);
    HANDLE_MSG(hwnd, WM_DESTROY, Cls_OnDestroy);
    HANDLE_MSG(hwnd, WM_COMMAND, Cls_OnCommand);
    HANDLE_MSG(hwnd, WM_CLOSE, Cls_OnClose);
    HANDLE_MSG(hwnd, WM_TASKTRAY, Cls_OnTaskTray);
    default:
      return DefWindowProc(hwnd, message, wParam, lParam);
  }
}

int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    LPTSTR lpsCmdLine, int nCmdShow) {
  (void)hPrevInstance;
  (void)lpsCmdLine;

#ifdef DEBUG
  FILE* fp = nullptr;
  AllocConsole();
  _wfreopen_s(&fp, L"CONOUT$", L"w", stdout);
  _wfreopen_s(&fp, L"CONOUT$", L"w", stderr);
  _wfreopen_s(&fp, L"CONIN$", L"r", stdin);
#endif

#ifdef DEBUG
  fwprintf(stdout, L"Hello world to stdout!\n");
  fwprintf(stderr, L"Hello world to stderr!\n");
#endif

  WNDCLASS wc;
  wc.style = CS_HREDRAW | CS_VREDRAW;
  wc.lpfnWndProc = WndProc;
  wc.cbClsExtra = 0;
  wc.cbWndExtra = 0;
  wc.hInstance = hInstance;
  wc.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_ICON1));
  wc.hCursor = LoadCursor(NULL, IDC_ARROW);
  wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
  wc.lpszMenuName = NULL;
  wc.lpszClassName = CLASS_NAME;
  if (!RegisterClass(&wc)) {
    return FALSE;
  }

  HWND hWnd = CreateWindow(CLASS_NAME, WINDOW_NAME, WS_DISABLED, CW_USEDEFAULT,
                           CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL,
                           NULL, hInstance, NULL);
  if (hWnd == NULL) {
    return FALSE;
  }

  MSG msg;
  while (GetMessage(&msg, NULL, 0, 0) > 0) {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }

#ifdef DEBUG
  FreeConsole();
#endif
  return 0;
}

(2019/9/10 追記)
Notification iconからポップアップメニューを表示する場合、先にSetForegroundWindow()を呼んでおかないと、マウスカーソルがよそに行ってもメニューが消えなない不具合が発生する。また、現在のウィンドウがフォアグラウンドウインドウだと、先に強制的にタスクスイッチを切り替えておかないと、次回以降のメニューの表示で一瞬ポップアップメニューが出てすぐに消えてしまう不具合が発生するらしい(参考 TrackPopupMenu function)。


ヘッダファイル
#include <wchar.h>

#ifndef _UTIL_H_
#define _UTIL_H_


#include <wchar.h>
#include <windows.h>
#include <windowsx.h>

// Application-defined message identifiers.
#define WM_TASKTRAY (WM_APP + 1)

// void Cls_OnTaskTray(HWND hwnd, UINT id, UINT uMsg)
#define HANDLE_WM_TASKTRAY(hwnd, wParam, lParam, fn) \
  ((fn)((hwnd), (UINT)(wParam), (UINT)(lParam)), 0L)

#endif  // _UTIL_H_


ポイント
  • TASKTRAY_ICONID
    タスクトレイアイコンに任意のIDを指定する。
    今回はアイコンを一つだけタスクトレイに登録するので、IDは一つでよい。
  • Shell_NotifyIcon
    プログラムがタスクトレイにアイコンを登録する際に呼ぶ関数。
    アイコンを登録する場合には第一引数をNIM_ADD、削除する場合にはNIM_DELETEとする。
    第二引数には、アイコンに関する細かな指定をするためのNOTIFYICONDATA構造体を渡す必要がある。
    アイコンの初期化と終了の処理は、それぞれCls_OnCreateとCls_OnDestroyで行う。
  • NOTIFYICONDATA
    uIDにはTASKTRAY_ICONIDを入れる。
    uCallbackMessageには、WM_TASKTRAYを入れる(後述)。
    そのほか細かい設定ができる。
  • ZeroMemory(&nid, sizeof(nid));
    若干脱線するが、sizeofについての小ネタ。
    sizeofには型を入れるべきか、変数名を入れるべきかという疑問を実装中に抱いた。
    調べたところ、Google Style Guideによると、後の変更の可能性を考慮してsizeofには変数名を渡した方が良いという。
  • GetWindowLong
    プログラムのインスタンスハンドル(hInstance)をウィンドウハンドル(hWnd)から取得したい場合によく使う関数。
    ほかにもいろいろできるらしい。
  • Cls_OnCommand
    メニューが選択された時、WM_COMMANDメッセージが送られて来る。
    メッセージクラッカでは引数のidにメニュー項目の識別子がいれられるので、switch文で押された項目を判定できる。

    似たメッセージにWM_MENUSELECTがある。
    最初はそちらのメッセージで、どのメニュー項目が選ばれたかを知ろうとしていた。
    しかし、WM_MENUSELECTは選択中のメニュー項目が変化したこと知るためのメッセージである。
    クリックやエンターで選択された項目の取得には使えないようだ。
    ちょっとハマったので注意。
  • WS_DISABLED
    ウィンドウ作成時に、ウィンドウスタイルとしてWS_DISABLEDを指定する。
    これにより、不可視のウィンドウが作成できる。
  • WM_TASKTRAY
    タスクトレイアイコンに対する操作のメッセージは、プログラマが任意に決めることができる。
    WM_TASKTRAYはもともとのWindows APIには存在しないメッセージ定数であるが、定義しておくことで利便性を図る。
  • Cls_OnTaskTray、HANDLE_WM_TASKTRAY
    WM_TASKTRAYに対応するメッセージクラッカも自前で作成する必要がある。
    HANDLE_WM_TASKTRAYはマクロ関数である。
    実装方法はwindowsx.hの他のマクロ関数と同様。

リソースファイル
// Generated by ResEdit 1.6.6
// Copyright (C) 2006-2015
// http://www.resedit.net

#include <windows.h>
#include <commctrl.h>
#include <richedit.h>
#include "resource.h"
#pragma code_page(65001)




//
// Menu resources
//
LANGUAGE LANG_JAPANESE, SUBLANG_JAPANESE_JAPAN
IDR_MENU1 MENU
{
    POPUP "PopupMenu"
    {
        MENUITEM "Folder", IDM_FOLDER
        MENUITEM "Quit", IDM_QUIT
    }
}



//
// Icon resources
//
LANGUAGE LANG_JAPANESE, SUBLANG_JAPANESE_JAPAN
IDI_ICON1          ICON           ".\\icon.ico"


こんな感じでメニュー項目を定義する。
リソースファイルは毎度のごとくResEditで作成した。

プロジェクト全体はGitHubにあげておく

0 件のコメント:

コメントを投稿

コメント表示は承認制に設定しています