Pré requis:

Un widget Qt comme cible de rendu pour SDL

Les moteurs graphiques des différents Qt ne sont souvent pas adaptés au travail graphique intense (sauf à passer par OpenGL, mais on détourne le problème - c'est d'ailleurs ce que l'on va faire ici).

Signifier à SDL la fenêtre à utiliser

Se servir d'un widget comme zone de rendu cible pour SDL est très simple: il suffit d'une variable d'environnement. Cette variable est SDL_WINDOWID et peut être de 2 formats:

  • un entier, tel quel
  • une valeur hexadécimale, préfixé par '0x'

Une variable valide est un handle de fenêtre (dont le format peut varier en fonction de l'environnement utilisé). Ce handle s'obtient par la méthode QWidget::winId(), qui est une méthode publique.

Une fois la zone de rendu définie par cette variable, on peut initialiser le moteur graphique de SDL. Voici par exemple le code permettant de réaliser cette intégration :

char windowid[64];
#ifdef Q_WS_WIN
	sprintf(windowid, "SDL_WINDOWID=0x%lx", reinterpret_cast<qlonglong>(winId()));
#elif defined Q_WS_X11
	sprintf(windowid, "SDL_WINDOWID=0x%lx", winId());
#else
	qFatal("Fatal: cast du winId() inconnu pour votre plate-forme; toute information est la bienvenue!");
#endif
SDL_putenv(windowid);
// Initialisation du système vidéo de SDL
SDL_Init(SDL_INIT_VIDEO);
screen = SDL_SetVideoMode(width(), height(), 32, SDL_SWSURFACE);

Attention : ne le faîtes pas l'initialisation du système vidéo de SDL avant que la création réelle de votre widget (le moment le plus sûr est lors du premier showEvent), sinon une seconde fenêtre sera créée par et pour SDL.

Configuration du widget de rendu

Le dessin direct par SDL sur un widget Qt implique une petite configuration des attributs de dessin du dit widget:

setAttribute(Qt::WA_PaintOnScreen);
setAttribute(Qt::WA_NoSystemBackground);

A faire lors de l'utilisation de SDL, sinon, vous aurez une bouillie de pixels "inspirée" par le fond de l'écran à cet emplacement ;)

Conflit de main

Il y a par contre un conflit entre SDL et Qt au niveau de la fonction main. Lorsque main n'est pas le point d'entrée d'un programme (ce qui est le cas sur win32 par exemple), Qt appelle main à partir de son implémentation de WinMain. SDL quant à lui fait un define (code extrait de SDL_main.h) :

#define main SDL_main
/* The prototype for the application's main() function */
extern C_LINKAGE int SDL_main(int argc, char *argv[]);

Pour supprimer ce problème, ils vous faut annuler la définition de SDL, juste après l'inclusion de SDL.h :

#include "SDL.h"
#undef main

Une fois ceci fait, vous pouvez utiliser sereinement le système vidéo de SDL au sein de votre application Qt :)

Modification du fichier projet

Comme toute librairie, son utilisation avec votre programme doit être expressément signalée sous peine d'erreurs de compilation et/ou de liaison. Les informations à ajouter dans le fichier projet sont :

  • le chemin vers les en-têtes SDL (ex: INCLUDEPATH += /usr/local/include/)
  • la librairie avec laquelle lier (et éventuellement son chemin) (ex: LIBS += -L/usr/local/lib/ -lSDL)

Exemple complet

Vous trouverez associé à ce tutoriel un exemple illustrant l'utilisation de la SDL pour un starfield - qui rappellera des souvenirs aux (ex-)possesseurs de vieilles machines et qui ont codé avant l’arrivée du S-VGA (définit comme étant haute-résolution à l’époque) ;-)

J'ai repris ici les points principaux de la classe :

[...]
 
#ifdef Q_WS_WIN
#include <SDL.h>
#elif defined Q_WS_X11
#include <SDL/SDL.h>
#endif
 
/* PIEGE: main est redéfini par SDL.h comme SDL_Main.
                On retire donc la définition puisque Qt le gère
*/
#undef main
 
[...]
 
class SDLWidget : public QWidget
{
    Q_OBJECT
 
public:
    SDLWidget()
    :refreshTimer(0), windowInitialized(false), screen(0), StarNumbers(100)
    {
        [...]
    }
 
    virtual ~SDLWidget()
    {
        SDL_Quit();
    }
 
protected:
    virtual void showEvent(QShowEvent *e)
    {
        if(!windowInitialized)
        {
            // C'est ici qu'on dis à SDL d'utiliser notre widget
            char windowid[64];
#ifdef Q_WS_WIN
 
			sprintf(windowid, "SDL_WINDOWID=0x%lx", reinterpret_cast<qlonglong>(winId()));
#elif defined Q_WS_X11
			sprintf(windowid, "SDL_WINDOWID=0x%lx", winId());
#else
			qFatal("Fatal: cast du winId() inconnu pour votre plate-forme; toute information est la bienvenue!");
#endif
            SDL_putenv(windowid);
 
            // Initialisation du système vidéo de SDL
            SDL_Init(SDL_INIT_VIDEO);
            screen = SDL_SetVideoMode(width(), height(), 32, SDL_SWSURFACE);
            windowInitialized = true;
        }
    }
 
private:
    [...]
 
private:
    [...]
 
private slots:
    void onRefresh()
    {
        if(windowInitialized && screen)
        {
            SDL_LockSurface(screen);
                // Nettoyage de l'écran
                SDL_FillRect(screen, NULL, 0);
                // Dessin du starfield
                drawStarfield();
            SDL_UnlockSurface(screen);
 
            // Rafraîchissement...
            SDL_UpdateRect(screen, 0, 0, 0, 0);
 
            // Et enfin, mise à jour des positions des étoiles
            updateStarfield();
        }
    }
 
private:
    [...]
};

N’oubliez pas d’ajouter les références d’inclusion et de bibliothèques de SDL au .pro, et laissez vous aller à un peu de nostalgie ;-)

Utilisation avancée

L'ensemble du code illustrant l'utilisation décrite ici est disponible sur la seconde pièce jointe.

La méthode décrite précédemment est suffisante si vous n'avez besoin de SDL que dans un seul widget. Mais si vous voulez utiliser SDL comme système d'affichage pour plusieurs widgets, alors il faut changer de stratégie. En effet, SDL ayant pris le parti d'utiliser une seule et unique variable d'environnement pointant vers un et un seul identifiant de fenêtre, il va nous falloir utiliser ce système différemment.

Nous allons donc devoir créer un widget invisible servant à initialiser le système SDL. (Vous trouverez une classe remplissant cette fonctionnalité dans les fichiers tools.h et tools.cpp (classe SDLContext) fournis avec l'archive.) Le dessin se fera alors sur diverses surfaces SDL (au moins 1 par widget où vous comptez l'utiliser en fait).

Il nous font donc aussi des fonctions de conversion entre QImage et SDL_Surface, ainsi qu'une fonction de création de SDL_Surface. Cette dernière est nécessaire afin de paramétrer les surfaces SDL de façon à ce que leur format soit compatible à 100% avec les formats proposés par QImage. Dans l'exemple, ces fonctions ne couvrent pas tout les cas nécessaires, il ne tient qu'à vous de les compléter selon vos besoins:

// Création d'une surface SDL rapidement convertible en QImage
SDL_Surface* createQImageCompliantSurface(Uint32 flags, int width, int height)
{
	static const int Rmask = 0x00FF0000;
	static const int Gmask = 0x0000FF00;
	static const int Bmask = 0x000000FF;
	static const int Amask = 0xFF000000;
 
	return SDL_CreateRGBSurface(flags, width, height, 32, Rmask, Gmask, Bmask, Amask);
}
 
/* Fonctions de conversion entre SDL_Surface et QImage
   SDLSurfaceToQImage **DOIT** recevoir une surface
   construite avec createQImageCompliantSurface
*/
QImage SDLSurfaceToQImage(SDL_Surface *s)
{
	SDL_LockSurface(s);
		QImage im(static_cast<uchar*>(s->pixels), s->w, s->h, QImage::Format_RGB32);
        SDL_UnlockSurface(s);
 
	return im;
}

2 améliorations conseillées:

  1. il est intéressant de définir notre propre type encapsulant ceci afin de réduire à 0 le risque de passer une mauvaise surface.
  2. on peut ajouter une méthode SDLSurfaceToQImage prenant en paramètre une référence vers un QImage afin de modifier les données d'un QImage précédemment créé. Dans les scènes requierant un affichage rapide, cela peut apporter une différence


Nous avons donc un système SDL fonctionnel pour l'appli ainsi que la possibilité d'avoir des surfaces dont la transformation en QImage est extrêmement rapide (puisqu'il s'agit de la copie brute des octets formant la surface). A la différence de la première partie du tutoriel, nous ne dessinons plus sur une surface affichée à l'écran (que nous utilisions ou non le double buffering ne change rien). Il va donc nous falloir surcharger paintEvent pour gérer l'affichage. La mise à jour de notre QImage se fera à chaque mise à jour de la surface. Inutile de le faire dans le paintEvent puisque certains de ces événements seront générés par le système sans qu'il y ait obligatoirement eu de modifications de la surface, par exemple le fait que le widget soit minimisé puis restauré/maximisé.

class Widget : public QWidget
{
protected:
   virtual void paintEvent(QPaintEvent *e)
   {
      QPainter p(this);
      p.drawImage(displayBuffer);
   }
 
   void modificationSurfaceSDL()
   {
      /* on modifie la surface renderer par diverses opérations de dessin */
      displayBuffer = SDLSurfaceToQImage(renderer);
      update(); // on demande par la même occasion un rafraichissement du widget
   }
 
private:
   SDL_Surface *renderer;
   QImage displayBuffer;
};

Voici le screenshot obligatoire présentant l'exemple en cours d'exécution: Illustration SDL multi-widget