Fred's Tutorial #3: Windows API Tutorial: Child Controls/Text Output To Screen

Started by Frederick J. Harris, August 20, 2007, 09:43:32 PM

Previous topic - Next topic

0 Members and 1 Guest are viewing this topic.

Frederick J. Harris

'Form3.bas Compilation Tested With PBWin 8.01, 7.04, 6.11
'*****************************************************************************************
'Introduction
'*****************************************************************************************
The detractors of Sdk programming style will oftentimes cite the unwieldy WndProc() function with its massive Select Case construct that in bad cases can run on for thousands of lines of code.  This is because that all important function is where every message deriving from user input will be channeled.  In this program I created a TYPE WndEventArgs - Windows Event Arguements - that will be filled out in the Window procedure and passed to various message processing procedures created to process only one specific message.  For example, when the main window is first created with a call to CreateWindowEx(), a %WM_CREATE message will be passed to fnWndProc.  In that procedure the fields of WndEventArgs will be filled in and the procedure

                    fnWndProc_OnCreate(wea As WndEventArgs)

will be called.  Likewise, %WM_COMMAND messages deriving from multitudes of sources, i.e., menu selections, listboxes, comboboxes, buttons, etc., will be directed to a switchboard procedure fnWndProc_OnCommand() for further redirection of those messages.  In that way the code can essentially be modularized in the exact manner Visual Basic did with its event procedures.  See cmdButton1_Click() below.

Also noteworthy in this program is a CreateWindowEx() call in response to a button click (cmdButton2_Click()) where an output screen window is created and displayed.  Right above the window procedure that handles messages for this second window, fnOutputScreen(), I discuss at length the issues involved in translating and calling Win Api C functions from PowerBasic, particularly in regard to strings.

