内网踩坑笔记:QWidget 套 QWindow,以及 QStackedWidget 里多个 QCustomPlot 为什么会崩

结论先行:

  1. Qt 5.10 里 QWidget 想装 QWindow,必须走 QWidget::createWindowContainer(),这不是“可选封装”,而是跨 QWidget 与 QWindow 两套窗口体系的结构性桥梁。
  2. 多个 QCustomPlot 放进 QStackedWidget 崩溃,锅不在 QStackedWidget,也不在 createWindowContainer。根因在 QCustomPlot 的 OpenGL 绘制路径:QCPPaintBufferGlFbo::draw() 缺少“当前 OpenGL 上下文是否就是本实例 mGLContext”的校验,导致在错误上下文上做 FBO 操作,最后在 makeCurrent() 处崩掉。
  3. 临时规避可以关 OpenGL 或只保留一个实例开 OpenGL;真正修复要么给 QCustomPlot 打上下文校验补丁,要么让每个实例用独立的 QOpenGLWidget 父控件做上下文隔离。

1. 今天的内网开发现场

今天在内网环境里折腾 Qt 5.10 的一个界面,外网查资料不方便,只能翻本地文档、源码和之前攒的社区记录。原本需求不复杂:一个 QWidget 主界面,里面要嵌一个 QWindow;同时界面上还有多个 QCustomPlot,放在 QStackedWidget 的不同页面里,切页显示。

结果两个坑连着踩:

  • 第一个坑:QWindow 直接往 QWidget 布局里塞,不行;后来加 QWidget::createWindowContainer() 才正常。
  • 第二个坑:多个 QCustomPlot 放进 QStackedWidget,一切页就崩,断点经常落在 QOpenGLContext::makeCurrent() 附近。当场试了几种办法都没根治,今天没能在内网里“当场解决”,但把问题钉死了。

这篇笔记就是把今天的现场记录、尝试记录和最终归因整理清楚,给以后的自己省点加班。


2. 现场记录:我试过什么,为什么没解决

2.1 QWidget 装 QWindow:直接放不行,createWindowContainer 才行

一开始想当然地把 QWindow 当普通部件往 QWidget 布局里加,结果不按预期工作。后来改成:

QWindow *window = ...;
QWidget *container = QWidget::createWindowContainer(window, parentWidget);
layout->addWidget(container);

界面才正常。

这里的问题出在哪: QWidget 和 QWindow 不是同一层的东西。多个 QWidget 通常共享顶层原生窗口句柄,由 Qt 的 Widgets 绘制体系调度;QWindow 则直接对应一个由操作系统管理的原生窗口,有自己的原生句柄。直接把 QWindow 塞进 QWidget 布局,在窗口句柄所有权和事件路由上就缺少合法挂载点。

createWindowContainer() 做的事是:在 Widget 层级里强制创建一个原生子窗口,把目标 QWindow reparent 到这个原生子窗口下,再由容器接管它的几何形状和可见性。Qt 官方博客和文档对这套机制表述一致。所以在 Qt 5.10 的 Widgets 体系里,QWidget 嵌 QWindow 必须走这个容器,不是可选项。

2.2 多个 QCustomPlot 放进 QStackedWidget:切页崩

界面结构大致是:

QStackedWidget *stack = new QStackedWidget;
QCustomPlot *plot1 = new QCustomPlot;
QCustomPlot *plot2 = new QCustomPlot;
QCustomPlot *plot3 = new QCustomPlot;

plot1->setOpenGl(true);
plot2->setOpenGl(true);
plot3->setOpenGl(true);

stack->addWidget(plot1);
stack->addWidget(plot2);
stack->addWidget(plot3);

现象是:切页时崩,或者静置一会儿再操作时崩;断点常落在 QOpenGLContext::makeCurrent() 相关调用。曲线错乱、数据串扰也偶发。

我当场试过这些:

尝试结果说明
切页前先 hide() 旧页、再 show() 新页仍可能崩没有解决上下文归属问题
延迟初始化、切页后手动 replot()/update()仍可能崩或只是降低触发概率不是根治
切页时重建 QCustomPlot不崩了但代价大属于绕路,不是修复
只给一个 QCustomPlot 开 setOpenGl(true),其他关掉不崩了这是关键验证:问题指向 OpenGL 上下文冲突,而不是布局或 QStackedWidget 本身
全部关 OpenGL不崩同样说明崩溃发生在 OpenGL 绘制路径

所以当场“没解决”的意思是:没有在不改库、不调架构的前提下找到一个既保留多实例 OpenGL 加速又不崩的方案。但上面的验证已经把范围缩得很小。


3. 根因定位:问题出在 QCustomPlot 的 OpenGL 上下文管理

