News:

Masm32 SDK description, downloads and other helpful links
Message to All Guests

Main Menu

Simple template for creating a window

Started by jj2007, October 14, 2013, 08:51:01 PM

Previous topic - Next topic

jj2007

Many new members are eager to see their first window in action. Here is a Masm32 template containing only the really essential elements. The code creates a window with a menu and an edit control, and is less than 70 lines long.

The attachment contains two more sources for "Hello World" console and MessageBox apps.

; *** minimalistic code to create a window; see also \masm32\examples\exampl01\generic\generic.asm and Iczelion tutorial #3 ***
include \masm32\include\masm32rt.inc        ; set defaults and include frequently used libraries (Kernel32, User32, CRT, ...)

.data        ; initialised data section
txClass        db "MyWinClass", 0                ; class name, will be registered below
wcx        WNDCLASSEX <WNDCLASSEX, CS_HREDRAW or CS_VREDRAW, WndProc, 0, 0, 1, 2, 3, COLOR_BTNFACE+1, 0, txClass, 4>

.data?        ; uninitialised data - use for handles etc
hEdit        dd ?

.code
WinMain proc uses ebx
LOCAL msg:MSG
  mov ebx, offset wcx
  wc equ [ebx.WNDCLASSEX]
  mov wc.hInstance, rv(GetModuleHandle, 0)
  mov wc.hIcon, rv(LoadIcon, NULL, IDI_APPLICATION)
  mov wc.hIconSm, eax
  ; mov wc.hCursor, rv(LoadCursor, NULL, IDC_ARROW)        ; not needed
  invoke RegisterClassEx, addr wc                ; the window class needs to be registered
  invoke CreateWindowEx, NULL, wc.lpszClassName, chr$("Hello World"),        ; window title
    WS_OVERLAPPEDWINDOW or WS_VISIBLE,
    CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,        ; x, y, w, h
    NULL, NULL, wc.hInstance, NULL
  .While 1
        invoke GetMessage, ADDR msg, NULL, 0, 0
        .Break .if !eax
        invoke TranslateMessage, ADDR msg
        invoke DispatchMessage, ADDR msg
  .Endw
  exit msg.wParam
WinMain endp

WndProc proc uses esi edi ebx hWnd, uMsg, wParam:WPARAM, lParam:LPARAM
  SWITCH uMsg
  CASE WM_CREATE
        mov esi, rv(CreateMenu)                ; create the main menu
        mov edi, rv(CreateMenu)                ; create a sub-menu
        invoke AppendMenu, esi, MF_POPUP, edi, chr$("&File")        ; add it to the main menu
        invoke AppendMenu, edi, MF_STRING, 101, chr$("&New")        ; and add
        invoke AppendMenu, edi, MF_STRING, 102, chr$("&Save")        ; two items
        invoke SetMenu, hWnd, esi                ; attach menu to main window
        invoke CreateWindowEx, WS_EX_CLIENTEDGE, chr$("edit"), NULL,
          WS_CHILD or WS_VISIBLE or WS_BORDER or ES_AUTOVSCROLL or ES_MULTILINE,
          9, 9, 300, 200,
          hWnd, 103, wcx.hInstance, NULL        ; we have added an edit control
        mov hEdit, eax        ; you may need this global variable for further processing

  CASE WM_COMMAND
        movzx eax, word ptr wParam        ; the Ids are in the LoWord of wParam
        Switch eax
        case 101
                MsgBox 0, "You clicked New", "Hi", MB_OK
        case 102
                MsgBox 0, "You clicked Save", "Hi", MB_OK
        Endsw
  CASE WM_DESTROY
        invoke PostQuitMessage, NULL                ; quit after WM_CLOSE
  ENDSW
  invoke DefWindowProc, hWnd, uMsg, wParam, lParam        ; default processing
  ret
WndProc endp

end WinMain

TWell

WM_CREATE could be after WM_COMMAND, because it comes only once ?

dedndave

also, i am a little surprised it works
i don't see how WM_CREATE, WM_COMMAND, WM_DESTROY return EAX=0

jj2007

Quote from: TWell on October 14, 2013, 09:53:52 PM
WM_CREATE could be after WM_COMMAND, because it comes only once ?
Could be, but it doesn't matter where the CASE sits. The create, command, destroy order looks a bit more "natural".

Quote from: dedndave on October 14, 2013, 10:16:53 PM
also, i am a little surprised it works
i don't see how WM_CREATE, WM_COMMAND, WM_DESTROY return EAX=0
Yep, that's right, Dave :t

