Tutorial 26: Splash Screen

Maintenant que nous savons comment utiliser un bitmap, nous pouvons progresser en s'en servant d'une manière plus créatrice. Le 'splash screen'. Downloadez l'exemple. Mais en fait cet exemple est zippé et puisqu'il fait appelle à un DLL, il ne fonctionnera correctement qu'en l'ayant dézippé d'abord!

Théorie

Un splash screen est une fenêtre qui n'a aucune barre de titre, aucun système de menu, aucune frontière et qui affiche un bitmap un court instant avant de le faire redisparaître automatiquement. On s'en sert d'habitude au démarrage d'un programme, pour montrer un logo ou pour détourner l'attention de l'utilisateur pendant que le programme rame à cause de longues initialisations. Nous allons donc créer un splash screen dans ce tutorial.
La première étape c'est d'inclure le bitmap dans le fichier de ressource. Cependant, si vous réfléchissez, c'est une perte de mémoire précieuse de charger un bitmap qui ne sera employé qu'une seule fois. Une solution bien meilleure, c'est de créer une sorte de *DLL resource* qui contient le bitmap et qui aura pour unique but d'afficher notre Splash screen. De cette façon, vous pouvez charger le DLL lorsque vous souhaitez afficher le Splash screen puis le décharger quand il n'est plus nécessaire. Donc on aura deux modules : le programme principal et le DLL Splash. Nous plaçons le bitmap dans le DLL ressource.
Le schéma général est comme suit :
  1. Mettez le bitmap dans le DLL comme un bitmap mis en ressource.
  2. Le programme principal appelle LoadLibrary pour charger le dll en mémoire.
  3. On appelle l'entrypoint du DLL. On créera un minuteur (un Timer) et nous définiront la durée d'affichage du Splash screen. Ensuite nous enregistrerons et créerons une fenêtre sans titre ni bords puis nous afficherons le bitmap dans son secteur client.
  4. Quand la durée indiquée s'est écoulée, le Splash screen est enlevé de l'écran et le contrôle est rendu au programme principal.
  5. Le programme principal appelle FreeLibrary pour enlever (décharger) le DLL de la mémoire et continue ensuite avec n'importe quelle autre tâche que nous supposons avoir mis à sa suite.
Nous allons examiner les mécanismes en détail.

Chargement/Déchargement du DLL (Load/Unload)

Vous pouvez charger dynamiquement un DLL grâce à la fonction LoadLibrary qui a la syntaxe suivante :
LoadLibrary  proto lpDLLName:DWORD
Elle ne prend seulement qu'un paramètre : l'adresse du nom du DLL que vous voulez charger en mémoire. Si l'appel est couronné de succès, il renvoie l'handle du DLL, sinon elle renvoie un NULL.
Pour décharger un DLL, appelez FreeLibrary :
FreeLibrary  proto hLib:DWORD
Il ne prend aussi qu'un seul paramètre : l'handle du DLL que vous souhaitez décharger. Normalement, vous avez obtenu cet handle grâce à LoadLibrary.

Comment employer un 'TIMER' (minuteur) ?

D'abord, vous devez créer un Timer avec SetTimer :
SetTimer  proto hWnd:DWORD, TimerID:DWORD, uElapse:DWORD, lpTimerFunc:DWORD

hWnd is the hest l'handle de la fenêtre qui recevra le message d'avis du Timer. Ce paramètre peut être NULL pour spécifier qu'il n'y a aucune fenêtre étant associée au Timer.
TimerID est une valeur définie par l'utilisateur, laquelle est employée en tant qu'ID du Timer.
uElapse est la valeur du temps d'attente en millisecondes.
lpTimerFunc est l'adresse de la fonction qui traitera les messages d'avis du Timer. Si vous passez un NULL, les messages du Timer seront envoyés à la fenêtre indiquée par le paramètre hWnd.

SetTimer renvoie l'ID du Timer en cas de succès. Autrement il renvoie un NULL. Donc il est préférable de ne pas mettre l'ID du Timer à 0.

Vous pouvez créer un Timer de deux façons : Nous emploierons la première approche dans cet exemple.
Quand la période de temps mort s'est écoulée, le message WM_TIMER est envoyé à la fenêtre qui est associée au Timer. Par exemple, si vous spécifiez un uElapse de 1000, votre fenêtre recevra WM_TIMER chaque seconde.
Lorsque vous n'avez plus besoin du Timer, on le détruit avec KillTimer :
KillTimer proto hWnd:DWORD, TimerID:DWORD

