第 26 章 處理對話盒中的按鍵


原理

在第十章與附錄三,曾介紹了如何以模式對話盒來撰寫程式。用這種方法,可以減少許多程式碼,非常的方便。一旦模式對話盒建立後,作業系統會建立對話盒管理器,對話盒管理器會處理許多事情,例如建立對話盒的各個控制項、處理訊息並呼叫對話盒函式 ( dialog box procedure )。這些都會自動完成,不必程式設計的人費心。一個對話盒堻q常含有數個控制項,在這些控制項中,只有一個控制項具有輸入焦點,使用者對鍵盤的輸入訊息,例如 WM_KEYDOWN、WM_KEYUP 等等,均會傳到這個控制項堙C但是,很不幸的,處理鍵盤訊息的程式碼都由對話盒管理器負責,並不會傳送給對話盒函式處理。因此在對話盒中,即使您增加了處理 WM_KEYDOWN 之類的程式碼,也一樣無法達到預期的目標。

小木偶打算撰寫最近很流行的小遊戲,2048,如下圖所示。我想以 16 個靜態控件 ( 靜態控制項 ) 模擬底下 4×4 的矩陣,再以 4 個靜態控件顯示右上方的分數靜態控件:「SCORE」、「BEST」、「65924」、「0」( 以黃色框線圍住 )。在智慧型手機可用手指滑動來移動數字;而在電腦上,小木偶想用鍵盤上的上、下、左、右鍵,分別使數字往這四個方向移動,並使得相鄰且相同的兩數字相加。除此之外,小木偶也想再添加一個新功能,就是玩家能夠反悔一步,當玩家按下 Ctrl-Z 時,能夠回到上一步的狀態。

不過問題來了,如一開始所說的,Windows 作業系統根本不會把鍵盤訊息傳給對話盒的對話盒函式,那麼又如何處理呢?小木偶查遍網路上的文獻,發現有兩種方法,第一種可能是較為正統的方法,利用攔截訊息 ( hook );第二種較為「土」,在本章堙A小木偶想介紹這種方法。此種方法是先「子類化」某個控制項,所謂「子類化」的意思,就是先取得某個控制項的視窗函式位址,存起來,再自行撰寫處理我們感興趣的訊息做為新的視窗函式,最後再呼叫原先的視窗函式。而小木偶的方法則是在自行撰寫的視窗函式堙A遇到像 WM_KEYDOWN、WM_KEYUP 這些鍵盤訊息時,利用 SendMessage 把這些訊息傳送給對話盒視窗函式。部份原始碼如下:

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
;---------------------------------------------------------------------------------------------------
;新的編輯框視窗函式
new_button_proc PROC    hWnd:HWND,uMsg:UINT,wParam:WPARAM,lParam:LPARAM
.IF uMsg==WM_KEYDOWN
        .IF (wParam>=VK_LEFT)&&(wParam<=VK_DOWN)
                INVOKE  SendMessage,hDialog,WM_KEYDOWN,wParam,lParam
        .ELSEIF wParam==VK_CONTROL
                mov     bCtrlDown,TRUE
        .ELSEIF wParam==VK_Z
                INVOKE  SendMessage,hDialog,WM_CHAR,wParam,lParam
        .ELSE
                INVOKE  CallWindowProc,lpOldButtonProc,hWnd,uMsg,wParam,lParam
                ret
        .ENDIF
.ELSEIF uMsg==WM_GETDLGCODE
                mov     eax,DLGC_WANTALLKEYS
                ret
.ELSE
                INVOKE  CallWindowProc,lpOldButtonProc,hWnd,uMsg,wParam,lParam
                ret
.ENDIF
                xor     eax,eax
                ret
new_button_proc ENDP
;---------------------------------------------------------------------------------------------------
;對話盒函式
DlgProc         PROC    hDlg:HWND,uMsg:UINT,wParam:WPARAM,lParam:LPARAM
.IF uMsg==WM_INITDIALOG
                mov     bCtrlDown,FALSE
            ;子類別化按鈕
                INVOKE  GetDlgItem,hDlg,IDB_NEW
                INVOKE  SetWindowLong,eax,GWL_WNDPROC,OFFSET new_button_proc
                mov     lpOldButtonProc,eax
 
.ELSEIF uMsg==WM_CHAR
   .IF bCtrlDown==TRUE
                mov     bCtrlDown,FALSE
      .IF (wParam=="Z")||(wParam=="z")
        ;此處為處理按下 Ctrl-Z 鍵的程式
      .ENDIF
   .ELSE
   .ENDIF

.ELSEIF uMsg==WM_KEYDOWN
   .IF wParam==VK_DOWN
        ;此處為處理按下「↓」鍵的程式
   .ELSEIF wParam==VK_UP
        ;此處為處理按下「↑」鍵的程式
   .ELSEIF wParam==VK_RIGHT
        ;此處為處理按下「→」鍵的程式
   .ELSEIF wParam==VK_LEFT
        ;此處為處理按下「←」鍵的程式
   .ENDIF

.ELSEIF uMsg==WM_CLOSE
                INVOKE  EndDialog,hDlg,NULL

.ELSE           ;其他未處理的訊息返回 FALSE
                mov     eax,FALSE
                ret

.ENDIF          ;已處理的訊息,返回 TRUE
                mov     eax,TRUE   
                ret
DlgProc         ENDP
;---------------------------------------------------------------------------------------------------

上面的程式片段的第 32 行,小木偶以 GWL_WNDPROC 為參數呼叫 SetWindowLong,把「新遊戲」按鈕的視窗函式重新設為 new_button_proc 副程式。SetWindowLong 在成功設定新的視窗函式位址時,會傳回「新遊戲」按鈕原先的視窗函式位址,接著把它存於 lpOldButtonProc 變數 ( 見第 33 行 )。在「新遊戲」按鈕中,新的視窗函式會處理 WM_KEYDOWN 訊息。它遇到 WM_KEYDOWN 訊息時,檢查使用者是否按下底下的三種按鍵:

  1. 如果按下方向鍵:即上、下、左、右鍵 ( 分別是 26h、28h、25h、27h ),如果是則把 WM_KEYDOWN 訊息原封不動傳給對話盒函式 ( 第 6 行 )。
  2. 如果按下 Ctrl 鍵,則設定 bCtrlDown 變數為 TRUE ( 第 8 行 )。
  3. 如果按下英文字母的「Z」鍵,則把 WM_CHAR 訊息傳給對話盒的視窗函式 ( 第 10 行 )。

由上面流程來看,要處理組合按鍵,例如 Ctrl-Z…,的方法是先設一個變數當作旗標,當 Ctrl 被壓下時,把此變數設為 TRUE,然後在視窗函式處理 WM_CHAR 時,在此變數為 TRUE 時,檢查「Z」鍵是否被按下即可 ( 見程式第 36∼41 行 )。

當然,這種以子類化按鈕欺騙作業系統,而把鍵盤訊息傳給對話盒函式的做法,是有缺點的。當「新遊戲」按鈕失去輸入焦點時,玩家按下方向鍵就無作用了,幸好在這個遊戲中,此缺點並不太大,甚至玩家可能都很難使「新遊戲」按鈕失去輸入焦點而不退出遊戲。不過如果對話盒含有許多按鈕、編輯框的話,此方法就難以使用了。


原始碼

底下是 2048.RC 的原始碼:

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
#include "c:\masm32\include\resource.h"
 
