一、问题描述
开发环境:Qt5.15.0、Win10、 Visual studio 2019、C++
在开发视频会议项目的过程中,被一个问题困扰了很久。就是整个视频会议的界面在拉伸四周改变大小的过程中,整个客户端界面闪烁的非常严重(非视频画面闪烁)。可以看到在下面的视频中,界面的背景会出现短暂的透明,非常影响使用感受。
图1.1 界面闪烁图
二、问题探索
一开始以为是因为子视频窗口多层嵌套,同时底层又传入了窗口句柄给D3D用于视频渲染,所以导致的在改变大小时 整个窗体在重绘时因为需要大量复杂的计算,导致背景和内容无法在同一个刷新周期内完成,于是做了以下尝试:
- 精简子窗口的界面
- 设置各种窗口属性比如Qt::WA_OpaquePaintEvent 、Qt::WA_PaintOnScreen等
- 在nativeEvent()中拦截WM_PAINT等事件进行双缓冲绘图处理
但是发现网上说的一些常见的解决窗口闪烁的方案都不起作用,只能暂时搁置。后来前几天有时间又来研究。突然想到这个现象和网上描述的不太一样,一般闪烁往往是因为在重绘过程中刷新出了窗口的默认的背景色,但是我的现象是在改变大小的过程的背景会直接变成透明,使我开始怀疑是因为我去除了边框,然后重写了鼠标拖动事件的代码逻辑有问题。于是我就尝试了取消设置Qt::FramelessWindowHint,让窗口露出系统自带的边框,竟然一下子就不闪了。
那为什么使用windows系统自带的边框就不闪了呢?这个东西到底是什么呢? 这里要引入一个概念:客户区和非客户区。我们以Qt的窗口体系为例,非客户区就是指标题栏,图标,窗口边框和标题按钮。而客户区就是我们平常操作geometry()获取的那一部分,一些在paintEvent()中的操作也是在客户区中进行的,Qt没有开放接口可以直接操作非客户区的样式,所以我们在开发过程中往往是直接设置Qt::FramelessWindowHint去除系统自带的标题栏和边框,然后通过自定义一个伪标题栏来实现定制化。
所以问题很可能就出现在去除边框后,客户区和非客户区大小相同,同时又叠加了视频渲染所导致的(后来在测试中也发现如果把非客户区的大小设为0,就会产生闪烁,即使只有一个1个像素也不会闪烁) 那么此时我们的目标就明确了:1.尽可能缩小非客户区的大小,这样不会导致原有的窗口尺寸受到影响 2.修改非客户区的颜色和客户区相同,这样用户就感知不到。
图 2.1 QT的窗口组成
三、问题解决
缩小客户区首先想到了Qt的一个属性:Qt::CustomizeWindowHint,这个属性虽然可以去除边框和标题,但是在上方还是会保留一个6像素的白条,无法完全满足我们的要求
图 3.1 标题栏残留
后来我又叠加了一个属性:Qt::CustomizeWindowHint | Qt::MSWindowsFixedSizeDialogHint 但是这个属性会有一些小问题,Qt在文档中也说明了,就是在跨越不同分辨率的显示器的时候系统会强制显示原有的大小,但是我在自己的扩展屏上测试了没有问题 ,可能是因为分辨率是一样的,有条件的朋友可以测试一下。
图3.2 Qt::MSWindowsFixedSizeDialogHint属性说明
叠加之后的效果如下:
图3.3 窄边框效果
之后就是改变边框的颜色,既然Qt无法直接修改非客户区的大小,那么只能求助于Windows操作系统了,Windows操作系统提供了一系列DWM (桌面窗口管理器)API 可以用来修改非客户区。Custom Window Frame Using DWM - Win32 apps | Microsoft DocsThis topic demonstrates how to use the Desktop Window Manager (DWM) APIs to create custom window frames for your application.https://docs.microsoft.com/en-us/windows/win32/dwm/customframe
图 3.4 DWM API
这里我主要在nativeEvent中处理了WM_NCPAINT和WM_NCACTIVE两个消息,下面给出代码,主要参考以下文章:
bool CMultiMeetingWgt::nativeEvent(const QByteArray& eventType, void* message, long* result)
{
#ifdef UC_OS_WIN
#if(QT_VERSION == QT_VERSION_CHECK(5,11,1))
MSG* msg = (MSG*)message;
#else
const auto msg = static_cast<LPMSG>(message);
#endif
switch (msg->message)
{
case WM_NCACTIVATE: {
//show RedrawWindow and break, or can't be actived when minisized;
RedrawWindow((HWND)winId(), NULL, NULL, RDW_UPDATENOW);
break;
}
case WM_NCPAINT:
{
#ifndef DCX_USESTYLE
#define DCX_USESTYLE 0x00010000
#endif
//Why here use DCX_USESTYLE that microsoft undocumented
//maybe you can refer to
//https://social.msdn.microsoft.com/Forums/windows/en-US/a407591a-4b1e-4adc-ab0b-3c8b3aec3153/the-evil-wmncpaint?forum=windowsuidevelopment
HWND hwnd = (HWND)winId();
HDC hdc = ::GetDCEx(hwnd,0, DCX_WINDOW | DCX_USESTYLE);
if (hdc) {
RECT rcclient;
::GetClientRect(hwnd, &rcclient);
RECT rcwin;
::GetWindowRect(hwnd, &rcwin);
POINT ptupleft;
ptupleft.x = rcwin.left;
ptupleft.y = rcwin.top;
//converts (maps) a set of points from a coordinate space relative to one window
//to a coordinate space relative to another window
::MapWindowPoints(0, hwnd, (LPPOINT)&rcwin, (sizeof(RECT) / sizeof(POINT)));
//Second param:Specifies the amount to move the rectangle left or right.
//This parameter must be a negative value to move the rectangle to the left.
//Third param:Specifies the amount to move the rectangle up or down.
//This parameter must be a negative value to move the rectangle to the up.
::OffsetRect(&rcclient, -rcwin.left, -rcwin.top);
::OffsetRect(&rcwin, -rcwin.left, -rcwin.top);
HRGN rgntemp = NULL;
if (msg->wParam == NULLREGION || msg->wParam == ERROR) {
::ExcludeClipRect(hdc, rcclient.left, rcclient.top, rcclient.right, rcclient.bottom);
}
else {
rgntemp = ::CreateRectRgn(rcclient.left + ptupleft.x, rcclient.top + ptupleft.y, rcclient.right + ptupleft.x, rcclient.bottom + ptupleft.y);
if (::CombineRgn(rgntemp, (HRGN)msg->wParam, rgntemp, RGN_DIFF) == NULLREGION) {
// nothing to paint
}
::OffsetRgn(rgntemp, -ptupleft.x, -ptupleft.y);
::ExtSelectClipRgn(hdc, rgntemp, RGN_AND);
}
HBRUSH hbrush = ::CreateSolidBrush(RGB(26, 26, 26));
::FillRect(hdc, &rcwin, hbrush);
::DeleteObject(hbrush);
::ReleaseDC(hwnd, hdc);
if (rgntemp != 0) {
::DeleteObject(rgntemp);
}
}
return true;
}
default:
break;
}
#endif
return QWidget::nativeEvent(eventType, message, result);
}
四、最终效果图
图4.1 最终效果
以上的方案虽然可以暂时解决闪烁的问题,但是明显是不完善的,比如不能跨平台,而且去拦截了windows消息不知道会不会引发其他的问题,还没有经过大规模的测试。在这里仅提供一种解决思路,如果有问题可以一起探讨。