Author Topic: Fred's Tutorial #6A: Pointers, Dynamic Memory Allocations, and Instance Data  (Read 8189 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
 
Dynamic Memory, Pointers, and Instance Data
                                                                           
By

Frederick J. Harris  fharris@evenlink.com

     This is a very important tutorial.  Once you fully understand the material in this tutorial, you won't be a beginner any longer.  Depending on your background, you may need to spend a lot of time with this material and accompanying programs.  Lets start with pointers.

     A pointer is a variable that holds the address of another variable.  Lets put off for a moment why this is important and just concentrate on how it works.  Very shortly you'll see why it is important.

     Lets start with this simple program below.  For the Console Compiler it will look like this (Windows version following)...

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

Function PBMain() As Long
  Local iNum1 As Long, iNum2 As Long
  Local ptrLong As Long Ptr

  iNum1=5                            'Assign 5 to iNum1
  Print "iNum1          =  "iNum1    'Print its value (5) through variable
  ptrLong=Varptr(iNum1)              'Assign address of var iNum to ptrLong
  Print "@ptrLong       =  "@ptrLong 'Print what's stored at ptrlong, i.e., @ptrLong
  Print "ptrLong        =  "ptrLong  'Print address held in ptrLong (Varptr(iNum1))
  Print "iNum2          =  "iNum2    'iNum2 should be 0 because we didn't
  ptrLong=Varptr(iNum2)              'initialize it.  Now set ptrLong to address
  @ptrLong=iNum1+1                   'of iNum2 and increment it through pointer.
  Print "iNum2          =  "iNum2    'Now output the value of iNum2 first
  Print "@ptrLong       =  "@ptrLong 'through variable then with pointer
  Waitkey$

  PBMain=0
End Function     

     Here is what the output looks like...

Code: [Select]
iNum1          =   5
@ptrLong       =   5
ptrLong        =   1244808
iNum2          =   0
iNum2          =   6
@ptrLong       =   6

     If you don't have the console compiler, try doing this little trick with the PBWin compiler for quick and dirty output...

Code: [Select]
#Compile Exe  “WinPtr1”  ‘PB Win Version
#Dim All

Function PBMain() As Long
  Local iNum1 As Long, iNum2 As Long, fp As Long
  Local ptrLong As Long Ptr

  fp=Freefile
  Open "Output.txt" For Output As #fp
  iNum1=5
  Print #fp, "iNum          =  "iNum1
  ptrLong=Varptr(iNum1)
  Print #fp, "@ptrLong      =  "@ptrLong
  Print #fp, "ptrLong       =  "ptrLong
  Print #fp, "iNum2         =  "iNum2
  ptrLong=Varptr(iNum2)
  @ptrLong=iNum1+1
  Print #fp, "iNum2         =  "iNum2
  Print #fp, "@ptrLong      =  "@ptrLong
  Close #fp
  Shell("Notepad.exe Output.txt", 3)

  PBMain=0
End Function
                 

     The above Windows version just opens an output log file into which output is printed.  Then the program closes the file and attempts to open it with Notepad.  If it doesn't open for you in Notepad find a path to Notepad on your computer and fill in the full path such as...

     Shell("C:\Winnt\System32\Notepad.exe Output.txt", 3)

     Or put a copy of Notepad in the directory with the program.  The point is to open the file with Notepad so you can see the results and follow along.  If you are new to the Shell command you can search for it in PowerBASIC help.

     Getting back to the program, it declares three variables, two longs, i.e., iNum1 and iNum2, and a pointer to a long, i.e., ptrLong.  iNum1 is initialized with the number five, and ptrLong is initialized with the address of iNum1.  Then the value of iNum1 is output first using the variable iNum1 itself, then it is accessed through the pointer variable which is actually holding the address of iNum1, not the '5' that iNum1 is equal to.  This is known as using indirection to access the contents of a variable.  Note that what is stored at the address held in ptrLong is different from the address itself.  The variable iNum1 in the above example has memory space allotted to it at 1244808.  This is the number ptrLong is holding.  To access what’s stored there using the pointer you need to put the ‘@’ symbol in front of it, that is @ptrLong.

     Next the value of iNum2 is output, and that value is zero because PowerBASIC initialized this local variable to zero when the variable was declared, and we never reinitialized it to anything else.  Then the pointer variable is reset to the address of iNum2 with the statement...

     ptrLong=Varptr(iNum2)

     The next statement stores at iNum2 the value of iNum1 + 1 which should be six.  It uses the pointer to do this instead of the variable iNum2.  When you put that '@' in front of a pointer variable it means you are referring to the memory space represented by the address held in the pointer.  For example, if the pointer variable named ptrLong is holding the number 1056254, then the expression...

     @ptrLong = 6

...means 'store the number 6 at the address 1056254.  It oftentimes helps me when I'm working with complicated pointer expressions to print out addresses with the programs to help me see what is going on.       As you can surely see the address of a variable and what's stored there at that address are two completely different things!

     Having shown you this you might be thinking, 'OK, so what's it good for?'  Well, there's more to come.  We're still in the building phase.  There certainly doesn't appear to be any great advantage to referring to a number through a pointer when you already have a perfectly good variable already declared to hold that number.  Where pointers become important is in situations where you must store data which for whatever reason you don't have or can't have named variables to hold the data.  In these situations you usually must call system memory allocation functions to obtain memory to store things.  Again, we will very shortly get to the 'why' and at that point the overwhelming significance of all this will hit you like a ton of bricks, but for now just concentrate on the 'how'.  Lets try another little example.  Type in the following Console Compiler program, and if you only have  PBWin, make the modifications as shown above to obtain text file output...

Code: [Select]
#Compile Exe    "Ptr2"
#Dim All
#Include "Win32Api.inc"

Function PBMain() As Long
  Local pszStr As Asciiz Ptr   'If you will need to 'Print' a string in Basic
  Local ptrByte As Byte Ptr    'through a pointer, it needs to be declared as
  Local szName As Asciiz*32    'an Asciiz Ptr.  A pointer to a byte can access
  Local hHeap As Dword         'any byte position.  Strings declared as
  Register i As Long           'Asciiz*N allocate memory in the stack frame.

  szName="PowerBASIC!"
  Print "szName                                 = "szName
  Print "Varptr(szName)                         = "Varptr(szName)
  hHeap=GetProcessHeap()
  pszStr=HeapAlloc(hHeap,%HEAP_ZERO_MEMORY,Len(szName)+1)
  Print "pszStr                                 = "pszStr
  If pszStr Then
     Poke$ Asciiz, pszStr, szName ‘This statement copies string bytes to another location
     Print "@pszStr                                = "@pszStr
     Print
     ptrByte=pszStr  ‘Sets a pointer to a byte = to address of 1st byte of string
     Print " i        Varptr(@ptrByte[i]           @ptrByte[i]            Chr$(@ptrByte[i])"
     Print "==============================================================================="
     For i=0 To Len(@pszStr)
       Print i, Varptr(@ptrByte[i]),,@ptrByte[i],,Chr$(@ptrByte[i])
     Next i
     Print
     Print "Abs(IsTrue(HeapFree(hHeap,0,pszStr)))  = "Abs(IsTrue(HeapFree(hHeap,0,pszStr)))
  End If
  Waitkey$

  PBMain=0
End Function   

     Below is the output from this program on my computer...