Exemple:

;-----------------------------------------------------------------------
;                         Voici le programme principal:
;-----------------------------------------------------------------------
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

.data
ClassName db "SplashDemoWinClass",0
AppName  db "Splash Screen Example",0
Libname db "splash.dll",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
.code
start:
 invoke LoadLibrary,addr Libname
 .if eax!=NULL
    invoke FreeLibrary,eax
 .endif
 invoke GetModuleHandle, NULL
 mov    hInstance,eax
 invoke GetCommandLine
 mov    CommandLine,eax
 invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
 invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
 LOCAL wc:WNDCLASSEX
 LOCAL msg:MSG
 LOCAL hwnd:HWND
 mov   wc.cbSize,SIZEOF WNDCLASSEX
 mov   wc.style, CS_HREDRAW or CS_VREDRAW
 mov   wc.lpfnWndProc, OFFSET WndProc
 mov   wc.cbClsExtra,NULL
 mov   wc.cbWndExtra,NULL
 push  hInstance
 pop   wc.hInstance
 mov   wc.hbrBackground,COLOR_WINDOW+1
 mov   wc.lpszMenuName,NULL
 mov   wc.lpszClassName,OFFSET ClassName
 invoke LoadIcon,NULL,IDI_APPLICATION
 mov   wc.hIcon,eax
 mov   wc.hIconSm,eax
 invoke LoadCursor,NULL,IDC_ARROW
 mov   wc.hCursor,eax
 invoke RegisterClassEx, addr wc
 INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
           WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
           CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
           hInst,NULL
 mov   hwnd,eax
 invoke ShowWindow, hwnd,SW_SHOWNORMAL
 invoke UpdateWindow, hwnd
 .while TRUE
  invoke GetMessage, ADDR msg,NULL,0,0
  .break .if (!eax)
  invoke TranslateMessage, ADDR msg
  invoke DispatchMessage, ADDR msg
 .endw
 mov     eax,msg.wParam
 ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
 .IF uMsg==WM_DESTROY
  invoke PostQuitMessage,NULL
 .ELSE
  invoke DefWindowProc,hWnd,uMsg,wParam,lParam
  ret
 .ENDIF
 xor eax,eax
 ret
WndProc endp
end start

;--------------------------------------------------------------------
;                         Et voici le DLL contenant le Bitmap:
;--------------------------------------------------------------------
.386
.model flat, stdcall
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\gdi32.lib
.data
BitmapName db "MySplashBMP",0
ClassName db "SplashWndClass",0
hBitMap dd 0
TimerID dd 0

.data
hInstance dd ?

.code

DllEntry proc hInst:DWORD, reason:DWORD, reserved1:DWORD
   .if reason==DLL_PROCESS_ATTACH  ; Quand le dll est chargé.
      push hInst
      pop hInstance
      call ShowBitMap
   .endif
   mov eax,TRUE

   ret
DllEntry Endp
ShowBitMap proc
        LOCAL wc:WNDCLASSEX
        LOCAL msg:MSG
        LOCAL hwnd:HWND
        mov   wc.cbSize,SIZEOF WNDCLASSEX
        mov   wc.style, CS_HREDRAW or CS_VREDRAW
        mov   wc.lpfnWndProc, OFFSET WndProc
        mov   wc.cbClsExtra,NULL
        mov   wc.cbWndExtra,NULL
        push  hInstance
        pop   wc.hInstance
        mov   wc.hbrBackground,COLOR_WINDOW+1
        mov   wc.lpszMenuName,NULL
        mov   wc.lpszClassName,OFFSET ClassName
        invoke LoadIcon,NULL,IDI_APPLICATION
        mov   wc.hIcon,eax
        mov   wc.hIconSm,0
        invoke LoadCursor,NULL,IDC_ARROW
        mov   wc.hCursor,eax
        invoke RegisterClassEx, addr wc
        INVOKE CreateWindowEx,NULL,ADDR ClassName,NULL,\
           WS_POPUP,CW_USEDEFAULT,\
           CW_USEDEFAULT,250,250,NULL,NULL,\
           hInstance,NULL
        mov   hwnd,eax
        INVOKE ShowWindow, hwnd,SW_SHOWNORMAL
        .WHILE TRUE
                INVOKE GetMessage, ADDR msg,NULL,0,0
                .BREAK .IF (!eax)
                INVOKE TranslateMessage, ADDR msg
                INVOKE DispatchMessage, ADDR msg
        .ENDW
        mov     eax,msg.wParam
        ret
