ウィンドウモードの切り替え|ボーダーレスフルスクリーンをWinAPIで実装する

window mode thumbnail ゲーム制作

「ゲームの設定画面にある『フルスクリーン』『ウィンドウ』の切り替え、中身はどうなってるの?」「ボーダーレスフルスクリーンって普通のフルスクリーンと何が違うの?」「WinAPIだけでできるの?」

こんな疑問はありませんか?

今回は、Windowsゲームの設定画面でよく見るウィンドウモードの切り替え(ウィンドウ⇔ボーダーレスフルスクリーン)を、実際にWin32アプリを書いて動かしながら解説していきます。

ボーダーレスフルスクリーンとは?

ウィンドウモードには、大きく分けて次の3種類があります。

ウィンドウモード…タイトルバー・枠つきの普通のウィンドウ
排他的フルスクリーン…OSの表示解像度そのものを切り替える昔ながらの全画面
ボーダーレスフルスクリーンタイトルバーと枠だけを外し、モニタと同じサイズ・位置に置いた「見た目だけ全画面のウィンドウ」

今回扱うのは3つ目のボーダーレスフルスクリーンです。実体はあくまで「ウィンドウ」なので、Alt+Tabでの切り替えが速い、配信ソフトとの相性が良いといった理由で、最近の多くのPCゲームで採用されています。

実装してみる

実際にWin32アプリを書いて、ウィンドウモードとボーダーレスフルスクリーンを切り替えるWindowManagerクラスを作ってみます。

enum class WindowMode {
    Windowed,
    BorderlessFullscreen,
};

class WindowManager {
public:
    static void Init(HWND hwnd) {
        s_Window = hwnd;
        s_CurrentMode = WindowMode::Windowed;
        GetWindowRect(s_Window, &s_WindowedRect); // ウィンドウモード時の矩形を覚えておく
    }

    static void SetMode(WindowMode mode) {
        if (mode == s_CurrentMode) return;

        if (mode == WindowMode::BorderlessFullscreen) {
            // 切り替え前の矩形を保存(Windowedに戻すときに使う)
            GetWindowRect(s_Window, &s_WindowedRect);

            // タイトルバー・枠を全部外す
            SetWindowLongPtr(s_Window, GWL_STYLE, WS_POPUP | WS_VISIBLE);

            // 今いるモニタ全体のサイズに合わせる
            HMONITOR monitor = MonitorFromWindow(s_Window, MONITOR_DEFAULTTONEAREST);
            MONITORINFO mi = { sizeof(MONITORINFO) };
            GetMonitorInfo(monitor, &mi);
            SetWindowPos(s_Window, HWND_TOP,
                mi.rcMonitor.left, mi.rcMonitor.top,
                mi.rcMonitor.right - mi.rcMonitor.left,
                mi.rcMonitor.bottom - mi.rcMonitor.top,
                SWP_FRAMECHANGED);
        } else {
            // タイトルバー付きのスタイルに戻す
            SetWindowLongPtr(s_Window, GWL_STYLE, WS_OVERLAPPEDWINDOW | WS_VISIBLE);
            SetWindowPos(s_Window, HWND_TOP,
                s_WindowedRect.left, s_WindowedRect.top,
                s_WindowedRect.right - s_WindowedRect.left,
                s_WindowedRect.bottom - s_WindowedRect.top,
                SWP_FRAMECHANGED);
        }

        s_CurrentMode = mode;
    }

    static WindowMode GetMode() { return s_CurrentMode; }

private:
    static HWND       s_Window;
    static WindowMode s_CurrentMode;
    static RECT       s_WindowedRect;
};

ポイントはSetWindowLongPtr(hwnd, GWL_STYLE, ...)です。WS_POPUPスタイルにすると、タイトルバー・枠・システムメニューが全部消えます。その状態でモニタ全体のサイズ・位置にウィンドウを合わせることで、見た目上は全画面と区別がつかなくなります。

実際に960×540のウィンドウで起動し、2秒後に自動でボーダーレスフルスクリーンへ切り替えるデモを作って実行しました。切り替え前後の実際のウィンドウ矩形をログに出しています。

