一、描述
QOpenGLWidget 类是一个用于渲染 OpenGL 图形的小部件。
QOpenGLWidget 提供了显示集成到 Qt 应用程序中的 OpenGL 图形的功能。它使用起来非常简单:自定义类继承它并像其他 QWidget 子类一样使用,还可以选择使用 QPainter 和标准 OpenGL 渲染命令。
QOpenGLWidget 提供了三个方便的虚函数,可以在子类中重新实现这些虚函数来执行典型的 OpenGL 任务:
- paintGL():渲染 OpenGL 场景,每当需要更新小部件时调用。
- resizeGL():设置 OpenGL 视口、投影等。在小部件调整大小时调用(以及首次显示时,因为所有新创建的小部件都会自动获得调整大小事件)。
- initializeGL():设置 OpenGL 资源和状态。在第一次调用 resizeGL() 或 paintGL() 之前被调用一次。
如果需要从 paintGL() 以外的地方触发重绘,应该调用小部件的 update() 函数来安排更新。
当调用 paintGL()、resizeGL()、initializeGL() 时,小部件的 OpenGL 渲染上下文将变为当前状态。如果需要从其他地方调用标准的 OpenGL API 函数(例如,在小部件的构造函数中或在自己的绘制函数中),必须首先调用 makeCurrent()。
所有渲染都发生在一个 OpenGL 帧缓冲区对象中。makeCurrent() 确保它被绑定在上下文中。
在请求 OpenGL 核心配置文件上下文时,在某些平台(例如 macOS)上,在构造 QApplication 实例之前调用 QSurfaceFormat::setDefaultFormat() 是强制性的。这是为了确保上下文之间的资源共享保持功能,因为所有内部上下文都是使用正确的版本和配置文件创建的。
1.1、绘制技巧
如上所述,子类 QOpenGLWidget 以通过以下方式呈现纯 3D 内容:
- 重新实现 initializeGL() 和 resizeGL() 函数来设置 OpenGL 状态并提供透视变换。
- 重新实现 paintGL() 来绘制3D 场景,只调用 OpenGL 函数。
也可以使用 QPainter 在 QOpenGLWidget 子类上绘制 2D 图形:
- 在 paintGL() 中,不是发出OpenGL 命令,而是构造一个QPainter 对象以在小部件上使用。
- 使用 QPainter 的成员函数绘制图元。
- 仍然可以发出直接的 OpenGL 命令。但是,必须确保这些都包含在对 QPainter 的 beginNativePainting() 和 endNativePainting() 的调用中。
QPainter painter(this);
painter.fillRect(0, 0, 128, 128, Qt::green);
painter.beginNativePainting();
glEnable(GL_SCISSOR_TEST);
glScissor(0, 0, 64, 64);
glClearColor(1, 0, 0, 1);
glClear(GL_COLOR_BUFFER_BIT);
glDisable(GL_SCISSOR_TEST);
painter.endNativePainting();
当仅使用 QPainter 进行绘图时,也可以像对普通小部件一样进行绘图:通过重新实现 paintEvent()。
- 重新实现 paintEvent() 函数。
- 构造一个以小部件为目标的 QPainter 对象。
- 使用 QPainter 的成员函数绘制图元。
- 绘画完成然后 QPainter 实例被销毁。
1.2、OpenGL 函数调用和 QOpenGLFunctions
在进行 OpenGL 函数调用时,强烈建议避免直接调用函数。建议使用 QOpenGLFunctions 。这样,应用程序将在所有 Qt 构建配置中正常工作,包括执行动态 OpenGL 实现加载的配置,这意味着应用程序不直接链接到 GL 实现,因此直接函数调用是不可行的。
在 paintGL() 中,当前上下文总是可以通过调用 QOpenGLContext::currentContext() 来访问。从这个上下文中,可以通过调用 QOpenGLContext::functions() 来检索一个已经初始化、准备好使用的 QOpenGLFunctions 实例。为每个 GL 调用添加前缀的另一种方法是从 QOpenGLFunctions 继承并在 initializeGL() 中调用 QOpenGLFunctions::initializeOpenGLFunctions()。
1.3、代码示例
首先,最简单的 QOpenGLWidget 子类可能如下所示:
class MyGLWidget : public QOpenGLWidget
{
public:
MyGLWidget(QWidget *parent) : QOpenGLWidget(parent) { }
protected:
void initializeGL() override
{
// 设置渲染上下文、加载着色器和其他资源等
QOpenGLFunctions *f = QOpenGLContext::currentContext()->functions();
f->glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
...
}
void resizeGL(int w, int h) override
{
// 更新投影矩阵和其他尺寸相关设置:
m_projection.setToIdentity();
m_projection.perspective(45.0f, w / float(h), 0.01f, 100.0f);
...
}
void paintGL() override
{
QOpenGLFunctions *f = QOpenGLContext::currentContext()->functions();
f->glClear(GL_COLOR_BUFFER_BIT);
...
}
};
或者,可以通过从 QOpenGLFunctions 派生来避免每个 OpenGL 调用的前缀:
class MyGLWidget : public QOpenGLWidget, protected QOpenGLFunctions
{
...
void initializeGL() override
{
initializeOpenGLFunctions();
glClearColor(...);
...
}
...
};
要获得与给定 OpenGL 版本或配置文件兼容的上下文,或请求深度和模板缓冲区,请调用 setFormat():
QOpenGLWidget *widget = new QOpenGLWidget(parent);
QSurfaceFormat format;
format.setDepthBufferSize(24);
format.setStencilBufferSize(8);
format.setVersion(3, 2);
format.setProfile(QSurfaceFormat::CoreProfile);
widget->setFormat(format); // 必须在小部件或其父窗口显示之前调用
对于 OpenGL 3.0+ 上下文,当可移植性不重要时,版本化的 QOpenGLFunctions 变体可以轻松访问给定版本中可用的所有现代 OpenGL 函数:
void paintGL() override
{
QOpenGLFunctions_3_2_Core *f = QOpenGLContext::currentContext()->versionFunctions<QOpenGLFunctions_3_2_Core>();
...
f->glDrawArraysInstanced(...);
...
}
全局设置请求的格式更简单,更健壮,以便它在应用程序的生命周期内应用于所有窗口和上下文。 下面是一个例子:
int main(int argc, char **argv)
{
QApplication app(argc, argv);
QSurfaceFormat format;
format.setDepthBufferSize(24);
format.setStencilBufferSize(8);
format.setVersion(3, 2);
format.setProfile(QSurfaceFormat::CoreProfile);
QSurfaceFormat::setDefaultFormat(format);
MyWidget widget;
widget.show();
return app.exec();
}
1.4、线程相关
OpenGLContext 支持在工作线程上执行屏幕外渲染,例如生成纹理,然后在 paintGL() 中的 GUI 主线程中使用,以便可以在每个线程上创建与其共享的其他上下文。
通过重新实现 paintEvent() 以不执行任何操作,可以直接在GUI/主线程之外绘制 QOpenGLWidget 的帧缓冲区。必须通过 QObject::moveToThread() 更改上下文的线程。之后,makeCurrent() 和 doneCurrent() 可以在工作线程上使用。之后将上下文移回 GUI/主线程。
当一个线程完成更新帧缓冲区时,在 GUI/主线程上调用 update() 来安排合成。
当 GUI/主线程执行合成时,必须特别注意避免使用帧缓冲区。当合成开始和结束时,将发出 aboutToCompose() 和 frameSwapped() 信号。它们在 GUI/主线程上发出。这意味着通过使用直接连接 aboutToCompose() 可以阻塞 GUI/主线程,直到工作线程完成其渲染。之后,工作线程必须在发出 frameSwapped() 信号之前不再执行进一步的渲染。如果这是不可接受的,工作线程必须实现双缓冲机制。
1.5、上下文共享
当多个 QOpenGLWidgets 作为子级添加到同一个顶级小部件时,它们的上下文将相互共享。
这意味着同一窗口中的所有 QOpenGLWidget 都可以访问彼此的可共享资源,例如纹理。
要设置属于不同窗口的 QOpenGLWidget 实例之间的共享,请在实例化 QApplication 之前设置 Qt::AA_ShareOpenGLContexts 应用程序属性。这将触发所有 QOpenGLWidget 实例之间的共享。
创建额外的 QOpenGLContext 实例以与 QOpenGLWidget 的上下文共享资源也是可能的。在调用 QOpenGLContext::create() 之前,只需将 context() 返回的指针传递给 QOpenGLContext::setShareContext()。生成的上下文也可以在不同的线程上使用。
1.6、资源初始化和清理
每当调用 initializeGL() 和 paintGL() 时,保证 QOpenGLWidget 的关联 OpenGL 上下文是最新的。在调用 initializeGL() 之前不要尝试创建 OpenGL 资源。例如,在子类的构造函数中尝试编译着色器、初始化顶点缓冲区对象或上传纹理数据将失败。这些操作必须推迟到 initializeGL()。Qt 的一些 OpenGL 辅助类,如 QOpenGLBuffer 或 QOpenGLVertexArrayObject,它们可以在没有上下文的情况下实例化,但所有初始化都延迟到 create() 或类似调用。
释放资源还需要当前的上下文。因此,执行此类清理的析构函数应在继续销毁任何 OpenGL 资源或包装器之前调用 makeCurrent()。
因此,在资源初始化和销毁方面,典型的子类通常如下所示:
class MyGLWidget : public QOpenGLWidget
{
...
private:
QOpenGLVertexArrayObject m_vao;
QOpenGLBuffer m_vbo;
QOpenGLShaderProgram *m_program;
QOpenGLShader *m_shader;
QOpenGLTexture *m_texture;
};
MyGLWidget::MyGLWidget()
: m_program(0), m_shader(0), m_texture(0)
{
//此处没有进行 OpenGL 资源初始化
}
MyGLWidget::~MyGLWidget()
{
// 确保上下文是当前的,然后明确销毁所有底层 OpenGL 资源
makeCurrent();
delete m_texture;
delete m_shader;
delete m_program;
m_vbo.destroy();
m_vao.destroy();
doneCurrent();
}
void MyGLWidget::initializeGL()
{
m_vao.create();
if (m_vao.isCreated())
m_vao.bind();
m_vbo.create();
m_vbo.bind();
m_vbo.allocate(...);
m_texture = new QOpenGLTexture(QImage(...));
m_shader = new QOpenGLShader(...);
m_program = new QOpenGLShaderProgram(...);
...
}
另外一种解决方案是使用 QOpenGLContext 的 aboutToBeDestroyed() 信号。通过连接此信号,可以在底层本机上下文句柄或整个 QOpenGLContext 实例将被释放时执行清理。以下代码段原则上等同于前一个代码段:
void MyGLWidget::initializeGL()
{
// context() 和 QOpenGLContext::currentContext() 在从 initializeGL 或 paintGL 调用时是等价的。
connect(context(), &QOpenGLContext::aboutToBeDestroyed, this, &MyGLWidget::cleanup);
}
void MyGLWidget::cleanup()
{
makeCurrent();
delete m_texture;
m_texture = 0;
...
doneCurrent();
}
当设置 Qt::AA_ShareOpenGLContexts 时,小部件的上下文永远不会改变,即使在重新设置父级时也不会改变,因为保证了小部件的关联纹理也可以从新的顶级上下文访问。
由于上下文共享,适当的清理尤为重要。即使每个 QOpenGLWidget 的关联上下文与 QOpenGLWidget 一起被销毁,该上下文中的可共享资源(如纹理)将保持有效,直到 QOpenGLWidget 所在的顶级窗口被销毁。此外,像 Qt::AA_ShareOpenGLContexts 和一些 Qt 模块这样的设置可能会触发更广泛的上下文共享范围,从而可能导致相关资源在应用程序的整个生命周期内保持活跃。因此,最安全和最健壮的方法始终是对 QOpenGLWidget 中使用的所有资源和资源包装器执行显式清理。
1.7、限制
将其他小部件放在下面并使 QOpenGLWidget 透明将会:下面的小部件将不可见。这是因为在实践中 QOpenGLWidget 是在所有其他常规的、非 OpenGL 小部件之前绘制的,因此透明类型的解决方案是不可行的。其他类型的布局,例如在 QOpenGLWidget 之上的小部件,将按预期运行。
当绝对必要时,可以通过在 QOpenGLWidget 上设置 Qt::WA_AlwaysStackOnTop 属性来取消这个限制。但是注意,这会破坏堆叠顺序,例如,在 QOpenGLWidget 之上不可能有其他小部件,因此它应该只用于需要在下面看到其他小部件的半透明 QOpenGLWidget 的情况。
请注意,当下面没有其他小部件并且意图是有一个半透明窗口时,这不适用。在这种情况下,在顶级窗口上设置 Qt::WA_TranslucentBackground 的传统方法就足够了。请注意,如果仅在 QOpenGLWidget 中需要透明区域,则在启用 Qt::WA_TranslucentBackground 后需要将 Qt::WA_NoSystemBackground 设置回 false。此外,可能还需要通过 setFormat() 为 QOpenGLWidget 的上下文请求 alpha 通道,具体取决于系统。
QOpenGLWidget 支持多种更新行为。在保留模式下,来自上一个 paintGL() 调用的渲染内容在下一个调用中可用,从而允许增量渲染。在非保留模式下,内容会丢失,并且 paintGL() 实现预计会重绘视图中的所有内容。
二、类型成员
1、enum QOpenGLWidget::UpdateBehavior:此枚举描述了 QOpenGLWidget 的更新方式。
- NoPartialUpdate:将在 QOpenGLWidget 渲染到屏幕后丢弃颜色缓冲区和辅助缓冲区的内容。当帧缓冲区对象用作渲染目标时,NoPartialUpdate 可以在移动和嵌入式空间中常见的某些硬件架构上获得一些性能优势。
- PartialUpdate:帧缓冲区对象、颜色缓冲区和辅助缓冲区在帧之间不会失效。
三、成员函数
1、【信号】void aboutToCompose()
当小部件的顶级窗口即将开始合成其 QOpenGLWidget 子窗口和其他窗口小部件的纹理时,会发出此信号。
2、【信号】void aboutToResize()
当小部件的大小发生变化时会发出此信号,此时将重新创建帧缓冲区对象。
3、【信号】void frameSwapped()
在窗口小部件的顶级窗口完成合成并从其可能阻塞的 QOpenGLContext::swapBuffers() 调用返回后发出此信号。
4、【信号】void resized()
由于调整小部件的大小,在重新创建帧缓冲区对象后发出此信号。
5、QOpenGLContext * context()
返回此小部件使用的 QOpenGLContext。
通过 setParent() 重设小部件的父级时,小部件使用的上下文和帧缓冲区对象会发生变化。
6、GLuint defaultFramebufferObject()
返回帧缓冲区对象句柄。帧缓冲区对象属于 context() 返回的上下文。
当通过 setParent() 重新设置小部件的父级时,小部件使用的上下文和帧缓冲区对象会发生变化。 此外,帧缓冲区对象在每次调整大小时都会发生变化。
7、void doneCurrent()
释放上下文。
大多数情况下没有必要调用这个函数,因为当调用 paintGL()时会确保上下文被正确的绑定和释放。
void makeCurrent()
通过使相应的上下文成为当前上下文并在该上下文中绑定帧缓冲区对象来准备为此小部件呈现 OpenGL 内容。
大多数情况下不需要调用这个函数,因为它是在调用 paintGL()之前自动调用的。
8、QImage grabFramebuffer()
渲染并返回帧缓冲区的 32 位 RGB 图像。
这是一个潜在的昂贵操作,因为它依赖 glReadPixels() 来读回像素。这可能会很慢,并且可能会导致 GPU 渲染管线停止。
9、void initializeGL()
这函数在第一次调用 paintGL() 或 resizeGL() 之前被调用一次。
应该在这个函数中设置任何需要的 OpenGL 资源和状态。
无需调用 makeCurrent(),因为调用此函数时已完成。但是请注意,在此阶段帧缓冲区尚不可用,因此请避免从此处发出绘图调用。
10、bool isValid()
如果小部件和 OpenGL 资源(如上下文)已成功初始化,则返回 true。
在显示小部件之前,返回值始终为 false。
11、void paintEvent(QPaintEvent *e)
这个函数会在经过一些准备之后调用 paintGL() 来更新QOpenGLWidget 的帧缓冲区的内容。
12、void paintGL()
每当需要绘制小部件时,都会调用此函数。
无需调用 makeCurrent(),因为调用此函数时已完成。
13、void resizeEvent(QResizeEvent *e)
处理在 e 事件参数中传递的调整大小事件。调用虚函数 resizeGL()。
注意:避免在派生类中覆盖此函数。 如果这不可行,请确保 QOpenGLWidget 的实现也被调用。 否则底层的帧缓冲区对象和相关资源将无法正确调整大小并导致不正确的渲染。
14、void resizeGL(int w, int h)
每当调整小部件的大小时,都会调用此函数。新大小在 w 和 h 中传递。
无需调用 makeCurrent(),因为调用此函数时已完成。 此外,还绑定了帧缓冲区。
15、void setFormat(const QSurfaceFormat &format)
当格式没有通过这个函数显式设置时,将使用 QSurfaceFormat::defaultFormat() 返回的格式。这意味着当有多个 OpenGL 小部件时,在创建第一个小部件之前,可以用对 QSurfaceFormat::setDefaultFormat() 的单个调用来替换对该函数的单独调用。
16、void setTextureFormat(GLenum texFormat)
设置 texFormat 的自定义内部纹理格式。
使用 sRGB 帧缓冲区时,需要指定格式,如 GL_SRGB8_ALPHA8。 这可以通过调用这个函数来实现。
注意:如果在小部件已显示并因此执行初始化之后调用此函数,则该函数无效。
注意:此函数通常必须与将颜色空间设置为 QSurfaceFormat::sRGBColorSpace 的 QSurfaceFormat::setDefaultFormat() 调用结合使用。
17、void setUpdateBehavior(QOpenGLWidget::UpdateBehavior updateBehavior)
小部件的更新方式设置。