如何解决QT视频会议窗口在拉伸过程中整个界面闪烁的问题

一、问题描述

       开发环境: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两个消息,下面给出代码,主要参考以下文章:

win32- 使用WM_NCPAINT在非客户区域绘制边框 - 编程猎人win32- 使用WM_NCPAINT在非客户区域绘制边框,编程猎人,网罗编程知识和经验分享,解决编程疑难杂症。https://www.programminghunter.com/article/34391117799/

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消息不知道会不会引发其他的问题,还没有经过大规模的测试。在这里仅提供一种解决思路,如果有问题可以一起探讨。

猜你喜欢

转载自blog.csdn.net/qq_39304481/article/details/125463067