Code: [Select]
szName                                 = PowerBASIC!
Varptr(szName)                         =  1244772
pszStr                                 =  1282264
@pszStr                                = PowerBASIC!

 i        Varptr(@ptrByte[i]           @ptrByte[i]            Chr$(@ptrByte[i])
===============================================================================
 0             1282264                     80                         P
 1             1282265                     111                        o
 2             1282266                     119                        w
 3             1282267                     101                        e
 4             1282268                     114                        r
 5             1282269                     66                         B
 6             1282270                     65                         A
 7             1282271                     83                         S
 8             1282272                     73                         I
 9             1282273                     67                         C
 10            1282274                     33                         !
 11            1282275                     0

Abs(IsTrue(HeapFree(hHeap,0,pszStr)))  =  1

     This example is all about the simple string "PowerBASIC!".  You can see above in the first line of the program after the variable declarations that this string is assigned to an Asciiz*16 string, then the string itself is output and also the address of the string with the Varptr() function.  Right after that is where the serious work of this program begins, and you should see two Windows Api functions with which you are probably not familiar, and that would be GetProcessHeap() and HeapAlloc().  These are really important functions and I'd recommend you look these functions up in your Windows Api help file before we proceed.  What these functions are all about is something called 'memory allocation'.  In this example we're essentially doing something nonsensical, i.e., making room for a string which we already have room for, but in later examples in this tutorial it will be a real need, and this example will help you understand how it works.  So read on.

     Note that there are three parameters to the HeapAlloc() function, and there is also a return value assigned to the pszStr Asciiz Pointer variable.  Lets start with the latter.  What memory allocation functions return is an address assigned by the operating system where one can start storing whatever it is that one wants to store there.  In this case we want to copy the string 'PowerBASIC!' to this new storage location which we just acquired.  You may note that immediately after the HeapAlloc() call there is an If statement that checks whether the value returned from the function is non-zero.  If memory allocation functions fail in allocating memory they universally return 0 to the calling app.  It is important to check for this value, because if you attempt to write memory you don't own, you'll likely end up with a GPF.

     So what you have then is a starting address where you may start putting things.  But there is a definite limit to how far you can go.  The last parameter (the third) to the HeapAlloc() function is the number of bytes to allocate. In the call above it was set to...

     Len(szName)+1

     Since the string "PowerBASIC!" is 11 characters, the above expression will resolve to the number 12.  The +1 term is to make room for a terminating NULL byte.  All Win32 Api character strings must be null terminated.  After all, that's why we preface them with 'sz' remember - string terminated by zero?  Before moving on to how we are going to get the bytes to this new storage location, lets just mention that the first parameter to the HeapAlloc() function is a handle to the heap or 'free store', and the second parameter is just an equate to tell the Api to zero the memory out.

     If this were a C program we would use strcpy() to get the string moved to the new storage just obtained, but since it isn't we'll use BASIC's time tested Poke statement.  Recently PowerBASIC upgraded this statement, and it can be used in the Poke$ syntax to move the whole string in one fell swoop to the new memory.  The statement in the program that does that is...

     Poke$ Asciiz, pszStr, szName

     This causes the string "PowerBASIC!", which is stored in szName, to be copied to the address stored in pszStr.  The Asciiz term tells PowerBASIC that it is an Asciiz null terminated string, and so PowerBASIC knows to append a null byte after the last character.  So what we have done here is simply moved some bytes that originally only existed in a named variable named szName, and moved them (copied, actually) to a new storage location without a name that can only be accessed through a pointer. 

     Just to flex our muscles a bit the remainder of the program shows some more pointer magic that can be done.  We declared another pointer variable in the program named ptrByte, and we assigned it an address to point to as follows...

     ptrByte=pszStr

     So in other words it is initially set to the address of the first byte of the string "PowerBASIC!".  We then run through a For loop from the first byte of the string to the last terminating null byte.  Note how the various bytes were accessed.  @ptrByte[0] would be 'P'. 

     However, Varptr(@ptrByte[0]) is 1282264 or pszStr.  That would be  the original starting address returned from HeapAlloc().  Finally, the program releases the memory back to the operating system through a call to HeapFree()...

     Abs(IsTrue(HeapFree(hHeap,0,pszStr)))

     I enclosed it in Abs(IsTrue... because I like to see a '1' returned if memory frees satisfactorily and a '0' if it doesn't.  If your program fails to return allocated memory that is bad and is termed the rather interesting term 'memory leak'.  That's a funny but apt term because we all know what happens when we have something that leaks.  When we need whatever it is that has leaked away we find we don't have any.  All programs need memory to run before anything else, and if there is none  that is a terminal condition.  I personally find it very important to check return values on memory deallocations because pointers are tricky to use at times, and sometimes you can make mistakes that don't crash the program or computer, but rather corrupt memory somewhere of which you are unaware, but these circumstances frequently cause memory deallocation failures, and thereby give you a hint that something needs to be checked closer, so as to find out what's wrong.

     OK, we've covered enough about pointers to try to see how they operate in a real GUI Windows program.  We'll start by looking at something I've presented before in this tutorial without explaining.  Recall that in all the Windows programs we've done so far we had a WinMain() function where we initialized a WNDCLASSEX type, then made a CreateWindowEx() call to create a window based on the Registered Class?  Obviously, Windows must keep track internally of quite a bit of data pertaining to every window it creates.  It actually fills out a structure/type known as a CREATESTRUCT that contains all the window initialization data passed to it in the CreateWindowEx() call.  Here is what the type looks like from the C documentation...

Code: [Select]
typedef struct tagCREATESTRUCT
{   // cs
    LPVOID    lpCreateParams;
    HINSTANCE hInstance;
    HMENU     hMenu;
    HWND      hwndParent;
    int       cy;
    int       cx;
    int       y;
    int       x;
    LONG      style;
    LPCTSTR   lpszName;
    LPCTSTR   lpszClass;
    DWORD     dwExStyle;
} CREATESTRUCT;

     You'll recognize the members of this type as the actual parameters to the CreateWindowEx() call.  When a program's execution hits a CreateWindowEx() call, a pointer to this structure will be passed in the lParam parameter during a WM_CREATE message sent to the window's window procedure.  If you recall the WM_CREATE message is special in that it is only sent one time.  And it is important to realize that synchronous execution is occurring here and program statements will be executing in the WM_CREATE message handler before the CreateWindowEx() function returns in WinMain().  The very first instant that a program can get access to a window's handle is not through the return value from a CreateWindowEx() call but rather through the hWnd parameter of the WM_CREATE message sent to the Window's window procedure.  And all of the Creation Parameters from the CreateWindowEx() call that created the window such as the initial x,y position, height, width, caption, etc., are available in the WM_CREATE message through this pointer to a CREATESTRUCT sent by Windows.  Since WM_CREATE processing is occurring before the function returns, it is even possible to send data back to WinMain() through this pointer.

     Also, please note the very first member of the CREATESTRUCT type/struct above.  That would be lpCreateParams.  We have not used that parameter yet in any of the previous programs, but we'll illustrate it in the next.  This would be that very last parameter in the CreateWindowEx() call where we've been putting a Byval 0 term.  What this parameter is used for is passing additional window creation data to the window procedure to be used in the construction of the object.  Let me give an example.  Say for instance you are building a grid custom control.  You want the grid custom control to work just like all the other Windows controls such as text boxes, list boxes, etc., that you use.  In other words, you want your control to be created by a CreateWindowEx() function call.  If you are building a grid you need some way of letting the grid's window procedure know how many rows and how many columns the grid is to have.  You can pass this data to the window procedure through this lpCreateParams parameter.  Here is how you would do it.  First, create a type that contains all the fields you need to describe the object.  For example, if its a grid you could do something like this...

     Type tagGridData
        NumRows As Long
        NumCols As Long
     End Type

     Then, in WinMain(), declare a variable of that type such as...

     Local GridData As tagGridData

     Then assign values for how many rows and columns you want your grid to have...

     GridData.NumRows = 20
     GridData.NumCols   = 12

     Then, in the CreateWindowEx() call you would pass the address of that GridData variable to the window procedure in that last lpCreateParams parameter.  Assuming a grid custom control exists with the szClassName of "grid", the call would look something like this...

Code: [Select]
hGrid=CreateWindowEx _
( _
  %WS_EX_CLIENTEDGE, _              'Extended Window Style
  "grid", _                         'Class of Custom Control To Create
  "My Grid Custom Control", _       'Caption Of Grid
  "%WS_CHILD Or %WS_VISIBLE, _      'Ordinary Window Styles
  10,20,400,500, _                  'Locations, Width, Height
  hMainWindow, _                    'Parent of Grid Control
  %IDC_CRID_CONTROL, _              'Control ID of Grid Control
  hInstance, _                      'Instance Handle
  VarPtr(GridData) _                'Address of Window Creation Data
)
     

     Please note the last parameter in the above hypothetical CreateWindowEx() call, i.e., VarPtr(GridData).  This is how this window creation data will be passed to the window procedure.  When this window is being created and the window procedure is processing the WM_CREATE message, this window creation data, as well as all the other data comprising the CreateWindowEx() call will be made available through a pointer to a CREATESTRUCT type.  Lets take a look at how this works.  It isn't that hard!
« Last Edit: September 08, 2007, 06:08:58 PM by José Roca »

Offline Frederick J. Harris

  • Hero Member
  • *****
  • Posts: 914
  • User-Rate: +16/-0
    • Frederick J. Harris
 
     Below is a simple program that creates a window that only has a bunch of labels and text boxes on it to display some of the various window creation parameters passed to the window procedure from the CreateWindowEx() call.  Compile and run the following program, CreateStruct.bas.  You'll find adequate comments within the program's code describing how it works.  We'll continue with the discussion afterwards...

Code: [Select]
#Compile Exe  "CreateStruct"      'CreateStruct.bas
#Include      "Win32api.inc"
'Labels
%IDC_LABEL1   = 1500        'These are equates and serve as
%IDC_LABEL2   = 1505        'control identifiers for the
%IDC_LABEL3   = 1510        'various controls used in a
%IDC_LABEL4   = 1515        'program.  This particular program
%IDC_LABEL5   = 1520        'only has one main window that
%IDC_LABEL6   = 1525        'contains nine labels and nine
%IDC_LABEL7   = 1530        'text boxes (edit controls).  You
%IDC_LABEL8   = 1535        'don't need to make window handles
%IDC_LABEL9   = 1540        'global in a program because if
                            'you know a control's parent and
'Edit Controls              'its control id you can get its
%IDC_EDIT1    = 1550        'window handle through a call tp
%IDC_EDIT2    = 1555        'GetDlgItem(hParent,%CONTROL_ID)
%IDC_EDIT3    = 1560
%IDC_EDIT4    = 1565
%IDC_EDIT5    = 1570
%IDC_EDIT6    = 1575
%IDC_EDIT7    = 1580
%IDC_EDIT8    = 1585
%IDC_EDIT9    = 1590

Type WndEventArgs               'Type for passing window procedure parameters.
  wParam           As Long      'Allows me to shorten parameter list.  .NET does
  lParam           As Long      'this all the time.  See, for example pea for
  hWnd             As Dword     'PaintEventArgs in the OnPaint()Message Handler.
End Type


Type MyUserData                 'Here is a type I created just to show you how to pass
  iAnyOldInt       As Long      'control/program specific user data from a point in a
  szAnyOldString   As Asciiz*32 'program where a CreateWindowEx() call is being made to
End Type                        'the location in a program where the WndProc() resides


Function fnWndProc_OnCreate(Wea As WndEventArgs) As Long
  Local lpCreateStruct As CREATESTRUCT Ptr
  Local ptrMud As MyUserData Ptr
  Local hCtrl As Dword                        'Right here we are in a message
  Local hInst As Dword                        'handler for the %WM_CREATE
                                              'message, and the %WM_CREATE
  lpCreateStruct=Wea.lParam                   'message will only be received
  hInst=@lpCreateStruct.hInstance             'once for each window that is
  ptrMud=@lpCreateStruct.lpCreateParams       'being created.  In other words,
                                              'there is a one to one relationship
  hCtrl= _       'First Label                 'between a CreateWindow() Call and
  CreateWindowEx _                            'a %WM_CREATE message.  The last
  ( _                                         'parameter in the Window Procedure
    0, _                                      'is lParam.  It was transported
    "static", _                               'here as part of the WndEventArgs
    "@lpCreateStruct.@lpszName", _            'type - Wea.lParam.  This
    %WS_CHILD Or %WS_VISIBLE, _               'parameter's meaning during a
    10,10,200,25, _                           'WM_CREATE message is a pointer to
    Wea.hWnd, _                               'a CREATESTRUCT type that Windows
    %IDC_LABEL1, _                            'itself filled out at the time of
    hInst, _                                  'the CreateWindowEx() call down in
    Byval 0 _                                 'WinMain.  The way we 'get at' that
  )                                           'data here is to declare a pointer
                                              'to a CREATESTRUCT type.  You will
  hCtrl= _       'First edit                  'be able to find this type in
  CreateWindowEx _                            'Win32Api.inc. Note the very first
  ( _                                         'local variable in this procedure
    %WS_EX_CLIENTEDGE, _                      'Local lpCreateStruct As CREATESTRUCT ptr.
    "Edit", _                                 'You'll note the very first line
    @lpCreateStruct.@lpszName, _              'in this procedure sets this
    %WS_CHILD Or %WS_VISIBLE, _               'variable to the address contained
    220,10,200,25, _                          'in Wea.lParam.  At this point then
    Wea.hWnd, _                               'we are able to access every member
    %IDC_EDIT1, _                             'of this type here because
    hInst, _                                  'PowerBASIC knows how a CREATESTRUCT
    Byval 0 _                                 'is laid out, and so can access
  )                                           'its member variables through an
                                              'ordinary variable of this type,
  hCtrl= _       'Second Label                'or through a pointer to this type
  CreateWindowEx _                            'as is the case here.  Further,
  ( _                                         'the first field or member
    0, _                                      'variable of the CREATESTRUCT
    "static", _                               'type is itself a pointer
    "@lpCreateStruct.@lpszClass", _           'lpCreateParams'.  Down in
    %WS_CHILD Or %WS_VISIBLE, _               'WinMain() a variable of type
    10,45,200,25, _                           'MyUserData was declared and
    Wea.hWnd, _                               'member iAnyOldInt was set to
    %IDC_LABEL2, _                            '12345 and member szAnyOldString
    hInst, _                                  'was set equal to "Any Old String".
    Byval 0 _                                 'The variable itself was named
  )                                           'mud which is short for MyUserData.
  'The term Varptr(mud) was placed in the last parameter of the CreateWindowEx()
  'call, and this of course would be the address of that variable - which is a
  'pointer.
  hCtrl= _       'Second edit                 'So, that address will show up
  CreateWindowEx _                            'here in fnWndProc_OnCreate() as
  ( _                                         'the first member variable of the
    %WS_EX_CLIENTEDGE, _                      'CREATESTRUCT type pointed to by
    "Edit", _                                 'the Wea.lParam variable. That
    @lpCreateStruct.@lpszClass, _             'does sound rather complicated but
    %WS_CHILD Or %WS_VISIBLE, _               'it really doesn't have to be
    220,45,200,25, _                          'unless you make it so!  You will
    Wea.hWnd, _                               'note though that we did have to
    %IDC_EDIT2, _                             'create a pointer to a MyUserData
    hInst, _                                  'type to get at this information
    Byval 0 _                                 'just as we had to create a pointer
  )                                           'to a CREATESTRUCT to get at the
                                              'overall containing type. Although
                                              'this program looks long it is very
  hCtrl= _       'Third Label                 'repetitious in that most of it is
  CreateWindowEx _                            'just code for creating nine
  ( _                                         'labels and nine textboxes. That's
    0, _                                      'what all this code over at the
    "static", _                               'left is about.  It shouldn't be
    "@lpCreateStruct.hInstance", _            'anything you don't understand at
    %WS_CHILD Or %WS_VISIBLE, _               'this point if you have followed
    10,80,200,25, _                           'my tutorials up to this point.
    Wea.hWnd, _                               'One thing I did here that I'd
    %IDC_LABEL3, _                            'like to finally point out to you
    hInst, _                                  'because I feel it may contain
    Byval 0 _                                 'possibilities for confusion
  )                                           'concerns the difference between
                                              'accessing a character string
  hCtrl= _       'Third edit                  'contained within a user defined
  CreateWindowEx _                            'type and accessing a character
  ( _                                         'string through a pointer member
    %WS_EX_CLIENTEDGE, _                      'of a user defined type.  Note that
    "Edit", _                                 'the very first bit of info that
    Str$(@lpCreateStruct.hInstance), _        'displays on the program's Form
    %WS_CHILD Or %WS_VISIBLE, _               'is the name of the app which is
    220,80,200,25, _                          'defined down in WinMain as
    Wea.hWnd, _                               '"CreateStruct".  It is accessed
    %IDC_EDIT3, _                             'here through the term -                                              '
    hInst, _                                  '@lpCreateStruct.@lpszName.  The
    Byval 0 _                                 'reason for the first '@' symbol
  )                                           'is because lpCreateStruct is a
  'pointer and we want what is stored at the address held in the pointer. The
  'reason for the 2nd '@' symbol is exactly the same.  The lpszName member of
  'the type doesn't hold a string
  hCtrl= _                                    'but rather a pointer to a string.
  CreateWindowEx _                            'Contrast this with the MyUserData
  ( _                                         'type I created for this program.
    0, _                                      'If you look at the 2nd member of
    "static", _                               'this type you'll see the variable
    "@lpCreateStruct.cx", _                   'szAnyOldString As Asciiz*32.  In
    %WS_CHILD Or %WS_VISIBLE, _               'this case the string contains 32
    10,115,200,25, _                          'bytes of space where a string of
    Wea.hWnd, _                               'up to 31 characters plus a null
    %IDC_LABEL4, _                            'byte may be stored.  This is
    hInst, _                                  'entirely different from the
    Byval 0 _                                 'lpszName and lpszClass member
  )                                           'variables of the CREATESTRUCT type
                                              'where each of these two variables
  hCtrl= _       'Fourth edit                 'only occupies 4 bytes - just
  CreateWindowEx _                            'enough for a 32 bit pointer on 32
  ( _                                         'bit systems.  So you see a type
    %WS_EX_CLIENTEDGE, _                      'can actually hold memory to
    "Edit", _                                 'contain an Asciiz string, or it
    Str$(@lpCreateStruct.cx), _               'can contain just a pointer member
    %WS_CHILD Or %WS_VISIBLE, _               'that contains just the integer
    220,115,200,25, _                         'address of a string stored
    Wea.hWnd, _                               'somewhere else.  If a user defined
    %IDC_EDIT4, _                             'type actually stores the whole
    hInst, _                                  'string itself it is accessed like
    Byval 0 _                                 'so - @ptrMud.szAnyOldString. No
  )                                           '2nd '@' symbol is needed.  If,
  'however, the type only contains a pointer to a string stored somewhere else
  'then the 2nd '@' symbol is needed - @lpCreateStruct.@lpszName.

  hCtrl= _
  CreateWindowEx _
  ( _
    0, _
    "static", _
    "@lpCreateStruct.cy", _
    %WS_CHILD Or %WS_VISIBLE, _
    10,150,200,25, _
    Wea.hWnd, _
    %IDC_LABEL5, _
    hInst, _
    Byval 0 _
  )

  hCtrl= _
  CreateWindowEx _
  ( _
    %WS_EX_CLIENTEDGE, _
    "Edit", _
    Str$(@lpCreateStruct.cy), _
    %WS_CHILD Or %WS_VISIBLE, _
    220,150,200,25, _
    Wea.hWnd, _
    %IDC_EDIT5, _
    hInst, _
    Byval 0 _
  )

  hCtrl= _
  CreateWindowEx _
  ( _
    0, _
    "static", _
    "@lpCreateStruct.x", _
    %WS_CHILD Or %WS_VISIBLE, _
    10,185,200,25, _
    Wea.hWnd, _
    %IDC_LABEL6, _
    hInst, _
    Byval 0 _
  )

  hCtrl= _
  CreateWindowEx _
  ( _
    %WS_EX_CLIENTEDGE, _
    "Edit", _
    Str$(@lpCreateStruct.x), _
    %WS_CHILD Or %WS_VISIBLE, _
    220,185,200,25, _
    Wea.hWnd, _
    %IDC_EDIT6, _
    hInst, _
    Byval 0 _
  )

  hCtrl= _
  CreateWindowEx _
  ( _
    0, _
    "static", _
    "@lpCreateStruct.y", _
    %WS_CHILD Or %WS_VISIBLE, _
    10,220,200,25, _
    Wea.hWnd, _
    %IDC_LABEL7, _
    hInst, _
    Byval 0 _
  )


  hCtrl= _
  CreateWindowEx _
  ( _
    %WS_EX_CLIENTEDGE, _
    "Edit", _
    Str$(@lpCreateStruct.y), _
    %WS_CHILD Or %WS_VISIBLE, _
    220,220,200,25, _
    Wea.hWnd, _
    %IDC_EDIT7, _
    hInst, _
    Byval 0 _
  )

  hCtrl= _
  CreateWindowEx _
  ( _
    0, _
    "static", _
    "@ptrMud.iAnyOldInt", _
    %WS_CHILD Or %WS_VISIBLE, _
    10,255,200,25, _
    Wea.hWnd, _
    %IDC_LABEL8, _
    hInst, _
    Byval 0 _
  )

  hCtrl= _
  CreateWindowEx _
  ( _
    %WS_EX_CLIENTEDGE, _
    "Edit", _
    Str$(@ptrMud.iAnyOldInt), _
    %WS_CHILD Or %WS_VISIBLE, _
    220,255,200,25, _
    Wea.hWnd, _
    %IDC_EDIT8, _
    hInst, _
    Byval 0 _
  )

  hCtrl= _
  CreateWindowEx _
  ( _
    0, _
    "static", _
    "@ptrMud.szAnyOldString", _
    %WS_CHILD Or %WS_VISIBLE, _
    10,290,200,25, _
    Wea.hWnd, _
    %IDC_LABEL9, _
    hInst, _
    Byval 0 _
  )

  hCtrl= _
  CreateWindowEx _
  ( _
    %WS_EX_CLIENTEDGE, _
    "Edit", _
    @ptrMud.szAnyOldString, _
    %WS_CHILD Or %WS_VISIBLE, _
    220,290,200,25, _
    Wea.hWnd, _
    %IDC_EDIT9, _
    hInst, _
    Byval 0 _
  )

  fnWndProc_OnCreate=0
End Function

Function fnWndProc_OnDestroy(Wea As WndEventArgs) As Long
  Call PostQuitMessage(0)
  fnWndProc_OnDestroy=0
End Function

Function fnWndProc(ByVal hWnd As Long,ByVal wMsg As Long,ByVal wParam As Long,ByVal lParam As Long) As Long
  Local Wea As WndEventArgs

  Select Case As Long wMsg
    Case %WM_CREATE
      Wea.hWnd=hWnd : Wea.wParam=wParam : Wea.lParam=lParam
      fnWndProc=fnWndProc_OnCreate(Wea)
      Exit Function
    Case %WM_DESTROY
      Call PostQuitMessage(0)
      fnWndProc=fnWndProc_OnDestroy(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 iShow As Long) As Long
  Local winclass As WndClassEx
  Local szAppName As Asciiz*16
  Local mud As MyUserData
  Local Msg As tagMsg
  Local hWnd As Dword

  mud.iAnyOldInt=12345                       'We've declared a type variable of MyUserData just above
  mud.szAnyOldString="Any Old String"        'named 'mud', which is short for MyUserData.  It contains
  szAppName="Pointers"                       'an arbitrary integer and an arbitrary string so as to
  winclass.cbSize=SizeOf(winclass)           'show how to pass window creation data to a window
  winclass.style=%CS_HREDRAW Or %CS_VREDRAW  'procedure through a CreateWindowEx() call.  Up in
  winclass.lpfnWndProc=CodePtr(fnWndProc)    'fnWndProc_OnCreate() I give a detailed explanation of
  winclass.cbClsExtra=0                      'how this information is extracted there.  Note the last
  winclass.cbWndExtra=0                      'parameter of the CreateWindowEx() call contains the term
  winclass.hInstance=hIns                    'VarPtr(mud).  This is how the address of this data is being
  winclass.hIcon=LoadIcon(%NULL, ByVal %IDI_APPLICATION)    'passed to the Window Procedure
  winclass.hCursor=LoadCursor(%NULL, ByVal %IDC_ARROW)
  winclass.hbrBackground=%COLOR_BTNFACE+1
  winclass.lpszMenuName=%NULL
  winclass.lpszClassName=VarPtr(szAppName)
  winclass.hIconSm=LoadIcon(hIns, ByVal %IDI_APPLICATION)
  RegisterClassEx winclass
  hWnd=CreateWindowEx(0,szAppName,"CreateStruct",%WS_OVERLAPPEDWINDOW,200,100,445,360,0,0,hIns,Byval Varptr(mud))
  Call ShowWindow(hWnd,iShow)
  While GetMessage(Msg,%NULL,0,0)
    TranslateMessage Msg
    DispatchMessage Msg
  Wend

  Function=msg.wParam
End Function

     This program - CreateStruct.bas, certainly shows you how to put label and text box child window controls on a Form/Dialog, but I hope you get more from it than that.  On a certain basic level it is about pointers and the CREATESTRUCT type Windows uses to maintain window data.  On a deeper level though I would like you to start thinking about our use of the CreateWindowEx() call in WinMain() which creates our main program window, and the CreateWindowEx() calls in the Window Procedure that creates all the child windows such as the labels and text boxes.  You should have noted that for our main program window we had to fill out the fields of a WNDCLASSEX type, RegisterClassEx() that type variable, and finally make a call to CreateWindowEx() with the third parameter the registered class name, i.e., "CreateStruct" in the above example.  When we did that we also had to create a window procedure to process windows messages for that window.

     When you look at similar CreateWindowEx() calls in the Window Procedure that create child windows you see that we were able to skip all these steps and simply pass the name of the class we wanted to instantiate to the CreateWindowEx() function in the third parameter to the call, and we got our label, text box, or whatever with no further work required on our part.  We didn't have to register any classes, create any window procedures, or anything like that to get these windows.  Obviously, these controls are built into Windows so we can just use them with little work on our part, other than learning about their different functionalities made accessible to us through their various window style parameters.

     This is all well and good but I don't want things to be too easy for you!  This particular tutorial is about pointers, dynamic memory allocation and 'instance data', and at least in terms of the last, I'm willing to bet you may not even know what 'instance data' is!  So here is my plan.  Let's try to actually construct one of these child window controls we just used in CreateStruct.bas.  'Which one, you ask'?  Well, we used labels and edit controls, so those are the choices.  The edit controls would be too hard, so lets look at labels, as uninteresting as labels are.

     So start CreateStruct up and take a close look at the labels.  What do you see?  Actually, you don't even see separate windows for each label, all you really see is a text string.  Well, that's about all there really is to a label, just a text string drawn in a child window atop the parent located at the position specified by the x,y coordinate pair in the CreateWindowEx() call that creates the label.  In terms of drawing a text string on a window, if you recall back in both Form2.bas and again in Form3.bas we covered drawing text on a window in considerable detail.  If your memory is growing dim revisit that material briefly, particularly the TextOut() Api function that actually makes it happen.  

     So what must be done if we want to make our own label instead of using the "static" window class that Windows has provided for us?  Well, first thing is we'll need to register a window class just like we've done for all the main program windows we've created so far in this tutorial.  And registering a window class necessitates the provision of a Window Procedure to handle messages for our 'custom' window class.  I'm excited about this project. Lets see how far we can get.  Lets first create a main window, kind of just a 'Hello, World!' program.  Here would be that, at least...

Code: [Select]
#Compile Exe  "Label1.exe"
#Include "Win32api.inc"

Function fnWndProc(ByVal hWnd As Long,ByVal wMsg As Long,ByVal wParam As Long,ByVal lParam As Long) As Long
  Select Case As Long wMsg
    Case %WM_DESTROY
      PostQuitMessage 0
      fnWndProc=0
      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 iShow As Long) As Long
  Local winclass As WndClassEx
  Local szAppName As Asciiz*16
  Local Msg As tagMsg
  Local hWnd As Dword

  szAppName="Label1"
  winclass.cbSize=SizeOf(winclass)
  winclass.style=%CS_HREDRAW Or %CS_VREDRAW
  winclass.lpfnWndProc=CodePtr(fnWndProc)
  winclass.cbClsExtra=0
  winclass.cbWndExtra=0
  winclass.hInstance=hIns
  winclass.hIcon=LoadIcon(%NULL, ByVal %IDI_APPLICATION)
  winclass.hCursor=LoadCursor(%NULL, ByVal %IDC_ARROW)
  winclass.hbrBackground=%COLOR_BTNFACE+1
  winclass.lpszMenuName=%NULL
  winclass.lpszClassName=VarPtr(szAppName)
  Call RegisterClassEx(winclass)
  hWnd=CreateWindowEx(0,szAppName,"Label1",%WS_OVERLAPPEDWINDOW,200,100,325,300,0,0,hIns,ByVal 0)
  Call ShowWindow(hWnd,iShow)
  While GetMessage(Msg,%NULL,0,0)
    TranslateMessage Msg
    DispatchMessage Msg
  Wend

  Function=msg.wParam
End Function

     Compile and run that and you should get a pretty boring basic blank window that however, will serve as the parent for our label which we now try to create.  As I mentioned above our first step is to attempt to register a window class for our label.  Lets assign a class name of "MyLabel".  Also, we'll need to provide a window procedure for the class, and lets name that fnLabelWndProc.  Finally, lets create and register the class during processing of the WM_CREATE message for the main window.  Below is the program I just described.  Please compile and run Label2.bas...

Code: [Select]
#Compile Exe  "Label2.exe"   'Label2.bas  Just registers a "MyLabel" window
#Include "Win32api.inc"      'class and tests to be sure it registers OK.  

Function fnLabelWndProc(ByVal hWnd As Long,ByVal wMsg As Long,ByVal wParam As Long,ByVal lParam As Long) As Long
  fnLabelWndProc=DefWindowProc(hWnd,wMsg,wParam,lParam)
End Function

Function fnWndProc(ByVal hWnd As Long,ByVal wMsg As Long,ByVal wParam As Long,ByVal lParam As Long) As Long
  Select Case As Long wMsg
    Case %WM_CREATE
      Local hInst As Dword
      Local lpCreateStruct As CREATESTRUCT Ptr
      Local szClassName As Asciiz*16
      Local wc As WndClassEx

      lpCreateStruct=lParam
      hInst=@lpCreateStruct.hInstance
      szClassName="MyLabel"   :  wc.lpfnWndProc=CodePtr(fnLabelWndProc)
      wc.lpszClassName=VarPtr(szClassName)
      wc.style=%CS_HREDRAW Or %CS_VREDRAW
      wc.cbClsExtra=0  : wc.cbWndExtra=0
      wc.hInstance=hInst
      wc.hIcon=LoadIcon(%NULL, ByVal %IDI_APPLICATION)
      wc.hCursor=LoadCursor(%NULL, ByVal %IDC_ARROW)
      wc.hbrBackground=%COLOR_BTNFACE+1
      wc.lpszMenuName=%NULL
      wc.cbSize=SizeOf(wc)
      If IsFalse(RegisterClassEx(wc)) Then
         MsgBox("We've Failed In Our Attempt To Register Our MyLabel Class!!!")
      End If
    Case %WM_DESTROY
      PostQuitMessage 0
      fnWndProc=0
      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 iShow As Long) As Long
  Local winclass As WndClassEx
  Local szAppName As Asciiz*16
  Local Msg As tagMsg
  Local hWnd As Dword

  szAppName="Label2"
  winclass.cbSize=SizeOf(winclass)
  winclass.style=%CS_HREDRAW Or %CS_VREDRAW
  winclass.lpfnWndProc=CodePtr(fnWndProc)
  winclass.cbClsExtra=0
  winclass.cbWndExtra=0
  winclass.hInstance=hIns
  winclass.hIcon=LoadIcon(%NULL, ByVal %IDI_APPLICATION)
  winclass.hCursor=LoadCursor(%NULL, ByVal %IDC_ARROW)
  winclass.hbrBackground=%COLOR_BTNFACE+1
  winclass.lpszMenuName=%NULL
  winclass.lpszClassName=VarPtr(szAppName)
  Call RegisterClassEx(winclass)
  hWnd=CreateWindowEx(0,szAppName,"Label2",%WS_OVERLAPPEDWINDOW,200,100,325,300,0,0,hIns,ByVal 0)
  Call ShowWindow(hWnd,iShow)
  While GetMessage(Msg,%NULL,0,0)
    TranslateMessage Msg
    DispatchMessage Msg
  Wend

  Function=msg.wParam
