• Welcome to Theos PowerBasic Museum 2017.

Fred's Tutorial #2: Windows API Tutorial: The Window Procedure

Started by Frederick J. Harris, August 20, 2007, 07:04:16 PM

Previous topic - Next topic

0 Members and 1 Guest are viewing this topic.

Frederick J. Harris

Form2.bas

     The one thing Form1.bas has in common with Form2.bas is that they both have only two procedures within themselves, i.e., a WinMain() function, and a Window Procedure - WndProc().  Looking a little closer you'll see that while the WinMain() function is virtually identical in both programs, the Window Procedure - WndProc() - is much larger in Form2.bas.  By this time you certainly should have compiled and ran Form1.bas, and when you did that you would have seen that while the program did create a normal window on your computer desktop, the program otherwise didn't do much.  And I don't say this to belittle in any way the effort it took us to get to the point of creating that first very simple window, because there certainly are many concepts one must understand to get even that far.  Don't ever let any naysayers about Windows fool you!  It is an extremely elegant, complex, sophisticated operating environment, and, because of this, programming for it requires mastering some complex architectural issues.  But if you compile and run Form2.bas, you'll finally see a program that actually does something other that wait for its own destruction through the click of the'x' button in its title bar.  After dismissing an initial message box about which I'll have more to say momentarily, you'll see a small white window with two lines of text.  If you move your mouse pointer anywhere over the window it will rapidly and accurately display for you in its first line the present pixel location of the mouse pointer.  Also, if you drag either or both borders of the window to resize it, the window will immediately update that second line of text to reflect the new window width and height dimensions.  But there's still more the window will do, but you probably havn't figured it out.  Try typing something with your keyboard, your name, for example.  Anything you type will show up on the Window's third line if the window has the focus.  Finally, if you do a left mouse button click anywhere in the client area of the window a location message of the click will be drawn at that point on the window.

     Being as the WinMain() function is virtually identical in both programs one can only come to the conclusion that the additional functionalities of Form2 stem from additional code in Form2's window procedure.  And this is indeed the case.  Lets back up a second and examine Form1's meager window procedure before moving on to Form2.  As you may recall, we purposefully disregarded discussing the window procedure back in Form1, but now is the time to examine it carefully. Reproduced below is the Window Procedure from Form1.bas...


Function WndProc(ByVal hWnd As Long,ByVal wMsg As Long,ByVal wParam As Long,ByVal lParam As Long) As Long
  If wMsg=%WM_DESTROY Then         'This is the all important window procedure.  It is not called anywhere
     Call PostQuitMessage(0)       'in this program.  Windows itself calls this function when any event
     WndProc=0                     'occurs that pertains to this window.  The runtime address of this
     Exit Function                 'procedure was obtained for Windows by PowerBASIC's CodePtr function
  End If                           'back in WinMain() when the wc.lpfnWndProc member was set in the
                                   'WndClassEx type variable.  All this particular WndProc is interested in
  WndProc=DefWindowProc(hWnd,wMsg,wParam,lParam)    'is being WM_DESTROY'ed when a WM_DESTROY message comes
