さんだすメモ

さメモ

技術ブログでは、ない・・・

ウィンドウの周りを塗りつぶす[C]

はじめに

PCゲームをするとき、ウィンドウを全画面にすると解像度が下がるのが嫌。
かといって、そのままプレイすると周りの画面が気になるから、対象のウィンドウの周りを真っ黒にしたい。

結構前にこのようなツイートを見たんですが、個人的に興味があり、手が届きそうなレベルだと思ったので作ってみました。ウィンドウの大きさを変えずに"全画面化"する操作を疑似全画面化と呼ぶことにします(わかりやすい名前が思いつかない)。大まかな流れとしては、対象のウィンドウを選択し、そのウィンドウの周りに黒いウィンドウを貼り付けることで実現しました(とても原始的で不格好ですが)。

使用言語はCで、APIについては主に標準 Windows APIで勉強しました。

ソースコードおよび実行ファイル

Github.com

処理の流れ

  • 対象ウィンドウの選択
  • 対象ウィンドウの上下左右を4つの黒いウィンドウで覆う(疑似全画面化)
  • 対象ウィンドウが移動・終了するか、Escが押されたら終了

対象ウィンドウの選択

LRESULT CALLBACK WndProcFirst(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp) {
  
  extern HWND hWndTarget;
  extern RECT rectClientTarget;
  extern RECT rectWindowTarget;
  extern POINT pointOriginTarget;
  
  switch (msg) {
    case WM_DESTROY: {
      PostQuitMessage(0);
      return 0;
    }
    case WM_KEYDOWN: {
      if (wp == VK_ESCAPE) PostMessage(hWnd, WM_CLOSE, 0, 0);
      return 0;
    }
    case WM_KILLFOCUS: {
      hWndTarget = GetForegroundWindow();
      GetClientRect(hWndTarget, &rectClientTarget); // 幅、高さ
      GetWindowRect(hWndTarget, &rectWindowTarget); // ウィンドウのスクリーン座標
      pointOriginTarget.x = (LONG)0;
      pointOriginTarget.y = (LONG)0;
      ClientToScreen(hWndTarget, &pointOriginTarget); // クライアント領域の左上の点のスクリーン座標
      SendMessage(hWnd, WM_CLOSE, 0, 0);
      return 0;
    }
  }
  return DefWindowProc(hWnd, msg, wp, lp);
}

対象ウィンドウが選択され、WM_KILLFOCUSを受け取ったら、hWndTarget = GetForegroundWindow()で対象ウィンドウのハンドルを取得します。
rectClientTargetpointOriginTargetは周りに黒いウィンドウを表示するときに使い、rectWindowTargetは対象ウィンドウが移動したことを検知するために使います。