End Function

     When you ran this program it looked the same as what we had with Label1.bas, and that was a blank screen.  Well, give me a chance!  We're still building!  What have we accomplished so far though?  Well, we added WM_CREATE processing code to the Main Window's Window Procedure, and within that code we managed to register a window class that would be our new "MyLabel" class.  And we know it registered OK because when we ran it we didn't get the message box stating that RegisterClassEx() failed (see the msgbox() above).  Finally, we provided an empty window procedure for our new "MyLabel" class.  We had to do that or the program would not have compiled.

     OK, so what's the next step then in getting this newly created class to produce a label for us?  Well, a class doesn't really produce any output.  In terms of the oft used analogy in object oriented programming the class is the cookie cutter and the object is the cookie.  All the class is, is a template out of which actual and specific instances of windows are created.  So we need to make an actual window of this new class with the CreateWindowEx() function call.  So lets think about that some.  Referring back to CreateStruct.bas and the various CreateWindowEx() calls we made to create the various labels in that program we realize that we want our label control to work pretty much just like other windows controls.  We'll want to pass into our label the text that is to appear in the label through that third 'szCaption' parameter.  Well, lets try adding a couple CreateWindowEx() calls to the program we have so far.  If you look at the parameters you'll see we need a control id for each label so I've added %IDC_LABEL1 and %IDC_LABEL2 to the top of the program.  Also, lets add a  %WS_BORDER style to the typical %WS_CHILD Or %WS_VISIBLE styles so we'll see the windows if they are even created.  Try Label3.bas below...

Code: [Select]
#Compile Exe  "Label3.exe"     ‘Actually attempts to create two labels of our
#Include "Win32api.inc"        ‘custom label class.  There is no text yet in
%IDC_LABEL1 = 1201             ‘the labels but at least they are visible.
%IDC_LABEL2 = 1202

Function fnLabelWndProc(ByVal hWnd As Long,ByVal wMsg As Long,ByVal wParam As Long,ByVal lParam As Long) As Long
  fnLabelWndProc=DefWindowProc(hWnd,wMsg,wParam,lParam)
End Function

Function fnWndProc(ByVal hWnd As Long,ByVal wMsg As Long,ByVal wParam As Long,ByVal lParam As Long) As Long
  Select Case As Long wMsg
    Case %WM_CREATE
      Local hInst As Dword
      Local lpCreateStruct As CREATESTRUCT Ptr
      Local szClassName As Asciiz*16
      Local wc As WndClassEx
      Local hLabel1 As Dword, hLabel2 As Dword
      lpCreateStruct=lParam
      hInst=@lpCreateStruct.hInstance
      szClassName="MyLabel"
      wc.lpfnWndProc=CodePtr(fnLabelWndProc)
      wc.lpszClassName=VarPtr(szClassName)
      wc.style=%CS_HREDRAW Or %CS_VREDRAW
      wc.cbClsExtra=0
      wc.cbWndExtra=0
      wc.hInstance=hInst
      wc.hIcon=LoadIcon(%NULL, ByVal %IDI_APPLICATION)
      wc.hCursor=LoadCursor(%NULL, ByVal %IDC_ARROW)
      wc.hbrBackground=%COLOR_BTNFACE+1
      wc.lpszMenuName=%NULL
      wc.cbSize=SizeOf(wc)
      If IsFalse(RegisterClassEx(wc)) Then
         MsgBox("We've Failed In Our Attempt To Register Our MyLabel Class!!!")
         fnWndProc=-1
         Exit Function
      End If
      hLabel1= _                                    'Try To Make 1st Label
      CreateWindowEx _
      ( _
        0, _                                        'No Extended Styles
        "MyLabel", _                                'Class Name We Conjured Up
        "Label1", _                                 'Caption or Window Text To Be In Label
        %WS_CHILD Or %WS_VISIBLE Or %WS_BORDER, _   'Lets add %WS_BORDER to see if this is working
        20,10,50,20, _                              'Where the label should show up on the Form
        hWnd, _                                     'Parent is main window
        %IDC_LABEL1, _                              'Control ID for label (see equates)
        hInst, _                                    'We got this from Window Creation CREATESTRUCT
        ByVal 0 _                                   'We're not passing any special window creation data
      )
      
      hLabel2= _                                    'Try To Make 2nd Label
      CreateWindowEx _
      ( _
        0, _                                        'No Extended Styles
        "MyLabel", _                                'Class Name We Conjured Up
        "Label2", _                                 'Caption or Window Text To Be In Label
        %WS_CHILD Or %WS_VISIBLE Or %WS_BORDER, _   'Lets add %WS_BORDER to see if this is working
        20,45,50,20, _                              'Where the label should show up on the Form
        hWnd, _                                     'Parent is main window
        %IDC_LABEL2, _                              'Control ID for label (see equates)
        hInst, _                                    'We got this from Window Creation CREATESTRUCT
        ByVal 0 _                                   'We're not passing any special window creation data
      )
      fnWndProc=0
      Exit Function
    Case %WM_DESTROY
      PostQuitMessage 0
      fnWndProc=0
      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 iShow As Long) As Long
  Local winclass As WndClassEx
  Local szAppName As Asciiz*16
  Local Msg As tagMsg
  Local hWnd As Dword

  szAppName="Label3"
  winclass.cbSize=SizeOf(winclass)
  winclass.style=%CS_HREDRAW Or %CS_VREDRAW
  winclass.lpfnWndProc=CodePtr(fnWndProc)
  winclass.cbClsExtra=0
  winclass.cbWndExtra=0
  winclass.hInstance=hIns
  winclass.hIcon=LoadIcon(%NULL, ByVal %IDI_APPLICATION)
  winclass.hCursor=LoadCursor(%NULL, ByVal %IDC_ARROW)
  winclass.hbrBackground=%COLOR_BTNFACE+1
  winclass.lpszMenuName=%NULL
  winclass.lpszClassName=VarPtr(szAppName)
  Call RegisterClassEx(winclass)
  hWnd=CreateWindowEx(0,szAppName,"Label3",%WS_OVERLAPPEDWINDOW,200,100,325,300,0,0,hIns,ByVal 0)
  Call ShowWindow(hWnd,iShow)
  While GetMessage(Msg,%NULL,0,0)
    TranslateMessage Msg
    DispatchMessage Msg
  Wend

  Function=msg.wParam
End Function

     Well, what do you think?  Are we making progress or not?  The CreateWindowEx() call must be succeeding, right?  We did get windows created because you must have seen the outlines of the labels due to the addition of that %WS_BORDER style?  OK, we got label child windows, so what's next?  We certainly need the label text right?  If we can get that we've succeeded!  OK, we know how to output text to a window because we've spent a lot of time studying Form2.bas and Form3.bas in this tutorial, right?  Right!  So then we have some more difficult questions to answer.  First is where are we going to put a TextOut() call to draw the label's text?  Could we put it right after the RegisterClassEx() function call where we registered the "MyLabel" class?  Yes, we could, but if we are going to do that then why are we even going to the trouble of creating a label custom control that will act and behave like other standard windows controls in that the data used by the control is passed in during a CreateWindowEx() call?  If we are going to do that we might as well dispense with the whole idea of a custom class and just use TextOut() directly on the window for a label to the text boxes instead of creating a control.  OK, we want to make a label control so as to get on with this tutorial and find out whatever it is that I'm trying to teach here.
« Last Edit: April 07, 2011, 11:04:39 PM by Frederick J. Harris »

Offline Frederick J. Harris

  • Hero Member
  • *****
  • Posts: 914
  • User-Rate: +16/-0
    • Frederick J. Harris
 
     All controls do whatever it is they do in response to standard windows messages that are received in their window procedure.  And the window procedure for "MyLabel" is fnLabelWndProc().  So that means that the TextOut() call to create the label's text must be in the label's window procedure - fnLabelWndProc().  But where?  At this point all we have is a skeleton made necessary so that the program would compile.  Any messages sent to the label's window procedure are just being passed back to windows for default message processing.  So should we make a WM_CREATE case and put a TextOut() call there to draw the label's text when the label is created?  Unfortunately, that won't work.  The painting of window text directly to a window will only work if the window is fully constituted and visible on the screen.  This is not the case during a WM_CREATE message.  Look down in WinMain() and you'll see that the ShowWindow() call that makes even the main window visible is under the CreateWindowEx() call for the main window, and WM_CREATE processing in fnWndProc is occurring before even the CreateWindowEx() call for the main window returns.  So not only won't the label be visible during the label's WM_CREATE message, the main window won't either.  The fact is, window painting or outputting text is best done in response to a WM_PAINT message, and that is where you will find other previous examples of TextOut() in this tutorial.

     Having settled that question, and looking in more detail at the TextOut() function call we're going to use in a WM_PAINT message handler to print the label's text, we find that we need as a parameter in the TextOut() call the text itself to be output.  That certainly makes sense.  Only the question is, "How are we going to get it"?

     As things stand now we have an empty window procedure in fnLabelWndProc() that has the usual four parameters that all window procedures must have, i.e., hWnd, wMsg, wParam, and lParam.  We can't go modifying that so as to pass the text string into fnLabelWndProc() through an extra fifth parameter because Windows won't allow that.  One of  the rules you must live by in programming for Windows is to leave the signature of the Window Procedure alone.  So if we want to get our label's text into the window procedure fnLabelWndProc() from the CreateWindowEx() call that creates the label in the main window's fnWndProc(), we can't pass it in as a parameter.  The third parameter in the CreateWindowEx() call that creates the label is the text we want, i.e., "Label1" and "Label2", but how will we get it there?  What is the title of this tutorial?  Did you work through CreateStruct.bas?  Of course you did!  We'll make a WM_CREATE case in a Select Case block in fnLabelWndProc(), and when that WM_CREATE message comes through, we know the lParam parameter is a pointer to a CREATESTRUCT type one of whose members is a pointer to the caption of the label first specified in the CreateWindowEx() call back in fnWndProc()!!!  See! There is a reason for the pain and torment I'm putting you through!

     Before we get too excited and launch directly into the next version of the program in our attempt to get our custom label working, let me point out one complication.  We're going to need two case statements in the fnLabelWndProc() to pull this off.  First, we'll need a WM_CREATE case to obtain the desired caption for the label from the pointer to a CREATESTRUCT variable passed in through the lParam.  But if we assign that caption to a local variable it will be lost as soon as WM_CREATE processing is complete and the procedure is exited.  Then, when a WM_PAINT message comes through and we want to print the label's text, it will have been lost.  This suggests that we need a variable that's declared as either static or global to pull this off successfully.  And globals are out of the question.  Recall the castigation and abuse I received back in Form2 by using one?  Statics are only slightly better, but at least their visibility is limited to the procedure where they are declared or to where they are passed as parameters.  We don't really have much choice here since locals simply won't work.  So lets give it a shot.  Compile and run Label4.bas, and we'll see how we are doing...

Code: [Select]
#Compile Exe  "Label4.exe"    'Label4.bas  Now we're actually managing to get the
#Include "Win32api.inc"       'the caption of the labels from the CREATESTRUCT
%IDC_LABEL1 = 1201            'passed in the WM_CREATE message.  However, all
%IDC_LABEL2 = 1203            'is not well.  The same caption shows up on both
                              'labels!
                             
Function fnLabelWndProc(ByVal hWnd As Long,ByVal wMsg As Long,ByVal wParam As Long,ByVal lParam As Long) As Long
  Static szCaption As Asciiz*32

  Select Case As Long wMsg
    Case %WM_CREATE
      Local lpCreateStruct As CREATESTRUCT Ptr
      lpCreateStruct=lParam
      szCaption=@lpCreateStruct.@lpszName
      fnLabelWndProc=0
      Exit Function
    Case %WM_PAINT
      Local lpPaint As PAINTSTRUCT
      Local hDC As Long
      hDC=BeginPaint(hWnd,lpPaint)
      Call SetBkMode(hDC,%TRANSPARENT)
      Call TextOut(hDC,0,0,szCaption,Len(szCaption))
      Call EndPaint(hWnd,lpPaint)
      fnLabelWndProc=0
      Exit Function
  End Select

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

