Author Topic: 20. Drawing the Gage Pointers  (Read 987 times)

0 Members and 1 Guest are viewing this topic.

Offline LarryMc

  • Administrator
  • Hero Member
  • *****
  • Posts: 5934
  • 'All I like is finishing'
20. Drawing the Gage Pointers
« on: September 17, 2011, 03:54:17 PM »
The final phase of drawing our control involves the gage pointers and, where appropriate, odometer looking counters.

Before we start adding code in the WM_PAINT section of our message handler we need to add some odds and ends to support the drawing code.

The first item we'll add is a new element to our GAGEPARAMS structure. The element will be used to hold the number of revolutions on those gages with counters.  We'll add the element at the bottom of the structure as shown below.

Code: [Select]
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
 INT iRevs
ENDTYPE

We will also need to initialize the iRevs element in the message handler as shown in the excerpt below:

Code: [Select]
...
      CASE WM_CREATE
#g_dat.nAngleStart = -230
#g_dat.nAngleEnd = 50
#g_dat.iRevs = 0
RETURN 0
      CASE WM_DESTROY
...

We'll need the following function in some of our drawing calculations.

Code: [Select]
sub gageLMMAX(a:INT, b:INT),INT
   if a>b then return a
   return b
endsub

The function will used to keep us from trying to divide by zero and crashing the user's application.  We'll place the function at the bottom of Section 8 of CCT_lib.iwb.

The following excerpt shows the bottom of the WM_PAINT handler as it is right now.  All the new code added in this discussion will be added following the 'draw our control' comment.