ShowBitMap endp
WndProc proc hWnd:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
        LOCAL ps:PAINTSTRUCT
        LOCAL hdc:HDC
        LOCAL hMemoryDC:HDC
        LOCAL hOldBmp:DWORD
        LOCAL bitmap:BITMAP
        LOCAL DlgHeight:DWORD
        LOCAL DlgWidth:DWORD
        LOCAL DlgRect:RECT
        LOCAL DesktopRect:RECT

        .if uMsg==WM_DESTROY
                .if hBitMap!=0
                        invoke DeleteObject,hBitMap
                .endif
                invoke PostQuitMessage,NULL
        .elseif uMsg==WM_CREATE
                invoke GetWindowRect,hWnd,addr DlgRect
                invoke GetDesktopWindow
                mov ecx,eax
                invoke GetWindowRect,ecx,addr DesktopRect
                push  0
                mov  eax,DlgRect.bottom
                sub  eax,DlgRect.top
                mov  DlgHeight,eax
                push eax
                mov  eax,DlgRect.right
                sub  eax,DlgRect.left
                mov  DlgWidth,eax
                push eax
                mov  eax,DesktopRect.bottom
                sub  eax,DlgHeight
                shr  eax,1
                push eax
                mov  eax,DesktopRect.right
                sub  eax,DlgWidth
                shr  eax,1
                push eax
                push hWnd
                call MoveWindow
                invoke LoadBitmap,hInstance,addr BitmapName
                mov hBitMap,eax
                invoke SetTimer,hWnd,1,2000,NULL
                mov TimerID,eax
        .elseif uMsg==WM_TIMER
                invoke SendMessage,hWnd,WM_LBUTTONDOWN,NULL,NULL
                invoke KillTimer,hWnd,TimerID
        .elseif uMsg==WM_PAINT
                invoke BeginPaint,hWnd,addr ps
                mov hdc,eax
                invoke CreateCompatibleDC,hdc
                mov hMemoryDC,eax
                invoke SelectObject,eax,hBitMap
                mov hOldBmp,eax
                invoke GetObject,hBitMap,sizeof BITMAP,addr bitmap
                invoke StretchBlt,hdc,0,0,250,250,\
                       hMemoryDC,0,0,bitmap.bmWidth,bitmap.bmHeight,SRCCOPY
                invoke SelectObject,hMemoryDC,hOldBmp
                invoke DeleteDC,hMemoryDC
                invoke EndPaint,hWnd,addr ps
        .elseif uMsg==WM_LBUTTONDOWN
                invoke DestroyWindow,hWnd
        .else
                invoke DefWindowProc,hWnd,uMsg,wParam,lParam
                ret
        .endif
        xor eax,eax
        ret
WndProc endp

End DllEntry

Analyse:

Dans un premier temps, nous allons nous pencher sur le code du programme principal.
 invoke LoadLibrary,addr Libname
 .if eax!=NULL
    invoke FreeLibrary,eax
 .endif
Nous appelons LoadLibrary pour charger le DLL nommé "splash.DLL". Et après ça, déchargez-le de la mémoire avec FreeLibrary. LoadLibrary ne retournera rien avant que le DLL n'en ait fini avec son initialisation..
C'est tout pour le programme principal. La partie intéressante c'est le DLL.

   .if reason==DLL_PROCESS_ATTACH  ; Quand le dll est chargé.
      push hInst
      pop hInstance
      call ShowBitMap

Lorsque le DLL est chargé, Windows appelle son entrypoint avec le flag DLL_PROCESS_ATTACH. Nous en profitons pour afficher le Splash screen. En premier lieu, nous stockons l'instance handle du DLL pour pouvoir l'utiliser plus tard. On appelle donc une fonction nommée ShowBitMap pour faire ce travail. ShowBitMap enregistre (une Window Class) une classe de fenêtre, et on crée la fenêtre puis on entre dans la boucle de message comme d'habitude. La partie intéressante est dans l'appel de CreateWindowEx :

        INVOKE CreateWindowEx,NULL,ADDR ClassName,NULL,\
           WS_POPUP,CW_USEDEFAULT,\
           CW_USEDEFAULT,250,250,NULL,NULL,\
           hInstance,NULL