Function fnWndProc(ByVal hWnd As Long,ByVal wMsg As Long,ByVal wParam As Long,ByVal lParam As Long) As Long
  Select Case As Long wMsg
    Case %WM_CREATE
      Local hInst As Dword
      Local lpCreateStruct As CREATESTRUCT Ptr
      Local szClassName As Asciiz*16
      Local wc As WndClassEx
      Local hLabel1 As Dword, hLabel2 As Dword
      lpCreateStruct=lParam
      hInst=@lpCreateStruct.hInstance
      szClassName="MyLabel"
      wc.lpfnWndProc=CodePtr(fnLabelWndProc)
      wc.lpszClassName=VarPtr(szClassName)
      wc.style=%CS_HREDRAW Or %CS_VREDRAW
      wc.cbClsExtra=0
      wc.cbWndExtra=0
      wc.hInstance=hInst
      wc.hIcon=LoadIcon(%NULL, ByVal %IDI_APPLICATION)
      wc.hCursor=LoadCursor(%NULL, ByVal %IDC_ARROW)
      wc.hbrBackground=%COLOR_BTNFACE+1
      wc.lpszMenuName=%NULL
      wc.cbSize=SizeOf(wc)
      Call RegisterClassEx(wc)

      hLabel1= _                      'Try To Make 1st Label
      CreateWindowEx _
      ( _
        0, _                          'No Extended Styles
        "MyLabel", _                  'Class Name We Conjured Up
        "Label1", _                   'Caption or Window Text To Be In Label
        %WS_CHILD Or %WS_VISIBLE, _   'Lets add %WS_BORDER to see if this is working
        20,10,50,20, _                'Where the label should show up on the Form
        hWnd, _                       'Parent is main window
        %IDC_LABEL1, _                'Control ID for label (see equates)
        hInst, _                      'We got this from Window Creation CREATESTRUCT
        ByVal 0 _                     'We're not passing any special window creation data
      )

      hLabel2= _                      'Try To Make 2nd Label
      CreateWindowEx _
      ( _
        0, _                          'No Extended Styles
        "MyLabel", _                  'Class Name We Conjured Up
        "Label2", _                   'Caption or Window Text To Be In Label
        %WS_CHILD Or %WS_VISIBLE, _   'Lets add %WS_BORDER to see if this is working
        20,45,50,20, _                'Where the label should show up on the Form
        hWnd, _                       'Parent is main window
        %IDC_LABEL2, _                'Control ID for label (see equates)
        hInst, _                      'We got this from Window Creation CREATESTRUCT
        ByVal 0 _                     'We're not passing any special window creation data
      )
      fnWndProc=0
      Exit Function
    Case %WM_DESTROY
      PostQuitMessage 0
      fnWndProc=0
      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 iShow As Long) As Long
  Local winclass As WndClassEx
  Local szAppName As Asciiz*16
  Local Msg As tagMsg
  Local hWnd As Dword

  szAppName="Label4"
  winclass.cbSize=SizeOf(winclass)
  winclass.style=%CS_HREDRAW Or %CS_VREDRAW
  winclass.lpfnWndProc=CodePtr(fnWndProc)
  winclass.cbClsExtra=0
  winclass.cbWndExtra=0
  winclass.hInstance=hIns
  winclass.hIcon=LoadIcon(%NULL, ByVal %IDI_APPLICATION)
  winclass.hCursor=LoadCursor(%NULL, ByVal %IDC_ARROW)
  winclass.hbrBackground=%COLOR_BTNFACE+1
  winclass.lpszMenuName=%NULL
  winclass.lpszClassName=VarPtr(szAppName)
  Call RegisterClassEx(winclass)
  hWnd=CreateWindowEx(0,szAppName,"Label4",%WS_OVERLAPPEDWINDOW,200,100,325,300,0,0,hIns,ByVal 0)
  Call ShowWindow(hWnd,iShow)
  While GetMessage(Msg,%NULL,0,0)
    TranslateMessage Msg
    DispatchMessage Msg
  Wend

  Function=msg.wParam
End Function
 

     Well, what do you think?  We've got labels that look just like Microsoft approved labels, but I believe we have a problem.  In case you haven't noticed, both labels read "Label2".  That definitely isn't right, or what we wanted.  What appears to be happening is that we only have one variable for the label's text, i.e., szCaption, and all instances of the label are getting assigned whatever text is supposed to be in the last one we created.  In the case above there would have been a WM_CREATE message when CreateWindowEx() was called for label1 and another when CreateWindowEx() was called for label2.  In each case the correct text would have been put into szCaption, but after the two WM_CREATE messages were done and the Form was made visible, all that would have been seen in the processing of the WM_PAINT message code was the last caption to be put in szCaption, and that is why Label2 is printed for both labels.  What it appears we need is a separate variable for each instantiation of a label.  A string array may come to mind but how many elements should the array have?  Should it be set at 10 elements and then a statement put into the documentation that the maximum number of "MyLabel" objects is 10?  Or should it be extended to a hundred?  Well, that would be a solution but not a very good one.  Realize that as windows are moved about on a screen and covered and then uncovered by other windows, WM_PAINT messages can come fast and furious.  So your logic would have to flawlessly handle the issue of array subscripting in seeing that the correct text be associated with the correct label.  The whole thing is a bad idea and a 'can of worms'.  So I guess you expect I'm going to give you the solution to this vexing problem?  Well, I will, but not right away.  I'm going to make you work and sweat for it.

     We're going to have to put this series of Label programs 'on hold' temporarily and look at another program where we will be able to find a solution to the issue of a control's storage of unique data pertaining specifically to a given unique instance of a control or class.  We'll take a look at one of my favorite Charles Petzold's programs, and that would be his Checkers3 program in his famous "Programming Windows 95".  I've made a PowerBASIC conversion of that program that is quite close to Petzold's C language example.  What the program does is create a main window that is painted plain white.  Then on top of this main window a five by five grid of child windows are created.  The child windows have a border and are white too, but the effect is a checkerboard or grid.  You can resize the main window and program logic will at the same time stretch or shrink the child windows to match the borders of the main window.  The functionality that the program has is that if you click your mouse over any particular one of the 25 child windows, the program crisscrosses the child window with a big 'X'.  If you like, you can go along and fill all 25 windows with X's.  But there is more.  If you again click on a window that has already been X'ed, the X is removed.  So you see there is a toggling mechanism involved with each window where each window must somehow manage its 'state', that is, keep track of whether it is toggled on or off in terms of the big X.  I personally like to play with it, but then I'm easily entertained!

     The key issue involved is one of maintaining 'state'.  If you think about Windows programs you have used that contain typical windows controls such as labels, text boxes, list boxes, check boxes, etc., you know that if you do something to alter the control in any way, for example type text in a text box or check or un-check a check box, the control will maintain its current state until you change it or close the program.  Related to this is the fact that each instantiation of a control must maintain its data separate and independent of other controls - even of the same type.  And our Labels program has failed to behave in this manner.

     Below is my PowerBASIC adaptation of Charles Petzold's Checkers program.  Please compile and run it.  I will discuss it afterwards...

Code: [Select]
'PowerBASIC adaptation of Charles Petzold's Checkers3 program from his
'Programming Windows 95' book.  This program creates a five by five grid of 25
#Compile Exe "Checkers"  'child windows on the surface of a parent 'main'
#Include "Win32api.inc"  'window.  When you left mouse button click on any
%GRID_SIZE = 5           'window in the grid the program draws a big X across
                         'the window.  The X is removed by clicking again.

Function fnChildWndProc(ByVal hWnd As Long,ByVal wMsg As Long,ByVal wParam As Long,ByVal lParam As Long) As Long
  Select Case As Long wMsg
    Case %WM_CREATE
      Call SetWindowLong _   'This is the Window Procedure for all windows of
      ( _                    'the class 'Child'  In this program 25 such
        hWnd, _              'windows are created in a loop down in the Window
        0, _                 'Procedure for the Main Window (fnMainWndProc).
        0 _                  'Every CreateWindow() Call there will cause a
      )                      'WM_CREATE message to be sent here.  This procedure
      fnChildWndProc=0       'will then receive 25 WM_CREATE messages.  When the
      Exit Function          'Window Class for these windows was registered down
    Case %WM_LBUTTONDOWN     'in fnMainWndProc(), the cbWndExtra bytes member of
      Call SetWindowLong _   'the WNDCLASSEX type was set equal to four.  What
      ( _                    'this means is that every window of this class
        hWnd, _              'created will have a four byte area to store
        0, _                 'something pertaining to the program's use of the
        1 Xor GetWindowLong(hWnd,0) _ 'window.What this program is going to do
      )                      'with those four bytes is store a simple %TRUE /
      Call InvalidateRect _  '%FALSE (1/0) value there that represents whether
      ( _                    'the window has an 'X' in it or not.  When the
        hWnd, _        'program starts up all the windows are to be empty.
        Byval %NULL, _ 'Therefore, in the WM_CREATE processing the Api function
        %TRUE _        'SetWindowLong() is used to store a value of zero in this
      )                'four byte area for each window.  When you click your
      fnChildWndProc=0 'mouse button over a window this procedure will receive
      Exit Function    'a WM_LBUTTONDOWN message with the hWnd parameter
    Case %WM_PAINT     'reflecting the window which received the mouse click.
      Local hDC As Dword     'At that point what the program must do is 'toggle'
      Local ps As PAINTSTRUCT'whatever value is present in the cbWndExtra bytes
      Local rc As RECT       'to its opposite. If true, we'll want to toggle to
      Local pt As POINTAPI   'false.  If false, we'll want to toggle to true. If
      hDC=BeginPaint(hWnd,ps)'1' is Xor'ed with the XOR bit wise operator 
      Call GetClientRect(hWnd,rc)   'against '0', the result is 1.  If '1' is
      If GetWindowLong(hWnd,0) Then 'Xor'ed against '1', the result is '0'.  You
         pt.x=0 : pt.y=0            'can see that done in the GetWindowLong()
         Call MoveToEx _  'call within the SetWindowLong() call in the
         ( _              'WM_LBUTTONDOWN processing.  What then finally causes
           hDC, _         'the big 'X' to crisscross the child window over 
           0, _           'which a mouse click occurs is the InvalidateRect()
           0, _           'call directly after the SetWindowLong() Call that
           pt _           ' toggled the cbWndExtra bytes.  This call forces a
         )                'WM_PAINT message, just like you learned back in
         Call LineTo _  'Form2.bas.  When the WM_PAINT message comes through
         ( _            'what the program's logic must do is interrogate those 
           hDC, _       'cbWndExtra bytes to determine whether a %TRUE or a
           rc.nRight, _ '%FALSE value is stored there.  If it finds a %TRUE 
           rc.nBottom _ 'there program must draw an 'X across the window
         )              'extremities. If it finds a %FALSE there, it simply
         Call MoveToEx _'repaints the window blank, thereby removing anything
         ( _            'that might have been there.  Note the PAINTSTRUCT, RECT
           hDC, _       'and POINTAPI types used in the WM_PAINT processing. You
           0, _         'need to understand what these are, where you can find
           rc.nBottom, _'them, and how they are used.  These are types required
           pt _         'by Windows API calls.  They can be found in
         )              'Win32Api.inc. You can learn how they are used by
         Call LineTo(hDC,rc.nRight,0)  'looking them up and studying this
      End If                 'tutorial.
      Call EndPaint(hWnd,ps)
      fnChildWndProc=0 
      Exit Function     
  End Select  '
              '
  fnChildWndProc=DefWindowProc(hWnd,wMsg,wParam,lParam)
End Function

Function fnMainWndProc(ByVal hWnd As Long,ByVal wMsg As Long,ByVal wParam As Long,ByVal lParam As Long) As Long
  Static hChild() As Dword    'The storage class of 'static' will create this
  Register i As Long          'variable in the program's data segment, which has
  Register j As Long          'a duration' extending for the life time of the
                              'program, however, its 'scope' is limited to this
  Select Case As Long wMsg    'procedure or any procedures to which it is passed
    Case %WM_CREATE           'from this procedure.
      Local lpCreateStruct As CREATESTRUCT Ptr
      Local szClassName As Asciiz*16
      Local wc As WNDCLASSEX
      Local hInst As Dword
      Local k As Dword
      Redim hChild(5,5)
      lpCreateStruct=lParam
      hInst=@lpCreateStruct.hInstance
      szClassName="Child" 'Windows Will Recognize This Window Class As 'Child'
      wc.lpszClassName=VarPtr(szClassName) 'Assign Pointer To lpszClassName
      wc.cbSize=SizeOf(wc)                 'Member Of WNDCLASSEX
      wc.style=%CS_HREDRAW Or %CS_VREDRAW
      wc.lpfnWndProc=CodePtr(fnChildWndProc) 'Pointer To Window Procedure For
      wc.cbClsExtra=0                        'All Windows Of Class 'Child
      wc.cbWndExtra=4                        'Four Extra Bytes To Hold Boolean
      wc.hInstance=hInst                     'True/False Value
      wc.hIcon=LoadIcon(%NULL, ByVal %IDI_APPLICATION)
      wc.hCursor=LoadCursor(%NULL, ByVal %IDC_ARROW)
      wc.hbrBackground=GetStockObject(%WHITE_BRUSH)
      wc.lpszMenuName=%NULL
      Call RegisterClassEx(wc)  'Register 'Child' Window Class.  Create 5 X 5
      For i=0 To %GRID_SIZE-1   '(GRID_SIZE) Grid Of 'Child' Windows On Main
        For j=0 To %GRID_SIZE-1 'Window. Main Window Is The Parent Of Each Child
          hChild(i,j)=CreateWindow("Child","",%WS_CHILD Or %WS_VISIBLE Or %WS_BORDER,0,0,0,0,hWnd,k,hInst,ByVal 0)
          Incr k
        Next j
      Next i
      fnMainWndProc=0
      Exit Function
    Case %WM_SIZE                       'A WM_SIZE Msg Will Come Through The
      Local cxBlock,cyBlock As Long     'Main WndProc After The WM_CREATE.  As
      cxBlock=Lowrd(lParam)/%GRID_SIZE  'You Learned Back In Form2.bas, When A
      cyBlock=Hiwrd(lParam)/%GRID_SIZE  'WM_SIZE Msg Comes Through, The lParam
      For i=0 To %GRID_SIZE-1           'Will Contain the Window Height And
        For j=0 To %GRID_SIZE-1         'Width In Its Hi And Lo Order Words.  At
          Call MoveWindow(hChild(j,i),j*cxBlock,i*cyBlock,cxBlock,cyBlock,%TRUE)
        Next j         'This Point The Child Windows Are Assigned The Correct         
      Next i           'Dimensions Through A Call To MoveWindow().  Check This   
      fnMainWndProc=0  'Out In Your Api Reference!
      Exit Function
    Case %WM_DESTROY
      Call PostQuitMessage(0)
      fnMainWndProc=0
      Exit Function
  End Select

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

Function WinMain(ByVal hIns As Long,ByVal hPrev As Long,ByVal lpCL As Asciiz Ptr,ByVal iShow As Long) As Long
  Local szAppName As Asciiz*16
  Local wc As WndClassEx
  Local Msg As tagMsg
  Local hWnd As Dword

  szAppName="Checkers"                             : wc.lpszClassName=VarPtr(szAppName)
  wc.lpfnWndProc=CodePtr(fnMainWndProc)            : wc.hInstance=hIns
  wc.cbSize=SizeOf(wc)                             : wc.style=%CS_HREDRAW Or %CS_VREDRAW
  wc.cbClsExtra=0                                  : wc.cbWndExtra=0
  wc.hIcon=LoadIcon(%NULL, ByVal %IDI_APPLICATION) : wc.hCursor=LoadCursor(%NULL, ByVal %IDC_ARROW)
  wc.hbrBackground=GetStockObject(%WHITE_BRUSH)    : wc.lpszMenuName=%NULL
  wc.hIconSm=LoadIcon(hIns, ByVal %IDI_APPLICATION)
  Call RegisterClassEx(wc)
  hWnd=CreateWindow(szAppName,"Checkers",%WS_OVERLAPPEDWINDOW,200,100,525,450,0,0,hIns,ByVal 0)
  Call ShowWindow(hWnd,iShow)
  While GetMessage(Msg,%NULL,0,0)
    Call TranslateMessage(Msg)
    Call DispatchMessage(Msg)
  Wend

  Function=msg.wParam
End Function

     I'm assuming you ran this program at this point.  Taking a high level first look at the program you see there are only three procedures within it.  Two of them are window procedures and the other is WinMain().  Looking at WinMain() you'll see there isn't really anything different there than in any of the other WinMain() procedures you've seen to this point.  However, in the window procedure for the main program window (fnWndProc()) and within the %WM_CREATE Case are statements to define another window class separate from the main "Checkers" window class.  And this new class has an lpszClassName of "Child".  These are the windows that will overlay the main window and form the checkerboard or grid.  You'll note that the lpfnWndProc member of the WndClassEx type being filled out is fnChildWndProc, and that function accounts for the last of the three procedures in this program.  If you look closely at how the other members of the WndClassEx type are being filled out you'll see something different from anything we've done up until now.  You may recall how I denigrated the filling out of WndClassEx types in the past as being basically a 'boilerplate' operation of copy and paste, but sometimes it is necessary to modify that code in some way.  Here you may note that we set the cbWndExtra bytes to 4.  This is the first time we have done this.  What these cbWndExtra bytes are for is to store any sort of data that a window may need to store in terms of maintaining state, or, for any reason at all really, that will be useful in some way to a program's functionality.  In this Checkers program the four bytes are used in a very simple way to store a %TRUE/%FALSE boolean value signifying whether the present state of a child window is X'ed or not.

     Windows has provided two Api functions to Get/Set values stored in cbWndExtra bytes.  The functions are GetWindowLong() and SetWindowLong().  These functions have other uses besides setting cbWndExtra bytes, but that is what we are going to be examining here.  Below is part of the documentation for SetWindowLong()...

Code: [Select]
SetWindowLong  The SetWindowLong function changes an attribute of the specified window.                               
               The function also sets a 32-bit (long) value at the specified offset
               into the extra window memory of a window.

LONG SetWindowLong
(
  HWND hWnd,       // handle of window
  int nIndex,      // offset of value to set
  LONG dwNewLong   // new value
);

     The first parameter identifies the specific window where a value needs to be stored.  The second parameter specifies the beginning byte position where a long value will be written.  It is possible, for example, to specify 20 or 40 cbWndExtra bytes.  If that is the case the first four bytes would be bytes 0 through 3, the second 4 through 7, etc.  In our case with just four bytes for one long value we will be specifying an offset of 0.  The last parameter is the value to be stored.  We will be putting either a 1 or 0 there for true/false.

     The getWindowLong() function is similar and looks like this...

Code: [Select]
GetWindowLong  The GetWindowLong function retrieves information about the specified
               window. The function also retrieves the 32-bit (long) value at the
               specified offset into the extra window memory of a window.

LONG GetWindowLong
(
  HWND hWnd,  // handle of window
  int nIndex  // offset of value to retrieve
);

     To retrieve the value stored in the cbWndExtra bytes you just call this function passing it the handle of the window you are interested in, and the byte offset of the value in the cbWndExtra bytes.  In our case the latter will be zero.

     Continuing then with the discussion in fnMainWndProc we see that the Child window class is filled out and registered.  Of course you understand by now that all this is happening long before even the main window becomes visible?  After the new Child window class is registered the remaining logic under the WM_CREATE case runs through a double i,j For Loop to CreateWindowEx() twenty-five child windows.  You may find it interesting to note that the windows are all created at location 0,0 on the main window and with zero width and height.  This works because they aren't visible yet anyway at the time of a WM_CREATE message, although all their internal data structures can be created by Windows.

     What actually happens as the program runs through the For Loop creating each window is that for every call a WM_CREATE message will be sent to the Child window's window procedure fnChildWndProc().  What happens there is that a SetWindowLong() call is made for each child window setting its cbWndExtra bytes to 0 or %FALSE.  This is because when the windows finally become visible, we don't want their initial condition to be X'ed.  We're a long way from there yet though.

     The next processing that will occur in this program is that a WM_SIZE message will be sent to the main window.  If you recall from back in Form2, associated with the WM_SIZE message is the lParam parameter containing in its low and high order 16 bit words, the width and height of the window's client area. You can see the logic in place within the WM_SIZE case to divide the main window width and height values into five equal parts (see the equate at top %GRID_SIZE), and then run all the child windows through another double i,j For Loop to resize the windows to appropriate dimensions.  The MoveWindow() Api function is used for this.

     What happens next is interesting.  WM_PAINT messages will be sent to all these windows, including the main window.  The main window, however, doesn't have a WM_PAINT handler.  The child windows, however, do.  If you look at the WM_PAINT logic in fnChildWndProc() you'll see the following ‘If’ in addition to other usual things you always see in Paint Message Handlers...

