SDL2/Native GUI - How to embed an SDL2 context into a QT application.

Note: For people who just want the source code, it can be found here.

Preface

A while ago I made a post talking about embedding SDL2 into a WxWidgets UI. It worked well but performance was pretty horrible, so I experimented further with different libraries. Turns out Qt (pronounced 'cute') works alongside SDL very well, with a few hacks. Unlike last time, these hacks give you pretty sturdy results and best of all; hardware accelerated rendering.

Let's make a really simple implementation.

The Parent Class

We'll inherit from this class for every custom SDL widget we want to make.

Here's the definition for our parent class.

* Please see the comments in the given code for more detail. This just makes everything a little easier to read.

// sdlwidget.h
#pragma once
#include <QWidget>
#include <QTimer>
#include <SDL2/SDL.h>

/* If you'd like, you can just write
 * `#define MS_PER_FRAME 17` here since that's all this is doing,
 * but I like making sure that the math is right (by not doing it myself). */
#define FRAME_RATE 60
#define MS_PER_FRAME 1000 / FRAME_RATE

class SDLWidget : public QWidget
{
public :
  SDLWidget(QWidget* parent);
  virtual ~SDLWidget() override;

/* A custom repaint slot, I'll explain this later */
public slots:
  void SDLRepaint();

protected:
  SDL_Window* window;
  SDL_Renderer* renderer;

private:
  /* To be overridden by child classes */
  virtual void Init() = 0;
  virtual void Update() = 0;
  virtual void OnResize(int w, int h) = 0;

  void resizeEvent(QResizeEvent* event) override;
  void showEvent(QShowEvent*) override;

  /* Overriding this method prevents the
   * "QWidget::paintEngine: Should no longer be called" error. */
  QPaintEngine* paintEngine() const override;

  /* Timer used to call the re-render method every 1/60th of a second */
  QTimer frameTimer;
};

Implementations of Parent Class methods

The parent class constructor (setting up the widget ready to render onto):

// sdlwidget.cpp
#include <sdlwidget.h>

SDLWidget::SDLWidget(QWidget *parent) : QWidget(parent) {
  // These widget attributes are important. This lets SDL take over what's
  // drawn onto the widget's screen space. 
  setAttribute(Qt::WA_OpaquePaintEvent);
  setAttribute(Qt::WA_NoSystemBackground);
  setFocusPolicy(Qt::StrongFocus);
  // HOWEVER, this important attribute stops the "paintEvent" slot from being called,
  // thus we'll need to write our own method that paints to the screen every frame.
  setAttribute(Qt::WA_PaintOnScreen);

  // Create an SDL window from the widget's native handle.
  window = SDL_CreateWindowFrom(reinterpret_cast<void*>(winId()));
  if(window == NULL)
    throw "Can't create window: " + std::string(SDL_GetError());

  // Create a renderer from the window. We'll use this for drawing all our stuff.
  // (NOTE this doesn't have the VSYNC flag. 
  //  It'd cause HEAVY flickering, so we'll do our own vsync.)
  renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
  if(renderer == NULL)
    throw "Can't create renderer: " + std::string(SDL_GetError());
}

Every frame, we want to update and display the renderer so it shows on screen. What we want to render in a frame may change depending on the widget, so for now we need a generic method that simply clears and presents the renderer.

We'll call the virtual method "Update" in between. This method must be overridden by the child with user-defined behavior such as drawing squares or images.

/* Our custom slot that we call 
   every frame with the frameTimer. */
void SDLWidget::SDLRepaint() {
  SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
  SDL_RenderClear(renderer);
  Update();
  SDL_RenderPresent(renderer);
}

Now we'll define our event overrides which are part of the QWidget class we inherit from.

/* Called when the widget is resized.
   Calls the child's "OnResize" method */
void SDLWidget::resizeEvent(QResizeEvent*) {
  int x = static_cast<int>(QWidget::width());
  int y = static_cast<int>(QWidget::height());
  OnResize(x, y);
}

/* This method is an event called when our widget is created.
   It calls the child's "Init" method and starts our frame timer,
   which will call "SDLWidget::SDLRepaint" every frame. */
void SDLWidget::showEvent(QShowEvent*) {
  Init();
  /* frameTimer will send signal timeout() every 60th of a second, connect to "repaint" */
  connect(&frameTimer, &QTimer::timeout, this, &SDLWidget::SDLRepaint);
  frameTimer.setInterval(MS_PER_FRAME);
  frameTimer.start();
}

/* Override default system paint engine to prevent errors. */
QPaintEngine* SDLWidget::paintEngine() const {
  return reinterpret_cast<QPaintEngine*>(0);
}

And lastly, our deallocator for our renderer and window. To keep it simple, we're not using SDL_Textures yet, but once you start using them, destroy them at this point.

SDLWidget::~SDLWidget() {
  SDL_DestroyRenderer(renderer);
  SDL_DestroyWindow(window);
}

A Custom Child Example

Now we have a generic SDL widget which we can inherit from and do whatever we want with, like drawing images, shapes or even embedding games (given you know how you can implement an event loop from here).

All we need to do with this child class is:

// example.h
#pragma once
#include <sdlwidget.h>

class Example : public SDLWidget
{
public:
    Example(QWidget* parent) : SDLWidget(parent) {}
private:
    void Init();
    void Update();
    void OnResize(int w, int h);
};

Now we'll implement our custom behavior for this example widget. All this does is draw a red rectangle at the top left of our screen.

// example.cpp
#include "example.h"

void Example::Init() {}

void Example::Update() {
    SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255);
    SDL_RenderFillRect(renderer, new SDL_Rect{0,0,300,200} );
}

void Example::OnResize(int, int) {}

One more thing...

In your main file, make sure to initialise SDL.

#include "mainwindow.h"
#include <QApplication>
#include <SDL2/SDL.h>
// undefine main otherwise SDL overrides it.
#undef main

int main(int argc, char *argv[])
{
    SDL_Init(SDL_INIT_EVERYTHING);

    // Your main QT application code..
    QApplication a(argc, argv);
    MainWindow w;
    w.show();

    return a.exec();
}

And now your application is ready to have an embedded SDL2 context. Let's make a simple form and promote a basic QWidget to your new Example class.

This example will be using Qt Designer. Create a widget (in the containers section on the left side) and place it where you want your SDL context to be.

We want to promote this widget to our new Example class.

Right click the new simple widget and select 'Promote to ...'
Enter 'Example' into the promoted class name box and click Add.
Once added, select your Example class entry and click promote.'

And there you have it, that should be all you need to get started!

A QT window displaying an SDL widget which contains a red square in the top left corner.

If you want, you can try this out with more complex behavior in the child widget, such as rendering a texture or moving blocks around using the arrow keys (hint: SDL_PollEvent works in your Update function!).

Thanks for reading! Tweet at me if you have any questions.

:wq


0 cools!! 🩷

Go back home