Author Topic: Fred's Tutorial #3: Windows API Tutorial: Child Controls/Text Output To Screen  (Read 5144 times)

0 Members and 1 Guest are viewing this topic.

Offline Frederick J. Harris

  • Hero Member
  • *****
  • Posts: 914
  • User-Rate: +16/-0
    • Frederick J. Harris
'Form3.bas Compilation Tested With PBWin 8.01, 7.04, 6.11
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

Code: [Select]
#Compile Exe
#Dim All
#Include ""

'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
'                     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 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:
' lpString AS ASCIIZ,     'what needs to be passed here is the address of the string
'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
'  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
      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)
      Exit Function
  End Select

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
'           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 #"
    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)
      Exit Function
    Case %WM_COMMAND
      wea.wParam=wParam: wea.lParam=lParam: wea.hWnd=hWnd
      Call fnWndProc_OnCommand(wea)
      Exit Function
    Case %WM_CLOSE
      wea.wParam=wParam: wea.lParam=lParam: wea.hWnd=hWnd
      Call fnWndProc_OnClose(wea)
      Exit Function
  End Select

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) Or %CS_VREDRAW
  wcl.hIcon=LoadIcon(%NULL, ByVal %IDI_APPLICATION)
  wcl.hCursor=LoadCursor(%NULL, ByVal %IDC_ARROW)
  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
     Exit Function
  End If

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
     MsgBox("Something Went South!")  'For Console Compiler Use Print Instead!
     'Print "Something Went South!"
  End If

End Function
« Last Edit: April 07, 2011, 09:49:30 PM by Frederick J. Harris »