Code: [Select]
If GetWindowLong(hWnd,0) Then
   pt.x=0 : pt.y=0           
   Call MoveToEx(hDC,0,0,pt)
   Call LineTo(hDC,rc.nRight,rc.nBottom)
   Call MoveToEx(hDC,0,rc.nBottom,pt)
   Call LineTo(hDC,rc.nRight,0)
End If


     Here the program is checking if a non-zero value is stored in the cbWndExtra bytes we set up in the class registration.  In the case of the first WM_PAINT message these windows will receive the program flow won't enter any of the If statements because we set a zero value in the cbWndExtra bytes with SetWindowLong() in the WM_CREATE processing.  So all that will happen is BeginPaint() and EndPaint() procedure calls with no kind of drawing activity, in other words, just blank white windows with borders around them.

     The core of the program though is what happens if a left button down occurs over any of the child windows.  Reproduced below is the case for that...
 
Case %WM_LBUTTONDOWN
  Call SetWindowLong(hWnd, 0, 1 Xor GetWindowLong(hWnd,0))

     This enigmatic looking thing is rather tricky, so I'll explain it.  Before I do that though I'll tell you in very simple terms what it does.  It simply toggles whatever boolean value is stored in the cbWndExtra bytes.  If a zero is there it replaces it with a 1.  If a 1 is there it replaces it with a 0.  You see used here the Xor logical operator.  The way this works is that the Xor operator will only return %TRUE if both its operands are different.  For example, consider this...

1 Xor 1 = 0
1 Xor 0 = 1
0 Xor 0 = 0

     So what we have here is that when the SetWindowLong() function is called in response to a left mouse button click, that function in turn calls GetWindowLong() to find out what is presently stored in the cbWndExtraBytes.  If a 0 is stored there (as would be the case when the window first becomes visible to you), then the operation ...

1 Xor 0

occurs and the result of that operation is 1 as you can see in my 'truth table' above.  In that case a 1 or %TRUE will be stored in the cbWndExtra bytes as SetWindowLong places that value there.  If when a mouse button is clicked GetWindowLong() finds a %TRUE stored there (as would be the case if the window was X'ed), then this operation will occur...

1 Xor 1.

     The result of that is zero because Xor only returns %TRUE if both operands are different.  As you can see this elegantly and effectively toggles a value stored in the cbWndExtra bytes between 0 and 1.

     But we're not done yet with WM_LEFTBUTTONDOWN processing.  The SetWindowLong() function call is followed by an InvalidateRect() call for the specific child window over which a left button down occurred.  This forces a WM_PAINT message to be sent to that specific child window and we are back again to the WM_PAINT message handler we were just discussing several paragraphs up where an If statement checked whether or not a zero or one was stored in the specific window's cbWndExtra bytes.  I believe you can put the rest of the pieces together.  Absolutely, fundamentally, astonishingly, brilliant, isn't it?

     The larger issue of course is that with this fun little program we have come face to face with a technique of staggering significance, power, and potentialities in terms of creating code objects that are capable  of maintaining their 'state' independently of other exactly similar objects.  We have found a way to create 'controls'.  For each of these child windows in Charles' program is essentially an incipient windows control.  Just like a check box or text box, each of these child windows are able to 'remember' what state they are in, and preserve that state independent of other exactly similar objects of the same class.  In short, something our poor Label control "MyLabel" couldn't do.

     Perhaps now we can return to our Label control and 'fix it' now that we have the techniques at our disposal to do so!  If you recall when we left Label4 we had a situation where Label1 couldn't tell itself from Label2 and vice versa, at least in terms of the text string captions they were to independently display.  In that case we had a variable named szCaption for the text the label was to display, but it wasn't independent of various separate instances of the control that were created.  What we clearly need to do after seeing the way Checkers saved its X'ed and not X'ed state in the cbWndExtra bytes is set up something similar where the label's text can be stored independently for each label created.  But how should we do that?  SetWindowLong() and SetWindowLong() are all about storing Longs, not strings, right???  And what was this Tutorial #6 about?  Pointers perhaps? Instance data?  Maybe even dynamic memory allocation?

     Tying all these loose ends together is Label5 below.  It brings together everything we have struggled to learn in this tutorial, i.e., pointers, dynamic memory allocation, instance data, and cbWndExtra bytes...

Code: [Select]
#Compile Exe  "Label5"         'Shows how to create a very simple
#Include "Win32api.inc"        'custom control through the use
%IDC_LABEL1 = 1500             'of cbWndExtra bytes, pointers, and
%IDC_LABEL2 = 1505             'dynamic memory allocation through
%IDC_EDIT1  = 1510             'the use of HeapAlloc() and HeapFree().
%IDC_EDIT2  = 1515

Function fnLabelWndProc(ByVal hWnd As Long,ByVal wMsg As Long,ByVal wParam As Long,ByVal lParam As Long) As Long
  Local pszStr As Asciiz Ptr
  Local hHeap As Dword

  Select Case As Long wMsg
    Case %WM_CREATE
      Local lpCS As CREATESTRUCT Ptr    'First declare CREATESTRUCT ptr to get
      Local szCaption As Asciiz*64      'CreateWindowEx() parameters.  Then
      lpCS=lParam                       'get address of CREATESTRUCT from
      szCaption=@lpCS.@lpszName         'lParam.  The caption we want for the
      hHeap=GetProcessHeap()            'label is stored by the compiler at
      pszStr= _                         '@lpszName.  We need to then allocate
      HeapAlloc _                       'enough memory to store the caption
      ( _                               'plus a null terminator.  Once we do
        hHeap, _                        'obtain a pointer to the memory from
        %HEAP_ZERO_MEMORY, _            'HeapAlloc() we need to copy the Asci
        Len(szCaption)+1 _              'bytes to the allocated location with
      )                                 'Poke$ (test to be sure the memory
      If pszStr Then                    'allocation was successful). Providing
         Poke$ Asciiz,pszStr,szCaption  'all is well, store the pointer to the
         Call SetWindowLong _           'allocated memory in the four byte
         ( _                            'area we set aside for it in the
           hWnd, _                      'cbWndExtra bytes when we created the
           0, _                         'class down in fnWndProc().  If the
           pszStr _                     'memory can't be allocated return a
         )                              '-1 from fnLabelWndProc() and the
      Else                              'CreateWindowEx() call down in
         fnLabelWndProc=-1              'fnWndProc will return a null handle
         Exit Function                  'and logic there can deal with ending
      End If                            'the program.
      fnLabelWndProc=0
      Exit Function
    Case %WM_PAINT
      Local lpPaint As PAINTSTRUCT      'When a label receives this message it
      Local hDC As Long                 'must paint itself.  In other words,
      hDC=BeginPaint(hWnd,lpPaint)      'it must draw its caption.  Before it
      Call SetBkMode(hDC,%TRANSPARENT)  'can do that it must find out what its
      pszStr=GetWindowLong(hWnd,0)      'caption is.  It 'knows' that a pointer
      Call TextOut _                    'to its caption string is stored at
      ( _                               'byte offset 0 in its cbWndExtra bytes.
        hDC, _                          'So all it has to do is retrieve that
        0, _                            'pointer and pass it to TextOut()
        0, _
        @pszStr, _
        Len(@pszStr) _
       )
      Call EndPaint(hWnd,lpPaint)
      fnLabelWndProc=0
      Exit Function                     'Memory that is allocated from the heap
    Case %WM_DESTROY                    'should be restored when it is no
      pszStr=GetWindowLong(hWnd,0)      'longer needed.  Since this program has
      hHeap=GetProcessHeap()            'created two labels, this fnLabelWndProc
      Call HeapFree(hHeap,0,pszStr)     'will receive a WM_DESTROY message for
      fnLabelWndProc=0                  'each one, so HeapFree() will be called
      Exit Function                     'twice here, once for each label.
  End Select

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

Function fnWndProc(ByVal hWnd As Long,ByVal wMsg As Long,ByVal wParam As Long,ByVal lParam As Long) As Long
  Select Case As Long wMsg
    Case %WM_CREATE
      Local hLbl1 As Dword, hLbl2 As Dword
      Local hEdit1 As Dword, hEdit2 As Dword,hInst As Dword
      Local lpCreateStruct As CREATESTRUCT Ptr
      Local szClassName As Asciiz*16
      Local wc As WndClassEx
      lpCreateStruct=lParam
      hInst=@lpCreateStruct.hInstance
      szClassName="MyLabel"                             'Here is where we will
      wc.lpfnWndProc=CodePtr(fnLabelWndProc)            'create a window class
      wc.lpszClassName=VarPtr(szClassName)              'for our custom label
      wc.style=%CS_HREDRAW Or %CS_VREDRAW               'control.  We will have
      wc.cbClsExtra=0                                   'to provide a window
      wc.cbWndExtra=4                                   'procedure for our
      wc.hInstance=hInst                                'label control, and
      wc.hIcon=LoadIcon(%NULL, ByVal %IDI_APPLICATION)  'we'll set aside four
      wc.hCursor=LoadCursor(%NULL, ByVal %IDC_ARROW)    'bytes in the cbWndExtra
      wc.hbrBackground=%COLOR_BTNFACE+1                 'bytes for a pointer to
      wc.lpszMenuName=%NULL                             'the label's text.
      wc.cbSize=SizeOf(wc)
      Call RegisterClassEx(wc)

      'Make Label1
      hLbl1= _                         'We'll create our custom label control
      CreateWindowEx _                 'in exactly the same manner as any other
      ( _                              'windows control, that is, with a
        0, _                           'CreateWindowEx() function call.  Note
        "MyLabel", _                   'that the 2nd parameter in this call is
        "Label1", _                    'the class of control to create.  Here
        %WS_CHILD Or %WS_VISIBLE, _    'we'll just put "MyLabel", and since the
        20,10,50,20, _                 'class has been Registered, Windows will
        hWnd, _                        'go about instantiating it just the same
        %IDC_LABEL1, _                 'as it would for any of the classes
        hInst, _                       'which are internal to it such as edit
        ByVal 0 _                      'controls or list boxes.  To prove this
      )                                'to yourself just compare this call to
                                       'the one below that creates an edit
      'Make Label2                     'control.
      hLbl2=CreateWindowEx( _
        0, _
        "MyLabel", _
        "Label2", _
        %WS_CHILD Or %WS_VISIBLE, _
        20,50,50,20, _
        hWnd, _
        %IDC_LABEL2, _
        hInst, _
        ByVal 0 _
      )

      'Make Edit1  (text box #1)
      hEdit1=CreateWindowEx( _
        %WS_EX_CLIENTEDGE, _
        "Edit", _
        "Edit1", _
        %WS_CHILD Or %WS_VISIBLE Or %ES_AUTOHSCROLL, _
        80,10,150,20, _
        hWnd, _
        %IDC_EDIT1, _
        GetWindowLong(hWnd,%GWL_HINSTANCE), _
        ByVal 0 _
      )

      'Make Edit2  (text box #2)
      hEdit2=CreateWindowEx( _
        %WS_EX_CLIENTEDGE, _
        "Edit", _
        "Edit2", _
        %WS_CHILD Or %WS_VISIBLE Or %ES_AUTOHSCROLL, _
        80,50,150,20, _
        hWnd, _
        %IDC_EDIT2, _
        GetWindowLong(hWnd,%GWL_HINSTANCE), _
        ByVal 0 _
      )
      fnWndProc=0
      Exit Function
    Case %WM_DESTROY
      Call PostQuitMessage(0)
      fnWndProc=0
      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 iShow As Long) As Long
  Local winclass As WndClassEx
  Local szAppName As Asciiz*16
  Local Msg As tagMsg
  Local hWnd As Dword

  szAppName="Label5"
  winclass.cbSize=SizeOf(winclass)
  winclass.style=%CS_HREDRAW Or %CS_VREDRAW
  winclass.lpfnWndProc=CodePtr(fnWndProc)
  winclass.cbClsExtra=0
  winclass.cbWndExtra=0
  winclass.hInstance=hIns
  winclass.hIcon=LoadIcon(%NULL, ByVal %IDI_APPLICATION)
  winclass.hCursor=LoadCursor(%NULL, ByVal %IDC_ARROW)
  winclass.hbrBackground=%COLOR_BTNFACE+1
  winclass.lpszMenuName=%NULL
  winclass.lpszClassName=VarPtr(szAppName)
  Call RegisterClassEx(winclass)
  hWnd=CreateWindow(szAppName,"Label5",%WS_OVERLAPPEDWINDOW,200,100,325,150,0,0,hIns,ByVal 0)
  Call ShowWindow(hWnd,iShow)
  While GetMessage(Msg,%NULL,0,0)
    TranslateMessage Msg
    DispatchMessage Msg
  Wend
     
  Function=msg.wParam
End Function
« Last Edit: September 08, 2007, 06:11:58 PM by José Roca »

Offline Frederick J. Harris

  • Hero Member
  • *****
  • Posts: 914
  • User-Rate: +16/-0
    • Frederick J. Harris
 
Function Pointers

     But believe it or not, we're not done yet!  There are still a couple more things I want to show you about pointers, one of which is how to use them in calling functions.  Yes, that can be done too.  After all, functions have a memory address just like an ordinary variable or user defined type variable. Not only that, we've been calling functions with pointers all along.  That is why in any discussion of pointers I feel this needs to be addressed.  If you look right above here in the WinMain() function for Label5.bas you'll see that the lpfnWndProc member of the winclass type was set equal to the runtime address of the fnWndProc function through PowerBASIC's CodePtr function.

     Lets start with a fairly simple program to show you the basics.  Imagine a program that will have four functions.  They each have one Long integer parameter, they all return a long integer result, but each raises the Long parameter to a different power.  To make it easy lets say the exponents will be two, three, four and five.

     Below is such a program.  Please compile and run it...

 
Code: [Select]
#Compile Exe  "fnPtrs1"                     'Shows how to use function pointers
#Dim All                                    'and PowerBASIC's powerful
                                            'Call Dword statement to execute a
Declare Function ptrFn(x As Long) As Long   'function at a given address.

Function x_Squared(n As Long) As Long       'Returns a long and takes a long
  x_Squared = n * n                         'as a parameter.
End Function

Function x_Cubed(n As Long) As Long         'Returns a long and takes a long
  x_Cubed = n * n * n                       'as a parameter
End Function

Function x_ToTheFourth(n As Long) As Long   'Returns a long and takes a long
  x_ToTheFourth = n * n * n * n             'as a parameter
End Function

Function x_ToTheFifth(n As Long) As Long    'Returns a long and takes a long
  x_ToTheFifth = n * n * n * n * n          'as a parameter
End Function

Function PBMain() As Long        'What matters with function pointers isn't
  Local pFns() As Dword          'what the function does so much as its address,
  Local iResult As Long          'its parameters, and its return value.
  Register i As Long

  Redim pFns(3) As Dword
  pFns(0)=CodePtr(x_Squared)     'pFns(0) will hold integer address of x_Squared
  pFns(1)=CodePtr(x_Cubed)       'pFns(1) will hold address of x_Cubed
  pFns(2)=CodePtr(x_ToTheFourth) 'pFns(2) -------------------- x_ToTheFourth
  pFns(3)=CodePtr(x_ToTheFifth)  'pFns(3) -------------------- x_ToTheFifth
  Print " i             pFns(i)       iResult"
  Print "===================================="
  For i=0 To 3                                     'Send program execution to
    Call Dword pFns(i) Using ptrFn(i) To iResult   'addresses stored in array
    Print i, pFns(i), iResult                      'of function pointers.  Use
  Next i                                           'a 'model' function ptrFn()
  Erase pFns                                       'that has the same function
  Waitkey$                                         'signature as the four fns
                                                   'to be called.  Return the
  PBMain=0                                         'result of the function call
End Function                                       'TO iResult.

Code: [Select]
'Output:
'
' i             pFns(i)       iResult
'====================================
' 0             4200232       0
' 1             4200298       1
' 2             4200369       16
' 3             4200445       243       

     You'll note near the top of this program, named fnPtrs1.bas, the following Declare...

     Declare Function ptrFn(x As Long) As Long

     Nowhere in this program does such a function exist.  This is an example of a declaration of a function without an implementation of the function.  What does exist are four actual functions named x_Squared(), x_Cubed(), x_ToTheFourth(), and x_ToTheFifth().  These four functions that do exist will exist at actual memory addresses when this program is loaded.  These memory addresses are of course 32 bit integers, and the PowerBASIC CodePtr() function can obtain these addresses.  If you look down in PBMain() you will see that the first Local variable declared, pFns(), is an array variable of Dwords.  It is into this array that the integer values of the four function pointers obtained with the CodePtr() function are stored.  pFns(0) was assigned the address of x_Squared(), and so on.  Then the program runs through a For Loop calling each of the four functions through their memory address stored in the pFns array.  You'll note the Call Dword statement was used to call a function loaded at a specific Dword address.  Where the ptrFn() 'model' or 'prototype' comes into play is that in order for the compiler to set up the function call mechanism through an address only it must know the number and type of parameters the function takes and also its return type.  Therefore it is setting up the function call 'Using' this 'model' procedure that itself is not implemented as a 'callable' function.  The return value from each of these called functions is assigned to iReturn through the To keyword.

     There is an interesting application of function pointers possible in modularizing the Window Procedure of a Windows program.  Think for a second of the various message handling functions we have created in this tutorial so far where we created such functions as...

