QT自繪標題和邊框


在QT中如果想要自繪標題和邊框,一般步驟是:

  1) 在創建窗口前設置Qt::FramelessWindowHint標志,設置該標志后會創建一個無標題、無邊框的窗口。

  2)在客戶區域的頂部創建一個自繪標題欄。

  3)給窗口繪制一個背景作為邊框。

  4)如果想要鼠標拖動效果,可以在WM_NCHITTEST消息中返回HTCAPTION,具體方法百度這里不再詳述。

  但是這樣做會導致一個問題:

    在win7系統上,將窗口移動到屏幕邊緣會自動排列(在屏幕頂部,左邊,右邊都會自動排列)的功能失效。

  如果你的窗口沒有這個功能,只有兩種可能:

  1)你的窗口不支持"移動到屏幕邊緣自動排列"功能。

  2)你從系統關閉了此項功能(控制面板\輕松訪問\輕松訪問中心\使任務更容易被關注\防止將窗口移動到屏幕邊緣時自動排列窗口)。

 

怎么樣才能夠既能夠自繪標題和邊框,又能夠使用屏幕自動排列功能:

  有一個windows消息能夠幫助我們,響應WM_NCCALCSIZE消息,直接返回true,就可以使客戶區域的大小和窗口大小完全一樣,這樣就沒有了標題欄和邊框,我們可以按照上面的一般步驟來自繪標題欄和邊框,唯一不同的是不需要設置Qt::FramelessWindowHint標志。

  這樣做也會有問題:

    窗口顯示的不完整,特別是在最大化的時候,非常明顯。

  為什么會顯示不完整,這個問題困擾我一整天。我新建了一個win32項目,響應WM_NCCALCSIZE消息,窗口顯示完整,應該是QT自己處理的問題,最后不斷調試QT源碼,終於明白問題所在:

  調用堆棧(從下往上看):

QWindowsWindow::frameMarginsDp() 行 1854    C++
QWindowsWindow::frameMargins() 行 188    C++
QWidgetPrivate::updateFrameStrut() 行 11824    C++
QWidget::create(unsigned int window, bool initializeWindow, bool destroyOldWindow) 行 1358    C++

  關鍵函數:

QMargins QWindowsWindow::frameMarginsDp() const
{
    // Frames are invalidated by style changes (window state, flags).
    // As they are also required for geometry calculations in resize
    // event sequences, introduce a dirty flag mechanism to be able
    // to cache results.
    if (testFlag(FrameDirty)) {
        // Always skip calculating style-dependent margins for windows claimed to be frameless.
        // This allows users to remove the margins by handling WM_NCCALCSIZE with WS_THICKFRAME set
        // to ensure Areo snap still works (QTBUG-40578).
        m_data.frame = window()->flags() & Qt::FramelessWindowHint
            ? QMargins(0, 0, 0, 0)
            : QWindowsGeometryHint::frame(style(), exStyle());
        clearFlag(FrameDirty);
    }
    return m_data.frame + m_data.customMargins;
}

  注釋里面清楚說明這是一個BUG(QTBUG-40578),我們雖然已經讓客戶區域大小和窗口大小完全一樣,但是QT還是認為系統有邊框,只有當設置了Qt::FramelessWindowHint標志,才會返回QMargins(0, 0, 0, 0)。

 

現在又回到了原點,且問題相互矛盾,想要自繪標題和邊框必須設置Qt::FramelessWindowHint標志,但是設置Qt::FramelessWindowHint標志后"屏幕邊緣自動排列"無效。

  首先要搞清楚Qt::FramelessWindowHint標志如何影響窗口,因為它直接導致"屏幕邊緣自動排列"無效:

