QT自绘标题和边框
Posted DC
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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
如果觉得好用,可以给我留个言,支持一下。
以上是关于QT自绘标题和边框的主要内容,如果未能解决你的问题,请参考以下文章