[Tutoriel] La gestion du clavier en C
Posté le 17/02/2016 14:11
Pour des questions de pratique ou d'habitude, beaucoup d'entre nous utilisent des fonctions comme IsKeyDown() pour gérer leurs entrées claviers. C'est mal, et voici pourquoi.
Le problème
Le principe d'un programme disposant d'une interface graphique est simple : il travaille quand l'utilisateur lui donne du boulot et le reste du temps il attend patiemment de recevoir des ordres (travailleur acharné, je vous dis), et se met donc en pause pour un temps indéterminé. En clair, il dort.
Seulement voilà, les bons jeux en temps réel donnent au programme du travail à faire régulièrement (comme faire bouger les adversaires sur une map ou faire avancer des animations) et cet ordre ne se donne pas au clavier.
Et là, l'erreur de logique apparaît : au lieu de réveiller le programme régulièrement pour lui rappeler qu'il a du travail, on va plutôt l'empêcher de dormir. « De toute façon il fonctionne tout aussi bien donc autant le maltraiter, ça ne fait de mal à personne », me direz-vous.
Sauf que ça fait du mal à la machine, qui d'une part n'apprécie pas de ne jamais pouvoir se reposer (effet similaire à de l'overlock), ce qui use les piles, et la rend peu réceptive aux signaux qu'on lui envoie puisqu'elle est occupée à faire autre chose.
Alors n'en déplaise aux concepteurs de bibliothèques (je pense à
input mais cela n'a rien contre Ninestars), les méthodes de gestion du clavier qui ne mettent pas le programme en pause (puisque finalement le clavier c'est l'interface de l'utilisateur pour donner des ordres) ne sont pas propres.
« On a setFps() » me répondrez-vous, mais setFps() utilise Sleep() (de manière un peu barbare d'ailleurs) et Sleep() est une fonction qui ne prend même pas la peine d'endormir le processeur (désassemblez le syscall 0x420, vous verrez, y'en a pour 20 lignes d'assembleur et c'est une horreur), donc c'est du pareil au même.
La solution
Assez parlé, la méthode correcte est donc, si l'on reprend notre analogie, de laisser dormir notre processeur, en lui programmant tout de même un réveil pour qu'il n'oublie pas de faire son boulot. Ça tombe bien, ce réveil s'appelle un timer et c'est probablement la fonctionnalité la moins utilisée de fxlib.
#include "fxlib.h"
#include "timer.h"
void SetTimer(int timer_id, int elapse, void (*callback)(void));
void KillTimer(int timer_id);
Ces fonctions sont très simples d'utilisation. Le timer_id s'écrit ID_USER_TIMER
X, avec X variant de 1 à 5. Si vous avez la flemme vous pouvez aussi écrire de 1 à 5, ça fonctionne aussi.
La durée s'exprime en millisecondes mais le résultat est arrondi à un multiple de 25 ms (probablement tronqué en fait). Vous parlez donc en millisecondes mais vous indiquez un multiple de 25.
Si vous n'êtes pas familier avec la notation du troisième paramètre, il s'agit d'un pointeur sur une fonction de la forme « void fonction(void) ». En gros c'est la fonction quoi.
Une fois SetTimer() appelée, le timer se lance et chaque fois que la durée indiquée s'écoule, la fonction que vous avez passée en troisième argument est appelée (et ce quoi que le programme soit en train de faire à ce moment-là). Notez que le timer se recharge automatiquement.
Lorsque vous voulez arrêter le timer, vous faites appel à KillTimer(). N'oubliez pas d'arrêter le timer quand vous quittez votre fonction, vous n'avez définitivement pas envie que vos ennemis continuent à bouger sur l'écran alors que vous êtes revenu au menu principal.
Maintenant que vous avez de quoi réveiller votre processeur, vous allez pouvoir lui octroyer du temps de sommeil avec la fameuse fonction GetKey().
int GetKey(unsigned int *key);
GetKey() endort le processeur jusqu'à ce qu'une pression sur une touche le réveille, excepté qu'aujourd'hui vous avez aussi prévu un timer pour le réveiller. L'argument est l'adresse d'une variable dans laquelle le code de la touche sera stocké en suivant la norme de l'en-tête keybios.h, et la valeur de retour n'intéresse jamais personne. Votre processeur retrouve enfin le sommeil, et le sourire.
Un exemple
Voici un exemple simple permettant d'afficher des animations à deux frames d'une seconde. C'est volontairement exhaustif. Il n'y a pas particulièrement de commentaires à faire, je pense que c'est assez simple. Si vous avez une question, laissez un commentaire.
#include "fxlib.h"
#include "timer.h"
static void draw(void);
static void callback(void);
static int animation_state = 0;
int main(void)
{
unsigned int key;
// On dessine une premiere fois le contenu de l'ecran au frame 0
animation_state = 0;
draw();
// On met en place le timer pour une seconde de delai
SetTimer(ID_USER_TIMER1, 1000, callback);
while(1)
{
// On laisse dormir le processeur \o/
GetKey(&key);
if(key == KEY_CTRL_EXIT) break;
}
// On arrete le timer avant de quitter la fonction
KillTimer(ID_USER_TIMER1);
}
static void draw(void)
{
Bdisp_AllClr_VRAM();
if(animation_state == 0)
{
// Dessiner au frame 0
}
else
{
// Dessiner au frame 1
}
Bdisp_PutDisp_DD();
}
static void callback(void)
{
// On passe de 0 a 1, ou de 1 a 0
animation_state = !animation_state;
// Et bien sur on redessine !
draw();
}
Le problème du retour au menu
Vous êtes sans doute déjà fou de joie de ces nouvelles connaissances et vous vous apprêtez probablement à les mettre en œuvre avec un enthousiasme renouvelé, et vous vous rendez soudain compte que GetKey() possède quelques fonctionnalités de plus que ses cousines de la famille de IsKeyDown() :
- La capacité de revenir au menu quand on appuie sur MENU
- La modification du contraste avec SHIFT et REPLAY
Le premier vous intéresse peut-être, mais il y a un souci : en effet, lorsque vous revenez au menu, les timers ne sont pas arrêtés. Donc une seconde après votre retour au menu, votre fonction callback() est appelée de nouveau et redessine la map de votre jeu alors que l'utilisateur se trouve bien dans le menu principal de la calculatrice.
Pour cela, il existe une solution (un workaround, en fait) qui consiste à passer par un peu d'assembleur (c'est du syscall en fait, mais les syscalls C sont très peu pratiques à utiliser) mais qui nécessite de vous convertir aux matrix codes.
Les matrix codes, c'est une autre façon de noter les codes des touches. C'est plus intuitif que la liste classique de KEY_CTRL_EXE, KEY_CTRL_EXIT et tout le reste. Observez le tableau suivant :
F1 F2 F3 F4 F5 F6 09
SHIFT OPTN VARS MENU ← ↑ 08
ALPHA ^2 ^ EXIT ↓ → 07
XTT log ln sin cos tan 06
ab/c F↔D ( ) , → 05
7 8 9 DEL 04
4 5 6 x div 03
1 2 3 + - 02
0 . EXP (-) EXE 01
AC 00
06 05 04 03 02 01 00
Vous voulez le code de la touche MENU ? Aucun problème, repérez la colonne... 3, et la ligne... 8. Vous ajoutez 1 (oui, désolé), ça fait 4/9, et vous avez tout de suite le code : 0x0409. EXE est en position 2/1, son code sera donc 0x0302. ALPHA donnera 0x0708. Facile, non ? Libre à vous de refaire des macros ensuite, par exemple :
#define KEY_UP 0x0209
#define KEY_RIGHT 0x0208
#define KEY_DOWN 0x0308
#define KEY_LEFT 0x0309
#define KEY_SHIFT 0x0709
#define KEY_ALPHA 0x0708
#define KEY_EXE 0x0302
#define KEY_EXIT 0x0408
#define KEY_MENU 0x0409
Pour régler le problème du menu, il vous suffit (si vous travaillez avec le fx-9860G SDK, les linuxiens trouveront tous seuls) d'inclure le fichier getkey.src, en fichier joint au tutoriel, à votre projet, et d'utiliser la fonction getkey() qui s'y trouve au lieu de GetKey(). N'oubliez pas de modifier l'extension pour un .src, le site n'accepte pas ce format.
unsigned int key = getkey();
Et là, miracle, la fonction ne vous renvoie plus au menu lorsque vous appuyez sur la touche mais vous renvoie le code de KEY_MENU défini plus haut (0x0409). Notez que cette fonction renvoie systématiquement les matrix codes, donc la touche EXE ne donne plus KEY_CTRL_EXE (30001) mais KEY_EXE (0x0302).
Et alors vous pouvez stopper vous-mêmes vos timers, lancer le menu principal en utilisant la deuxième fonction fournie dans le fichier, et redémarrer vos timers dès que l'utilisateur retourne dans votre application.
key = getkey();
if(key == KEY_MENU)
{
KillTimer(ID_USER_TIMER1);
system_menu();
draw();
SetTimer(ID_USER_TIMER1, 1000, callback);
}
Notez qu'il est intéressant de redessiner le contenu de votre écran pour éviter à l'utilisateur de contempler le menu principal encore une seconde avant que votre timer n'arrive à expiration et ne le fasse lui-même.
Je n'ai pas encore de solution viable pour le problème du contraste (SHIFT + REPLAY), n'oubliez pas lorsque vous testez sur émulateur qu'après SHIFT les directions gauche et droite ne répondent plus, ce n'est pas un bug de votre programme.
En conclusion
Si vous avez lu jusqu'ici, vous n'avez plus le droit d'utiliser IsKeyDown(). Rappelez-vous, il n'y a quasiment aucune circonstance atténuante qui justifie que vous maltraitiez votre processeur.
Je terminerai sur une petite citation de Kirafi :
Kirafi a écrit :Bon c'est bien beau tout ça
, mais maintenant faut balancer le tuto sur le Combo Getkey/Timer pour que Phénix arrête de vomir à chaque fois que quelqu'un sort un Add-In
...
Je compte sur vous...
Fichier joint
Citer : Posté le 04/02/2017 15:38 | #
Pardon, je me suis trompé. T'as le droit de m'engueuler.
Oui, c'est tout à fait ça. C'est juste que t'as pas de moteur encore...
Citer : Posté le 04/02/2017 15:43 | #
Aaaah j'ai eu peur d'avoir tout fais de travers et de devoir tout refaire !
Bon donc walla ça fonctionne bien .
Et si si mon moteur c'est le callback "engine" .
De cette manière d'ailleurs (oui y'a que la trajectoire du perso pour l'instant, mais faut imaginer).
{
// Mouvement du personnage (déplacement et saut)
perso_refresh(&p, key, &g);
// Ici action masquée pour pas révéler le jeu x)
// Actualisation des trajectoires
trajectory_refresh(&p);
// Actualisation de l'écran
screen_refresh(&d, &p, &g);
// Réinitialisation du booléen de saut (pour bloquer la répétition autmatique du saut) et de la touche
if(key != K_UP && key != K_SHIFT && g.jump) g.jump = 0;
key = 0;
}
Sachant que "g" c'est mes globale booleennes, "p" l'objet personnage, et "d" les données (sauvegarde)
Pourras-tu survivre plus de 20 secondes dans ce fameux tunnel appelé Graviton
Rebondis entre les murs en évitant les piques dans SpikeBird
Pourras-tu éviter de te faire écraser dans FallBlocs (élu Jeu Du Mois)
La version 2048 tactile amélioré au plus haut point : 2048 Delux !
Pars à la recherche des morceaux d'étoile dans Lumyce (élu Jeu Du Mois)
Citer : Posté le 04/02/2017 15:48 | #
Je ne suis pas trop sûr pour le coup de « key » dans le moteur. Pour moi tu devrais avoir quelque chose comme ça :
void engine(void)
{
perso_move(direction);
}
int main(void)
{
while(1)
{
// config...
while(key ≠ K_MENU && key ≠ K_EXIT)
{
key = getkey();
if(key == KEY_LEFT) direction = 0;
else if(key == KEY_RIGHT) drection = 1:
}
// etc...
}
Citer : Posté le 04/02/2017 15:55 | #
Oui en y réfléchissant je venais de comprendre ça .
En gros, toutes les action avec "key" sont après le getkey dans le while .
Du coup j'ai mis la fonction "perso_refresh" à cet endroit, et bon bah ça change rien au final .
Donc bon c'est plus propre donc je laisse comme ça .
J'ai juste repéré un p'tit soucis :
Le perso à 3 directions, gauche, centre et droite, avec leur image correspondante.
Le perso est censé se recentrer si aucune touche directionnelles n'est pressée.
Or en mettant la fonction "refresh_perso" après le getkey, il garde sa direction lorsque qu'on relache la touche .
{
if(key == K_LEFT) perso_move(p, L);
else if(key == K_RIGHT) perso_move(p, R);
else perso_center(p);
if((key == K_UP || key == K_SHIFT) && !g->jump) perso_jump(p, g); // Saut si Shift ou ↑ pressée
}
Pourras-tu survivre plus de 20 secondes dans ce fameux tunnel appelé Graviton
Rebondis entre les murs en évitant les piques dans SpikeBird
Pourras-tu éviter de te faire écraser dans FallBlocs (élu Jeu Du Mois)
La version 2048 tactile amélioré au plus haut point : 2048 Delux !
Pars à la recherche des morceaux d'étoile dans Lumyce (élu Jeu Du Mois)
Citer : Posté le 04/02/2017 15:58 | #
Oui, parce que getkey() ne se termine pas quand on relâche la touche. Il faudrait modifier la manière dont cette fonction appelle GetKeyWait() pour qu'elle se termine si rien ne se passe au bout d'un délai convenu.
Citer : Posté le 04/02/2017 16:29 | #
Bah c'est toi le boss de cette fonction getkey custom .
Enfin pour l'instant je laisse comme j'avais puis on verra plus tard du coup .
Ajouté le 06/02/2017 à 22:27 :
Hum Lephé, du coup t'aurais une idée de comment faire pour que le relachement de touche soit détecté ça fait une heure que je cherche, et je trouve pas niveau conception...
J'ai essayé en faisant un truc comme ça, mais ça ne fonctionne pas !
{
...
if(boolean)
{
// Action réalisée tant que la touche K est pressée
if(key != K) boolean = 1;
}
...
}
...
while(1)
{
key = getkey();
if(key == K && !boolean)
{
boolean = 1;
}
}
...
En vrai c'est archi relou, parce qu'avec ce problème, et le soucis de auvegarde que je n'arrive pas à créer, bah j'suis bloqué j'peux pas avancer dans la programmation ...
Pourras-tu survivre plus de 20 secondes dans ce fameux tunnel appelé Graviton
Rebondis entre les murs en évitant les piques dans SpikeBird
Pourras-tu éviter de te faire écraser dans FallBlocs (élu Jeu Du Mois)
La version 2048 tactile amélioré au plus haut point : 2048 Delux !
Pars à la recherche des morceaux d'étoile dans Lumyce (élu Jeu Du Mois)
Citer : Posté le 07/02/2017 07:47 | #
Ah mais tu pourras pas le faire sans toucher à l'assembleur, c'est pas possible !
J'ai pas la doc sous les yeux mais il faut modifier le paramètre de délai et le type d'attente dans les registres. Dès que je passe sur mon ordinateur, je te modifie ça (je m'occuperai aussi du fichier de sauvegarde)
Citer : Posté le 07/02/2017 09:30 | #
Super .
Parce que ce matin j'ai pensé à un système, mais vraiment bricolage ...
Car le truc que je n'explique pas c'est que ce que je veux fonctionne pour déplacer le personnage : tant que droite ou gauche est appuyé, il se déplace, niquel, mais dès que j'appuis sûr autre chose ça le fait pas .
C'est parce que la détection de touche du perso est dans le callback je pense .
Je précise aussi que je tourne avec un timer à 25 millisecondes et une répétition de touche paramétrée avec (1, 1).
Donc en gros faut ça :
- Action ponctuelle à l'appuis (le plus simple à faire -> juste après le getkey)
- Action ponctuelle au relachement (là ça coince)
- Action continue durant l'appuis (visiblement j'ai réussi mais je comprend plus trop , j'checkerais tout ça ce soir)
- Action continue sans l'appuis (j'en ai à priori pas besoin, mais cela devrait juste être une condition dans le callback)
Pourras-tu survivre plus de 20 secondes dans ce fameux tunnel appelé Graviton
Rebondis entre les murs en évitant les piques dans SpikeBird
Pourras-tu éviter de te faire écraser dans FallBlocs (élu Jeu Du Mois)
La version 2048 tactile amélioré au plus haut point : 2048 Delux !
Pars à la recherche des morceaux d'étoile dans Lumyce (élu Jeu Du Mois)
Citer : Posté le 07/02/2017 13:06 | #
Pour tes conditions, il te suffit d'avoir une variable d'état qui est modifiée lors des appuis/relâchements et qui est utilisée par le moteur physique toutes les 25 millisecondes. La modification du getkey() suffit tant qu'on peut détecter les relâchements.
Ajouté le 07/02/2017 à 15:30 :
Raah, c'était pas si compliqué d'être intelligent pourtant... mais Casio a programmé GetKeyWait() avec un timer qui compte en secondes...
Je te mets le diff mais il ne te servira plus à rien je pense... va falloir que je réfléchisse à une solution utilisable pour fxlib. (Note : gint permet ça correctement)
index 1ce79ff..c216e4b 100644
--- a/getkey.s
+++ b/getkey-2.s
@@ -17,8 +17,8 @@ _getkey:
mov r12, r4 ; column
mov r13, r5 ; row
- mov #0, r6 ; type_of_waiting
- mov #0, r7 ; timeout_period
+ mov #2, r6 ; type_of_waiting
+ mov #1, r7 ; timeout_period
; Pushing the following arguments to the stack. (in order !)
mov.l r1, @-r15 ; keycode
Citer : Posté le 07/02/2017 20:00 | #
Ouai bon... Au final tu me dis que ton truc là je dois le mettre où ? À la fin du fichier getkey.src ?
Et en fait ça changera quoi exactement ?
Hum le truc avec le booléen ne fonctionne pas !
Au pire, si t'es dispo pour TS ce soir tu me dis ...
Pourras-tu survivre plus de 20 secondes dans ce fameux tunnel appelé Graviton
Rebondis entre les murs en évitant les piques dans SpikeBird
Pourras-tu éviter de te faire écraser dans FallBlocs (élu Jeu Du Mois)
La version 2048 tactile amélioré au plus haut point : 2048 Delux !
Pars à la recherche des morceaux d'étoile dans Lumyce (élu Jeu Du Mois)
Citer : Posté le 07/02/2017 21:25 | #
C'est un diff... il faut que tu remplaces :
mov #0, r7 ; timeout_period
par :
mov #1, r7 ; timeout_period
Mais ton personnage ne se remettra droit qu'après une seconde parce que le délai est pas plus précis... é_é
Citer : Posté le 07/02/2017 21:28 | #
Okay je change, ce sera déjà ça .
Ajouté le 07/02/2017 à 22:53 :
!! En fait le problème était simple à régler, pour tout dire, je l'avais déjà réglé... Juste que ça ne fonctionne pas pour la touche SHIFT !! Avec d'autres touche ça marche nikel mon bazar là .
En fait j'ai l'impression que quand on appuis sur SHIFT, le getkey la capte ponctuellement, puis la relache.
Ajouté le 07/02/2017 à 22:59 :
Ooookay, visiblement ça le fait pour tous, sauf les touches fléchées...
Pourras-tu survivre plus de 20 secondes dans ce fameux tunnel appelé Graviton
Rebondis entre les murs en évitant les piques dans SpikeBird
Pourras-tu éviter de te faire écraser dans FallBlocs (élu Jeu Du Mois)
La version 2048 tactile amélioré au plus haut point : 2048 Delux !
Pars à la recherche des morceaux d'étoile dans Lumyce (élu Jeu Du Mois)
Citer : Posté le 08/02/2017 12:09 | #
Mais oui, puisque GetKey() répète les touches fléchées. Si tu ne reçois pas d'événement de répétition tu peux en conclure que la touche a été relâchée. Mais ça ne marchera pour aucune autre touche et surtout pas [SHIFT] qui sert de modifieur...
Citer : Posté le 08/02/2017 12:20 | #
Mais fallait le dire avant !!
Comment je fais pour que getkey répéte n'importe quel touche du coup ?
Pourras-tu survivre plus de 20 secondes dans ce fameux tunnel appelé Graviton
Rebondis entre les murs en évitant les piques dans SpikeBird
Pourras-tu éviter de te faire écraser dans FallBlocs (élu Jeu Du Mois)
La version 2048 tactile amélioré au plus haut point : 2048 Delux !
Pars à la recherche des morceaux d'étoile dans Lumyce (élu Jeu Du Mois)
Citer : Posté le 08/02/2017 13:05 | #
Ha ! Tu ne fais pas, justement. Ce n'est pas possible.
Citer : Posté le 08/02/2017 13:59 | #
Okay, j'pensais que getkey bloquait seulement si on appuyais pas, et que ça enregistrait la touche si on laissait appuyé ...
Donc au final, cette méthode a un bon gros défaut .
J'ose imaginer que gint palie à tout ça .
Ajouté le 08/02/2017 à 20:31 :
Mais whaaat ! Je lance le SDcaca, et là, bah ce que je voulais faire initialement avec mon bazar depuis lundi, ce met à fonctionner ! (avec la touche shift)
Mais, j'ai recompilé, et là... marche plus , sans rien changer entre temps bien sûr...
Incompréhension...
Pourras-tu survivre plus de 20 secondes dans ce fameux tunnel appelé Graviton
Rebondis entre les murs en évitant les piques dans SpikeBird
Pourras-tu éviter de te faire écraser dans FallBlocs (élu Jeu Du Mois)
La version 2048 tactile amélioré au plus haut point : 2048 Delux !
Pars à la recherche des morceaux d'étoile dans Lumyce (élu Jeu Du Mois)
Citer : Posté le 08/02/2017 21:32 | #
GCC + gint
Au moins le comportement est bien moins aléatoire
Citer : Posté le 08/02/2017 21:48 | #
En vrai tu as bien raison, ton expérience démontre que ma méthode est difficilement compatible avec fxlib. Je n'ai pas trop pensé aux implémentations utilisant ce système que j'ai écrit ce tutoriel... c'est sans doute un tort.
Ajouté le 08/02/2017 à 21:49 :
(Bon après tu peux passer un coup de IsKeyDown() dans le timer handler pour gérer les relâchements, c'est pas parce que je trouve ça moche qu'il faut pas le faire )
Citer : Posté le 08/02/2017 22:20 | #
Ouai dernier recourt le isKeyDown tant pis.
Sinon ouai j'installerais GCC et gint plus tard, là j'veux juste finir ce jeu rapidement (avant la fin de la semaine).
Pourras-tu survivre plus de 20 secondes dans ce fameux tunnel appelé Graviton
Rebondis entre les murs en évitant les piques dans SpikeBird
Pourras-tu éviter de te faire écraser dans FallBlocs (élu Jeu Du Mois)
La version 2048 tactile amélioré au plus haut point : 2048 Delux !
Pars à la recherche des morceaux d'étoile dans Lumyce (élu Jeu Du Mois)
Citer : Posté le 25/11/2017 21:00 | #
Excusez ma probable ignorance profonde des choses informatiques les plus savantes ainsi que ce déterrage le plus ignominieux (qui plus est d'un sujet qui exacerba les passions si j'en crois les précédents commentaires), mais pour avoir testé quelque peu cette fonctionnalité fort intéressante j'en convient, il m'a semblé constater ceci après de nombreux tests: le timer n'appelle pas la fonction callback toutes les x millisecondes, il l'appelle systématiquement x millisecondes après que sa précédente exécution soit achevée. x correspondrait donc au laps de temps entre deux appels de callback. Du coup, la fréquence d'appel peut ne pas être constante, si la vitesse d'exécution de callback ne l'est pas.
Evidemment, pour une fonction dont la durée d'exécution est extrêmement courte, l'influence est négligeable. Notons tout de même que le risque que callback soit appelée avant que sa précédente exécution ne soit achevée serait donc nul. Je pense que ce détail, s'il s'avère exact, mériterait tout de même d'être noté. Donc quelqu'un peut il me confirmer (ou infirmer) cette hypothèse ?
(Pour tester, j'ai simplement mis en fonction 'timée' une animation cyclique ralentie via Sleep. J'ai constaté visuellement que l'animation réalisait à chaque tour une pause de x milisecondes, et que si je fixais x à 0, elle ne s'interrompait plus, sans qu'il y ait pour autant de problème, d'où ma déduction)
PS : je n'ai pas la moindre idée de si quelqu'un à déjà remarqué cela, donc si ça devait être le cas, ou pire : si tout ce que j'ai raconté ici, c'est que des conneries, pas taper sivouplait !
Citer : Posté le 26/11/2017 06:15 | #
As-tu bien vérifié que tu as utilisé un x multiple de 25 ? Le timer du système tourne à une vitesse de 40 Hz et n'est donc pas capable d'une précision plus élevée ; le délai passé à SetTimer() est automatiquement arrondi au plus proche multiple de 25 (pour une certaine notion de "plus proche" que je n'ai plus en tête).
Du reste, développer un timer qui attend la fin du callback n'est pas très intéressant et en fait plus compliqué que la façon naïve ; ça me surprendrait que ça se passe comme ça (en tous cas gint ne fonctionne pas comme ça).
Concernant ton hypothèse, le callback ne peut pas être interrompu par un autre callback dans un modèle simple de gestion des interruptions. Si le callback est plus lent que le timer, chaque appel sera exécuté juste après que le précédent aura terminé de travailler, donc le callback sera exéxuté en continu. Ce serait une manière de décider quel modèle est en jeu (car dans le tien, le programme principal ne s'arrête jamais entièrement, il dispose toujours exactement du délai entre deux exécutions du callback).
Du coup, que ça tourne en continu n'est pas contradictoire. Je peux développer plus si tu veux.