End Function                                        'through


     The Window Procedure (often termed a 'callback function') is a function which is called by the Windows operating system itself to Notify  a program that an event has occurred which pertains to that program.  There are hundreds of events possible, and every one has a corresponding equate defined in the windows header files.  To end Form1 you must click on the little 'x' button in its title bar, and when you do that Windows sends the program several messages, among them %WM_CLOSE and %WM_DESTROY, and you can see the If statement testing the wMsg parameter of the WndProc() function looking for that %WM_DESTROY message.  Any messages the program receives that are not %WM_DESTROY messages will not enter the body of the If statement, but will rather fall through to a call to DefWindowProc() in the function's terminating statement.  This is known as calling Window's Default Window Procedure (DefWindowProc()).  All messages Windows sends to the window procedure should be handled either by code you yourself provide, or by the default window procedure.  Note that when a %WM_DESTROY message is received and program logic enters the body of the If statement, WndProc() is set equal to zero and the function makes an early exit.  Zero is the most common value to return to the operating system when a program handles a message, although a few messages require other return values (more about that later).  The thing that causes the program to end, however, is not the WM_DESTROY message.  That only causes the main window to be destroyed.  The program is still running down in WinMain(), only now it doesn't have any windows!  The program's destruction is caused by the PostQuitMessage(0) call.  That causes the message pump, i.e., message processing loop, to terminate and WinMain() returns.

         Moving on to Form2's WndProc() you will note that it lacks an If statement in its window message processing logic.  A Select Case was used instead because this particular window procedure is testing for seven different messages.  If you recall back in Form1.bas I stated that when WinMain() executes a CreateWindow() call, a %WM_CREATE message will be sent to the window's window procedure.  This is the first message a window will receive upon creation.  It is in most instances the best place to place initialization code for a window that you are creating, especially if it will be a top level main program window which you will adorn with textboxes, listboxes, etc.  Also, at the time of the WM_CREATE message the window is not yet visible, and this message will be received in the window procedure before the CreateWindow() call in WinMain() returns.  So, the first place a valid window handle for this window will become available is through the first parameter in the window procedure's parameter list, here hWnd, during that WM_CREATE message.  Window handles are 32 bit numbers that Windows assigns to windows upon their creation.

     In this particular WndProc() there are six lines of code that execute when the %WM_CREATE message is received.  The purpose of those six lines of code is to determine on your computer system how high in pixels a character is in terms of your computer configuration.  I can imagine at this point that probably sounds outrageous to you but it isn't really so outrageous when you consider that the program prints several lines of text to your screen, and for decent looking output it is necessary to know at what spacing the lines of text should be drawn.  Don't forget we're not in DOS anymore.  Even text is graphics.  Things have gotten more complicated!

     If you search your Windows Api documentation you will find a TYPE known as TEXTMETRIC.  We have declared a local in WndProc() of this type, i.e., 'tm', and we pass the address of this variable to the GetTextMetrics() Api function (exactly like we did with rc of type RECT back in Form1) and when it returns the tm.tmHeight member variable will contain the character height we are looking for.  In order to call GetTextMetrics() you see we needed something called a handle to a device context ( hDC ) and this we obtained with the GetDC() api call.  I know I'm hitting you with some heavy stuff here fast and furious, and if your eyes are starting to glaze over don't worry too much, as you'll eventually get through this if you stick with it.  Let me give you a little hint though if the above is a bit too much at this point.  If you eliminate wCharHt from the program, then completely delete the WM_CREATE message from the WndProc() function, then finally replace both occurances of wCharHt with 16 in WM_PAINT, the program will probably work exactly the same for you!  This might be a good exercise for you to try out in any case. 

     Continuing then, I store the character height in a statically dimensioned variable, wCharHt, and this is the value you see displayed in the small message box when the program first starts. Note that you see the message box before you see the program window, because, like I said, we are still in the processing of the WM_CREATE message, and the CreateWindow() call down in WinMain() hasn't even returned yet.  This is still before the ShowWindow() call in WinMain() that will make the window visible.

     Perhaps I had best point out something about the variable declarations at the top of the window procedure - WndProc().  Except for hDC, they are all declared as statics.  This means that they won't be accessable from anywhere else in the program - which protects them from inadvertant corruption - but it also means that they will retain their values between invocations of WndProc().  This is actually critical, because, believe it or not, depending on the speed of your computer, the window procedure may be getting called 25, 50, even 100 times per second, depending if windows has placed messages for your program in its 'message queene' (another topic, I'm afraid).  The reason it is critical here is that, had for example, wCharHt been declared as local and its value initialized during the WM_CREATE message, that value would have been lost after WM_CREATE processing was complete and the function exited.  Then, when the window finally became visible and the lines of text needed to be printed, the second and third lines would have overwritten the first line.  You see, all drawing on the window is done (in this program anyway) during the processing of the WM_PAINT message.  This fact in itself will probably seem very strange to you if you even thought about it, but I'll have a bit more to say on that later.

     However, we havn't gotten that far yet.  The next message the window will handle is the WM_SIZE message.  When ShowWindow() is called down in WinMain(), and before the window becomes visible, Windows will send a WM_SIZE message, and here things start to get interesting!

     You may note that so far we've discussed only two of the four parameters in that Window Procedure, i.e., WndProc().  We've shown that the 1st parameter, hWnd, is the Window Handle to which the message, 'wMsg', applies.  In this program there is only one main window so most messages this window's WndProc() will receive pertain to this specific window.  Those third and fourth parameters, - wParam and lParam, are important though.  They are message specific containers for information Windows wishes for us to know concerning the specific message in question.  In the case of the WM_SIZE message the low 16 bits of that 32 bit lParam variable will contain the width of the window in pixels, and the high 16 bits will contain the window height.  These values are obtained with the LOWRD() and HIWRD() macros.  After obtaining these values (which, by the way are held in variables of static duration) an Api function named InvalidateRect() is called, and this call forces windows to send a WM_PAINT message to the Window Procedure - WndPrioc().  This is how the drawing of text on the window occurs.  Note that even if the InvalidateRect() call wasn't made the window would have received a WM_PAINT message after a WM_CREATE and WM_SIZE message, but the InvalidateRect() call in WM_SIZE specifically relates to the manual resizing you may do with your mouse after the window becomes visible.

     Moving on then to the next message this window will receive during its creation, that message would be WM_PAINT.  This is a very important message if you are drawing any text or graphics on a window.  If all you are doing is placing windows controls on a window such as text boxes, listboxes, etc., it is not importand and can even be omitted.  Which brings up another important point!  All programs don't process every available message.  It is only necessary to process a message if your program needs to do something in relation to that message.  That point will eventually become clear to you.

     Starting back in Form1.bas I tried to make the point that the Windows Api documentation was critically important to Api programming, and that you need to be able to find your way around the Win32Api.inc file.  As you can see under the WM_PAINT message, several new Api functions are called.  And there sits another TYPE too.  This time something called a 'PAINTSTRUCT'.

     I'm going to make a sleight digression here.  Hopefully I've convinced you by this point that to do this API style programming you need quick and efficient access to two other documents.  The first would be a Windows Api help file and viewer program, and the Win32Api.inc file.  I personally use the documents that came with my version of Visual Studio 6, and I keep a shortcut to that on my desktop.  Oftentimes when I'm coding I keep it open and minimized.  In terms of the Win32Api.inc, I use the Lynx Project Explorer (obtainable free) in conjunction with either the JellyFish Pro Editor from Paul Squires, or SED from Jose Roca's web site.  With this Lynx Project Explorer, if you need information on an equate, a TYPE, a DECLARE, or anything else, its only a few mouse clicks and a second or two away.  This is infinitely easier than opening a header file in your programming editor and doing a search for any of these items.  For example, right now I'm looking at that PAINTSTRUCT in the left pane of the SED editor, and I can see all the fields of which it is comprised.  If I want to actually view the TYPE in the header file itself, that would be one more click away.  Highly, highly recommended for this work!

     So, BeginPaint() and EndPaint() are two Api functions you must call if you intend to handle the WM_PAINT message in the Window Procedure.  BeginPaint() is a function whose return value is a handle to a device context as we met in WM_CREATE when attempting to obtain character heights associated with the current font selected into the device context.  If you wish to draw to the screen in messages other than WM_PAINT then you use GetDC() for your device context handle.  In WM_PAINT though you need BeginPaint() and EndPaint().  If you look up BeginPaint() in your Api reference you'll see this...


