Qt Weekly #20: Completing the offering: QOpenGLWindow and QRasterWindow

Together with the introduction of QOpenGLWidget, Qt 5.4 adds two more classes: QOpenGLWindow and QRasterWindow. Let us now look at the list of native window classes and OpenGL container widgets. The list may look long and confusing at first glance, but it is all quite logical so everything will fall into place quickly:

  • QWindow: Represents a native window in the windowing system. The fundamental window class in Qt 5. Every top-level window, be it widget or Quick based, will have a QWindow under the hood. Can also be used directly, without widgets or Quick, both for OpenGL and software rendered graphics. Has no dependencies to the traditional QWidget stack.
  • QRasterWindow: Convenience wrapper over QWindow for software rendered graphics.
  • QOpenGLWindow: Convenience wrapper over QWindow for OpenGL graphics. Optionally backed by a framebuffer object, but the default behavior (and thus performance) is equivalent to QWindow.
  • QOpenGLWidget: The modern replacement for Qt 4's QGLWidget. A widget for showing OpenGL rendered content. Can be used like any other QWidget. Backed by a framebuffer object.
  • QQuickWindow: A QWindow subclass for displaying a Qt Quick 2 (QML) scene.
  • QQuickView: Convenience wrapper for QQuickWindow for easy setup of scenes loaded from QML files.
  • QQuickWidget: The equivalent of QQuickView in the QWidget world. Like QOpenGLWidget, it allows embedding a Qt Quick 2 scene into a traditional widget-based user interface. Backed by a framebuffer object.

For completeness sake, it is worth noting two additional APIs:

  • QQuickRenderControl: Allows rendering Qt Quick 2 scenes into framebuffer objects, instead of targeting an on-screen QQuickWindow.
  • QWidget::createWindowContainer(): In Qt 5.1 & 5.2 the only way to embed Qt Quick 2 content (or in fact any QWindow) into a widget-based UI was via this function. With the introduction of QQuickWidget and QOpenGLWidget this approach should be avoided as much as possible. Its usage should be restricted to cases where it is absolutely neccessary to have a real native window embedded into the widget-based interface and the framebuffer object-based, more robust alternatives are not acceptable, or where it is known in advance that the user interface layout is such that the embedded window will not cause issues (for example because the embedded window does not care about input, is not part of complex layouts that often get resized, etc.).

We will now take a look at no. 2 & 3, the QWindow convenience wrappers.

Ever since the introduction of the QPA architecture and QWindow, that is, since Qt 5.0, it has been possible to create windows based on QWindow that perform custom OpenGL drawing. Such windows do not use any QWidget-derived widgets, instead they render everything on their own. A game or a graphics intensive application with its own custom user interface is a good example.

This is the most lightweight and efficient way to perform native OpenGL rendering with Qt 5. It is free from the underlying complexities of the traditional widget stack and can operate with nothing but the QtCore and QtGui modules present. On space-constrained embedded devices this can be a big benefit (no need to deploy QtWidgets or any additional modules).

Power and efficiency comes at a cost: A raw QWindow does not hide contexts, surfaces and related settings, and it does not provide any standard mechanism for triggering updates or opening a QPainter (backed by the OpenGL 2.0 paint engine) targeting the window's associated native window surface.

