A Minimal Win95 GUI Program--WINDOW01.ASM

This program displays a window using very little information. It illustrates the minimal requirements for creating a custom window.
    This particular program is almost the smallest stoppable GUI program with a custom window. To get it much smaller would require using a predefined window class (message box, dialog, or control).
    You can move the window. If you select some other window, you can click on the window to reactivate (reselect) our program. You can reactivate it with Alt-Tab, as well. Use Alt-F4, when it is selected, to terminate the program.
    You can also create a shortcut and use it to start the program with the window in minimized or maximized form. Double click the caption/title bar to change the window to "normal" size.

[Download source code.]

More programming information
Back to Win95 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.]

To link with Borland's linker, the startup address is specified in the END directive.
To link with Microsoft's linker, the startup address must be PUBLIC and specified by the linker's /ENTRY: switch. The linker will automatically prepend an underscore, "_".

    .386
    model  flat

    public _start

    ; ...

    .code
_start:

    ; ...

    end    _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 name. Associated with the window class is a callback function known as a window procedure. The callback defines the behavior for all windows created with this class.
    Before we can create a window, its window class must be registered. Windows preregisters a few window classes, but these are usually used for creating controls.
    There are two ways to register a custom window class:  the old way by calling RegisterClass with a WNDCLASS structure, or the new way by calling RegisterClassEx with a WNDCLASSEX structure. The new way allows specifying the new Win95 small icon. The old way is required if you want to be compatible with older (pre-4.0) NT systems.
WNDCLASSEX struc         ; comments show the C/C++ name
wcxSize          dd ?    ; cbSize, size of WNDCLASSEX
wcxStyle         dd ?    ; style
wcxWndProc       dd ?    ; lpfnWndProc
wcxClsExtra      dd ?    ; cbClsExtra
wcxWndExtra      dd ?    ; cbWndExtra
wcxInstance      dd ?    ; hInstance
wcxIcon          dd ?    ; hIcon
wcxCursor        dd ?    ; hCursor
wcxBkgndBrush    dd ?    ; hbrBackground
wcxMenuName      dd ?    ; lpszMenuName
wcxClassName     dd ?    ; lpszClassName
wcxSmallIcon     dd ?    ; hIconSm
WNDCLASSEX ends
Wow! that's a lot of fields. Four fields are the minimum needed to create a window: cbSize, lpfnWndProc, hInstance, and lpszClassName. Unused fields must be zeroed out.
    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. Although Win32, in general, allows strings with either ANSI or Unicode characters, Win95, in most cases, handles only ANSI. So we encode the window class name in ANSI.
    extrn   GetModuleHandle:near,RegisterClassEx:near
    .data
wc  WNDCLASSEX <size WNDCLASSEX,0,WndProc,0,0, 0, 0,0,0, 0,wndclsname, 0>
wndclsname db 'window01',0

    .code
    push    large 0            ; NULL string pointer means
    call    GetModuleHandle    ; get HINSTANCE/HMODULE of EXE file
    mov     [wc.wcxInstance],eax
    push    offset wc
    call    RegisterClassEx
    [See linking.]

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 four (and nulling out the others): 1) the class name, 2) window location, 3) window size, and 4) an option to start with a visible window. The last three can be nulled out provided you're willing to perform the equivalent display functions elsewhere.
    The module instance handle must be an "owner" of the given window class, so together they uniquely identify the window class. This ownership is established by RegisterClassEx via the hInstance field in the WNDCLASSEX data structure.
WS_VISIBLE equ 10000000h

    extrn   CreateWindowEx:near

    .code
    push    large 0            ; lpParam
    push    [wc.wcxInstance]   ; hInstance
    push    large 0            ; menu hmenu
    push    large 0            ; parent hwnd
    push    large 200          ; height
    push    large 200          ; width
    push    large 100          ; y
    push    large 100          ; x
    push    large WS_VISIBLE   ; Style
    push    large 0            ; Window text (caption)
    push    offset wndclsname  ; Class name
    push    large 0            ; extended style
    call    CreateWindowEx
    [See linking.]