BeginPaint

The BeginPaint function prepares the specified window for painting and fills a PAINTSTRUCT
structure with information about the painting.

HDC BeginPaint
(
  HWND             hwnd,       // handle to window
  LPPAINTSTRUCT    lpPaint     // pointer to structure for paint information
);

Parameters

hwnd      Handle to the window to be repainted.
lpPaint   Pointer to the PAINTSTRUCT structure that will receive painting information.
'
Return Values

If the function succeeds, the return value is the handle to a display device context for the
specified window.


As you see below under WM_PAINT, this is how I satisfied these requirements in PowerBASIC...

Local ps As PAINTSTRUCT
hDC=BeginPaint(hWnd,ps)

     The ps variable is of course a variable (complex type) of TYPE PAINTSTRUCT.  Since the placement of that variable in the second position of the function call will cause PowerBASIC to pass the variable's address to the BeginPaint() function, this satisfies the function's requirement of needing a pointer to a PAINTSTRUCT structure.  Why?  Because, as I'm sure you've either heard or read a thousand times, PowerBASIC passes variables to functions By Reference, which means the variable's address gets passed, which address is the definition of a pointer!  You may wish to examine the PowerBASIC DECLARE for BeginPaint() at this time to prove this to yourself.  And where would that DECLARE be?  You guessed it!  In none other than your Win32Api.inc file!

     Although I don't use any of the fields in the PAINTSTRUCT type after BeginPaint() is called, we nontheless needed to make the call in order to use the TextOut() function to output text. TextOut() for Windows is like 'Print' for DOS or Console mode PowerBASIC.  The handle to a device context returned by BeginPaint() is needed as a parameter to TextOut().  The TextOut() function is covered in some depth in Form3.bas (the next program in this tutorial), but I'll cover it some here too.  It is defined like so...


