A custom rendering in a Qt5 Widget
For a recent project, I chose Qt to handle my application (honestly to get rid of Win32 and MFC but to keep the C/C++ flavor). Then I searched a way to integrate my custom rendering engine into a Qt5 QWidget.
As Qt is pushing its new OpenGL rendering pipeline, it’s hard to find a correct and official way to do it beside using their QOpenGLWidget
.
Code
This is a base code example on how I did it in C++. For more details about the implementation, see below.
This code has been tested with Qt5, Visual Studio 2013/2015, Windows 7/8/10, 32/64 bits. It was also used to create a custom widget for Oculus, with success, supporting OpenGL 3.3 and Oculus SDK 0.7.
#include <QWidget>
#include <QResizeEvent>
#include <QApplication>
class WidgetCustomRender : public QWidget
{
Q_OBJECT;
public:
explicit WidgetCustomRender(QWidget* parent)
: QWidget{parent}
{
setAttribute(Qt::WA_NativeWindow);
setAttribute(Qt::WA_PaintOnScreen);
setAttribute(Qt::WA_NoSystemBackground);
}
virtual ~WidgetCustomRender() = default;
QPaintEngine* paintEngine() const override
{
return nullptr;
}
void render()
{
if(_updatePending == false)
{
_updatePending = true;
QApplication::postEvent(
this,
new QEvent{QEvent::UpdateRequest});
}
}
public:
bool continuousRender = false;
protected:
void paintEvent(QPaintEvent* paintEvent) override
{
if(_isInit == false)
_init();
render();
}
void showEvent(QShowEvent* showEvent) override
{
QWidget::showEvent(showEvent);
if(_isInit == false)
_init();
}
void resizeEvent(QResizeEvent* resizeEvent) override
{
QWidget::resizeEvent(resizeEvent);
auto sz = resizeEvent->size();
if((sz.width() < 0) || (sz.height() < 0))
return;
//
// put your resize code here...
//
// because Qt is not sending update request when resizing smaller
render();
}
bool event(QEvent* event) override
{
switch(event->type())
{
case QEvent::UpdateRequest:
_updatePending = false;
_doRender();
return true;
default:
return QWidget::event(event);
}
}
private:
void _doRender()
{
if(isVisible() == false)
return;
if(_isInit == false)
return;
// you may want to add some code to control rendering frequency
// and ensure you are not rendering too fast in case of continuous
// rendering...
// if you control the rendering frequency, don't forget to make a
// call to render() if you're not going to do the rendering...
//
// do your custom rendering here...
//
// next frame if rendering continuously
if(continuousRender == true)
render();
}
void _init()
{
// you can grab the native window handler (HWND for Windows) of
// this widget:
auto nativeWindowHandler = winId();
// do your init code here...
_isInit = true;
}
private:
bool _updatePending = false;
bool _isInit = false;
};
Implementation details
Constructor
Setting the attribute WA_NativeWindow
ensures the widget will have a native window created (i.e.: HWND
for Windows). It’s just a precaution, because as far as I tested, it seems that Qt is creating a native window for almost all widgets.
Setting both WA_PaintOnScreen
, WA_NoSystemBackground
and returning a nullptr
paint engine, tells Qt that you will handle the whole painting of the widget yourself.
Widget behaviour
The way I handle resizeEvent
may trigger two renderings. This comes from (I guess) an optimization where Qt will only trigger an UpdateRequest
event if the new size is bigger than the old size. As a result, on smaller resize, one rendering is triggered, on bigger resize, two rendering may be triggered (depending on the speed of the rendering).
_init()
is the place where you can be sure that the native window has been created and is valid. It’s where you should do any initialization related to the native window system (i.e.: creating an OpenGL context, initializing a Direct3D device, etc.).
Rendering
All rendering code should go inside _doRender()
.
The function _doRender()
is not and shouln’t be called directly. It is called, inside the event processing function, on UpdateRequest
event. This helps the rendering to be synchronized with other Qt events (like mouse or keyboard) and prevent simultaneous render calls.
When the widget needs to be refreshed or repainted just make a call to render()
, which generates an UpdateRequest
event.
In case of continuous rendering, render()
is automatically called at the end of the rendering. As a result, highest render frequency is limited by the speed of the processing of the Qt event pipeline which is fairly fast enough to reach very high rendering frequency.
In my project, I was not doing direct API calls (OpenGL, DirectX) in _doRender()
.
Instead, the API calls (i.e.: the real rendering calls) were registered in _doRender()
as command lists and executed in a separate thread.