Corrected above - I took away the DEFAULT case and moved DefWindowProc before the ret (which returns zero for the three messages you mentioned).

P.S.: It worked because eax=hEdit, and the only value that would make WM_CREATE fail is -1.

dedndave

that doesn't look right, either   :P
the messages you process shouldn't always go to DefWindowProc

i use .if/elseif/endif - which i know you don't like
but, each message type that is handled is allowed to set its' own return value

the way you had it before wasn't too bad
you just needed to zero EAX at the end of each case (except DEFAULT)

jj2007

Quote from: dedndave on October 14, 2013, 11:07:22 PM
that doesn't look right, either   :P
the messages you process shouldn't always go to DefWindowProc

Windows can process all messages, and will return the right value. The only reason not to use DefWindowProc is when you want to bypass the default processing, which is not necessary here.

qWord

DefWindowProc is commonly placed in the default case. The template leads to code that process messages two times, which is either useless or simply wrong.
Quote from: msdn: DefWindowProcCalls the default window procedure to provide default processing for any window messages that an application does not process. This function ensures that every message is processed
MREAL macros - when you need floating point arithmetic while assembling!

jj2007

Quote from: qWord on October 15, 2013, 12:19:05 AM
DefWindowProc is commonly placed in the default case. The template leads to code that process messages two times, which is either useless or simply wrong.

Jein...

  CASE WM_DESTROY
        invoke PostQuitMessage, NULL                ; quit after WM_CLOSE
  CASE WM_NCLBUTTONDOWN
          .if word ptr lParam>200 && word ptr lParam+2<100
                  print "X was above 200", 13, 10
          .endif
  CASE WM_NCLBUTTONUP
          .if word ptr lParam>200 && word ptr lParam+2<100
                  print "X was above 200", 13, 10
          .endif
usedefault=1        ; set to one or to zero and try closing the window by clicking into the "x" in the upper right corner
if usedefault
  DEFAULT
        invoke DefWindowProc, hWnd, uMsg, wParam, lParam        ; default processing
  ENDSW
  ret
else
  ENDSW
  invoke DefWindowProc, hWnd, uMsg, wParam, lParam        ; default processing
  ret
endif
WndProc endp

(console assembly & link)

dedndave

    mov     eax,uMsg
    .if eax==WM_COMMAND

        ;WM_COMMAND code

        xor     eax,eax

    .elseif eax==WM_CREATE

        ;WM_CREATE code

        xor     eax,eax

    .elseif eax==WM_DESTROY

        ;WM_DESTROY code

        xor     eax,eax

    .else
        INVOKE  DefWindowProc,hWnd,uMsg,wParam,lParam
    .endif
    ret


    SWITCH uMsg
    CASE WM_COMMAND

        ;WM_COMMAND code

        xor     eax,eax

    CASE WM_CREATE

        ;WM_CREATE code

        xor     eax,eax

    CASE WM_DESTROY

        ;WM_DESTROY code

        xor     eax,eax

    DEFAULT
        INVOKE  DefWindowProc,hWnd,uMsg,wParam,lParam
    ENDSW
    ret


ok - so it takes an additional XOR EAX,EAX for each message handled - so what ?
it allows flexibilty in handling different return values for different message types
for example - WM_CTLCOLORxxx messages, you want to return an HBRUSH
another - WM_CLOSE - if you offer a MessageBox to verify user exit, you can return TRUE to ignore the message

default processing after you have handled a message may work ok for the few you are currently processing
but, that will bite you in the ass, at some point - and it's slow, too

jj2007

Quote from: dedndave on October 15, 2013, 01:06:25 AM
for example - WM_CTLCOLORxxx messages, you want to return an HBRUSH

  invoke CreateBrush, ...
  ret


Quoteanother - WM_CLOSE - if you offer a MessageBox to verify user exit, you can return TRUE to ignore the message

Thanks for the example, Dave:

  CASE WM_CLOSE
     MsgBox 0, "Sure to close?", "Hi", MB_YESNO
     sub eax, IDNO
     .if Zero?
        ret
     .endif
usedefault=0   ; set to one and try closing the window
if usedefault
  DEFAULT
   invoke DefWindowProc, hWnd, uMsg, wParam, lParam   ; default processing
  ENDSW
  ret
else
  ENDSW
  invoke DefWindowProc, hWnd, uMsg, wParam, lParam   ; default processing
  ret
