QStackedWidget 里多个 QCustomPlot 为什么会崩
内网踩坑笔记:QWidget 套 QWindow,以及 QStackedWidget 里多个 QCustomPlot 为什么会崩
结论先行:
- Qt 5.10 里 QWidget 想装 QWindow,必须走
QWidget::createWindowContainer(),这不是“可选封装”,而是跨 QWidget 与 QWindow 两套窗口体系的结构性桥梁。- 多个 QCustomPlot 放进
QStackedWidget崩溃,锅不在QStackedWidget,也不在createWindowContainer。根因在 QCustomPlot 的 OpenGL 绘制路径:QCPPaintBufferGlFbo::draw()缺少“当前 OpenGL 上下文是否就是本实例mGLContext”的校验,导致在错误上下文上做 FBO 操作,最后在makeCurrent()处崩掉。- 临时规避可以关 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. 氛围组
- 上一篇:根据QComboBox调整界面label并整理至json
- 下一篇:没有了