start: mode=Windowed rect=(200,150)-(1160,690) size=960x540
timer fired: switched to BorderlessFullscreen
after: mode=BorderlessFullscreen rect=(0,0)-(1920,1080) size=1920x1080

切り替え前は指定通り960×540のウィンドウでしたが、切り替え後はモニタの解像度そのもの(1920×1080)、原点(0,0)にぴったり一致しているのが分かります。実際にスクリーンショットで見比べると一目瞭然です。

ウィンドウモード時のスクリーンショット。タイトルバーが表示されている
ウィンドウモード:タイトルバーと枠がある
ボーダーレスフルスクリーン時のスクリーンショット。タイトルバーもタスクバーも無い
ボーダーレスフルスクリーン:タイトルバー・タスクバーが完全に消えている

ボーダーレス側は、タイトルバーはもちろん、下のタスクバーまで完全に隠れています。実体はウィンドウのままですが、画面全体を専有しているのが分かります。

【重要】記事のスクショを撮っていて実際にハマった罠:DPI仮想化

この記事のスクリーンショットを撮る過程で、地味に厄介なバグを踏みました。DPI仮想化と呼ばれる、ディスプレイの拡大率が100%以外の環境で起きる問題です。

ウィンドウの位置とサイズをキャプチャ用のスクリプトで取得しようとしたところ、GetWindowRectが返す座標と、実際に画面をキャプチャしたときの物理ピクセルの位置がズレて、切り抜いた画像に隣のウィンドウが写り込んでしまいました。

原因は、アプリがDPI対応を宣言していなかったため、Windowsが座標を「仮想化」して、実際のディスプレイ拡大率とは違う縮尺で報告していたことでした。125%拡大の環境だと、アプリが「960×540」で作ったつもりのウィンドウが、別のプロセスからは768×432(960÷1.25)のように違うサイズで見えてしまいます。

対処法は、ウィンドウを作る前に1行呼ぶだけです。

// DPI仮想化を無効化する(これが無いと、拡大率が100%以外の環境で
// GetWindowRectの座標とスクリーンキャプチャの物理ピクセルがズレる)
SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);

これをウィンドウ作成前に呼んでおくだけで、アプリは「自分はDPI対応済みです」とOSに申告したことになり、以降は物理ピクセルそのままの座標でやり取りできるようになります。ゲームの設定画面を作るときも、この宣言を忘れると「フルスクリーンにしたのに画面の一部しか映らない」のような不具合につながるため、意外と見落とせないポイントです。

ウィンドウモード切り替えのよくある失敗例と対処法

①DPI対応を宣言し忘れる

上で実演した通りです。SetProcessDpiAwarenessContextをウィンドウ作成前に呼ぶだけで解決します。

②切り替え前の矩形を保存し忘れる

ボーダーレスに切り替えるs_WindowedRectを保存しておかないと、ウィンドウモードに戻すときにどのサイズ・位置に戻せばいいか分からなくなります。切り替え関数の中で、切り替える直前に必ず保存しましょう。

③SWP_FRAMECHANGEDを付け忘れる

SetWindowLongPtrでスタイルを変えただけでは、見た目にすぐ反映されないことがあります。SetWindowPosSWP_FRAMECHANGEDフラグを付けて呼ぶことで、変更後のスタイルを確実に再描画させられます。

注意点

  • ウィンドウ作成前にDPI対応を宣言する
  • ボーダーレスへの切り替え直前に元のウィンドウ矩形を保存する
  • スタイル変更後はSWP_FRAMECHANGEDを付けて反映させる
  • マルチモニタ環境ではMonitorFromWindowで「今いるモニタ」を正しく取得する

まとめ

  • ボーダーレスフルスクリーンはタイトルバー・枠を外してモニタ全体に合わせたウィンドウ
  • WS_POPUP + SetWindowPosで実装できる
  • DPI仮想化を宣言し忘れると、座標系がズレて様々な不具合につながる(実際にハマった)
  • 対処法はSetProcessDpiAwarenessContextを1行呼ぶだけ

ウィンドウモードの切り替え自体はWinAPIの基本的な組み合わせだけで作れますが、DPIまわりは実際に複数の環境で確認しないと気づきにくい落とし穴でした。

ここまで読んでくださりありがとうございました。

関連記事