如果你是从搜索框点进来的,那你屏幕上现在大概是这样一幅画面。
主界面好好的。左边算例树,右边属性面板,中间一个 QMdiSubWindow里嵌着QWebEngineView,正在加载公司内网的在线帮助。第一次打开,一切正常,页面滚得顺滑。你松了口气,觉得这块总算收尾了。
然后你切到另一个算例窗口,再切回来。
黑的。
不是白屏。白屏你还能骗自己说是加载慢。是黑——连背景色都没有,一块干干净净、没有生气的矩形,像有人拿黑纸把那个区域贴上了。
更气人的是,它只是看不见,不是死了。
你把鼠标移进去,光标变成手型,链接能点。你滚一下滚轮,滚动条在动,说明页面确实在滚。你框选一段文字,Ctrl+C,粘到记事本里,内容一字不差。你打开devtools,console.log还在一条一条往外打,网络请求200,一点毛病没有。
它在正常工作,就是不给你看。
你开始试那些最朴素的办法。拖一下窗口,画面活过来半秒,然后继续黑。最大化,好了。还原,又黑了。拖到屏幕边缘再拖回来,能救一次,救不了第二次。你在resizeEvent里补了一句update(),当天有效;第二天你发现它变成了"拖动时疯狂闪烁"。
你换台机器。你那台必现,测试同学那台不复现。他在群里发了一句"我这边是好的呀",你盯着这七个字看了很久。
然后你摸到了规律:Windows 10上基本看不见,Windows 11加上150%缩放就天天见;单屏没事,接了扩展屏就出事;本地跑得好好的,远程桌面一连上去,黑。你把笔记本合上再打开、Alt+Tab切出去再切回来、被一个模态对话框盖一下——任何一个动作都可能让它黑,也都可能让它恢复。你既没法稳定复现,也就没法稳定修。
你想截个图留证据。截出来是黑的。系统截图工具、OBS、QWidget::grab(),全黑。因为那块像素压根不在一块你能读到的位置上。
然后是最要命的那次。 客户来公司看演示。你打开产品,八个算例窗口平铺,切到第三个,帮助面板黑了。会议室安静了三秒,客户问:"这个是还没做完吗?"
你回到工位,把这个功能的代码从头读了一遍。你的代码没有任何问题——就是几行new QWebEngineView、load()、show()。问题不在你这儿,但也不在别处,它悬在两套系统的中间,谁都不怪自己。
你提了个issue到Qt的bug tracker。一周后回复来了:MDI不是推荐的用法。
你试了最后一个办法。命令行加上 QTWEBENGINE_CHROMIUM_FLAGS=--disable-gpu。
好了。真的好了,一点都不黑。
然后你打开那个带三维模型预览的页面,转一下视角,卡成幻灯片。客户还在旁边坐着。
到今天,这个bug在你的看板上挂了快一个月,标题是"帮助面板显示异常",优先级从P1降到了P3。不是因为它不严重,是因为没人知道该派给谁。
如果你正卡在这儿,这篇是写给你的。我们不卖课,就把这件事从头到尾讲清楚——包括我们自己是怎样一头栽进去的。
去年十一月,一家做装备仿真软件的团队找到我们。下称A所,名字按对方要求隐去。
他们的产品是一个多文档工作台:一个QMdiArea,平铺着七八个算例窗口,工程师要左右对比不同工况的计算结果。这是工业软件最典型的形态,我们自己的伏图(Virtu)也是这么长的。
问题出在他们新加的一块功能上:把"在线帮助中心"和"云端报告"嵌进主界面。选型很自然,QtWebEngine嘛,Chromium内核,HTML直接跑,还能复用一套前端。
上线前一周,测试提了一个bug:切换算例之后,帮助面板黑屏。
他们的两个工程师查了三周。我后来看过他们的尝试记录,写了满满两页:
setAttribute(Qt::WA_NativeWindow)、手动 raise() 加update()、给子窗口加 WA_TranslucentBackground、换QOpenGLWidget当视口、从Qt5.15升到Qt6.5、关GPU加速、实在不行用QTextBrowser顶上……
结果是:黑屏变成了白屏,白屏变成了闪烁,闪烁变成了输入焦点丢到另一个窗口,关GPU之后性能直接不可用——工业场景里那些三维预览、几万行的大表格页面,全部卡成PPT。
没有一个真解决。
第四十七天,他们在一次行业会上碰到我们的人,问了一句:"你们Virtu里也嵌网页吧?你们怎么没这问题?"
其实我们也踩过。而且踩得更早、更惨。
要讲清楚,得先说一个很多人没意识到的事:Qt的窗口和Windows的窗口,不是一回事。
QMdiSubWindow是逻辑窗口。 它在Qt眼里就是一个QWidget,没有HWND。QMdiArea管着它,靠的是几何计算+裁剪区域(clip region)+ 一块共享的backing store。子窗口重叠时,Qt把那块被盖住的区域裁掉,不画。这套机制非常高效,也非常"Qt"。
Chromium不是这么想的。 它在Windows上输出画面,最终要落到一个原生surface上——说白了,一个真的HWND,或者一条绑定到窗口的D3D swap chain。
于是你就干了一件很拧巴的事:在一个"没有HWND的逻辑窗口"里,塞进了一个"必须有HWND的原生surface"。
这两套系统对"谁该被谁裁掉"的理解,从根上就不一样:
Qt认为:我给你一个clip region,你照着裁。
Windows 认为:这个HWND是我的合成单元,父窗口裁不裁跟我没关系,我按z-order自己画。
谁都没错。但放在一起,就有一块像素没人负责——你看到的那块黑,就是那块没人负责的像素。
QMdiSubWindow恰恰是把这矛盾放大到极致的容器。它会移动、会缩放、会被别的子窗口盖住、会被QMdiArea的滚动条整体平移、会最大化再还原。每一次变化,都在要求那个原生surface"跟着动,并且正确裁剪"。
它做不到。于是你黑屏。
给子窗口设WA_NativeWindow。 问题只是换了个位置:现在是两个原生窗口抢z-order。黑屏变成闪烁和遮挡,外加一个更难查的焦点问题。
raise()加update()。 治标。能救一次,救不了连续拖动。
升级Qt 版本。 窗口模型没变,Chromium的嵌入方式没变。症状会换一张脸,病还在。
disable-gpu。 这是唯一"有效"的,因为它让Chromium走软件合成——画面变成CPU画的一张位图,老老实实塞进QWidget的backing store,两套系统只剩一套。代价是WebGL、Canvas、视频、复杂CSS全部退化。工业软件里那些三维模型预览,直接就废了。
换QTextBrowser。 等于放弃整个web技术栈。
所以结论是:这条路走不通,不是因为你不够努力,是因为方向反了。你一直在想办法"让原生窗口听话",而它天生就不听话。
我们最后没去改Chromium,也没去改QMdiSubWindow。思路只有一句话:把"原生窗口"这个前提干掉。
落到工程上是三层:
第一层,让画面不再经过原生surface。 控制渲染后端与合成路径,让Chromium 输出的是一张离屏纹理,交给Qt自己的backing store去合成。这样MDI的裁剪、z-order、滚动,全部回到Qt的规则里——两套系统只剩一套,才有"规则"可言。
第二层,做一个窗口语义代理。 MDI的移动、缩放、激活、遮挡、最小化还原,这些事件在Qt里是零散的,时序不保证,还会重复触发。我们把它归一成一个状态机,保证web容器在任何时刻都清楚三件事:我现在该是什么尺寸、我该不该可见、我上一帧还有效吗。这一步才是黑屏反复出现的真正原因——不是画不出来,是不知道该画什么。
第三层,把崩溃当成常态。 GPU进程会挂,渲染上下文会丢,驱动会抽风,远程桌面一切换分辨率就翻脸。我们不假设它不出问题,而是保证出问题之后能自愈:用户看到的是半秒重绘,不是一块永久黑。
这套东西在我们内部叫"原生窗口嵌入治理",属于伏图(Virtu)桌面底座的一部分。它在几十个客户现场、上千台机器上跑过——包括那些显卡驱动三年没更新过的老工作站。
至于中间踩了多少坑、每一层具体怎么写的,那是另一个故事了。这里面有相当一部分是拿客户的机器一台一台试出来的,不太适合写在公 众号里。
A所最后把这块交给了我们。改完之后他们做了一轮回归:八窗口平铺、连续拖动、最大化还原、双屏切换、DPI 150%、远程桌面、外加人为注入一次GPU崩溃。
全过。
后来他们问:这问题这么难,你们怎么这么快?
不快的。我们自己在这个坑里待过很久,只是没写在脸上。
做工业软件跟做互联网产品不一样。你没有"发个版明天再修"的机会——客户的机器在车间里、在机房里、在没有外网的实验台上,一个黑屏就是一次上门服务,就是一次信任损耗。
优飞迪科技这些年主要就干一件事:国产工业仿真软件,伏图(Virtu)。热仿真、多物理场、大规模三维场景、几十万零件的装配体,还要跑在从图形工作站到轻薄本的各色机器上。这种活干久了会训练出一种很具体的能力——不是"会写Qt",而是知道Qt和操作系统在哪些地方会打架,以及打起来之后怎么收场。
我们今天能接住这个黑屏,是因为我们曾经被它按在地上摩擦过。

