第 25 章 通用控件的 Custom Draw


Custom Draw 簡介

Custom Draw 簡介與 Owner Draw 比較

Custom Draw 並不是一種通用控件 ( 也稱通用控制項,英文是 common controls ),而是一種能使某些通用控件改變外觀的方式。就字面上解釋,可以看成是「客製化」的繪圖方式,意思是 Windows 可以依程式設計師 ( 即顧客 ) 的要求,對於某些通用控件進行繪製。能進行 custom draw 的通用控件包含標題欄 ( header controls )、清單檢視 ( list-view controls )、rebar controls、工具列 ( toolbar controls )、工具提示 ( tooltip controls )、trackbar controls、樹狀檢視 ( tree-view controls ) 七種通用控件,而且只有 4.70 版及其以後的 COMCTL32.DLL 才能支援 custom draw。COMCTL32.DLL 4.70 版本是隨 Internet Exploere 3.0 安裝的,所以如果您安裝好 IE 3.x 後,同時安裝好 COMCTL32.DLL 4.70 版了。現在 ( 民國 103 年,也就是西元 2014 年 ) 幾乎所有的 Windows 系統的 COMCTL32.DLL 版本,都應在 4.70 以上了。

在第 12 章提到的 owner draw 也能改變控件的外觀,與 custom draw 不同的地方約有下面幾點:

由上面的比較,您應當可以發現 custom draw 似乎比 owner draw 更有彈性,也更靈活。那麼,custom draw 又是如何辦到的呢?原來所有支援 custom draw 的控件會在繪製該控件時,自動的發送通知碼為 NM_CUSTOMDRAW 的 WM_NOTIFY 訊息給父視窗的視窗函式,其所攜帶的 wParam 為控件的識別碼,lParam 為 NMCUSTOMDRAW 結構體位址或以 NMCUSTOMDRAW 結構體為起頭的另一較大結構體位址。這個結構體堶悼]含了一個欄位,dwDrawStage,其意義為此刻即將進行哪個繪製階段,程式必 須做適當的返回值,以指定程式負責哪些部份的繪製,而系統又負責繪製哪些部份,這個 dwDrawStage 所代表的數值與該返回什麼,是 custom draw 的精髓,容稍後再詳述。所謂的「以 NMCUSTOMDRAW 結構體為起頭的另一較大結構體」通常是給較為複雜的通用控件所使用,例如「樹狀檢視」、「清單檢視]……等等。參考下表:

控件結構體
清單檢視 ( list view )NMLVCUSTOMDRAW
工具列 ( toolbar )NMTBCUSTOMDRAW
工具提示 ( tooltip )NMTTCUSTOMDRAW
樹狀檢視 ( tree-view )NMTVCUSTOMDRAW
標題欄 ( header controls )、rebar controls、trackbar controls NMCUSTOMDRAW

之所以有這些不同的結構體,當然是因為每種通用控件性質不同的關係,不過本章僅僅討論樹狀檢視而已。

以 custom draw 繪製樹狀檢視後,多變的外觀

由上面的敘述可知,許多通用控件都能進行 custom draw,但是本章僅僅說明樹狀檢視的 custom draw 用法,所以也只介紹 NMTVCUSTOMDRAW 而已。至於其他通用控件的 custom draw 大同小異,請自行研究 MSDN。一開始,先看看本章的例子,TVCD.EXE。它執行後,使用者可以由主選單的「檢視」選擇四種字形與三種大小,也可以選擇四種不同的背景,如下圖所示:

上面的兩張顯示的是不用 custom draw 後的結果,這也是一般執行樹狀檢視看到的情形。底下的三張則是實行 custom draw 後的結果︰左邊的樹狀檢視,是使用者選擇了背景為「黃紅漸進」,字形為「12」點的「新細明體」;中間的樹狀檢視是選擇了背景為「黃白交錯」,字形為「14」點的「微軟正黑體」;右邊的樹狀檢視則是選擇了背景為「美女圖」,字形為「14」點的「Courier New」。

如果不使用 custom draw 方式,應該是無法造成上面的畫面。底下小木偶將介紹在樹狀檢視使用 custom draw。


以 custom draw 繪製樹狀檢視

原理

Win32 系統在繪製樹狀檢視前,會自行發出通知碼為 NM_CUSTOMDRAW 的 WM_NOTIFY 訊息,其 wParam 是樹狀檢視的識別碼;lParam 則是 NMTVCUSTOMDRAW 結構體位址,這個結構體的各欄位如下所示。您應當可以看見,NMTVCUSTOMDRAW 的第一個成員是 NMCUSTOMDRAW 結構體,另外還有三個欄位,所以本章一開始才說 lParam 所指的位址是一個以 NMCUSTOMDRAW 為起頭的結構體。

NMTVCUSTOMDRAW  STRUCT
nmcd            NMCUSTOMDRAW    <>
clrText         COLORREF        ?       ;項目名稱的顏色
clrTextBk       COLORREF        ?       ;項目名稱背景顏色
iLevel          DWORD           ?       ;根項目為 0;根項目的子項目為 1;根項目的孫項目為 2……
NMTVCUSTOMDRAW  ENDS

NMTVCUSTOMDRAW 結構體中的 clrText、cltRextBk、iLevel 可參見上面右邊的註釋。如果返回時,您設定了 clrText 或 clrTextBk,並作其他適當的設定,那麼就會改變項目名稱的顏色及項目名稱的背景顏色 ( 項目名稱的背景僅僅是指包住項目名稱文字的裁剪矩形範圍,見上面左下圖的說明;視窗檢視的背景則是除了裁剪矩形以外的其他部份 )。NMTVCUSTOMDRAW 結構體的第一個欄位是 NMCUSTOMDRAW,它是另一個結構體,其各欄位是:

NMCUSTOMDRAW    STRUCT
hdr             NMHDR   <>
dwDrawStage     DWORD   ?
hdc             DWORD   ?
rc              RECT    <>
dwItemSpec      DWORD   ?
uItemState      DWORD   ?
lItemlParam     DWORD   ?
NMCUSTOMDRAW    ENDS

hdr結構體已在前面已經提過,此處再列一遍,它們是:

NMHDR       STRUC
hwndFrom    HWND    ?       ;發出 WM_NOTIFY 的控制項代碼
idFrom      DWORD   ?       ;發出 WM_NOTIFY 的控制項識別碼
code        DWORD   ?       ;通知碼 ( notification )
NMHDR       ENDS

NMCUSTOMDRAW 結構體的 hdc 是樹狀控件的設備內容代碼。rc 是即將繪製的矩形區域只有 CDDS_ITEMPREPAINT、CDDS_PREPAINT 階段才有效 ( CDDS_ 是指 custom draw draw stage 之意,中文是「客製化的繪製階段」,這是本章的重軸戲,稍後再詳述 )。dwItemSpec 在樹狀檢視中,如果繪製階段為 CDDS_ITEM* 時,dwItemSpec 表示即將繪製或擦除的項目代碼;在清單檢視中,為即將繪製或擦除的項目索引。lItemParam 是由程式自行定義的數值。uItemState 表示項目狀態,可以是下面幾種情形:

狀態數值說  明
CDIS_CHECKED8 處於被核選狀態
CDIS_DEFAULT20H 內定狀態
CDIS_DISABLED4 處於禁止狀態
CDIS_FOCUS10H 具有焦點
CDIS_DISABLED2 處於灰色狀態
CDIS_HOT40H 滑鼠游標停留在這個項目上
CDIS_SELECTED1 被選中狀態
CDIS_INDETERMINATE100H
CDIS_MARKED80H
CDIS_SHOWKEYBOARDCUES200H
CDIS_NEARHOT400H
CDIS_OTHERSIDEHOT800H
CDIS_DROPHILITED1000H 在拖曳過程中,此項目處於拖曳目標狀態下

底下要說明本章重軸戲「客製化的繪製階段」了,它是 NMCUSTOMDRAW 結構體的一個欄位,dwDrawStage,是指系統即將繪製樹狀檢視時所處的階段,可以是下面一種數值:

  1. 畫出整個樹狀檢視控件時:
  2. 畫出某個項目時:

在系統把樹狀檢視顯示在螢幕上時,歷經四個階段:繪製前→擦去前→擦去後→繪製後,亦即 CDDS_PREPAINT→CDDS_PREERASE→CDDS_POSTERASE→CDDS_POSTPAINT。這堛澈鉿疻瓡雓O把「擦去」也當成是繪製的一部份。試想如果系統要繪製一控件,便立即進入繪製前階段,然後系統要擦去原先圖案,於是第二個階段是擦去前,接著系統執行擦去圖案的步驟,接著進入擦去後階段,然後應該是系統繪製,完成後就是繪製控件之後的階段。而系統繪製整個控件時,也會對每個項目進行繪製。每一個項目繪製時也歷經四個階段:繪製前→擦去前→擦去後→繪製後,亦即 CDDS_ITEMPREPAINT→CDDS_ITEMPREERASE→CDDS_ITEMPOSTERASE→CDDS_ITEMPOSTPAINT。思維方式一如上面繪製整個控件。因此,當系統進行繪製控件時,控件會不斷的對父視窗發出攜帶 NM_CUSTOMDRAW 通知碼的 WM_NOTIFY 訊息,此 WM_NOTIFY 訊息的 lParam 參數所指結構體中的 dwDrawStage 欄位,便會是上面那八個繪製階段的某一個。

