Windows 95 相較於 Windows 3.x 最大的突破之一,在於引入了一套豐富的「通用控件」。這些控件如工具列、狀態列、樹狀檢視、列表檢視、進度條、分頁控件等,早在 Windows 95 正式釋出前已在系統內部使用。過去程式設計師必須自行撰寫複雜代碼才能實現這些介面元素,如今 Microsoft 將它們封裝在 comctl32.dll 中,任何應用程式皆可輕鬆調用,極大地降低了開發門檻。
通用控件種類繁多,以下列出常用控件及其對應的窗口類別名稱(以 VC++ 資源編輯器為準,精確可靠):
| 類別名稱 | 控件名稱 |
|---|---|
| ToolbarWindow32 | 工具列 |
| tooltips_class32 | 提示文字 |
| msctls_statusbar32 | 狀態列 |
| SysTreeView32 | 樹狀檢視 |
| SysListView32 | 列表檢視 |
| SysAnimate32 | 動畫控件 |
| SysHeader32 | 表頭控件 |
| msctls_hotkey32 | 熱鍵控件 |
| msctls_progress32 | 進度條 |
| RICHEDIT | 豐富文字編輯 |
| msctls_updown32 | 微調控件 |
| SysTabControl32 | 分頁控件 |
注意:Property Sheet、Property Page、Image List、Drag List 等控件擁有專屬建立函式,不依賴標準類別名稱。
為避免一次性將所有控件載入記憶體造成浪費,Windows 採用動態載入策略。除 RTF 編輯控件(位於 richedxx.dll)需手動呼叫 LoadLibrary/FreeLibrary 外,其餘控件皆在 comctl32.dll 中。
呼叫 InitCommonControls 函式的實際作用極其巧妙:該函式本體僅有一條 ret 指令,真正目的是讓 PE Loader 在載入應用程式時,於 PE 標頭的「匯入表」中註冊 comctl32.dll。如此,系統便會自動載入該 DLL,並在其進入點註冊所有通用控件類別,後續即可像建立普通子視窗一樣建立控件。
幾乎所有通用控件均可透過 CreateWindowEx 或 CreateWindow 建立,只需傳入正確的類別名稱。為了方便開發,系統提供了封裝函式:
CreateToolbarEx - 建立工具列CreateStatusWindow - 建立狀態列CreatePropertySheetPage / PropertySheet - 屬性頁ImageList_Create - 影像清單通用控件與父視窗的通訊不再使用傳統的 WM_COMMAND,改以 WM_NOTIFY 訊息傳遞複雜的通知資訊(透過 NMHDR 結構)。父視窗則可發送專屬訊息(如 PBM_SETRANGE、SB_SETTEXT)控制控件行為。各控件擁有專屬訊息集合,詳情請參閱 Win32 API 文件。
以下完整 MASM 程式碼展示如何建立主視窗、初始化通用控件、建立進度條與狀態列,並利用計時器驅動進度條遞增,完成後彈出訊息框並重置介面。
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comctl32.inc
includelib \masm32\lib\comctl32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD
.const
IDC_PROGRESS equ 1
IDC_STATUS equ 2
IDC_TIMER equ 3
.data
ClassName db "CommonControlWinClass",0
AppName db "Common Control Demo",0
ProgressClass db "msctls_progress32",0
Message db "Finished!",0
TimerID dd 0
.data?
hInstance HINSTANCE ?
hwndProgress dd ?
hwndStatus dd ?
CurrentStep dd ?
.code
start:
invoke GetModuleHandle, NULL
mov hInstance, eax
invoke WinMain, hInstance, NULL, NULL, SW_SHOWDEFAULT
invoke ExitProcess, eax
invoke InitCommonControls ; 故意置於 ExitProcess 之後,驗證僅為匯入表註冊
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 hInst
pop wc.hInstance
mov wc.hbrBackground, COLOR_APPWORKSPACE
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, WS_EX_CLIENTEDGE, ADDR ClassName, ADDR AppName, \
WS_OVERLAPPED or WS_CAPTION or WS_SYSMENU or WS_MINIMIZEBOX or WS_MAXIMIZEBOX or WS_VISIBLE, \
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, \
NULL, NULL, hInst, NULL
mov hwnd, eax
.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_CREATE
; 建立進度條
invoke CreateWindowEx, NULL, ADDR ProgressClass, NULL, \
WS_CHILD or WS_VISIBLE, 100, 200, 300, 20, \
hWnd, IDC_PROGRESS, hInstance, NULL
mov hwndProgress, eax
mov eax, 1000
mov CurrentStep, eax
shl eax, 16
invoke SendMessage, hwndProgress, PBM_SETRANGE, 0, eax ; 設定範圍 0-1000
invoke SendMessage, hwndProgress, PBM_SETSTEP, 10, 0 ; 步進 10
; 建立狀態列
invoke CreateStatusWindow, WS_CHILD or WS_VISIBLE, NULL, hWnd, IDC_STATUS
mov hwndStatus, eax
; 建立計時器,每 100ms 觸發
invoke SetTimer, hWnd, IDC_TIMER, 100, NULL
mov TimerID, eax
.elseif uMsg == WM_DESTROY
invoke PostQuitMessage, NULL
.if TimerID != 0
invoke KillTimer, hWnd, TimerID
.endif
.elseif uMsg == WM_TIMER
invoke SendMessage, hwndProgress, PBM_STEPIT, 0, 0
sub CurrentStep, 10
.if CurrentStep == 0
invoke KillTimer, hWnd, TimerID
mov TimerID, 0
invoke SendMessage, hwndStatus, SB_SETTEXT, 0, addr Message
invoke MessageBox, hWnd, addr Message, addr AppName, MB_OK or MB_ICONINFORMATION
invoke SendMessage, hwndStatus, SB_SETTEXT, 0, 0
invoke SendMessage, hwndProgress, PBM_SETPOS, 0, 0
.endif
.else
invoke DefWindowProc, hWnd, uMsg, wParam, lParam
ret
.endif
xor eax, eax
ret
WndProc endp
end start
程式刻意將 InitCommonControls 放在 ExitProcess 之後。即使該函式從未真正執行,程式仍能正常運作,證明其唯一作用確為在 PE 匯入表中註冊 comctl32.dll,由系統載入器自動完成 DLL 載入與類別註冊。
PBM_SETRANGE 的 lParam 低字為最小值、高字為最大值。範例設定 0~1000,步進 10,故需 100 步完成。亦可用 PBM_SETPOS 直接設定位置。
SetTimer 建立 100ms 間隔計時器,WM_TIMER 處理程式中呼叫 PBM_STEPIT 遞增進度條。當 CurrentStep 歸零時,殺死計時器、顯示完成訊息於狀態列、彈出對話框,最後重置進度條與狀態列文字。
通用控件是 Windows 視窗程式設計的基石。掌握其載入機制、類別名稱、建立方式與 WM_NOTIFY 通訊模型,即可輕鬆構建專業級使用者介面。本文配合完整 MASM 範例,從底層機制到上層應用完整呈現,為深入學習 Windows 核心程式設計奠定堅實基礎。