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

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 ^^.
Ce sous-titre est purement théorique, et si vous le sautez cela n'affectera pas le fonctionnement du programme. Vous pouvez donc passer direcement au sous-titre suivant ("Les mains dans le cambouis") - ce que je ne vous conseille pas, mais comme on dit vous êtes maîtres de votre "deasthaing" ^^.
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 : WikipediaEn 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 :
Code : C++ - déclarations statiques | // 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 :
Code : C++ - run, V4.1 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61 | // 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 :
Code : C++ - run, V4.2 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62 | // 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 :
Code : C++ - WindowProc | long CALLBACK childWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
if (uMsg == WM_DESTROY) DestroyReceived = true;
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
|
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 :
Secret (cliquez pour afficher)
Code : C++ - code complet ScreenSaver.cpp 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235 | #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.