#define IDS_00          0x08000
#define IDS_MARK        0x08100
#define IDS_BEST        0x08101
#define IDS_MARKTEXT    0x08102
#define IDS_BESTTEXT    0x08103
#define IDB_NEW         0x08104
#define IDB_EXIT        0x08105
#define x               4
#define y               22
#define RT_MANIFEST     24
 
Game2048        DIALOG  200, 100, 180, 200
STYLE   DS_SETFONT|WS_POPUP|WS_CAPTION|WS_VISIBLE|WS_SYSMENU
CAPTION "2048"
FONT    16,"Times New Roman"
BEGIN
 CTEXT  "SCORE",IDS_MARKTEXT,  78,    4, 45,  7
 CTEXT  "",         IDS_MARK,  78,   11, 45,  7
 CTEXT  "BEST", IDS_BESTTEXT, 129,    4, 45,  7
 CTEXT  "",         IDS_BEST, 129,   11, 45,  7
 CTEXT  "",IDS_00,         x,    y, 38, 35,SS_OWNERDRAW
 CTEXT  "",IDS_00+0x1,  x+44,    y, 38, 35,SS_OWNERDRAW
 CTEXT  "",IDS_00+0x2,  x+88,    y, 38, 35,SS_OWNERDRAW
 CTEXT  "",IDS_00+0x3, x+132,    y, 38, 35,SS_OWNERDRAW
 CTEXT  "",IDS_00+0x4,     x, y+40, 38, 35,SS_OWNERDRAW
 CTEXT  "",IDS_00+0x5,  x+44, y+40, 38, 35,SS_OWNERDRAW
 CTEXT  "",IDS_00+0x6,  x+88, y+40, 38, 35,SS_OWNERDRAW
 CTEXT  "",IDS_00+0x7, x+132, y+40, 38, 35,SS_OWNERDRAW
 CTEXT  "",IDS_00+0x8,     x, y+80, 38, 35,SS_OWNERDRAW
 CTEXT  "",IDS_00+0x9,  x+44, y+80, 38, 35,SS_OWNERDRAW
 CTEXT  "",IDS_00+0xA,  x+88, y+80, 38, 35,SS_OWNERDRAW
 CTEXT  "",IDS_00+0xB, x+132, y+80, 38, 35,SS_OWNERDRAW
 CTEXT  "",IDS_00+0xC,     x,y+120, 38, 35,SS_OWNERDRAW
 CTEXT  "",IDS_00+0xD,  x+44,y+120, 38, 35,SS_OWNERDRAW
 CTEXT  "",IDS_00+0xE,  x+88,y+120, 38, 35,SS_OWNERDRAW
 CTEXT  "",IDS_00+0xF, x+132,y+120, 38, 35,SS_OWNERDRAW
 PUSHBUTTON  "新遊戲",IDB_NEW , 26,180, 50, 14
 PUSHBUTTON  "離開"  ,IDB_EXIT,104,180, 50, 14
END
 
1   RT_MANIFEST MOVEABLE PURE "2048.EXE.MANIFEST"
 
Game2048        ICON    2048.ico

這個資源描述檔埵 16 個靜態控件用來顯示數值,其識別碼範圍是 8000h∼800Fh,但是在資源描述檔中,在數值前加上「0x」為字首表示此數為十六進位表示法。另外也可以用像第 23∼38 行的方式表示識別碼,連控制元件的位置也可以用常數作運算來描述。小木偶把控制元件的位置以 x、y 常數表示,其原因是可以很容易修改其位置,但是每個控制元件上下左右的距離卻不變。

底下是 2048.EXE.MANIFEST 的原始碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version='1.0' encoding='UTF-8' standalone='yes'?>
<assembly xmlns='urn:schemas-microsoft-com:asm.v1' manifestVersion='1.0'>
<dependency>
   <dependentAssembly>
      <assemblyIdentity type='win32'
                        name='Microsoft.Windows.Common-Controls'
                        version='6.0.0.0'
                        processorArchitecture='*'
                        publicKeyToken='6595b64144ccf1df'
                        language='*'
      />
   </dependentAssembly>
</dependency>
</assembly>

底下是 2048.ASM 的原始碼:

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
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
                OPTION  CASEMAP:NONE
                .586
                .MODEL  FLAT,STDCALL
 
INCLUDE         WINDOWS.INC
INCLUDE         COMCTL32.INC
INCLUDE         GDI32.INC
INCLUDE         KERNEL32.INC
INCLUDE         USER32.INC
INCLUDELIB      COMCTL32.LIB
INCLUDELIB      GDI32.LIB
INCLUDELIB      KERNEL32.LIB
INCLUDELIB      USER32.LIB
 
IDS_00          EQU     8000h
IDS_03          EQU     8003h
IDS_07          EQU     8007h
IDS_0B          EQU     800Bh
IDS_0C          EQU     800Ch
IDS_0F          EQU     800Fh
IDS_MARK        EQU     8100h
IDS_BEST        EQU     8101h
IDS_MARKTEXT    EQU     8102h
IDS_BESTTEXT    EQU     8103h
IDB_NEW         EQU     8104h
IDB_EXIT        EQU     8105h
 
cr0Text         EQU     0b4c0cdh
cr2Text         EQU     0656e77h
cr8Text         EQU     0f2f6f9h
crScoreText     EQU     0dae4eeh
 
DIMENSION       EQU     4       ;4×4靜態控件矩陣
TOPTODOWN       EQU     0       ;當使用者按下VK_DOWN鍵,由上而下
DOWNTOTOP       EQU     1       ;當使用者按下VK_UP鍵,由下而上
RIGHTTOLEFT     EQU     2       ;當使用者按下VK_LEFT鍵,由右而左
LEFTTORIGHT     EQU     3       ;當使用者按下VK_RIGHT鍵,由左而右
;***************************************************************************************************
.CONST
;crBk是靜態控件的背景顏色,背景顏色隨靜態控件的標題數值不同而不同,例如數值0時,顏色為0b4c0cdh
                        ;  0        2        4        8       16       32       64      128
crBk            DWORD   0b4c0cdh,0dae4eeh,0c8e0edh,079b1f2h,06395f5h,05f7cf6h,03b5ef6h,072cfedh
                        ;256      512      1024     2048     4096     8192     16384    32768    65536
                DWORD   061ccedh,050c8edh,03fc5edh,02ec2edh,0323a3ch,0323a3ch,0323a3ch,0323a3ch,0323a3ch
