Tutorial 18: Common Controls
We will learn what common controls are and how to use them.
This tutorial will be a quick introduction to them only.
Download the example source code here.
Theory:
Windows 95 comes with several user-interface enhancements over Windows 3.1x. They
make the GUI richer. Several of them are in widely used before Windows 95 hit
the shelf, such as status bar, toolbars etc. Programmers have to code them themselves.
Now Microsoft has included them with Windows 9x and NT. We will learn about them
here.
These are the new controls:
- Toolbar
- Tooltip
- Status bar
- Property sheet
- Property page
- Tree view
- List view
- Animation
- Drag list
- Header
- Hot-key
- Image list
- Progress bar
- Right edit
- Tab
- Trackbar
- Up-down
Since there are many of them, loading them all into memory and registering them
would be a waste of resource. All of them, with the exception of rich edit control,
are stored in comctl32.dll with applications can load when they want to use the
controls. Rich edit control resides in its own dll, richedXX.dll, because it's
very complicated and hence larger than its brethren.
You can load comctl32.dll by including a call to InitCommonControls in
your program. InitCommonControls is a function in comctl32.dll, so referring to
it anywhere in your code will make PE loader load comctl32.dll when your program
runs.You don't have to execute it, just include it in your code somewhere.
This function does NOTHING! Its only instruction is "ret". Its sole purpose
is to include reference to comctl32.dll in the import section so that PE loader
will load it whenever the program is loaded. The real workhorse is the DLL entrypoint
function which registers all common control classes when the dll is loaded. Common
controls are created based on those classes just like other child window controls
such as edit, listbox etc.
Rich edit is another matter entirely. If you want to use it, you have to call
LoadLibrary to load it explicitly and call FreeLibrary to unload it.
Now we learn how to create them. You can use a resource editor to incorporate
them into dialog boxes or you can create them yourself. Nearly all common controls
are created by calling CreateWindowEx or CreateWindow, passing it the name of
the control class. Some common controls have specific creation functions , however,
they are just wrappers around CreateWindowEx to make it easier to create those
controls. Existing specific creation functions are listed below:
- CreateToolbarEx
- CreateStatusWindow
- CreatePropertySheetPage
- PropertySheet
- ImageList_Create
In order to create common controls, you have to know their class names. They are
listed below:
Class Name
|
Common Control
|
ToolbarWindow32 |
Toolbar |
tooltips_class32 |
Tooltip |
msctls_statusbar32 |
Status bar |
SysTreeView32 |
Tree view |
SysListView32 |
List view |
SysAnimate32 |
Animation |
SysHeader32 |
Header |
msctls_hotkey32 |
Hot-key |
msctls_progress32 |
Progress bar |
RICHEDIT |
Rich edit |
msctls_updown32 |
Up-down |
SysTabControl32 |
Tab |
Property sheets and property pages and image list control have their own specific
creation functions. Drag list control are souped-up listbox so it doesn't have
its own class. The above class names are verified by checking resource script
generated by Visual C++ resource editor. They differ from the class names listed
by Borland's win32 api reference and Charles Petzold's Programming Windows 95.
The above list is the accurate one.
Those common controls can use general window styles such as WS_CHILD etc. They
also have their own specific styles such as TVS_XXXXX for tree view control, LVS_xxxx
for list view control, etc. Win32 api reference is your best friend in this regard.
Now that we know how to create common controls, we can move on to communication
method between common controls and their parent. Unlike child window controls,
common controls don't communicate with the parent via WM_COMMAND. Instead they
send WM_NOTIFY messages to the parent window when some interesting events occur
with the common controls. The parent can control the children by sending messages
to them. There are also many new messages for those new controls. You should consult
your win32 api reference for more detail.
Let's examine progress bar and status bar controls in the following example.
Sample code:
.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
; control IDs
IDC_STATUS equ 2
IDC_TIMER equ 3
.data
ClassName db "CommonControlWinClass",0
AppName db "Common Control Demo",0
ProgressClass db "msctls_progress32",0
; the class name of the progress bar
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
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+WS_CAPTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEBOX+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+WS_VISIBLE,100,\
200,300,20,hWnd,IDC_PROGRESS,\
hInstance,NULL
mov hwndProgress,eax
mov eax,1000
; the lParam of PBM_SETRANGE message contains the range
mov CurrentStep,eax
shl eax,16
; the high range is in the high word
invoke SendMessage,hwndProgress,PBM_SETRANGE,0,eax
invoke SendMessage,hwndProgress,PBM_SETSTEP,10,0
invoke CreateStatusWindow,WS_CHILD+WS_VISIBLE,NULL,hWnd,IDC_STATUS
mov hwndStatus,eax
invoke SetTimer,hWnd,IDC_TIMER,100,NULL
; create a timer
mov TimerID,eax
.elseif uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.if TimerID!=0
invoke
KillTimer,hWnd,TimerID
.endif
.elseif uMsg==WM_TIMER
; when a timer event occurs
invoke SendMessage,hwndProgress,PBM_STEPIT,0,0
; step up the progress in the progress bar
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+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
Analysis:
invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
invoke ExitProcess,eax
invoke InitCommonControls
I deliberately put InitCommonControls after ExitProcess to demonstrate that InitCommonControls
is just there for putting a reference to comctl32.dll in the import section. As
you can see, the common controls work even if InitCommonControls doesn't execute.
.if uMsg==WM_CREATE
invoke CreateWindowEx,NULL,ADDR
ProgressClass,NULL,\
WS_CHILD+WS_VISIBLE,100,\
200,300,20,hWnd,IDC_PROGRESS,\
hInstance,NULL
mov hwndProgress,eax
Here is where we create the common control. Note that this CreateWindowEx call
contains hWnd as the parent window handle. It also specifies a control ID for
identifying this control. However, since we have the control's window handle,
this ID is not used. All child window controls must have WS_CHILD style.
mov eax,1000
mov CurrentStep,eax
shl eax,16
invoke SendMessage,hwndProgress,PBM_SETRANGE,0,eax
invoke SendMessage,hwndProgress,PBM_SETSTEP,10,0
After the progress bar is created, we can set its range. The default range is
from 0 to 100. If you are not satisfied with it, you can specify your own range
with PBM_SETRANGE message. lParam of this message contains the range, the maximum
range is in the high word and the minimum one is in the low word. You can specify
how much a step takes by using PBM_SETSTEP message. The example sets it to 10
which means that when you send a PBM_STEPIT message to the progress bar, the progress
indicator will rise by 10. You can also set your own indicator level by sending
PBM_SETPOS messages. This message gives you tighter control over the progress
bar.
invoke CreateStatusWindow,WS_CHILD+WS_VISIBLE,NULL,hWnd,IDC_STATUS
mov hwndStatus,eax
invoke SetTimer,hWnd,IDC_TIMER,100,NULL
; create a timer
mov TimerID,eax
Next, we create a status bar by calling CreateStatusWindow. This call is easy
to understand so I'll not comment on it. After the status window is created, we
create a timer. In this example, we will update the progress bar at a regular
interval of 100 ms so we must create a timer control. Below is the function prototype
of SetTimer.
SetTimer PROTO hWnd:DWORD, TimerID:DWORD, TimeInterval:DWORD, lpTimerProc:DWORD
hWnd : Parent window handle
TimerID : a nonzero timer identifier. You can create your own identifier.
TimerInterval : the timer interval in milliseconds that must pass before
the timer calls the timer procedure or sends a WM_TIMER message
lpTimerProc : the address of the timer function that will be called when
the time interval expires. If this parameter is NULL, the timer will send WM_TIMER
message to the parent window instead.
If this call is successful, it will return the TimerID. If it failed, it returns
0. This is why the timer identifer must be a nonzero value.
.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+MB_ICONINFORMATION
invoke
SendMessage,hwndStatus,SB_SETTEXT,0,0
invoke
SendMessage,hwndProgress,PBM_SETPOS,0,0
.endif
When the specified time interval expires, the timer sends a WM_TIMER message.
You will put your code that will be executed here. In this example, we update
the progress bar and then check if the maximum limit has been reached. If it has,
we kill the timer and then set the text in the status window with SB_SETTEXT message.
A message box is displayed and when the user clicks OK, we clear the text in the
status bar and the progress bar.