Qtで実現するWindowsネイティブ機能対応のフルフレームレスウィンドウ

QtにおけるWindowsネイティブ機能対応のフレームレスウィンドウ実装

Qtアプリケーションで、Windowsのネイティブなタイトルバーや枠線を完全に除去しつつ、同時にサイズ変更、最大化、最小化、そしてDWM (Desktop Window Manager) による美しいシャドウといったOS本来のウィンドウ機能を保持する方法について解説します。

標準のQt::FramelessWindowHintを使用すると、多くの場合、ウィンドウの端にわずかなシステム境界が残るか、サイズ変更機能が失われることがあります。本記事では、Windows APIメッセージを直接処理することで、この問題を解決し、完全にシームレスなユーザーエクスペリエンスを提供します。

実装の概要

カスタムウィジェットを作成し、そのnativeEventメソッド内でWindowsメッセージを傍受します。これにより、ウィンドウの非クライアント領域の描画とヒットテストを制御し、必要な機能を再構築します。

ヘッダーファイル (.h)

まず、カスタムフレームレスウィンドウの基本クラスを定義します。Windows APIを使用するため、windows.hおよびDWM機能のためのdwmapi.hをインクルードします。

#ifndef CUSTOM_FRAMELESS_WINDOW_H
#define CUSTOM_FRAMELESS_WINDOW_H

#include <QWidget>
#include <windows.h> // HWND, MSGなど
#include <dwmapi.h>  // DwmExtendFrameIntoClientArea

class CustomFramelessWindow : public QWidget
{
    Q_OBJECT

public:
    explicit CustomFramelessWindow(QWidget *parent = nullptr);

    // ウィンドウタイトルバーとして機能させるウィジェットを設定
    void setDraggableTitleBar(QWidget *titleBarWidget);

protected:
    // ネイティブイベント処理をオーバーライド
    bool nativeEvent(const QByteArray &eventType, void *message, qintptr *result) override;

private:
    int m_resizeBorderWidth; // リサイズ領域の幅
    HWND m_hWnd; // ウィンドウのネイティブハンドル
    QWidget *m_titleBarWidget; // ドラッグ可能なタイトルバーとして指定されたウィジェット

    // システム電源イベント処理
    void handlePowerBroadcast(MSG *msg);
};

#endif // CUSTOM_FRAMELESS_WINDOW_H

プロジェクトファイル (.pro) の設定

Windows APIライブラリをリンクするために、.proファイルに以下の行を追加する必要があります。

LIBS += -ldwmapi -luser32

ソースファイル (.cpp)

次に、CustomFramelessWindowクラスの具体的な実装です。

#include "customframelesswindow.h"
#include <QScreen>
#include <QGuiApplication>
#include <QDebug>
#include <windowsx.h> // GET_X_LPARAM, GET_Y_LPARAM

CustomFramelessWindow::CustomFramelessWindow(QWidget *parent)
    : QWidget(parent)
    , m_resizeBorderWidth(8) // リサイズ領域のデフォルト幅 (ピクセル単位)
    , m_titleBarWidget(nullptr)
{
    // Qtのフレームレスヒントを設定し、システムによる枠の描画を無効化
    setWindowFlags(windowFlags() | Qt::FramelessWindowHint);

    // ネイティブウィンドウハンドルを取得
    m_hWnd = reinterpret_cast<HWND>(winId());

    // ウィンドウのスタイルを変更して、システムによるリサイズ、最大化、およびスナップ機能を有効にする
    // WS_CAPTIONは視覚的には表示されないが、OSレベルのウィンドウ管理機能 (例: Aero Snap) に必須
    DWORD currentStyle = ::GetWindowLong(m_hWnd, GWL_STYLE);
    ::SetWindowLong(m_hWnd, GWL_STYLE, currentStyle | WS_MAXIMIZEBOX | WS_THICKFRAME | WS_CAPTION);

    // DWMを使用してウィンドウの影を有効にする
    // 全面をクライアント領域として拡張することで、OSが影を描画するようになる
    MARGINS margins = { -1, -1, -1, -1 };
    DwmExtendFrameIntoClientArea(m_hWnd, &margins);
}

void CustomFramelessWindow::setDraggableTitleBar(QWidget *titleBarWidget)
{
    m_titleBarWidget = titleBarWidget;
    // 必要であれば、タイトルバーウィジェットにイベントフィルターをインストールして、
    // ウィンドウの移動などの特殊な挙動を制御できます。
    // 例: m_titleBarWidget->installEventFilter(this);
}

// システム電源イベントの処理
void CustomFramelessWindow::handlePowerBroadcast(MSG *msg)
{
    if (msg->wParam == PBT_APMSUSPEND) {
        // システムがサスペンドするとき、ウィンドウを最小化
        // これにより、潜在的な描画や状態の問題を回避できる場合がある
        showMinimized();
    } else if (msg->wParam == PBT_APMRESUMEAUTOMATIC) {
        // システムがレジュームするとき、ウィンドウを通常状態に戻す
        showNormal();
    }
}

bool CustomFramelessWindow::nativeEvent(const QByteArray &eventType, void *message, qintptr *result)
{
    // Windowsメッセージのみを処理
    if (eventType == "windows_generic_MSG")
    {
        MSG *msg = static_cast<MSG *>(message);

        switch (msg->message)
        {
        case WM_NCCALCSIZE:
            // 非クライアント領域の計算を抑制し、クライアント領域がウィンドウ全体を占めるようにする
            // これにより、標準のタイトルバーと枠線が除去される
            *result = 0;
            return true;

        case WM_NCHITTEST:
        {
            // マウスカーソルのスクリーン座標を取得
            long globalX = GET_X_LPARAM(msg->lParam);
            long globalY = GET_Y_LPARAM(msg->lParam);
            QPoint globalPos(globalX, globalY);

            // DPIスケーリングを考慮して、ウィジェットのローカル座標に変換
            // Windowsのカーソル位置は物理ピクセルであるため、論理ピクセルに変換する必要がある
            qreal scaleFactor = QGuiApplication::primaryScreen()->devicePixelRatio();
            QPoint localPos = mapFromGlobal(globalPos / scaleFactor);

            // ウィンドウの各辺と角がリサイズ可能領域内にあるかを判定
            bool nearLeft = localPos.x() < m_resizeBorderWidth;
            bool nearRight = localPos.x() > width() - m_resizeBorderWidth;
            bool nearTop = localPos.y() < m_resizeBorderWidth;
            bool nearBottom = localPos.y() > height() - m_resizeBorderWidth;

            *result = 0; // デフォルトではヒットしない

            // まずリサイズ境界のヒットテストを行う
            if (nearLeft && nearTop) {
                *result = HTTOPLEFT;
            } else if (nearRight && nearTop) {
                *result = HTTOPRIGHT;
            } else if (nearLeft && nearBottom) {
                *result = HTBOTTOMLEFT;
            } else if (nearRight && nearBottom) {
                *result = HTBOTTOMRIGHT;
            } else if (nearLeft) {
                *result = HTLEFT;
            } else if (nearRight) {
                *result = HTRIGHT;
            } else if (nearTop) {
                *result = HTTOP;
            } else if (nearBottom) {
                *result = HTBOTTOM;
            }

            // リサイズ領域にヒットした場合は、ここで処理を終了してWindowsに指示
            if (*result != 0) {
                return true;
            }

            // カスタムタイトルバー領域のドラッグ判定
            if (m_titleBarWidget && m_titleBarWidget->geometry().contains(localPos)) {
                // タイトルバー内の子ウィジェットを考慮し、子ウィジェットがなければドラッグ可能
                QWidget *childAtCursor = m_titleBarWidget->childAt(localPos);
                if (!childAtCursor) {
                    *result = HTCAPTION; // タイトルバーとして認識させ、ウィンドウの移動を可能にする
                    return true;
                }
            }
            break; // 他のメッセージ処理へ
        }
        case WM_POWERBROADCAST:
            handlePowerBroadcast(msg);
            break;

        case WM_SYSKEYDOWN:
        case WM_SYSKEYUP:
            // Altキー押下時のシステムメニュー表示などを抑制したい場合はここにロジックを追加
            // 現状ではデフォルトの動作を維持するか、必要に応じてtrueを返して抑制
            break;

        default:
            break;
        }
    }
    // 未処理のイベントは基底クラスに渡してデフォルト処理を行う
    return QWidget::nativeEvent(eventType, message, result);
}

補足事項

  • この実装では、DPIスケーリングを考慮に入れています。特にWM_NCHITTESTの処理では、QGuiApplication::primaryScreen()->devicePixelRatio()を使用して、マウス座標を正しく変換することが重要です。
  • SetWindowLongでWS_MAXIMIZEBOX、WS_THICKFRAME、WS_CAPTIONを設定することで、視覚的なフレームがなくてもWindowsの「スナップ」機能(画面端へのドラッグで最大化や分割表示)が機能するようになります。
  • ウィンドウの非表示 (hide()) 後に再度表示 (show()) した際に、DWMの影が消える問題が発生することがあります。その場合は、show()を呼び出した後にコンストラクタ内で実行したDWM関連のAPI呼び出し(SetWindowLongとDwmExtendFrameIntoClientArea)を再度実行する必要があるかもしれません。

このアプローチにより、QtアプリケーションはWindowsのネイティブなフレームレスウィンドウとしての完全な機能と外観を持つことができます。これにより、OSとの統合感が向上し、より洗練されたユーザーインターフェースデザインが可能になります。

タグ: Qt Windows API Frameless Window DWM WM_NCCALCSIZE

10月3日 08:26 投稿