TextOut

The TextOut function writes a character string at the specified location, using the currently
selected font, background color, and text color.

BOOL TextOut
(
  HDC hdc,           // handle to device context
  int nXStart,       // x-coordinate of starting position
  int nYStart,       // y-coordinate of starting position
  LPCTSTR lpString,  // pointer to string
  int cbString       // number of characters in string
);

Parameters

hdc           Handle to the device context.
nXStart       Specifies the logical x-coordinate of the reference point that the system uses to
              align the string.
nYStart       Specifies the logical y-coordinate of the reference point that the system uses to
              align the string.
lpString      Pointer to the string to be drawn. The string does not need to be zero-terminated,
              since cbString specifies the length of the string.
cbString      Specifies the number of characters in the string.


     While I didn't cover the WM_MOUSEMOVE processing, it isn't at all different from the WM_SIZE processing which I did cover.  The first line of text to be drawn by TextOut() during the WM_PAINT processing is a line that contains the coordinates of the mouse pointer if you have it over the window.  The code from WM_PAINT is as follows...

Local szLine As Asciiz*32

hDC=BeginPaint(hWnd,ps)
szLine="MouseX="+Trim$(Str$(xCoord)) & "  MouseY="+Trim$(Str$(yCoord))
TextOut(hDC,0,0,szLine,Len(szLine))

     I used an Asciiz string to contain the text as they are a bit easier to use in Api functions than dynamic strings.  Asciiz strings are assembler or C strings that are essentially just a fixed length buffer containing the characters to be drawn and terminated by a NULL character.  This documentation states that the string doesn't even have to be null terminated, but PowerBASIC will null terminate it anyway.  The 0,0 in the function call tell TextOut() to start printing in the upper left corner of the window.  szLine is the character buffer (string) to draw, and Len(szLine) is the required length.

     The second line of text will contain the window size as determined during the processing of the WM_SIZE message.  Perhaps now you can see why these variables were declared as statics as they must retain their values across invocations of WndProc().  Note also the overall architecture of the program where a WM_SIZE, WM_MOUSEMOVE or WM_CHAR message is received, and an InvalidateRect() call is made to force a WM_PAINT message, which message causes the updating or painting of the window.  'Invalidate' and 'Validate' are terms you will come across in indows rogramming books and the documentation, and refer to internal data structures Windows uses to keep track of which windows or parts of windows are no longer valid and are in need of repainting.  When Windows determines that a window's contents are no longer valid, it will send that window a WM_PAINT message, and that window must be prepared to replace the contents of that window, as we have done here.  We had to do that here because the purpose of this program was to demonstrate these things to you the reader, and to do that we output text of ever changing content as you moved the mouse, drug the window borders, or typed text.  If instead the Window only 'contained windows controls such as text boxes or combo boxes of which Windows itself was responsible, then we wouldn't have had to do anything special in our WM_PAINT message.  In fact, we wouldn't have even needed a WM_PAINT message handler in our program, as Windows would have taken care of the restoration of these controls in the internal WM_PAINT message handlers of the controls themselves, none of which would have been our responsibility.

     Finally, I should discuss the processing of keypresses, or, more accurately, WM_CHAR messages. Up to this point I have led you through the three messages this window will receive during its creation for which message handling code is in place.  The three messages are WM_CREATE, WM_SIZE, and WM_PAINT.  After the receipt of these three messages the window will be visible on the screen.  The next messages to be processed will depend on what you, the user, does.  If you move the mouse pointer across the window the program's window procedure will be barraged with WM_MOUSEMOVE messages.  For every message the program receives the code under WM_MOUSEMOVE will execute, and the execution of that code will cause another message to be sent to the window procedure to repaint the window and the mouse pointer coordinate position will be updated along with the rest of the window.  Somewhat similiar processing will occur with any resizing of the window borders or minimize or maximize operations.  The processing of WM_CHAR messages is only slightly different.

     Remember when I stated that the wParam and lParam parameters are containers for message packets of information windows wishes us to have concerning the specific message sent.  Well, that's the case here too with WM_CHAR messages.  These messages are received whenever a character key is pressed when the window in question has the focus.  Windows places the decimal value of the character's Asciiz code in the wParam parameter.  PowerBASIC's Chr$() function easily converts this code to the character itself. This statement...

                                szText=szText+Chr$(wParam)

