Bonjour à tous les Zér0s, et bienvenue dans ce tutoriel sur la réalisation d'un écran de veille.
Certains d'entre vous ont déjà du voir l'ancienne version du tutoriel. Cette dernière a fait l'objet de remarques selon lesquelles il était bien trop lourd et indigeste. Je vous présente donc ce tutoriel entièrement reformulé pour essayer de le faire plus court et plus clair. J'espère qu'il vous plaira à tous.
Pour les nouveaux, eh bien bienvenue aussi. J'espère également que ce tutoriel vous conviendra.
Je vais donc vous apprendre à réaliser un écran de veille en C++, ou plutôt à réaliser un "point d'entrée pour écran de veille".
Gné ? Un point d'entrée pour un écran de veille ? :euh:
Oui. Disons que je vais vous expliquer comment réaliser une classe qui permettra de mettre en route une application de type écran de veille. En gros, il vous suffira par la suite de créer une instance de cette classe pour créer vos propres écrans de veille à partir de celle-ci sans avoir à vous soucier du technique qu'il y a derrière. Pendant le développement de cette classe, nous allons créer un écran de veille d'exemple, pour avoir quelque chose comme ceci :
En raison de certaines parties du code assez pointues, ce tutoriel est classé en difficulté intermédiaire. Pour la plupart d'entre vous, vous y découvrirez des concepts non appris dans les cours de base du C. Je vous rappelle cependant que le but est d'écrire ce code une fois pour toutes, et qu'il ne sera pas nécessaire d'utiliser ces notions pour créer vos écrans de veille sur cette base par la suite. Si vous ne comprenez pas à 100% les concepts futurs, l'important est que vous réussissiez à les implémenter. Je m'efforcerai de faire du mieux possible pour que ça passe.
(au passage si quelqu'un sait comment réaliser le même genre de chose sous Linux, je suis preneur bien entendu :-° )
Ah, petit détail avant de commencer encore : le tuto est marqué en "copie non autorisée", mais bien évidemment vous pouvez utiliser le code fourni comme bon vous semble. Je souhaite simplement que le contenu textuel de ce tutoriel ne soit pas recopié quelle qu'en soit la fin. Je vous remercie par avance.
Allez c'est parti ! Ta-taa yo-yooooooooooooooo :pirate:
Avant de foncer dans le code, nous allons d'abord voir ensemble ce qu'est un écran de veille, et donc ce qui nous attend par la suite. Ensuite nous mettrons tout en place et nous pourrons attaquer le code à proprement parler :magicien:
Un écran de veille... c'est quoi?
C'est une bonne question ; mais la réponse est tout aussi simpliste vous allez voir ^^ En fait, un écran de veille est un fichier au format .scr, qui se trouve dans le dossier System32 de Windows (ou SysWOW64 si vous possédez une version 64 bits de votre système) En allant y faire un tour, on peut y voir des fichiers comme celui-ci (pêché dans Windows 7)
C'est le système qui s'occupe de lancer le .scr correspondant à l'écran de veille choisi lorsque l'utilisateur n'est pas actif pendant une certain laps de temps (celui que l'on choisit dans l'écran de configuration des écrans de veille). Mais un .scr peut également être lancé directement, essayez donc. Quand vous lancez le .scr, vous avez l'écran de veille correspondant qui se met en route, et qui disparaît quand vous bougez la souris par exemple. Vous noterez que en lançant l'écran de veille de cette façon, même en mode protection le système ne vous demandera pas votre mot de passe pour le fermer.
Donc, c'est un fichier .scr. C'est bien, mais en programmation C++ on ne peut faire que des .exe et des librairies statiques et dynamiques, mais un .scr, on fait comment ?
J'y viens : vous allez être surpris, mais c'est parfaitement faisable. Vous allez à présent découvrir la plus grande révélation du monde entier, préparez vous au choc ^^ Un .scr, eh bien... c'est un .exe que l'on aura renommé en .scr :ninja:
:waw: :waw: Donc un .scr c'est un .exe ; mais dans ce cas, qu'y a t-il à nous apprendre ? si on sait faire un .exe on sait faire un .scr non ?
Eh bien, pas tout à fait, et pour une raison très simple : un écran de veille, ce n'est pas seulement une application que l'on lance et qui affiche une jolie animation (tant qu'à faire :lol: ) à l'écran. En effet, c'est une application qui doit avoir différents comportements selon ce qu'on lui demande :
Afficher l'écran de veille en plein écran ;
Afficher un aperçu dans la fenêtre de sélection des écrans de veille (voir la première image du tuto) ;
Lancer le mode configuration pour pouvoir régler les paramètres.
Ce que nous allons développer ici, c'est donc une classe C++ qui aura la charge de distinguer toutes ces différentes demandes et initialiser le programme en fonction de ces dernières. Il ne restera plus pour le programmeur qu'à développer la partie configuration et la partie affichage - voire 2 parties différentes si l'aperçu n'est pas identique à l'écran de veille en lui-même, ce qui est parfois le cas.
Nous allons à présent voir quels outils et quelles librairies nous allons utiliser pour cela.
Prérequis
Alors pour travailler tranquille, nous allons avoir besoin d'une petite ribambelle d'outils. Pour commencer, il faut un IDE. Je vais utiliser pour cet exemple Visual Studio 2008 de Microsoft, que je vous conseille fortement. Mais vous êtes bien entendu libres d'utiliser l'IDE qui vous plaît. Je n'ai pas testé la compilation des fichiers sources avec d'autres IDE et je ne sais pas ce que ça peut donner, donc il est probable que la compilation puisse être plus délicate. À noter que certains IDE comme Code::Blocks proposent d'utiliser le compilateur de Visual Studio. Vous pouvez donc installer VS et utiliser son compilateur fourni avec votre IDE préféré.
Si vous souhaitez installer VS, il suffit de procéder comme suit :
Cliquez sur "Visual Studio 2008 express" en dessous de "Downloads", ensuite sélectionnez "Visual C++ 2008 express edition", "French" et enfin "Free download".
Lancez l'exéctuable que vous venez de télécharger et installez VS.
A l'heure où j'écris ces lignes, Visual Studio 2010 existe déjà, cependant les librairies tierce-partie que nous allons utiliser pour notre projet ne sont pas encore disponibles pour cette version. Si vous l'utilisez, il vous faudra télécharger les sources de ces librairies et les recompiler avec VS 2010 pour pouvoir les utiliser - expérience faite par un ami qui s'est bien amusé semblerait t-il ^^.
Ensuite, nous allons utiliser une autre bibliothèque : nous aurons besoin de créer une application graphique avec des fenêtres. De plus une bibliothèque qui nous fournit un support pour développer avec une librairie 3D comme OpenGL nous donnerait un avantage supplémentaire. Nous allons donc utiliser...
SDL ?
Non. :lol: C'était bien tenté, mais nous n'allons pas utiliser SDL, pour une raison plus que valable, que je ne peux pas vous expliquer ici (c'est un peu technique), mais sur laquelle nous allons revenir plus tard. A la place, nous allons utiliser SFML. SFML est une bibliothèque qui a le même but et les mêmes fonctionnalités que la SDL, mais qui est plus complète et qui est basée sur l'objet (SDL fonctionne en procédural). Je remercie au passage Sylphcius pour cette petite précision concernant SFML dans son commentaire sur l'ancien tutoriel.
Je viens de me rendre compte qu'un tuto sur SFML est désormais disponible sur le site du zér0. Si vous voulez vous pencher sur cette bibliothèque intéressante, je vous propose de lire ce big-tuto.
Pour télécharger SFML, il suffit de se rendre ici : Télécharger SFML Choisissez simplement la version de SFML correspondant au compilateur que vous utilisez, avec ou sans la doc et les exemples selon votre humeur du jour. Ensuite lancez l'exécutable et suivez les instructions.
Vous trouverez sur le site des tutoriels pour vous expliquer comment configurer votre IDE pour accuillir SFML.
Maintenant que nous avons tout le nécessaire, nous allons pouvoir attaquer la mise en place du projet et du squelette de base de l'application.
Stooooooop ! o_O Ca me paraît un peu gros de devoir utiliser tout ça ! J'ai entendu parler d'une librairie fournie par Windows qui s'appelle "scrnsave" et qui permet de faire des écrans de veille justement. Ce n'est pas plus simple de faire avec ça plutôt que de réinventer la roue ?
Bonne question ^^ Je voulais éviter le sujet, mais vu qu'elle est posée je réponds franchement : à la question "est-ce que je réinvente la roue ?", je répondrais ceci :
Citation
Ptêt' ben qu'oui, ptêt' ben qu'non !
En effet, cette librairie existe, mais pour deux raisons particulières je ne tiens pas à l'utiliser et à vous l'apprendre :
Cette librairie utilise à peu près les mêmes concepts que je veux vous apprendre, mais les appels sont faits à un niveau plus bas (programmation système au lieu d'une classe claire et facile à utiliser). De plus, elle fonctionne en utilisant un timer (un évènement est reçu tous les x temps) et qu'il faut pouvoir traiter de manière très intelligente afin de tirer profit au maximum de la machine (entre 2 timers, on fait quoi?).
Je ne rentrerai pas dans les détails, sinon le tutoriel risquerait d'être très très long ( :-° ), mais pour faire simple je dirais que cette librairie peut être utile pour faire des écrans de veilles très basiques, et si vous avez envie de vous mettre à la programmation système. Je n'ai rien contre, je le précise, et vous pouvez bien entendu si vous le souhaitez rechercher toutes les infos que vous voulez la-dessus sur le web, c'est juste une technique que je n'utiliserai pas ici.
Ok, est-ce qu'il y a d'autres questions avant de commencer ?
Nous allons avant de faire une librairie commencer par faire un projet d'application Win32 tout simple. Créez donc un projet vide de type application Win32 (pas en console, nous n'en avons pas besoin). Pour ceux qui utilisent VS, il n'est pas la peine d'utiliser l'en-tête précompilé, car nous n'aurons qu'un seul header et une seule classe à faire. Décochez également l'option ATL.
Avant de commencer, il va falloir configurer le projet. En effet, nous souhaitons avoir un fichier .scr en sortie, et non pas un fichier .exe !
Pour les utilisateurs de VS, cette option de configuration se trouve dans la page de configuration du projet, outil "éditeur de liens", onglet "général" et champ "sortie". Remplacez seulement ".exe" par ".scr". Pensez à appliquer cette modification aux deux configurations debug et release du projet.
Maintenant que notre projet est créé et configuré, nous allons écrire la classe qui constituera le coeur de notre application. Cette classe servira de base à chacun des projets d'écran de veille que vous réaliserez, et sera celle qui sera placée dans la bibliothèque statique que nous créerons à la fin du tutoriel. Nous allons ainsi utiliser le même principe qui est utilisé dans Qt pour la création d'une application, et qui est appliqué encore dans bien d'autres bibliothèques. Je m'explique :
La classe de base est une classe qui contient la structure basique permettant la mise en route de l'application visée (ici notre écran de veille). Elle contient un certain nombre de fonctions virtuelles que l'utilisateur va pouvoir dériver pour ajouter le code nécessaire à la réalisation précise de son application, sans avoir à se soucier de la base, déjà construite. Je vous fais un petit schéma (comme demandé) pour que vous y voyiez plus clair :
Comme on peut le voir sur ce schéma, chaque application d'écran de veille utilisera deux classes. Celle d'en haut, "ScreenSaver", la classe que nous allons écrire ici servira de base à tous les écrans de veille que vous écrirez. Une fois cette classe écrite, vous n'y toucherez plus jamais. Cette classe aura pour rôle définitif de déterminer ce que l'on demande à l'écran de veille (aperçu, configuration, ou plein écran ?), et d'appeler certaines méthodes en fonction afin de faire l'écran de veille souhaité par héritage.
Celle d'en bas, par contre, sera spécifique à chacun de vos écrans de veille. Elle redéfinit les fonctions déclarées virtuelles dans la classe parente et possède un corps qui permet d'avoir le comportement voulu pour l'écran de veille en question. Par exemple, elle peut redéfinir une fonction de rendu (voir un peu plus loin) à chaque passage dans la boucle principale de la classe mère.
Créez donc un fichier "ScreenSaver.h" qui contiendra le code suivant :
#ifndef _SCREEN_SAVER_H_
#define _SCREEN_SAVER_H_
#include <SFML/Window.hpp>
#include <Windows.h>
#ifdef _DEBUG
#pragma comment (lib, "sfml-window-s-d.lib")
#pragma comment (lib, "sfml-system-s-d.lib")
#else
#pragma comment (lib, "sfml-window-s.lib")
#pragma comment (lib, "sfml-system-s.lib")
#endif
// notre classe point d'entrée
class ScreenSaver
{
public:
// CONSTRUCTEUR /!\ DESTRUCTEUR
// toujours écrire les 2, sans aucune exception, même si les corps sont vides
// cela permet de rappeller que faire une allocation dynamique implique la libération mémoire dans le destructeur
ScreenSaver();
~ScreenSaver();
// La fonction run. Cette fonction est lancée par le main de l'application
// retourne le code de retour que le main doit rendre
// si debug est à true, alors on lance l'écran de veille en mode fenêtré, pour permettre le déboguage
// si debug est à true, il empêche également la fenêtre de se détruire sur un évènement de mouvement de souris
int run(HINSTANCE hInstance, LPSTR lpCmdLine, const sf::VideoMode & videoMode, const bool debug);
protected:
// La fonction config est appelée depuis la fonction run, et permet de configurer l'écran de veille
// son comportement par défaut est d'afficher une boîte de dialogue indiquant que rien n'est configurable
// elle retourne 0 si tout s'est bien passé, sinon un code d'erreur (qui sera retourné par run)
virtual int config();
// La fonction init permet l'initialisation du rendu, et est appelée par le main avant le premier rendu (aperçu ou plein écran)
// retourne true si l'appel à réussi, false sinon. Dans ce cas, la fonction run se termine et renvoie le code d'erreur EXIT_FAILURE
// le handle passé est celui de la fenêtre de dessin
virtual bool init();
// La fonction shutDown est appelée par la fonction run à la fin de l'application et a l'effet inverse de init
// retourne true si l'appel à réussi, false sinon. Dans ce cas, la fonction run se termine et renvoie le code d'erreur EXIT_FAILURE
virtual bool shutDown();
// Les fonctions suivantes sont appelées à chaque boucle et ont pour fonction d'effectuer le rendu à l'écran
// celle-ci permet de faire un rendu en mode plein écran
// elle retourne true si on doit continuer le rendu, false sinon
virtual bool render() = 0;
// celle-là permet de faire un rendu en mode aperçu
// si cette fonction n'est pas surchargée, son comportement par défaut sera d'appeler la fonction render à sa place
// elle retourne true si on doit continuer le rendu, false sinon
virtual bool renderPreview();
// pointeur vers une fenêtre SFML, que nous allons instancier si nécessaire et qui servira pour le rendu
sf::Window * m_RenderWindow;
// handle de fenêtre windows, désignant la fenêtre de dessin, la même que la fenêtre SFML ci-dessus
HWND m_HandleFenetre;
// mode vidéo utilise (résolution d'écran actuelle)
sf::VideoMode m_VideoMode;
// booléen indiquant si on est en mode plein écran ou non
bool m_FullScreen;
private:
// ce type définit les 3 types de comportements que l'on peut attendre de la part d'un écran de veille
enum RequestType
{
CONFIG,
PREVIEW,
SCREEN_SAVER
};
};
#endif // _SCREEN_SAVER_H_
Voilà donc notre fameuse classe. On a besoin pour la créer du header SFML Window.hpp, qui nous permet d'utiliser les fenêtres SFML. Le header <Windows.h> est ici car notre fonction principale (la fonction run) prend en paramètre formel une variable d'un type spécifique à Windows. Le type du membre protégé "m_HandleFenetre" l'est également. Les #pragma que vous voyez au début sont une alternative pour indiquer à VS que les librairies citées doivent être ajoutées à la compilation (les librairies de SFML). L'autre solution consiste à les indiquer dans la configuration du projet (outil "édition des liens", onglet "entrée" et champ "dépendances supplémentaires").
SFML possède plusieurs types de librairies que l'on peut distinguer grâce à leur nom. Le "-d" à la fin indique qu'il s'agit d'une librairie compilée en mode debug, et qu'il faut donc utiliser comme librairie lors de la compilation de notre projet en mode debug, et le "-s" signifie static. En effet on peut soit utiliser les librairies statiques, soit les libraries non statiques qui font le lien avec des DLL fournies par SFML. Ici on utilise les librairies statiques pour ne pas avoir à se trimballer les DLL partout. C'est un choix personnel, et vous pouvez bien entendu faire avec les dynamiques si vous le souhaitez.
Comme vous le voyez, notre classe comporte une seule fonction en public. C'est la fonction qui sera appelée par le main de l'application finale. Elle aura la charge de distinguer ce qu'on nous demande de faire, et d'appeler les fonctions protégées en fonction de ce que l'application doit faire (aperçu, configuration, etc.). Cette fonction prend quatre arguments :
Le premier argument de notre fonction run nous permet de savoir quel est l'instance de l'application en cours : lors du lancement d'une application, Windows lui octroie un numéro unique qui permet de la distinguer dans le système. Ce numéro nous sera utile dans la fonction run. Cet argument est reçu lors de l'entrée dans le programme (WinMain).
Le deuxième argument est la ligne de commande passée lors du lancement de l'application : c'est par l'intermédiaire de cette ligne de commande que le système va nous donner les instructions quant à quoi faire. Nous la passons à la fonction run pour qu'elle puisse la lire et la traiter pour distinguer la demande du système. Cet argument est également reçu lors de l'entrée dans le programme.
Le troisième argument est une structure spécifique à SFML contenant la résolution d'écran à utiliser dans le cas du mode plein écran.
Le quatrième et dernier argument est un booléen indiquant si l'on souhaite lancer l'écran de veille en mode debug ou non. Le mode debug affecte certaines propriétés de l'écran de veille afin d'en faciliter le déboguage : l'écran de veille apparaît en mode fenêtré afin d'avoir accès à l'IDE pour la manipulation des points d'arrêts et du déboguage pas à pas, et il ne réagit plus aux mouvements de souris (difficile de déboguer sans bouger la souris !)
La fonction run va donc devoir définir le travail qu'on lui demande, et appeler les fonctions protégées selon le schéma suivant :
Lors d'une demande de configuration :
Appel de la fonction "config", puis renvoi de son retour
Lors d'une demande d'aperçu (dans le petit écran de l'écran de sélection des écrans de veille) :
Appel de la fonction "init"
Boucle infinie
Appel de la fonction "renderPreview", une fois par boucle
Appel de la fonction "shutDown"
Lors d'une demande de rendu plein écran :
Appel de la fonction "init"
Boucle infinie
Traitement des évènements : on quitte sur un clic souris, sur un mouvement souris ou sur une touche clavier
Appel de la fonction "render", une fois par boucle
Appel de la fonction "shutDown"
Sous une autre forme, on peut dire que les fonctions protégées ont chacune le rôle suivant :
La fonction "config" a pour but de lancer la configuration de l'écran de veille, et de retourner une valeur en fonction de sa réussite ou non.
La fonction "init" permet d'initiliser le rendu en mode aperçu ou plein écran. Si l'initialisation foire, elle renvoie false, et la fonction run se termine aussitôt.
La fonction "shutDown" permet de nettoyer ce qu'il est nécessaire de nettoyer avant de quitter l'application. Cette fonction renvoie également false si elle foire.
La fonction "render" permet de faire le rendu en mode plein écran. Cette fonction renvoie true si on doit continuer à dessiner, false si on veut arrêter.
La fonction "renderPreview", permet de faire également le rendu, mais pour le mode fenêtré. Elle fait le même type de retour que son homologue "render".
Notez que la fonction "render" est déclarée virtuelle pure. Les applications développées qui utilisent cette classes seront au minimum obligées de redéfinir cette fonction dans leur dérivation de la classe ScreenSaver.
Ce qu'on appelle le rendu en programmation graphique est la technique qui constitue a construire le dessin à l'écran. Vous trouverez plus d'infos sur cet article sur wikipedia si vous le souhaitez. Bien entendu, la version anglaise de l'article est bien plus complète, comme toujours ^^
La classe contient également quelques variables membres en protégé. Elles sont dans cette partie car elles peuvent être utiles dans les classes qui la dérivent (par exemple, le handle fenêtre peut être utile pour la fonction dérivée "init" si on utilise directX pour le dessin). Les commentaires en disent assez sur leur fonction.
Enfin, l'enum déclaré en privé nous servira à distinguer la tâche a effectuer dans la fonction "run". Il est déclaré en privé, car ce type n'est pas utile à l'extérieur de la classe.
Maintenant que nous avons la structure de base de notre classe, nous allons écrire les comportements par défaut de chacune des méthodes protégées.
Nous allons en premier lieu définir les comportements par défaut, c'est à dire ce que nos fonctions vont faire si elles ne sont pas redéfinies. Ceci nous permet d'avoir une application correcte, même si nous ne réécrivons pas certaines fonctions. Par exemple, le comportement par défaut de la fonction "config" peut être d'afficher un message indiquant que rien n'est configurable. Ceci nous évite ainsi d'avoir à le faire pour chaque écran de veille dont nous ne souhaitons pas faire de configuration.
Dans le jargon informatique, on parle d'écrire des "bouchons".
Créez donc le fichier "ScreenSaver.cpp" qui contiendra les corps de fonctions de la classe fraîchement écrite, et remplissez-le avec ce code :
#include "ScreenSaver.h"
#pragma warning (disable : 4996)
/************************************************************************/
/* Fonctions publiques */
/************************************************************************/
// construction et destructions
ScreenSaver::ScreenSaver() : m_RenderWindow(NULL)
{}
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
ScreenSaver::~ScreenSaver()
{
// destruction de m_RenderWindow si celui-ci a été construit
if (m_RenderWindow) delete m_RenderWindow;
}
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
/************************************************************************/
/* Fonction protégées */
/************************************************************************/
int ScreenSaver::config()
{
MessageBox(m_HandleFenetre, L"Il n'y a rien à configurer", L"Information", MB_OK | MB_ICONASTERISK);
return EXIT_SUCCESS;
}
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
bool ScreenSaver::init()
{
return true;
}
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
bool ScreenSaver::shutDown()
{
return true;
}
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
bool ScreenSaver::renderPreview()
{
return render();
}
Bon ben ici rien de bien compliqué, le constructeur initialise notre membre privé pointeur à NULL, et notre destructeur le détruit si il y a lieu, rien de plus classique ^^
La fonction "config" utilise l'appel système messageBox pour afficher une boite de dialogue indiquant que rien n'est configurable pour cet écran de veille (si vous voulez configurer, redéfinissez "config" dans votre implémentation de la classe). À noter que par défaut, les fonctions systèmes prenant une chaîne de caractères en argument prennent une chaine de caractères en Unicode, le "L" précédant la chaîne est donc obligatoire ici. Cela permet la bonne gestion des accents. Vous pouvez même mettre des caractères chinois (ou japonais ^^) si le cœur vous en dit !
Les fonctions "init" et "shutDown" renvoient par défaut true, pour indiquer que tout s'est bien passé.
La fonction "renderPreview" retourne l'appel de la fonction "render" par défaut. Ceci permet, lors de la création d'un écran de veille, de n'avoir à écrire que la fonction "render" quand on souhaite faire le même rendu en mode aperçu et en mode plein écran. Inutile d'écrire la fonction "renderPreview" pour lui dire d'appeler "render", ce sera le comportement par défaut.
La fonction "render" est déclarée virtuelle pure, elle n'est donc pas ici.
Quant à la fonction "run", elle est un peu compliquée, car c'est elle qui a le rôle de point d'entrée et qui donne tout l'intérêt que mérite cette classe. Nous allons d'ailleurs commencer à la détailler.
La fonction run - première partie
Bon, nous allons maintenant attaquer la partie violente du code : la fonction run. Sa responsabilité est lourde, elle doit faire tout le travail d'initialisation et d'appel des fonctions protégées.
La première chose à faire est la séparation des tâches. Pour cela nous alons devoir savoir comment les ordres sont passées par Windows. En fonction de ce que le système demande, la ligne de commande peut se présenter sous l'une des trois formes suivantes :
Pour la configuration (Config) : ligne de commande = "/c:xxxxx" : xxxxx représente un nombre (nous verrons quoi plus tard) ;
Pour le mode aperçu (Preview) : ligne de commande = "/p xxxxx" ; xxxxx représente un nombre (idem, plus tard) ;
Pour le mode plein écran (Screen saver) : ligne de commande = "/s" ; nous considèrerons que si on a ni "/c:xxxxx" ni "/p xxxxx" c'est qu'on est en mode plein écran, on peut donc oublier le "/s".
Pourquoi le "/c" est séparé du nombre par un ":" et pourquoi le "/p" par un espace, cela reste un mystère complet.
Pour traiter cette ligne de commande nous alons donc commencer par écrire le code suvant :
int ScreenSaver::run(HINSTANCE hInstance, LPSTR lpCmdLine, const sf::VideoMode & videoMode, const bool debug)
{
RequestType request; // la requête du système
char * jeton; // ce qui sera retourné par strtok
sf::Event toProcess; // évènement à traiter
int mouseX = -1; int mouseY = -1;
WNDCLASS childClass; // la classe utilisée pour créer une fenêtre
HWND childWindow = NULL; // la fenêtre fille utilisée pour le mode aperçu
// on fait un strtok sur lpCmdLine pour récupérer la requête
jeton = strtok(lpCmdLine, " :");
// si jeton est différent de null on teste
if (NULL != jeton)
{
// on fait ensuite un if, pour savoir où l'on est
if (strncmp(jeton, "/c", 2) == 0) // si on a /c
{
request = CONFIG;
}
else if (strncmp(jeton, "/p", 2) == 0) // si on a /p
{
request = PREVIEW;
}
else
{
request = SCREEN_SAVER; // si on /s ou rien ou n'importe quoi d'autre
}
}
else
request = SCREEN_SAVER;
// fin de la fonction
return EXIT_SUCCESS;
}
Il y a ici toutes les variables déjà déclarées, cela nous évitera à revenir dessus plus tard. Pour l'instant seules les variables request et jeton seront utilisées. La variable request est de type RequestType, l'enum que nous avons déclaré en privé.
Ici, on utilise la puissance de la fonction strtok pour récupérer la première partie de la ligne de commande. Elle est séparée de la suite soit par un ":", soit par un espace, d'où le deuxième argument du strtok (oui oui, il y a bien un espace entre le premier guillemet et le caractère ":" en argument, ce n'est pas une erreur). Il nous renvoie donc une chaine correspondant à la première partie de notre ligne de commande, celle qui nous intéresse ici. Il ne faut pas oublier que ce jeton peut être nul, dans le cas où la ligne de commande est vide (on lance directement le .scr)
Ensuite, eh bien en fonction de ce que notre jeton contient, on définit la valeur de request. Nous pourrons ainsi par la suite faire un switch sur cette valeur pour déterminer ce que nous allons faire.
Le return EXIT_SUCCESS est ici afin que vous puissiez compiler votre code sans générer d'erreur. Il restera bien entendu à la fin de la fonction ; ce que nous allons rajouter par la suite se placera directement au dessus de ce dernier. Dans la prochaine partie, nous allons commencer à effectuer le travail de base en fonction de la requête.
Ici nous allons donc passer à la V2 de notre fonction "run", et y rajouter un travail d'initialisation pour chaque requête.
Nous allons donc rajouter le code suivant à la fin de la fonction (avant le return quand même hein ^^) :
// en fonction du type de requête, on fait différentes initialisations
switch(request)
{
case CONFIG:
// dans le cas d'une config, on quitte direct la fonction run après l'appel de config
// tout ce qui suit ne sera ainsi jamais appelé dans le cas d'une configuration
m_HandleFenetre = (HWND)(atoi(strtok(NULL, "")));
return config();
break;
case SCREEN_SAVER:
// création de la fenêtre en plein écran, et remplissage des variables
if (debug)
{
m_VideoMode = sf::VideoMode(800, 600, 32);
m_FullScreen = false;
m_RenderWindow = new sf::Window(m_VideoMode, "SiteDuZero");
}
else
{
m_VideoMode = videoMode;
m_FullScreen = true;
m_RenderWindow = new sf::Window(m_VideoMode, "SiteDuZero" , sf::Style::Fullscreen);
}
// on récupère le handle de la fenêtre SFML
m_HandleFenetre = GetActiveWindow();
// on cache le curseur de la souris
m_RenderWindow->ShowMouseCursor(false);
break;
case PREVIEW:
// création de la fenêtre depuis le handle récupéré
m_HandleFenetre = (HWND)(atoi(strtok(NULL, "")));
m_RenderWindow = new sf::Window(m_HandleFenetre);
// enfin on définit les membres
m_VideoMode = sf::VideoMode(m_RenderWindow->GetWidth(), m_RenderWindow->GetHeight());
m_FullScreen = false;
break;
default:
break;
}
On en vient donc à expliquer ces fameux xxxxxx dans les linges de commande pour le mode config et le mode aperçu. En fait, ce nombre est un identifiant de fenêtre, ou Handle WiNDow (HWND). Il permet au système d'identifier une fenêtre dans celles existantes de toutes les applications.
Ainsi, le xxxxx après config renseigne sur le handle de la fenêtre de sélection des écrans de veille, ainsi la configuration peut se faire en définissant la fenêtre de configuration comme modale à la fenêtre de sélection. En d'autres termes, tant que la configuration de l'écran de veille est en cours, la fenêtre dont le handle est passé en paramètre ne peut plus être accédée par l'utilisateur.
Pour le mode aperçu, le xxxxx est le handle de la fenêtre dans laquelle on doit dessiner notre aperçu. C'est quand même mieux de savoir où dessiner !
C'est d'ailleurs à ce niveau que SDL ne peut plus suivre, et c'est pourquoi nous avons pris SFML. En effet, la SDL permet de créer une fenêtre facilement, mais il est impossible de créer une fenêtre en utilisant un handle existant. SFML permet cette opération. Nous allons voir ça un peu plus bas.
Dans le cas où notre requête est CONFIG, eh bien on récupère le handle de la fenêtre de sélection et on appelle la fonction "config". Vous remarquerez que le comportement par défaut de la fonction "config" affiche une boite de dialogue avec ce handle en paramètre. Quand vous essaierez, vous verrez que quand la boite de dialogue est affichée, l'écran de sélection n'est plus accessible.
Quand on est en mode plein écran (requête SCREEN_SAVER), le comportement dépend du drapeau debug. Rappelez-vous, il est préférable d'être en mode fenêtré pour faciliter le débogage. Ce qu'on fait dans ce mode est donc simplement de créer une fenêtre SFML qui servira pour le rendu, et de définir les variables de la classe. Vous remarquerez que j'ai choisi ici de forcer le dessin dans une fenêtre en 800x600 en mode debug, mais vous pouvez mettre la résolution que vous voulez ou appliquer simplement ce qui arrive en paramètre dans la fonction main (videoMode). Une fois la fenêtre SFML créée, on pense bien à définir la variable "m_HandleFenetre" pour qu'elle soit associée à la même fenêtre. On utilise pour cela la fonction "GetActiveWindow()" de l'API Windows, qui nous renvoie la fenêtre active courante, donc ici celle que l'on vient juste de créer.
Enfin, quand on est en mode aperçu (requête PREVIEW), on récupère le handle de la fenêtre dans laquelle dessiner et on fait une fenêtre SFML en l'utilisant. On définit ensuite les variables de classe. Pour définir le champ m_VideoMode, on récupère les informations dans la fenêtre nouvellement créée (merci SFML !). Ce que nous faisons ici n'est absolument pas suffisant pour ce mode, mais nous verrons plus tard ce qui manque : c'est assez subtil, vous verrez !
Nous allons enfin pouvoir passer à la boucle principale. Comme dit en commentaire dans le code, tout ce qui suit n'est accédé que si on est en mode aperçu ou plein écran. En mode config, on quitte l'application immédiatement après le retour de la fonction config.
La boucle principale de notre fonction "run" va comporter trois parties.
La première partie est la partie initialisation, durant laquelle nous allons faire l'appel à la fonction "init".
La deuxième est la boucle en elle-même.
La troisième quant à elle est la partie durant laquelle nous allons faire l'appel à la fonction "shutDown".
Après cette troisième partie, la fonction "run" se termine.
La boucle principale a deux fonctions :
Gérer les évènements clavier/souris
Appeler la fonction de rendu
À noter qu'on ne gère que les évènements en mode plein écran. Arrêter l'aperçu en bougeant la souris n'aurait aucun intérêt et serait absurde.
Nous avons donc le code suivant, à rajouter toujours au même endroit avant le "return EXIT_SUCCESS;" à la fin de la fonction :
// boucle de rendu
// d'abord on initialise
if (!init()) return EXIT_FAILURE;
// on rentre dans la boucle
while (m_RenderWindow->IsOpened())
{
// traitement des évènements
while (m_RenderWindow->GetEvent(toProcess))
{
// demande de fermeture de l'application
if (sf::Event::Closed == toProcess.Type)
{
m_RenderWindow->Close();
continue;
}
if (SCREEN_SAVER == request)
{
// évènements clavier et souris, sauf mouse moved
if (sf::Event::KeyPressed == toProcess.Type ||
sf::Event::KeyReleased == toProcess.Type ||
sf::Event::MouseButtonPressed == toProcess.Type ||
sf::Event::MouseButtonReleased == toProcess.Type)
{
m_RenderWindow->Close();
continue;
}
if (sf::Event::MouseMoved == toProcess.Type && !debug)
{
if (mouseX < 0 || mouseY < 0)
{
mouseX = toProcess.MouseMove.X;
mouseY = toProcess.MouseMove.Y;
}
else if (mouseX != toProcess.MouseMove.X || mouseY != toProcess.MouseMove.Y)
{
m_RenderWindow->Close();
continue;
}
}
}
}
// appel de render ou de renderPreview
if (SCREEN_SAVER == request)
{
if (!(render())) m_RenderWindow->Close();
}
else
{
if (!(renderPreview())) m_RenderWindow->Close();
}
}
// on finit par l'appel de shutDown, pour tout terminer correctement
if (!shutDown()) return EXIT_FAILURE;
Alors ici c'est un peu technique dans la partie gestion des évènements, car nous devons tenir compte du drapeau debug et du type de requête. De plus la détection du mouvement souris est un peu tordue : cet évènement est systématiquement appelé une fois lors de l'entrée de la souris dans la fenêtre, et donc quand la fenêtre est créée. Il faut pouvoir neutraliser ce premier appel pour éviter que l'écran de veille ne se ferme immédiatement dès son lancement.
La gestion des évènements avec SFML est similaire à celle de la SDL. On prend un évènement, puis on le traite. Et ainsi de suite, jusqu'à ce qu'il y en aie plus.
Si on reçoit l'event close, on ne se pose pas de question et on ferme la fenêtre SFML. Cet évènement peut avoir plusieurs origines : le clic sur le bouton croix en mode fenêtré (debug), ou le système qui demande l'arrêt de l'application pour une raison X ou Y. Fermer la fenêtre aura pour effet de terminer la boucle principale, car la condition de la boucle while se base sur le fait que la fenêtre SFML est ouverte ou non. Dès qu'on demande la fermeture de la fenêtre on fait un continue, pour abréger la boucle et revenir à la condition, ce qui nous la fera quitter.
Les autres évènements ne sont traités que si la requête vaut "SCREEN_SAVER", c'est à dire en mode plein écran.
Parmi ceux-ci, l'appui sur une touche clavier et/ou le clic souris. Ces deux types d'évènements ont pour conséquence immédiate la fermeture de la fenêtre.
Il reste le mouvement de la souris. Il nous faut traiter plusieurs informations : Si la souris a bougé et qu'on est pas en mode debug, on vérifie que ce n'est pas la première fois. Pour cela, on utilise deux variables déclarées au début de la fonction qui sont mouseX et mouseY. Ces variables sont initialisées à -1 lors de leur déclaration. Une position souris ne pouvant pas être négative, il est facile de vérifier si c'est notre premier évènement de souris ou non. Si une des deux valeurs est négative, on leur donne à toutes les deux les valeurs reçues avec l'évènement. Si ce n'est pas le cas, on est pas à notre première entrée, et dans ce cas on ferme la fenêtre SFML.
Pour le reste, on vérifie le retour de la fonction "init" au début, et pareil pour "shutDown" à la fin. Après le traitement des évènements dans chaque boucle, on appelle la fonction de rendu en fonction du type de requête. Si la fonction retourne false, on arrête le dessin et on ferme la fenêtre SFML.
Bien. À ce stade-ci nous avons une classe ScreenSaver fonctionnelle. Nous allons pouvoir créer notre premier exemple :soleil:
Pour cet exemple, nous allons reprendre le code proposé par Laurent GOMILA dans l'un de ses exemples fournis avec SFML : le rendu de base avec openGL.
Créez donc un fichier main.cpp, qui contiendra le code suivant :
#include "ScreenSaver.h"
#ifdef _DEBUG
#pragma comment (lib, "sfml-system-s-d.lib")
#else
#pragma comment (lib, "sfml-system-s.lib")
#endif
#pragma comment (lib, "opengl32.lib")
#pragma comment (lib, "glu32.lib")
class SiteDuZero : public ScreenSaver
{
protected:
virtual bool init()
{
// initialisation d'openGL
// définition de la profondeur par défaut et de la couleur par défaut lors d'un clear
glClearDepth(1.0f);
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
// activation du Z_BUFFER
glEnable(GL_DEPTH_TEST);
glDepthMask(GL_TRUE);
// définition du projection en perspective
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(90.0f, (float)m_VideoMode.Width/(float)m_VideoMode.Height, 1.0f, 500.0f);
// on retourne vrai
return true;
}
virtual bool render()
{
// premièrement, on réinitialise les buffers
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// on applique les transformations sur le modèle que l'on va dessiner
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glTranslatef(0.f, 0.0f, -200.0f); // la caméra est en identité
glRotatef(m_Clock.GetElapsedTime() * 50.0f, 1.0f, 0.0f, 0.0f);
glRotatef(m_Clock.GetElapsedTime() * 50.0f, 0.0f, 1.0f, 0.0f);
glRotatef(m_Clock.GetElapsedTime() * 50.0f, 0.0f, 0.0f, 1.0f);
// on dessine notre cube en mode immédiat
glBegin(GL_QUADS);
glColor3f(1.f, 0.f, 0.f);
glVertex3f(-50.f, -50.f, -50.f);
glVertex3f(-50.f, 50.f, -50.f);
glVertex3f( 50.f, 50.f, -50.f);
glVertex3f( 50.f, -50.f, -50.f);
glColor3f(1.f, 0.f, 0.f);
glVertex3f(-50.f, -50.f, 50.f);
glVertex3f(-50.f, 50.f, 50.f);
glVertex3f( 50.f, 50.f, 50.f);
glVertex3f( 50.f, -50.f, 50.f);
glColor3f(0.f, 1.f, 0.f);
glVertex3f(-50.f, -50.f, -50.f);
glVertex3f(-50.f, 50.f, -50.f);
glVertex3f(-50.f, 50.f, 50.f);
glVertex3f(-50.f, -50.f, 50.f);
glColor3f(0.f, 1.f, 0.f);
glVertex3f(50.f, -50.f, -50.f);
glVertex3f(50.f, 50.f, -50.f);
glVertex3f(50.f, 50.f, 50.f);
glVertex3f(50.f, -50.f, 50.f);
glColor3f(0.f, 0.f, 1.f);
glVertex3f(-50.f, -50.f, 50.f);
glVertex3f(-50.f, -50.f, -50.f);
glVertex3f( 50.f, -50.f, -50.f);
glVertex3f( 50.f, -50.f, 50.f);
glColor3f(0.f, 0.f, 1.f);
glVertex3f(-50.f, 50.f, 50.f);
glVertex3f(-50.f, 50.f, -50.f);
glVertex3f( 50.f, 50.f, -50.f);
glVertex3f( 50.f, 50.f, 50.f);
glEnd();
// finalement, on fait le rendy
m_RenderWindow->Display();
// on retourne true
return true;
}
private:
sf::Clock m_Clock;
};
int _stdcall WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
SiteDuZero app;
#ifdef _DEBUG
return app.run(hInstance, lpCmdLine, sf::VideoMode(GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN), 32), true);
#else
return app.run(hInstance, lpCmdLine, sf::VideoMode(GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN), 32), false);
#endif
}
Ce fichier contient l'implémentation de la classe que nous venons d'écrire par héritage, et la fonction main de notre programme. Remarquez comme la fonction main est courte ! Nous utilisons la macro _DEBUG, qui est définie uniquement quand on est en mode debug, pour déterminer si on doit passer le flag debug à true ou à false lors de l'appel de "run". Astucieux, n'est-ce pas ?
La classe instanciée dans le main est la classe déclarée au dessus, et il s'agit donc d'une classe fille de celle que nous avons créé. Dans cette dernière, seules les fonctions "init" et "render" sont redéfinies, la redéfinition n'étant pas jugée nécessaire pour les autres (rendu identique en mode aperçu et en mode plein écran, pas de config disponible, et pas de travail à faire pendant le "shutDown"). La fonction init effectue quelques initialisations pour openGL, et la fonction "render" affiche notre cube qui tourne.
Nous voilà donc avec un écran de veille à priori fonctionnel. À priori, car le mode plein écran et le mode configuration fonctionnent, mais un problème de taille s'est glissé très discrètement (trop) dans le mode aperçu.
Pour les plus courageux d'entre vous, vous pouvez tenter de copier le fichier .scr dans system32 (ou sysWOW64), le renommer avec le nom que vous voudriez voir dans la fenêtre de sélection, puis ouvrir cette dernière. Choisissez ensuite votre écran de veille. Vous devriez voir le cube tourner dans la petite fenêtre d'aperçu. Cliquez maintenant sur "configurer". Une boite de message vous avertit que il n'y a rien à configurer, cliquez donc sur ok.
Eh, mais... attend ! Y'a un problème là ! Quand je ferme ma boîte de dialogue, j'ai mon aperçu qui revient, mais j'ai l'image qui saute : c'est grave docteur ?
Eh oui, il s'agit du problème que je viens de mentionner, et plutôt grave en effet. Explications : quand le système lance en mode aperçu, c'est lui qui détient la fenêtre de rendu. Notre programme ne fait que créer une fenêtre SFML avec le handle de la fenêtre de rendu, mais ne possède pas cette dernière. Du coup, il ne reçoit jamais l'évènement close, et l'application ne se termine jamais, ce qui implique la suite d'évènements dont la résultante est ce bug visuel :
Quand vous choisissez votre écran de veille dans la fenêtre de sélection, celui-ci se lance en mode aperçu.
Quand vous cliquez sur "configurer", le signal "close" est envoyé par Windows à la fenêtre, mais l'application ne le voit pas. Elle continue comme si de rien n'était.
Pendant ce temps, une autre instance de l'application est lancée en mode configuration, et affiche la bien connue boîte de dialogue.
Quand vous cliquez sur OK, la fonction config se termine et donc l'application également.
Windows relance l'application en mode aperçu.
À ce stade-là, il y a deux instances de l'application en mode aperçu qui font toutes les deux le rendu dans la même fenêtre ! Et cet effet d'image qui saute apparaît.
Le problème le plus grave ici est donc que le programme tourne maintenant deux fois, et ça ne va pas s'arranger en quittant la fenêtre de sélection des écrans de veille. Ces programmes continuent à bouffer des ressources système inutilement, et ceci est inacceptable. Nous allons remédier à ce problème, mais avant tout il faut fermer ces deux programmes renégats.
Ouvrez donc le gestionnaire de tâches de Windows (soit en faisant un clic droit sur votre barre de tâches puis en cliquant sur "gestionnaire de tâches", soit en faisant "démarrer" -> "exécuter", puis en tapant "taskmgr" et entrée).
Allez dans l'onglet processus donc, et trouvez vos programmes rebelles dans la liste. Terminez-les tous l'un après l'autre en les sélectionnant puis en cliquant sur "Arrêter le processus" et en validant par "Oui" après avoir vérifié une nouvelle fois qu'il s'agissait bien du processus que vous voulez tuer.
Nous allons maintenant pouvoir régler le problème dans le code, mais avant tout, un peu de théorie système ! (ça fait jamais de mal, allez...)
Nous allons donc voir un peu de théorie système pour mieux comprendre le problème, puis le résoudre.
Un peu de théorie (ça fait jamais de mal)
Bon allez, après tout la théorie on s'en fout ça n'a aucun intérêt ! Faux, archi faux, triplement faux :diable: Ce sous-titre va vous paraître rébarbatif, mais sans théorie vous n'arriverez à rien. Allons accrochez vos ceintures, vous verrez ça ne sera pas long ^^.
Pour commencer, une question idiote mais toutefois pertinente : Qu'est-ce que la programmation système?
Il s'agit de programmation, dans une application, qui nécessite et utilise explicitement des composants systèmes, ici de Windows. Par exemple, la création d'une fenêtre passe par le système d'exploitation, et de ce fait n'est donc pas portable. SFML l'est, certes, mais SFML possède dans son moi profond une méthode qui en fonction du système sur lequel elle est utilisée utilise des fonctions différentes : des fonctions système. Nous avons déjà fait un appel système plus tôt, rappelez-vous la fonction "MessageBox" dans le comportement par défaut de la fonction "config".
La programmation système de Windows suit un principe assez particulier, que nous allons détailler ici. Mais avant tout, nous allons expliquer ce qu'est une fonction dite "callback", car cette notion est nécessaire pour comprendre la suite.
La fonction callback
D'après ma propre définition, je dirais que : Une fonction callback est une fonction dont le but est d'être appelée par une autre fonction qui ne la connait pas au moment de la compilation.
Ouais bon, je regarde sur Wikipedia, je suis sûr que ce sera plus clair :
Citation : Wikipedia
En informatique, une fonction de rappel (callback en anglais) est une fonction qui est passée en argument à une autre fonction. Cette dernière peut alors faire usage de cette fonction de rappel comme de n'importe quelle autre fonction, alors qu'elle ne la connaît pas par avance (NDLA : à la compilation).
Ah, je n'étais pas loin (et sans tricher ^^). Enfin bref, comme Wikipedia le définit si bien, c'est une fonction de rappel. Cela viendrait, d'après les mêmes sources, de Hollywood, où les gens ont pour coutume de laisser leurs coordonnées afin d'être recontactés plus tard quand cela était nécessaire. En effet, une fonction callback suit ce même principe :
Supposons que nous créons un analyseur syntaxique, c'est à dire un programme qui analyse un texte et traite chacun des mots rencontrés. On souhaite que l'utilisateur de cette fonction (récupération des mots et traitement) puisse choisir lui-même quel traitement effectuer pour chacun des mots (compter le nombre d'occurrences, etc), sans pour autant qu'il aie accès à cette fonction d'analyse (fournie dans une librairie par un tiers par exemple). C'est une mission parfaite pour Super Callback !!!
Ainsi, l'utilisateur peut créer sa propre fonction qui reçoit un mot, et qui y effectue le traitement voulu. Il s'agira de la fonction de rappel de notre analyseur. Ensuite, l'utilisateur appelle l'analyseur, en lui donnant sa fonction (par un pointeur de fonction). L'analyseur n'a plus qu'à lire le texte, et appeler la fonction qui lui a été donnée pour chacun des mots rencontrés : le tour est joué !
Nous avons rempli notre objectif : la fonction d'analyse ne connait pas la fonction de traitement lors de sa compilation (à l'avance, comme cité par Wikipedia), mais elle est capable d'appeler une fonction personnalisée par l'utilisateur, qui donc est bien une fonction de rappel. Comme cité également par Wikipedia, une fonction de rappel s'écrit exactement comme une fonction normale (pour Windows, on peut rajouter le mot CALLBACK devant la fonction, mais cela marche également sans. On se permettra de le faire quand même lors de l'écriture de nos programmes systèmes, dans un souci de clarté).
Si vous voulez un exemple, je vous invite à lire l'article correspondant sur Wikipedia. En anglais, vous y trouverez un petit schéma. Pour les plus courageux, l'article est plus complet en japonais ^^
Revenons à nos moutons
Maintenant que vous savez ce qu'est une fonction de rappel (callback), je peux vous expliquer la suite :
Toute application Windows (qu'elle soit en mode console ou en mode fenêtrée ou même sans fenêtre du tout) est reliée au système, qui communique avec elle grâce à des messages. L'application a le devoir de traiter tous ces messages, et même si au final elle en ignorera certains, elle se doit de tout recevoir et de tout lire. Une application revient donc a créer un programme qui initialise certaines choses, puis qui passe sa vie à déballer des messages et à les traiter.
Elle contient donc une boucle qui ne fait que ça, et que l'on appelle la pompe a message : Cette boucle reçoit un message, le traduit, puis le dispatche à sa fonction de callback de destination, appelée aussi window procedure (pour Windows, window ne représente pas seulement qu'une fenêtre dans laquelle on affiche, mais n'importe quel élément système d'un programme : par exemple, un bouton ou une barre de progression sont représentés par une "window").
Cette fonction reçoit les messages traduits, puis les traite. C'est ici que l'on reçoit par exemple les messages de clic de souris sur un bouton, de saisie de texte dans un champ, etc. et que l'on peut traiter pour par exemple ouvrir une nouvelle fenêtre, ou lancer automatiquement une correction d'orthographe, etc. C'est également cette fonction qui reçoit les messages de fermeture de fenêtre. Le système ne connaît pas cette fonction à l'avance, d'où l'intérêt d'utiliser une callback.
Quand vous programmez en mode console, c'est l'exécutable cmd.exe qui exécute toutes ces tâches de traitement, et dans notre cas il s'agit de SFML. C'est d'ailleurs ces messages qui permettent à SFML de remplir sa liste d'évènements que nous lisons dans la boucle de notre programme. Nous avons donc a priori pas à nous en soucier, car nous travaillons toujours à un niveau d'abstraction supérieur.
Ici, nous aurons besoin de passer par là pour pouvoir contourner le problème posé. En programmation système Windows, chaque fenêtre (au sens large du terme) possède sa propre callback, et une fenêtre reçoit toujours dans sa callback les messages de ses fenêtres parentes : il est possible en effet de définir une hiérarchie au niveau des fenêtres (un peu comme le principe d'héritage en C++), et c'est cette possibilité que nous allons exploiter.
Les mains dans le cambouis
Le problème que nous avons constaté est donc que le message de destruction de la fenêtre (qui entraîne la terminaison de l'application) n'est pas reçu par notre programme en mode aperçu, car la fenêtre qui sert au dessin ne lui appartient pas. Nous allons pouvoir contourner le problème, en utilisant le fait cité plus haut qu'une fenêtre fille reçoit les messages de ses parents.
Il nous suffit donc de créer une fenêtre fille invisible (on la cache) à celle utilisée pour le rendu, et nous pourrons ainsi espionner sa mère. Cependant, nous sortons également des capacités de la SFML, et nous allons devoir écrire du code système Windows pour résoudre le problème.
Nous allons procéder comme suit :
Créer une fonction callback qui aura pour but de capturer ce fameux message de destruction
Créer une fenêtre fille à la fenêtre de rendu et l'associer au callback
Pour cela, nous allons rajouter les deux déclarations suivantes en haut de notre fichier ScreenSaver.cpp :
// déclaration de notre propre callback, pour le mode aperçu
static long CALLBACK childWndProc(HWND, UINT, WPARAM, LPARAM);
static bool DestroyReceived = false;
le bool sera mis à true lors de la réception du message, ce qui permettra à la boucle principale de la fonction "run" de s'en rendre compte.
Ensuite, on rajoute quelques lignes de code dans la partie initialisation des tâches de notre fonction "run", qui devient ainsi :
// en fonction du type de requête, on fait différentes initialisations
switch(request)
{
case CONFIG:
// dans le cas d'une config, on quitte direct la fonction run après l'appel de config
// tout ce qui suit ne sera ainsi jamais appelé dans le cas d'une configuration
m_HandleFenetre = (HWND)(atoi(strtok(NULL, "")));
return config();
break;
case SCREEN_SAVER:
// création de la fenêtre en plein écran, et remplissage des variables
if (debug)
{
m_VideoMode = sf::VideoMode(800, 600, 32);
m_FullScreen = false;
m_RenderWindow = new sf::Window(m_VideoMode, "SiteDuZero");
}
else
{
m_VideoMode = videoMode;
m_FullScreen = true;
m_RenderWindow = new sf::Window(m_VideoMode, "SiteDuZero" , sf::Style::Fullscreen);
}
// on récupère le handle de la fenêtre SFML
m_HandleFenetre = GetActiveWindow();
// on cache le curseur de la souris
m_RenderWindow->ShowMouseCursor(false);
break;
case PREVIEW:
// création de la fenêtre depuis le handle récupéré
m_HandleFenetre = (HWND)(atoi(strtok(NULL, "")));
m_RenderWindow = new sf::Window(m_HandleFenetre);
//## Y'A DU CODE EN PLUS ICI ! ##//
// on remplit les champs de notre classe windows pour créer une autre fenêtre
childClass.style = 0;
childClass.lpfnWndProc = childWndProc;
childClass.cbClsExtra = 0;
childClass.cbWndExtra = 0;
childClass.hInstance = hInstance;
childClass.hIcon = NULL;
childClass.hCursor = NULL;
childClass.hbrBackground = NULL;
childClass.lpszMenuName = NULL;
childClass.lpszClassName = L"childWindowClass";
// on l'enregistre dans le système
RegisterClass(&childClass);
// et enfin, on créée la fenêtre
childWindow = CreateWindow(L"childWindowClass", L"SiteDuZero", WS_CHILD, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, m_HandleFenetre, NULL, hInstance, NULL);
// si la fenêtre n'a pas été créée, on quitte immédiatement.
if (!childWindow) return EXIT_FAILURE;
//## ET CA SE TERMINE LA ##//
// enfin on définit les membres
m_VideoMode = sf::VideoMode(m_RenderWindow->GetWidth(), m_RenderWindow->GetHeight());
m_FullScreen = false;
break;
default:
break;
}
La création d'une fenêtre dans Windows se fait en plusieurs étapes. Premièrement, on doit créer une "classe". Ce que Windows appelle une classe, c'est une structure contenant des paramètres nécessaires à la création d'une fenêtre. Ensuite, on enregistre cette classe dans le système (registerClass). Et enfin, on créée la fenêtre (en prenant bien soir de lui indiquer qui est sa mère !), en précisant la classe utilisée. Vous pouvez rechercher les fonctions utilisées ici dans la documentation MSDN si vous souhaitez avoir plus de détail (les détailler ici prendrait beaucoup de place, de temps et prendrait la tête à beaucoup d'entre vous, je vous ai déjà assez embêté avec la théorie système ^^)
Notez au passage que si la création de la fenêtre fille échoue on demande à arrêter immédiatement l'application, car dans ce cas on retrouve le problème posé ce qui comme dit précédemment n'est pas acceptable.
Ensuite, on modifie la boucle de rendu pour tenir compte de notre fameux booléen, comme suit :
// boucle de rendu
// d'abord on initialise
if (!init()) return EXIT_FAILURE;
// on rentre dans la boucle
while (m_RenderWindow->IsOpened())
{
//## LE CODE EN PLUS EST ICI ##//
// si notre booléen est à true, on ferme la fenêtre
if (DestroyReceived)
{
m_RenderWindow->Close();
continue; // on applique immédiatement en terminant cette boucle
}
//## ET VOILÀ ##//
// traitement des évènements
while (m_RenderWindow->GetEvent(toProcess))
{
// demande de fermeture de l'application
if (sf::Event::Closed == toProcess.Type)
{
m_RenderWindow->Close();
continue;
}
if (SCREEN_SAVER == request)
{
// évènements clavier et souris, sauf mouse moved
if (sf::Event::KeyPressed == toProcess.Type ||
sf::Event::KeyReleased == toProcess.Type ||
sf::Event::MouseButtonPressed == toProcess.Type ||
sf::Event::MouseButtonReleased == toProcess.Type)
{
m_RenderWindow->Close();
continue;
}
if (sf::Event::MouseMoved == toProcess.Type && !debug)
{
if (mouseX < 0 || mouseY < 0)
{
mouseX = toProcess.MouseMove.X;
mouseY = toProcess.MouseMove.Y;
}
else if (mouseX != toProcess.MouseMove.X || mouseY != toProcess.MouseMove.Y)
{
m_RenderWindow->Close();
continue;
}
}
}
}
// appel de render ou de renderPreview
if (SCREEN_SAVER == request)
{
if (!(render())) m_RenderWindow->Close();
}
else
{
if (!(renderPreview())) m_RenderWindow->Close();
}
}
// on finit par l'appel de shutDown, pour tout terminer correctement
if (!shutDown()) return EXIT_FAILURE;
Voilà, pas bien délicat, si notre booléen est à true on s'arrête.
Enfin, il faut écrire le corps de la window procedure, ce qui est relativement court à faire :
Si on reçoit le message WM_DESTROY (Window Message DESTROY), on met le booléen à true, puis on appelle la window procedure par défaut pour ne pas s'embêter avec le reste.
Si je récapépète, le code de "ScreenSaver.cpp" devrait ressembler à ça au final :
#include "include/ScreenSaver.h"
#pragma warning (disable : 4996)
// déclaration de notre propre callback, pour le mode aperçu
static long CALLBACK childWndProc(HWND, UINT, WPARAM, LPARAM);
static bool DestroyReceived = false;
/************************************************************************/
/* Fonctions publiques */
/************************************************************************/
// construction et destructions
ScreenSaver::ScreenSaver() : m_RenderWindow(NULL)
{}
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
ScreenSaver::~ScreenSaver()
{
// destruction de m_RenderWindow si celui-ci a été construit
if (m_RenderWindow) delete m_RenderWindow;
}
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
int ScreenSaver::run(HINSTANCE hInstance, LPSTR lpCmdLine, const sf::VideoMode & videoMode, const bool debug)
{
RequestType request; // la requête du système
char * jeton; // ce qui sera retourné par strtok
sf::Event toProcess; // évènement à traiter
int mouseX = -1; int mouseY = -1;
WNDCLASS childClass; // la classe utilisée pour créer une fenêtre
HWND childWindow = NULL; // la fenêtre fille utilisée pour le mode aperçu
// on fait un strtok sur lpCmdLine pour récupérer la requête
jeton = strtok(lpCmdLine, " :");
// si jeton est différent de null on teste
if (NULL != jeton)
{
// on fait ensuite un if, pour savoir où l'on est
if (strncmp(jeton, "/c", 2) == 0) // si on a /c
{
request = CONFIG;
}
else if (strncmp(jeton, "/p", 2) == 0) // si on a /p
{
request = PREVIEW;
}
else
{
request = SCREEN_SAVER; // si on /s ou rien ou n'importe quoi d'autre
}
}
else
request = SCREEN_SAVER;
// en fonction du type de requête, on fait différentes initialisations
switch(request)
{
case CONFIG:
// dans le cas d'une config, on quitte direct la fonction run après l'appel de config
// tout ce qui suit ne sera ainsi jamais appelé dans le cas d'une configuration
m_HandleFenetre = (HWND)(atoi(strtok(NULL, "")));
return config();
break;
case SCREEN_SAVER:
// création de la fenêtre en plein écran, et remplissage des variables
if (debug)
{
m_VideoMode = sf::VideoMode(800, 600, 32);
m_FullScreen = false;
m_RenderWindow = new sf::Window(m_VideoMode, "SiteDuZero");
}
else
{
m_VideoMode = videoMode;
m_FullScreen = true;
m_RenderWindow = new sf::Window(m_VideoMode, "SiteDuZero" , sf::Style::Fullscreen);
}
// on récupère le handle de la fenêtre SFML
m_HandleFenetre = GetActiveWindow();
// on cache le curseur de la souris
m_RenderWindow->ShowMouseCursor(false);
break;
case PREVIEW:
// création de la fenêtre depuis le handle récupéré
m_HandleFenetre = (HWND)(atoi(strtok(NULL, "")));
m_RenderWindow = new sf::Window(m_HandleFenetre);
// on remplit les champs de notre classe windows pour créer une autre fenêtre
childClass.style = 0;
childClass.lpfnWndProc = childWndProc;
childClass.cbClsExtra = 0;
childClass.cbWndExtra = 0;
childClass.hInstance = hInstance;
childClass.hIcon = NULL;
childClass.hCursor = NULL;
childClass.hbrBackground = NULL;
childClass.lpszMenuName = NULL;
childClass.lpszClassName = L"childWindowClass";
// on l'enregistre dans le système
RegisterClass(&childClass);
// et enfin, on créée la fenêtre
childWindow = CreateWindow(L"childWindowClass", L"SiteDuZero", WS_CHILD, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, m_HandleFenetre, NULL, hInstance, NULL);
// si la fenêtre n'a pas été créée, on quitte immédiatement.
if (!childWindow) return EXIT_FAILURE;
// enfin on définit les membres
m_VideoMode = sf::VideoMode(m_RenderWindow->GetWidth(), m_RenderWindow->GetHeight());
m_FullScreen = false;
break;
default:
break;
}
// boucle de rendu
// d'abord on initialise
if (!init()) return EXIT_FAILURE;
// on rentre dans la boucle
while (m_RenderWindow->IsOpened())
{
// si notre booléen est à true, on ferme la fenêtre
if (DestroyReceived)
{
m_RenderWindow->Close();
continue; // on applique immédiatement en terminant cette boucle
}
// traitement des évènements
while (m_RenderWindow->GetEvent(toProcess))
{
// demande de fermeture de l'application
if (sf::Event::Closed == toProcess.Type)
{
m_RenderWindow->Close();
continue;
}
if (SCREEN_SAVER == request)
{
// évènements clavier et souris, sauf mouse moved
if (sf::Event::KeyPressed == toProcess.Type ||
sf::Event::KeyReleased == toProcess.Type ||
sf::Event::MouseButtonPressed == toProcess.Type ||
sf::Event::MouseButtonReleased == toProcess.Type)
{
m_RenderWindow->Close();
continue;
}
if (sf::Event::MouseMoved == toProcess.Type && !debug)
{
if (mouseX < 0 || mouseY < 0)
{
mouseX = toProcess.MouseMove.X;
mouseY = toProcess.MouseMove.Y;
}
else if (mouseX != toProcess.MouseMove.X || mouseY != toProcess.MouseMove.Y)
{
m_RenderWindow->Close();
continue;
}
}
}
}
// appel de render ou de renderPreview
if (SCREEN_SAVER == request)
{
if (!(render())) m_RenderWindow->Close();
}
else
{
if (!(renderPreview())) m_RenderWindow->Close();
}
}
// on finit par l'appel de shutDown, pour tout terminer correctement
if (!shutDown()) return EXIT_FAILURE;
// fin de la fonction
return EXIT_SUCCESS;
}
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
/************************************************************************/
/* Fonction protégées */
/************************************************************************/
int ScreenSaver::config()
{
MessageBox(m_HandleFenetre, L"Il n'y a rien à configurer", L"Information", MB_OK | MB_ICONASTERISK);
return EXIT_SUCCESS;
}
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
bool ScreenSaver::init()
{
return true;
}
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
bool ScreenSaver::shutDown()
{
return true;
}
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
bool ScreenSaver::renderPreview()
{
return render();
}
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
long CALLBACK childWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
if (uMsg == WM_DESTROY) DestroyReceived = true;
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
Et voilà, notre classe ScreenSaver est maintenant complète et fonctionnelle à 100%. Enjoy !
Nous allons maintenant finir en beauté et créer la librairie que vous pourrez utiliser dans tous vos projets d'écran de veille.
Nous y voilà. Vous verrez que créer la librairie ne relève pas du très grand défi.
Création et configuration du projet
La première chose à faire consiste à créer un projet de librairie statique. Ici aussi, pas de PCH ni d'ATL. Dans le répertoire de votre projet, créez un dossier "include" et un dossier "lib". Nous mettrons le header dans le dossier include, et la librairie dans le dossier lib.
Ensuite, configurez le projet comme suit :
Les librairies s'appellent "SSLIb.lib" et "SSLib-d.lib" en fonction de la configuration, et sont dans le répertoire "lib" du projet (pour VS, outil "générateur de librairie", "général" et "sortie", la valeur prend "$(ProjectDir)lib/SSLib.lib" en release et "$(ProjectDir)lib/SSLib-d.lib" en debug).
Copiez ensuite "ScreenSaver.h" dans le dossier include, et "ScreenSaver.cpp" dans le dossier de base du projet.
N'oubliez pas de modifier l'include dans le cpp en :
#include "include/ScreenSaver.h"
Car le header est dans un autre répertoire maintenant.
Générez votre projet, et le tour est joué.
Il vous reste maintenant à configurer votre IDE afin de prendre en compte le dossier "include" dans la liste de recherche des headers, et le dossier "lib" pour les librairies (de la même manière que vous avez configuré SFML).
A présent, pour créer un écran de veille il suffit de faire un projet Win32, et d'utiliser le code minimal suivant (il s'agit du code simplifié de l'écran de veille que j'ai créé pour mettre en ligne très bientôt) :
// pour faire un écran de veille
#include <ScreenSaver.h>
// ajout de la librairie
#ifdef _DEBUG
#pragma comment (lib, "SSLib-d.lib")
#else
#pragma comment (lib, "SSLib.lib")
#endif
// classe dérivant de ScreenSaver
class BoundingBox : public ScreenSaver
{
public:
protected:
virtual bool render()
{
// faire du rendu ici !
}
private:
};
int _stdcall WinMain(HINSTANCE hInstance, HINSTANCE /*hPrevInstance*/, LPSTR lpCmdLine, int /*nShowCmd*/)
{
BoundingBox app; // on instancie notre classe fille !
#ifdef _DEBUG
return app.run(hInstance, lpCmdLine, sf::VideoMode(GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN), 32), true);
#else
return app.run(hInstance, lpCmdLine, sf::VideoMode(GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN), 32), false);
#endif
}
Et voilà, il ne reste plus qu'à faire jouer votre imagination ^^
Bon ben voilà, notre petit tutoriel est terminé. J'espère que tout ceci vous a plu. J'espère également que vous avez appris beaucoup de choses intéressantes que vous y aurez pris du plaisir. N'hésitez pas à me laisser des commentaires sur ce tutoriel. J'ouvrirai très bientôt un sujet sur les forums, dans lequel vous pourrez si vous le souhaitez nous proposer vos créations qui je n'en doute pas seront aussi intéressantes les unes que les autres.
Merci beaucoup aux quelques courageux qui ont lu ma première version du tutoriel (ils en seraient encore à la première partie là ^^) et qui m'ont laissé les commentaires m'encourageant à revoir ce dernier, en espérant que j'ai répondu à leurs attentes. Merci également aux nombreux lecteurs de cette version qui m'ont également proposé certaines modifications.
Mesdames et messieurs les Zéros, à vos claviers, la chasse est ouverte !
Et à une prochaine fois, pour de nouvelles aventures, promis !
Bientôt un nouveau tutoriel en ligne pour vous... :)