Fred's Tutorial#8: Scrolling. The Very Basics of Scroll Code - Simplest.bas

Started by Frederick J. Harris, September 10, 2007, 12:47:05 AM

Previous topic - Next topic

0 Members and 1 Guest are viewing this topic.

Frederick J. Harris

     This is the first and simplest of a series of programs that demonstrate the concepts involved in scrolling in Windows.  A comment thread has been started at...

http://www.jose.it-berater.org/smfforum/index.php?topic=1255.0

     In the beginning of Tutorial #1 at    http://www.jose.it-berater.org/smfforum/index.php?topic=1243.0     can be found a listing, descriptions, and links to the other tutorials.  Scrolling is actually somewhat involved, and the programs in this series will hopefully further improve your understanding of the messaging system in Windows, and help you to understand scrolling.  I might add that in all of Charles Petzold's books, including his recent .NET books, he spent a lot of effort on teaching scrolling.  He probably felt it was foundational type material that all serious programmers should understand.

     The program below, Simplest.bas, is about the simplest example I could conjure up that will actually scroll a window vertically.  There are only six functions, and, subtracting WinMain() and the Window Procedure (fnWndProc() -- which in most of my programs is just a shell that acts as a main switchboard and calls the actual functions that do the real work), there are only four that constitute the core of the program's functionality.  Before I continue, compile and run the program, as that will help you understand my further explanations.

     If you ran the program you may have noted the following: First, there are sixteen lines of text but only about eleven are visible at a time.  To see all sixteen lines one must use the vertical scroll bar.  Note the equate on line four of the program -- %LAST_LINE = 15.  That equate is used to dimension a string array as the first line of code in fnWndProc_OnCreate().  Remember that the very first message Windows sends to a window on creation is WM_CREATE.  That gives an app the opportunity to run initialization code.

     Second, if you tried to resize or maximize the window you found that you couldn't.  All you could do was move it around on the screen or minimize/restore it.  That is because in WinMain() I didn't include any window styles in the CreateWindow() function call that would enable Window sizing.  The topic of Window Styles in particular and bits in general will have to await another tutorial after scrolling.  The reason I did this is because I wanted to create the simplest possible program that demonstrates scrolling, and window resizing code somewhat complicates that endeavor.

     Speaking in very high level and general terms, scrolling will involve an interplay between the processing of two messages 1) WM_VSCROLL (or WM_HSCROLL which I won't cover here, but the general technique is the same) and WM_PAINT.  For this reason it is necessary to create several variables that are necessary to scroll logic that will maintain their values between function calls.  One poor way of doing this is to create global variables.  The duration of a global variable is the entire lifetime of a program (while it is in memory), and the scope of a global variable is the entire program  where it is defined.  Our needs here are considerably less, and a more restrictive scope will suit our needs in scrolling and that scope is termed static.  A variable defined as static has program lifetime duration but its scope is restricted to the procedure where it is defined and procedures to which it is passed.  Note in fnWndProc() the variable declaration:

     Static wea As WndEventArgs

     It is within the fields of this Type that our necessary variables to support scrolling will be passed between our procedure to handle scroll logic and our procedure to handle window painting.

     So lets begin.  Unlike DOS, Windows is a totally graphics system, even where text is concerned.  There is no distinction as with DOS between text modes and graphics modes.  Everything is graphics and pixels.  To set up vertical scroll logic one must determine the vertical height of a character at the font presently in force on a system, and then the vertical height of the window in which lines of text at a specific font will be scrolled.  Let us begin with the first (I know this sounds hard, but it isn't).

     Let's start by slightly modifying the fnWndProc_OnCreate() message handler below to produce some output for us to study.  It is critical to know the values your variables are taking on as a program runs.  Some people use and like debuggers for that purpose.  I have always personally preferred an output log to log my program's execution.  Modify that procedure as shown below, or simply copy and paste this code into the program then comment out the original fnWndProc_OnCreate(), then rerun the program.


Function fnWndProc_OnCreate(wea As WndEventArgs) As Long
  Local lpMinPos,lpMaxPos As Long
  Local tm As TEXTMETRIC
  Register i As Long
  Local hDC As Long
  Local rc As RECT
  Local fp As Integer
 
  fp=FreeFile
  Open "Output.txt" For Output As #fp
  ReDim strLines(%LAST_LINE) As String
  For i = 0 To %LAST_LINE
    strLines(i)=Str$(i) & "  " & _
    "PowerBASIC Scrolling Demonstration (Simplest)"
  Next i
  hDC=GetDC(wea.hWnd)               'In Windows, you need something called a 'Device Context' to get
  Call GetTextMetrics(hDC,tm)       'info on how things such as text will be displayed.  To find out
  wea.cyChar=tm.tmHeight            'how many lines of text will be visible on any screen size chosen
  Call ReleaseDC(wea.hWnd,hDC)      'by a user, we need the height of a character, 'wea.cyChar'.
  Call GetClientRect(wea.hWnd,rc)   'wea.hWnd is an 'input' parameter, rc is an 'output' parameter
  wea.iBegin=0  : wea.iEnd=rc.nBottom\wea.cyChar-1  'and its members will contain size of client area
  wea.iCtTotLns=%LAST_LINE+1                        'of window.
  wea.iCtVisLns=rc.nBottom\wea.cyChar
  Call SetScrollRange(wea.hWnd,%SB_VERT,0,wea.iCtTotLns-wea.iCtVisLns,%FALSE)
  Call GetScrollRange(wea.hWnd,%SB_VERT,lpMinPos,lpMaxPos)
  Print #fp,"wea.cyChar                       = "wea.cyChar
  Print #fp,"rc.nTop                          = "rc.nTop
  Print #fp,"rc.nBottom                       = "rc.nBottom
  Print #fp,"rc.nBottom/wea.cyChar            = "rc.nBottom/wea.cyChar
  Print #fp,"rc.nBottom\wea.cyChar            = "rc.nBottom\wea.cyChar
  Print #fp,"wea.iCtTotLns                    = "wea.iCtTotLns
  Print #fp,"wea.iCtVisLns                    = "wea.iCtVisLns
  Print #fp,"lpMinPos                         = "lpMinPos
  Print #fp,"lpMaxPos                         = "lpMaxPos
  Close #fp
 
  fnWndProc_OnCreate=0
End Function


     Note that output.txt will be opened in the program's folder.  On my machine it produced the
following results on a program run:


wea.cyChar                       =  16
rc.nTop                          =  0
rc.nBottom                       =  181
rc.nBottom/wea.cyChar            =  11.3125
rc.nBottom\wea.cyChar            =  11
wea.iCtTotLns                    =  16
wea.iCtVisLns                    =  11
lpMinPos                         =  0
lpMaxPos                         =  5


     Perhaps I should back up and explain the whole procedure in detail from the beginning.  The first two lines in the modified procedure obtain a file handle and open a text file.  The next statement ( Redim ) ultimately causes a memory buffer to be allocated when the program runs into which %LAST_LINE + 1 lines of text will be copied.  The For Loop places the %LAST_LINE + 1 lines of text into the allocated storage.

     In order to obtain access to Windows internal storage mechanism containing details of a graphics nature (for example, the vertical height of the font presently being used) you obtain a handle to a device context with either the GetDC() or BeginPaint() Api functions.  BeginPaint() can only be used during the processing of a %WM_PAINT message, so in fnWndProc_OnCreate() we must use GetDC().  The only parameter to that function is the window handle which in our case is stored in wea.hWnd (it was put there in fnWndProc() -- check it out!).  Having obtained an hDC we can call 'GetTextMetrics() to obtain info on the height in pixels of a character.

     Back in Form3 I attempted to provide something of an explanation of using and converting Windows Api functions for use in PowerBASIC programs, and here I'll try to elaborate a bit further.  GetTextMetrics() is a pretty simple function (most of them are) and all you need to do is pass the proper parameters.  The first parameter is an 'input' parameter and that means the function needs input from us to do its job.  It wants to have a handle to a device context and this we just obtained in the previous statement, so we just 'plug it in'.  The second parameter is what is termed an 'output' parameter, and it is called this because rather than returning the result of the function through a return value, the result of the function call is returned through a function parameter, and through a fairly complicated TYPE at that.  At this point you should open up Win32Api.inc in either Notepad or the programming editor you are using to study the TEXTMETRIC type.  Win32Api.inc can be found in the \Win32Api directory of your PowerBASIC installation.  When open do a search for TEXTMETRIC by typing in

     Type TEXTMETRIC

     I'm telling you this not because I think you should memorize the 20 members or the TEXTMETRIC TYPE, or even know what they all are, but rather to get you in the habit of searching through the Win32Api.inc file to learn the definitions of types, equates, and functions with which you are not familiar.  If you have examined this type in Win32Api.inc you will have noticed that the first member is tmHeight.  On my machine a value of 16 pixels was returned.  This value is saved in the statically defined wea type because it is one of the variables that will be used many times throughout the program's duration.  The cyChar can be interpreted as count of pixels along the vertical y axis.

     GetClientRect() is then called to obtain the height in pixels of the client rectangle, i.e., that part of the window in which painting occurs.  As with GetTextMetrics() an output parameter is used to extract the information from Windows through a type, in this case a RECT type.  A pattern should be becoming obvious to you by now in terms of dealing with the Windows API.  Variables of a specific type predefined for you in the Windows include files are declared in procedures you write and you both feed information to the Api through these variables and extract information from Windows through them.  In the case above a value of 181 pixels was returned through the rc.nBottom member of the RECT type.  Again, you should search for Type RECT in the Win32Api.inc file.  This type is so commonly used I'd actually consider memorizing it.

     So at this point we know that a character takes up about 16 pixels, and we have 181 pixels 'up and 'down' in the window.  A division yields 11.3125 lines, but since I decided to not display partial lines I used integer division to truncate that value to 11, which value the program assigns to wea.iCtVisLns, 'iCountVisibleLines', and that of course is the count of the number of full lines visible at any one time in the window.  Shortly after that the program sets the value of wea.iCtTotLns which is the one based count of the total numbers of lines in the memory buffer. Since the Redim statement set the buffer like so...

     ReDim strLines(%LAST_LINE) As String

     ...and %LAST_LINE = 15, then the count stored in wea.iCtTotLns will be one more than %LAST_LINE because we need to count strLines(0) too.  The final critical ingredient in fnWndProc_OnCreate() is the following Api call...

     Call SetScrollRange(wea.hWnd, %SB_VERT, 0, wea.iCtTotLns - wea.iCtVisLns, %FALSE)

     It is defined as follows in MSDN...


     BOOL SetScrollRange
     (
      HWND hWnd,    // handle to window with scroll bar
      int nBar,     // scroll bar flag
      int nMinPos,  // minimum scrolling position
      int nMaxPos,  // maximum scrolling position
      BOOL bRedraw  // redraw flag
     );


     The first parameter should be self-explanatory at this point.  The second parameter is an equate that informs Windows of whether the call applies to a vertical scroll bar, a horizontal scroll bar, or a scroll bar control.  In this case we have a vertical scroll bar that is part of the window as opposed to being a separate control, as would be the case with a  scroll bar control.  The next two parameters are critical and are where the 'rubber meets the road' with scrolling.  Note that immediately after calling SetScrollRange() I called the complementary Api function GetScrollRange() so as to print out the nMinPos and nMaxPos variables. Below are the results again...


     wea.iCtTotLns                    =  16
     wea.iCtVisLns                    =  11
     lpMinPos                         =  0
     lpMaxPos                         =  5


     In interpreting this information it is worthwhile to use the concept of 'state'.  If there are sixteen lines of text and only eleven are visible, then the program will have to allow for the existence of six distinct 'states' where the following lines of text from the text buffer will be made visible through scrolling...


                 wea.iBegin             wea.iEnd

    state     top line visible     bottom line visible
    ==================================================
     0              0                      10
     1              1                      11
     2              2                      12
     3              3                      13
     4              4                      14
     5              5                      15


     Note that when the program first starts it is in state '0', that is, lines zero ( strLines(0) ) through ten (strLines(10)) are visible (eleven lines, right?).  Also, of course, the scroll bar thumb is right against the upper boundary of the scroll bar track.  By clicking the down button on the scroll bar track one should be able to cause the program to jump to state '1' where lines 1 through 11 are visible, and so on and so forth until the maximum state is reached, that is state '5' where lines 5 through 15 are visible.  Remember several paragraphs up I spewed forth the broad generalization that scroll bars are at base a graphical user interface device for picking an integer number from a range of values?  Well, the states shown above, i.e., 0 through 5 are just that!

     Unfortunately, none of what we have covered so far will cause text to scroll in a window.  However, we have built the solid foundation on which scrolling is based, and that is setting up the integral range of legal values or states in which the program can exist.  What is left to do is understand what occurs when a mouse click happens to either the up or down scroll bar arrow.  But before we do that lets take a look at how the initial eleven lines of text numbered zero through ten are initially shown when the program first becomes visible on screen.

   The first Windows message this particular program responds to following the WM_CREATE message discussed at length above is the WM_PAINT message.  Please note two members of the WndEventArgs Type that I haven't discussed yet - iBegin and iEnd.  These are actually set back in WM_CREATE and are simply the integer values representing the array index of the lines first visible when the window is first painted.  When the window is in state zero, wea.iBegin is set to 0 and wea.iEnd is set to 10.  Now note why this type is declared as static in fnWndProc().  When we set the beginning and end lines through which a For construct must loop back in WM_CREATE when we determined the size of the window and the number of lines visible, those values must persist through function calls and most particularly after the WM_CREATE message terminates.  If you care to see the devastating impact declaring wea as local instead of static back in fnWndProc() would have on this program just try that simple exercise and rerun the program.  When the window is first made visible by the operating system a WM_PAINT message will be sent to the window procedure fnWndProc().  At that time fnWndProc() will call fnWndProc_OnPaint() and all the necessary prerequisites for displaying those first 11 lines of text and for supporting further scrolling will have already have been set up during the processing of the initial WM_CREATE message.  Specifically, by examining fnWndProc_OnPaint() you should see that a For loop makes lines 0 through 10 visible.  Further note that the painting of the lines is at a spacing of wea.cyChar apart.

   Hopefully the output to the Output.txt file was of some use to you in understanding the processing of the WM_CREATE message, so lets do the same with the WM_PAINT message.  Comment out the fnWndProc_OnPaint() in the program below and replace it with the following:


Function fnWndProc_OnPaint(wea As WndEventArgs) As Long
  Local hDC As Long,y As Long
  Local LpPaint As PAINTSTRUCT
  Local fp As Integer
  Register i As Long
 
  fp=Freefile
  Open "Output.txt" For Append As #fp
  hDC = BeginPaint(wea.hWnd, LpPaint)
  Print #fp,
  Print #fp,"Top Of fnWndProc_OnPaint()"
  Print #fp,
  Print #fp,"wea.iBegin = "wea.iBegin
  Print #fp,"wea.iEnd   = "wea.iEnd
  Print #fp,
  Print #fp, " i             y        y * wea.cyChar  "
  Print #fp, "========================================"
  For i = wea.iBegin To wea.iEnd
    Print #fp,i,y,y*wea.cyChar
    Call TextOut(hDC,0,y*wea.cyChar,ByVal StrPtr(strLines(i)),Len(strLines(i)))
    Incr y
   Next i
   Call EndPaint(wea.hWnd,LpPaint)
   Print #fp,
   Print #fp,"Leaving fnWndProc_OnPaint()"
   Close #fp
   
   fnWndProc_OnPaint=0
End Function


   Below is the output generated by the processing of the first WM_PAINT message.  If you scroll the window several times and examine the output you'll clearly see how the incrementing and decrementing of wea.iBegin and wea.iEnd affect the output file and the lines of text visible in the window.


   Top Of fnWndProc_OnPaint()

   wea.iBegin =  0
   wea.iEnd   =  10

    i             y        y * wea.cyChar
   ========================================
    0             0             0
    1             1             16
    2             2             32
    3             3             48
    4             4             64
    5             5             80
    6             6             96
    7             7             112
    8             8             128
    9             9             144
    10            10            160


   Leaving fnWndProc_OnPaint()

   The TextOut() Api function in fnWndProc_OnPaint() is somewhat complex, so I'll spend some time on explaining it.  TextOut() is defined at MSDN as follows:


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
);


     The tricky part for a PowerBASIC programmer is the fourth parameter.  What this function needs to see is the address of the first character of the string to be displayed.  While the inner workings of this function are unknown to us - and in that sense it is something of a 'black box' - we can count on the fact that if we can get the address of the first character of a string to this function, it will gladly do its job and display the string for us.  Further, it will do this for us irregardless of what programming language we are using to call it, whether it be C, assembler, fortran running on a PC, Cobol, or anything else.  Now in the Win32Api.inc file we have the following PowerBASIC declaration of TextOut():


DECLARE FUNCTION TextOut LIB "GDI32.DLL" ALIAS "TextOutA" _
( _
  BYVAL hdc AS DWORD, _
  BYVAL x AS LONG, _
  BYVAL y AS LONG, _
  lpString AS ASCIIZ, _
  BYVAL nCount AS LONG _
) AS LONG


     The address of the first character of each string in the dynamic string array strLines() can be returned by the StrPtr function (if you are in doubt, read about it in your PowerBASIC help), and so that is how I came by the string pointer needed by this function.  Because I chose to obtain the address of a dynamic string through the StrPtr function I need to bypass Basic's default By Reference parameter passing convention and pass that address by value.  That is why you see 'Byval StrPtr(strLines(i)) in my function call in fnWndProc_OnPaint().  Had I defined the string array as an array of Asciiz strings, say - szStrings(), then my function call would have been more in keeping with PowerBASIC's declare and would have looked like this...

     Call TextOut(hDC,0,y*wea.cyChar,szStrings(i),Len(szStrings(i))

     In that case the PowerBASIC compiler's construction of the function call mechanism would have passed the address of each string to the underlying Api function.  I didn't want to make this too easy for you by doing this, and in any case dynamic strings are usually easier to work with.

     Play with this some and examine that Output.txt log file and eventually you'll come to understand the deterministic inevitability of the whole thing.  All the mystery that may be left for you at this point is that strange fnWndProc_OnVScroll() function.

     That function will be called from fnWndProc() when the latter receives a WM_VSCROLL message.  Packaged along with the message in the wParam parameter will be a numeric constant (an equate in Win32Api.inc) specifying which particular scroll message is being sent.  That is why the Select Case logic in fnWndProc_OnVScroll() checks the lower two bytes of wParam looking for either SB_LINEUP or SB_LINEDOWN.  If  SB_LINEDOWN then wea.iBegin and wea.iEnd are incremented and conversely if SB_LINEUP then these variables are decremented.  Checks have to be put in place to prevent either of these variables from assuming values outside of the allocated storage, or otherwise GPFs would occur.  Note the Api function call...

                          Call SetScrollPos(wea.hWnd,%SB_VERT,wea.iBegin,%TRUE)

which call is necessary to position correctly the scroll bar thumb after each position change.  The first parameter is the window handle of the window with the scroll bar.  The second parameter is an equate specifying whether the call applies to a vertical or horizontal scroll bar.  The third parameter specifies an integer within the scroll bar range specified in the SetScrollRange() function call in fnWndProc_OnCreate().  The fourth specifies whether we want the scroll bar redrawn.  Finally, the motor that makes the whole outrageous thing work is that InvalidateRect() call in each of the case statements.  Don't forget that the For loop that actually paints the screen with a different set of lines of text is not in fnWndProc_OnVScroll() but in fnWndProc_OnPaint()!  So neither incrementing or decrementing iBegin and iEnd, nor repositioning the scroll bar thumb will in itself cause the screen to scroll.  What is needed after these events occur is a WM_PAINT message to be generated, and this can be precipitated by an InvalidateRect() call.  Windows keeps track internally of regions that are invalid and in need of repainting, and by placing that %NULL in the second parameter of the function call, it informs Windows that the entire client area is invalid.  I'd recommend you search your MSDN or Win Api help file for InvalidateRect and related documents.

     One final point I'd like to make.  In examining the overall architecture of the application it might occur to you that the For loop that repaints the window with incremented or decremented lines of text could be placed in fnWndProc_OnVScroll() instead of in the WM_PAINT message handler.  After all, I did manage to get a handle to a device context in fnWndProc_OnCreate() so as to get font info, and this very same device context handle is the key needed to make TextOut() work.  Well, this is all very true, but before you head down this road let me warn you that you won't be satisfied with the results, because the text you paint on the window won't persist through window resizing, or in cases where your window is uncovered by other windows the user may move about on the screen.

     If you are serious about learning SDK style Windows programming I'd recommend you spend a fair amount of time playing with this app, and even try to start from scratch in developing your own version of it.  In my next tutorial we'll complicate 'Simplest.bas' considerably by using the ScrollWindow() Api function in conjunction with the SCROLLINFO Type, and by adding the functionality to respond to more scroll essages.


#Compile Exe   "Simplest"   '''Simplest.bas
#Include "Win32api.inc"
Global strLines() As String   'Array to hold lines to be scrolled.
%LAST_LINE  = 15              'Highest zero based array position holding text data.

Type WndEventArgs             'This type will be declared as static in the window procedure
  hIns As Dword               'and will be passed to the various message handling functions.
  hWnd As Dword               'In that way several variables critical to the development of
  wParam As Dword             'scroll logic such as the minimum and maximum scroll bar
  lParam As Dword             'positions will maintain their values between function calls.
  cyChar As Long
  iBegin As Long
  iEnd As Long
  iCtVisLns As Long
  iCtTotLns As Long
End Type


Function fnWndProc_OnCreate(wea As WndEventArgs) As Long
  Local tm As TEXTMETRIC
  Register i As Long
  Local hDC As Long
  Local rc As RECT
   
  ReDim strLines(%LAST_LINE) As String
  For i = 0 To %LAST_LINE
    strLines(i)=Str$(i) & "  " & _
    "PowerBASIC Scrolling Demonstration (Simplest)"
  Next i
  hDC=GetDC(wea.hWnd)               'In Windows, you need something called a 'Device Context' to get
  Call GetTextMetrics(hDC,tm)       'info on how things such as text will be displayed.  To find out
  wea.cyChar=tm.tmHeight            'how many lines of text will be visible on any screen size chosen
  Call ReleaseDC(wea.hWnd,hDC)      'by a user, we need the height of a character, 'wea.cyChar'.
  Call GetClientRect(wea.hWnd,rc)   'wea.hWnd is an 'input' parameter, rc is an 'output' parameter
  wea.iBegin=0                      'and its members will contain size of client area of window.
  wea.iEnd=rc.nBottom\wea.cyChar-1  'Subtract 1 from wea.iCtVisLns to account for zero based strLines()
  wea.iCtTotLns=%LAST_LINE+1        'array
  wea.iCtVisLns=rc.nBottom\wea.cyChar
  Call SetScrollRange(wea.hWnd,%SB_VERT,0,wea.iCtTotLns-wea.iCtVisLns,%FALSE)
 
  fnWndProc_OnCreate=0
End Function


Function fnWndProc_OnVScroll(wea As WndEventArgs) As Long
  Select Case LoWrd(wea.wParam)
    Case %SB_LINEUP
      Decr wea.iBegin
      If wea.iBegin < 0 Then
         wea.iBegin = 0
      End If
      wea.iEnd=wea.iBegin+wea.iCtVisLns-1
      Call SetScrollPos(wea.hWnd,%SB_VERT,wea.iBegin,%TRUE)
      Call InvalidateRect(wea.hWnd,ByVal %NULL,%TRUE)
    Case %SB_LINEDOWN
      Incr wea.iBegin
      If wea.iBegin > wea.iCtTotLns-wea.iCtVisLns Then
        wea.iBegin=wea.iCtTotLns-wea.iCtVisLns
      End If
      wea.iEnd=wea.iBegin+wea.iCtVisLns-1
      Call SetScrollPos(wea.hWnd,%SB_VERT,wea.iBegin,%TRUE)
      Call InvalidateRect(wea.hWnd,ByVal %NULL,%FALSE)
  End Select
 
  fnWndProc_OnVScroll = 0
End Function


Function fnWndProc_OnPaint(wea As WndEventArgs) As Long
  Local hDC As Long,y As Long
  Local LpPaint As PAINTSTRUCT
  Register i As Long
 
  hDC = BeginPaint(wea.hWnd, LpPaint)
  For i = wea.iBegin To wea.iEnd
    Call TextOut(hDC,0,y*16,ByVal StrPtr(strLines(i)),Len(strLines(i)))
    Incr y
  Next i
  Call EndPaint(wea.hWnd,LpPaint)
 
  fnWndProc_OnPaint=0
End Function


Function fnWndProc_OnClose(wea As WndEventArgs) As Long
  Erase strLines()
  Call PostQuitMessage(0)
 
  fnWndProc_OnClose=0
End Function


Function fnWndProc(ByVal hWnd As Long,ByVal wMsg As Long,ByVal wParam As Long,ByVal lParam As Long) As Long
  Static wea As WndEventArgs
 
  Select Case wMsg
    Case %WM_CREATE
      wea.hWnd=hWnd : wea.wParam=wParam : wea.lParam=lParam
      fnWndProc = fnWndProc_OnCreate(wea)
      Exit Function
    Case %WM_VSCROLL
      wea.hWnd=hWnd : wea.wParam=wParam : wea.lParam=lParam
      fnWndProc=fnWndProc_OnVScroll(wea)
      Exit Function
    Case %WM_PAINT
      wea.hWnd=hWnd : wea.wParam=wParam : wea.lParam=lParam
      fnWndProc=fnWndProc_OnPaint(wea)
      Exit Function
    Case %WM_CLOSE
      wea.hWnd=hWnd : wea.wParam=wParam : wea.lParam=lParam
      fnWndProc=fnWndProc_OnClose(wea)
      Exit Function
  End Select
 
  fnWndProc=DefWindowProc(hWnd, wMsg, wParam, lParam)
End Function


Function WinMain(ByVal hIns As Long, ByVal hPrev As Long,ByVal lpCL As Asciiz Ptr, ByVal Is As Long) As Long
  Local hMainWnd As Dword,dwStyle As Dword
  Local winclass As WndClassEx
  Local szAppName As Asciiz*16
  Local Msg As tagMsg
 
  szAppName="Simplest.bas"
  winclass.cbSize=SizeOf(winclass)                       :winclass.style=%CS_HREDRAW Or %CS_VREDRAW
  winclass.lpfnWndProc=CodePtr(fnWndProc)                :winclass.cbClsExtra=0
  winclass.cbWndExtra=0                                  :winclass.hCursor=LoadCursor(%NULL, ByVal %IDC_ARROW)
  winclass.hIcon=LoadIcon(%NULL, ByVal %IDI_APPLICATION) :winclass.hInstance=hIns
  winclass.hbrBackground=GetStockObject(%WHITE_BRUSH)    :winclass.lpszMenuName=%NULL
  winclass.lpszClassName=VarPtr(szAppName)               :winclass.hIconSm=0
  Call RegisterClassEx(winclass)
  dwStyle=%WS_CAPTION Or %WS_MINIMIZEBOX Or %WS_VISIBLE Or %WS_VSCROLL Or %WS_SYSMENU
  hMainWnd=CreateWindowEx(0,szAppName,szAppName,dwStyle,200,100,375,206,0,0,hIns,ByVal 0)
  Call ShowWindow(hMainWnd,Is)
  Call UpdateWindow(hMainWnd)
  While GetMessage(Msg,%NULL,0,0)
    TranslateMessage Msg
    DispatchMessage Msg
  Wend
 
  Function=msg.wParam
End Function