Notez que le style de fenêtre est seulement WS_POPUP ce qui fera qu'on obtiendra une fenêtre sans bords et sans titre. Nous limitons aussi la largeur et la hauteur de la fenêtre à 250x250 pixels.
Maintenant que la fenêtre est créée, avec le message WM_CREATE nous plaçons la fenêtre au centre de l'écran grâce au code suivant.

                invoke GetWindowRect,hWnd,addr DlgRect
                invoke GetDesktopWindow
                mov ecx,eax
                invoke GetWindowRect,ecx,addr DesktopRect
                push  0
                mov  eax,DlgRect.bottom
                sub  eax,DlgRect.top
                mov  DlgHeight,eax
                push eax
                mov  eax,DlgRect.right
                sub  eax,DlgRect.left
                mov  DlgWidth,eax
                push eax
                mov  eax,DesktopRect.bottom
                sub  eax,DlgHeight
                shr  eax,1
                push eax
                mov  eax,DesktopRect.right
                sub  eax,DlgWidth
                shr  eax,1
                push eax
                push hWnd
                call MoveWindow

Ceci retrouve les dimensions de notre bureau (qui vont servir de référence) puis celles de la fenêtre en calculant les coordonnées appropriées du coin supérieur gauche de la fenêtre pour pouvoir la placer au centre.

                invoke LoadBitmap,hInstance,addr BitmapName
                mov hBitMap,eax
                invoke SetTimer,hWnd,1,2000,NULL
                mov TimerID,eax

Ensuite on charge le 'bitmap ressource' avec LoadBitmap et on crée un minuteur (un Timer) avec l'ID du Timer ayant la valeur de '1', et un intervalle de temps de 2 secondes. Le Timer enverra des messages WM_TIMER à la fenêtre toutes les 2 secondes.

        .elseif uMsg==WM_PAINT
                invoke BeginPaint,hWnd,addr ps
                mov hdc,eax
                invoke CreateCompatibleDC,hdc
                mov hMemoryDC,eax
                invoke SelectObject,eax,hBitMap
                mov hOldBmp,eax
                invoke GetObject,hBitMap,sizeof BITMAP,addr bitmap
                invoke StretchBlt,hdc,0,0,250,250,\
                       hMemoryDC,0,0,bitmap.bmWidth,bitmap.bmHeight,SRCCOPY
                invoke SelectObject,hMemoryDC,hOldBmp
                invoke DeleteDC,hMemoryDC
                invoke EndPaint,hWnd,addr ps

Quand la fenêtre reçoit le message WM_PAINT, elle crée une mémoire DC, choisit le bitmap dans la mémoire DC, obtient la taille du bitmap avec GetObject et met ensuite le bitmap sur la fenêtre en appelant StretchBlt, lequel se comporte comme BitBlt (il recopie le bitpam sur notre fenêtre) mais il en plus est capable de compresser ou de décompresser le bitmap à la dimension désirée. Dans notre cas, nous voulons que le bitmap s'accorde aux dimensions de la fenêtre, c'est pourquoi nous utilisons StretchBlt au lieu de BitBlt. Nous supprimons la mémoire DC après ça.

        .elseif uMsg==WM_LBUTTONDOWN
                invoke DestroyWindow,hWnd

Ce serait frustrant pour l'utilisateur qu'il soit obligé d'attendre que le Splash screen disparaisse. On aimerait que l'utilisateur ait le choix. Quand il clique sur Splash screen, il faudrait qu'il disparaisse. C'est pourquoi nous avons besoin de traiter le message WM_LBUTTONDOWN dans le DLL. En recevant ce message, la fenêtre est détruite par l'appel de DestroyWindow.

        .elseif uMsg==WM_TIMER
                invoke SendMessage,hWnd,WM_LBUTTONDOWN,NULL,NULL
                invoke KillTimer,hWnd,TimerID

Si l'utilisateur attend, le Splash screen disparaîtra lorsque le temps indiqué se sera écoulé (dans notre exemple, au bout de 2 secondes). On fait ça en traitant le message WM_TIMER. Après avoir reçu ce message, nous fermons la fenêtre en lui envoyant le message WM_LBUTTONDOWN. Ça permet d'éviter la duplication du code. Puisque nous n'avons plus besoin d'utiliser le Timer, nous le détruisons avec KillTimer.
Après que la fenêtre soit fermée, le DLL renverra le contrôle au programme principal.


[Iczelion's Win32 Assembly HomePage]


Traduit par Morgatte