前面說過,使用 custom draw 時,可以非常有彈性,程式可以只負責繪製一小部份,也可以負責繪製大部分,或是全部由程式負責。其中的關鍵在於當處於各種繪製階段時,必須設定適當的返回值,那麼控件就能依據返回值繪製該畫出的部份。當 dwDrawStage 為 CDDS_PREPAINT 時,必須指定下面的一個數值或數個數值的聯集做為返回值:

返回值十六進位
數值
說  明
CDRF_DODEFAULT0 控件不再發出 NM_CUSTOMDRAW 通知碼,系統處理所有的繪製過程。CDRF_DODEFAULT 不可與其它返回值聯集使用。
CDRF_DOERASE08H 系統僅僅負責繪製背景,其餘部份必須由程式自行繪製。Windows Vista 及其以後的系統才能使用。
CDRF_SKIPDEFAULT04H 在所有繪製階段,控件都會發出 NM_CUSTOMDRAW 通知碼,由程式負責繪製控件所有部份,系統不繪製任何階段。
CDRF_NOTIFYPOSTERASE40H 在擦去某個項目之後,控件發出 NM_CUSTOMDRAW 通知碼。
CDRF_NOTIFYPOSTPAINT10H 在系統完成繪製整個控件後,對父視窗發出 NM_CUSTOMDRAW 通知碼。
CDRF_NOTIFYITEMDRAW20H 在系統繪製某個項目之前或繪製之後,對父視窗發出 NM_CUSTOMDRAW 通知碼。
CDRF_SKIPPOSTPAINT100H 系統不繪製被選中項目矩形區域,需由程式繪製。

底下說明幾個情形︰

  1. 如果您在 CDDS_PREPAINT 的繪製階段,返回 CDRF_DODEFAULT,那麼控件就不會再發出 NM_CUSTOMDRAW 通知碼,換句話說,就是由系統來繪製所有畫面。
  2. 如果在 CDDS_PREPAINT 階段返回 CDRF_NOTIFYITEMDRAW,那麼系統繪製樹狀檢視內的每一個項目前,就會發出 dwDrawStage 為 CDDS_ITEMPREPAINT 的 NM_CUSTOMDRAW 通知碼,程式可以在此時做一些事情。
  3. 如果您在 CDDS_PREPAINT 階段返回 CDRF_NOTIFYPOSTPAINT,那麼在完成繪製整個控件後,控件會發出 dwDrawStage 為 CDDS_POSTPAINT 的 NM_CUSTOMDRAW 通知碼,您可以在此階段做一些收尾的工作。

此處特別要注意的是,繪製階段的返回值必須呼叫 SetWindowLong,這樣才能傳回給系統,如下︰

        INVOKE  SetWindowLong,hDlg,DWL_MSGRESULT,CDRF_NOTIFYITEMDRAW

照小木偶的經驗,雖然控件的繪製有四個階段,但是似乎只有 CDDS_PREPAINT 最為常用,CDDS_POSTPAINT 次之,CDDS_PREERASE 與 CDDS_POSTERASE 幾乎很少用。除非是返回 CDRF_SKIPDEFAULT,您的程式才能接管這兩個繪製階段,但是在這兩個繪製階段能做的事,似乎也不多。


TVCD 原始碼

TVCD.RC

說了這麼多,底下來看看實際的範例。先看 TVCD.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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include "c:\masm32\include\resource.h"
 
#define RT_MANIFEST     24
#define IDM_DRVA        6001
#define IDC_TREEVIEW    1600
#define IDM_PMingLiU    5000
#define IDM_MSJhengHe   5001
#define IDM_CourierNew  5002
#define IDM_TNR         5003
#define IDM_TEN         5004
#define IDM_TWELVE      5005
#define IDM_FOURTEEN    5006
#define IDM_WHITE       5007
#define IDM_GRADIENT    5008
#define IDM_INTERLOCK   5009
#define IDM_LADY        5010
#define IDM_Exit        5011
 
TVCustomDraw ICON    folder2.ico
 
TVCustomDraw DIALOG  100,100,200,150
STYLE   WS_CAPTION|WS_VISIBLE|WS_SYSMENU
FONT    10,"新細明體"
CAPTION "樹狀檢視客製化繪製"
MENU    FLST
BEGIN
 CONTROL        "",IDC_TREEVIEW,"SysTreeView32",WS_BORDER|TVS_HASBUTTONS|TVS_NOTOOLTIPS|
                TVS_HASLINES|TVS_LINESATROOT|TVS_INFOTIP,5,5,190,140
END
 
FLST    MENU
BEGIN
  MENUITEM "離開",IDM_Exit
  POPUP    "磁碟機"
  {
        MENUITEM    "A:",IDM_DRVA
  }
  POPUP    "檢視"
  {
    POPUP  "字型"
    {
        MENUITEM    "新細明體",IDM_PMingLiU
        MENUITEM    "微軟正黑體",IDM_MSJhengHe
        MENUITEM    "Courier New",IDM_CourierNew
        MENUITEM    "Times New Roman",IDM_TNR
        MENUITEM    SEPARATOR
        MENUITEM    "10",IDM_TEN
        MENUITEM    "12",IDM_TWELVE
        MENUITEM    "14",IDM_FOURTEEN
    }
    POPUP  "背景"
    {
        MENUITEM    "白色",IDM_WHITE
        MENUITEM    "黃紅漸進",IDM_GRADIENT
        MENUITEM    "黃白交錯",IDM_INTERLOCK
        MENUITEM    "背景:美女圖",IDM_LADY
    }
  }
END
 
1       RT_MANIFEST MOVEABLE PURE "TVCD.EXE.MANIFEST"
 
Lady     BITMAP lady.bmp
Expand   BITMAP add.bmp
Collapse BITMAP sub.bmp

其他還有 lady.bmp、add.bmp、sub.bmp、folder2.ico 等四個圖形檔案,小木偶將它壓縮到這個檔案堙A請點擊下載。

TVCD.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>

TVCD.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
        .586
        .MODEL  FLAT,STDCALL
        OPTION  CASEMAP:NONE
 
INCLUDE         WINDOWS.INC
INCLUDE         COMCTL32.INC
INCLUDE         GDI32.INC
INCLUDE         KERNEL32.INC
INCLUDE         MSIMG32.INC
INCLUDE         USER32.INC
INCLUDELIB      COMCTL32.LIB
INCLUDELIB      GDI32.LIB
INCLUDELIB      KERNEL32.LIB
INCLUDELIB      MSIMG32.LIB
INCLUDELIB      USER32.LIB
 
IDC_TREEVIEW    EQU     1600
IDM_PMingLiU    EQU     5000
IDM_MSJhengHe   EQU     5001
IDM_CourierNew  EQU     5002
IDM_TNR         EQU     5003
IDM_TEN         EQU     5004
IDM_TWELVE      EQU     5005
IDM_FOURTEEN    EQU     5006
IDM_WHITE       EQU     5007
IDM_GRADIENT    EQU     5008
IDM_INTERLOCK   EQU     5009
IDM_LADY        EQU     5010
IDM_Exit        EQU     5011
IDM_DRVA        EQU     6000
 
TVINSERTSTRUCT  STRUC
hParent         HANDLE  ?
hInsertAfter    HANDLE  ?
UNION
itemex  TVITEMEX        <>
item    TVITEM          <>
ENDS
TVINSERTSTRUCT  ENDS
;***************************************************************************************************
.CONST
szDlgName       DB              "TVCustomDraw",0
szIconName      DB              "TVCustomDraw",0
szLogicalDrv    DB              "E:\",0
szPMingLiU      DB              "新細明體",0
szMSJhengHe     DB              "微軟正黑體",0
szCourierNew    DB              "Courier New",0
szTNR           DB              "Times New Roman",0
arypFont        LPSTR           OFFSET szPMingLiU,OFFSET szMSJhengHe,OFFSET szCourierNew,OFFSET szTNR
szLady          DB              "Lady",0        ;美女圖的名稱
szCollapse      DB              "Collapse",0    ;減號,表示可以展開
szExpand        DB              "Expand",0      ;加號,表示可以收攏
;***************************************************************************************************
.DATA
hInstance       HINSTANCE       ?
hTreeView       HANDLE          ?
hbrWhite        HBRUSH          ?       ;白色畫刷
hbrYellow       HBRUSH          ?       ;黃色畫刷
hbmpLady        HBITMAP         ?
hbmpExpand      HBITMAP         ?       ;收攏時的符號,加號
hbmpCollapse    HBITMAP         ?       ;展開後的符號,減號
cxLady          DWORD           ?       ;「Lady」圖的寬度
cyLady          DWORD           ?       ;「Lady」圖的高度
cxExpand        DWORD           ?       ;「Expand」圖的寬度
cyExpand        DWORD           ?       ;「Expand」圖的高度
hMainMenu       HMENU           ?       ;主選單代碼
hDrvMenu        HMENU           ?       ;選擇磁碟機的子選單代碼
hViewMenu       HMENU           ?       ;「檢視」子選單代碼,下有字形、背景兩個子選單
hViewFontMenu   HMENU           ?       ;「字形」子選單代碼
hViewBgMenu     HMENU           ?       ;「背景」子選單代碼
hTreeRootItem   HTREEITEM       ?
dwBk            DWORD           0       ;樹狀檢視的背景:0-白色;1-紅黃漸進;2-黃白交錯;3-美女圖
dwFont          DWORD           0       ;字形:0-新細明體;1-微軟正黑體;2-"Courier New";3-"Times New Roman"
dwFontSize      DWORD           1       ;字形大小:0-10點;1-12點;2-14點
dwDrv           DWORD           2       ;磁碟機:0-A:;1-B:;2-C:;3-D:‥‥‥
nLogDrv         DWORD           1       ;記錄邏輯磁碟個數,以做為刪除子項目之用
rcTreeView      RECT            <>      ;樹狀檢視工作區大小
dwIndent        DWORD           ?       ;子項目與父項目內縮多少點
ary_tvt         TRIVERTEX       <0,0,0ffffh,0,0,0>,<?,?,0ffffh,0ffffh,0,0>
grect           GRADIENT_RECT   <0,1>
xScroll         DWORD           0       ;捲軸操縱桿的水平位置
yScroll         DWORD           0       ;捲軸操縱桿的鉛垂位置
;***************************************************************************************************
.CODE
;---------------------------------------------------------------------------------------------------
;在樹狀檢視的背景中,畫出由紅漸進變黃色的背景圖
gradient        PROC
                ASSUME  ebx:PTR NMTVCUSTOMDRAW
               ;LOCAL   hdcMem:HDC,hbmMem:HBITMAP
               ;INVOKE  CreateCompatibleDC,[ebx].nmcd.hdc
               ;mov     hdcMem,eax
               ;INVOKE  CreateCompatibleBitmap,[ebx].nmcd.hdc,[ebx].nmcd.rc.right,[ebx].nmcd.rc.bottom
               ;mov     hbmMem,eax
               ;INVOKE  SelectObject,hdcMem,eax
                mov     edx,SIZEOF TRIVERTEX
                mov     eax,[ebx].nmcd.rc.right
                mov     ecx,[ebx].nmcd.rc.bottom
                mov     ary_tvt[edx].x,eax
                mov     ary_tvt[edx].y,ecx
                INVOKE  GradientFill,[ebx].nmcd.hdc,OFFSET ary_tvt,2,OFFSET grect,1,GRADIENT_FILL_RECT_H
               ;INVOKE  BitBlt,[ebx].nmcd.hdc,0,0,[ebx].nmcd.rc.right,[ebx].nmcd.rc.bottom,hdcMem,0,0,SRCCOPY
               ;INVOKE  DeleteDC,hdcMem
               ;INVOKE  DeleteObject,hbmMem
                ASSUME  ebx:NOTHING
                ret
gradient        ENDP
;---------------------------------------------------------------------------------------------------
;取得邏輯磁碟的根目錄及其所有檔案名稱、子目錄名稱,作為樹狀檢視的項目
;輸入:hTreeCtrl-樹狀檢視代碼
;     nLogHD-邏輯磁碟名稱
;輸出:EAX-根項目代碼
GetFileAsItem   PROC    USES esi edi hTreeCtrl:DWORD,nLogHD:DWORD
                LOCAL   szRootDir[8]:BYTE       ;「X:\*.*0」字串
                LOCAL   w32fd:WIN32_FIND_DATA
                LOCAL   tvis:TVINSERTSTRUCT
                LOCAL   hSearch:HANDLE
                LOCAL   hRootItem:HTREEITEM
                mov     tvis.hParent,TVI_ROOT
                mov     tvis.hInsertAfter,TVI_FIRST
                mov     tvis.item.imask,TVIF_CHILDREN or TVIF_TEXT
                lea     edi,szRootDir
                mov     eax,nLogHD
                mov     edx,edi
                add     eax,'A'
                stosb
                mov     eax,2e2a5c3ah
                stosd
                mov     ax,2ah
                stosw
                mov     tvis.item.pszText,edx
                mov     tvis.item.cChildren,1
                INVOKE  SendMessage,hTreeCtrl,TVM_INSERTITEM,0,ADDR tvis
                mov     hRootItem,eax
                INVOKE  FindFirstFile,ADDR szRootDir,ADDR w32fd
                mov     hSearch,eax
                cmp     eax,INVALID_HANDLE_VALUE
                je      finish
        .WHILE eax!=0
                mov     ecx,hRootItem
                mov     tvis.hParent,ecx
                mov     tvis.hInsertAfter,TVI_LAST
                mov     tvis.itemex.imask,TVIF_CHILDREN or TVIF_TEXT or TVIF_IMAGE or TVIF_SELECTEDIMAGE
                lea     edx,w32fd.cFileName
                mov     tvis.itemex.cChildren,0
                mov     tvis.itemex.pszText,edx
                mov     ecx,3
                test    w32fd.dwFileAttributes,FILE_ATTRIBUTE_DIRECTORY
                jz      @f
                mov     ecx,2
@@:             mov     tvis.itemex.iImage,ecx
                mov     tvis.itemex.iSelectedImage,ecx
                INVOKE  SendMessage,hTreeCtrl,TVM_INSERTITEM,0,ADDR tvis
                INVOKE  FindNextFile,hSearch,ADDR w32fd
        .ENDW
finish:         INVOKE  FindClose,hSearch
                mov     eax,hRootItem
                ret
GetFileAsItem   ENDP
;---------------------------------------------------------------------------------------------------
;背景由白色與黃色交錯而成
interlock       PROC
                LOCAL   rcExtItem:RECT
                LOCAL   dwItemHight,nItem,n:DWORD
                INVOKE  SendMessage,hTreeView,TVM_GETITEMHEIGHT,0,0
                mov     ecx,eax
                sub     edx,edx
                mov     eax,rcTreeView.bottom
                div     ecx
                mov     dwItemHight,ecx
                mov     nItem,eax
                mov     n,0
                mov     edx,rcTreeView.right
                mov     rcExtItem.left,0
                mov     rcExtItem.right,edx
                mov     rcExtItem.top,0
                mov     rcExtItem.bottom,ecx
            .WHILE nItem>0
                mov     ecx,hbrWhite
                test    n,1
                jz      @f
                mov     ecx,hbrYellow
                ASSUME  ebx:PTR NMTVCUSTOMDRAW
@@:             INVOKE  FillRect,[ebx].nmcd.hdc,ADDR rcExtItem,ecx
                ASSUME  ebx:NOTHING
                mov     ecx,dwItemHight
                add     rcExtItem.bottom,ecx
                add     rcExtItem.top,ecx
                inc     n
                dec     nItem
            .ENDW
                ret
interlock       ENDP
;---------------------------------------------------------------------------------------------------
;依據dwFont、dwFontSize之值,設定字形及其大小
set_font        PROC
                ASSUME  ebx:PTR NMTVCUSTOMDRAW
                mov     ecx,dwFont
                mov     eax,dwFontSize
                shl     ecx,2
                shl     eax,1
                add     ecx,OFFSET arypFont
                add     eax,10
                mov     edx,[ecx]
                INVOKE  CreateFont,eax,0,0,0,FW_NORMAL,0,0,0,ANSI_CHARSET,OUT_DEFAULT_PRECIS,\
                        CLIP_DEFAULT_PRECIS,DEFAULT_QUALITY,DEFAULT_PITCH or FF_DONTCARE,edx
                INVOKE  SelectObject,[ebx].nmcd.hdc,eax
                INVOKE  DeleteObject,eax
                ret
set_font        ENDP
;---------------------------------------------------------------------------------------------------
;此副程式畫出各項目之前的虛線
draw_line       PROC
                LOCAL   x,y,cyItem,counter:DWORD        ;畫出項目前的虛線時使用的變數
                INVOKE  SendMessage,hTreeView,TVM_GETITEMHEIGHT,0,0
        ;畫出項目所在矩形內的垂直虛線
                mov     cyItem,eax
                shr     eax,1
                mov     counter,eax
                mov     ecx,[ebx].nmcd.rc.top
                dec     ecx
                mov     x,26
                mov     y,ecx
        ;檢查是否為最後一個項目,若是,EAX=0
                INVOKE  SendMessage,hTreeView,TVM_GETNEXTITEM,TVGN_NEXT,[ebx].nmcd.dwItemSpec
        .IF eax==0
                shr     counter,1
                inc     counter
        .ENDIF
        .WHILE counter!=0
                INVOKE  SetPixel,[ebx].nmcd.hdc,x,y,99a8ach
                add     y,2
                dec     counter
        .ENDW
        ;畫出項目所在矩形內的水平虛線
                mov     eax,cyItem
                shr     eax,1
                add     eax,[ebx].nmcd.rc.top
                dec     eax
                mov     x,28
                mov     y,eax
                mov     counter,4
        .WHILE counter!=0
                INVOKE  SetPixel,[ebx].nmcd.hdc,x,y,99a8ach
                add     x,2
                dec     counter
        .ENDW
                ASSUME  ebx:NOTHING
                ret
draw_line       ENDP
;---------------------------------------------------------------------------------------------------
DlgProc         PROC    hDlg:HWND,uMsg:UINT,wParam:WPARAM,lParam:LPARAM
                LOCAL   buffer[MAX_PATH]:BYTE
                LOCAL   sLogDrvStr[4*26]:BYTE
                LOCAL   rcDlgOrItm:RECT ;對話盒視窗矩形或項目剪裁矩形
                LOCAL   hdcMem:HDC
                LOCAL   mii:MENUITEMINFO
                LOCAL   bm:BITMAP,tvi:TVITEM
.IF uMsg==WM_NOTIFY
                ASSUME  ebx:PTR NM_TREEVIEW
                push    ebx
                mov     ebx,lParam
                mov     eax,[ebx].hdr.hwndFrom  ;EAX=發出WM_NOTIFY的控制項代碼
  .IF eax==hTreeView
     .IF [ebx].hdr.code==NM_CUSTOMDRAW
                ASSUME  ebx:PTR NMTVCUSTOMDRAW
         .IF [ebx].nmcd.dwDrawStage==CDDS_PREPAINT
            .IF dwBk==1         ;背景為紅色漸進變為黃色
                call    gradient
            .ELSEIF dwBk==2     ;背景為黃白交錯
                call    interlock
            .ELSEIF dwBk==3     ;背景為美女圖
                INVOKE  CreateCompatibleDC,[ebx].nmcd.hdc
                mov     hdcMem,eax
                INVOKE  SelectObject,hdcMem,hbmpLady                                    ;選定來源設備內容的位元圖
                INVOKE  BitBlt,[ebx].nmcd.hdc,0,0,cxLady,cyLady,hdcMem,0,0,SRCCOPY      ;傳送位元圖到視窗的設備內容
                INVOKE  DeleteDC,hdcMem
            .ENDIF
                INVOKE  SetWindowLong,hDlg,DWL_MSGRESULT,CDRF_NOTIFYITEMDRAW OR CDRF_NOTIFYPOSTPAINT
         .ELSEIF [ebx].nmcd.dwDrawStage==CDDS_ITEMPREPAINT
                call    set_font        ;依據dwFont、dwFontSize設定文字字形與字體大小
            .IF dwBk==0                 ;背景為白色
                mov     edx,CDRF_DODEFAULT
            .ELSEIF (dwBk==1)||(dwBk==3);背景為美女圖或紅黃漸進色
                INVOKE  SetBkMode,[ebx].nmcd.hdc,TRANSPARENT
              ;取得項目資料,包含名稱、狀態、有無子項目
                mov     eax,[ebx].nmcd.dwItemSpec
                lea     ecx,buffer
                mov     tvi.imask,TVIF_TEXT or TVIF_STATE or TVIF_CHILDREN
                mov     tvi.pszText,ecx
                mov     tvi.hItem,eax
                mov     tvi.cchTextMax,SIZEOF buffer
                INVOKE  SendMessage,hTreeView,TVM_GETITEM,0,ADDR tvi
                cmp     [ebx].iLevel,0  ;檢查是否為根項目,根項目不畫線
                jz      print_icon
                call    draw_line       ;畫出虛線
print_icon:   ;印出「+」「-」圖示,代表有子項目的項目,處於「收攏」或「展開」狀態
              .IF tvi.cChildren>=1
                INVOKE  CreateCompatibleDC,[ebx].nmcd.hdc
                mov     hdcMem,eax
                mov     ecx,hbmpCollapse
                test    tvi.state,TVIS_EXPANDED
                jnz     print_it
                mov     ecx,hbmpExpand
print_it:       INVOKE  SelectObject,hdcMem,ecx
                INVOKE  BitBlt,[ebx].nmcd.hdc,8,2,cxExpand,cyExpand,hdcMem,0,0,SRCCOPY
                INVOKE  DeleteDC,hdcMem
              .ENDIF
              ;項目名稱在縮排點數×(第幾階+1)之後
                mov     eax,dwIndent
                sub     edx,edx
                mov     ecx,[ebx].iLevel
                inc     ecx
                mul     ecx
                add     eax,[ebx].nmcd.rc.left
                mov     ecx,[ebx].nmcd.rc.top
                mov     rcDlgOrItm.left,eax
                mov     rcDlgOrItm.top,ecx
                INVOKE  DrawText,[ebx].nmcd.hdc,ADDR buffer,-1,ADDR rcDlgOrItm,DT_CALCRECT or DT_SINGLELINE
                test    tvi.state,TVIS_SELECTED
                jz      transparent
                INVOKE  GetSysColor,COLOR_HIGHLIGHT
                INVOKE  CreateSolidBrush,eax
                push    eax
                INVOKE  FillRect,[ebx].nmcd.hdc,ADDR rcDlgOrItm,eax
                call    DeleteObject
              ;印出項目名稱
transparent:    INVOKE  DrawText,[ebx].nmcd.hdc,ADDR buffer,-1,ADDR rcDlgOrItm,DT_VCENTER
                mov     edx,CDRF_SKIPDEFAULT
            .ELSEIF dwBk==2     ;背景為黃白交錯
                INVOKE  GetSysColor,COLOR_HIGHLIGHT
                mov     [ebx].clrTextBk,eax     ;被選定的項目文字背景顏色
                INVOKE  GetSysColor,COLOR_HIGHLIGHTTEXT
                mov     [ebx].clrText,eax       ;被選定的項目文字顏色
                test    [ebx].nmcd.uItemState,CDIS_SELECTED     ;檢查是否被選定,是的話NZ
                jnz     @f
                INVOKE  SendMessage,hTreeView,TVM_GETITEMHEIGHT,0,0
                mov     ecx,eax ;當下要繪製的矩形區域座標(存於[ebx].nmcd.rc.top)
                sub     edx,edx ;除以每列高度(以TVM_GETITEMHIGH得到),即為第幾列
                mov     eax,[ebx].nmcd.rc.top
                mov     [ebx].clrText,edx       ;未被選定的文字為黑色
                mov     [ebx].clrTextBk,0ffffffh;偶數列的文字背景為白色
                div     ecx
                test    eax,1
                jz      @f
                mov     [ebx].clrTextBk,0ffffh  ;奇數列的文字背景為黃色
@@:             mov     edx,CDRF_NOTIFYITEMDRAW
            .ENDIF
                INVOKE  SetWindowLong,hDlg,DWL_MSGRESULT,edx
         .ELSEIF [ebx].nmcd.dwDrawStage==CDDS_POSTPAINT
                INVOKE  GetScrollPos,hTreeView,SB_HORZ
            .IF eax!=xScroll
                mov     xScroll,eax
                jmp     redraw
            .ENDIF
                INVOKE  GetScrollPos,hTreeView,SB_VERT
            .IF eax!=yScroll
                mov     yScroll,eax
redraw:         INVOKE  InvalidateRect,hTreeView,0,FALSE
            .ENDIF
         .ENDIF
     .ENDIF
  .ENDIF
                ASSUME  ebx:NOTHING
                pop     ebx
 
.ELSEIF uMsg==WM_COMMAND
   .IF lParam==0
                mov     eax,wParam
                mov     mii.cbSize,SIZEOF mii
                mov     mii.fMask,MIIM_STATE
                mov     mii.fState,MFS_UNCHECKED
                and     eax,0ffffh              ;EAX=選單識別碼
       .IF eax==IDM_Exit
                jmp     exit
       .ELSEIF (eax==IDM_PMingLiU)||(eax==IDM_MSJhengHe)||(eax==IDM_CourierNew)||(eax==IDM_TNR)
       ;以下處理字形
                sub     eax,IDM_PMingLiU        ;EAX=現在被選定的背景選項識別碼再減去IDM_PMingLiU
                mov     ecx,dwFont              ;ECX=原來被勾選的背景選項識別碼再減去IDM_PMingLiU
           .IF eax!=ecx                         ;若相等,表示選到原先就已勾選的選項
                add     ecx,IDM_PMingLiU
                mov     dwFont,eax
                INVOKE  SetMenuItemInfo,hViewFontMenu,ecx,FALSE,ADDR mii        ;把原來選項設為未選定
                mov     mii.fState,MFS_CHECKED
                INVOKE  SetMenuItemInfo,hViewFontMenu,wParam,FALSE,ADDR mii     ;設定新的選定選項
                INVOKE  InvalidateRect,hTreeView,0,TRUE
           .ENDIF
       .ELSEIF (eax==IDM_TEN)||(eax==IDM_TWELVE)||(eax==IDM_FOURTEEN)
       ;以下處理字體大小
                sub     eax,IDM_TEN
                mov     ecx,dwFontSize
           .IF eax!=ecx
                add     ecx,IDM_TEN
                mov     dwFontSize,eax
                INVOKE  SetMenuItemInfo,hViewFontMenu,ecx,FALSE,ADDR mii
                mov     mii.fState,MFS_CHECKED
                INVOKE  SetMenuItemInfo,hViewFontMenu,wParam,FALSE,ADDR mii
                INVOKE  InvalidateRect,hTreeView,0,TRUE
           .ENDIF
       .ELSEIF (eax==IDM_WHITE)||(eax==IDM_GRADIENT)||(eax==IDM_INTERLOCK)||(eax==IDM_LADY)
       ;以下處理背景
                sub     eax,IDM_WHITE
                mov     ecx,dwBk
           .IF eax!=ecx
                add     ecx,IDM_WHITE
                mov     dwBk,eax
                INVOKE  SetMenuItemInfo,hViewBgMenu,ecx,FALSE,ADDR mii
                mov     mii.fState,MFS_CHECKED
                INVOKE  SetMenuItemInfo,hViewBgMenu,wParam,FALSE,ADDR mii
                INVOKE  InvalidateRect,hTreeView,0,TRUE
           .ENDIF
       .ELSEIF eax>=IDM_DRVA
       ;以下處理磁碟機
                sub     eax,IDM_DRVA
                mov     ecx,dwDrv
           .IF eax!=ecx
                add     ecx,IDM_DRVA
                mov     dwDrv,eax
                INVOKE  SetMenuItemInfo,hDrvMenu,ecx,FALSE,ADDR mii
                mov     mii.fState,MFS_CHECKED
                INVOKE  SetMenuItemInfo,hDrvMenu,wParam,FALSE,ADDR mii
                INVOKE  SendMessage,hTreeView,TVM_DELETEITEM,0,hTreeRootItem    ;刪除原先根項目
                INVOKE  GetFileAsItem,hTreeView,dwDrv   ;插入新的根項目及其子項目
                mov     hTreeRootItem,eax
                INVOKE  InvalidateRect,hTreeView,0,TRUE
           .ENDIF
       .ENDIF
   .ENDIF
 
.ELSEIF uMsg==WM_MENUSELECT
                mov     eax,wParam
                mov     ecx,eax
                shr     eax,10h
                and     ecx,0ffffh
                and     eax,MF_POPUP
   .IF ecx==1                   ;ECX=主選單內的第1個選項,即「磁碟機」彈出選單
       .IF eax==MF_POPUP        ;顯示「磁碟機」彈出選單
                INVOKE  GetLogicalDriveStrings,SIZEOF sLogDrvStr,ADDR sLogDrvStr
           ;刪除原有的「磁碟機」彈出選項所出現的子選單內的所有選項,這是因為有可能使用者
           ;新插入或拔出隨身碟,而造成邏輯磁碟機改變,所以先刪除再重新獲得邏輯磁碟
           .WHILE nLogDrv!=0
                ;每次刪除第零個項目時,下一個項目就變成第零個了
                INVOKE  DeleteMenu,hDrvMenu,0,MF_BYPOSITION
                dec     nLogDrv
           .ENDW
           ;把邏輯磁碟機名稱加入到「磁碟機」彈出選單內
                push    esi
                mov     mii.cbSize,SIZEOF mii
                mov     mii.fMask,MIIM_ID or MIIM_TYPE
                lea     esi,sLogDrvStr
                mov     mii.fType,MFT_STRING
           .WHILE BYTE PTR [esi]!=0
                movzx   edx,BYTE PTR [esi]
                sub     edx,41h
                mov     mii.dwTypeData,esi
                add     edx,IDM_DRVA
                mov     mii.cch,4
                mov     mii.wID,edx     ;A:、B:、C:……選項ID分別是6000、6001、6002、6003……
                INVOKE  InsertMenuItem,hDrvMenu,edx,TRUE,ADDR mii
                add     esi,4
                inc     nLogDrv
           .ENDW
                mov     mii.fMask,MIIM_STATE
                mov     ecx,dwDrv
                mov     mii.fState,MFS_CHECKED
                add     ecx,IDM_DRVA
                INVOKE  SetMenuItemInfo,hDrvMenu,ecx,FALSE,ADDR mii
                pop     esi
       .ENDIF
   .ENDIF
 
.ELSEIF uMsg==WM_INITDIALOG
        ;把對話盒移至螢幕中央
                INVOKE  GetWindowRect,hDlg,ADDR rcDlgOrItm
                mov     edx,rcDlgOrItm.left
                mov     ecx,rcDlgOrItm.top
                sub     rcDlgOrItm.right,edx
                sub     rcDlgOrItm.bottom,ecx
                INVOKE  GetSystemMetrics,SM_CXSCREEN
                sub     eax,rcDlgOrItm.right
                shr     eax,1
                push    eax
                INVOKE  GetSystemMetrics,SM_CYSCREEN
                sub     eax,rcDlgOrItm.bottom
                pop     edx
                shr     eax,1
                INVOKE  MoveWindow,hDlg,edx,eax,rcDlgOrItm.right,rcDlgOrItm.bottom,TRUE
        ;取得主選單及子選單代碼
                INVOKE  GetMenu,hDlg
                mov     hMainMenu,eax           ;主選單代碼
                INVOKE  GetSubMenu,eax,1
                mov     hDrvMenu,eax            ;「磁碟機」子選單代碼
                INVOKE  GetSubMenu,hMainMenu,2
                mov     hViewMenu,eax           ;「檢視」子選單代碼
                INVOKE  GetSubMenu,hViewMenu,0
                mov     hViewFontMenu,eax       ;「字形」子選單代碼
                INVOKE  GetSubMenu,hViewMenu,1
                mov     hViewBgMenu,eax         ;「背景」子選單代碼
        ;設定內定的字形及顏色
                mov     mii.cbSize,SIZEOF mii
                mov     mii.fMask,MIIM_STATE
                mov     mii.fState,MFS_CHECKED
                INVOKE  SetMenuItemInfo,hViewBgMenu,IDM_WHITE,FALSE,ADDR mii
                INVOKE  SetMenuItemInfo,hViewFontMenu,IDM_PMingLiU,FALSE,ADDR mii
                INVOKE  SetMenuItemInfo,hViewFontMenu,IDM_TWELVE,FALSE,ADDR mii
        ;設定圖示
                INVOKE  LoadIcon,hInstance,OFFSET szIconName
                INVOKE  SendMessage,hDlg,WM_SETICON,ICON_SMALL,eax
        ;取得樹狀檢視代碼
                INVOKE  GetDlgItem,hDlg,IDC_TREEVIEW
                mov     hTreeView,eax
        ;插入邏輯磁碟「C:\」
                INVOKE  GetFileAsItem,eax,dwDrv
                mov     hTreeRootItem,eax
        ;取得「Lady」、「Expand」、「Collapse」位元圖的代碼
                INVOKE  LoadBitmap,hInstance,OFFSET szLady
                mov     hbmpLady,eax
                INVOKE  LoadBitmap,hInstance,OFFSET szExpand
                mov     hbmpExpand,eax
                INVOKE  LoadBitmap,hInstance,OFFSET szCollapse
                mov     hbmpCollapse,eax
        ;取得「Lady」圖的寬度與高度
                INVOKE  GetObject,hbmpLady,SIZEOF BITMAP,ADDR bm
                mov     ecx,bm.bmWidth
                mov     edx,bm.bmHeight
                mov     cxLady,ecx
                mov     cyLady,edx
                INVOKE  GetObject,hbmpExpand,SIZEOF BITMAP,ADDR bm
                mov     ecx,bm.bmWidth
                mov     edx,bm.bmHeight
                mov     cxExpand,ecx
                mov     cyExpand,edx
        ;建立黃、白色畫刷
                INVOKE  CreateSolidBrush,0ffffh
                mov     hbrYellow,eax
                INVOKE  CreateSolidBrush,0ffffffh
                mov     hbrWhite,eax
        ;取得樹狀檢視的工作區大小
                INVOKE  GetClientRect,hTreeView,OFFSET rcTreeView
        ;取得樹狀檢視項目縮排多少圖素
                INVOKE  SendMessage,hTreeView,TVM_GETINDENT,0,0
                mov     dwIndent,eax
 
.ELSEIF uMsg==WM_CLOSE
exit:           INVOKE  EndDialog,hDlg,NULL
 
;其他訊息
.ELSE
                mov     eax,FALSE
                ret
.ENDIF
                mov     eax,TRUE
                ret
DlgProc         ENDP
;---------------------------------------------------------------------------------------------------
START:          INVOKE  GetModuleHandle,NULL
                mov     hInstance,eax
                INVOKE  DialogBoxParam,hInstance,OFFSET szDlgName,NULL,OFFSET DlgProc,NULL
                INVOKE  ExitProcess,eax
                INVOKE  InitCommonControls
;***************************************************************************************************
END             START

把 TVCD.ASM、TVCD.RC、TVCD.EXE.MANIFEST、ADD.BMP、SUB.BMP、LADY.BMP、FOLDER2.ICO 七個檔案都放在同一子目錄堙A然後按下面方式組譯、連結即可得到 TVCD.EXE︰

E:\HomePage\SOURCE\Win32\TVCD>rc tvcd.rc

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

 Assembling: tvcd.asm

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

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

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

E:\HomePage\SOURCE\Win32\TVCD>

解說

TVCD 處理 NM_CUSTOMDRAW 的流程

說了這麼多,底下來看看 TVCD 的視窗函式如何處理 NM_CUSTOMDRAW 通知碼。小木偶用三個變數 dwBk、dwFont、dwFontSize 分別表示背景、字形、字體大小,如下︰

72
73
74
dwBk            DWORD           0       ;樹狀檢視的背景:0-白色;1-紅黃漸進;2-黃白交錯;3-美女圖
dwFont          DWORD           0       ;字形:0-新細明體;1-微軟正黑體;2-"Courier New";3-"Times New Roman"
dwFontSize      DWORD           1       ;字體大小:0-10點;1-12點;2-14點

底下的程式片段是 WM_NOTIFY 訊息的處理過程,但只列出大致結構。底下第 6 行,判斷 WM_NOTIFY 是否為樹狀檢視,hTreeView,所發出的,如果是則執行第 6 行到 34 行內的程式。第七行則判斷通知碼是否為 NM_CUSTOMDRAW,如果是,則執行第 7 行到第 33 行的程式。接下來便是重點了,第 9、18、30 行,依照繪製階段,分支執行各相關程式。

258
259
260
261
262
263
264
265
266
267
268
269
270
271
272∼276
277
278
279
280
281
282
283
284∼327
328
329
330∼345
346
347
348
349
350∼359
360
361
362
363
364
.IF uMsg==WM_NOTIFY
                ASSUME  ebx:PTR NM_TREEVIEW
                push    ebx
                mov     ebx,lParam
                mov     eax,[ebx].hdr.hwndFrom  ;EAX=發出WM_NOTIFY的控制項代碼
  .IF eax==hTreeView
     .IF [ebx].hdr.code==NM_CUSTOMDRAW
                ASSUME  ebx:PTR NMTVCUSTOMDRAW
         .IF [ebx].nmcd.dwDrawStage==CDDS_PREPAINT
            .IF dwBk==1
                ;此處畫出樹狀檢視背景為黃色漸進變為紅色
            .ELSEIF dwBk==2
                ;此處畫出樹狀檢視背景為黃、白交錯
            .ELSEIF dwBk==3
                ;此處畫出樹狀檢視背景為美女圖
            .ENDIF
                INVOKE  SetWindowLong,hDlg,DWL_MSGRESULT,CDRF_NOTIFYITEMDRAW OR CDRF_NOTIFYPOSTPAINT
         .ELSEIF [ebx].nmcd.dwDrawStage==CDDS_ITEMPREPAINT
                call    set_font        ;;依據dwFont、dwFontSize設定文字字形與字體大小
            .IF dwBk==0                 ;背景為白色
                mov     edx,CDRF_DODEFAULT
            .ELSEIF (dwBk==1)||(dwBk==3);背景為美女圖或紅黃漸進色
                ;此處畫出項目,包含項目名稱、線…等等
                mov     edx,CDRF_SKIPDEFAULT
            .ELSEIF dwBk==2             ;背景為黃白交錯
                ;此處畫出項目,包含項目名稱…等等
@@:             mov     edx,CDRF_NOTIFYITEMDRAW
            .ENDIF
                INVOKE  SetWindowLong,hDlg,DWL_MSGRESULT,edx
         .ELSEIF [ebx].nmcd.dwDrawStage==CDDS_POSTPAINT
                ;此處繪製結束,需處理捲動時留下殘影的問題
         .ENDIF
     .ENDIF
  .ENDIF
                ASSUME  ebx:NOTHING
                pop     ebx

底下說明如何處理 NM_CUSTOMDRAW 的各個繪製階段 ( 其實只有三個 ),您可以比照上面的程式片段加以對照。

  1. 如果是在 CDDS_PREPAINT 繪製階段,要做的事是先依照 dwBk 內的數值,畫出背景,這個背景是指樹狀檢視的背景。最後在第 278 行返回 CDRF_NOTIFYITEMDRAW OR CDRF_NOTIFYPOSTPAINT,指示樹狀檢視在繪製每個項目之前,也就是 CDDS_ITEMPREPAINT 階段,必須發送 NM_CUSTOMDRAW 通知碼給父視窗;除此之外,在繪製完成所有樹狀檢視之後,也就是 CDDS_POSTPAINT 階段,也要發送 NM_CUSTOMDRAW 通知碼給父視窗。

  2. 因為在 CDDS_PREPAINT 繪製階段返回值包含了 CDRF_NOTIFYITEMDRAW,因此父視窗的視窗函式才能接收到繪製階段為 CDDS_ITEMPREPAINT 的 NM_CUSTOMDRAW 通知碼。在處理 CDDS_ITEMPREPAINT 時,小木偶把字形與背景分開來處理,先處理字形。處理字形的程式在第 280 行,呼叫 set_font 副程式,這個副程式依據 dwFont、dwFontSize 建立邏輯字形,然後選入 [ebx].nmcd.hdc 堙A就算完成了。處理背景的方式是依據 dwBk 決定,值得一提的是,此處的背景其實是指項目名稱文字的裁剪矩形內,扣除文字本身以外的範圍,亦即文字背景不是樹狀檢視的背景。如果是白色背景,事實上就是樹狀檢視的內定值,因此不須處理,直接交給系統去繪製,故返回 CDRF_DODEFAULT,見第 282 行。

    如果 dwBk 是 1 或 3,表示背景是紅色漸進為黃色及美女圖,這兩種情形,最好能使項目名稱的文字背景是透明的,這樣較為好看,像右邊的圖片就是不透明的情形。如果要使文字背景透明,便不能交由系統繪製,必須全部由程式自行繪製,包括項目名稱、根項目前的展開按鈕 ( )、項目前的連接虛線、被選定的項目背景。所以返回 CDRF_SKIPDEFAULT,見第 328 行。如果 dwBk 為 2,表示背景是黃白色互相交錯,本來也可以比照美女圖或紅色漸進為黃色的處理方式,但是為了示範另一種返回值,同時為了方便,所以另外獨立出來。這堜瓵蛌滿u方便」是指返回 CDRF_NOTIFYITEMDRAW 可以讓系統繪製項目名稱、根項目前的展開按鈕 ( )、項目前的連接虛線。而程式要做的事,僅僅是在 NMTVCUSTOMDRAW 結構體的 clrText、clrTextBk 填入適當的顏色就可以了。

  3. 如果是在 CDDS_POSTPAINT 階段,需要處理的是捲動所帶來殘影的問題,這段程式碼在 350∼359 行。為了要能處理 CDDS_POSTPAINT,必須在 CDDS_PREPAINT 繪製階段返回值包含了 CDRF_NOTIFYPOSTPAINT。

CDDS_PREPAINT 時,樹狀檢視的背景為黃色漸進變為紅色

在 CDDS_PREPAINT 繪製階段要畫出黃色漸進變為紅色的背景,是執行第 268 行的「call gradient」,gradient 副程式在第 85∼106 行,程式碼如下。其中灰色的部份是產生相容的設備內容,先在此設備內容媯e出圖形,再將它拷貝到原來的設備內容堙A以防止閃動,但似乎成效不彰,所以僅僅列出並沒有組譯成程式碼。除去這些防止閃動的程式碼之後,當可發現,gradient 副程式的核心是 GradientFill API。

;在樹狀檢視的背景中,畫出由紅漸進變黃色的背景圖
gradient        PROC
                ASSUME  ebx:PTR NMTVCUSTOMDRAW
               ;LOCAL   hdcMem:HDC,hbmMem:HBITMAP
               ;INVOKE  CreateCompatibleDC,[ebx].nmcd.hdc
               ;mov     hdcMem,eax
               ;INVOKE  CreateCompatibleBitmap,[ebx].nmcd.hdc,[ebx].nmcd.rc.right,[ebx].nmcd.rc.bottom
               ;mov     hbmMem,eax
               ;INVOKE  SelectObject,hdcMem,eax
                mov     edx,SIZEOF TRIVERTEX
                mov     eax,[ebx].nmcd.rc.right
                mov     ecx,[ebx].nmcd.rc.bottom
                mov     ary_tvt[edx].x,eax
                mov     ary_tvt[edx].y,ecx
                INVOKE  GradientFill,[ebx].nmcd.hdc,OFFSET ary_tvt,2,OFFSET grect,1,GRADIENT_FILL_RECT_H
               ;INVOKE  BitBlt,[ebx].nmcd.hdc,0,0,[ebx].nmcd.rc.right,[ebx].nmcd.rc.bottom,hdcMem,0,0,SRCCOPY
               ;INVOKE  DeleteDC,hdcMem
               ;INVOKE  DeleteObject,hbmMem
                ASSUME  ebx:NOTHING
                ret
gradient        ENDP

GradientFill API

GradientFill API 是用漸變顏色去畫滿矩形區域或者三角形區域。因為 GradientFill 在 MSIMG32.DLL 內部,因此要呼叫這個 API,必須包含 MSIMG32.INC 與 MSIMG32.LIB。它的原型是:

BOOL GradientFill(
  HDC           hdc,            // handle to DC
  PTRIVERTEX    pVertex,        // array of vertices
  ULONG         dwNumVertex,    // number of vertices
  PVOID         pMesh,          // array of gradients
  ULONG         dwNumMesh,      // size of gradient array
  ULONG         dwMode          // gradient fill mode
);

RadientFill 可以同時畫滿數個矩形或數個三角形,依據 pVertex、dwNumVertex、pMesh、dwNumMesh 來決定,而所畫的是矩形還是三角形,由 dwMode 決定。底下是 RadientFill 參數的說明:

  1. hdc 是要畫滿的矩形或三角形區域所在的設備內容 ( device context )。
  2. pVertex 是數個 TRIVERTEX 結構體所組成的陣列之位址,TRIVERTEX 結構體是用來存放各頂點的位置及顏色資料,每一個 TRIVERTEX 的結構體欄位如下︰
    TRIVERTEX       STRUCT
    x       DD      ?
    y       DD      ?
    Red     DW      ?
    Green   DW      ?
    Blue    DW      ?
    Alpha   DW      ?
    TRIVERTEX       ENDS
    很明顯的,每一個 TRIVERTEX 結構體代表著一個點,x、y 分別是其橫座標與縱座標,Red、Green、Blue 分別代表三原色,Alpha 是透明度。 GradientFill 所畫出來的顏色漸進改變的矩形,只能是正立擺放的,亦即有兩個對邊是水平,另兩個對邊是鉛垂的,所以只需要兩個 TRIVERTEX,分別代表矩形左上角及右下角的座標,這兩個 TRIVERTEX 結構體就組成含有兩個元素的陣列;如果要畫出一個三角形,就需要三個 TRIVERTEX,分別代表三個頂點,這三個 TRIVERTEX 就組成含有三個元素的陣列。
  3. dwNumVertex 是 pVertex 所指之陣列中所含結構體個數,亦即有幾個點。
  4. pMesh 有兩種情形︰如果您想填充漸變色的矩形,那麼 pMesh 是由 GRADIENT_RECT 結構體或所組成的陣列;如果您想填充漸變色的三角形,那麼 pMesh 是由 GRADIENT_TRIANGLE 結構體所組成的陣列。這兩個結構體的欄位是︰
    GRADIENT_RECT   STRUCT
    UpperLeft       DD      ?
    LowerRight      DD      ?
    GRADIENT_RECT   ENDS
    GRADIENT_TRIANGLE       STRUCT
    Vertex1         DD      ?
    Vertex2         DD      ?
    Vertex3         DD      ?
    GRADIENT_TRIANGLE       ENDS
    如前所述,矩形只需左上角與右下角兩個點,所以 GRADIENT_RECT 結構體有兩個欄位;三角形有三個頂點,所以 GRADIENT_TRIANGLE 結構體有三個欄位。這兩個結構體的欄位是表示 pVertex 參數所指位址的結構體陣列中,哪些元素組成矩形或三角形。
  5. dwNumMesh 是 pMesh 所指的位址有幾個結構體,亦即有幾個三角形或矩形。
  6. dwMode 是指示 GradientFill 如何填充矩形或三角形,可以是下表中的其中一種︰
    填充方式數值說  明
    GRADIENT_FILL_RECT_H0由左邊的顏色漸進變為右邊的顏色
    GRADIENT_FILL_RECT_V1由上面的顏色漸進變為下面的顏色
    GRADIENT_FILL_TRIANGLE2填充三角形

講了這麼多 GradientFill 的用法,不看一些例子,是無法了解的。這個例子最重要的就是 pVertex 所指的 TRIVERTEX 陣列如何與 pMesh 所指陣列相配合。底下的程式碼所做出來的效果如右圖所示 ( 圖中的黑點與座標是小木偶加上的註解 )︰

ary_tvt TRIVERTEX           <0,0,0ffffh,0,0,0>,<0,50,0ffffh,0ffffh,0,0>,<50,50,0ff00h,0ff00h,0,0>
        TRIVERTEX           <100,100,0,0,0ffffh,0>,<0,100,0,0ffffh,0,0>,<50,100,0ffffh,0,0ffffh,0>
ary_gt  GRADIENT_TRIANGLE   <0,1,2>,<1,4,5>
        INVOKE  GradientFill,hDC,OFFSET ary_tvt,6,OFFSET ary_gt,2,GRADIENT_FILL_TRIANGLE

這個例子堙A小木偶畫出兩個三角形,所以 dwNumMesh 為 2,因為兩個三角形有 6 個頂點,所以 dwNumVertex 為 6。又因為是畫三角形,所以 dwMode 為 GRADIENT_FILL_TRIANGLE,當系統收到 GRADIENT_FILL_TRIANGLE 後,就會把 pMesh 當成是 GRADIENT_TRIANGLE 結構體所組成的陣列。一個三角形由哪些頂點組成,由 pMesh 所指的 GRADIENT_TRIANGLE 結構體陣列指定。第一個 GRADIENT_TRIANGLE 為 <0,1,2>,表示在 pVertex 所指的結構體陣列中的第 0、1、2 個結構體構成此三角形三個頂點;第二個 GRADIENT_TRIANGLE 為 <1,4,5>,表示在 pVertex 所指的結構體陣列中的第 1、4、5 個結構體形成第二個三角形。

CDDS_PREPAINT 時,樹狀檢視的背景為黃、白交錯的矩形

在 CDDS_PREPAINT 繪製階段要畫出黃、白色交錯的矩形作為背景,此處的背景是指樹狀檢視中,工作區的背景。畫出黃白交錯的方法是把每個項目所在之處,看成是一個一個的長條狀矩形,由樹狀檢視工作區的最左邊延伸到最右邊,並且使每個矩形相間塗滿黃色或白色。實際的程式碼是執行第 270 行的「call interlock」,interlock 副程式在第 160∼192 行,程式碼如下:

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
;背景由白色與黃色交錯而成
interlock       PROC
                LOCAL   rcExtItem:RECT
                LOCAL   dwItemHight,nItem,n:DWORD
                INVOKE  SendMessage,hTreeView,TVM_GETITEMHEIGHT,0,0
                mov     ecx,eax
                sub     edx,edx
                mov     eax,rcTreeView.bottom
                div     ecx
                mov     dwItemHight,ecx
                mov     nItem,eax
                mov     n,0
                mov     edx,rcTreeView.right
                mov     rcExtItem.left,0
                mov     rcExtItem.right,edx
                mov     rcExtItem.top,0
                mov     rcExtItem.bottom,ecx
            .WHILE nItem>0
                mov     ecx,hbrWhite
                test    n,1
                jz      @f
                mov     ecx,hbrYellow
                ASSUME  ebx:PTR NMTVCUSTOMDRAW
@@:             INVOKE  FillRect,[ebx].nmcd.hdc,ADDR rcExtItem,ecx
                ASSUME  ebx:NOTHING
                mov     ecx,dwItemHight
                add     rcExtItem.bottom,ecx
                add     rcExtItem.top,ecx
                inc     n
                dec     nItem
            .ENDW
                ret
interlock       ENDP

第 164 行取得每個項目的高度 ( 有幾個點,pixel ),存於 ECX 堙A然後把樹狀檢視的高度除以 ECX,就可以得到樹狀檢視中可以看到幾個項目,把它存於 nItem ( 第 170 行 )。第 171 行,把區域變數,n,設為 0,這個變數將在底下第 177∼190 行迴圈中,由零遞增。程式在第 179 行檢查 n 之數值,若為偶數,則使矩形畫滿白色,亦即使用白色畫刷;反之則使用黃色畫刷。把矩形塗上顏色的方法是呼叫 FillRect,如果您仔細觀察,應當可以發現,每個矩形左上角及右下角的橫座標並不會改變,只有縱座標會比上個矩形多了項目的高度,第 185∼187 行的程式碼就是實現設定下一個矩形的範圍。至於橫座標與縱座標的初始值,在第 172∼176 行設定,之後,橫座標之值不改變。

CDDS_PREPAINT 時,樹狀檢視的背景為美女圖

在 CDDS_PREPAINT 繪製階段要畫出美女圖背景,程式碼在第 271∼276 行。這段程式碼應該是很簡單的,原理在第 13 章,請自行參考。這張美女圖,已存在資源檔堙A主要的步驟是首先在記憶體中用 CreateCompatibleDC 另外再建立與樹狀檢視的設備內容一模一樣的設備內容,接著把美女圖選入記憶體內的設備內容,最後再經由 BitBlt 複製到樹狀檢視的設備內容。

271
272
273
274
275
276
            .ELSEIF dwBk==3     ;背景為美女圖
                INVOKE  CreateCompatibleDC,[ebx].nmcd.hdc
                mov     hdcMem,eax
                INVOKE  SelectObject,hdcMem,hbmpLady                                    ;選定來源設備內容的位元圖
                INVOKE  BitBlt,[ebx].nmcd.hdc,0,0,cxLady,cyLady,hdcMem,0,0,SRCCOPY      ;傳送位元圖到視窗的設備內容
                INVOKE  DeleteDC,hdcMem

CDDS_ITEMPREPAINT 時,背景為黃色漸變為紅色或美女圖

如果背景為黃色漸變為紅色或美女圖,在處理 CDDS_ITEMPREPAINT 時,因為要使得項目名稱的文字背景為透明的,才能顯現背景圖案。要達到這個效果,程式就得進行全部事情的繪製,包含項目名稱、根項目前的展開按鈕 ( )、項目前的連接虛線、被選定的項目背景。最後返回時,返回值為 CDRF_SKIPDEFAULT。這段過程所有的程式碼在第 283∼328 行。所以這種情形的返回值為 CDRF_SKIPDEFAULT。底下一段一段解說。程式第 284 行是先設定文字背景為透明的,方法是呼叫 SetBkMode API。SetBkMode API 是用來設定文字的背景、非 PS_SOLID 畫筆、某些畫刷中背景的模式。某些畫筆 ( 例如虛線畫筆 ) 或畫刷 ( 斜線圖案 ) 是可以有背景的,文字也是有背景的,SetBkMode 可以顯這些背景 ( 即不透明 )或是不顯示 ( 即透明 )。SetBkMode 原型為:

int SetBkMode(
  HDC hdc,      // handle of device context
  int iBkMode   // flag specifying background mode
);

iBkMode 可以有兩種選擇,一是 OPAQUE 為不透明的,亦即顯示文字、非 PS_SOLID 畫筆、某些畫刷中背景顏色,這樣就看不見樹狀檢視的背景圖案了。第二是 TRANSPARENT,不顯示文字、非 PS_SOLID 畫筆、某些畫刷中背景顏色,這樣能看見樹狀檢視的背景圖案了。

程式第 385∼292 行主要是呼叫 SendMessage,把 TVM_GETITEM 傳給樹狀檢視,以獲得項目的資料,包含項目名稱、狀態、是否有子項目,稍後即將要用到這些資料。第 293∼295 行,檢查是否為根項目,如果不是才要畫出項目前的虛線。畫出項目前的虛線的副程式為 draw_line,在程式第 211∼249 行,並不會太難,小木偶不解說。不管是否為根項目,都會執行到第 296 行的「print_icon:」,從第 296 行到第 307 行,判斷是否有子項目,如果沒有子項目則直接跳到第 308 行;如果有子項目,則必須判斷要畫出 還是 圖案,這兩張 BITMAP 圖案都已存在資源檔堣F,所以其步驟與畫出美女圖背景是一樣的。至於畫出哪一個,則視該項目的狀態是否為展開來決定 ( 第 301∼302 行 )。畫出圖案的程式碼在第 304∼306 行。

接下來的第 308∼318 行則是畫出項目名稱,方法是呼叫 DrawText。項目名稱所在的剪裁矩形在 NMCUSTOMDRAW 的 rc 欄位堣w有了大致的位置,但是要考慮到子項目的縮排,必須調整左上角的橫座標。總共縮排幾點 ( pixel ) 與項目在第幾層有關,在 NMTVCUSTOMDRAW 結構體的 iLevel 欄位堸O載著項目所在層數,把它乘以每個項目縮排點數就可以獲得總共要縮排幾點。但是考慮到根項目是第零層,所以先把 iLevel 加一,再乘以每個項目縮排點數。每個項目縮排的點數在第 540∼541 行獲得。得到總共縮排幾點後,加上原先在 NMCUSTOMDRAW 的 rc 堛漸炊W角的橫座標就是剪裁矩形的左上角橫座標,左上角的縱座標由 NMTVCUSTOMDRAW 結構體的 rc.top 取得,然後設定 DT_CALCRECT,呼叫 DrawText,可以計算出剪裁矩形。第 319∼322 行是用來判斷該項目是否被選中,如果不是直接呼叫 DrawText 印出項目名稱;如果被選中,則先把剪裁矩形塗上高亮度的背景色,然後再印出項目名稱。

最後返回時,返回 CDRF_SKIPDEFAULT,這是因為背景為黃色漸變為紅色或美女圖,程式必須畫出所有的畫面。

CDDS_ITEMPREPAINT 時,背景為黃、白色交錯

當背景為黃白色交錯時,只需要在 NMTVCUSTOMDRAW 結構體的 clrText、clrTextBk 兩個欄位中填入適當的顏色值,系統就能幫我們塗滿項目名稱文字的剪裁矩形,以及文字顏色。共分兩種情形:

  1. 如果該項目是被選中的,那麼剪裁矩形的背景是高亮度的背景,文字顏色則是高亮度的顏色。取得這兩種顏色的方式是呼叫 GetSysColor,見第 330∼333 行。然後判斷是否為被選定的項目,如果是則跳到第 346 行,返回系統。
  2. 如果不是被選中的項目,還要判斷該項目在可見到的樹狀檢視內,是奇數行還是偶數行,這段程式碼在第 336∼342 行。方法是把項目所在的左上角縱座標除以每個項目的高度,所得的商如為偶數則剪裁矩形的背景為白色;否則為黃色。文字顏色均為黑色。

CDDS_POSTPAINT 階段

如果樹狀檢視捲動時,包含使用者轉動滑鼠上的滾輪、使用者移動捲軸…等等,都會造成背景圖案變調,這是捲動時所造成的殘影。解決這個問題的方法很簡單,只要呼叫 InvalidateRect,重新畫一遍即可。不過樹狀檢視捲動時,不會產生通知碼,那麼如何得知捲動事件呢?這個問題困擾小木偶很久,還好最後解決了。小木偶使用的方法是呼叫 GetScrollPos,檢查捲軸操縱桿的位置,如果與之前不同,表示發生捲動事件,這時就呼叫 InvalidateRect 重新畫出樹狀檢視。檢查捲軸的操縱桿,必須水平捲軸與鉛垂捲軸各檢查一次。見第 349∼360 行。

349
350
351
352
353
354
355
356
357
358
359
360
         .ELSEIF [ebx].nmcd.dwDrawStage==CDDS_POSTPAINT
                INVOKE  GetScrollPos,hTreeView,SB_HORZ
            .IF eax!=xScroll
                mov     xScroll,eax
                jmp     redraw
            .ENDIF
                INVOKE  GetScrollPos,hTreeView,SB_VERT
            .IF eax!=yScroll
                mov     yScroll,eax
redraw:         INVOKE  InvalidateRect,hTreeView,0,FALSE
            .ENDIF
         .ENDIF

GetScrollPos API

GetScrollPos 的原型是:

int GetScrollPos ( HWND hWnd,
                   int nBar
);

GetScrollPos 的功用是傳回捲軸操縱桿的位置,如果呼叫成功傳回值即為操縱桿位置;否則返回 0。雖然微軟建議以 GetScrollInfo 取代 GetScrollPos,但是至少在 Windows 7中,還可以使用,底下說明 GetScrollPos 用法。hWnd 是捲軸控制項代碼或是擁有標準捲軸的視窗代碼,nBar 是指定傳回哪一個捲軸操縱桿的位置,可以是底下的其中一個:

旗標數值說 明
SB_HORZ0 取得水平捲軸的位置,用於擁有標準捲軸的視窗
SB_VERT1 取得垂直捲軸的位置,用於擁有標準捲軸的視窗
SB_CTL2 取得捲軸控制項的位置。如果此時 hWnd 不是捲軸控制項代碼,那麼此視窗會收到 SBM_GETPOS 訊息,如果不處理它,GetScrollPos 就會產生錯誤

見上面第 350 行,以 SB_HORZ 呼叫 GetScrollPos 後,把傳回值與 xScroll 比較,如果不同表示操縱桿位置改變,把新的位置存到 xScroll 堙A同時跳到 redraw: 標號處呼叫 InvalidateRect。如果相同,則檢查垂直捲軸的操縱桿位置,步驟與剛剛相同,小木偶就不再描述了。

WM_MENUSELECT

在大部分的應用程式堙A常在程式開始執行時就已確定了磁碟機的多寡以及磁碟機編號 ( A:\、B:\、C:\…等稱為磁碟機編號 )。不過隨著隨身碟的使用,可熱插拔的儲存裝置越來越流行,有可能應用程式已經執行一段時間,使用者才插入 USB 隨身碟,因此不宜在 WM_CREATE 或 WM_INITDIALOG 訊息處理中決定磁碟機多寡。TVCD.ASM 程式是在處理 WM_MENUSELECT 訊息時,才即時取得磁碟機多寡。當使用者選擇選單中的某個選項時,Windows 系統會對該視窗發出 WM_MENUSELECT 訊息,程式可以在該視窗的視窗函式中做一些事情。當 Windows 系統發出 WM_MENUSELECT 訊息時,其所攜帶的 lParam 是被選定的選單代碼,wParam 較高的 16 位元是下表中的一個或是數個的聯集:

旗標數值說 明
MF_BITMAP04H 此選項是點陣圖
MF_CHECKED8H 此選項前有勾選記號
MF_DISABLED2H 此選項是被禁用
MF_GRAYED1H 此選項呈現灰色
MF_HILITE80H 此選項是被選定的
MF_MOUSESELECT8000H 此選項被滑鼠選定
MF_OWNERDRAW100H 此選項為 owner-drawn
MF_POPUP10H 此選項為彈出選項,會顯示一個子選單
MF_SYSMENU2000H 此選項包含在系統選單內

wParam 較低的 16 位元是選項識別碼或子選單代碼。如果被選定的項目是一般選項,那 wParam 較低的 16 位元是被選定的選項識別碼;如果是彈出選項,那 wParam 較低的 16 位元是被選定的選項索引 ( 即選項的位置 ),由 0 開始。

TVCD.ASM 的第 429∼436 行在處理 WM_MENUSELECT 時,使 ECX 為 wParam 的低十六位元,EAX 為高十六位元。在 435、436 行檢查使用者是否選擇第一個位置 ( 即「磁碟機」選項 ),再檢查是否為 MF_POPUP,如果都符合,則進行彈出選單的更新。見下面程式碼。

429
430
431
432
433
434
435
436
.ELSEIF uMsg==WM_MENUSELECT
                mov     eax,wParam
                mov     ecx,eax
                shr     eax,10h
                and     ecx,0ffffh
                and     eax,MF_POPUP
   .IF ecx==1                   ;ECX=主選單內的第1個選項,即「磁碟機」彈出選單
       .IF eax==MF_POPUP        ;顯示「磁碟機」彈出選單

更新彈出選單的方法是先刪除原有的「磁碟機」彈出選單所出現的子選單內的所有選項,然後再呼叫 GetLogicalDriveStrings 取得現在所有的邏輯磁碟機,再把這些邏輯磁碟機加入彈出選單堙C這段程式不難,所以也不詳細解釋了。


到第二十四章回到首頁到第二十六章