Finally, in this program I broke out the Window Class initialization code from the
WinMain() function to show how this modularization can be done.  While I never do it myself, you will frequently see this done in the documentation.  The purpose for it is to seperate application initialization code (filling out a Window Class type) from instance instantiation code (creating the window and falling into the message processing loop, and to abort the app if failures occur in either.  This was somewhat more of an issue with 16 bit Windows, but may have merit again as modern computer users have grown accostomed to never closing programs, nor turning off their computers.

It would be a good idea to compile and run this program before reading explanations below.
'******************************************************************************************
'End Introduction
'******************************************************************************************


#Compile Exe
#Dim All
#Include "Win32api.inc"
%IDC_BUTTON1=1501
%IDC_BUTTON2=1502

'******************************************************************************************
'Three of these four variables are about the most important in windows programming, as they
'are the message passing mechanism between your hardware, Windows and your program.  I
'packaged them into a TYPE as it tends to keep the parameter list in procedures shorter.
'Further, if your program would benifit from it, you can add your own variables to it.
'******************************************************************************************
Type WndEventArgs       'Windows Event Arguements
 wParam As Long
 lParam As Long
 hWnd   As Dword
 hInst  As Dword
End Type

'******************************************************************************************
'Function Type:  Window Procedure
'Function Name:  fnOutputScreen
'******************************************************************************************
'When we called RegisterClassEx() the second time down in AppInitialize(), we set the
'window procedure for the OutputScreen class to the address of fnOutputScreen with this
'statement...
'
'                     wcl.lpfnWndProc=CodePtr(fnOutputScreen).
'
'For 'lpfnWndProc' read 'long pointer to function window procedure'.  The long pointer
'terminology is a hold over from 16 bit Windows.  So all messages to the OutputScreen window
'are sent to fnOutputScreen.  The only functionality of the window is to print the words
'"This Is The Output Screen" shortly after the window is created during the WM_PAINT msg.

'Since using the Windows Api is something of a skill in itself, let me try to provide a little
'insight here.  Below is how the TextOut() function is defined in the Win32 Api docs:
'
'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              <<that ugly thing is a char pointer!
'  int cbString        // number of characters in string
');
'
'Of course, this is a C definition of that function, and in that language there is a seemingly
'infinite number of ways of describing a 32 bit variable because of the typedef keyword, which,
'among other things, allows for the creation of synonyms for existing data types.  Every one
'of the variables in the above function declaration are 32 bit integers. The PowerBasic Declare
'for that function is in Win32Api.inc and the contents of this file you will have to become
'somewhat familiar with to do this style of programming. Here is the PowerBasic declare:
'
'DECLARE FUNCTION TextOut LIB "GDI32.DLL" ALIAS "TextOutA"
'(
' BYVAL hdc AS DWORD,
' BYVAL x AS LONG,
' BYVAL y AS LONG,
' lpString AS ASCIIZ,     'what needs to be passed here is the address of the string
' BYVAL nCount AS LONG
') AS LONG
'
'What I'm going to tell you now is important.  In C the default method of passing a function
'parameter is to make a copy of the variable and push that copy on the stack for retrieval by
'the called function.  In this way the function will not and can not alter the value of the
'passed parameter back in the calling function.  In C, if one wants to be able to alter or
'directly work with the address and hence value of a function parameter, then a pointer variable
'is used. The address held in the pointer variable will be pushed on the stack at the time of the
'function call and the function will work directly with that address, so that if any changes to
'the variable are made in the called function, upon return from the function back to the calling
'function that variable's value will have been changed.  Note that in the C definition of
'TextOut() the fourth parameter is a pointer to a character string.  In that function the string
'won't be altered in any way, however, TextOut() needs to have the address of the string to
'output it to the screen.
'
'Now in PowerBasic, the default parameter passing mechanism is by reference, which means that
'when you stick a variable in a parameter list to a function, that variables address will be
'directly passed to the function via the stack - the reverse situation from C.  The implication
'of all this is that in PowerBasic declares of Win32 Api functions you will see non-pointer
'parameters with the BYVAL keyword placed before them, and pointer variables oftentimes lacking
'any parameter passing method keyword due to the fact that by default PowerBasic passes pointers
'to called functions, i.e., Byref.  That is the situation above with the TextOut() function's
'fourth parameter.
'
'What TextOut() in GDI32.dll needs to see is the 32 bit address of the first character of the
'string that needs to be output. In PowerBasic you get that address with the Strptr() function
'for dynamic variable length strings, and the Varptr() function for Asciiz strings.  However,
'to pass that value to the function in PowerBasic you need to over ride the default Byref passing
'mechanism with the specific Byval override (see fnOutputScreen below).
'
'Another way to get the function to work is to declare an Asciiz Ptr in the function and do the
'following:
'
'  Local pszText As Asciiz Ptr
'
'  pszText=Strptr(strText)
'  Call TextOut(hDC,0,0,@pszText,Len(strText))
'
'I personally find that a somewhat obscure and obtuse way of doing something that in C is very
'straightforward.  Further, it requires an extra variable declaration and an extra statement on
'top of being obscure and obtuse.
'
'The reason I decided to go into some detail about this issue is that you'll need to understand it
'to deal with passing strings to other Api functions, for example with comboboxes, listboxes, etc.
'******************************************************************************************
Function fnOutputScreen(ByVal hWnd As Long,ByVal wMsg As Long,ByVal wParam As Long,ByVal lParam As Long) Export As Long
 Local lpPaint As PAINTSTRUCT      'a type needing to be allocated in order to get a handle to a device context
 Local strText As String
 Local hDC As Long

 Select Case wMsg
   Case %WM_PAINT
     hDC=BeginPaint(hWnd,LpPaint)                 'gives you access to graphics drawing capabilities of OS
     strText="This Is The Output Screen!"                     'text to be output
     Call TextOut(hDC,0,0,ByVal StrPtr(strText),Len(strText)) 'windows version of Print statement
     Call EndPaint(hWnd,lpPaint)                  'paired with BeginPaint() >> Releases resources
     fnOutputScreen=0
     Exit Function
   Case %WM_DESTROY
     Call ShowWindow(hWnd,%SW_HIDE)               'Another tricky Api Fn for locating a window
     Call ShowWindow(FindWindowEx(ByVal 0,ByVal 0,"Form3","Form3"),%SW_SHOWNORMAL)
     fnOutputScreen=0
     Exit Function
 End Select

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


'*********************************************************************************************
'Sub cmdButton1_Click()    Event Procedure
'*********************************************************************************************
'VB.NET refugees like myself should like this one!  In VB, if you start up a blank project,
'you'll get a default Form1 which can be run 'as is' by clicking the 'run' button.  If you
'double click a command button in the toolbox you'll get a command button added to your
'Form1, which, if double clicked on the form opens up a code window with...
'
'Private Sub Command1_Click()
'
'End Sub
'
'Well, below is the way to do it in PowerBasic
'**********************************************************************************************
Sub cmdButton1_Click()
 MsgBox("You Clicked Button #1!") 'For Console Compiler Use Print Instead.
 'Print "You Clicked Button #1!"
End Sub


'**********************************************************************************************
'cmdButton2_Click(wea As WndEventArgs)   Event Procedure
'**********************************************************************************************
'cmdButton2_Click() will be called when the user clicks button #2.  Windows will then
''package up' a %WM_COMMAND message for fnWndProc, and it will set the low 16 bits of wParam
'to the %IDC_BUTTON2, i.e., 2, and the high 16 bits of wParam to %BN_CLICKED, i.e., 0.  When
'this message is received in fnWndProc, the Select Case logic will sort out the message to
'a %WM_COMMAND case, fill out the WndEventArgs TYPE, then Call fnWndProc_OnCommand() as
'follows:
'           Case %WM_COMMAND
'             wea.wParam=wParam: wea.lParam=lParam: wea.hWnd=hWnd
'             Call fnWndProc_OnCommand(wea)
'
'Once inside fnWndProc_OnCommand(), that procedure will use more Select Case logic to
'determine which control, i.e., button in our case, is the sender or source of the message.
'It will do this by examining the low 16 bit value in wea.wParam to get the control ID set
'in the CreateWindowEx() function when the control was created.  If you look back in
'fnWndProc_OnCreate() you'll see it was the tenth parameter in the call. When this procedure
'determines that the source of the message was %IDC_BUTTON2, it calls cmdButton2_Click()
'as seen just below.
'
'Note the first function call is to Api function SystemParametersInfo().  Here is the definition
'for that function from the Win32 Api help:
'
'BOOL SystemParametersInfo(
' UINT  uiAction , // system parameter To query Or Set
' UINT  uiParam ,  // depends On action To be taken
' PVOID  pvParam , // depends On action To be taken
' UINT  fWinIni   // User Profile update flag
');
'
'See the help file for a full description of this function.  Note our simple use of it to get
'the desktop dimensions in order to fill the whole screen with the output screen.  This function
'should show you how easy it is to call Api functions.  They are not all as challenging as TextOut()
'or DrawText(). Note further that the third parameter is a pointer to a Rect TYPE/structure.  In C
'you would Declare a Rect similiar to below, but In the Function Call you would put &rc instead of
'rc as seen below.  This Is because In C the '&' is the address of operator which signifies to the
'compiler to push the address of rc on the stack, which resolves to an acceptable pointer arguement
'for the Function.  In Powerbasic all you have to do is use rc itself as a parameter as Powerbasic
'will push the address of rc on the stack due to its default Byref parameter passing convention.
'****************************************************************************************************
Sub cmdButton2_Click(wea As WndEventArgs)
 Local hOutputScreen As Dword
 Local rc As Rect
 
 Call SystemParametersInfo(%SPI_GETWORKAREA,0,rc,0)  'this Api Fn will return the size of the desktop in
 hOutputScreen= _                                    'a Rect structure/type.  Note info returned in param
 CreateWindowEx( _
 0, _                    'no extended window styles, just plain jane window
 "OutputScreen", _       'this is important!  It is the class from which the window will be instantiated
 "Output Screen", _      'caption of window
 %WS_OVERLAPPEDWINDOW, _ 'window style - see CreateWindow or CreateWindowEx in Api docs
 0, _                    'X coord
 0, _                    'Y coord
 rc.nRight, _            'this info returned in SystemParametersInfo()
 rc.nBottom, _           'ditto
 wea.hWnd, _             'all CreateWindow calls require handle of parent window
 0, _
 wea.hInst, _
 Byval 0)
 Call ShowWindow(wea.hWnd,%SW_HIDE)
 Call ShowWindow(hOutputScreen,%SW_SHOWNORMAL)
 Call UpdateWindow(hOutputScreen)
End Sub


Sub fnWndProc_OnCreate(wea As WndEventArgs)
 Local pCreateStruct As CREATESTRUCT Ptr
 Local strString() As String
 Local szCaption() As Asciiz * 10
 Local hButton() As Dword
 Register i As Long

 pCreateStruct=wea.lParam              'When Windows 'packages up' a WM_CREATE msg, it sets
 wea.hInst=@pCreateStruct.hInstance    'the lParam variable equal to the address of a
 Dim szCaption(1) As Asciiz*10         'CREATESTRUCT.  One can obtain the instance handle
 Dim hButton(1) As Dword               'in this way.  Yes, it is bizarre and complicated (and
 For i=0 To 1                          'not really necessary here).
   szCaption(i)="Button #"
   szCaption(i)=szCaption(i)+Right$(Str$(i+1),1)
   hButton(i)= _                    'control handles can be globally defined
   CreateWindowEx _                 'CreateWindow() Api function call
   ( _
    0, _                            'didn't use any extended styles
    "button", _                     'one of the more important predefined window classes
    szCaption(i), _                 'caption, in this case a button caption
    %WS_CHILD Or %WS_VISIBLE, _     'window style bit field
    93, _                           'X coordinate pixel position of top left corner of window
    30*i+50, _                      'Y coordinate pixel position of top of window (button)
    100, _                          'width of window (button)
    25, _                           'height of window (button)
    wea.hWnd, _                     'parent window handle
    i+1501, _                       'control id! very important!
    wea.hInst, _                    'instance handle of app
    ByVal 0 _                       'pointer to window creation parameters.  Not used here.
   )
 Next i
End Sub


Sub fnWndProc_OnCommand(wea As WndEventArgs)   'Our main window only has two controls on it
 Select Case LoWrd(wea.wParam)                'the wParam variable contains the control id of the
   Case %IDC_BUTTON1                          'window (button) sending the message.  We set the
     Call cmdButton1_Click()                  'ids ourselves in the CreateWindow() call.
   Case %IDC_BUTTON2
     Call cmdButton2_Click(wea)
 End Select
End Sub


Sub fnWndProc_OnClose(wea As WndEventArgs)
 Call PostQuitMessage(0)    'causes app to drop out of message loop and end
End Sub
'
Function fnWndProc(ByVal hWnd As Long,ByVal wMsg As Long,ByVal wParam As Long,ByVal lParam As Long) Export As Long
 Local wea As WndEventArgs

 Select Case wMsg
   Case %WM_CREATE
     wea.wParam=wParam: wea.lParam=lParam: wea.hWnd=hWnd
     Call fnWndProc_OnCreate(wea)
     fnWndProc=0
     Exit Function
   Case %WM_COMMAND
     wea.wParam=wParam: wea.lParam=lParam: wea.hWnd=hWnd
     Call fnWndProc_OnCommand(wea)
     fnWndProc=0
     Exit Function
   Case %WM_CLOSE
     wea.wParam=wParam: wea.lParam=lParam: wea.hWnd=hWnd
     Call fnWndProc_OnClose(wea)
     fnWndProc=0
     Exit Function
 End Select

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


Function blnAppInitialize(wcl As WndClassEx,hIns As Long,szCls1 As Asciiz,szCls2 As Asciiz) As Dword
 Local szOutputScreen As Asciiz*16
 Local szAppName As Asciiz*6

 'Fill out WndClassEx Type/structure for your main window
 wcl.cbSize=SizeOf(wcl)
 wcl.style=%CS_HREDRAW Or %CS_VREDRAW
 wcl.lpfnWndProc=CodePtr(fnWndProc)
 wcl.cbClsExtra=0
 wcl.cbWndExtra=0
 wcl.hInstance=hIns
 wcl.hIcon=LoadIcon(%NULL, ByVal %IDI_APPLICATION)
 wcl.hCursor=LoadCursor(%NULL, ByVal %IDC_ARROW)
 wcl.hbrBackground=%COLOR_BTNFACE+1
 wcl.lpszMenuName=%NULL
 wcl.lpszClassName=VarPtr(szCls1)
 wcl.hIconSm=LoadIcon(hIns,ByVal %IDI_APPLICATION)
 If IsFalse(RegisterClassEx(wcl)) Then                'Register main window class
    blnAppInitialize=%FALSE                           'Fail and exit proc & app on failure
    Exit Function
 End If

 'Now modify several fields of type with unique
 'characteristics for OutputScreen
 wcl.lpszClassName=VarPtr(szCls2)                     'Output Screen Class Name
 wcl.lpfnWndProc=CodePtr(fnOutputScreen)              'Output Screen Window Proceedure
 wcl.hbrBackground=GetStockObject(%WHITE_BRUSH)       'Output Screen Background
 If IsFalse(RegisterClassEx(wcl)) Then
    blnAppInitialize=%FALSE
    Exit Function
 End If

 Function=%TRUE
End Function


Function WinMain(ByVal hIns As Long, ByVal hPrevIns As Long,ByVal lpCmdLine As Asciiz Ptr, ByVal iShow As Long) As Long
 Local szAppName As Asciiz*8,szOutputScreen As Asciiz*16
 Local winclass As WndClassEx
 Local hMainWnd As Dword
 Local dwStyle As Dword
 Local Msg As tagMsg

 szAppName="Form3"                 'don't have to make these globals
 szOutputScreen="OutputScreen"     'once they are registered with windows the OS knows about them
 If blnAppInitialize(winclass,hIns,szAppName,szOutputScreen) Then
    dwStyle=%WS_OVERLAPPEDWINDOW  '1040384
    hMainWnd=CreateWindowEx(0,szAppName,"Form3",dwStyle,200,100,300,250,%HWND_DESKTOP,0,hIns,ByVal 0)
    ShowWindow hMainWnd,iShow
    UpdateWindow hMainWnd
    While GetMessage(Msg,%NULL,0,0)
      TranslateMessage Msg
      DispatchMessage Msg
    Wend
 Else
    MsgBox("Something Went South!")  'For Console Compiler Use Print Instead!
    'Print "Something Went South!"
 End If

 Function=msg.wParam
End Function