concatenates each character to a statically allocated Asciiz buffer, and the InvalidateRect() call invokes its printing on the third line of the window through the processing of the WM_PAINT message.

     In terms of the WM_LBUTTONDOWN message processing, I wasn't going to put that in this program, as I was going to leave that as a recommendation for you to try it out yourself.  But I thought I had better at least try it out first to see whether or not it might be too difficult for a beginner, and in doing that I finished it and left it in the program.  I guess I want all the glory and fun for myself!  But all is not lost!  There are still middle buttons, the right button, as well as various control and shift key states you could try.  I would like you to try though.  Why don't you attempt to duplicate what I did with the left button, but do it with the right button?  I'll even give you a hint!  In your Win Api help do a search for the
WM_LBUTTONDOWN message or the WM_RBUTTONDOWN message.

     The program is just below.  I left several lines of code in the program that are commented out.  The first one of these is a line declaring a global variable 'fp' for file pointer or 'handle'.  All the lines relating to this variable are commented out in the program, but you might want to un-comment these lines so as to allow the program to open up an Output.txt file in the directory you are running the program.  This would be useful to you in seeing the number of times Windows actually calls the Window Procedure while you are moving the mouse, re-sizing borders, typing text, clicking the mouse button, etc.  It may be quite an eye opener for you.

     Finally, right below the program is a second version of the program that shows the way I personally code Windows Sdk style programs.  In other words, it is my own personal style, and it is a bit different from standard Sdk style you'll see in books.  It shows a highly modularized approach to coding whereby each message is channelled to its own message handling function for processing.  All that remains of the Window Procedure then is a small 'switchboard' structure which directs processing elsewhere as each message is received.  Also, I have a personal preference for short parameter lists, so instead of passing the four Window Procedure parameters to each message handler function, I agglomerate them into a single type, i.e., WndEventArgs (Windows Event Arguements) and this type I pass to the message handlers.  The reason I came by this style is that when I first taught myself Windows programming back in the nineties with C, and I began putting real programs together as opposed to just demonstration programs like this one, I ended up with final programs with only two procedures in them, i.e., a WinMain() function with about 40 lines of code, and a WndProc() with 2000 or 3000 lines of code!  And jumbled up at the top of the program and at the top of WndProc() were tons of globals!  It was truely miserable and would have been my end if I hadn't come up with some way to straighten it out. The second program shows how I straightened out the mess.  It was really no great intellectual leap for me to come by what I did.  During the nineties I coded a lot with MFC and Visual Bacic, and both these development environments encapsulated code into message processing routines.  Later, with VB.NET and C#.NET I noted that parameters from the window procedure were agglomerated into types such as 'pea' for 'Paint Event Arguments', for example, in Paint Event Handlers.  So I borrowed from there.  Anyway, here is pure SDK style Form2.bas...