endif
WndProc endp


Quotedefault processing after you have handled a message may work ok for the few you are currently processing
but, that will bite you in the ass, at some point - and it's slow, too

It must work for all messages because that's Windows' default option. And it would be slow only if it happened a thousand times in an innermost loop, which is never the case...

qWord

Quote from: jj2007 on October 15, 2013, 12:58:06 AM[...]WM_NC[...]
a typical problem for people who want to see their first window in action.

BTW: I would tend to dave's approach.
MREAL macros - when you need floating point arithmetic while assembling!

jj2007

I'll try to explain it in simple words:
- If you put invoke DefWindowProc, ... into the DEFAULT case, then certain messages such as WM_(NC)LBUTTONDOWN and WM_CLOSE stop working properly. They need the DefWindowProc processing, always. Try the WM_CLOSE example above (use console assembly & link, and remember that Ctrl C closes a console program; otherwise, use Task Manager to kill the process).
- you can prevent specific messages from being def-processed simply by returning (e.g. a brush)
- you can do no harm by using DefWindowProc for messages that don't need it; you will just occasionally lose a few nanoseconds.

qWord

when you process WM_CLOSE and want to close the window, you must call DestroyWindow as per documentation. And of course the none client area message requires a special a handling, but how often did you work with such messages? IMO, especially for beginners, a template should follow the typical WndProc layout as proposed by Microsoft:
Code ("msdn: Using Window Procedures") Select
LRESULT CALLBACK MainWndProc(
    HWND hwnd,        // handle to window
    UINT uMsg,        // message identifier
    WPARAM wParam,    // first message parameter
    LPARAM lParam)    // second message parameter
{

    switch (uMsg)
    {
        case WM_CREATE:
            // Initialize the window.
            return 0;

        case WM_PAINT:
            // Paint the window's client area.
            return 0;

        case WM_SIZE:
            // Set the size and position of the window.
            return 0;

        case WM_DESTROY:
            // Clean up window-specific data objects.
            return 0;

        //
        // Process other messages.
        //

        default:
            return DefWindowProc(hwnd, uMsg, wParam, lParam);
    }
    return 0;
}


BTW: I can't see what is problematic on WM_LBUTTONDOWN not being processed by DefWindowProc
MREAL macros - when you need floating point arithmetic while assembling!

dedndave

as for WM_CLOSE....
i have written code for WM_CLOSE, many times, without calling DefWindowProc
DefWindowProc handles it with a call to DestroyWindow

so - if you choose to handle WM_CLOSE, you can call DestroyWindow (or opt not to), then return 0

my previous comment about returning TRUE was incorrect
sorry about that - i was merely pointing out that it's nice to have individual control of return values

i will say this....
if you write the code so that everything goes through DefWindowProc,
it may work with the limited set of messages you are processing
at some point, that's not going to be desirable

the idea of a template is to be expandable
i.e., the beginner should be able to add processing of a new message with minimal difficulty

hutch--

Well, after about 20 years of writing windows, I have learnt a few things about WndProc procedures. Even though a normal switch/.if or whatever block processes sequentially, only one message will be processed at a time so it is bad design to preserve registers by default with the USES notation as many messages simply don't need to have extra registers. This allows you to use an instruction pair like XOR EAX, EAX / RET for messages that need to have zero returned by bypassing the DefWindowProc function call.

You properly do your register preservations on a message by message basis as you may for example have complex code that requires extra registers in a WM_COMMAND processing but nothing for many of the other messages.

For WndProc switch or .IF blocks, the most common mistake for people learning this style of coding is messing up where the DefWindowProc needs to be so critical messages get dumped out the other end without the correct default processing. Why doesn't my window show or what can't I move my window are common questions from WndProc errors.

For folks learning this style of coding, it is not always good advice to follow 1996 window templates as they were still in transition from 16 bit versions and had a lot of extra crap in them for 16 bit.

The style I recommend in a normal WndProc is LOCAL variables first like normal, then the .IF or switch block and when it is terminated (.endif or endsw), !!!! THEN !!!!! set you DefWindowProc call followed by a RET.

Now just for example, this is register preservation at a message level.


  .elseif uMsg == WM_COMMAND
    .if wParam == 1234
      push ebx
      push esi
      push edi
    ; write the complex code here
      pop edi
      pop esi
      pop ebx
    .endif

  .elseif uMsg == WM_whatever_comes_next