Function fnWndProc_OnCreate(Wea As WndEventArgs) As Long
   ---
   ---
End Function

     You've certainly seen me do that many times for the various messages that need to be handled.  Now every function such as this has a fixed integer runtime memory address such as those in fnPtrs1.bas just above.  Further, the particular message handling function that needs to be called from the Window Procedure is dependant on just one other integer, and that would be the integer value of the wMsg parameter that is sent to the Window Procedure from Windows.  This leads to a situation where there is a one to one relationship between two integers, and they could both be collected together into a type like so...

Code: [Select]
Type MessageHandler                  'This Type Associates a message with the
  wMessage As Long                   'address of the message handler which
  fnPtr    As Dword                  'handles it.  Messages are usually fairly
End Type                             'low numbers, e.g., WM_PAINT is 15.

     An array of such variables could be put together for any given program where you have every message you want to handle associated with the address obtained from the CodePtr function for the function that is to handle that message, like so...

Code: [Select]
Sub AttachMessageHandlers()
  ReDim MsgHdlr(6) As MessageHandler  'Dimension a UDT array of MessageHandler variables
                                     
  MsgHdlr(0).wMessage=%WM_CREATE      : MsgHdlr(0).fnPtr=CodePtr(fnWndProc_OnCreate)
  MsgHdlr(1).wMessage=%WM_MOUSEMOVE   : MsgHdlr(1).fnPtr=CodePtr(fnWndProc_OnMouseMove)
  MsgHdlr(2).wMessage=%WM_SIZE        : MsgHdlr(2).fnPtr=CodePtr(fnWndProc_OnSize)
  MsgHdlr(3).wMessage=%WM_CHAR        : MsgHdlr(3).fnPtr=CodePtr(fnWndProc_OnChar)
  MsgHdlr(4).wMessage=%WM_LBUTTONDOWN : MsgHdlr(4).fnPtr=CodePtr(fnWndProc_OnLButtonDown)
  MsgHdlr(5).wMessage=%WM_PAINT       : MsgHdlr(5).fnPtr=CodePtr(fnWndProc_OnPaint)
  MsgHdlr(6).wMessage=%WM_DESTROY     : MsgHdlr(6).fnPtr=CodePtr(fnWndProc_OnDestroy)
End Sub       

     Why don't we test out the idea for such a program with a Console Compiler program?  If you only have  PBWin then do what we did earlier and open an Output.txt file, use Print #fp, and Shell() to it as previously done.  Compile and run Ptrs2.bas...

Code: [Select]
#Compile Exe         “Ptrs2”      ‘Ptrs2.bas  Shows how to use function
#Dim All                          ‘pointers to call event/message procedures.
%WM_CREATE           = &H1        'Note I left out Win32Api.inc
%WM_MOUSEMOVE        = &H200      'and just copied these equates
%WM_LBUTTONDOWN      = &H201      'I needed from that file.
%WM_SIZE             = &H5
%WM_CHAR             = &H102
%WM_PAINT            = &HF
%WM_DESTROY          = &H2

Type MessageHandler               'This Type Associates a message with the
  wMessage As Long                'address of the message handler which
  fnPtr    As Dword               'handles it.  Messages are usually fairly
End Type                          'low numbers, e.g., WM_PAINT is 15.

Global MsgHdlr() As MessageHandler

Type WndEventArgs
  hWnd     As Dword
  wParam   As Long
  lParam   As Long
End Type

Declare Function fnPtr(wea As WndEventArgs) As Long  'Prototype will provide compiler with function signature

Function fnWndProc_OnCreate(Wea As WndEventArgs) As Long         'WM_CREATE Message Handler
  Print "Just Called fnWndProc_OnCreate()!"
  Function=0
End Function

Function fnWndProc_OnMouseMove(Wea As WndEventArgs) As Long      'WM_MOUSEMOVE Message Handler
  Print "Just Called fnWndProc_OnMouseMove()!"
  Function=0
End Function

Function fnWndProc_OnSize(Wea As WndEventArgs) As Long           'WM_SIZE Message Handler
  Print "Just Called fnWndProc_OnSize()!"
  Function=0
End Function

Function fnWndProc_OnChar(Wea As WndEventArgs) As Long           'WM_CHAR Message Handler
  Print "Just Called fnWndProc_OnChar()!"
  Function=0
End Function

Function fnWndProc_OnLButtonDown(Wea As WndEventArgs) As Long    'WM_LBUTTONDOWN Message Handler
  Print "Just Called fnWndProc_OnLButtonDown()!"
  Function=0
End Function

Function fnWndProc_OnPaint(Wea As WndEventArgs) As Long          'WM_PAINT Message Handler
  Print "Just Called fnWndProc_OnPaint()!"
  Function=0
End Function

Function fnWndProc_OnDestroy(Wea As WndEventArgs) As Long        'WM_DESTROY Message Handler
  Print "Just Called fnWndProc_OnDestroy()!"
  Function=0
End Function

Sub AttachMessageHandlers() 
  ReDim MsgHdlr(6) As MessageHandler

  MsgHdlr(0).wMessage=%WM_CREATE      : MsgHdlr(0).fnPtr=CodePtr(fnWndProc_OnCreate)
  MsgHdlr(1).wMessage=%WM_MOUSEMOVE   : MsgHdlr(1).fnPtr=CodePtr(fnWndProc_OnMouseMove)
  MsgHdlr(2).wMessage=%WM_SIZE        : MsgHdlr(2).fnPtr=CodePtr(fnWndProc_OnSize)
  MsgHdlr(3).wMessage=%WM_CHAR        : MsgHdlr(3).fnPtr=CodePtr(fnWndProc_OnChar)
  MsgHdlr(4).wMessage=%WM_LBUTTONDOWN : MsgHdlr(4).fnPtr=CodePtr(fnWndProc_OnLButtonDown)
  MsgHdlr(5).wMessage=%WM_PAINT       : MsgHdlr(5).fnPtr=CodePtr(fnWndProc_OnPaint)
  MsgHdlr(6).wMessage=%WM_DESTROY     : MsgHdlr(6).fnPtr=CodePtr(fnWndProc_OnDestroy)
End Sub

Function PBMain() As Long
  Local wea As WndEventArgs
  Local iReturn As Long
  Register i As Long

  Call AttachMessageHandlers()        'Attach Message Handlers
  For i=0 To UBound(MsgHdlr,1)        'Loop Through All Message Handlers
    Print i,;
    Call Dword MsgHdlr(i).fnPtr Using fnPtr(wea) To iReturn
  Next i
  Waitkey$

  PBMain=0
End Function

Code: [Select]
'Output:
'
' 0            Just Called fnWndProc_OnCreate()!
' 1            Just Called fnWndProc_OnMouseMove()!
' 2            Just Called fnWndProc_OnSize()!
' 3            Just Called fnWndProc_OnChar()!
' 4            Just Called fnWndProc_OnLButtonDown()!
' 5            Just Called fnWndProc_OnPaint()!
' 6            Just Called fnWndProc_OnDestroy()!

     I hope you find this program and its output as interesting as I do.  It proves that we can use the function pointer mechanism to call event/message handlers in a Windows program.  In this particular 'dummy' program no real message handling was done, as it isn't even a real GUI Windows program.  All we wanted to do was prove that the concept was valid.  Lets now try fnPtrs3.bas which is a real Windows program, and in fact is the one Edwin made me do so as to eliminate globals or statics from a program, that is, Form2A.bas from Fred's Tutorial #2.  Remember that one?  This particular program brings together essentially everything we've covered so far in this rather huge and daunting Tutorial #6, i.e., pointers, dynamic memory allocation, instance data concepts, Get/SetWindowLong(), and function pointers.  Please compile and run it, and a slight discussion will follow afterwards, although there should be enough comments distributed alongside the code to give you a good idea how it works...

Code: [Select]
#Compile Exe   "fnPtrs3"
#Include       "Win32api.inc"

Type MessageHandler                  'This Type Associates a message with the
  wMessage As Long                   'address of the message handler which
  fnPtr    As Dword                  'handles it.  Messages are usually fairly
End Type                             'low numbers, e.g., WM_PAINT is 15.

Global MsgHdlr() As MessageHandler

Type WndEventArgs               'Type for passing window procedure parameters.  Allows me
  wParam           As Long      'to shorten parameter list.  .NET does this all the time.
  lParam           As Long      'See, for example pea for PaintEventArgs in the OnPaint()
  hWnd             As Dword     'Message Handler.
End Type

Type tagInstanceData   'User Defined Type for persisting or storing message
  wWidth   As Word     'information across invocations of fnWndProc.  It is
  wHeight  As Word     'necessary to persist this data because such items of
  wX       As Word     'information as, for example, the window height and
  wY       As Word     'width would be lost after they were obtained in a
  xPos     As Word     'WM_SIZE message after that message handler was exited.
  yPos     As Word     'Then, when a WM_PAINT message was received and it was
  wCharHt  As Word     'desired to update the window with this newly acquired
  szText   As Asciiz*128  'information, it would be gone.
End Type

'Model procedure showing function signature of all message handlers.
Declare Function fnPtr(wea As WndEventArgs) As Long

Function fnWndProc_OnCreate(wea As WndEventArgs) As Long
  Local ptrInstanceData As tagInstanceData Ptr
  Local tm As TEXTMETRIC      'When the main program window is created
  Local hHeap As Dword        'HeapAlloc() is called and a memory request is
  Local hDC As Dword          'made for enough bytes to store a tagInstanceData
                              'Type.  If HeapAlloc()returns a non-zero number
  hHeap=GetProcessHeap()      '(a valid address), a call to GetTextMetrics() is
  ptrInstanceData= _          'made to obtain the height of a character for text
  HeapAlloc _                 'spacing purposes.  Then a Call to SetWindowLong()
  ( _                         'stores the address of the memory acquired by
    hHeap, _                  'HeapAlloc() into the four cbWndExtra bytes of
    %HEAP_ZERO_MEMORY, _      'extra window storage in the Window Class
    SizeOf(tagInstanceData) _ 'structure requested when the "Form2B" Class was
  )                           'Registered in WinMain().
  If ptrInstanceData Then
     hDC=GetDC(wea.hWnd)
     Call GetTextMetrics(hDC,tm)
     Call ReleaseDC(wea.hWnd,hDC)
     @ptrInstanceData.wCharHt=tm.tmHeight
     Call SetWindowLong(wea.hWnd,0,ptrInstanceData)
     MsgBox("@ptrInstanceData.wCharHt=" & Trim$(Str$(@ptrInstanceData.wCharHt)))
  Else
     fnWndProc_OnCreate=-1
  End If

  fnWndProc_OnCreate=0
End Function

Function fnWndProc_OnMouseMove(wea As WndEventArgs) As Long  'If a WM_MOUSEMOVE
  Local ptrInstanceData As tagInstanceData Ptr   'message is received a pointer
                                                 'to a tagInstanceData Type is
  ptrInstanceData= _                             'declared.  This variable is
  GetWindowLong _                                'initialized with the address
  ( _                                            'stored in the cbWndExtra bytes.
    wea.hWnd, _                                  'The mouse coordinates are
    0 _                                          'extracted from the lParam
  )                                              'parameter and 'persisted'
  @ptrInstanceData.wX=LoWrd(wea.lParam)          'in the allocated memory using
  @ptrInstanceData.wY=HiWrd(wea.lParam)          'pointer indirection techniques.
  Call InvalidateRect _                          'Finally, an InvalidateRect()
  ( _                                            'Call is made.  This will force
    wea.hWnd, _                                  'a WM_PAINT message to be sent
    ByVal %NULL, _                               'to this Window Procedure in a
    %TRUE _                                      'completely separate WndProc()
  )                                              'Call or invocation.  At that
                                                 'point in the WM_PAINT handler
  fnWndProc_OnMouseMove=0                        'the mouse coordinates will be
End Function                                     'extracted and displayed.



Function fnWndProc_OnSize(wea As WndEventArgs) As Long  'The same pattern
  Local ptrInstanceData As tagInstanceData Ptr   'described just above for the
                                                 'WM_MOUSEMOVE message will be
  ptrInstanceData= _                             'repeated here in WM_SIZE logic
  GetWindowLong _                                'and, in all the other message
  ( _                                            'handlers.  The only thing that
    wea.hWnd, _                                  'will be different is the data
    0 _                                          'transferred in the wParam and
  )                                              'lParam variables, as that is
  @ptrInstanceData.wWidth=LoWrd(wea.lParam)      'dependent on the message
  @ptrInstanceData.wHeight=HiWrd(wea.lParam)     'itself.  The repeating pattern
  Call InvalidateRect(wea.hWnd,ByVal %NULL,%TRUE)'will be to obtain the pointer
                                                 'from the cbWndExtra bytes,
  fnWndProc_OnSize=0                             'store data there, then force
End Function                                     'a paint msg. to display data.

Function fnWndProc_OnChar(wea As WndEventArgs) As Long
  Local ptrInstanceData As tagInstanceData Ptr

  ptrInstanceData=GetWindowLong(wea.hWnd,0)
  @ptrInstanceData.szText=@ptrInstanceData.szText+Chr$(wea.wParam)
  Call InvalidateRect(wea.hWnd,ByVal %NULL,%TRUE)

  fnWndProc_OnChar=0
End Function

Function fnWndProc_OnLButtonDown(wea As WndEventArgs) As Long
  Local ptrInstanceData As tagInstanceData Ptr

  ptrInstanceData=GetWindowLong(wea.hWnd,0)
  If wea.wParam=%MK_LBUTTON Then
     @ptrInstanceData.xPos=LoWrd(wea.lParam)
     @ptrInstanceData.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 ptrInstanceData As tagInstanceData Ptr
  Local szLine As Asciiz*64
  Local ps As PAINTSTRUCT     'This procedure will be called right after
  Local hDC As Long           'window creation, and every time an
                              'InvalidateRect() call is made when the mouse is
  hDC=BeginPaint(wea.hWnd,ps) 'moved over the window, the window is being
  ptrInstanceData=GetWindowLong(wea.hWnd,0)       'resized, a char key is
  szLine= _                                       'pressed, or a left mouse down
  "MouseX="+Trim$(Str$(@ptrInstanceData.wX)) & _  'is received.  Before the
  "  MouseY="+Trim$(Str$(@ptrInstanceData.wY))    'InvalidateRect() call is made
  TextOut(hDC,0,0,szLine,Len(szLine))             'the data from the specific
  szLine= _                                       'handler that is to be
  "@ptrInstanceData.wWidth="+ _                   'persisted across WndProc()
  Trim$(Str$(@ptrInstanceData.wWidth)) & _        'invocations will be stored in
  " @ptrInstanceData.wHeight=" + _                'the memory pointed to by the
  Trim$(Str$(@ptrInstanceData.wHeight))           'cbWndExtra bytes pointer.
  TextOut _                                       'That pointer will then be
  ( _                                             'retrieved here in WM_PAINT,
    hDC, _                                        'and the data in its totality
    0, _                                          'displayed to the window.  It
    16, _                                         'is one awesome machine.
    szLine, _
    Len(szLine) _
  )
  TextOut(hDC,0,32,@ptrInstanceData.szText,Len(@ptrInstanceData.szText))
  If @ptrInstanceData.xPos<>0 And @ptrInstanceData.yPos<>0 Then
     szLine= _
     "WM_LBUTTONDOWN At (" & Trim$(Str$(@ptrInstanceData.xPos)) & _
     "," & Trim$(Str$(@ptrInstanceData.yPos)) & ")"
     Call TextOut _
     ( _
       hDC, _
       @ptrInstanceData.xPos, _
       @ptrInstanceData.yPos, _
       szLine, _
       Len(szLine) _
     )
     @ptrInstanceData.xPos=0 : @ptrInstanceData.yPos=0
  End If
  Call EndPaint(wea.hWnd,ps)

  fnWndProc_OnPaint=0
End Function

Function fnWndProc_OnDestroy(wea As WndEventArgs) As Long  'The most noteworthy
  Local ptrInstanceData As tagInstanceData Ptr   'event that must occur here is
  Local hHeap As Dword,blnFree As Dword          'that the memory allocated in
  Static iCtr As Long                            'WM_CREATE, a pointer to which
                                                 'is stored in the cbWndExtra
  hHeap=GetProcessHeap()                         'bytes, must be released or
  ptrInstanceData=GetWindowLong(wea.hWnd,0)      'freed.  HeapFree() is used for
  If ptrInstanceData Then                        'that purpose.  Note that since 
     blnFree=HeapFree(hHeap,0,ptrInstanceData)   'two windows of class fnPtrs3
     MsgBox _                                    'were created in WinMain(), and
     ( _                                         'two separate memory allocations
       "blnFree=" & _                            'were made for the two WM_CREATE
       Trim$(Str$(IsTrue(blnFree))) _            'messages, two deallocations
     )                                           'will need to be done - one for
     MsgBox _                                    'each window's data.  A static
     ( _                                         'counter variable was stuck in
       "Always Make Sure Memory Deallocates!  Memory Leaks Are Bad!" _
     )                                           'here so that when it gets
  End If                                         'incremented to %TRUE a
  If iCtr Then                                   'PostQuitMessage() will be
     Call PostQuitMessage(0)                     'called and a WM_QUIT msg put
  End If                                         'in the message quene so the
  Incr iCtr                                      'app can correctly terminate.

  fnWndProc_OnDestroy=0
End Function

Sub AttachMessageHandlers() 'Here is where the numeric value of a specific message that
  ReDim MsgHdlr(6) As MessageHandler  'is going to be handled in this program is associated
                                      'with the actual address of the message handler.
  MsgHdlr(0).wMessage=%WM_CREATE      : MsgHdlr(0).fnPtr=CodePtr(fnWndProc_OnCreate)
  MsgHdlr(1).wMessage=%WM_MOUSEMOVE   : MsgHdlr(1).fnPtr=CodePtr(fnWndProc_OnMouseMove)
  MsgHdlr(2).wMessage=%WM_SIZE        : MsgHdlr(2).fnPtr=CodePtr(fnWndProc_OnSize)
  MsgHdlr(3).wMessage=%WM_CHAR        : MsgHdlr(3).fnPtr=CodePtr(fnWndProc_OnChar)
  MsgHdlr(4).wMessage=%WM_LBUTTONDOWN : MsgHdlr(4).fnPtr=CodePtr(fnWndProc_OnLButtonDown)
  MsgHdlr(5).wMessage=%WM_PAINT       : MsgHdlr(5).fnPtr=CodePtr(fnWndProc_OnPaint)
  MsgHdlr(6).wMessage=%WM_DESTROY     : MsgHdlr(6).fnPtr=CodePtr(fnWndProc_OnDestroy)
End Sub

Function WndProc(ByVal hWnd As Long,ByVal wMsg As Long,ByVal wParam As Long,ByVal lParam As Long) As Long
  Local wea As WndEventArgs  'Every time a message is retrieved from the message
  Register iReturn As Long   'loop in WinMain() and ends up here, this For loop
  Register i As Long         'will cycle through all the messages it wants to
                             'handle to see if it can make a match.  If it makes
  For i=0 To 6               'a match the message handler will be called through
    If wMsg=MsgHdlr(i).wMessage Then     'a UDT array of function pointers.
       wea.hWnd=hWnd: wea.wParam=wParam: wea.lParam=lParam
       Call Dword MsgHdlr(i).fnPtr Using fnPtr(wea) To iReturn
       WndProc=iReturn
       Exit Function
    End If
  Next i

  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 hWnd1 As Dword, hWnd2 As Dword
  Local szClassName As Asciiz*16
  Local wc As WndClassEx
  Local Msg As tagMsg

  Call AttachMessageHandlers()  'Attach User Defined Type array of messages and function pointers.
  szClassName="fnPtrs3"         'Note that in the WndClassEx Type the cbWndExtra bytes were set to four.
  wc.cbSize=SizeOf(wc)                               : wc.style=0
  wc.lpfnWndProc=CodePtr(WndProc)                    : wc.cbClsExtra=0
  wc.cbWndExtra=4                                    : 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)
  hWnd1=CreateWindowEx(0,szClassName,"fnPtrs3",%WS_OVERLAPPEDWINDOW,200,100,425,360,%HWND_DESKTOP,0,hIns,ByVal 0)
  hWnd2=CreateWindowEx(0,szClassName,"fnPtrs3",%WS_OVERLAPPEDWINDOW,250,150,425,360,%HWND_DESKTOP,0,hIns,ByVal 0)
  Call ShowWindow(hWnd1,iShow)
  Call ShowWindow(hWnd2,iShow)
  While GetMessage(Msg,%NULL,0,0)
    Call TranslateMessage(Msg)
    Call DispatchMessage(Msg)
  Wend

  WinMain=msg.wParam
End Function

     When you ran this program you should have seen two top level windows be created and both should have been the same.  You can see just above in WinMain() the two CreateWindowEx() calls that created these two windows.  You should have found in experimenting with these two windows that their individual data output to their screens was completely independent of each other.  Upon each window's creation a call would have been made to fnWndProc_OnCreate(), and in that event handler memory would have been allocated to store a UDT variable of type tagInstanceData.  A pointer to this allocated memory would have been stored in the four cbWndExtra bytes of extra window storage provided by windows for instance data specific to each window.  When an input event occurs pertaining to either of these windows such as a mouse movement or char key press, the window procedure for the fnPtrs3 class will be called and the message parameters will identify which of the two windows the message applies to through the hWnd parameter, the wMsg parameter will identify the message, and the other parameters will contain information specific to the message.  Every message handling function will then as its very first action obtain the pointer to its window data using GetWindowLong().  The key it will need to get its specific window data is of course that all important hWnd.  Then the handler will interpret the data contained in the wParam and lParam parameters and store this information in the allocated memory.  Finally, an InvalidateRect() call will in every case force the window to repaint and display the updated window size, mouse position or whatever the input event caused to happen.  And of course, all this activity is being transmitted through our newly discovered function pointer mechanism that in actuality has formed the basis of all Windows Application Development technologies over the past twenty years such as Visual Basic, MFC (Microsoft Foundation Classes), and .NET. Something more or less similar to this is lurking underneath all these technologies.

     Before leaving this program it might be instructive for me to mention one issue I came upon when I decided to create two main windows of the fnPtrs3 class to illustrate for you the independence of the 'instance data' through the use of pointers and the cbWndExtra bytes.  Since there were two WM_CREATE messages - one for each window, there has to be two WM_DESTROY messages.  This is necessary because the allocated memory needs to be released for each window.  However, there is a PostQuitMessage() call in WM_DESTROY.  What this function does is put a WM_QUIT message in the program's message quene, and when this message is retrieved in the message loop in WinMain() the program will end.  Well, this will occur if you close the first of the two windows visible on the screen.  Not only will the first window close, but also the other one too.  This will cause a memory leak because my testing has shown that a second WM_DESTROY message won't be received and the second window's memory won't be released.  To counter this unfortunate event and allow for both deallocations to occur I stuck a static iCtr variable in fnWndProc_OnDestroy that gets incremented from %FALSE to %TRUE when you close either of the two windows, and this variable will maintain this state across invocations of the window procedure due to its static duration.  So when you close the first window its memory will be deallocated but the second window will remain visible and alive until you explicitly close it, which behavior is more in line with what a user might expect.  And when you finally close that second window the test of iCtr will cause the PostQuiteMessage() function to satisfactorily terminate the program.
« Last Edit: September 08, 2007, 06:12:53 PM by José Roca »

Offline Frederick J. Harris

  • Hero Member
  • *****
  • Posts: 914
  • User-Rate: +16/-0
    • Frederick J. Harris
 
Pointers To Access Memory

     The final topic in this huge tutorial #6 will be accessing and minipulating memory with pointers.  A very good context for an example can be found within the Open Database Connectivity - ODBC Application Programming Interface, i.e., ODBC Api.  When you install your Windows operating system database drivers are installed by default for a good many popular databases, e.g., Microsoft Access, Excel, SQL Server, Oracle, etc.  Also installed are several Dlls that provide functionality to access these drivers.  Therefore, it is possible with PowerBASIC or C to call ODBC functions to access various databases in a strictly procedural manner and without the necessity of involving any large bloated OOP or .NET technologies which, as an aside, sit on top of ODBC as an abstraction layer anyway.

     There is an ODBC function named SQLDrivers() that lists the database drivers and their attributes that are installed on your system.  The data returned by this function is in a somewhat complicated format, and indeed the function itself is somewhat complex.  But we're going to take a crack at it.  Much of the discussion and material presented back in Form5.bas concerning the Open File Dialog Box and its memory buffers will apply here.  For you see, the way this SQLDrivers() function works is to return successive streams of information in memory buffers that we must provide for the function in the same manner as we supplied buffers to the Open File Dialog Box in which it returned our selected file path information.  What is somewhat different in the SQLDrivers() case is that instead of one function call returning information to us, we need to repeatedly call SQLDrivers() until it signifies to us in a returned equate %SQL_NO_DATA that we've run through all the data or drivers available.  We do this within a loop and print out and format the data as we go. 

     What my strategy will be to explain this material to you is to start with the completed program so you see how the whole thing works and what the 'end game' is, and then redo it in pieces to see how the whole thing functions and is put together.  Please compile and run SQLDrivers.bas and we'll turn to examining it afterwards.  The below program uses a macro to determine whether you are running it in the console compiler or PBWin, so you can use either. 

Code: [Select]
#Compile Exe   "SQLDrivers"
#Dim All
#Include "Win32Api.inc"

%SQL_NULL_HANDLE         = 0&    'Note!  These eight equates and four functions
%SQL_HANDLE_ENV          = 1     'I abstracted from the PB ODBC includes at
%SQL_ATTR_ODBC_VERSION   = 200   'http://www.powerbasic.com/files/pub/pbwin/database/pbodbc30.zip
%SQL_OV_ODBC3            = 3???  'I didn't want to force you to download these
%SQL_IS_INTEGER          = (-6)  'files just to get this tutorial program to
%SQL_FETCH_NEXT          = 1     'work.  However, if you are interested in
%SQL_NO_DATA             = 100   'learning to use ODBC function calls I'd recom-
%SQL_ERROR               = -1    'mend you download the ODBC includes.


Declare Function SQLAllocHandle Lib "ODBC32.DLL" Alias "SQLAllocHandle" _
( _
  Byval HandleType          As Integer,_
  Byval InputHandle         As Dword,_
  Byref OutPutHandle        As Dword _
) As Integer


Declare Function SQLSetEnvAttr Lib "ODBC32.DLL" Alias "SQLSetEnvAttr" _
( _
  Byval EnvironmentHandle   As Dword,_
  Byval Attribute           As Long,_
  Byref Value               As Any,_
  Byval StringLength        As Long _
) As Integer


Declare Function SQLDrivers Lib "ODBC32.DLL" Alias "SQLDrivers" _
( _
  Byval henv                As Dword,_
  Byval fDirection          As Word,_
  Byref szDriverDesc        As Asciiz,_
  Byval cbDriverDescMax     As Integer,_
  Byref pcbDriverDesc       As Integer,_
  Byref szDriverAttributes  As Asciiz,_
  Byval cbDrvrAttrMax       As Integer,_
  Byref pcbDrvrAttr         As Integer _
) As Integer


Declare Function SQLFreeHandle Lib "ODBC32.DLL" Alias "SQLFreeHandle" _
( _
  Byval HandleType          As Integer,_
  Byval TheHandle           As Dword _
) As Integer


Macro Display(msg,fp)     'If program is running in Console Compiler, Output
  #If %Def(%Pb_Cc32)      'goes to StdOut of Console Window.
      StdOut msg
  #Else
      Print #fp, msg      'Otherwise, to a file which will be ShellExecut()'ed
  #EndIf                  'at program termination.
End Macro


Function PBMain() As Long
  Local iLen1 As Integer, iLen2 As Integer, fn As Integer
  Local szDriverDes As Asciiz*64, szDriverAttr As Asciiz*256
  Local szTextFile As Asciiz*256, szCurDir As Asciiz*256
  Local ptrByte As Byte Ptr
  Local strArr() As String
  Local hEnvr As Dword
  Register i As Long

  #If %Def(%Pb_Cc32)
      Console Set Screen 400, 80
  #Else
      szTextFile=CurDir$ & "\" & "Output.txt"
      fn=FreeFile
      Open szTextFile For Output As #fn
  #EndIf
  If SQLAllocHandle(%SQL_HANDLE_ENV,%SQL_NULL_HANDLE,hEnvr)<>%SQL_ERROR Then
     Call SQLSetEnvAttr(hEnvr,%SQL_ATTR_ODBC_VERSION,ByVal %SQL_OV_ODBC3,%SQL_IS_INTEGER)
     While SQLDrivers(hEnvr,%SQL_FETCH_NEXT,szDriverDes,64,iLen1,szDriverAttr,256,iLen2)<>%SQL_NO_DATA
       Display(szDriverDes,fn)
       Display(Chr$(13),fn)
       ptrByte=VarPtr(szDriverAttr)
       Decr iLen2
       For i=0 To iLen2
         If @ptrByte[i]=0 Then
            @ptrByte[i]=44
         End If
       Next i
       @ptrByte[iLen2]=0
       ReDim strArr(ParseCount(szDriverAttr)-1)
       Parse szDriverAttr,strArr()
       For i=0 To UBound(strArr,1)
         Display(strArr(i),fn)
       Next i
       Display(Chr$(13,10) & Chr$(13),fn)
     Loop
     Call SQLFreeHandle(%SQL_HANDLE_ENV,hEnvr)
  Else
     Display("ODBC Function Call Failure.  Couldn't Allocate Environment Handle!",fn)
  End If
  #If %Def(%Pb_Cc32)
      WaitKey$
  #Else
      Close #fn
      szCurDir=CurDir$
      Call ShellExecute(0,"Open",szTextFile,ByVal 0,szCurDir,%SW_SHOWDEFAULT)
  #EndIf

  PBMain=0
End Function

     Before I start discussing this program I would like to ask you to copy those eight equates and four ODBC Function Declarations into a separate include file named "ODBCIncs.inc".  Create the file either in your programming editor or in Notepad, and make sure you save t in text file format.  Then in all the example programs below it will appear under the #Include "Win32Api.inc" file.  That will save space here.

     If you ran the example you will have noted that for each driver there is a verbose description, such as, 'SQL Server' or 'Microsoft Access Driver (*.mdb)', followed by a blank line and half a dozen or so attribute/value pairs.  On my system the first driver for which information is provided is the SQL Server driver like so...

SQL Server