WindowCreationData::fromWindow(const QWindow * w, const QFlags<enum Qt::WindowType> flagsIn, unsigned int creationFlags) 行 519    C++
QWindowsWindowData::create(const QWindow * w, const QWindowsWindowData & parameters, const QString & title) 行 1075    C++
QWindowsIntegration::createWindowData(QWindow * window) 行 316    C++
QWindowsIntegration::createPlatformWindow(QWindow * window) 行 340    C++
QWindowPrivate::create(bool recursive) 行 392    C++
QWindow::create() 行 549    C++
QWidgetPrivate::create_sys(unsigned int window, bool initializeWindow, bool destroyOldWindow) 行 1456    C++
QWidget::create(unsigned int window, bool initializeWindow, bool destroyOldWindow) 行 1321    C++
QWidgetPrivate::createWinId(unsigned int winid) 行 2528    C++
void WindowCreationData::fromWindow(const QWindow *w, const Qt::WindowFlags flagsIn,
                                    unsigned creationFlags)
{
    if (popup || (type == Qt::ToolTip) || (type == Qt::SplashScreen)) {
        style = WS_POPUP;
    } else if (topLevel && !desktop) {
        if (flags & Qt::FramelessWindowHint)
            style = WS_POPUP;                // no border
        else if (flags & Qt::WindowTitleHint)
            style = WS_OVERLAPPED;
        else
            style = 0;
    } else {
        style = WS_CHILD;
    }

    if (!desktop) {
        if (topLevel) {
            if ((type == Qt::Window || dialog || tool)) {
                if (!(flags & Qt::FramelessWindowHint)) {
                    style |= WS_POPUP;
                    if (flags & Qt::MSWindowsFixedSizeDialogHint) {
                        style |= WS_DLGFRAME;
                    } else {
                        style |= WS_THICKFRAME;
                    }
                    if (flags & Qt::WindowTitleHint)
                        style |= WS_CAPTION; // Contains WS_DLGFRAME
                }
            } else {
                 exStyle |= WS_EX_TOOLWINDOW;
            }
        }
    }
}

   上面一個是調用堆棧(從下往上看),一個是關鍵函數(函數中不重要的內容已經刪除)。從代碼中可以看出,設置Qt::FramelessWindowHint標志會改變窗口樣式,從而影響創建的窗口,現在基本已經知道,"屏幕邊緣自動排列"功能與窗口樣式有關。

  新建一個win32窗口程序,不斷改變窗口的樣式,最后得出結論:只有在窗口擁有WS_MAXIMIZEBOX | WS_THICKFRAME樣式時,"屏幕邊緣自動排列"功能才有效,最好還要添加WS_CAPTION樣式,否則窗口最大化會覆蓋任務欄。

 

原本以為完美結束了,但是不要高興的太早,經過不斷測試,還有幾個問題:

  1)在任務欄點擊窗口時,不能最小化:

    只要加上Qt::WindowMinimizeButtonHint標志即可解決該問題。

 

  2)如果有多個顯示器,在輔屏上直接顯示最大化,窗口顯示不完整:

QWindowsWindow::show_sys() 行 1230    C++
QWindowsWindow::setVisible(bool visible) 行 1092    C++
QWindow::setVisible(bool visible) 行 518    C++
QWidgetPrivate::show_sys() 行 7897    C++
QWidgetPrivate::show_helper() 行 7826    C++
QWidget::setVisible(bool visible) 行 8110    C++
QWidget::showMaximized() 行 3154    C++
void QWindowsWindow::show_sys() const
{
    int sm = SW_SHOWNORMAL;
    bool fakedMaximize = false;
    const QWindow *w = window();
    const Qt::WindowFlags flags = w->flags();
    const Qt::WindowType type = w->type();
    if (w->isTopLevel()) {
        const Qt::WindowState state = w->windowState();
        if (state & Qt::WindowMinimized) {
            sm = SW_SHOWMINIMIZED;
            if (!isVisible())
                sm = SW_SHOWMINNOACTIVE;
        } else {
            updateTransientParent();
            if (state & Qt::WindowMaximized) {
                sm = SW_SHOWMAXIMIZED;
                // Windows will not behave correctly when we try to maximize a window which does not
                // have minimize nor maximize buttons in the window frame. Windows would then ignore
                // non-available geometry, and rather maximize the widget to the full screen, minus the
                // window frame (caption). So, we do a trick here, by adding a maximize button before
                // maximizing the widget, and then remove the maximize button afterwards.
                if (flags & Qt::WindowTitleHint &&
                        !(flags & (Qt::WindowMinMaxButtonsHint | Qt::FramelessWindowHint))) {
                    fakedMaximize = TRUE;
                    setStyle(style() | WS_MAXIMIZEBOX);
                }
            } // Qt::WindowMaximized
        } // !Qt::WindowMinimized
    }
    if (type == Qt::Popup || type == Qt::ToolTip || type == Qt::Tool || testShowWithoutActivating(w))
        sm = SW_SHOWNOACTIVATE;

    if (w->windowState() & Qt::WindowMaximized)
        setFlag(WithinMaximize); // QTBUG-8361

    ShowWindow(m_data.hwnd, sm);

    clearFlag(WithinMaximize);

    if (fakedMaximize) {
        setStyle(style() & ~WS_MAXIMIZEBOX);
        SetWindowPos(m_data.hwnd, 0, 0, 0, 0, 0,
                     SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER
                     | SWP_FRAMECHANGED);
    }
}

  還是老樣子,上面一個是調用堆棧,一個是關鍵函數,我們可以看到最后QT調用了ShowWindow函數來顯示最大化窗口,但是為什么會顯示不完整呢?

  通常遇到一個復雜的問題,我會新建一個簡單的項目來做實驗。新建一個win32項目,最開始顯示就讓它最大化,結果顯示正常,證明還是QT自己處理的問題,應該是在ShowWindow之后進行其他的處理,導致窗口顯示不完整,最后發現是

處理WM_GETMINMAXINFO消息導致的,接下來我們看看QT如何處理WM_GETMINMAXINFO消息。