szDlgName       DB      "Game2048",0    ;對話盒面板名稱
szZero          DB      "0",0
szTwo           DB      "2",0
szFour          DB      "4",0
szFontFace      DB      "Verdana",0     ;顯示在4×4靜態控件矩陣上的字形
szFilename      DB      "BEST2048.TXT",0;最佳分數檔案
szNumFmt        DB      "%d",0
szFailure       DB      "遊戲結束!",0
szFailureTitle  DB      "通知",0
;***************************************************************************************************
.DATA
hInstance       HANDLE  ?               ;執行實例代碼
hDialog         HANDLE  ?               ;對話盒代碼
lpOldButtonProc DWORD   ?               ;「新遊戲」原有的視窗函式位址
;靜態控件的邏輯字形,標題小於等於64者用hFont[0],大於64且小於等於8192者用hFont[4],其他用hFont[8]
hFont           HFONT   3 DUP (?)
hbrBkStatic     HBRUSH  17 DUP (?)      ;靜態控件的背景畫刷
hbrBkDlg        HBRUSH  ?               ;對話盒的背景畫刷
hbrBkScore      HBRUSH  ?               ;表示分數(IDS_MARK、IDS_BEST、IDS_MARKTEXT、IDS_BESTTEXT)的背景畫刷
dwScore         DWORD   0               ;當下的分數
dwOldScore      DWORD   ?               ;上次按下方向鍵後的分數,用於玩家按下Ctrl-Z時
dwBest          DWORD   ?               ;最高分記錄
number          DWORD   16 DUP (?)
bCtrlDown       DWORD   ?               ;Ctrl鍵是否按下的旗標,TRUE表示正被按下;FALSE表示沒被按下
hFile           HFILE   ?               ;BEST2048.TXT的檔案代碼
BytesRead       DWORD   ?
BytesWritten    DWORD   ?
dwN             DWORD   DIMENSION DUP (?)
szBuffer        DB      10h DUP (?)
;***************************************************************************************************
.CODE
;---------------------------------------------------------------------------------------------------
;取得識別碼為ECX靜態控件的數值與參數x比較,如果相等,EAX設為1;如果不等,EAX設為0
chk_reduce      PROC    x:DWORD
                INVOKE  GetDlgItemInt,hDialog,ecx,0,0
            .IF eax==x
                mov     eax,1
            .ELSE
                sub     eax,eax
            .ENDIF
                ret
chk_reduce      ENDP
;---------------------------------------------------------------------------------------------------
;檢查所有相鄰的靜態控件是否相等,若有相等則返回TRUE,否則返回FALSE
check_gameover  PROC
                LOCAL   IdNum,value,nReduce:DWORD
                mov     nReduce,0       ;nReduce紀錄有相等的靜控控件個數,可供縮減
                mov     IdNum,IDS_00
    .WHILE IdNum<IDS_0F
                INVOKE  GetDlgItemInt,hDialog,IdNum,0,0
                mov     value,eax
        .IF IdNum<=IDS_0B
                mov     ecx,IdNum
                add     ecx,4
                INVOKE  chk_reduce,value
                add     nReduce,eax
            .IF (IdNum==IDS_03)||(IdNum==IDS_07)||(IdNum==IDS_0B)
            .ELSE
                mov     ecx,IdNum
                inc     ecx
                INVOKE  chk_reduce,value
                add     nReduce,eax
            .ENDIF
        .ELSE
                mov     ecx,IdNum
                inc     ecx
                INVOKE  chk_reduce,value
                add     nReduce,eax
        .ENDIF
                inc     IdNum
    .ENDW
                xor     eax,eax
                cmp     nReduce,eax
                jz      exit
                inc     eax
exit:           ret
check_gameover  ENDP
;---------------------------------------------------------------------------------------------------
;arrange_a_row只處理一行或一列,它把dwN陣列往低位址方向縮減,縮減方式是如有低位址有空格(該數值為0),
;則在較高位址的雙字組數值移到低位址有空格處,接下來檢查相鄰的數值,如果相同則相加。
;返回時,如有縮減或相加,返回NC;如沒有縮減也沒相加,返回CY
arrange_a_row   PROC    USES esi edi
                LOCAL   n,m,TimesOfReduce:DWORD
                xor     eax,eax
                mov     TimesOfReduce,eax
 ;假如有空格的話,把高位址的數值填到低位址的空格中
                mov     m,eax
                mov     edx,DIMENSION-1
 .WHILE m<DIMENSION-2
                mov     n,eax
    .WHILE n<edx
                mov     esi,n
                shl     esi,2
                add     esi,OFFSET dwN
        .IF DWORD PTR [esi]==eax
             ;若低位址的數值為0,檢查其他高位址是否也為0
                mov     ecx,3
                mov     edi,esi
                sub     ecx,n
                add     edi,4
                push    ecx
                repe    scasd
                pop     ecx
               ;若全為0,跳至next_turn;若有任何一個不為0,則把其他高位址的數值往低位址移4個位元組
                jz      next_turn
                mov     edi,esi
                add     esi,4
                rep     movsd
                inc     TimesOfReduce
                mov     [edi],eax
        .ENDIF
next_turn:      inc     n
    .ENDW
                inc     m
                sub     edx,m
 .ENDW
 ;把相鄰位址且數值相同的相加,並向低位址處移動
                mov     m,eax
 .WHILE m<DIMENSION-1
                mov     esi,m
                shl     esi,2
                add     esi,OFFSET dwN
                mov     edx,[esi]
                cmp     edx,eax
                jz      finish
        .IF edx==DWORD PTR [esi+4]
                add     edx,[esi+4]
                inc     TimesOfReduce
                mov     [esi],edx
                add     dwScore,edx
                mov     ecx,DIMENSION-2
                sub     ecx,m
                add     esi,4
                mov     edi,esi
                add     esi,4
                rep     movsd
                mov     [edi],eax
        .ENDIF
                inc     m
 .ENDW
finish: .IF eax==TimesOfReduce
                stc
        .ELSE
                clc
        .ENDIF
                ret
arrange_a_row   ENDP
;---------------------------------------------------------------------------------------------------
;整理整個4×4矩陣,整理方法是呼叫arrange_a_row副程式四次,參數direct是表示玩家所按的方向按鍵
;返回時,如果有任何一行曾縮減或相加,返回NC;否則返回CY
arrange_matrix  PROC    USES edi direct:DWORD
                LOCAL   n,row,bArrange:DWORD
                LOCAL   ID:DWORD
                ;difference=每一行內靜態控件識別碼的差,idBetweenRow=每一行開始的靜態控件識別碼的差
                LOCAL   idBetweenRow,difference,StartID:DWORD
                mov     bArrange,0
    .IF direct==TOPTODOWN
        ;如果是由上而下,低位址由IDS_0C開始,ID每次減4;每換一行,StartID增加一(此處的一記錄在idBetweenRow)
                mov     StartID,IDS_0C
                mov     difference,-4
                mov     idBetweenRow,1
    .ELSEIF direct==DOWNTOTOP
        ;如果是由下而上,低位址由IDS_00開始,ID每次加4;每換一行,StartID增加一(此處的一記錄在idBetweenRow)
                mov     StartID,IDS_00
                mov     difference,4
                mov     idBetweenRow,1
    .ELSEIF direct==RIGHTTOLEFT
        ;如果是由右而左,低位址識別碼由IDS_03開始,ID每次減一;每換一列,StartID增加四(此處的四記錄在idBetweenRow)
                mov     StartID,IDS_00
                mov     difference,1
                mov     idBetweenRow,4
    .ELSE
        ;如果是由左而右,低位址由IDS_03開始,ID每次加一;每換一列,StartID增加四(此處的四記錄在idBetweenRow)
                mov     StartID,IDS_03
                mov     difference,-1
                mov     idBetweenRow,4
    .ENDIF
  ;共四行,記錄在row變數堙A由0開始到3
                mov     row,0
    .WHILE row<4
        ;取得一行內的四個靜態控件數值,存於dwN陣列中
                mov     eax,StartID
                mov     n,0
                mov     ID,eax
        .WHILE n<4
                INVOKE  GetDlgItemInt,hDialog,ID,0,FALSE
                mov     edi,n
                shl     edi,2
                mov     dwN[edi],eax
                mov     ecx,difference
                inc     n
                add     ID,ecx
        .ENDW
        ;呼叫arrange_a_row副程式,整理縮減一行
                call    arrange_a_row
            .IF !CARRY?
                inc     bArrange
            .ENDIF
        ;把整理後的數值變成字串,然後把靜態控件標題設為此字串
                mov     eax,StartID
                mov     n,0
                mov     ID,eax
        .WHILE n<4
                mov     edi,n
                shl     edi,2
                INVOKE  wsprintf,OFFSET szBuffer,OFFSET szNumFmt,dwN[edi]
                INVOKE  SetDlgItemText,hDialog,ID,OFFSET szBuffer
                mov     ecx,difference
                add     ID,ecx
                inc     n
        .ENDW
                inc     row
                mov     eax,idBetweenRow
                add     StartID,eax
    .ENDW
    .IF bArrange>0
                clc
    .ELSE
                stc
    .ENDIF
                ret