Code: [Select]
...
BitBlt(hdc2, 0, 0, 200, 200, #g_dat.dcBack, 0, 0, SRCCOPY)
GdipCreateFromHDC(hdc2, &pGraphics)
         '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)
if (pGraphics) then GdipDeleteGraphics(pGraphics)
EndPaint(hWnd, ps)
RETURN 0

The first block of code we'll add will create our font for the numbers in the odometer type counter and performer the preliminary setup of GDI+ objects.  It is identical to the code we added for the same purpose in the subroutine for drawing the background. The only differences are the size of the font , the color of the brush for drawing the text, and the color of the pen for drawing our graphic objects.  The code is shown below:

Code: [Select]
'setup font for counter
LOGFONTW lf2
ZeroMemory(lf2,LEN(LOGFONTW))
lf2.lfWidth     = 12
lf2.lfWeight   =FW_NORMAL
lf2.lfHeight   = -MulDiv(8,GetDeviceCaps(hdc2, 90),72)
lf2.lfItalic      = FALSE
lf2.lfFaceName    = L"arial"

'setup GDI+
GdipSetSmoothingMode(pGraphics, SmoothingModeAntiAlias)
pointer pStringFormat=0,pFont=0,pPen=0,pBrush=0
'new pStringFormat
GdipCreateStringFormat(4,LANG_NEUTRAL,&pStringFormat)
GdipSetStringFormatAlign(pStringFormat,StringAlignmentCenter)
'new pFont
GdipCreateFontFromLogfontW(hdc2,lf2,&pFont)
'new pBrush
GdipCreateSolidFill(RGBA(0,0,0,255), &pBrush)
'new pPen
GdipCreatePen1(RGBA(255,0,0,255),1,UnitPixel,&pPen)
GpRectF ri, rx, rt
iwstring tout[32]

Next we'll add the code to draw the odometer type counter, shown below:

Code: [Select]
'draw odometer type counter
select #g_dat.sStyle
case ROUND360SGL
case& ROUND360DBL
ri.X=0
ri.Y=0
ri.Width=500
ri.Height=500
tout =S2W(USING("#######0", #g_dat.iRevs))
GdipMeasureString(pGraphics,tout, len(tout), pFont, &ri, pStringFormat,&rx,0,0)
rt.X = 100 -rx.Width/2.0
rt.Y = 110
rt.Width = rx.Width
rt.Height = rx.Height
GdipDrawString(pGraphics,tout, len(tout), pFont, &rt, pStringFormat, pBrush)
GdipDrawRectangle(pGraphics,pPen,99-rx.Width/2.0,119-(rx.Height+4)/2.0,rx.Width,rx.Height)
endselect

This code follows the same pattern that was used for drawing the backgrounds text.  Notice the following line of code:

Code: [Select]
GdipDrawRectangle(pGraphics,pPen,99-rx.Width/2.0,119-(rx.Height+4)/2.0,rx.Width,rx.Height)

We're using the rectangle values obtained in the GdipMeasureString function to draw a box around the text.

Since there is no other text to be drawn we will go ahead and add out text associated cleanup with the following code:

Code: [Select]
'GDI+ text cleanup
IF pPen THEN GdipDeletePen(pPen)
pPen=0
'delete pFont
if pFont then GdipDeleteFont(pFont)
pFont=0
'delete pBrush
IF pBrush THEN GdipDeleteBrush(pBrush)
pBrush=0
'delete pStringFormat
if pStringFormat then GdipDeleteStringFormat(pStringFormat)
pStringFormat=0

We'll start our pointer drawing code with the following framework:

Code: [Select]
'draw gage pointers
FLOAT  dPercent = 0.0
INT        x, x2, x3
select #g_dat.sStyle
default

case ROUND2PENV

case ROUND2PENH

endselect

The following discussion all falls within the default case. We'll start with the following:

Code: [Select]
'calculate pointer positions
select #g_dat.sStyle
case ROUND270B
case& ROUND270T
case& ROUND270R
case& ROUND270L
dPercent   = (#g_dat.nPos1 - #g_dat.nRangeMinAct) / (gageLMMAX(1, (#g_dat.nRangeMaxAct - #g_dat.nRangeMinAct))+0.0)
x = (#g_dat.nAngleEnd - (#g_dat.nAngleStart))* dPercent  +(#g_dat.nAngleStart)
case ROUND360SGL
case& ROUND360DBL
dPercent   = (#g_dat.nPos1 - #g_dat.nRangeMinAct) / (gageLMMAX(1, (#g_dat.nRangeMaxAct - #g_dat.nRangeMinAct))+0.0)
#g_dat.iRevs=dPercent*1000
#g_dat.iRevs=#g_dat.iRevs/1000
x = (#g_dat.nAngleEnd - (#g_dat.nAngleStart))* dPercent  +(#g_dat.nAngleStart)
x2 = (#g_dat.nAngleEnd - (#g_dat.nAngleStart))* dPercent/10  +(#g_dat.nAngleStart)
case ROUNDCLOCK12
x=VAL(RIGHT$(TIME$,2))*6-90
INT mn=VAL(MID$(TIME$,4,2))
x2=mn*6-90
INT hr=VAL(LEFT$(TIME$,2))+#g_dat.clockoffset
if hr>11 then hr=hr-12
if hr=12 then hr=0
x3=(hr*30)+(mn/2)-90
case ROUNDCLOCK24
x=VAL(RIGHT$(TIME$,2))*6-90
mn=VAL(MID$(TIME$,4,2))
x2=mn*6-90
hr=VAL(LEFT$(TIME$,2))+#g_dat.clockoffset
if hr>23 then hr=hr-24
if hr=24 then hr=0
x3=(hr*15)+(mn/4)-90
endselect

For all the gages that fall in the default category we have to calculate the current angular position of the pointer(s).

We'll look at the 270 types first.

Code: [Select]
case ROUND270B
case& ROUND270T
case& ROUND270R
case& ROUND270L
dPercent   = (#g_dat.nPos1 - #g_dat.nRangeMinAct) / (gageLMMAX(1, (#g_dat.nRangeMaxAct - #g_dat.nRangeMinAct))+0.0)
x = (#g_dat.nAngleEnd - (#g_dat.nAngleStart))* dPercent  +(#g_dat.nAngleStart)

First we determine what percent the current input value is of the total input span as defined by the user.  We then apply that percentage to the dial span to find the current angular position.

The ROUND360 types are a little more elaborate.

Code: [Select]
case ROUND360SGL
case& ROUND360DBL
dPercent   = (#g_dat.nPos1 - #g_dat.nRangeMinAct) / (gageLMMAX(1, (#g_dat.nRangeMaxAct - #g_dat.nRangeMinAct))+0.0)
#g_dat.iRevs=dPercent*1000
#g_dat.iRevs=#g_dat.iRevs/1000
x = (#g_dat.nAngleEnd - (#g_dat.nAngleStart))* dPercent  +(#g_dat.nAngleStart)
x2 = (#g_dat.nAngleEnd - (#g_dat.nAngleStart))* dPercent/10  +(#g_dat.nAngleStart)

Here we calculate the percentage the same way.  We use that to first calculate the value to display in the counter.
We then calculate the current angular position for the longer, faster moving pointer followed by the calculation for the shorter, slower moving pointer.

Our two CLOCK types don't use the percent calculation of the current input input because they have no current input.  Instead, they use the time on the user's computer as shown below:

Code: [Select]
case ROUNDCLOCK12
x=VAL(RIGHT$(TIME$,2))*6-90
INT mn=VAL(MID$(TIME$,4,2))
x2=mn*6-90
INT hr=VAL(LEFT$(TIME$,2))+#g_dat.clockoffset
if hr>11 then hr=hr-12
if hr=12 then hr=0
x3=(hr*30)+(mn/2)-90
case ROUNDCLOCK24
x=VAL(RIGHT$(TIME$,2))*6-90
mn=VAL(MID$(TIME$,4,2))
x2=mn*6-90
hr=VAL(LEFT$(TIME$,2))+#g_dat.clockoffset
if hr>23 then hr=hr-24
if hr=24 then hr=0
x3=(hr*15)+(mn/4)-90

In both cases, the calculations for x (seconds) and x2 (minutes) are identical.  Also, in both cases, the hour value is adjusted by the offset in order to display time zones other than the local time for the user.  The angular position for the hour is contained in x3.  The degrees per hour for the 24 hour clock is half that of the 12 hour clock and the calculation reflects that.

That completes all the required calculations for our default case.  We will now start drawing our pointers, for the default case.

Code: [Select]
'paint center red
GdipCreateSolidFill(RGBA(255,0,0,255),&pBrush)
GdipFillEllipse(pGraphics,pBrush, 90, 90, 20, 20)
IF pBrush THEN GdipDeleteBrush(pBrush)
pBrush=0

In the above, we create a red brush, draw a red circle in the center of the gage and then do our cleanup.

The following code creates a red pen and draws the two indicated points for the designated gage types. That is followed by our pen cleanup.
 
Code: [Select]
GdipCreatePen1(RGBA(255,0,0,255),6,UnitPixel,&pPen)
GdipSetPenEndCap(pPen, LineCapTriangle)
select #g_dat.sStyle
case ROUND360DBL
'draw short hand
GdipDrawLine(pGraphics,pPen,100,100,COSD(x2)*60+100, SIND(x2)*60+100)
'draw long hand
GdipDrawLine(pGraphics,pPen,100,100,COSD(x)*70+100, SIND(x)*70+100)
case ROUNDCLOCK12
case& ROUNDCLOCK24
'draw short hand
GdipDrawLine(pGraphics,pPen,100,100,COSD(x3)*50+100, SIND(x3)*50+100)
'draw long hand
GdipDrawLine(pGraphics,pPen,100,100,COSD(x2)*70+100, SIND(x2)*70+100)
endselect
IF pPen THEN GdipDeletePen(pPen)
pPen=0

What is new in the above block of code is the use of the GdipSetPenEndCap function.  Using it we can change the appearence of the end of our pointer.  It should be noted that when using this function care should be given to the order of the x,y values passed to the GdipDrawLine function.  The second pair are always the end of the line.

In this block of code we draw the red point for gages with a single pointer (default case) or the black pointer for the seconds hand on our clocks.

Code: [Select]
select #g_dat.sStyle
case ROUNDCLOCK12
case& ROUNDCLOCK24
GdipCreatePen1(RGBA(0,0,0,255),4,UnitPixel,&pPen)
default
GdipCreatePen1(RGBA(255,0,0,255),4,UnitPixel,&pPen)
endselect
'draw clock second hand / single pointer
GdipSetPenStartCap(pPen, LineCapRoundAnchor)
GdipSetPenEndCap(pPen, LineCapTriangle)
GdipDrawLine(pGraphics,pPen,100,100,COSD(x)*75+100, SIND(x)*75+100)
IF pPen THEN GdipDeletePen(pPen)
pPen=0

Here we have added another new function; GdipSetPenStartCap.  On single pointer gages you can not see any change by using it because of the red circle we've already drawn at the center of our gages.  But on the clocks second hand you will see a small black circle at the center of the gage (the starting point of the line).   With our pen cleanup, that ends the default block of code.

The following code addresses the pointers on our two special cases:

Code: [Select]
case ROUND2PENV
'left
dPercent   = (#g_dat.nPos1 - #g_dat.nRangeMinAct) / (gageLMMAX(1, (#g_dat.nRangeMaxAct - #g_dat.nRangeMinAct))+0.0)
x = -40* dPercent  +20
GdipCreatePen1(RGBA(255,0,0,255),4,UnitPixel,&pPen)
GdipSetPenStartCap(pPen, LineCapTriangle)
GdipSetPenEndCap(pPen, LineCapRoundAnchor)
GdipDrawLine(pGraphics,pPen, COSD(x)*220-137, SIND(x)*212+100, 15, 100)
'right
dPercent   = (#g_dat.nPos2 - #g_dat.nRangeMinAct) / (gageLMMAX(1, (#g_dat.nRangeMaxAct - #g_dat.nRangeMinAct))+0.0)
x = 40* dPercent  +160
GdipDrawLine(pGraphics,pPen, COSD(x)*220+337, SIND(x)*212+100, 185, 100)
IF pPen THEN GdipDeletePen(pPen)
pPen=0
case ROUND2PENH
'top
dPercent   = (#g_dat.nPos1 - #g_dat.nRangeMinAct) / (gageLMMAX(1, (#g_dat.nRangeMaxAct - #g_dat.nRangeMinAct))+0.0)
x = -40* dPercent  -250
GdipCreatePen1(RGBA(255,0,0,255),4,UnitPixel,&pPen)
GdipSetPenStartCap(pPen, LineCapTriangle)
GdipSetPenEndCap(pPen, LineCapRoundAnchor)
GdipDrawLine(pGraphics,pPen,COSD(x)*212+100, SIND(x)*220-137, 100,15)
'bottom
dPercent   = (#g_dat.nPos2 - #g_dat.nRangeMinAct) / (gageLMMAX(1, (#g_dat.nRangeMaxAct - #g_dat.nRangeMinAct))+0.0)
x = 40* dPercent  +250
GdipDrawLine(pGraphics,pPen,COSD(x)*212+100, SIND(x)*220+337, 100,185)
IF pPen THEN GdipDeletePen(pPen)
pPen=0

There is nothing new that we haven't already discussed in the above block of code.

The only thing left to do is our final GDI+ cleanup for this section.  We will accomplish that with the following block of code:

Code: [Select]
'GDI+ final line and text cleanup
IF pPen THEN GdipDeletePen(pPen)
pPen=0
'delete pFont
if pFont then GdipDeleteFont(pFont)
pFont=0
'delete pBrush
IF pBrush THEN GdipDeleteBrush(pBrush)
pBrush=0
'delete pStringFormat
if pStringFormat then GdipDeleteStringFormat(pStringFormat)
pStringFormat=0

We are now through with coding for the WM_PAINT message handler.

If we compile and tun our project right now we would be disappointed.  None of our pointers, including the clocks, are moving.
NOTE: If you drag another window over the clocks you will notice that the second hand will move.  That gives us a hint of what we are missing.

First, we need to have something to change the current values that are sent to the gages. And, we need to cause something to generate WM_PAINT messages.

With the way we have constructed our code all we need to do is use our SetGagePos1RLM and SetGagePos2RLM functions and everything else should take care of itself.

The code we need to add will reside in the CCT_test.iwb file and will approximate what a user of our custom control might have to do.

Back when we set up out test file we added this section of code:
Code: [Select]
/*----------------SECTION 6-----------------------------------------
 This section sets the timer which will update our controls.
---------------------------------------------------------------------*/
STARTTIMER main, 100, 42
run = 1
/*=============== END OF SECTION 6 ===================================*/

And in the handler in Section 9 we had this:

Code: [Select]
...
      CASE @IDTIMER
SELECT @CODE
CASE 42
ENDSELECT
...

We're going to add our new code to the CASE 42 section. We'll start with:

Code: [Select]
static int rawvalue = 0
int tickmin = 0, tickmax = 100
static int toggle = 0

rawvalue will contain the raw value we are going to send to our gages. We declared it as STATIC so that it will be zero on the first pass through the timer handler.  Thereafter it will be whatever it was the last time through.

tickmin and tickmax will be used to limit the range of values we'll send to our gages.

toggle will be used to reverse the direction on the non-clock gages.

Now we will add the block of code to inc/dec our raw value and reverse direction when we hit a limit. The complete section now appears as:

Code: [Select]
     CASE @IDTIMER
SELECT @CODE
CASE 42
static int rawvalue = 0
int tickmin = 0,tickmax = 100
static int toggle = 0
select toggle
case 0
rawvalue++
if rawvalue > tickmax
rawvalue = tickmax
toggle=1
endif
case 1
rawvalue--
if rawvalue < tickmin
rawvalue = tickmin
toggle=0
endif
endselect

ENDSELECT

To the above we will append to following, which updates our controls:

Code: [Select]
SetGagePos1RLM(main,1,rawvalue) 'ROUND270B
SetGagePos1RLM(main,2,rawvalue) 'ROUND270T
SetGagePos1RLM(main,3,rawvalue) 'ROUND270L
SetGagePos1RLM(main,4,rawvalue) 'ROUND270R

SetGagePos1RLM(main,5,rawvalue) 'ROUND360SGL
SetGagePos1RLM(main,6,rawvalue) 'ROUND360DBL

SetGagePos1RLM(main,7,rawvalue) 'ROUND2PENV
SetGagePos2RLM(main,7,rawvalue) 'ROUND2PENV

SetGagePos1RLM(main,8,rawvalue) 'ROUND2PENH
SetGagePos2RLM(main,8,rawvalue) 'ROUND2PENH

SetGagePos1RLM(main,9,0) 'ROUNDCLOCK12
SetGagePos1RLM(main,10,0) 'ROUNDCLOCK24

Notice that for the clocks the value doesn't matter.

This completes all the drawing code for our control.

<edit> - updated attachments
_____________________________

Coming Next - Notifying the Parent
LarryMc
Larry McCaughn :)
Author of IWB+, Custom Button Designer library, Custom Chart Designer library, Snippet Manager, IWGrid control library, LM_Image control library