対象ウィンドウの上下左右を黒いウィンドウで覆う(疑似全画面化)

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR lpCmdLine, int nCmdShow) {
  
  ...
  
  RECT rectDesk;
  HWND hWndDesk = GetDesktopWindow();
  GetWindowRect(hWndDesk, &rectDesk);
  
  hWndSecondBackground = CreateWindow( // 対象ウィンドウの後ろにウィンドウを敷いておく
    WINDOW_SECOND, TEXT("windowSecondBackground"), WS_POPUP | WS_VISIBLE,
    rectDesk.left,
    rectDesk.top,
    rectDesk.right + 1,
    rectDesk.bottom + 1,
    NULL, NULL, hInstance, NULL
  );
  if (hWndSecondBackground == NULL) return -1;
  SetWindowPos(hWndSecondBackground, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
  
  hWndSecondTop = CreateWindow( // 上側のウィンドウ
    WINDOW_SECOND, TEXT("windowSecondTop"), WS_POPUP | WS_VISIBLE,
    rectDesk.left,
    rectDesk.top,
    rectDesk.right + 1,
    pointOriginTarget.y + 1,
    NULL, NULL, hInstance, NULL
  );
  hWndSecondLeft = CreateWindow( // 左側のウィンドウ
    WINDOW_SECOND, TEXT("windowSecondLeft"), WS_POPUP | WS_VISIBLE,
    rectDesk.left,
    rectDesk.top,
    pointOriginTarget.x + 1,
    rectDesk.bottom + 1,
    NULL, NULL, hInstance, NULL
  );
  hWndSecondBottom = CreateWindow( // 下側のウィンドウ
    WINDOW_SECOND, TEXT("windowSecondBottom"), WS_POPUP | WS_VISIBLE,
    rectDesk.left,
    pointOriginTarget.y + rectClientTarget.bottom - 1,
    rectDesk.right + 1,
    rectDesk.bottom - pointOriginTarget.y - rectClientTarget.bottom + 2,
    NULL, NULL, hInstance, NULL
  );
  hWndSecondRight = CreateWindow( // 右側のウィンドウ
    WINDOW_SECOND, TEXT("windowSecondRight"), WS_POPUP | WS_VISIBLE,
    pointOriginTarget.x + rectClientTarget.right - 1,
    rectDesk.top,
    rectDesk.right - pointOriginTarget.x - rectClientTarget.right + 2,
    rectDesk.bottom + 1,
    NULL, NULL, hInstance, NULL
  );
  if (hWndSecondTop == NULL) return -1;
  if (hWndSecondLeft == NULL) return -1;
  if (hWndSecondBottom == NULL) return -1;
  if (hWndSecondRight == NULL) return -1;
  SetWindowPos(hWndSecondTop, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
  SetWindowPos(hWndSecondLeft, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
  SetWindowPos(hWndSecondBottom, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
  SetWindowPos(hWndSecondRight, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
  
  ...
  
}

WinMain関数内で作業します。対象ウィンドウの後ろにウィンドウを作っているのは、対象ウィンドウが先に終了してしまったときに見栄えが悪くなるのを防ぐためで、本質的に必要ではありません。
pointOriginTarget.x + 1などで+1しているのは、対象ウィンドウの境界部分にマウスカーソルが合わないようにするためです(サイズ変更ができてしまう)。

対象ウィンドウが移動・終了するか、Escが押されたら終了

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR lpCmdLine, int nCmdShow) {
  
  ...
  
  dwThreadId = GetCurrentThreadId(); // このプロセスのスレッドid
  dwThreadIdTarget = GetWindowThreadProcessId(hWndTarget, NULL); // 対象ウィンドウのスレッドid
  if (dwThreadId != dwThreadIdTarget) {
    if (!AttachThreadInput(dwThreadId, dwThreadIdTarget, TRUE)) return -1;
  }
  
  SetActiveWindow(hWndTarget); // 使いやすさのため、対象ウィンドウを最前面・アクティブにする。
  
  while (TRUE) { // メインループ
    if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { // hWndSecondのどれかがメッセージを受け取っているとき
      if (msg.message == WM_QUIT) break;
      TranslateMessage(&msg);
      DispatchMessage(&msg);
    }
    else {
      if (GetKeyState(VK_ESCAPE) < 0) {
        flag = TRUE; // Escが押されているとき
      }
      else if (GetWindowRect(hWndTarget, &rectWindowTargetBuffer)) {
        if (
          rectWindowTargetBuffer.left == rectWindowTarget.left &&
          rectWindowTargetBuffer.top == rectWindowTarget.top &&
          rectWindowTargetBuffer.right == rectWindowTarget.right &&
          rectWindowTargetBuffer.bottom == rectWindowTarget.bottom
        ) flag = FALSE; // 対象ウィンドウが移動していないとき
        else flag = TRUE; // 対象ウィンドウが移動したとき
      }
      else flag = TRUE; // ウィンドウがないとき
      if (flag == TRUE) {
        AttachThreadInput(dwThreadId, dwThreadIdTarget, FALSE);
        PostQuitMessage(0);
      }
    }
  }
  
  ...
  
}

対象ウィンドウでEscが入力されたかどうかは、GetKeyState(VK_ESCAPE)が負であるかそうでないかで調べることができます。ただし、対象ウィンドウのスレッドidを現在のスレッドidにアタッチしておく必要があります。
また、イベントを処理するループにGetMessageを使うと、hWndSecondのどれかがメッセージを受け取るまで待機するので、終了条件を満たしていても、条件の処理がされないことがあります。そのため、イベントループにはPeekMessageを使っています。

最後に

うまく実行されない例

対象ウィンドウを選ぶ際に、タスクマネージャーやMicrosoft Edgeエクスプローラのファイル部分を選択すると、疑似全画面化せずに終了します。理由はAttachThreadIdが失敗しているためだと思います。

追加してみたいこと

対象ウィンドウを選択する際に、カーソルのあったウィンドウを強調する機能。Snipping Toolのような感じです。

疑似全画面化の別の案

「一部が透明のウィンドウを作り、透明領域に送られたメッセージを対象ウィンドウに送る」ことができたらいいと思いました(かっこいい)。メッセージを送る操作がうまくできずにつまづいています。

その他

他のウィンドウで入力されたキーを取得しても、GetMessageだとメッセージループが回っていないということにはなかなか気づきませんでした。