A Basic Win32 GUI Program--WINBASIC.ASM

This program produces a basic application window. You can move the window around. You can change the window's size and all the usual buttons appear and are functional. The only things missing are scroll bars and a menu bar. [ More programming information ]
[ Back to Win32 ASM Page ]

Program startup

For single-threaded programs the initial values of the registers are almost of no importance. But just in case you're interested: CS:EIP = start of program; SS:ESP = start of stack; DS = ES = SS; FS = TIB the thread information block; GS = 0 the null selector. CS and DS map to the same linear address. The direction flag DF is cleared.
    [More on program startup.]

We start our GUI app in the same manner as the Console App.

model  flat

; If using TLINK32
don't include vclib.inc
include vclib.inc    ; Microsoft VC++ .lib link names

include win32hst.inc ; constants
and entry names

public _start


The window class

Every window is described by a run-time data structure known as a window class. (This "class" is not a C++ class.) Every window class has a callback function known as a window procedure that defines most of the behavior of all windows created with this class.
    Before we can create a window its window class defined by a WNDCLASSEX structure must be given a name and registered by calling RegisterClassEx. Windows preregisters a few window classes but these are usually used for creating controls.
    First let's look at the WNDCLASSEX structure defined in WIN32HST.INC. For ease in translating to other assemblers I have prefixed each field with what I call the structure's "signature". These signatures are shown as comments in the .h header files for C. The remainder of the field name after the underscore is exactly the same as in the C header files.
wcx_cbSize              dd ?
wcx_style               dd ?
wcx_lpfnWndProc         dd ?
wcx_cbClsExtra          dd ?
wcx_cbWndExtra          dd ?
wcx_hInstance           dd ?
wcx_hIcon               dd ?
wcx_hCursor             dd ?
wcx_hbrBackground       dd ?
wcx_lpszMenuName        dd ?
wcx_lpszClassName       dd ?
wcx_hIconSm             dd ?
Wow! that's a lot of fields. Three fields are the minimum needed to create a window: lpfnWndProc hInstance and lpszClassName. We will also define four other fields: style hIcon hCursor and hbrBackground. Unused fields must be zeroed out.
    We use the names defined in the API docs but we prefix the field names with a class "signature" which is shown in comments in the .h header files for C.
    The lpfnWndProc field contains the address of the window procedure that defines how the windows of this class will behave.
    The hInstance field contains a handle for the module that "owns" the window class. Win16 made a distinction between an instance and a module handle but in Win32 they are one and the same. These module instance handles identify loaded modules (EXE and DLL files) somewhat like file handles identifying opened files. Like file handles module/instance handles are unique only within a running program instance or process.
    The lpszClassName field contains the address of a zero-terminated (C-style) string which names the window class.
    Instead of defining the structure with WNDCLASSEX we will expand it and comment on what is in the WNDCLASSEX structure.
align   4
wcx dd      size WNDCLASSEX           ; cbSize
dd      CS_VREDRAW or CS_HREDRAW  ; style
dd      WndProc                   ; lpfnWndProc
dd      0
0                       ; cbClsExtra
dd      0                         ; hInstance
dd      0                         ; hIcon
dd      0                         ; hCursor
dd      COLOR_WINDOW+1            ; hbrBackground
dd      0                         ; lpszMenuName
dd      wndclsname                ; lpszClassName
dd      0                         ; hIconSm

wndclsname db 'winmain'
The field cbSize has the total size of the WNDCLASSEX structure. A number of structures have a structure size as the first field. This makes it easy to extend structures. Later releases of Windows can use this field to determine which version of the structure is being used and act accordingly. It's important to set this field when its structure is used to receive data.
    CS_VREDRAW and CS_HREDRAW in the style field force the normal behavior of redrawing the window whenever it changes size.
    WndProc is the window procedure we will define later in the program.
    The hbrBackground field specifies the default or background coloring of the framed area known as the client area. The value (COLOR_WINDOW + 1) makes it the same as the other window client areas.
    The label "wndclsname" defines the location of the zero-terminated window class name.

Now for the fields that must be filled in at runtime.

extrn   GetModuleHandle:near

push    large 0            ; NULL string pointer means
call    GetModuleHandle    ; get HINSTANCE/HMODULE of EXE file
mov     [wcx.wcx_hInstance]
Calling GetModuleHandle with a NULL pointer gives us the instance handle of the EXE file. This is the module that will "own" the window class.
extrn   LoadIcon:near

push    large IDI_WINLOGO
push    large 0         ; hInstance
0 = stock icon
call    LoadIcon
mov     [wcx.wcx_hIcon]

push    large IDC_ARROW
push    large 0         ; hInstance
0 = stock cursor
call    LoadCursor
mov     [wcx.wcx_hCursor]
The hIcon field gives us an "application" icon. And the hCursor field gives us the cursor image that is used when the cursor is over the window we will create.
extrn   RegisterClassEx:near

push    offset wcx
call    RegisterClassEx
And thus the window class is registered.
    All versions of Windows use the structure to create their own internal window class "object" so after the window class is registered we can discard our "window class structure" with no ill effects.

Window creation

After the window class name is registered we can create the window using CreateWindowEx.
    Although there are a lot of individual arguments we're basically providing five (and nulling out the others): 1) the window class name 2) a window caption 3) the window location 4) the window size and 5) some standard "style" options.
    The window class name is qualified by the module instance handle that owns it. The owner is established by RegisterClassEx via the hInstance field in the WNDCLASSEX data structure.
    In the following code we use a different approach to passing parameters. We can treat the argument list as a data structure. Instead of pushing each argument individually we create a structure to hold all the arguments (in proper order) and then push the entire structure onto the stack. We can modify any portion before stacking the structure or modify the stacked structure. The following code chooses the latter approach.
align   4
cwargs  dd   0                  ; dwExStyle
dd   wndclsname         ; lpszClass
dd   wnd_title          ; lpszName
dd   100                ; x
dd   100                ; y
dd   200                ; cx (width)
dd   200                ; cy (height)
dd   0                  ; hwndParent
dd   0                  ; hMenu
dd   0                  ; hInstance
dd   0                  ; lpCreateParams

wnd_title db 'Application'

extrn   CreateWindowEx:near

sub     esp
48            ; allocate argument list
mov     esi
offset cwargs ; set block move source
mov     edi
esp           ; set block move destination
mov     ecx
12            ; number of arguments
rep movsd
mov     eax
mov     [esp+40]
eax      ; set hInstance argument in stack
call    CreateWindowEx

The message loop

After the window is created and displayed we can then deal with any message sent to our program. Our basic GUI program repeatedly fetches messages from the message queue with GetMessage. We do this with a message loop. This loop is sometimes called the message pump.
    If there are any messages originating from one of the various forms of SendMessage GetMessage will invoke the appropriate window procedure directly. Otherwise the message is copied into a local buffer (the lpMsg argument) and control returns to the caller of GetMessage. A zero (FALSE) value is returned if GetMessage retrieves a WM_QUIT message.
    In our program the message loop quits if a WM_QUIT message is retrieved. Otherwise it invokes the proper window procedure using DispatchMessage.
extrn GetMessage:near

msgbuf MSG    <>

push  large 0       ; uMsgFilterMax
push  large 0       ; uMsgFilterMin
push  large 0       ; hWnd (filter)
0 = all windows
push  offset msgbuf ; lpMsg
call  GetMessage    ; returns FALSE if WM_QUIT
or    eax
jz    end_loop

push  offset msgbuf
call  DispatchMessage

jmp   msg_loop


Terminating the program

We exit the program with ExitProcess.
extrn ExitProcess:near

push    large 0    ; (error) return code
call    ExitProcess

The window procedure

Except for some initialization and some cleanup a GUI program is expected to do everything within the various window procedures.  The window procedure may be called by Windows itself (e.g. when a window is first created) or it may be invoked via DispatchMessage. The window procedure receives four DWORD arguments in this order: window handle (hWnd) message ID (nMessage) first message parameter (wParam) second message parameter (lParam). The argument wParam gets its original name from Win16 where it was a WORD argument.
    When a window is closed the default action handled by DefWindowProc is to destroy it. The window is removed from the screen and a WM_DESTROY message is sent to the window procedure. If we want to terminate our program upon closing a specific window then that window must respond to a message associated with closing a window. The recommended message is WM_DESTROY. Our window procedure does this for the one and only window calling PostQuitMessage as a response. After the window procedure is exited a WM_QUIT message will be picked up by GetMessage in our message loop.
    All other messages get processed by DefWindowProc.

One of the advantages of going from 16-bit code to 32-bit code is the extra addressing modes available. All eight primary 32-bit registers can be used in complex addressing modes.
    WndProc takes advantage of this by using ESP to access its arguments. We use the decomposed form [ESP+4+4] to show that part of the offset is for skipping the stacked EIP and the other part is the offset to the second argument (the message ID).

extrn DefWindowProc:near

cmp   dword ptr [esp+4+4]
jne   DefWindowProc

push  large 0
call  PostQuitMessage

xor   eax
ret   16

end   _start


Linking a standard application is similar to linking a Console App. With the Microsoft linker we specify a different subsystem.
    link winbasic kernel32.lib user32.lib /entry:start /subsystem:windows
With the Borland linker we specify a standard Windows app with /aa instead of /ap. The /V option tells the linker to set the subsystem version to 4.0. (Warning: The linker that came with TASM 4.0 does not support /V.)
tlink32 /Tpe /aa /c /V4.0 winbasic