如果你手上正卡着一个类似的问题,先别急着改代码,按这个顺序过一遍,能省你至少两周:
1. 先判断黑的是"谁"。 黑屏时把窗口拖出屏幕一半。如果露出的部分正常、被遮的部分黑,是裁剪问题;如果整体黑且resize后能恢复,是合成路径问题;如果只有输入框能打字但画面不动,是帧提交问题。三种病,三种药。
2. 看是不是只在特定DPI/多屏下复现。只在高分屏复现的,八成跟surface尺寸和缩放比不一致有关。
3. 把GPU加速关掉再试一次。如果关掉就好了,那基本可以确诊是原生surface的合成问题,别再往业务代码里找。
4. 别用QMdiSubWindow装WebEngine做验证。换成一个普通的 QWidget顶层窗口,如果问题消失,说明你的MDI容器就是那个"放大器"。
5. 在事件里打点,不要在日志里猜。记录每一次move/resize/show/hide之后的容器几何和可见状态,你会发现有一帧的尺寸是0。
如果你正在做类似的事——工业软件、桌面客户端、要在Qt里嵌web、要做多文档多窗口——欢迎带着现象来找我们聊。
不用带着合同来。你可以先发一段复现视频、一张截图、或者你试过的那份方案清单。这类问题我们看一眼,基本能判断是渲染路径的问题还是窗口语义的问题,十分钟给你一个方向。