IonicWind Software

IWBasic => Tutorials => Create a Custom Control => Topic started by: LarryMc on September 09, 2011, 07:26:48 PM

Title: 17. WM_PAINT Setup
Post by: LarryMc on September 09, 2011, 07:26:48 PM
We're finally at the point where we are going to set up the code structure for the actual drawing of our control.  From earlier discussions we know that the OS sends the WM_PAINT / @IDPAINT message whenever there is a need to redraw our control.  That is, when all or a portion of our control display area has been made invalid.  That can be caused by moving another object over our control on the screen or by calling the InvalidateRect API function

This is the code excerpt we will start with:

SUB Gage1Proc_LM(hWnd:UINT, uMsg:UINT, wParam:UINT, lParam:ANYTYPE),UINT
...
   PAINTSTRUCT ps
...
   SELECT uMsg
      CASE WM_PAINT
         BeginPaint(hWnd, ps)
         'draw our control

         EndPaint(hWnd, ps)
         RETURN 0
...



The BeginPaint API function prepares the current window/control for painting and fills a PAINTSTRUCT type structure with information about the painting.  The function is passed two parameters: the handle (hWnd) to the current instance of our control and a previously defined variable (ps) of type PAINTSTRUCT.  The only element of the  PAINTSTRUCT UDT that we are interested in is the first; hdc.  This is the screen device context used for painting our control.

The last API function that we call in the WM_PAINT portion of the handler is EndPaint.  It passes the same two parameters as the BeginPaint function. The main purpose of this function, as far as we are concerned, is to validate our control. In other words, it tells the OS that our control has been updated and no further WM_PAINT messages are needed at this time.

Some readers may be asking themselves why don't we use GetDC / ReleaseDC instead of BeginPaint / EndPaint.  It all has to do with the invalidating / validating of the client area of our control.  The rule of thumb to remember is inside the WM_PAINT handler always use BeginPaint / EndPaint otherwise use GetDC / ReleaseDC.

The code structure above is enough to allow us to start using GDI functions to draw our control.  But there is a potential problem.  With this arrangement there is the potential for flickering if our drawing code is very complicated (time consuming).
To eliminate the possible problem we will use a technique called "double-buffering".

This is done by creating a compatible device context in memory along with a compatible bitmap in memory and doing all our drawing to memory instead of directly to the screen.  When we are through with all our drawing we'll transfer the complete memory bitmap to the screen device context.  In that way we update our control on the screen all at one time, flicker free.
The following is how the existing code would need to be modified

...
   PAINTSTRUCT ps
   UINT        hdc2
...
   SELECT uMsg
      CASE WM_PAINT
         BeginPaint(hWnd, ps)
         'create our memory DC that is compatible with our screen DC (ps.hdc)
         hdc2 = CreateCompatibleDC(ps.hdc)
         'create our memory bitmap that is compatible with our screen DC (ps.hdc) and make it 200 x 200 pixels
         UINT hbmp = SelectObject(hdc2, CreateCompatibleBitmap(ps.hdc, 200, 200))
         'draw our control

         EndPaint(hWnd, ps)
         RETURN 0
....


         hdc2 = CreateCompatibleDC(ps.hdc)
This line creates a memory DC that is compatible with the screen DC for our control(ps.hdc) and returns the memory DC's handle (hdc2).

         UINT hbmp = SelectObject(hdc2, CreateCompatibleBitmap(ps.hdc, 200, 200))
This line creates a memory 200 x 200 pixel bitmap that is compatible with the screen DC for our control(ps.hdc).  It then selects the memory bitmap object into the memory DC (hdc2).  Said another way, it assigns the memory bitmap to the memory DC.
When finished, hbmp will contain the handle to the bitmap that is being replaced.

The golden rule of windows programming is that when you create something temporarily that uses memory you must free (give back) the memory when you are finished with it.  Failure to do so will cause erratic program behavior and ultimately a program crash.  The previous two lines of code consumed memory.  We now need to add code to give the memory back. We'll start with the memory bitmap. Remember from the SelectObject function above that it returns the handle to the object being replace. We saved the original, replaced bitmap handle in hbmp.  So, by selecting it, the function will return the handle to our compatible bitmap that we created.  With that handle, we can delete the object thus freeing up the memory. The following line of code accomplishes this for us:

DeleteObject(SelectObject(hdc2, hbmp))