QWindowsWindow::getSizeHints(tagMINMAXINFO * mmi) 行 2044    C++
QWindowsContext::windowsProc 行 1015    C++
qWindowsWndProc(HWND__ * hwnd, unsigned int message, unsigned int wParam, long lParam) 行 1271    C++
void QWindowsWindow::getSizeHints(MINMAXINFO *mmi) const
{
    const QWindowsGeometryHint hint(window(), m_data.customMargins);
    hint.applyToMinMaxInfo(m_data.hwnd, mmi);

    if ((testFlag(WithinMaximize) || (window()->windowState() == Qt::WindowMinimized))
            && (m_data.flags & Qt::FramelessWindowHint)) {
        // This block fixes QTBUG-8361: Frameless windows shouldn't cover the
        // taskbar when maximized
        const QScreen *screen = window()->screen();

        // Documentation of MINMAXINFO states that it will only work for the primary screen
        if (screen && screen == QGuiApplication::primaryScreen()) {
            mmi->ptMaxSize.y = screen->availableGeometry().height();

            // Width, because you can have the taskbar on the sides too.
            mmi->ptMaxSize.x = screen->availableGeometry().width();

            // If you have the taskbar on top, or on the left you don't want it at (0,0):
            mmi->ptMaxPosition.x = screen->availableGeometry().x();
            mmi->ptMaxPosition.y = screen->availableGeometry().y();
        } else if (!screen){
            qWarning() << "window()->screen() returned a null screen";
        }
    }

    qCDebug(lcQpaWindows) << __FUNCTION__ << window() << *mmi;
}

  當程序在輔屏上時,它的screen是輔屏,如果當前screen不等於QGuiApplication::primaryScreen(主屏),則不設置MINMAXINFO結構,但是由於它已經處理了WM_GETMINMAXINFO消息,導致這個消息不會被系統默認的窗口處理函數處理(DefWindowProc),所以才會顯示不完整,解決辦法是優先響應WM_GETMINMAXINFO消息,讓后交給系統默認的窗口處理函數進行處理。

 

  3)最大化后,窗口內容變小,最明顯的就是最小化、最大化、關閉按鈕變小了:

  窗口最大化時,系統會在屏幕上面顯示所有的客戶區域,此時系統會計算邊框的大小,然后超出屏幕范圍進行顯示,例如邊框的寬為8高為8,則系統會在(-8,-8,寬度+8,高度+8)的位置顯示窗口,給人的感覺窗口的內容變小了,

 

  除去底部的任務欄,程序最大化可顯示的最大寬度是1600*860,而窗口的實際位置是(-8,-8,1608,868)。這樣我們可以添加一個QWidget作為主顯示窗口,然后在程序最大化時,添加一個外邊框,讓它向內部縮一點。

 

最后的解決方案是:

  1. 在窗口的構造函數中添加以下代碼,改變窗口的樣式:

this->setWindowFlags(Qt::FramelessWindowHint | Qt::WindowMinimizeButtonHint);
// QMainWindow透明顯示,當設置主顯示窗口的外邊距時,防止外邊距顯示出來。
this->setAttribute(Qt::WA_TranslucentBackground, true);

HWND hwnd = (HWND)this->winId();
DWORD style = ::GetWindowLong(hwnd, GWL_STYLE);
::SetWindowLong(hwnd, GWL_STYLE, style | WS_MAXIMIZEBOX | WS_THICKFRAME | WS_CAPTION);

   2. 重載nativeEvent函數,處理WM_NCHITTEST、WM_NCCALCSIZE和WM_GETMINMAXINFO消息

bool CustomWindow::nativeEvent(const QByteArray &eventType, void *message, long *result)
{
    MSG* msg = (MSG*)message;
    switch (msg->message) {

    case WM_NCHITTEST:
    {
        int xPos = GET_X_LPARAM(msg->lParam) - this->frameGeometry().x();
        int yPos = GET_Y_LPARAM(msg->lParam) - this->frameGeometry().y();
        if (m_title->isCaption(xPos, yPos)) {
            *result = HTCAPTION;
            return true;
        }
    }
        break;
case WM_NCCALCSIZE:
        return true;

    case WM_GETMINMAXINFO:
    {
        if (::IsZoomed(msg->hwnd)) {
            // 最大化時會超出屏幕,所以填充邊框間距
            RECT frame = { 0, 0, 0, 0 };
            AdjustWindowRectEx(&frame, WS_OVERLAPPEDWINDOW, FALSE, 0);
            frame.left = abs(frame.left);
            frame.top = abs(frame.bottom);
             this->setContentsMargins(frame.left, frame.top, frame.right, frame.bottom);
        }
        else {
            this->setContentsMargins(2, 2, 2, 2);
        }

        *result = ::DefWindowProc(msg->hwnd, msg->message, msg->wParam, msg->lParam);
        return true;
    }
        break;
    }

    return QMainWindow::nativeEvent(eventType, message, result);
}

 

顯示效果:

  

 

最后完成的Demo:CustomWindow.zip

如果覺得好用,可以給我留個言,支持一下。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM