Tutorial 26: Splash Screen

これまでのチュートリアルを読んできた読者ならビットマップについての知識があるはずなので、 このチュートリアルではさらに一歩進んで「スプラッシュスクリーン」について説明することにする。
 メインソース   ビットマップ   実行結果 
 DLLソース   DEFファイル   リソース 

Theory:

スプラッシュスクリーンというのはタイトルバーとシステムメニューを持たず、縁の無いウィンドウで、 ビットマップをしばらく表示した後、自動的に消滅するウィンドウである。 プログラムの起動時によく使用され、ロゴを表示したり、 起動に時間がかかるような場合にユーザの気を紛らわせる(かもしれない・・)。 このチュートリアルではスプラッシュスクリーンの実装を行う。

まずはじめに、リソースファイルにビットマップを挿入する。 しかし、考えてみると、プログラムが終了するまでビットマップをロードするために 貴重なメモリを使用してしまうことになり非常にもったいない。 解決策は、ビットマップを保持するリソースDLLを作成することで、 そのDLLは、ただスプラッシュスクリーンを表示するだけが目的である。 この方法を用いれば、表示したいそのときだけDLLをロードして、 いらなくなったらアンロードすればいいのである。 なので、2つのモジュールが必要になる。 1つはメインプログラムで、もう1つがスプラッシュDLLで、そのDLLにビットマップを格納する。

一般的な方法は以下の通りである。

  1. ビットマップリソースとしてDLLにビットマップを格納する
  2. メインプログラムで、LoadLibrary関数をCALLしてDLLをロードする
  3. DLLのエントリポイント関数が呼ばれる。タイマを作成し、スプラッシュスクリーンがどのぐらいの間表示されるかを指定する。 次に、キャプションと境界の無いウィンドウを登録、作成し、クライアントエリアにビットマップを表示する。
  4. 指定した時間が過ぎると、スプラッシュスクリーンは画面から消え去り、制御がメインプログラムに戻る。
  5. メインプログラムに戻ってから、FreeLibrary関数をCALLし、メモリからDLLを解放する。そして、自分の処理を続ける。

では、詳細に説明しよう。

●DLLのロード/アンロード

LoadLibrary関数をCALLすることにより動的にDLLをロードすることができる。

LoadLibrary proto lpDLLName:DWORD

この関数はただ1つの引数である、ロードしたいDLLの文字列を取る。呼び出しが成功すればDLLのモジュールハンドルが帰り、失敗すればNULLが帰る。
DLLをアンロードするには、FreeLibrary関数をCALLする。

FreeLibrary proto hLib:DWORD

この関数も同じく1つの引数となっており、アンロードしたいDLLのモジュールハンドルを受け取る。 通常、LoadLibrary関数から取得したハンドルである。

●タイマの使い方

タイマを使用するにはまず、SetTimer関数をCALLする。

SetTimer proto hWnd:DWORD, TimerID:DWORD, uElapse:DWORD, lpTimerFunc:DWORD

関数が成功すれば、SetTimer関数はタイマのIDを返すが、失敗すればNULLを返す。

タイマを作成する方法は以下の2通りがある。

  1. もしタイマメッセージを送信したいウィンドウがある場合、SetTimer関数に4つ全ての引数を渡さなければならない。(lpTimerFuncはNULLでなければならない)
  2. ウィンドウが無かったり、タイマメッセージをウィンドウプロシージャで処理したくない場合は、ウィンドウハンドル hWnd にNULLを指定しなければならず、また、タイマメッセージを処理するタイマ関数へのポインタを指定しなければならない。

今回のサンプルでは最初の方法を使用している。

タイムアウト期間が過ぎたとき、WM_TIMERメッセージがウィンドウに送信される。 例えば、uElapseを1000としたら、WM_TIMERメッセージが指定したウィンドウに1秒ごとに送信される。

もうタイマが必要なくなれば、KillTimer関数をCALLする。

KillTimer proto hWnd:DWORD, TimerID:DWORD

Example:


main program

.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


Bitmap DLL

.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 ; When the dll is loaded
     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

Analysis:

メインプログラムの最初で、以下のコードを実行している。

invoke LoadLibrary,addr Libname
.if eax!=NULL
  invoke FreeLibrary,eax
.endif

これにより、"splash.dll"というDLLをロードしている。その後、FreeLibrary関数でアンロードしている。 LoadLibrary関数はDLLの初期化が終了するまで制御が帰らないことになっている。

メインプログラムについてはこれが全てだ。面白いところはDLL側のプログラムにある。

.if reason==DLL_PROCESS_ATTACH ; When the dll is loaded
   push hInst
   pop hInstance
   call ShowBitMap

DLLがロードされたら、WindowsはDLL_PROCESS_ATTACHフラグ付きでエントリポイント関数をCALLすることになっている。 この時に、スプラッシュスクリーンを表示する。 まず、後で使用することになるDLLのインスタンスハンドルを保存しておく。 そして、ShowBitMap関数をCALLしてメインの処理を行う。 ShowBitMap関数はウィンドウクラスを登録して、ウィンドウを作成しいつものようにメッセージループに入る。 CreateWindowEx関数に注目してみよう。

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

ここで注意するのはウィンドウスタイルで、WS_POPUPしか指定していない。 これにより、ウィンドウの境界とキャプションのないウィンドウになる。 同時にウィンドウサイズを250×250に指定する。

ウィンドウが作成されると、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

次に、LoadBitmap関数によりリソースからビットマップをロードし、タイマIDが1で、2秒ごとにメッセージを発行するタイマを作成する。 このタイマは2秒ごとに指定したウィンドウにWM_TIMERメッセージを送信する。

.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

ウィンドウがWM_PAINTメッセージを受け取ると、メモリデバコンを作成し、 そのデバコンにロードしたビットマップを選択する。 その後、GetObject関数によりビットマップのサイズを取得し、 StretchBlt関数によりビットマップをウィンドウに貼り付ける。 このStretchBlt関数はBitBlt関数によく似ているが、 指定した幅と高さに従って伸縮してくれる機能を有している。
今回は作成したウィンドウにぴったりフィットするようにしたいので、 BitBlt関数ではなくStretchBlt関数を使用した。 その後、デバコンを削除する。

.elseif uMsg==WM_LBUTTONDOWN
        invoke DestroyWindow,hWnd

ユーザにとってはスプラッシュスクリーンが消えるまで待たなければならないのは非常に不愉快かもしれないので、 ユーザがスプラッシュスクリーンをクリックすればすぐに消えるようにしておいた。 このため、DLLでWM_LBUTTONDOWNメッセージを処理する必要がでてくる。 このメッセージを受け取るとDestroyWindow関数でウィンドウを削除する。

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

ユーザが何もしなければ、スプラッシュスクリーンはタイマに指定した時間(この例では2秒)が過ぎれば消え去ることになっている。 これはWM_TIMERメッセージにより処理を行うのだが、WM_LBUTTONDOWNメッセージを発行してウィンドウを閉じる。 これにより、同じようなコードを書かないですむ。タイマはもう必要ないので、KillTimer関数により削除する。

ウィンドウが閉じられれば、DLLはメインプログラムに制御を戻すことになる。


[戻る]