It is important to note that an object that is currently selected into (assigned to) a DC can not be deleted.  The object must first be de-selected using the technique just demonstrated.

After all objects have been deleted we can delete the memory DC itself.  We accomplish this with the following line of code:

DeleteDC(hdc2)

Adding these two lines to our handler excerpt we have:

...
   PAINTSTRUCT ps
   UINT        hdc2
...
   SELECT uMsg
      CASE WM_PAINT
         BeginPaint(hWnd, ps)
         'create our memory DC that is compatible with our screen DC (ps.hdc)
         hdc2 = CreateCompatibleDC(ps.hdc)
         'create our memory bitmap that is compatible with our screen DC (ps.hdc) and make it 200 x 200 pixels
         UINT hbmp = SelectObject(hdc2, CreateCompatibleBitmap(ps.hdc, 200, 200))
         'draw our control

         EndPaint(hWnd, ps)
         DeleteObject(SelectObject(hdc2, hbmp))
         DeleteDC(hdc2)
         RETURN 0
....


Now that we have our buffer taken care of we need to add the code that transfers what we draw on the memory map to the screen DC.  That is done with the following code:

SetStretchBltMode(ps.hdc, HALFTONE)
SetBrushOrgEx(ps.hdc,0, 0,null)
WINRECT     rc
GetClientRect(hWnd, rc)
int rcsize = rc.right
StretchBlt(ps.hdc, 0, 0, rcsize, rcsize, hdc2, 0, 0, 200, 200, SRCCOPY)


When we created our memory bitmap we set it to 200 x 200 pixels.  This was done so that we could draw all of our gages at one size.  This eliminates any rounding errors that might appear due to scaling all our calculations to the control's actual size, set by the user.  With the above block of code we are going to scale the entire control to the size requested by the user.

SetStretchBltMode(ps.hdc, HALFTONE)
This line sets the bitmap stretching mode in the screen DC to HALFTONE.  HALFTONE mode maps pixels from the source rectangle into blocks of pixels in the destination rectangle. The average color over the destination block of pixels approximates the color of the source pixels. Using this mode requires that we use the following function:

SetBrushOrgEx(ps.hdc,0, 0,null)
This function sets the brush origin that GDI assigns to the next brush an application selects into the memory DC.  The line above sets the default values.

winrect rc
GetClientRect(hWnd, rc)
int rcsize = rc.right

These three lines load the dimensions of the controls client area into a WINRECT UDT and then uses the right element for the rcsize value in the next function.

StretchBlt(ps.hdc, 0, 0, rcsize, rcsize, hdc2, 0, 0, 200, 200, SRCCOPY)
This function does the actual copying of our memory DC to the screen DC; scaling up/down based upon the value of rcsize which is the radius of the control requested by the user.

Our handler excerpt now looks like the following:

...
   PAINTSTRUCT ps
   UINT        hdc2
...
   SELECT uMsg
      CASE WM_PAINT
         BeginPaint(hWnd, ps)
         'create our memory DC that is compatible with our screen DC (ps.hdc)
         hdc2 = CreateCompatibleDC(ps.hdc)
         'create our memory bitmap that is compatible with our screen DC (ps.hdc) and make it 200 x 200 pixels
         UINT hbmp = SelectObject(hdc2, CreateCompatibleBitmap(ps.hdc, 200, 200))
         'draw our control

         SetStretchBltMode(ps.hdc, HALFTONE)
         SetBrushOrgEx(ps.hdc,0, 0,null)
         WINRECT     rc
         GetClientRect(hWnd, rc)
         int rcsize = rc.right
         StretchBlt(ps.hdc, 0, 0, rcsize, rcsize, hdc2, 0, 0, 200, 200, SRCCOPY)
         EndPaint(hWnd, ps)
         DeleteObject(SelectObject(hdc2, hbmp))
         DeleteDC(hdc2)
         RETURN 0
....


With the resulting code block (shown above) we can now start writing the actual drawing code.  However, if we do our code will not be very efficient. Each time the WM_PAINT message is received by the handler we are creating a memory DC and bitmap and then deleting it. 

There is no reason why we need to do it that way.  It would be much more efficient to create the memory DC and bitmap the first time WM_PAINT message is sent to the current instance; save their handles so we can reuse them for all future updates and then give the memory back when we no longer the control.

We're going to modify our code above to make the improvement.  We'll start by creating two variables to store the handles of the memory DC and the memory bitmap.  Since these variables have to be unique to each instance of our control we will make them elements in our g_dat UDT.  We'll start by adding the two new elements to the top of our GAGEPARAMS UDT structure definition in Section 2 of CCT_lib.iwb.  The definition will now look like:

TYPE GAGEPARAMS
   UINT dcHand
   UINT bitHand
   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*/
   FLOAT nAngleStart
   FLOAT nAngleEnd
ENDTYPE


Next we'll initialize the elements at the top portion of the WM_CREATE section of our message handler; which will now appear as:

CASE WM_CREATE
g_dat = NEW GAGEPARAMS
SetWindowLong(hWnd, GWL_USERDATA, g_dat)
'init g_dat
#g_dat.dcHand = 0
#g_dat.bitHand = 0
#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
#g_dat.nAngleStart = -230
#g_dat.nAngleEnd = 50
RETURN 0


Next we will modify the following portion of the WM_PAINT code:

      CASE WM_PAINT
         BeginPaint(hWnd, ps)
         'create our memory DC that is compatible with our screen DC (ps.hdc)
         hdc2 = CreateCompatibleDC(ps.hdc)
         'create our memory bitmap that is compatible with our screen DC (ps.hdc) and make it 200 x 200 pixels
         UINT hbmp = SelectObject(hdc2, CreateCompatibleBitmap(ps.hdc, 200, 200))
         'draw our control


to look like this:

      CASE WM_PAINT
         BeginPaint(hWnd, ps)
  if #g_dat.dcHand = 0
            'create our memory DC that is compatible with our screen DC (ps.hdc)
     hdc2 = CreateCompatibleDC(ps.hdc)
     'save the memory dc handle
     #g_dat.dcHand = hdc2
            'create our memory bitmap that is compatible with our screen DC (ps.hdc) and make it 200 x 200 pixels
            'and save the PREVIOUS bitmap handle
     #g_dat.bitHand = SelectObject(hdc2, CreateCompatibleBitmap(ps.hdc, 200, 200))
  endif
  hdc2=#g_dat.dcHand
         'draw our control


Now, the first time WM_PAINT is sent after this instance of the control is created the memory DC and bitmap will be created and their handles are saved.

We also have to make changes to give the memory back to the system at the proper time.  We need to change the following:

...
         EndPaint(hWnd, ps)
         DeleteObject(SelectObject(hdc2, hbmp))
         DeleteDC(hdc2)
         RETURN 0


by moving

         DeleteObject(SelectObject(hdc2, hbmp))
         DeleteDC(hdc2)


and modifying the lines to use the proper UDT elements so that the result appears as:

      case WM_DESTROY
  DeleteObject(SelectObject(#g_dat.dcHand, #g_dat.bitHand))
  DeleteDC(#g_dat.dcHand)
  FreeHeap(g_dat)
  return 0


Now, our memory DC and bitmap will be created once, when the first WM_PAINT message is sent after creation of the control, and they will be deleted once, when this instance of the control is being deleted.

There is another opportunity for optimization.  With the code we have created so far we are setup to draw 100% of each instance of our control each time the WM_PAINT message is sent.  But is that really necessary?

When you consider all the components of our control our drawing can be divided into two categories, static and dynamic.
The dynamic portion is the pointers. They are constantly changing position.  The static portion contains everything else; the gage border, the dial with its numbering, the text labeling.  If we draw the static portion once when an instance of the control is created, and each time its configuration is actually changed, we can save it and use it over and over without having to redraw it.

We can accomplish this by creating a second memory DC and bitmap to hold the static / background drawing.  We get started by doing what we did above.

First, we add two new elements to the GAGEPARAMS UDT:

TYPE GAGEPARAMS
   UINT dcHand
   UINT bitHand
   UINT dcBack
   UINT bmBack
   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*/
   FLOAT nAngleStart
   FLOAT nAngleEnd
ENDTYPE


Next we'll initialize the elements in the upper portion of the WM_CREATE section of our message handler.

CASE WM_CREATE
g_dat = NEW GAGEPARAMS
SetWindowLong(hWnd, GWL_USERDATA, g_dat)
'init g_dat
#g_dat.dcHand = 0
#g_dat.bitHand = 0
#g_dat.dcBack = 0
#g_dat.bmBack = 0
#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
#g_dat.nAngleStart = -230
#g_dat.nAngleEnd = 50
RETURN 0


The following shows the modification to the WM_PAINT handler where we create the second DC and bitmap and store their handles:

      CASE WM_PAINT
         BeginPaint(hWnd, ps)
  if #g_dat.dcHand = 0
            'create our memory DC that is compatible with our screen DC (ps.hdc)
     hdc2 = CreateCompatibleDC(ps.hdc)
     'save the memory dc handle
     #g_dat.dcHand = hdc2
            'create our memory bitmap that is compatible with our screen DC (ps.hdc) and make it 200 x 200 pixels
            'and save the PREVIOUS bitmap handle
     #g_dat.bitHand = SelectObject(hdc2, CreateCompatibleBitmap(ps.hdc, 200, 200))
  endif
  hdc2=#g_dat.dcHand

  if (!#g_dat.dcBack)
     'create our bg memory DC that is compatible with our screen DC (ps.hdc)
     #g_dat.dcBack = CreateCompatibleDC(ps.hdc)
            'create our bg memory bitmap that is compatible with our screen DC (ps.hdc) and make it 200 x 200 pixels
     #g_dat.bmBack = SelectObject(#g_dat.dcBack, CreateCompatibleBitmap(ps.hdc, 200, 200))
            'call our subroutine to draw the background of this instance of the control
     GageDrawbackground(g_dat)
  endif
         'draw our control


Notice the addition of
     GageDrawbackground(g_dat)

We have to split the actual drawing commands in to because they will be drawn at different times. We will cover that subroutine shortly.

To finish up with what we did previously we need to give the memory back when we are through with it. We add that code here:

      case WM_DESTROY
        DeleteObject(SelectObject(#g_dat.dcHand, #g_dat.bitHand))
        DeleteDC(#g_dat.dcHand)
        DeleteObject(SelectObject(#g_dat.dcBack, #g_dat.bmBack))
        DeleteDC(#g_dat.dcBack)
        FreeHeap(g_dat)
        return 0


We now need a way to copy the background that we have drawn into the DC that we're going to draw to gage pointers in. There's a simple API function to accomplish that:

BitBlt(hdc2, 0, 0, 200, 200, #g_dat.dcBack, 0, 0, SRCCOPY)

The placement of this function is very important.  It has to be after the background is drawn and before we draw any of the gage pointers. The following shows the proper location:

      CASE WM_PAINT
         BeginPaint(hWnd, ps)
  if #g_dat.dcHand = 0
            'create our memory DC that is compatible with our screen DC (ps.hdc)
     hdc2 = CreateCompatibleDC(ps.hdc)
     'save the memory dc handle
     #g_dat.dcHand = hdc2
            'create our memory bitmap that is compatible with our screen DC (ps.hdc) and make it 200 x 200 pixels
            'and save the PREVIOUS bitmap handle
     #g_dat.bitHand = SelectObject(hdc2, CreateCompatibleBitmap(ps.hdc, 200, 200))
  endif
  hdc2=#g_dat.dcHand

  if (!#g_dat.dcBack)
     'create our bg memory DC that is compatible with our screen DC (ps.hdc)
     #g_dat.dcBack = CreateCompatibleDC(ps.hdc)
            'create our bg memory bitmap that is compatible with our screen DC (ps.hdc) and make it 200 x 200 pixels
     #g_dat.bmBack = SelectObject(#g_dat.dcBack, CreateCompatibleBitmap(ps.hdc, 200, 200))
            'call our subroutine to draw the background of this instance of the control
     GageDrawbackground(g_dat)
  endif
  BitBlt(hdc2, 0, 0, 200, 200, #g_dat.dcBack, 0, 0, SRCCOPY)
         'draw our control


Our code now is at the point where, when the first WM_PAINT message for an instance of a control is sent, our background DC is created and the subroutine to draw the background is called.  After that first time it is not called again.

There is a problem with that.  Our command to create an instance of a control causes the WM_CREATE message to be sent followed by a WM_PAINT message.  This happens before we have used our configure command.  With the current code code we draw the background only one time.  We need to make it redraw when we change its configuration.

We need to go back and look at our configure subroutine that we covered in an earlier section.  The code is in Section 6 of CCT_lib.iwb, shown below:

SUB ConfigGageRLM(win as WINDOW,_
    style as INT,_
    title as STRING,_
    u as STRING,_
    rawmin as INT,_
    rawmax as INT,_
    dialmin as INT,_
    dialmax as INT,_
    sf as INT,_
    id as UINT )
   SENDMESSAGE(win, GAGE_STYLE, 0,style, id)
   STRING ttemp=title
   select style
      case ROUND270B
      case& ROUND270T
      case& ROUND270R
      case& ROUND270L
         if (dialmax-dialmin < 1) | (dialmax-dialmin > 15)
            dialmin=0 : dialmax=1
            ttemp="Dial Min/Max Error!"
         endif
   endselect
   SENDMESSAGE(win, GAGE_TITLE, 0,ttemp, id)
   SENDMESSAGE(win, GAGE_UNITS, 0,u, id)
   SELECT style
      CASE ROUNDCLOCK12
      CASE& ROUNDCLOCK24
         SENDMESSAGE(win, GAGE_CLKOFFSET, dialmin,0, id)
      DEFAULT
         SENDMESSAGE(win, GAGE_RAWRNG, rawmin,rawmax, id)
   ENDSELECT
   SELECT style
      CASE ROUND360SGL
      CASE& ROUND360DBL
      CASE& ROUND2PENV
      CASE& ROUND2PENH
         dialmin = 0 : dialmax = 10
   ENDSELECT
   SENDMESSAGE(win, GAGE_DIALRNG, dialmin,dialmax, id)
   SENDMESSAGE(win, GAGE_MULTI, 0,sf, id)
   GageInvalidateBackground(win, id)
   RETURN
ENDSUB


The last line of code before the RETURN statement is what we are interested in.  We said at the time we would cover it later and and we created the following subroutine shell in Section 8 of CCT_lib.iwb.

SUB GageInvalidateBackground(win as WINDOW, id as UINT)

ENDSUB


The purpose of this routine is force the background to be redrawn whenever we change an instance of a control's configuration.
the first thing we need to do is get a copy of the parameters that are stored for this instance with the following two lines of code:

pointer g_dat = GetWindowLong(GETCONTROLHANDLE(win,id), GWL_USERDATA)
settype g_dat, GAGEPARAMS


The first line we use to get the location of our GAGEPARAMS UDT structure for this instance of the control.  Since the definition of the type of variable a pointer points to can not cross module/subroutine lines we have to add the second to define the type.  It allows us to use

g_dat.element

instead of

#<GAGEPARAMS>g_dat.element

in our code.

We now add the following:

if (g_dat && g_dat.dcBack)
DeleteObject(SelectObject(g_dat.dcBack, g_dat.bmBack))
DeleteDC(g_dat.dcBack)
g_dat.dcBack = 0
g_dat.bmBack = 0
endif


If the background DC exists we delete the bg bitmap and DC.  We then set the two associated elements to zero.

We know, from our discussion above that if a message is sent to the control's handler that the background DC and bitmap will be created if g_dat.dcBack is equal to zero.  And we just got through setting it to zero.  So, all we are missing is a way to make it redraw.  We will accomplish it by using the following OS API function.

InvalidateRect(GETCONTROLHANDLE(win,id), NULL, FALSE)

The above line will cause a WM_PAINT message to be sent which will result in the background, with the new configuration parameters, being redrawn.

Our complete subroutine will appear as follows in Section 8 of CCT_lib.iwb.

SUB GageInvalidateBackground(win as WINDOW, id as UINT)
pointer g_dat = GetWindowLong(GETCONTROLHANDLE(win,id), GWL_USERDATA)
settype g_dat, GAGEPARAMS
if (g_dat && g_dat.dcBack)
DeleteObject(SelectObject(g_dat.dcBack, g_dat.bmBack))
DeleteDC(g_dat.dcBack)
g_dat.dcBack = 0
g_dat.bmBack = 0
endif
InvalidateRect(GETCONTROLHANDLE(win,id), NULL, FALSE)
ENDSUB


As promised earlier, the last item we need to address is our background  drawing routine.  The skeleton for it is as follows:

SUB GageDrawbackground(pointer g_dat)
settype g_dat,GAGEPARAMS
uint hdc2 = #g_dat.dcBack
       'draw background


ENDSUB


As in other cases we have to specify the type for our g_dat pointer.  And we create a variable to hold the handle to the DC we will be drawing to.

At this point our structure is established so that we can start drawing with GDI API functions.

____________________________

Coming Next - GDI+ Setup