'Program Name Form2.bas
#Compile Exe
#Include "Win32api.inc"  'Equates, declares, types translated from Windows.h
'Global fp As Integer


Function WndProc(ByVal hWnd As Long,ByVal wMsg As Long,ByVal wParam As Long,ByVal lParam As Long) As Long
  Static xCoord As Word, yCoord As Word, wWidth As Word, wHeight As Word, wCharHt As Word
  Static xPos As Word, yPos As Word
  Static szText As Asciiz*256
  Local hDC As Long

  Select Case As Long wMsg
    Case %WM_CREATE
      'fp=Freefile
      'Open "Output.txt" For Output As #fp
      'Print #fp,"In WM_CREATE"
      Local tm As TEXTMETRIC
      hDC=GetDC(hWnd)
      Call GetTextMetrics(hDC,tm)
      wCharHt=tm.tmHeight
      Call ReleaseDC(hWnd,hDC)
      MsgBox("wCharHt=" & Trim$(Str$(tm.tmHeight)))  'Remark this out and use
      'Print "wCharHt=" & Trim$(Str$(tm.tmHeight))   'Print if using Console Compiler
    Case %WM_MOUSEMOVE
      'Print #fp,"In WM_MOUSEMOVE"
      xCoord=LoWrd(lParam) : yCoord=HiWrd(lParam)
      Call InvalidateRect(hWnd,ByVal 0,%TRUE)
      WndProc=0
      Exit Function
    Case %WM_LBUTTONDOWN
      'Print #fp,"In WM_LBUTTONDOWN"
      If wParam=%MK_LBUTTON Then
         xPos=LoWrd(lParam) : yPos=HiWrd(lParam)
         Call InvalidateRect(hWnd,ByVal 0,%TRUE)
         WndProc=0
         Exit Function
      End If
    Case %WM_SIZE
      'Print #fp,"In WM_SIZE"
      wWidth=LoWrd(lParam) : wHeight=HiWrd(lParam)
      Call InvalidateRect(hWnd,ByVal 0,%TRUE)
      WndProc=0
      Exit Function
    Case %WM_CHAR
      'Print #fp,"In WM_CHAR"
      szText=szText+Chr$(wParam)
      Call InvalidateRect(hWnd,ByVal 0,%TRUE)
      WndProc=0
      Exit Function
    Case %WM_PAINT
      'Print #fp,"In WM_PAINT"
      Local szLine As Asciiz*32
      Local ps As PAINTSTRUCT
      hDC=BeginPaint(hWnd,ps)
      szLine="MouseX="+Trim$(Str$(xCoord)) & "  MouseY="+Trim$(Str$(yCoord))
      TextOut(hDC,0,0,szLine,Len(szLine))
      szLine="wWidth="+Trim$(Str$(wWidth)) & " wHeight="+Trim$(Str$(wHeight))
      TextOut(hDC,0,wCharHt,szLine,Len(szLine))
      TextOut(hDC,0,wCharHt*2,szText,Len(szText))
      If xPos<>0 And yPos<>0 Then
         szLine="WM_LBUTTONDOWN At (" & Trim$(Str$(xPos)) & "," & Trim$(Str$(yPos)) & ")"
         TextOut(hDC,xPos,yPos,szLine,Len(szLine))
         xPos=0 : yPos=0
      End If
      Call EndPaint(hWnd,ps)
      WndProc=0
      Exit Function
    Case %WM_DESTROY
      'Print #fp,"IN WM_DESTROY"
      'Close #fp
      Call PostQuitMessage(0)
      WndProc=0
      Exit Function
  End Select

  WndProc=DefWindowProc(hWnd,wMsg,wParam,lParam)
End Function