3.1 崩溃点为什么在 makeCurrent()

QCustomPlot 开 OpenGL 后,会通过 QCPPaintBufferGlFbo 做离屏渲染。社区开发者审查 qcustomplot.cpp 时确认:QCPPaintBufferGlFbo 的其他成员函数里通常有“当前上下文是否等于本实例 mGLContext”的检查,唯独 draw() 路径没有校验 QOpenGLContext::currentContext() 是否就是本实例持有的 mGLContext,而是直接执行 mGlFrameBuffer->toImage() 并继续绘制。

Qt 源码注释里有一条很关键的说明:用同一个 QSurface 对两个不同的 QOpenGLContext 对象调用 makeCurrent(),第二次调用不一定会切换到不同的原生上下文;因此第二次调用之后执行的状态变更,可能同时改变第一个上下文的状态。 换句话说,在 QStackedWidget 切页时,前一个 QCustomPlot 的上下文可能还处于“当前”状态;后一个实例如果没有先切到自己的 mGLContext 就做 FBO 操作和 makeCurrent(),就会在错误上下文上继续跑,崩溃或状态污染就来了。

这也解释了为什么“只留一个 OpenGL 实例”就不崩:上下文冲突的触发条件被拿掉了。但这是规避,不是修复。

3.2 为什么说不是 QStackedWidget 的锅

QStackedWidget 只负责页面切换和可见性管理,它不拥有也不应该替 QCustomPlot 管理 OpenGL 上下文。崩溃发生在 QCustomPlot 的 OpenGL 绘制路径内部,且可通过“只留一个 OpenGL 实例”消除,说明触发条件是多个 QCustomPlot 实例之间的上下文一致性缺失。把锅扣给 QStackedWidget 不成立。

同样,createWindowContainer 是 QWidget 嵌 QWindow 的机制,和 QCustomPlot 的 FBO 绘制是两件事,不能混为一谈。


4. 明确结论与以后怎么做

结论一:QWidget 嵌 QWindow,必须用 createWindowContainer

在 Qt 5.10 的 Widgets 体系里,QWidget::createWindowContainer() 是合法且必要的挂载方式。它通过创建原生子窗口并 reparent 目标 QWindow,解决了两套窗口体系在原生句柄所有权上的结构性差异。直接往 QWidget 布局里塞 QWindow,不是“不推荐”,而是机制上不成立。

结论二:多 QCustomPlot 在 QStackedWidget 崩溃,根因是 QCustomPlot 的 OpenGL 上下文管理不完整

具体问题点:QCPPaintBufferGlFbo::draw() 缺少对 QOpenGLContext::currentContext() == mGLContext 的校验与显式切换。页面切换等上下文边界场景下,FBO 绘制可能发生在错误上下文上,进而在 makeCurrent() 处崩溃。这不是 QStackedWidget 的缺陷,也不是 createWindowContainer 引起的。

结论三:修复路径是确定的

  • 根治方案 A: 在 QCustomPlot 源码的 QCPPaintBufferGlFbo::draw() 中补上下文一致性检查,不一致时显式 makeCurrent() 到本实例的 mGLContext。社区已有对应修复记录,并在多实例场景下验证有效。
  • 根治方案 B: 从架构上给每个 QCustomPlot 实例提供独立的 QOpenGLWidget 父控件,用 Qt 自身的上下文隔离机制消除冲突。
  • 临时规避: 关闭 OpenGL,或只保留一个实例开 OpenGL。能躲开崩溃,但不属于修复。

5. 收工一句话

今天没白加班:第一个坑确认了 createWindowContainer 是 Qt 5.10 里 QWidget 嵌 QWindow 的必经之路;第二个坑把锅从 QStackedWidget 身上摘了下来,钉到了 QCustomPlot 的 QCPPaintBufferGlFbo::draw() 缺少上下文校验上。以后内网再遇到“多个 QCustomPlot 切页崩、断在 makeCurrent()”,先查 OpenGL 上下文一致性,再怀疑容器。

6. 一点题外感触

AI 能帮你快速查 API、对堆栈、给候选方案,但“问题到底出在哪一层”这个判断,还是得自己下。比如“只留一个 QCustomPlot 开 OpenGL 就不崩”这个验证,AI 可以建议,但真正把锅从 QStackedWidget 摘到 QCPPaintBufferGlFbo::draw() 上的,是现场断点和源码对照。至少在 vibe coding 能完全封装古典 coding 行为之前,独立思考这根拐杖还不能扔——AI 可以陪你走,但路得自己认。

7. 氛围组

发现停车场居然还有一位🐮🐎哈哈哈哈哈
图 1 发现停车场居然还有一位🐮🐎哈哈哈哈哈
老年人下班儿
图 2 老年人下班儿