arrange_matrix  ENDP
;---------------------------------------------------------------------------------------------------
;建立畫刷,此畫刷用於塗滿靜態控件的背景顏色,背景顏色依照靜態控件標題的數值不同而不同。所建立的畫刷
;存於hbrBkStatic陣列
create_bk_brush PROC
                LOCAL   n:DWORD
                mov     n,0
        .WHILE n<=16
                mov     edx,n
                INVOKE  CreateSolidBrush,crBk[edx*4]
                mov     edx,n
                mov     hbrBkStatic[edx*4],eax
                inc     n
        .ENDW
                ret
create_bk_brush ENDP
;---------------------------------------------------------------------------------------------------
delete_bk_brush PROC
                LOCAL   n
                mov     n,0
        .WHILE n<=17
                mov     edx,n
                INVOKE  DeleteObject,hbrBkStatic[edx*4]
                inc     n
        .ENDW
                INVOKE  DeleteObject,hbrBkDlg
                INVOKE  DeleteObject,hbrBkScore
                ret
delete_bk_brush ENDP
;---------------------------------------------------------------------------------------------------
;返回時,EAX為0∼(iRange-1)的亂數
tiny_random     PROC    USES edx iRange
                LOCAL   A,B:DWORD
                rdtsc
                mov     B,0
                mov     A,100711433
                adc     eax,edx
                adc     eax,B
                mul     A
                adc     eax,edx
                mov     B,eax
                mul     iRange
                mov     eax,edx
                ret
tiny_random     ENDP
;---------------------------------------------------------------------------------------------------
;在空白的靜態控件中,以亂數選定其中一個靜態控件,並將其標題為「2」或「4」
;返回值:NC-成功的設定好靜態控件
;    CY-失敗。若EAX=1,表示沒有空白的靜態控件;若EAX=0,表示僅有一個空白的靜態控件,但設定後無法縮減
set_number_in_random    PROC
                LOCAL   idBlankStatic[10h]:DWORD
                LOCAL   nBlankStatic:DWORD
                LOCAL   id,n:DWORD
                LOCAL   pidBlankStatic:LPSTR    ;idBlankStatic陣列的指標
                mov     n,0                     ;n為標題為「0」的靜態控件個數,此處先設為0
                mov     id,IDS_00
                lea     edx,idBlankStatic
                mov     pidBlankStatic,edx
    ;尋找標題為「0」的靜態控件識別碼,存入idBlankStatic陣列中
    .WHILE id<=IDS_0F
                INVOKE  GetDlgItemInt,hDialog,id,NULL,FALSE
        .IF eax==0
                mov     eax,pidBlankStatic
                mov     edx,id
                mov     [eax],edx
                add     pidBlankStatic,4
                inc     n
        .ENDIF
                inc     id
    .ENDW
                cmp     n,0
                je      no_blank
    ;在idBlankStatic陣列中,以亂數取出一個靜態控件的識別碼
                INVOKE  tiny_random,n
                shl     eax,2
                lea     edx,idBlankStatic
                add     edx,eax
                mov     ecx,[edx]       ;ECX=取出的靜態控件識別碼
                INVOKE  tiny_random,10h
        .IF eax<0ch
                mov     edx,OFFSET szTwo
        .ELSE
                mov     edx,OFFSET szFour
        .ENDIF
    ;設定亂數選出的靜態控制項的標題為EDX所指字串
                INVOKE  SetDlgItemText,hDialog,ecx,edx
    ;如果n=1,表示已沒有空格,檢查每一個靜態控件上下左右是否能縮減
        .IF n==1
                call    check_gameover
                or      eax,eax
                jz      failure
        .ENDIF
                clc
                jmp     exit
no_blank:       mov     eax,1
failure:        stc
exit:           ret
set_number_in_random    ENDP
;---------------------------------------------------------------------------------------------------
initial_game    PROC
                LOCAL   idCtrl:DWORD
                mov     idCtrl,IDS_00
                INVOKE  GetDlgItemInt,hDialog,IDS_BEST,0,0      ;取得目前最高記錄,存於dwBest
        .IF eax>dwBest
                mov     dwBest,eax
        .ENDIF
        .WHILE idCtrl<=IDS_0F
                INVOKE  SetDlgItemText,hDialog,idCtrl,OFFSET szZero
                inc     idCtrl
        .ENDW
                mov     dwScore,0
                call    set_number_in_random
                call    set_number_in_random
                ret
initial_game    ENDP
;---------------------------------------------------------------------------------------------------
new_button_proc PROC    hWnd:HWND,uMsg:UINT,wParam:WPARAM,lParam:LPARAM
.IF uMsg==WM_KEYDOWN
        .IF (wParam>=VK_LEFT)&&(wParam<=VK_DOWN)
                INVOKE  SendMessage,hDialog,WM_KEYDOWN,wParam,lParam
        .ELSEIF wParam==VK_CONTROL
                mov     bCtrlDown,TRUE
        .ELSEIF wParam==VK_Z
                INVOKE  SendMessage,hDialog,WM_CHAR,wParam,lParam
        .ELSE
                INVOKE  CallWindowProc,lpOldButtonProc,hWnd,uMsg,wParam,lParam
                ret
        .ENDIF
.ELSEIF uMsg==WM_GETDLGCODE
                mov     eax,DLGC_WANTALLKEYS
                ret
.ELSE
                INVOKE  CallWindowProc,lpOldButtonProc,hWnd,uMsg,wParam,lParam
                ret
.ENDIF
                xor     eax,eax
                ret
new_button_proc ENDP
;---------------------------------------------------------------------------------------------------
;儲存當前的狀態,當使用者按下Ctrl-Z時,須返回當前狀態
save_number     PROC
                LOCAL   n:DWORD
                mov     n,0
        .WHILE n<10h
                mov     ecx,n
                add     ecx,IDS_00
                INVOKE  GetDlgItemInt,hDialog,ecx,0,FALSE
                mov     edx,n
                mov     number[edx*4],eax
                inc     n
        .ENDW
                mov     edx,dwScore     ;儲存當下分數
                mov     dwOldScore,edx
                ret
save_number     ENDP
;---------------------------------------------------------------------------------------------------
;把number陣列中的數值變成字串,然後把各個徑控控件的標題設為相對應的字串
restore_number  PROC
                LOCAL   n:DWORD
                mov     n,0
        .WHILE n<10h
                mov     edx,n
                INVOKE  wsprintf,OFFSET szBuffer,OFFSET szNumFmt,number[edx*4]
                mov     edx,n
                add     edx,IDS_00
                INVOKE  SetDlgItemText,hDialog,edx,OFFSET szBuffer
                inc     n
        .ENDW
                mov     edx,dwOldScore
                mov     dwScore,edx
                ret
restore_number  ENDP
;---------------------------------------------------------------------------------------------------
;由數值換算成位址,用於計算某個數值是在hbrBkStatic陣列中的哪個偏移位址
;  數值(十進位)   0  2  4  8  16  32  64  128  256  512  1024  2048  4096
;偏移位址(十六進位) 0  4  8  C  10  14  18   1C   20   24    28    2C    30
;輸入:EAX-數值
;輸出:EDX-偏移位址
get_index       PROC
                sub     edx,edx
                cmp     eax,edx
                je      finish1
@@:             shr     eax,1
                jc      finish0
                inc     edx
                jmp     @b
finish0:        shl     edx,2
finish1:        ret
get_index       ENDP
;---------------------------------------------------------------------------------------------------
;對話盒的視窗函式
DlgProc         PROC    hDlg:HWND,uMsg:UINT,wParam:WPARAM,lParam:LPARAM
                LOCAL   BkColor:DWORD
                LOCAL   rect:RECT
                LOCAL   hfont:HFONT
                LOCAL   szText[10h]:BYTE
.IF uMsg==WM_INITDIALOG
                INVOKE  CreateFile,OFFSET szFilename,GENERIC_READ or GENERIC_WRITE,0,0,OPEN_ALWAYS,\
                        FILE_ATTRIBUTE_NORMAL,0
                mov     hFile,eax
        ;如果讀取到4個位元組表示BEST2048.TXT已存在;否則表示沒有BEST2048.TXT檔案
                INVOKE  ReadFile,hFile,OFFSET dwBest,4,OFFSET BytesRead,0
        .IF DWORD PTR [BytesRead]==4
                mov     eax,dwBest
        .ELSE
                sub     eax,eax
        .ENDIF
            ;把最高記錄顯示在IDS_BEST靜態控件上
                INVOKE  wsprintf,OFFSET szBuffer,OFFSET szNumFmt,eax
                INVOKE  SetDlgItemText,hDlg,IDS_BEST,OFFSET szBuffer
                mov     ecx,hDlg
                mov     hDialog,ecx
            ;子類別化按鈕
                INVOKE  GetDlgItem,hDlg,IDB_NEW
                INVOKE  SetWindowLong,eax,GWL_WNDPROC,OFFSET new_button_proc
                mov     lpOldButtonProc,eax
            ;設定圖示
                INVOKE  LoadIcon,hInstance,OFFSET szDlgName
                INVOKE  SendMessage,hDlg,WM_SETICON,ICON_SMALL,eax
            ;建立靜態控件的背景畫刷
                call    create_bk_brush
            ;建立對話盒的背景畫刷
                INVOKE  CreateSolidBrush,0a0adbbh
                mov     hbrBkDlg,eax
            ;建立分數(IDS_MARK、IDS_BEST、IDS_MARKTEXT、IDS_BESTTEXT)背景畫刷
                INVOKE  CreateSolidBrush,0909caah
                mov     hbrBkScore,eax
            ;建立邏輯文字,靜態控件標題小於等於64者用hFont[0]、大於64且小於等於8192者用hFont[4]
                INVOKE  CreateFont,48,0,0,0,800,0,0,0,0,0,0,0,0,OFFSET szFontFace
                mov     hFont[0],eax
                INVOKE  CreateFont,40,0,0,0,800,0,0,0,0,0,0,0,0,OFFSET szFontFace
                mov     hFont[4],eax
                INVOKE  CreateFont,32,0,0,0,800,0,0,0,0,0,0,0,0,OFFSET szFontFace
                mov     hFont[8],eax
                call    initial_game
                jmp     complete
 
.ELSEIF uMsg==WM_CTLCOLORDLG
                mov     eax,hbrBkDlg
                ret
 
.ELSEIF uMsg==WM_CTLCOLORSTATIC
                INVOKE  GetDlgCtrlID,lParam
                mov     ecx,eax
    .IF (ecx==IDS_MARK)||(ecx==IDS_BEST)
                mov     ecx,0ffffffh
                mov     BkColor,0909caah
                push    hbrBkScore
    .ELSEIF (ecx==IDS_MARKTEXT)||(ecx==IDS_BESTTEXT)
                mov     ecx,crScoreText
                mov     BkColor,0909caah
                push    hbrBkScore
    .ELSE
                INVOKE  GetDlgItemInt,hDlg,ecx,0,FALSE
        .IF eax==0
                mov     ecx,cr0Text
                sub     edx,edx
        .ELSEIF eax==2
                mov     ecx,cr2Text
                mov     edx,4
        .ELSEIF eax==4
                mov     ecx,cr2Text
                mov     edx,8
        .ELSE
                call    get_index
                mov     ecx,cr8Text
        .ENDIF
                push    hbrBkStatic[edx]
                mov     eax,crBk[edx]
                mov     BkColor,eax
    .ENDIF
                INVOKE  SetTextColor,wParam,ecx
                INVOKE  SetBkColor,wParam,BkColor
                pop     eax
                ret
 
.ELSEIF uMsg==WM_DRAWITEM
                ASSUME  ebx:PTR DRAWITEMSTRUCT
                push    ebx
                mov     ebx,lParam
                INVOKE  GetClientRect,[ebx].hwndItem,ADDR rect
                INVOKE  GetDlgItemText,hDlg,[ebx].CtlID,ADDR szText,8
                INVOKE  GetDlgItemInt,hDlg,[ebx].CtlID,0,FALSE
        .IF eax<=64
                mov     ecx,hFont[0]
        .ELSEIF (eax>64)&&(eax<=8192)
                mov     ecx,hFont[4]
        .ELSE
                mov     ecx,hFont[8]
        .ENDIF
                mov     hfont,ecx
                call    get_index
                INVOKE  FillRect,[ebx].hdc,ADDR rect,hbrBkStatic[edx]
                INVOKE  SelectObject,[ebx].hdc,hfont
                INVOKE  DrawText,[ebx].hdc,ADDR szText,-1,ADDR rect,DT_CENTER or DT_SINGLELINE or DT_VCENTER
                pop     ebx
                ASSUME  ebx:NOTHING
 
.ELSEIF uMsg==WM_COMMAND
                mov     eax,wParam
                and     eax,0ffffh      ;EAX=識別碼
        .IF eax==IDB_EXIT
                jmp     exit
        .ELSEIF eax==IDB_NEW
restart:        call    initial_game
                jmp     complete
        .ENDIF
 
.ELSEIF uMsg==WM_KEYDOWN
                call    save_number
   .IF wParam==VK_DOWN
                mov     ecx,TOPTODOWN
   .ELSEIF wParam==VK_UP
                mov     ecx,DOWNTOTOP
   .ELSEIF wParam==VK_RIGHT
                mov     ecx,LEFTTORIGHT
   .ELSEIF wParam==VK_LEFT
                mov     ecx,RIGHTTOLEFT
   .ENDIF
                INVOKE  arrange_matrix,ecx
                jc      complete
                call    set_number_in_random
                jnc     complete
            ;遊戲結束,先比較是否超越最高記錄,如果超越最佳記錄,把最高分數寫入檔案
                mov     eax,dwScore
            .IF eax>dwBest
                mov     dwBest,eax
                INVOKE  wsprintf,OFFSET szBuffer,OFFSET szNumFmt,eax
                INVOKE  SetDlgItemText,hDlg,IDS_MARK,OFFSET szBuffer
                INVOKE  SetFilePointer,hFile,0,0,FILE_BEGIN
                INVOKE  WriteFile,hFile,OFFSET dwScore,4,OFFSET BytesWritten,0
            .ENDIF
            ;顯示彈出對話盒,詢問是否要繼續玩?
                INVOKE  MessageBox,hDlg,OFFSET szFailure,OFFSET szFailureTitle,MB_OKCANCEL or MB_ICONSTOP
            .IF eax==IDOK
                jmp     restart
            .ELSE
                jmp     exit
            .ENDIF
complete:       INVOKE  wsprintf,OFFSET szBuffer,OFFSET szNumFmt,dwScore
                INVOKE  SetDlgItemText,hDlg,IDS_MARK,OFFSET szBuffer
                INVOKE  GetDlgItemInt,hDlg,IDS_BEST,0,0
            .IF eax<dwScore
                INVOKE  SetDlgItemText,hDlg,IDS_BEST,OFFSET szBuffer
            .ENDIF
 
.ELSEIF uMsg==WM_CHAR
   .IF bCtrlDown==TRUE
                mov     bCtrlDown,FALSE
      .IF (wParam=="Z")||(wParam=="z")
                call    restore_number
                jmp     complete
      .ENDIF
   .ENDIF
 
.ELSEIF uMsg==WM_CLOSE
exit:           call    delete_bk_brush
            ;檢查是否超越最高記錄
                mov     eax,dwScore
            .IF eax>dwBest
                mov     dwBest,eax
                INVOKE  SetFilePointer,hFile,0,0,FILE_BEGIN
                INVOKE  WriteFile,hFile,OFFSET dwScore,4,OFFSET BytesWritten,0
            .ENDIF
                INVOKE  CloseHandle,hFile
                INVOKE  EndDialog,hDlg,NULL
 
.ELSE           ;其他未處理的訊息返回 FALSE
                mov     eax,FALSE
                ret
 
.ENDIF          ;已處理的訊息,返回 TRUE
                mov     eax,TRUE   
                ret
DlgProc         ENDP
;---------------------------------------------------------------------------------------------------
start:          call    InitCommonControls
                INVOKE  GetModuleHandle,NULL
                mov     hInstance,eax
                INVOKE  DialogBoxParam,hInstance,OFFSET szDlgName,NULL,OFFSET DlgProc,NULL
                INVOKE  ExitProcess,eax
;***************************************************************************************************
END             start

下載 2048.ico 後,把它和 2048.EXE.MANIFEST、2048.RC、2048.ASM 放在同一子目錄下,一下面步驟組譯及連結,就可以得到 2048.exe 可執行檔,從此即使不連上網際網路,也能單機使用。

E:\HomePage\SOURCE\Win32\2048>rc 2048.rc [ENTER]

E:\HomePage\SOURCE\Win32\2048>ml 2048.asm /link 2048.res [ENTER]
Microsoft (R) Macro Assembler Version 6.14.8444
Copyright (C) Microsoft Corp 1981-1997.  All rights reserved.

 Assembling: 2048.asm

***********
ASCII build
***********

Microsoft (R) Incremental Linker Version 5.12.8078
Copyright (C) Microsoft Corp 1992-1998. All rights reserved.

/SUBSYSTEM:WINDOWS
"2048.obj"
"/OUT:2048.exe"
"2048.res"

E:\HomePage\SOURCE\Win32\2048>

解說

其實,這一章的主題已經在上面說完了,只是這個範例,2048,的其他部份卻是複雜,所以底下的說明,並沒有提到對話盒的訊息傳遞部份。

對話盒及靜態控件的外觀

對話盒的背景顏色

在作業系統要畫出對話盒之前,作業系統會先發出 WM_CTLCOLORDLG 給對話盒函式,程式可以處理 WM_CTLCOLORDLG 訊息以改變對話盒的背景顏色,只需在處理完 WM_CTLCOLORDLG 時,傳回畫刷代碼即可,傳回值必須放在 EAX 暫存器堙A作業系統會以此畫刷填滿對話盒背景。參見程式第 503∼505 行。

靜態控件的背景顏色、文字背景顏色與文字顏色

這一小節的標題是「靜態控件的背景顏色、文字背景顏色與文字顏色」,要知道,這三種區域是不同的地方,可以參考上圖。上圖中,左上角第二個靜態控件,以藍色框線圍住的是識別碼為 IDS_01 的靜態控件,在藍色框線內與紫紅色框線外,塗上的顏色是靜態控件的背景顏色。文字「2」外圍被紫紅色框線圍住的區域稱為剪裁矩形,在剪裁矩形內,而且扣除文字外的區域才稱為「文字的背景」,文字的顏色與文字的背景顏色不同時,人眼才能分辨。

要設定文字的顏色與文字背景顏色,可以在處理 WM_CTLCOLORSTATIC 時,分別呼叫 SetBkColor 與 SetTextColor。但是,如果要設定靜態控件的背景顏色,可分為兩種情形。第一種是,如果靜態控件不具有「SS_OWNERDRAW」風格,那麼,同對話盒一樣,在處理 WM_CTLCOLORSTATIC 訊息結束時,傳回畫刷代碼,作業系統就會以此畫刷把靜態控件的背景塗滿。第二種,如果是靜態控件具有「SS_OWNERDRAW」風格,那麼程式得自行繪出背景顏色,請參考底下靜態控件的字形。假如靜態控件具有「SS_OWNERDRAW」風格,而程式在處理完 WM_CTLCOLORSTATIC,傳回畫刷,是沒有效的;不過對於用 SetBkColor 與 SetTextColor 設定文字背景色和文字顏色,卻不管靜態控件是否具有「SS_OWNERDRAW」風格,都是有效的。

在 2048 對話盒埵@有 20 個靜態控件,有四個是與分數有關的,其中兩個是「SCORE」與「BEST」,這兩個靜態控件的文字顏色相同、文字背景顏色相同、靜態控件背景顏色也相同;另外兩個是當前分數與最高分數,它們的文字顏色、文字背景顏色與靜態控件背景顏色也都相同。最後是對話盒埵最大區域的 4×4 個靜態控件所組成的矩陣,它們所顯示的文字顏色、文字背景顏色與靜態控件背景顏色會隨著靜態控件的標題而改變。講了這麼多,意思就是說,在 2048 堛瑰R態控件,共分為三種,因此在 508∼509 行取得靜態控件的識別碼後,立即進入三種「分支」。第一種是 510∼513 行,處理當前分數與最高分數的顏色;第二種是 514∼517 行,處理「SCORE」與「BEST」的顏色;第三種是 518∼536 行,處理 4×4 顯示數值的靜態控件。處理這三種靜態控件完後,都是把文字顏色存入 ECX 堙A文字背景顏色存入 BkColor 堙A靜態控件背景畫刷推入堆疊堙C到了第 537∼539 行時,再以這些數值設定文字顏色、文字背景顏色和靜態控件背景顏色。

靜態控件的字形

如果不特別設定,靜態控件的字形是由資源描述檔中的對話盒面板的項目「FONT」來定義,這種做法會使對話盒中所有控件字形都是相同的。除此之外,我們也可以經由設定靜態控件的風格為「SS_OWNERDRAW」,來單獨改變某個靜態控件的字形。如果靜態控件的風格含有「SS_OWNERDRAW」,不管在什麼時候,作業系統於螢幕上繪出靜態控件時,就會對該靜態控件的父視窗之視窗函式發出「WM_DRAWITEM」訊息,程式就可以在此訊息中改變字形。有關 WM_DRAWITEM 訊息,請參考第十二章 WM_DRAWITEM 訊息與 DRAWITEMSTRUCT 結構體 。在 2048 程式堙A因為數值位數不同,所以那 4×4 靜態控件所構成的矩陣,得用不同的字形,因此這 16 個靜態控件均具有「SS_OWNERDRAW」風格。

在 2048 的第 543∼545 三行堙A先把 EBX 指定為 DRAWITEMSTRUCT 所在位址。接著的第 546∼548 行,是取得靜態控件的工作區大小、取得靜態控件的標題 ( 存於 szText 字串 ) 及數值。接著第 549∼555 行,根據數值有幾位數,選擇適當的字形,這些字形在第 493∼499 行被建立,存於 hFont 陣列中。您應當可以看出來,如果靜態控件的數值僅有個位數或兩位數,使用 hFont[0] 字形,亦即 48 點的「Verdana」字體;如果是三位數、四位數,則使用 hFont[4] 字形,亦即 40 點的「Verdana」字體;如果是五位數,則使用 hFont[8] 字形,亦即 32 點的「Verdana」字體。最後在第 556 行,把被選定的字形代碼存入區域變數,hfont,堙C到了第 559 行,呼叫 SelectObject,把該字形選入到靜態控件的設備內容 ( device context ) 堙A那麼,這個靜態控件的標題字形就會改變了。

第 557∼558 行,主要的目的是在整個靜態控件上畫上一個填滿的長方形。為什麼要這樣做呢?原來具有「SS_OWNERDRAW」風格的靜態控件是無法利用處理 WM_CTLCOLORSTATIC 後,傳回畫刷以填滿整個靜態控件,必須自行繪製背景,所有才叫使用者自行繪製。因此,在第 558 行,呼叫 FillRect 填滿背景。呼叫 FillRect 所需的設備內容代碼 ( hDC ) 可由 DRAWITEMSTRUCT 結構體的 hdc 欄位得知,矩形大小由第 546 行取得,該矩形大小就是靜態控件的工作區大小。FillRect 所需畫刷代碼則在 hbrBkStatic 陣列中。但是所需畫刷是在 hbrBkStatic 的第幾個位址,則是由 get_index 副程式決定。get_index 副程式在第 439∼454 行,輸入靜態控件的標題數值給 get_index,get_index 會轉換該數值在 hbrBkStatic 陣列的偏移位址,存於 EDX 傳回來。

initial_game 副程式

initial_game 是用來初始化遊戲的副程式,它要做的工作有四個:①設定最高分數、②把位於對話盒中央部份的 4×4 矩陣內的 16 個靜態控件標題設為「0」( 此「0」乃是字串 )、③把當前分數歸零、④以亂數於 4×4 矩陣中的靜態控件,隨機選出兩個靜態控件並隨機將其標題設為「2」或「4」。前三項顯然是很簡單,請自行參閱程式第 365∼376 行。最後一個工作牽涉到亂數,在 2048 程式中,取得亂數的副程式是 tiny_random;而隨機設定哪一個靜態控件則是 set_number_in_random 副程式。底下小木偶要解說的是tiny_random、set_number_in_random 這兩個副程式。

tiny_random 副程式

老實說,tiny_random 副程式是小木偶在TUTS4YOU 論壇中的 Programming and Coding 得到的,作者不知是誰。雖然有人 ( Blue ) 在該論壇上發言說,這個程式每次執行時傳回的亂數均為「2」,但是小木偶在我的 AMD A4-3300 上執行,每次結果均不同,很符合亂數的行為。這個 tiny_random 副程式需要輸入一個參數,iRange,經過計算後,傳回的亂數在 0 到 ( iRange-1 ) 之間,亦即如果 iRange 為 100,傳回的亂數在 0∼99 之間。

tiny_random 副程式一開始使用了 RDTSC 指令,RDTSC 指令是 Pentium CPU 新增的一條指令,原來的英文名稱是「ReaD Time Stamp Counter」。Pentium 或 Pentium 以上等級的 CPU 堶情A含有一個 64 位元的「時脈計數暫存器」( time-stamp counter,簡稱 TSC )。一開機後,時脈計數暫存器就會從零開始,每經過一個時脈週期 ( clock cycle ),其內之數值就增加一。有關時脈週期,可參考組合語言準備工作註一。而 RDTSC 指令則是取出時脈計數暫存器之值,並把它存在 EDX:EAX 所組成的 64 位元暫存器內。不同機型、不同種類的 CPU,其時脈週期也不同。假如有一個 CPU 工作頻率是 2000MHz 的話,亦即石英振盪器每秒鐘振動 2×109次,每振動一次,就會使時脈計數暫存器增加一,因此每一秒鐘就可以使時脈計數暫存器增加 2×109,2×109 是十六進位的 77359400h,64 位元能夠儲存的最大數值是 0FFFFFFFFFFFFFFFFh,除以 77359400h 得到 9223372036 秒,換算成年數,約 292 年。

有時候 RDTSC 必須在特權等級 0 時才能執行,取決於 CPU 的 CR4 暫存器。這個暫存器的第 2 個位元稱為 TSD 旗標 ( time stamp disable ),若設定此旗標 ( 亦即 TSD=1 ),就只能在特權等級 0 時才能讀取「時脈計數暫存器」;否則不受限制。幸好Windows 作業系統並沒有設定此旗標,因此可放心在 Windows 作業系統中使用。

set_number_in_random 副程式

遊戲一開始時,對話盒中央的 4×4 個靜態控件所組成的矩陣,標題均為「0」,接著會以亂數挑出其中兩個,使其標題設為「2」或「4」。在遊戲過程中,這 4×4 的靜態控件所組成的矩陣中,有些是空白的,亦即標題為「0」;有些標題在「2」∼「32768」之間。每按一次按鍵之後,程式也必須由標題為「0」的靜態控件中,隨機選出一個控件,並把它標題設為「2」或「4」。小木偶的做法是,在這 4×4 的靜態控件,把標題為「0」的識別碼挑出來,存在 idBlankStatic 陣列中。在這過程堙A以 pidBlankStatic 當做在 idBlankStatic 的指標,以 n 當做標題為「0」的靜態控件個數。參見 320∼335 行。

接著檢查 n 是否為零,若為零表示沒有標題為「0」的靜態控件,將 EAX 設為一,並設定進位旗標,跳回 set_number_in_random 副程式。若 n 不為零,則在 idBlankStatic 陣列中,以亂數取出一個靜態控件的識別碼,見程式第 338∼343。程式第 338∼343 行,是呼叫 tiny_random 隨機取得 0 到 ( n-1 ) 的亂數,以此亂數到 idBlankStatic 陣列中取出某個靜態控件的識別碼。

程式第 344∼351 行,呼叫 tiny_random 得到 0 到 0Fh 的亂數,如果此亂數小於 0Ch,則把此靜態控件標題設為「2」,若大於或等於 0Ch,則把標題設為「4」。這樣就能保證新產生的標題,出現「2」的機率會比「4」來得多。程式第 352∼357 行,處理 n 是否等於一,如果等於一,表示這次已將最後一個標題是「0」的靜態控件設為「2」或「4」。此時要檢查相鄰的靜態控件標題是否有相同的,如果沒有相同的,代表無法相加而使其中一個靜態控件的標題變為「0」,遊戲因此結束。詳細檢查相鄰的靜態控件標題是否有相同的過程是在 check_gameover 副程式堻B理。