Function WinMain(ByVal hIns As Long,ByVal hPrevIns As Long,ByVal lpCmdLine As Asciiz Ptr,ByVal iShow As Long) As Long
  Local szClassName As Asciiz*6
  Local hMainWnd As Dword
  Local wc As WndClassEx
  Local Msg As tagMsg

  szClassName="Form2"
  wc.cbSize=SizeOf(wc)                               : wc.style=0
  wc.lpfnWndProc=CodePtr(WndProc)                    : wc.cbClsExtra=0
  wc.cbWndExtra=0                                    : wc.hInstance=hIns
  wc.hIcon=LoadIcon(%NULL,ByVal %IDI_APPLICATION)    : wc.hCursor=LoadCursor(%NULL,ByVal %IDC_ARROW)
  wc.hbrBackground=GetStockObject(%WHITE_BRUSH)      : wc.lpszMenuName=%NULL
  wc.lpszClassName=VarPtr(szClassName)               : wc.hIconSm=LoadIcon(hIns,ByVal %IDI_APPLICATION)
  Call RegisterClassEx(wc)
  hMainWnd=CreateWindowEx(0,szClassName,"Form2",%WS_OVERLAPPEDWINDOW,200,100,325,300,%HWND_DESKTOP,0,hIns,ByVal 0)
  Call ShowWindow(hMainWnd,iShow)
  While GetMessage(Msg,%NULL,0,0)
    Call TranslateMessage(Msg)
    Call DispatchMessage(Msg)
  Wend

  WinMain=msg.wParam
End Function



     .....and here is the second version I promised showing a different architecture for the program taking advantage of modularization.  You will note no globals and no piling of variables at the top of the window procedure.  All variables are declared as needed in the various message handling procedures...



'Program Name Form2.bas
#Compile Exe
#Include "Win32api.inc"  'Equates, declares, types translated from Windows.h

Type WndEventArgs
  wParam  As Long
  lParam  As Long
  hWnd    As Dword
  hInst   As Dword
  wWidth  As Word
  wHeight As Word
  wX      As Word
  wY      As Word
  wCharHt As Word
  xPos    As Word
  yPos    As Word
  szText  As Asciiz*128
End Type


Function fnWndProc_OnCreate(wea As WndEventArgs) As Long
  Local tm As TEXTMETRIC
  Local hDC As DWord

  hDC=GetDC(wea.hWnd)
  Call GetTextMetrics(hDC,tm)
  wea.wCharHt=tm.tmHeight
  Call ReleaseDC(wea.hWnd,hDC)
  MsgBox("wea.wCharHt=" & Trim$(Str$(tm.tmHeight))) 'If using Console Compiler remark this
  'Print "wea.wCharHt=" & Trim$(Str$(tm.tmHeight))  'line out and use Print instead!

  fnWndProc_OnCreate=0
End Function


Function fnWndProc_OnMouseMove(wea As WndEventArgs) As Long
  wea.wX=LoWrd(wea.lParam) : wea.wY=HiWrd(wea.lParam)
  Call InvalidateRect(wea.hWnd,ByVal %NULL,%TRUE)
 
  fnWndProc_OnMouseMove=0
End Function


Function fnWndProc_OnSize(wea As WndEventArgs) As Long
  wea.wWidth=LoWrd(wea.lParam) : wea.wHeight=HiWrd(wea.lParam)
  Call InvalidateRect(wea.hWnd,ByVal %NULL,%TRUE)
 
  fnWndProc_OnSize=0
End Function


Function fnWndProc_OnChar(wea As WndEventArgs) As Long
  wea.szText=wea.szText+Chr$(wea.wParam)
  Call InvalidateRect(wea.hWnd,ByVal %NULL,%TRUE)

  fnWndProc_OnChar=0
End Function


Function fnWndProc_OnLButtonDown(wea As WndEventArgs) As Long
  If wea.wParam=%MK_LBUTTON Then
     wea.xPos=LoWrd(wea.lParam) : wea.yPos=HiWrd(wea.lParam)
     Call InvalidateRect(wea.hWnd,ByVal 0,%TRUE)
  End If

  fnWndProc_OnLButtonDown=0
End Function


Function fnWndProc_OnPaint(wea As WndEventArgs) As Long
  Local szLine As Asciiz*48
  Local ps As PAINTSTRUCT
  Local hDC As Long

  hDC=BeginPaint(wea.hWnd,ps)
  szLine="MouseX="+Trim$(Str$(wea.wX)) & "  MouseY="+Trim$(Str$(wea.wY))
  TextOut(hDC,0,0,szLine,Len(szLine))
  szLine="wea.wWidth="+Trim$(Str$(wea.wWidth)) & " wea.wHeight=" + Trim$(Str$(wea.wHeight))
  TextOut(hDC,0,16,szLine,Len(szLine))
  TextOut(hDC,0,32,wea.szText,Len(wea.szText))
  If wea.xPos<>0 And wea.yPos<>0 Then
     szLine="WM_LBUTTONDOWN At (" & Trim$(Str$(wea.xPos)) & "," & Trim$(Str$(wea.yPos)) & ")"
     TextOut(hDC,wea.xPos,wea.yPos,szLine,Len(szLine))
     wea.xPos=0 : wea.yPos=0
  End If
  Call EndPaint(wea.hWnd,ps)

  fnWndProc_OnPaint=0
End Function


Function WndProc(ByVal hWnd As Long,ByVal wMsg As Long,ByVal wParam As Long,ByVal lParam As Long) As Long
  Static wea As WndEventArgs

  Select Case As Long wMsg
    Case %WM_CREATE
      wea.hWnd=hWnd : wea.lParam=lParam : wea.wParam=wParam
      WndProc=fnWndProc_OnCreate(wea)
      Exit Function
    Case %WM_MOUSEMOVE
      wea.hWnd=hWnd : wea.lParam=lParam : wea.wParam=wParam
      WndProc=fnWndProc_OnMouseMove(wea)
      Exit Function
    Case %WM_SIZE
      wea.hWnd=hWnd : wea.lParam=lParam : wea.wParam=wParam
      WndProc=fnWndProc_OnSize(wea)
      Exit Function
    Case %WM_CHAR
      wea.hWnd=hWnd : wea.lParam=lParam : wea.wParam=wParam
      WndProc=fnWndProc_OnChar(wea)
      Exit Function
    Case %WM_LBUTTONDOWN
      wea.hWnd=hWnd : wea.lParam=lParam : wea.wParam=wParam
      WndProc=fnWndProc_OnLButtonDown(wea)
      Exit Function
    Case %WM_PAINT
      wea.hWnd=hWnd : wea.lParam=lParam : wea.wParam=wParam
      WndProc=fnWndProc_OnPaint(wea)
      Exit Function
    Case %WM_DESTROY
      Call PostQuitMessage(0)
      WndProc=0
      Exit Function
  End Select

  WndProc=DefWindowProc(hWnd,wMsg,wParam,lParam)
End Function


Function WinMain(ByVal hIns As Long,ByVal hPrevIns As Long,ByVal lpCmdLine As Asciiz Ptr,ByVal iShow As Long) As Long
  Local szClassName As Asciiz*6
  Local wc As WndClassEx
  Local hMainWnd As Dword
  Local Msg As tagMsg

  szClassName="Form1"
  wc.cbSize=SizeOf(wc)                               : wc.style=0
  wc.lpfnWndProc=CodePtr(WndProc)                    : wc.cbClsExtra=0
  wc.cbWndExtra=0                                    : wc.hInstance=hIns
  wc.hIcon=LoadIcon(%NULL,ByVal %IDI_APPLICATION)    : wc.hCursor=LoadCursor(%NULL,ByVal %IDC_ARROW)
  wc.hbrBackground=GetStockObject(%WHITE_BRUSH)      : wc.lpszMenuName=%NULL
  wc.lpszClassName=VarPtr(szClassName)               : wc.hIconSm=LoadIcon(%NULL,ByVal %IDI_APPLICATION)
  Call RegisterClassEx(wc)
  hMainWnd=CreateWindowEx(0,szClassName,"Form1",%WS_OVERLAPPEDWINDOW,200,100,325,300,%HWND_DESKTOP,0,hIns,ByVal 0)
  Call ShowWindow(hMainWnd,iShow)
  While GetMessage(Msg,%NULL,0,0)
    Call TranslateMessage(Msg)
    Call DispatchMessage(Msg)
  Wend

  WinMain=msg.wParam
End Function