For example, a simple QWindow subclass that performs continous drawing (synchronized to the display's vertical refresh by the blocking swapBuffers call) both via QPainter and directly via OpenGL could look like the following:


class MyWindow : public QWindow
{
public:
    MyWindow() : m_paintDevice(0) {
        setSurfaceType(QSurface::OpenGLSurface);

QSurfaceFormat format; format.setDepthBufferSize(24); format.setStencilBufferSize(8); setFormat(format);

m_context.setFormat(format); m_context.create(); }

~MyWindow() { delete m_paintDevice; }

void exposeEvent(QExposeEvent *) { if (isExposed()) render(); }

void resizeEvent(QResizeEvent *) { ... }

void render() { m_context.makeCurrent(this);

if (!m_paintDevice) m_paintDevice = new QOpenGLPaintDevice; if (m_paintDevice->size() != size()) m_paintDevice->setSize(size());

QOpenGLFunctions *f = m_context.functions(); f->glClear(GL_COLOR_BIT | GL_DEPTH_BUFFER_BIT); // issue some native OpenGL commands

QPainter p(m_paintDevice); // draw using QPainter p.end();

m_context.swapBuffers(this);

// animate continuously: schedule an update QCoreApplication::postEvent(new QEvent(QEvent::UpdateRequest), this); }

bool event(QEvent *e) { if (e->type() == QEvent::UpdateRequest) { render(); return true; } return QWindow::event(e); }

private: QOpenGLContext m_context; QOpenGLPaintDevice *m_paintDevice; };

Now compare the above code with the QOpenGLWindow-based equivalent:


class MyWindow : public QOpenGLWindow
{
public:
    MyWindow() {
        QSurfaceFormat format;
        format.setDepthBufferSize(24);
        format.setStencilBufferSize(8);
        setFormat(format);
    }

void resizeGL(int w, int h) { ... }

void paintGL() { QOpenGLFunctions *f = context()->functions(); f->glClear(GL_COLOR_BIT | GL_DEPTH_BUFFER_BIT); // issue some native OpenGL commands

QPainter p(this); // draw using QPainter

// animate continuously: schedule an update update(); } };

That is a bit shorter, isn't it. The API familiar from QOpenGLWidget (initializeGL/resizeGL/paintGL) is there, together with the update() function and the ability to easily open a painter on the window. While QOpenGLWindow, when used this way, does not remove or add anything compared to raw QWindow-based code, it makes it easier to get started, while leading to shorter, cleaner application code.

QRasterWindow follows the same concept. While everything it does can be achieved with QWindow and QBackingStore, like in the raster window example, it is definitely more convenient. With QRasterWindow, the example in question can be reduced to something like the following:


class RasterWindow : public QRasterWindow
{
    void paintEvent(QPaintEvent *) {
        QPainter painter(this);
        painter.fillRect(0, 0, width(), height(), Qt::white);
        painter->drawText(QRectF(0, 0, width(), height()), Qt::AlignCenter, QStringLiteral("QRasterWindow"));
    }
};

Painters opened on a QOpenGLWindow are always backed by the OpenGL paint engine, whereas painters opened on a QRasterWindow are always using the raster paint engine, regardless of having OpenGL support enabled or available at all. This means that QRasterWindow, just like the traditional widgets, is available also in -no-opengl builds or in environments where OpenGL support is missing.

Now, what about incremental rendering? In the QRasterWindow example above there is strictly speaking no need to clear the entire drawing area on each paint. Had the application wished so, it could have continued drawing over the existing, preserved backing store content in each invocation of paintEvent(), as long as the window did not get resized. With QGLWidget, QWindow or the QOpenGLWindow example shown above this is not possible, unless preserved swap is enabled via the underlying windowing system interface, since on each paintGL() call the color buffer contents is effectively undefined. QOpenGLWidget does not have this problem since it is backed by a framebuffer object instead of targeting the window surface directly. The same approach can be applied to QOpenGLWindow too. Hence the introduction of the different update behaviors that can be set on a QOpenGLWindow.

Take the following QWindow-based code:


class MyWindow : public QWindow
{
public:
    MyWindow() : m_paintDevice(0), m_fbo(0), m_iter(0) {
        ... // like in the first snippet above
    }

...

void render() { m_context.makeCurrent(this);

if (!m_fbo || m_fbo->size() != size()) { delete m_fbo; m_fbo = new QOpenGLFramebufferObject(size(), QOpenGLFramebufferObject::CombinedDepthStencilAttachment); m_iter = 0; }

if (!m_paintDevice) m_paintDevice = new QOpenGLPaintDevice; if (m_paintDevice->size() != size()) m_paintDevice->setSize(size());

m_fbo->bind(); QPainter p(m_paintDevice);

// Draw incrementally using QPainter. if (!m_iter) p.fillRect(0, 0, width(), height(), Qt::white);

p.drawText(QPointF(10, m_iter * 40), QString(QStringLiteral("Hello from repaint no. %1")).arg(m_iter));

++m_iter;

p.end(); m_fbo->release();

// Now either blit the framebuffer onto the default one or draw a textured quad. ...

m_context.swapBuffers(this);

// animate continously: schedule an update QCoreApplication::postEvent(new QEvent(QEvent::UpdateRequest), this); }

private: QOpenGLContext m_context; QOpenGLPaintDevice *m_paintDevice; QOpenGLFramebufferObject *m_fbo; int m_iter; };

For brevity the code for getting the framebuffer's content onto the window surface is omitted. With QOpenGLWindow's PartialUpdateBlit or PartialUpdateBlend the same can be achieved in a much more concise way. Note the parameter passed to the base class constructor.


class MyWindow : public QOpenGLWindow
{
public:
    Window() : QOpenGLWindow(PartialUpdateBlit), m_iter(0) {
        QSurfaceFormat format;
        format.setDepthBufferSize(24);
        format.setStencilBufferSize(8);
        setFormat(format);
    }

void resizeGL(int, int) { m_iter = 0; }

void paintGL() { QPainter p(this);

// Draw incrementally using QPainter. if (!m_iter) p.fillRect(0, 0, width(), height(), Qt::white);

p.drawText(QPointF(10, m_iter * 40), QString(QStringLiteral("Hello from repaint no. %1")).arg(m_iter));

++m_iter;

update(); }

private: int m_iter; };

That's it, and there is no code omitted in this case. Internally the two are approximately equivalent. With the QOpenGLWindow-based approach managing the framebuffer object is no longer the application's responsibility, it is taken care by Qt. Simple and easy.


Blog Topics:

Comments