Author Topic: 15a. Saving Passed Parameters  (Read 989 times)

0 Members and 1 Guest are viewing this topic.

Offline LarryMc

  • Administrator
  • Hero Member
  • *****
  • Posts: 5965
  • 'All I like is finishing'
15a. Saving Passed Parameters
« on: August 28, 2011, 11:55:38 AM »
In the previous two sections we generated code to create an instance of our custom control.  Then we created functions we could use to configure a specific instance of our control by sending messages.  Really pretty straight forward once you understand what is going on.

Now, let's consider this. We can have an application that has hundreds of instances of our custom control. And we previously discussed registering the class for our control.  And that a class defines one, and only one, message handler for all controls of that class (type).

Therefore, one message handler routine is responsible for drawing hundreds of instances of our control with each one being different from the next.

At this time two things should be obvious. We have to save the configuration values for each instance of the control and the message handler has to be able to access the proper set of values when they are needed.

The first thing a lot of people would think of is a big array somewhere.  But that brings up the issue of dynamically resizing arrays since we have no idea how many instances of our control the user will create. And if we create a big static array then we have created another set of problems; the biggest of which is that we have placed an arbitrary limit on the number of our controls that can be used in a single application.    The natural response to that is to make the array extremely large.  That's fine but what if the user wants to use only one instance of the control.  That's a lot of wasted memory.

Those that are a little more advance programmers might suggest using a linked list to store the values for each control.  That would remove any limiting of the number of controls in an application.  A potential problem there is timing.  Linked list are sequential access.  The more controls you use the more time it takes to find the data for a specific instance.

Fortunately the OS has given us two simple methods to resolve this issue. The first method is what IWBasic uses internally when creating IWBasic windows and controls.  It centers around what the OS calls window's properties.  The OS allows the assigning of a property value to a specific instance of a window/control via the OS's SetPropA API function.  The following are examples of the two most commonly used:

Code: [Select]
SetPropA(hwnd,"FGRND",GetSysColor(fg))
SetPropA(hwnd,"BGRND",GetSysColor(bg))

Three parameters are passed to the function:
1) the handle to the specific instance of the control;
2) a user defined name to identify the specific property;
3) a UINT type variable that is either the specific value itself (as shown above) or a pointer to the data.

To retrieve the property later the OS's GetPropA API function is used. Retrieving the above values could look something like this:

Code: [Select]
UINT myfg = GetPropA(hwnd,"FGRND")
UINT mybg = GetPropA(hwnd,"BGRND")

If we had opted to use this technique to save our configuration values there is one other OS API that we would have to use; RemovePropA.

Code: [Select]
RemovePropA(hwnd,"BGRND")
RemovePropA(hwnd,"FGRND")

The OS system requires that when you use memory that you give it back when you are through with it.  This technique would require that we add the RemovePropA commands in the

Code: [Select]
CASE @IDDESTROY
section of our control's message handler.  If you check Section 3 of CCT_lib.iwb you will see that the @IDDESTROY message case is already there. Although we aren't going to use the method just described to store our parameters we will still need to use the @IDDESTROY message.

With the method we are going to use we won't be reading and writing the various parameters on a strictly independent basis. Instead, we are going to store all our parameters in  a UDT type variable. So the first thing we need to do is construct our UDT.

As with any IWBasic UDT we have to give it a name.  Ours will be GAGEPARAMS.  So we start with:

Code: [Select]
TYPE GAGEPARAMS

ENDTYPE

Now we will add comments identifying each of the messages we use to send the parameters. To make our list we will simply look at our message handler and copy the various messages we have already set up to handle. Our UDT now looks like this:

Code: [Select]
TYPE GAGEPARAMS
/*GAGE_STYLE*/
/*GAGE_TITLE*/
/*GAGE_UNITS*/
/*GAGE_RAWRNG*/
/*GAGE_DIALRNG*/
/*GAGE_MULTI*/
/*GAGE_SETPANEL_COLOR*/
/*GAGE_DIAL_DARK*/
/*GAGE_CLKOFFSET*/
/*GAGE_SETPOS1*/
/*GAGE_SETPOS2*/
ENDTYPE

Now we will give each element that we need a definition statement.

Code: [Select]
TYPE GAGEPARAMS
  INT       sStyle               /*GAGE_STYLE*/
  STRING sName              /*GAGE_TITLE*/
  STRING sMultiUnits        /*GAGE_UNITS*/
  INT       nRangeMinAct    /*GAGE_RAWRNG*/
  INT       nRangeMaxAct   /*     "     */
  INT       nRangeMinDial   /*GAGE_DIALRNG*/
  INT       nRangeMaxDial  /*     "      */
  INT       nDialMulti          /*GAGE_MULTI*/
  UINT     nPanelColor       /*GAGE_SETPANEL_COLOR*/
  UINT     nGageColor       /*GAGE_DIAL_DARK*/
  INT       clockoffset        /*GAGE_CLKOFFSET*/
  INT       nPos1               /*GAGE_SETPOS1*/
  INT       nPos2               /*GAGE_SETPOS2*/
ENDTYPE

We can go ahead and place this UDT definition in Section 2 of the CCT_lib.iwb file.  We don't need to place it in the CCT_test.iwb file since it is for internal use only by our control.

In order to benefit from our UDT we have to define a variable of that type. A proper declaration would be:

Code: [Select]
GAGEPARAMS g_dat
Since the g_dat variable is going to be used in our control's message handler, our normal inclination is to place the declaration where we normally place declarations in subroutines which would look like this:

Code: [Select]
SUB Gage1Proc_LM(hWnd:UINT, uMsg:UINT, wParam:UINT, lParam:ANYTYPE),UINT
   GAGEPARAMS g_dat
.....
ENDSUB

Let's think about what we just did.  We created a UDT type variable in a subroutine.  We know that each time the subroutine is called the variable will be initialized. So that doesn't appear to be the way to save our passed parameters (since we typically only send them once in the beginning).

Those that are aware of the new features of IWBasic 2.x might suggest we use the STATIC keyword like this:

Code: [Select]
SUB Gage1Proc_LM(hWnd:UINT, uMsg:UINT, wParam:UINT, lParam:ANYTYPE),UINT
   STATIC GAGEPARAMS g_dat
.....
ENDSUB

We know with this code the g_dat variable will be initialized on the first call to the subroutine.  Thereafter the variable will contain the values stored during the last call of the sub.  Sounding good so far.  It preserves our values.

Not so fast.  This routine could be used for hundreds of our controls in an application.  When we go through and configured all of them, which ones' configuration will be stored the next call that the OS makes to repaint the control.  Since there is only one variable it will contain the configuration of the last control we configured.  So, in essence, all our controls will look the same as each one is repainted because they will all be drawn using the same data.

What we need is a way to create a variable that contains the configuration for each instance of our control. We need a way to insure that the correct data is used with the proper control when the OS needs to redraw it. And, finally, we need a way to do it without using arrays or linked list like we discussed earlier.

In the Review section we talked about an IWBasic window having a UDT associated with it and that the first element was the OS handle to the created window.  It appears that that concept of having a UDT with associated data is not unique to IWBasic.  When the OS creates a window (remember that a window and a control are one and the same) it creates some reserved memory locations in which to store data that the OS needs.  These locations are read with the GetWindowLong API function and written to with the SetWindowLong API function.

If you have been using IWBasic (or its predecessor ) very long you will probably recognize that those commands are used to point to the handler routines when sub-classing a control.  

Both commands pass two common parameters: the OS handle to the appropriate window/control and an OS constant that identifies a specific reserved memory location.  The memory location we are interested in is identified as  GWL_USERDATA.  The OS sets aside this memory location for each corresponding window/control to allow the user to tie one piece of information to that specific object.  In our case we're going to store a pointer to a variable.

We'll start by changing our g_dat declaration. While we are at it we will also add the required SetWindowLong command. The function excerpt now looks like this:

Code: [Select]
SUB Gage1Proc_LM(hWnd:UINT, uMsg:UINT, wParam:UINT, lParam:ANYTYPE),UINT
   POINTER     g_dat /*GAGEPARAMS*/
   g_dat = new GAGEPARAMS
   SetWindowLong(hWnd, GWL_USERDATA, g_dat)
.....
ENDSUB

We have declared g_dat to be a pointer.
We created a new UDT of type  GAGEPARAMS in memory and the pointer to that memory is stored in g_dat.
We have used the SetWindowLong function to store the pointer value in the reserved GWL_USERDATA memory location for the current window.

Problem with the code structured this way is that after running for a while our application would crash. Remember hundreds of controls receiving hundreds of messages.  With this setup we're taking a chuck of memory the size of our UDT each time any message is sent to each of our control instances.  But this is an easy fix.  Our message handler is already set up to handle the WM_CREATE / @IDCREATE message. This message is sent by the OS when a window/control is first created but before it is displayed. So, if we adjust our code structure to the following we will wind up with one UDT pointer stored per control when the control is first created.

Code: [Select]
SUB Gage1Proc_LM(hWnd:UINT, uMsg:UINT, wParam:UINT, lParam:ANYTYPE),UINT
   POINTER     g_dat /*GAGEPARAMS*/
   SELECT uMsg
      CASE WM_CREATE
         g_dat = NEW GAGEPARAMS
         SetWindowLong(hWnd, GWL_USERDATA, g_dat)
.....
ENDSUB

The next step is to obtain a control's specific g_dat UDT each time a message is sent for that instance of the control. We do that with:

Code: [Select]
g_dat = GetWindowLong(hWnd, GWL_USERDATA)
The following shows the proper placement of the above line of code:

Code: [Select]
SUB Gage1Proc_LM(hWnd:UINT, uMsg:UINT, wParam:UINT, lParam:ANYTYPE),UINT
   POINTER     g_dat /*GAGEPARAMS*/

   g_dat = GetWindowLong(hWnd, GWL_USERDATA)
   SELECT uMsg
      CASE WM_CREATE
         g_dat = NEW GAGEPARAMS
         SetWindowLong(hWnd, GWL_USERDATA, g_dat)
.....
ENDSUB

Each time a message is sent hWnd identifies the specific instance of our control.  Before proceeding we obtain the pointer to our data.  Before WM_CREATE the pointer will be NULL.  After WM_CREATE is sent the pointer will always point to the appropriate data.

There is one thing that we ALWAYS have to remember about pointers and the NEW command.  When we take memory we have to give it back when we're through with it.  If we don't we wind up with what is called 'memory leaks'.  Slowly but surely we use up all our memory and when we do, we crash.

When it is time for a control to be deleted the OS sends the WM_DESTROY / @IDDESTROY destroy message.  This is done in order to give us the opportunity to do this very kind of cleanup.

The following shows how we return the memory used by g_dat for the current instance of the control.

Code: [Select]
SUB Gage1Proc_LM(hWnd:UINT, uMsg:UINT, wParam:UINT, lParam:ANYTYPE),UINT
   POINTER     g_dat /*GAGEPARAMS*/

   g_dat = GetWindowLong(hWnd, GWL_USERDATA)
   SELECT uMsg
      CASE WM_CREATE
         g_dat = NEW GAGEPARAMS
         SetWindowLong(hWnd, GWL_USERDATA, g_dat)
         RETURN 0
      CASE WM_DESTROY
         FreeHeap(g_dat)
         RETURN 0
.....
ENDSUB
         
The next step is to establish some default values for the elements in g_dat.

Code: [Select]
  'init g_dat
   #g_dat.sStyle = 0
   #g_dat.sName = ""
   #g_dat.sMultiUnits = ""
   #g_dat.nRangeMinAct = 0
   #g_dat.nRangeMaxAct = 100
   #g_dat.nRangeMinDial = -1
   #g_dat.nRangeMaxDial = 14
   #g_dat.nDialMulti = 10
   int r1,g1,b1
   GageunRGBLM(GetSysColor(0), r1, g1, b1)
   #g_dat.nPanelColor = RGBA(r1,g1,b1,255)
   #g_dat.nGageColor = RGBA(255,255,255,255)
   #g_dat.clockoffset = 0

GageunRGBLM is a utility function that converts a RGB color value into its individual color components.  We're doing that here because we need the three values to pass to the RGBA IWBasic function which adds the alpha component ( where 0 is fully transparent and 255 is fully opaque ). The GDI+ library, which will be used for drawing our control, requires colors be in this format.
We are actually setting the default color of the rectangle that contains our control to the grey color of a scrollbar. And the gage color (dial face) to white.

The following code for the GageunRGBLM function is placed in Section 10 of CCT_lib.iwb:

Code: [Select]
SUB GageunRGBLM(col as uint, r as int byref, g as int byref, b as int byref)
   r=col%256 :'Red
   g=(col%65536)/256 :'Green
   b=col/65536 :'Blue
   RETURN
ENDSUB

The following shows the proper insertion point for our default values (after an instance of g_dat is created):

Code: [Select]
SUB MyProc_CC(hWnd:UINT, uMsg:UINT, wParam:UINT, lParam:ANYTYPE),UINT
   POINTER     g_dat /*GAGEPARAMS*/

   g_dat = GetWindowLong(hWnd, GWL_USERDATA)
   SELECT uMsg
      CASE WM_CREATE
         g_dat = NEW GAGEPARAMS
         SetWindowLong(hWnd, GWL_USERDATA, g_dat)
         'init g_dat
         #g_dat.sStyle = 0
         #g_dat.sName = ""
         #g_dat.sMultiUnits = ""
         #g_dat.nRangeMinAct = 0
         #g_dat.nRangeMaxAct = 100
         #g_dat.nRangeMinDial = -1
         #g_dat.nRangeMaxDial = 14
         #g_dat.nDialMulti = 10
         int r1,g1,b1
         GageunRGBLM(GetSysColor(0), r1, g1, b1)
         #g_dat.nPanelColor = RGBA(r1,g1,b1,255)
         #g_dat.nGageColor = RGBA(255,255,255,255)
         #g_dat.clockoffset = 0
         RETURN 0
      CASE WM_DESTROY
         FreeHeap(g_dat)
         RETURN 0
......

____________________

Coming Next - Saving Passed Parameters - Part B

« Last Edit: August 28, 2011, 12:03:24 PM by LarryMc »
LarryMc
Larry McCaughn :)
Author of IWB+, Custom Button Designer library, Custom Chart Designer library, Snippet Manager, IWGrid control library, LM_Image control library