The message loop

After the window is created and displayed, we can then deal with any message sent to our program. Our minimal 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 (one of the GetMessage parameters), 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.
MSG struc          ; comments show the C/C++ names
msgHwnd    dd ?    ; hwnd
msgMessage dd ?    ; message
msgWparam  dd ?    ; wParam
msgLparam  dd ?    ; lParam
msgTime    dd ?    ; time, message posting time
msgPtX     dd ?    ; pt.x, cursor position
msgPtY     dd ?    ; pt.y, cursor position
MSG ends

    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:
    [See linking.]

Terminating the program

We exit the program with ExitProcess.
    extrn ExitProcess:near

    .code
    push    large 0    ; (error) return code
    call    ExitProcess
    [See linking.]

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.
    When a window is closed (e.g., with Alt-F4), 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 by closing some 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


WM_DESTROY equ 2

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

start_destroy:
    push  large 0
    call  PostQuitMessage

    xor   eax,eax
    ret   16
    [See linking.]

Strings, ANSI and Unicode

Character data can be in either ANSI (8-bit) or Unicode (16-bit). Consequently, most (not all!) Win32 functions which handle strings, directly or indirectly, have two versions--one for ANSI and one for Unicode. The functions are distinguished by appending A or W (wide) to the function name.
    Unicode is more efficient on NT, but Win95 has only a limited Unicode capability. So, except for a few cases, Win95 programs use ANSI characters and strings.
    Most Windows strings follow the C standard of terminating with a zero-valued (NUL) character.

 Linking

If you're a DOS programmer, you might be wondering what INT's are being used to call API routines such as ExitProcess. The answer is none. The API functions being called are located in DLL (dynamic link library) files. (How the program switches between the "application" and OS "kernel" modes requires knowledge of how the Intel 386 and later chips implement protected mode and memory paging.)
    The linker doesn't add the DLL code into the EXE file. Instead, it produces references to DLL entry points in the EXE files. This is done via associated import libraries. You must tell the linker what import libraries are needed. Unfortunately, there isn't a standard import library format. Below are examples for using the Borland and Microsoft libraries..
    An important feature needed in your assembler is preservation of letter case, because Win32 API names are case-sensitive. On many assemblers this is an option which is normally disabled.

Borland uses one library, import32.lib, for the core set of Win32 functions. The import link names are the same as the entry point names in the DLL.

CreateWindowEx   equ <CreateWindowExA>
DefWindowProc    equ <DefWindowProcA>
DispatchMessage  equ <DispatchMessageA>
GetMessage       equ <GetMessageA>
GetModuleHandle  equ <GetModuleHandleA>
RegisterClassEx  equ <RegisterClassExA>
Microsoft uses several libraries, one per DLL. The link names for Win32 functions are "decorated" names. The rule is simple: prepend an underscore (_) and append an at-sign (@) and the number of argument bytes in decimal. So the ANSI version of GetMessage, which has four DWORD arguments, is linked as _GetMessageA@16 (_ + GetMessage + A + @ + 16 [= 4*4]).
    This decoration scheme is primarily for the convenience of Microsoft compilers. The link name needn't have this relationship to the entry point name, but this scheme is used by the Microsoft Win32 libraries.
CreateWindowEx   equ <_CreateWindowExA@48>
DefWindowProc    equ <_DefWindowProcA@16>
DispatchMessage  equ <_DispatchMessageA@4>
ExitProcess      equ <_ExitProcess@4>
GetMessage       equ <_GetMessageA@16>
GetModuleHandle  equ <_GetModuleHandleA@4>
PostQuitMessage  equ <_PostQuitMessage@4>
RegisterClassEx  equ <_RegisterClassExA@4>