Qt 自定义 frameless window 显示器之间的绘画问题
Painting issue in Qt custom frameless window between monitors
对于我的一个应用程序,我想在 Windows 操作系统(如 Firefox、Avast、Microsoft Word 等)下定制 window。因此,我从 Win32 API 重新实现了一些消息处理 (QWidget::nativeEvent ()
),以保留 AeroSnap 和其他功能。
虽然效果很好,但当我的自定义 window 从一个屏幕转移到另一个屏幕(我有两个屏幕)时,会出现视觉故障(如下所示)。故障出现后,调整 window 的大小,修复错误。此外,经过一些调试,我发现Qt QWidget::geometry()
与出现错误时Win32 API GetWindowRect()
返回的几何图形不一样。
我的两台显示器都是高清的(1920x1080,所以导致错误的不是分辨率差异,而是 DPI 差异?)。当 window 在两个屏幕之间移动时(当 windows 将 window 从一个屏幕转移到另一个屏幕时,似乎会出现故障?)。
window 无故障
的屏幕截图
window 故障
的屏幕截图
QWidget::geometry()
最初报告的几何形状是 QRect(640,280 800x600)
,QWidget::frameGeometry()
是 QRect(640,280 800x600)
,Win32 GetWindowRect()
是 QRect(640,280 800x600)
.所以,相同的几何形状。但是,在 window 在两个监视器之间移动后,QWidget::geometry()
报告的几何形状变为 QGeometry(1541,322 784x561)
。 QWidget::frameGeometry()
或 GetWindowRect()
报告的几何图形未更改。当然,之后,当 window 调整大小时,几何重新正确报告,绘画问题消失。结论,当 window 在两个监视器之间移动时,Qt 似乎假设 "appear" 帧。
可以找到 BaseFramelessWindow
class 实现 here。这是一个 Qt QMainWindow subclass 重新实现一些 Win32 API 本机事件 (QWidget::nativeEvent()
).
那么有人会怎么避免这个bug呢?这是一个 Qt 错误吗?我尝试了很多东西,但没有任何效果。而且我在互联网上找不到任何提及此问题的信息(也许我看起来很糟糕?)。
最小示例代码:
// main.cpp
#include "MainWindow.h"
#include <QtWidgets/qapplication.h>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
// MainWindow.h
#pragma once
#include <QtWidgets/qmainwindow.h>
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = Q_NULLPTR);
protected:
void paintEvent(QPaintEvent* event) override;
bool nativeEvent(const QByteArray& eventType, void* message, long* result) override;
};
// MainWindow.cpp
#include "MainWindow.h"
#include <QtGui/qpainter.h>
#include <Windows.h>
#include <Windowsx.h>
MainWindow::MainWindow(QWidget* parent)
: QMainWindow(parent)
{
::SetWindowLongPtr((HWND)winId(), GWL_STYLE, WS_POPUP | WS_THICKFRAME | WS_CAPTION | WS_SYSMENU | WS_MAXIMIZEBOX | WS_MINIMIZEBOX);
}
void MainWindow::paintEvent(QPaintEvent* event)
{
QPainter painter(this);
// Using GetWindowRect() instead of rect() seems to be a valid
// workaround for the painting issue. However many other things
// do not work cause of the bad geometry reported by Qt.
RECT winrect;
GetWindowRect(reinterpret_cast<HWND>(winId()), &winrect);
QRect rect = QRect(0, 0, winrect.right - winrect.left, winrect.bottom - winrect.top);
// Background
painter.fillRect(rect, Qt::red);
// Border
painter.setBrush(Qt::NoBrush);
painter.setPen(QPen(Qt::blue, 1));
painter.drawRect(rect.adjusted(0, 0, -1, -1));
// Title bar
painter.fillRect(QRect(1, 1, rect.width() - 2, 19), Qt::yellow);
}
bool MainWindow::nativeEvent(const QByteArray& eventType, void* message, long* result)
{
MSG* msg = reinterpret_cast<MSG*>(message);
switch (msg->message)
{
case WM_NCCALCSIZE:
*result = 0;
return true;
case WM_NCHITTEST: {
*result = 0;
RECT winrect;
GetWindowRect(reinterpret_cast<HWND>(winId()), &winrect);
// Code allowing to resize the window with the mouse is omitted.
long x = GET_X_LPARAM(msg->lParam);
long y = GET_Y_LPARAM(msg->lParam);
if (x > winrect.left&& x < winrect.right && y > winrect.top&& y < winrect.top + 20) {
// To allow moving the window.
*result = HTCAPTION;
return true;
}
repaint();
return false;
}
default:
break;
}
return false;
}
查看此答案:
我在屏幕之间捕捉时遇到了完全相同的问题,如该答案的第 3 段所述:
I ran into some painting issues when snapping from one screen to another, but worked around that with a mask (notes in code comments). Would be nice to find a cleaner solution (it may qualify as a Qt bug actually).
解决方法在 paintEvent()
方法中 FramelessWidget
代码块的底部,在长注释之后。我也将其粘贴在这里并附上一些上下文:
QStyleOption opt;
opt.initFrom(this);
// be sure to use the full frame size, not the default rect() which is inside frame.
opt.rect.setSize(frameSize());
.....
// Setting a mask works around an issue with artifacts when switching screens with Win+arrow
// keys. I don't think it's the actual mask which does it, rather it triggers the region
// around the widget to be polished but I'm not sure. As support for my theory, the mask
// doesn't even have to follow the border radius.
setMask(QRegion(opt.rect));
我在你的代码中没有看到 paintEvent()
但也许你可以从其他地方做类似的事情。
顺便说一句,这也可能会提示您为什么 QWidget::geometry()
不正确...您可能想要 QWidget::frameGeometry()
.
问题是Qt在修改WM_NCCALCSIZE
时没有正确处理window的绘图!
我意识到你已经有一项工作正在进行中,但我开发了一项并行工作,它以一种你可能感兴趣的简单方式实现了你的目标,它处理了 [=] 的所有低级别 API 28=] 用于少边框 window,而您可以只使用 setCentralWidget
API 并且您的小部件将填满整个 window!
ExampleMinimal
的 header(干净的例子)只是:
#include <QGoodWindow>
class MainWindow : public QGoodWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
};
之后我遇到了这个确切的错误,并通过实施 Qt 的 moveEvent
来检测屏幕变化然后调用 SetWindowPos
在相同位置重绘 window 来解决它。
例如:
class MainWindow : public QMainWindow
{
// rest of the class
private:
QScreen* current_screen = nullptr;
void moveEvent(QMoveEvent* event)
{
if (current_screen == nullptr)
{
current_screen = screen();
}
else if (current_screen != screen())
{
current_screen = screen();
SetWindowPos((HWND) winId(), NULL, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER |
SWP_NOOWNERZORDER | SWP_FRAMECHANGED | SWP_NOACTIVATE);
}
}
};
我不确定是否确实需要 current_screen == nullptr
子句,但如果没有它,我在处理多个 windows/dialogs 时遇到了麻烦(有时它们会变得无响应)。我也不知道 SetWindowPos
需要哪些标志。
我也不好说这个方法好不好,可能会在其他地方出问题。希望其他人可以对此发表评论。
在简单的情况下,无论哪种方式都对我有用。希望它提供了一个解决方案,或者至少是一个尝试解决这个问题的起点。
对于我的一个应用程序,我想在 Windows 操作系统(如 Firefox、Avast、Microsoft Word 等)下定制 window。因此,我从 Win32 API 重新实现了一些消息处理 (QWidget::nativeEvent ()
),以保留 AeroSnap 和其他功能。
虽然效果很好,但当我的自定义 window 从一个屏幕转移到另一个屏幕(我有两个屏幕)时,会出现视觉故障(如下所示)。故障出现后,调整 window 的大小,修复错误。此外,经过一些调试,我发现Qt QWidget::geometry()
与出现错误时Win32 API GetWindowRect()
返回的几何图形不一样。
我的两台显示器都是高清的(1920x1080,所以导致错误的不是分辨率差异,而是 DPI 差异?)。当 window 在两个屏幕之间移动时(当 windows 将 window 从一个屏幕转移到另一个屏幕时,似乎会出现故障?)。
window 无故障
的屏幕截图window 故障
的屏幕截图QWidget::geometry()
最初报告的几何形状是 QRect(640,280 800x600)
,QWidget::frameGeometry()
是 QRect(640,280 800x600)
,Win32 GetWindowRect()
是 QRect(640,280 800x600)
.所以,相同的几何形状。但是,在 window 在两个监视器之间移动后,QWidget::geometry()
报告的几何形状变为 QGeometry(1541,322 784x561)
。 QWidget::frameGeometry()
或 GetWindowRect()
报告的几何图形未更改。当然,之后,当 window 调整大小时,几何重新正确报告,绘画问题消失。结论,当 window 在两个监视器之间移动时,Qt 似乎假设 "appear" 帧。
可以找到 BaseFramelessWindow
class 实现 here。这是一个 Qt QMainWindow subclass 重新实现一些 Win32 API 本机事件 (QWidget::nativeEvent()
).
那么有人会怎么避免这个bug呢?这是一个 Qt 错误吗?我尝试了很多东西,但没有任何效果。而且我在互联网上找不到任何提及此问题的信息(也许我看起来很糟糕?)。
最小示例代码:
// main.cpp
#include "MainWindow.h"
#include <QtWidgets/qapplication.h>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
// MainWindow.h
#pragma once
#include <QtWidgets/qmainwindow.h>
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = Q_NULLPTR);
protected:
void paintEvent(QPaintEvent* event) override;
bool nativeEvent(const QByteArray& eventType, void* message, long* result) override;
};
// MainWindow.cpp
#include "MainWindow.h"
#include <QtGui/qpainter.h>
#include <Windows.h>
#include <Windowsx.h>
MainWindow::MainWindow(QWidget* parent)
: QMainWindow(parent)
{
::SetWindowLongPtr((HWND)winId(), GWL_STYLE, WS_POPUP | WS_THICKFRAME | WS_CAPTION | WS_SYSMENU | WS_MAXIMIZEBOX | WS_MINIMIZEBOX);
}
void MainWindow::paintEvent(QPaintEvent* event)
{
QPainter painter(this);
// Using GetWindowRect() instead of rect() seems to be a valid
// workaround for the painting issue. However many other things
// do not work cause of the bad geometry reported by Qt.
RECT winrect;
GetWindowRect(reinterpret_cast<HWND>(winId()), &winrect);
QRect rect = QRect(0, 0, winrect.right - winrect.left, winrect.bottom - winrect.top);
// Background
painter.fillRect(rect, Qt::red);
// Border
painter.setBrush(Qt::NoBrush);
painter.setPen(QPen(Qt::blue, 1));
painter.drawRect(rect.adjusted(0, 0, -1, -1));
// Title bar
painter.fillRect(QRect(1, 1, rect.width() - 2, 19), Qt::yellow);
}
bool MainWindow::nativeEvent(const QByteArray& eventType, void* message, long* result)
{
MSG* msg = reinterpret_cast<MSG*>(message);
switch (msg->message)
{
case WM_NCCALCSIZE:
*result = 0;
return true;
case WM_NCHITTEST: {
*result = 0;
RECT winrect;
GetWindowRect(reinterpret_cast<HWND>(winId()), &winrect);
// Code allowing to resize the window with the mouse is omitted.
long x = GET_X_LPARAM(msg->lParam);
long y = GET_Y_LPARAM(msg->lParam);
if (x > winrect.left&& x < winrect.right && y > winrect.top&& y < winrect.top + 20) {
// To allow moving the window.
*result = HTCAPTION;
return true;
}
repaint();
return false;
}
default:
break;
}
return false;
}
查看此答案:
我在屏幕之间捕捉时遇到了完全相同的问题,如该答案的第 3 段所述:
I ran into some painting issues when snapping from one screen to another, but worked around that with a mask (notes in code comments). Would be nice to find a cleaner solution (it may qualify as a Qt bug actually).
解决方法在 paintEvent()
方法中 FramelessWidget
代码块的底部,在长注释之后。我也将其粘贴在这里并附上一些上下文:
QStyleOption opt;
opt.initFrom(this);
// be sure to use the full frame size, not the default rect() which is inside frame.
opt.rect.setSize(frameSize());
.....
// Setting a mask works around an issue with artifacts when switching screens with Win+arrow
// keys. I don't think it's the actual mask which does it, rather it triggers the region
// around the widget to be polished but I'm not sure. As support for my theory, the mask
// doesn't even have to follow the border radius.
setMask(QRegion(opt.rect));
我在你的代码中没有看到 paintEvent()
但也许你可以从其他地方做类似的事情。
顺便说一句,这也可能会提示您为什么 QWidget::geometry()
不正确...您可能想要 QWidget::frameGeometry()
.
问题是Qt在修改WM_NCCALCSIZE
时没有正确处理window的绘图!
我意识到你已经有一项工作正在进行中,但我开发了一项并行工作,它以一种你可能感兴趣的简单方式实现了你的目标,它处理了 [=] 的所有低级别 API 28=] 用于少边框 window,而您可以只使用 setCentralWidget
API 并且您的小部件将填满整个 window!
ExampleMinimal
的 header(干净的例子)只是:
#include <QGoodWindow>
class MainWindow : public QGoodWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
};
之后我遇到了这个确切的错误,并通过实施 Qt 的 moveEvent
来检测屏幕变化然后调用 SetWindowPos
在相同位置重绘 window 来解决它。
例如:
class MainWindow : public QMainWindow
{
// rest of the class
private:
QScreen* current_screen = nullptr;
void moveEvent(QMoveEvent* event)
{
if (current_screen == nullptr)
{
current_screen = screen();
}
else if (current_screen != screen())
{
current_screen = screen();
SetWindowPos((HWND) winId(), NULL, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER |
SWP_NOOWNERZORDER | SWP_FRAMECHANGED | SWP_NOACTIVATE);
}
}
};
我不确定是否确实需要 current_screen == nullptr
子句,但如果没有它,我在处理多个 windows/dialogs 时遇到了麻烦(有时它们会变得无响应)。我也不知道 SetWindowPos
需要哪些标志。
我也不好说这个方法好不好,可能会在其他地方出问题。希望其他人可以对此发表评论。
在简单的情况下,无论哪种方式都对我有用。希望它提供了一个解决方案,或者至少是一个尝试解决这个问题的起点。