處理 WM_KEYDOWN 訊息

原本對話盒函式式不處理按鍵訊息的 ( 包含 WM_KEYDOWN、WM_KEYUP…),但是經由子類化後,欺騙按鈕的視窗函式,把 WM_KEYDOWN 傳給對話盒函式,使對話盒也能接收到 WM_KEYDOWN 訊息。一開始處理 WM_KEYDOWN 訊息是先把當前 16 個靜態控件的標題變成數值,存入 number 陣列中,這件事由 save_number 副程式完成。接著是呼叫 arrange_matrix 副程式,這個副程式處理上、下、左、右鍵,底下小木偶說明 arrange_matrix 副程式。

arrange_matrix 與 arrange_a_row

arrange_matrix 需要一個參數,這個參數決定數字往哪個方向移動,程式第 576∼584 行依據 WM_KEYDOWN 訊息的 wParam 決定 ECX 值,然後在第 585 行以 ECX 為參數呼叫 arrange_matrix 副程式。例如,如果使用者按下向「↓」鍵,那麼 wParam 參數為 VK_DOWN,則 ECX 被設為「TOPTODOWN」( 表示數字由上往下移動,其值為 0 ),然後才呼叫 arrange_matrix 副程式。

每按一次方向鍵,程式就得處理四行數字,而處理這四行數字的方法都是相同的。因此,只要寫好處理一行的副程式,其他三行就可以比照辦理,只要呼叫這個副程式四次就可以了。很幸運的,這個副程式不難寫,稱為 arrange_a_row。處理每一行數字的方法是先把數字向該方向移動,再檢查相鄰的數字是否相同,如果相同就相加並使其和存到該方向的前一個靜態控件上,而且使後一個靜態控件變為「0」( 所以有時小木偶稱相加為縮減 )。另外,如果您很仔細觀察 2048 中,會發現不管數字向哪一個方向移動,只要掌握三個變數,就可以只用一個副程式處理四個方向的數字移動與縮減。這三個變數是第一行的數字往哪個靜態控件移動、每一行第一個靜態控件識別碼與下一行第一個靜態控件識別碼之差、該行下一個靜態控件識別碼與上一個識別碼之差,分別以 StartID、idBetweenRow、difference 變數表示。

請參考右圖。舉兩個例子說明 StartID 和 idBetweenRow 兩個變數:

  1. 如果按下向「↓」鍵,那麼每一行的數字都向下移,除非底下有數字,因此最左一行往 IDS_0C 移動、左邊第二行往 IDS_0D 移動…。最左一行與左邊第二行之間的識別碼相差 1。因此 StartID 為 IDS_0C,idBetweenRow 為 1。這樣每處理完一行,StartID 只要加上 idBetweenRow 就可以處理下一行。
  2. 如果按下向「→」鍵,則最上一行往 IDS_03 移動、上面第二行往 IDS_07 移動…,最上一行與上面第二行之間的識別碼相差 4。所以 StartID 為 IDS_03,idBetweenRow 為 4。

關於 deifference 是每一行前後靜態控件識別碼之差,但是為了要配合 arrange_a_row 副程式移動數字與縮減方式,因此有些是正值,表示增加,有些是負值,表示減少。arrange_a_row 副程式移動數字或縮減如下:設一個含有 4 個成員的陣列,dwN,如果較低位址的數值為「0」,則較高位址的數值向低位址移動。移動完成之後,檢查陣列中相鄰的成員是否有相同的,如果有相同的則相加,把所得之和存入低位址,高位址變為「0」。因此,如果按下向「↓」鍵,那麼,IDS_0C 的數字不動,除非為「0」才會使 IDS_08 的數字移到 IDS_0C,因此 StartID 其實也代表了基準點,而 difference 則表示與基準點的差值。

程式第 201∼221 行,依據 direct 設定 StartID、idBetweenRow、difference 三個變數,direct 是玩家所按的方向按鍵。第 224∼259 行的迴圈,處理 16 個靜態控件的移動或縮減,可分為四部分說明:

  1. 第一部分是從 225∼237 行的程式碼,是把某一行的靜態控件標題變成數值,存入 dwN 陣列堙A以供給第 239∼242 行 arrange_a_row 使用。您應當可以看見程式讀取識別碼為 ID 的靜態控件標題,且 dwN 的最低位址存放的是識別碼為 StartID 的靜態控件標題所變成的數值,接著第二低位址的成員存放的是識別碼為 ( StartID+difference ) 的靜態控件標題所變成的數值,依此類推。
  2. 第二部分是呼叫 arrange_a_row,返回後 dwN 陣列會變成經過移動數字或縮減的結果,此外,如有縮減或相加,則清除進位旗標;如沒有縮減也沒相加,則設定進位旗標。
  3. 第三部分是從 243∼255 行,把 dwN 陣列變成字串,把靜態控件標題設為相對應的字串。
  4. 等一行 4 個靜態控件處理完後,您可以在 257∼258 行看見每處理完一行 4 個靜態控件後,StartID 會重新設過,變成原來的 StartID 再加上 idBetweenRow,於是又重新開始新的一行。

WM_GETDLGCODE 訊息

當對話盒在螢幕的最上層時 ( 亦即正在使用中,標題欄為明亮色,非暗色 ),如果使用者按下方向鍵時,作業系統預定的處理方式並不是把方向鍵訊息傳送給具有輸入焦點的控件,而是把它們用來在各個控件之間切換輸入焦點。假設不想以作業系統預定方式處理方向鍵,而要讓方向鍵傳給控件,那麼就得先處理 WM_GETDLGCODE 訊息。當作業系統把 WM_GETDLGCODE 傳給控件的視窗函式時,wParam 是使用者按下按鍵的虛擬鍵碼 ( virtual key code ),lParam 則是位址指標,指向 MSG 結構體。處理完 WM_GETDLGCODE 訊息後,應該給作業系統返回值,如下表:

返回值說明
DLGC_BUTTON把按鈕的訊息傳給對話盒函式
DLGC_DEFPUSHBUTTON把內定下壓式按鈕的訊息傳給對話盒函式
DLGC_HASSETSEL把 EM_SETSEL 訊息傳給對話盒函式
DLGC_RADIOBUTTON把圓形按鈕的訊息傳給對話盒函式
DLGC_STATIC把靜態控件的訊息傳給對話盒函式
DLGC_UNDEFPUSHBUTTON
DLGC_WANTALLKEYS把所有按鍵訊息傳給對話盒函式
DLGC_WANTARROWS僅把方向鍵訊息傳給對話盒函式
DLGC_WANTCHARS把 WM_CHAR 傳給對話盒函式
DLGC_WANTMESSAGE把所有按鍵訊息傳給控件的視窗函式
DLGC_WANTTAB把 Tab 按鍵訊息傳給對話盒函式

在子類化的按鈕視窗函式堙Anew_button_proc,您可以見到處理 WM_GETDLGCODE 訊息的過程,僅僅傳回 DLGC_WANTALLKEYS,就能使所有按鍵訊息傳給對話盒視窗函式了。( 程式第 394∼396 行 )


到第二十五章回到首頁到第二十七章