UsageCount=5
SQLLevel=1
FileUsage=0
DriverODBCVer=02.50
ConnectFunctions=YYY
APILevel=2
CPTimeout=60

     And this format of data is repeated for all drivers on the system on which the program is running.  Now the thing is, this format of the data was created by the program, as it makes sense to view it in that manner.  SQLDrivers() does not present it in that format.  The format SQLDrivers() presents the data in is one where a string buffer provided by the program for the driver description is filled with that description, and another buffer provided for the Attribute/value pairs are filled with that information - all in one Asciiz character string buffer with unchanging address and each attribute/value pair separated by a null byte.  In addition, the ODBC functions always return in a separate buffer the numbers of bytes returned in adjacent character string buffers.  Our work in this example will be to use all the information provided by SQLDrivers() so as to format it into the readily readable format that you see when you run the program.

     So lets begin.  The first thing you always have to do when using ODBC functions is set up an ODBC Environment - sometimes called a 'workspace' in other contexts, and set the ODBC version.  A handle to an ODBC Environment is first obtained by calling the following function:

     SQLAllocHandle(%SQL_HANDLE_ENV, %SQL_NULL_HANDLE, hEnvr)

     ODBC functions never return database data through return values, but rather through function parameters.  Note that before this call in PBMain() hEnvr was dimensioned as a local DWORD variable but was not initialized before its use in this function, other than its default initialization to zero by the compiler.  The reason for this is that after the function call, if the function call succeeds, hEnvr will contain a valid ODBC handle to an environment.  In terms of actual return values, ODBC functions return success/failure codes, as can be observed in PBMain() above where you can see I tested for %SQL_SUCCESS.  %SQL_SUCCESS is defined in the equates above as zero, and can be thought of a zero errors.

     Armed with a valid hEnvr we can set the ODBC version through...

     Call SQLSetEnvAttr(hEnvr, %SQL_ATTR_ODBC_VERSION, ByVal %SQL_OV_ODBC3, %SQL_IS_INTEGER)

     After that we're ready to get down to business.  At this point I'd recommend you obtain Microsoft's actual documentation for SQLDrivers() as we are ready to call it.  There are over a thousand pages of documentation on the ODBC API but the documentation on SQLDrivers is only a few pages or so and is located at...

     http://msdn2.microsoft.com/en-gb/library/ms712400.aspx

     Referring either to that Microsoft documentation, or to the PowerBASIC declare above in ODBCIncs.inc, you can see the first parameter is the handle to the environment we obtained above.  The second parameter is an equate equaling either 1 or 2 depending on which way you want to move through the database driver information.  The %SQL_FETCH_NEXT is most useful and we'll use it here.  After that are six more complicated looking parameters but in actuality the six refer to only two pieces of real data, and they are the character string driver description name (szDriverDes) such as SQL Server, and the driver attribute/value pairs (szDriverAttributes) as seen and described above.

     To set up the function call the programmer has to provide memory owned by the program into which the function can place the driver and attribute data.  Programmers often refer to such memory as buffers.  Note in PBMain() the variable declarations...

     Local szDriverDes As Asciiz*64, szDriverAttr As Asciiz*256

     Those variables are the buffers into which SQLDrivers() will repeatedly place driver description and attribute/value information until the list of database information is exhausted.  I can't overemphasize enough that these are output parameters meaning that information is not fed to the function through them but rather extracted from it by the programmer and his/her app.  If you examine the Microsoft documentation you will see in the extensive write up on the function that they clearly mark each parameter as either an input parameter, an output parameter, or an input/output parameter (yes, you can have those too!  But in SQLDrivers() they are either/or).

     Another point that just occurred to me is that if you are coming from a programming background limited to the Basic family of languages, the whole idea of a string data type such as Asciiz and the idea of creating a 'buffer owned by the application' might seem peculiar to you.  What is going on here is actually fairly profound, and consists of the fact that in the entire world of computerdom, there are two entirely different and opposing ways of representing strings (I fear this is going to be a 'digression!).

     Let me give an example.  Suppose in a PowerBASIC program you wrote you had a procedure where you needed an integer long and a string like so:

     Local iNumber As Long
     Local strCompanyName As String

     and then you assigned data to the variables like this...

     iNumber = 17
     strCompanyName = "Acme Warehousing Company, Inc"

     The steps the compiler must take to compile the statement with the integer is considerably different than the steps it must take with the string.  In the case of the long the compiler must reserve a four byte piece of memory to store the '17', then move the binary representation of '17', i.e. 00010001 to that location.  In the case of the string the compiler must generate code to first measure the string, then do an Api call to perform a memory allocation for the length of the string, after that create a string descriptor that holds information on the string such as its length, and finally actually move the bytes of the string's characters to the buffer it allocated to hold the string.  And this all occurs transparently to you the programmer.

     When you enter the world of the Windows Api or the ODBC Api you are entering a world where string handling doesn't work that way.  In fact, if you even search for the word 'string' in the documentation for any of the Api functions that involve what we think of as string data you will be hard pressed to find it (In SQLDrivers() I found the word string exactly one time).  You will constantly see references instead to 'buffers' and 'pointer to a buffer'.  The reason for this is also fairly profound and far reaching.  In the assembler and C code in which Windows was written, in the assembler and C code in which any operating system was written, there is no string data type.  When C++ eventually came on the scene after both Unix and Windows were already written a C++ String data type was created using C++ classes along the lines of Basic implementations.  In later additions to the Windows API involving COM and OLE you will find other kinds of 'strings' similar to Basic strings, but not in the original libraries dating from the early days of Windows.

     Digression over and back to SQLDrivers().  Below is a simplified version of the program in which there is only one call to the SQLDrivers() function, as I eliminated its placement within the while loop construct.  What we'll do is simply call the function one time after setting up a few necessary parameters, and then we'll examine in detail what data was placed into the output parameters by the single function call.  So copy the following code to a new editor window, but make sure both includes are accessible to the program from wherever on your computer it is running.

Code: [Select]
SQLDrivers.bas
#Compile Exe
#Register None
#Dim All
#Include "Win32Api.inc"   'Main Windows Include File.
#Include "ODBCIncs.inc"   'Highly Limited Abstract From SQLTypes.inc, SQL32.Inc, and SQLExt32.inc

Macro Display(msg,fp)     'If program is running in Console Compiler, Output goes to StdOut of
  #If %Def(%Pb_Cc32)      'Console Window.
      StdOut msg
  #Else
      Print #fp, msg  'Otherwise, to a file which will be ShellExecut()'ed at program termination.
  #EndIf
End Macro

Function PBMain() As Long
  Local iLen1 As Integer, iLen2 As Integer, fn As Integer
  Local szDriverDes As Asciiz*64, szDriverAttr As Asciiz*256
  Local szTextFile As Asciiz*256, szCurDir As Asciiz*256
  Local ptrByte As Byte Ptr
  Local strArr() As String
  Local hEnvr As Dword
  Register i As Long

  #If %Def(%Pb_Cc32)
      Console Set Screen 400, 80
  #Else
      szTextFile=CurDir$ & "\" & "Output.txt"
      fn=FreeFile
      Open szTextFile For Output As #fn
  #EndIf
  If SQLAllocHandle(%SQL_HANDLE_ENV,%SQL_NULL_HANDLE,hEnvr)<>%SQL_ERROR Then
     Call SQLSetEnvAttr(hEnvr,%SQL_ATTR_ODBC_VERSION,Byval %SQL_OV_ODBC3,%SQL_IS_INTEGER)
     Call SQLDrivers(hEnvr,%SQL_FETCH_NEXT,szDriverDes,64,iLen1,szDriverAttr,256,iLen2)
     Display("szDriverDes         = " & szDriverDes,fn)
     Display("Len(szDriverDes)    = " & Trim$(Str$(Len(szDriverDes))),fn)
     Display("iLen1               = " & Trim$(Str$(iLen1)),fn)
     Display(Chr$(13),fn)
     Display("szDriverAttr        = " & szDriverAttr,fn)
     Display("Len(szDriverAttr)   = " & Trim$(Str$(Len(szDriverAttr))),fn)
     Display("iLen2               = " & Trim$(Str$(iLen2)),fn)
     Call SQLFreeHandle(%SQL_HANDLE_ENV,hEnvr)
  Else
     Display("ODBC Function Call Failure.  Couldn't Allocate Environment Handle!",fn)
  End If
  #If %Def(%Pb_Cc32)
      WaitKey$
  #Else
      Close #fn
      szCurDir=CurDir$
      Call ShellExecute(0,"Open",szTextFile,ByVal 0,szCurDir,%SW_SHOWDEFAULT)
  #EndIf

  PBMain=0
End Function

Code: [Select]
Below Is Output From Above Program:
=====================================
szDriverDes         = SQL Server
Len(szDriverDes)    = 10
iLen1               = 10

szDriverAttr        = UsageCount=5
Len(szDriverAttr)   = 12
iLen2               = 101

     I have already explained what the first two parameters to SQLDrivers() are.  The third parameter, szDriverDes, is that Asciiz fixed length string I started to describe when I launched myself into the digression about buffers.  You should be able to figure out now what that third parameter is, and how it was used.  SQLDrivers() put its first driver description into it.  On your machine it may not be SQL Server, but it will be a text description of some sort of driver.  The fourth parameter is 64 and I just pulled that number out of the air, as my derivation of this function followed the steps I am outlining for you here, and I found that none of the text strings were very long.  That was an input parameter, by the way.  The fifth parameter is an output parameter and when SQLDrivers() was called it returned to us the length of the text string output in the szDriverDes buffer.  As you can see (and count), 'SQL Server' contains 10 characters and SQLDrivers() informs us that it is returning 10 characters to us.  So far so good.

     Now, however, we get into trouble.  The pattern and meaning of the next three parameters is exactly the same as for szDriverDes, and indeed we do get a string with an attribute/value pair, but the numbers definitely don't seem to match as they did for szDriverDes.  The full set of information for the SQL Server driver from the first program was like so:

UsageCount=5
SQLLevel=1
FileUsage=0
DriverODBCVer=02.50
ConnectFunctions=YYY
APILevel=2
CPTimeout=60

     It seems we only got the first line.  The 'UsageCount=5' certainly equals 12 characters, but according to the iLen2 output parameter of SQLDrivers(), 101 characters of information were returned to us.  If you read the 'Comments' in Microsoft's SQLDrivers() documentation you'll find the
following:

          Each [attribute/value] pair is terminated with a null byte, and the entire list
          is terminated with a null byte (that is, two null bytes mark the end of the list).
          For example, a file-based driver using C syntax might return the following list of
          attributes:

          (“\0” represents a null character):

          FileUsage=1\0FileExtns=*.dbf\0\0

     I'm hoping that at this point you can figure out what is going on here.  Assuming someone out there hasn't figured it out, those null bytes after the end of each attribute/value pair is fooling Basic into thinking it hit the end of the string after hitting a null byte after 'UsageCount=5'.  It isn't seeing those additional 89 bytes after that null.  We own those bytes.  They are safely in 'our' buffer.  But how do we get to them?

     About the fastest and easiest way to fix it is to set a pointer equal to the address of the first byte of the string, then loop through the string one byte at a time for 101 bytes (the count returned to us in iLen2, remember) testing all the way for null bytes.  When a null byte is found, just stick some other less troublesome character in its place.  In terms of just what particular character to stick there, just remember that all versions of Basic love commas as separators.  There's just nothing Basic loves more than seeing commas separating bits of data.  Now C, that language has a fondness for white space, i.e., tabs, spaces, anything like that.  But for Basic, commas do it every time.  So here is the next version of the program.  Try this...

Code: [Select]
#Compile Exe
#Register None
#Dim All
#Include "Win32Api.inc"   'Main Windows Include File.
#Include "ODBCIncs.inc"   'Highly Limited Abstract From SQLTypes.inc, SQL32.Inc, and SQLExt32.inc

Macro Display(msg,fp)     'If program is running in Console Compiler, Output goes to StdOut of
  #If %Def(%Pb_Cc32)      'Console Window.
      StdOut msg
  #Else
      Print #fp, msg  'Otherwise, to a file which will be ShellExecut()'ed at program termination.
  #EndIf
End Macro

Function PBMain() As Long
  Local iLen1 As Integer, iLen2 As Integer, fn As Integer
  Local szDriverDes As Asciiz*64, szDriverAttr As Asciiz*256
  Local szTextFile As Asciiz*256, szCurDir As Asciiz*256
  Local ptrByte As Byte Ptr
  Local strArr() As String
  Local hEnvr As Dword
  Register i As Long

  #If %Def(%Pb_Cc32)
      Console Set Screen 400, 150
  #Else
      szTextFile=CurDir$ & "\" & "Output.txt"
      fn=FreeFile
      Open szTextFile For Output As #fn
  #EndIf
  If SQLAllocHandle(%SQL_HANDLE_ENV,%SQL_NULL_HANDLE,hEnvr)<>%SQL_ERROR Then
     Call SQLSetEnvAttr(hEnvr,%SQL_ATTR_ODBC_VERSION,Byval %SQL_OV_ODBC3,%SQL_IS_INTEGER)
     Call SQLDrivers(hEnvr,%SQL_FETCH_NEXT,szDriverDes,64,iLen1,szDriverAttr,256,iLen2)
     ptrByte=Varptr(szDriverAttr)  'The VarPtr function returns the address of a variable.
     Decr iLen2                    'So ptrByte holds the address of the 256 byte long Asciiz
     For i=0 To iLen2              'string buffer.  The iLen2 parameter holds the one based
       If @ptrByte[i]=0 Then       'count of characters returned.  I decremented it because
          @ptrByte[i]=44           'in using a base pointer you are working with zero based
       End If                      'offsets from a starting point.  Pointer subscript notation
     Next i                        'is used to move through buffer substituting ','s for '0's.
     Display("szDriverAttr        = " & szDriverAttr,fn)
     Display("Len(szDriverAttr)   = " & Trim$(Str$(Len(szDriverAttr))),fn)
     Display("iLen2               = " & Trim$(Str$(iLen2)),fn)
     Call SQLFreeHandle(%SQL_HANDLE_ENV,hEnvr)
  Else
     Display("ODBC Function Call Failure.  Couldn't Allocate Environment Handle!",fn)
  End If
  #If %Def(%Pb_Cc32)
      WaitKey$
  #Else
      Close #fn
      szCurDir=CurDir$
      Call ShellExecute(0,"Open",szTextFile,ByVal 0,szCurDir,%SW_SHOWDEFAULT)
  #EndIf

  PBMain=0
End Function

Code: [Select]
Below Is Output From Above Program:
=====================================
szDriverAttr      = UsageCount=5,SQLLevel=1,FileUsage=0,DriverODBCVer=02.50,ConnectFunctions=YYY,APILevel=2,CPTimeout=60,
Len(szDriverAttr) = 101
iLen2             = 100

     Now doesn't that look like we're getting somewhere?  It is close, but not exactly right.  Referring back to Microsoft's SQLDrivers() documentation they state that a double null byte sequence marks the end of the whole attribute/value sequence.  One of those bytes is counted in the iLen2 output parameter and one isn't.  What we need to do to get rid of that lone comma at the end and stick a null byte where the comma is at in the string which will have the effect of removing the comma and shortening the string by one byte.  So in the next program there is an addition of only one extra line...

     @ptrByte[iLen2]=0

     So run this program...

Code: [Select]
#Compile Exe
#Register None
#Dim All
#Include "Win32Api.inc"   'Main Windows Include File.
#Include "ODBCIncs.inc"   'Highly Limited Abstract From SQLTypes.inc, SQL32.Inc, and SQLExt32.inc

Macro Display(msg,fp)     'If program is running in Console Compiler, Output goes to StdOut of
  #If %Def(%Pb_Cc32)      'Console Window.
      StdOut msg
  #Else
      Print #fp, msg 'Otherwise, to a file which will be ShellExecut()'ed at program termination.
  #EndIf
End Macro

Function PBMain() As Long
  Local iLen1 As Integer, iLen2 As Integer, fn As Integer
  Local szDriverDes As Asciiz*64, szDriverAttr As Asciiz*256
  Local szTextFile As Asciiz*256, szCurDir As Asciiz*256
  Local ptrByte As Byte Ptr
  Local strArr() As String
  Local hEnvr As Dword
  Register i As Long

  #If %Def(%Pb_Cc32)
      Console Set Screen 400, 150
  #Else
      szTextFile=CurDir$ & "\" & "Output.txt"
      fn=FreeFile
      Open szTextFile For Output As #fn
  #EndIf
  If SQLAllocHandle(%SQL_HANDLE_ENV,%SQL_NULL_HANDLE,hEnvr)<>%SQL_ERROR Then
     Call SQLSetEnvAttr(hEnvr,%SQL_ATTR_ODBC_VERSION,Byval %SQL_OV_ODBC3,%SQL_IS_INTEGER)
     Call SQLDrivers(hEnvr,%SQL_FETCH_NEXT,szDriverDes,64,iLen1,szDriverAttr,256,iLen2)
     ptrByte=Varptr(szDriverAttr)  'The VarPtr function returns the address of a variable.
     Decr iLen2                    'So ptrByte holds the address of the 256 byte long Asciiz
     For i=0 To iLen2              'string buffer.  The iLen2 parameter holds the one based
       If @ptrByte[i]=0 Then       'count of characters returned.  I decremented it because
          @ptrByte[i]=44           'in using a base pointer you are working with zero based
       End If                      'offsets from a starting point.  Pointer subscript notation
     Next i                        'is used to move through buffer substituting ','s for '0's.
     @ptrByte[iLen2]=0             'Finally, insert null byte at trailing comma.
     Display("szDriverAttr        = " & szDriverAttr,fn)
     Display("Len(szDriverAttr)   = " & Trim$(Str$(Len(szDriverAttr))),fn)
     Display("iLen2               = " & Trim$(Str$(iLen2)),fn)
     Call SQLFreeHandle(%SQL_HANDLE_ENV,hEnvr)
  Else
     Display("ODBC Function Call Failure.  Couldn't Allocate Environment Handle!",fn)
  End If
  #If %Def(%Pb_Cc32)
      WaitKey$
  #Else
      Close #fn
      szCurDir=CurDir$
      Call ShellExecute(0,"Open",szTextFile,ByVal 0,szCurDir,%SW_SHOWDEFAULT)
  #EndIf

  PBMain=0
End Function

Code: [Select]
Below Is Output From Above Program:
=====================================
szDriverAttr        = UsageCount=5,SQLLevel=1,FileUsage=0,DriverODBCVer=02.50,ConnectFunctions=YYY,APILevel=2,CPTimeout=60
Len(szDriverAttr)   = 100
iLen2               = 100

     Now we've reached the point where we have all the information for one database driver.  We have its name and the attribute/value pairs that describe it.  All we need to do now is format the data into a readily readable list.  The easiest way to do that is to use PowerBASIC's great ParseCount and Parse combination. If you've never used these you are in for a treat as they are wonderful and easy to use.  ParseCount returns the number of delimited fields in a string.  If the delimiter is the comma all you have to do is call the function like so:

     iNumberFields=ParseCount(szDriverAttr)

     For the example above iNumberFields would be equal to seven.  The Parse statement can then be used to load a dynamically dimensioned array with the individual attribute/value pairs.  So the sequence of operations is to dimension an array with ParseCount-1 elements (Parse starts filling the array at element zero, so we're back to reconciling zero based and one based counts), then execute the Parse statement passing it first the string to be parsed and second the correctly dimensioned string array into which it will place the parsed strings.  So now try this next iteration of the program...

Code: [Select]
#Compile Exe
#Register None
#Dim All
#Include "Win32Api.inc"   'Main Windows Include File.
#Include "ODBCIncs.inc"   'Highly Limited Abstract From SQLTypes.inc, SQL32.Inc, and SQLExt32.inc

Macro Display(msg,fp)     'If program is running in Console Compiler, Output goes to StdOut of
  #If %Def(%Pb_Cc32)      'Console Window.
      StdOut msg
  #Else
      Print #fp, msg      'Otherwise, to a file which will be ShellExecut()'ed at program termination.
  #EndIf
End Macro

Function PBMain() As Long
  Local iLen1 As Integer, iLen2 As Integer, fn As Integer
  Local szDriverDes As Asciiz*64, szDriverAttr As Asciiz*256
  Local szTextFile As Asciiz*256, szCurDir As Asciiz*256
  Local ptrByte As Byte Ptr
  Local strArr() As String
  Local hEnvr As Dword
  Register i As Long

  #If %Def(%Pb_Cc32)
      Console Set Screen 400, 150
  #Else
      szTextFile=CurDir$ & "\" & "Output.txt"
      fn=FreeFile
      Open szTextFile For Output As #fn
  #EndIf
  If SQLAllocHandle(%SQL_HANDLE_ENV,%SQL_NULL_HANDLE,hEnvr)<>%SQL_ERROR Then
     Call SQLSetEnvAttr(hEnvr,%SQL_ATTR_ODBC_VERSION,Byval %SQL_OV_ODBC3,%SQL_IS_INTEGER)
     Call SQLDrivers(hEnvr,%SQL_FETCH_NEXT,szDriverDes,64,iLen1,szDriverAttr,256,iLen2)
     Display(szDriverDes,fn)       
     Display(Chr$(13),fn)
     ptrByte=Varptr(szDriverAttr)  'The VarPtr function returns the address of a variable.
     Decr iLen2                    'So ptrByte holds the address of the 256 byte long Asciiz
     For i=0 To iLen2              'string buffer.  The iLen2 parameter holds the one based
       If @ptrByte[i]=0 Then       'count of characters returned.  I decremented it because
          @ptrByte[i]=44           'in using a base pointer you are working with zero based
       End If                      'offsets from a starting point.  Pointer subscript notation
     Next i                        'is used to move through buffer substituting ','s for '0's.
     @ptrByte[iLen2]=0             'Finally, insert null byte at trailing comma.
     Redim strArr(ParseCount(szDriverAttr)-1)
     Parse szDriverAttr,strArr()
     For i=0 To UBound(strArr,1)
       Display(strArr(i),fn)
     Next i
     Display(Chr$(13,10) & Chr$(13),fn)
     Call SQLFreeHandle(%SQL_HANDLE_ENV,hEnvr)
  Else
     Display("ODBC Function Call Failure.  Couldn't Allocate Environment Handle!",fn)
  End If
  #If %Def(%Pb_Cc32)
      WaitKey$
  #Else
      Close #fn
      szCurDir=CurDir$
      Call ShellExecute(0,"Open",szTextFile,ByVal 0,szCurDir,%SW_SHOWDEFAULT)
  #EndIf

  PBMain=0
End Function

     At this point I don't believe I need to provide any further examples as we've come full circle.  The next version of the program is the first program at the top of this post where SQLDrivers() is called repeatedly inside a while loop with the exit condition being when there is no more data to retrieve marked by SQLDrivers() returning %SQL_NO_DATA.

     This program shows a particularly involved Api function, and if you were able to follow along and understand it, you will easily be able to master the Api functions required to create a graphical interface in Windows.  Probably the hardest issues for programmers whose programming background is limited to Basic are the issues relating to character string buffers, pointers to character string buffers, and null terminated Asciiz strings.  Some insight into these issues can be obtained by carefully reading the PowerBASIC documentation on all the string data types and pointers.
« Last Edit: September 08, 2007, 06:14:06 PM by José Roca »