QGLWidget
概述
QGLWidget类是用于呈现OpenGL图形的小部件。
QGLWidget提供了显示集成到Qt应用程序中的OpenGL图形的功能。它很容易使用。继承它并使用子类,就像其他任何QWidget一样,额外的可以选择使用QPainter和标准OpenGL渲染命令。
注意:这个类是传统QtOpenGL模块的一部分,与其他QGL类一样,应该在新的应用程序中避免使用。相反,从Qt5.4开始,Qt推荐使用QOpenGLWidget和QOpenGL类。
QGLWidget提供了三个方便的虚拟函数,可以在子类中重写这些函数来执行典型的OpenGL任务:
- paintGL():渲染OpenGL场景。每当需要更新小部件时调用。
- resizeGL ():设置OpenGL视区、投影等。每当小部件调整了大小时都会调用该视区(并且当它第一次显示时也会调用,因为所有新创建的小部件都会自动获得一个调整大小的事件)。
- initializeGL():设置OpenGL呈现上下文,定义显示列表等。在第一次调用resizeGL ()或paintGL ()之前调用一次。
QGLWidget子类示例
继承后,主要是实现三个虚拟函数。
更新绘制
如果需要从paintGL()以外的地方触发重新绘制(典型的例子是使用计时器来动画场景),应该调用小部件的updateGL()函数。
当调用paintGL()、resizeGL()或initializeGL()时,小部件的OpenGL呈现上下文成为当前上下文。如果需要从其他地方调用标准的OpenGL API函数(例如,在小部件的构造函数或自己的paint函数中),则必须首先调用makeCurrent()。
QGLWidget提供了请求新显示格式的功能,您还可以使用自定义的呈现上下文创建小部件。
还可以在QGLwidget对象之间共享OpenGL显示列表(有关详细信息,请参阅QGLwidget构造函数的文档)。
请注意,在Windows下,重新编写QGLWidget时,必须重新创建属于QGLWidget的QGLContext。由于Windows平台的限制,这是必需的。这很可能会给子类化并在QGLwidget上安装了自己的QGLContext的用户带来问题。通过将QGLWidget放在一个虚拟小部件中,然后重新设置虚拟小部件(而不是QGLWidget),可以解决这个问题。这将完全避免这个问题,并且是我们为需要这种功能的用户推荐的。
在MacOS上,当Qt使用Cocoa支持构建时,QGLWidget不能将任何兄弟的widget放在自己的本体上。这是由于可可API的限制,苹果不支持。
覆盖层
如果底层系统支持覆盖,那么QGLWidget除了正常上下文之外还创建一个GL覆盖层上下文。
如果要使用覆盖层,请以格式指定。(注意:必须以传递给QGLWidget构造函数的格式请求覆盖。)GL部件还应实现以下部分或全部虚拟方法:
- paintOverlayGL()
- resizeOverlayGL()
- initializeOverlayGL()
这些方法与普通paintGL()等函数的工作方式相同,只是当覆盖上下文变为当前时将调用这些方法可以使用makeOverlayCurrent()显式地使覆盖上下文成为当前上下文,并且可以通过调用overlayContext()直接访问覆盖上下文(例如,请求其透明颜色)。
在默认视觉效果位于覆盖平面的X服务器上,非GL Qt窗口也可用于覆盖。
绘制技术
如上所述,子类QGLWidget以下方式呈现纯3D内容:
- 重新实现QGLWidget::initializeGL()和QGLWidget::resizeGL(),以设置OpenGL状态并提供从始至终的转换。
- 重新实现QGLWidget::paintGL()以绘制3D场景,只调用OpenGL函数在小部件上绘制。
若要在QGLWidget子类上绘制二维图形,需要重新实现QGLWidget:: paintEvent()并执行以下操作:
- 构造QPainter对象。
- 初始化它以在带有QPainer::begin()函数的小部件上使用。
- 使用QPainter的成员函数绘制基元。
- 调用QPainter::end()以完成绘制。
线程
从Qt4.8版开始,对线程化GL渲染的支持已经得到了改进。目前支持三种方案:
方案一:在线程中进行缓冲区交换。
在双缓冲上下文中交换缓冲区可能是一个同步的锁定调用,在某些GL实现中这可能是一个代价高昂的操作。尤其是嵌入式设备。在GPU进行缓冲区交换时让CPU空转并不是最佳选择。在这些情况下,可以在主线程中进行渲染,并在单独的线程中进行实际的缓冲区交换。这可以通过以下步骤完成:
- 步骤一:渲染完成后,在主线程中调用DoneCurrent()。
- 步骤二:调用QGLContext::moveToThread(swapThread)将上下文的所有权转移到交换线程。
- 步骤三:通知交换线程它可以获取上下文。
- 步骤四:使用makeCurrent()使交换线程中的呈现上下文成为当前上下文,然后调用swapBuffers()。
- 步骤五:在交换线程中调用DoneCurrent()。
- 步骤六:调用QGLContext::moveToThread(qApp->thread())并通知主线程交换已完成。
这样做将释放主线程,以便它可以继续处理UI事件或网络请求。即使涉及到上下文交换,也比在GPU完成交换操作时让主线程等待要好。请注意,这是高度依赖于实现的。
方案二:在线程中上载纹理。
在线程中进行纹理上载对于处理需要显示的大量图像的应用程序(例如照片库应用程序)可能非常有用。这在qt中通过现有的bindTexture() API得到支持。一个简单的方法是创建两个共享的QGLWidget。一个在主GUI线程中是当前的,另一个在纹理上载线程中是当前的。上载线程中的小部件从未显示,它仅用于与主线程共享纹理。对于通过bindTexture()绑定的每个纹理,通知主线程以便它可以开始使用该纹理。
方案三:使用QPainer在线程中绘制到QGLWidget中。
在Qt4.8中,可以使用单独线程中的QPainer绘制到QGLWidget中。请注意,这对于QGLPIxelBuffers和QGLframeBufferObjects也是可能的。由于这仅在GL2绘制引擎中受支持,因此需要OpenGL 2.0或OpenGL ES 2.0。
QGLwidgets只能在主GUI线程中创建。这意味着需要调用doneCurrent()才能从主线程释放GL上下文,然后其他线程才能将小部件拉入其中。然后,需要调用QGLContext::moveToThread()将上下文的所有权转移到要使其成为当前线程的线程。此外,当小部件被调整大小时,或者小部件的一部分暴露出来或者需要重新绘制时,主GUI线程将把调整大小和绘制事件分派给QGLWidget。因此,有必要处理这些事件,因为QGLWidget中的默认实现将尝试使QGLWidget的上下文成为当前上下文,这将再次干扰呈现到小部件中的任何线程。重新实现QGLWidget::paintEvent()和QGLWidget::resizeEvent(),通知渲染线程需要调整大小或更新,注意不要调用基类实现。如果要渲染动画,则可能根本不需要处理绘制事件,因为渲染线程正在进行定期更新。然后,只需重新实现QGLWidget::paintEvent()就可以了。
在进行线程渲染时,一般规则是:请注意,不同线程中的绑定和释放上下文必须由用户同步。GL呈现上下文在任何时候都只能在一个线程中是最新的。如果您试图在QGLwidget上打开一个QPainer,而该Widget的呈现上下文在另一个线程中是最新的,那么它将失败。
除此之外,还支持在单独的线程中使用原始GL调用进行渲染。
QOpenGLWidget
概述
QOpenGLWidget类是用于呈现OpenGL图形的小部件。
QOpenGLWidget提供显示集成到Qt应用程序中的OpenGL图形的功能。使用起来非常简单:让类继承它,并像其他QWidget一样使用子类,额外可以选择使用QPainer和标准的OpenGL渲染命令。
QOpenGLWidget提供了三个方便的虚拟函数,可以在子类中重新实现这些函数来执行典型的OpenGL任务:
- paintGL():渲染OpenGL场景。每当需要更新小部件时调用。
- resizeGL ():设置OpenGL视区、投影等。每当小部件调整了大小时都会调用该视区(并且当它第一次显示时也会调用,因为所有新创建的小部件都会自动获得一个调整大小的事件)。
- initializeGL():设置OpenGL呈现上下文,定义显示列表等。在第一次调用resizeGL ()或paintGL ()之前调用一次。
更新绘制
如果需要从paintGL()以外的地方触发重新绘制(典型的例子是使用计时器来动画场景),您应该调用小部件的update()函数来安排更新。
当调用paintGL()、resizeGL()或initializeGL()时,小部件的OpenGL呈现上下文成为当前上下文。如果需要从其他地方调用标准的OpenGL API函数(例如,在小部件的构造函数或自己的paint函数中),则必须首先调用makeCurrent()。
所有渲染都发生在OpenGL帧缓冲区对象中。makeCurrent()确保它在上下文中绑定。在paintGL()中的呈现代码中创建和绑定其他framebuffer对象时,请记住这一点。永远不要重新绑定ID为0的帧缓冲区。相反,调用defaultFrameBufferObject()获取应该绑定的ID。
当平台支持时,QOpenGLWidget允许使用不同的OpenGL版本和配置文件。只需通过setFormat()设置请求的格式。但是请记住,在同一窗口中拥有多个QOpenGLWidget实例需要它们都使用相同的格式,或者至少使用不使上下文不可共享的格式。要解决此问题,最好使用QSurfaceFormat::setDefaultFormat()而不是setFormat()。
注意:当请求OpenGL核心配置文件上下文时,在某些平台(例如MacOS)上,在构造QApplication实例之前调用QSurfaceFormat::setDefaultFormat()是必需的。这是为了确保上下文之间的资源共享保持功能性,因为所有内部上下文都是使用正确的版本和配置文件创建的。
绘制技术
子类QOpenGLWidget以以下方式呈现纯3D内容:
- 重新实现QGLWidget::initializeGL()和QGLWidget::resizeGL(),以设置OpenGL状态并提供从始至终的转换。
- 重新实现QGLWidget::paintGL()以绘制3D场景,只调用OpenGL函数在小部件上绘制。
若要在QGLWidget子类上绘制二维图形,需要重新实现QGLWidget:: paintEvent()并执行以下操作:
- 在paintGL()中,不要发出OpenGL命令,而是构造一个用于小部件的QPainter对象。
- 使用QPainter的成员函数绘制基元。
- 仍然可以发出直接的OpenGL命令。但是,必须确保通过调用画师的beginNativePainting()和endNativePainting()将这些内容括起来。
仅使用QPainter执行绘图时,也可以像对普通小部件执行一样执行绘图:通过重新实现paintEvent()。
- 重新实现paintEvent()函数。
- 构造针对小部件的QPainer对象。将小部件传递给构造函数或QPainter::begin()函数。
- 使用QPainter的成员函数绘制基元。
- 绘制完成后,销毁QPainter实例,或者,显式调用QPainter::end()。
调用OpenGL头文件和函数
在进行OpenGL函数调用时,强烈建议避免直接调用函数。相反,在面向现代、仅桌面的OpenGL时,更喜欢使用QOpenglFunctions(在制作可移植应用程序时)或版本化的变体(例如,QopenglFunctions_3_2_Core和类似的变体)。这样,应用程序将在所有qt构建配置中正常工作,包括执行动态OpenGL实现加载的配置,这意味着应用程序没有直接链接到GL实现,因此直接函数调用是不可行的。
在paintGL()中,始终可以通过调用QOpenGLContext::currentContext()访问当前上下文。通过调用QOpenGLContex::Functions(),可以从此上下文中检索已初始化、准备好使用的QOpenGLFunctions实例。为每个GL调用添加前缀的另一种方法是从QOpenGLFunctions继承并在InitializeGL()中调用QOpenGLFunctions::nitializeOpenGLFunctions()。
至于OpenGL头文件,请注意,在大多数情况下,不需要直接包含任何头文件,如gl.h。OpenGL相关的qt报头将包括qopengl.h,而qopengl.h又将包含适合系统的报头。这可能是OpenGL ES 3.x或2.0头文件,可用的最高版本,或者是系统提供的gl.h。此外,还为OpenGL和OpenGL ES提供了扩展头文件(在某些系统上称为glext.h)的副本,作为qt的一部分。在可行的情况下,这些将自动包含在平台上。这意味着来自arb、ext、oes扩展的常量和函数指针typedef将自动可用。
代码示例
最简单的QOpenGLWidget子类可以如下所示: