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.

    .386
    model  flat

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

    include win32hst.inc ; constants, structures, and entry names

    public _start

    .code
_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.
WNDCLASSEX STRUC
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 ?
WNDCLASSEX ENDS
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.
    .data
    align   4
wcx dd      size WNDCLASSEX           ; cbSize
    dd      CS_VREDRAW or CS_HREDRAW  ; style
    dd      WndProc                   ; lpfnWndProc
    dd      0,0                       ; cbClsExtra, cbWndExtra
    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',0
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

    .code
    push    large 0            ; NULL string pointer means
    call    GetModuleHandle    ; get HINSTANCE/HMODULE of EXE file
    mov     [wcx.wcx_hInstance],eax
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,LoadCursor:near

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

    push    large IDC_ARROW
    push    large 0         ; hInstance, 0 = stock cursor
    call    LoadCursor
    mov     [wcx.wcx_hCursor],eax
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

    .code
    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.
        .data
        align   4
cwargs  dd   0                  ; dwExStyle
        dd   wndclsname         ; lpszClass
        dd   wnd_title          ; lpszName
        dd   WS_VISIBLE or WS_OVERLAPPED or WS_SYSMENU or WS_THICKFRAME \ 
             or WS_MINIMIZEBOX or WS_MAXIMIZEBOX  ; style
        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',0

        extrn   CreateWindowEx:near

        .code
        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,[wcx.wcx_hInstance]
        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,DispatchMessage:near

    .data
msgbuf MSG    <>

    .code
msg_loop:
    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,eax
    jz    end_loop

    push  offset msgbuf
    call  DispatchMessage

    jmp   msg_loop

end_loop:

Terminating the program

We exit the program with ExitProcess.
    extrn ExitProcess:near

    .code
    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,PostQuitMessage:near

    .code
WndProc:
    cmp   dword ptr [esp+4+4],WM_DESTROY
    jne   DefWindowProc

on_destroy:
    push  large 0
    call  PostQuitMessage

    xor   eax,eax
    ret   16

    end   _start

Linking

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,,,import32.lib