March 28, 2024, 03:58:28 PM

News:

IonicWind Snippit Manager 2.xx Released!  Install it on a memory stick and take it with you!  With or without IWBasic!


19d. Drawing the Background

Started by LarryMc, September 15, 2011, 08:10:02 PM

Previous topic - Next topic

0 Members and 1 Guest are viewing this topic.

LarryMc

Part D

The last portion of the background we have to deal with is the writing of the text on our gages.  Text requires a little more code than what has been needed to draw our simple arcs and lines.
This is the framework in which we be doing our coding:

...
endselect
GdipDeletePen(pPen)

'section for drawing text
'setup font

'gdi+ setup

'draw scale numbers

'draw gage titles

'draw range multipliers and units

'gdi+ text cleanup

GdipDeleteGraphics(pGraphics)
endif
ENDSUB


The first step in drawing text is creating a font to use.  We do that with the following:

'setup font
LOGFONTW lf2
ZeroMemory(lf2,LEN(LOGFONTW))
lf2.lfWidth  = 12
lf2.lfWeight =FW_NORMAL
lf2.lfHeight = -MulDiv(10,GetDeviceCaps(hdc2, 90),72)
lf2.lfItalic      = FALSE
lf2.lfFaceName    = L"arial"

The OS LOGFONTW structure defines the attributes of a UNICODE font.  We're using a UNICODE font instead of an ASCII font purely to meet the requirements of the GDI+ library.  We'll create our copy of the structure and name the variable lf2.
ZeroMemory(lf2,LEN(LOGFONTW))
This line insures that elements of the structure that we are not setting are free of random data.

lf2.lfWidth  = 12
Sets the width of characters in our font.

lf2.lfWeight =FW_NORMAL
Sets the desired weight of our font.  FW_BOLD will give us a bold font, if desired.

lf2.lfHeight = -MulDiv(10,GetDeviceCaps(hdc2, 90),72)
Sets the desired height of our font.  The formula is the one given in the OS SDK.  The 10 represents what we normally refer to as Font Size.

lf2.lfItalic      = FALSE
No italics.

lf2.lfFaceName    = L"arial"
Specifies that we want to use the Arial font face.

Next we'll setup GDI+ :

'gdi+ setup
GdipSetTextRenderingHint(pGraphics, TextRenderingHintAntiAlias)
'new pFont
pointer pFont=0
GdipCreateFontFromLogfontW(hdc2,lf2,&pFont)
'new pBrush
GdipCreateSolidFill(RGBA(100,100,100,255), &pBrush)
'new pStringFormat
pointer pStringFormat=0
GdipCreateStringFormat(4,LANG_NEUTRAL,&pStringFormat)
GdipSetStringFormatAlign(pStringFormat,StringAlignmentCenter)
GpRectF ri, rx, rt
INT snum
iwstring tout[32]


GdipSetTextRenderingHint(pGraphics, TextRenderingHintAntiAlias)
This sets the text rendering mode for antialiasing. Since all our printing is normal (as opposed to on an angle) the antialiasing benefit is not quite so obvious for text.

'new pFont
pointer pFont=0
GdipCreateFontFromLogfontW(hdc2,lf2,&pFont)

Here we are creating the pFont object that contains our LOGFONTW information and is compatible with our DC.

'new pBrush
GdipCreateSolidFill(RGBA(100,100,100,255), &pBrush)

The GDI+ text functions use a brush to draw the text.  Here we are creating one that uses our solid grey color.

'new pStringFormat
pointer pStringFormat=0
GdipCreateStringFormat(4,LANG_NEUTRAL,&pStringFormat)
GdipSetStringFormatAlign(pStringFormat,StringAlignmentCenter)

GDI+ needs a pStringFormat object to hold formatting information for the text we'll draw.  The "4" is used because that was what was being used in the example I found.  LANG_NEUTRAL indicates that the user's language will be used. StringAlignmentCenter sets how the text will be drawn in the rectangle that will contain the text.

GpRectF ri, rx, rt
As just mentioned, text is drawn in rectangles.  These variables will be used by GDI+ to determine the size of the required rectangle and display the text.

INT snum
Used as an index when creating our scale numbers in a FOR loop.

iwstring tout[32]
The UNICODE variable that will contain our text to be drawn.

The next segment is the actual drawing of the scale numbers.  We'll start with:

'draw scale numbers
select #g_dat.sStyle
case ROUND270B
case& ROUND270T
case& ROUND270R
case& ROUND270L

case ROUND360SGL
case& ROUND360DBL

case ROUNDCLOCK12

case ROUNDCLOCK24

case ROUND2PENV

case ROUND2PENH

endselect


We'll address the 270 degree types first.

case ROUND270B
case& ROUND270T
case& ROUND270R
case& ROUND270L
snum=#g_dat.nRangeMinDial
ri.X=0
ri.Y=0
ri.Width=500
ri.Height=500
for x= int( #g_dat.nAngleStart) to int (#g_dat.nAngleEnd) step int(#g_dat.nAngleEnd -#g_dat.nAngleStart)/(#g_dat.nRangeMaxDial-#g_dat.nRangeMinDial)
GdipMeasureString(pGraphics,tout, wsprintfW(tout, L"%d", snum), pFont, &ri, pStringFormat,&rx,0,0)
rt.X = (COSD(x)*62.0 -(rx.Width/2.0) )+100.0
rt.Y = (SIND(x)*62.0 -(rx.Height/2.0))+100.0
rt.Width = rx.Width
rt.Height = rx.Height
GdipDrawString(pGraphics,tout, len(tout), pFont, &rt, pStringFormat, pBrush)
snum++
next x


We'll go through the block of code line by line:

snum=#g_dat.nRangeMinDial
This sets snum equal to the first number we want to draw.

ri.X=0
ri.Y=0
ri.Width=500
ri.Height=500

This initializes a GDI+ rectangle that will be used by the GdipMeasureString function to be called next.  The size in this case is purely arbitrary but taking care that is more than large enough to hold the text string we want to draw.  Once we pass it to the function it no longer is relevant.

for x= int( #g_dat.nAngleStart) to int (#g_dat.nAngleEnd) step int(#g_dat.nAngleEnd -#g_dat.nAngleStart)/(#g_dat.nRangeMaxDial-#g_dat.nRangeMinDial)

With the above we set up our FOR loop. The above can be described as:

FOR start_angle TO end_angle STEP angle_between_numbers

which is the same thing we did previously when drawing our major tic marks.

GdipMeasureString(pGraphics,tout, wsprintfW(tout, L"%d", snum), pFont, &ri, pStringFormat,&rx,0,0)

The GdipMeasureString function measures the extent of the string (tout) in the specified font (pFont), format (pStringFormat), and layout rectangle (ri).  The last three parameters contain information returned from function.  rx contains the size of the smallest rectangle that is required to contain the text that is no smaller than the ri rectangle.  The two zero (NULL) parameters could be variables that would contain how many characters on a line and how many lines can be drawn in ri if it turns out that rx is larger than ri.  Since we made ri way larger than what will be needed we know we don't need those last two values.
That leaves us wondering about two things: we haven't converted snum to a string and assigned it to tout; and what is this:

wsprintfW(tout, L"%d", snum)

The above is in the parameter slot that where we normally find LEN(tout); the length of the string tout.  The wsprintfW function takes the number in snum, converts it to a UNICODE string with no leading spaces, and stores the result in tout.  And in doing that it returns the length of the string.
So now tout contains the proper text and we know the size of the box that contains it.

rt.X = (COSD(x)*62.0 -(rx.Width/2.0) )+100.0
rt.Y = (SIND(x)*62.0 -(rx.Height/2.0))+100.0


Here we are using our trig functions to find a point on a 'spoke' that corresponds with where our major tic marks are located.  We're then backing off by an amount equal to 1/2 of the box that contains our text. NOTE: Remember that set set our text alignment to be the center of the text.  Then, as with our tic drawings, we have to adjust to the center of control instead of the upper left corner.  We store the results of the calculations in the rt structure.

rt.Width = rx.Width
rt.Height = rx.Height


These two set the rt elements to the values from our GdipMeasureString function.

We can now draw our text with:

GdipDrawString(pGraphics,tout, len(tout), pFont, &rt, pStringFormat, pBrush)

We finish up by incrementing our number and closing the FOR loop with:

snum++
next x


All the remaining text drawing follows this same format.  For all of them we will show the code block and only note the difference from the above discussion.

case ROUND360SGL
case& ROUND360DBL
snum=#g_dat.nRangeMinDial
ri.X=0
ri.Y=0
ri.Width=500
ri.Height=500
for x= int( #g_dat.nAngleStart) to int (#g_dat.nAngleEnd-10) step int(#g_dat.nAngleEnd -#g_dat.nAngleStart)/(#g_dat.nRangeMaxDial-#g_dat.nRangeMinDial)
GdipMeasureString(pGraphics,tout, wsprintfW(tout, L"%d", snum), pFont, &ri, pStringFormat,&rx,0,0)
rt.X = (COSD(x)*61.0 -(rx.Width/2.0) )+100.0
rt.Y = (SIND(x)*61.0 -(rx.Height/2.0))+100.0
rt.Width = rx.Width
rt.Height = rx.Height
GdipDrawString(pGraphics,tout, len(tout), pFont, &rt, pStringFormat, pBrush)
snum++
next x


As it turns out the only difference with the above block is the radius of the imaginary circle used in the trig calculations.

case ROUNDCLOCK12
snum=1
ri.X=0
ri.Y=0
ri.Width=500
ri.Height=500
for x= -60 to 270 step 30
GdipMeasureString(pGraphics,tout, wsprintfW(tout, L"%d", snum), pFont, &ri, pStringFormat,&rx,0,0)
rt.X = (COSD(x)*63.0 -(rx.Width/2.0) )+100.0
rt.Y = (SIND(x)*63.0 -(rx.Height/2.0))+100.0
rt.Width = rx.Width
rt.Height = rx.Height
GdipDrawString(pGraphics,tout, len(tout), pFont, &rt, pStringFormat, pBrush)
snum++
next x


With the above snum doesn't use the gdat elements and the FOR loop indexes don't require any calculations.

case ROUNDCLOCK24
snum=2
ri.X=0
ri.Y=0
ri.Width=500
ri.Height=500
for x= -60 to 270 step 30
GdipMeasureString(pGraphics,tout, wsprintfW(tout, L"%d", snum), pFont, &ri, pStringFormat,&rx,0,0)
rt.X = (COSD(x)*63.0 -(rx.Width/2.0) )+100.0
rt.Y = (SIND(x)*63.0 -(rx.Height/2.0))+100.0
rt.Width = rx.Width
rt.Height = rx.Height
GdipDrawString(pGraphics,tout, len(tout), pFont, &rt, pStringFormat, pBrush)
snum+=2
next x


The 24 hour clock, above, only draws the 12 even numbered hours. As a result it differs from the 12 hour clock with the initial value of snum and its incrementing.

case ROUND2PENV
snum=0
ri.X=0
ri.Y=0
ri.Width=500
ri.Height=500
for x= 160 to 200 step 4
GdipMeasureString(pGraphics,tout, wsprintfW(tout, L"%d", snum), pFont, &ri, pStringFormat,&rx,0,0)
rt.X = (100 -(rx.Width/2.0) )
rt.Y = (SIND(x)*210.0 -(rx.Height/2.0))+100.0
rt.Width = rx.Width
rt.Height = rx.Height
GdipDrawString(pGraphics,tout, len(tout), pFont, &rt, pStringFormat, pBrush)
snum++
next x


The significant difference with the above is that we are not using the COSD function in determining the value for rt.X.  That is due to the fact that all the numbers will be drawn in a vertical column.

case ROUND2PENH
snum=0
ri.X=0
ri.Y=0
ri.Width=500
ri.Height=500
for x= 250 to 290 step 4
GdipMeasureString(pGraphics,tout, wsprintfW(tout, L"%d", snum), pFont, &ri, pStringFormat,&rx,0,0)
rt.X = (COSD(x)*210.0 -(rx.Width/2.0))+100.0
rt.Y = (100 -(rx.Height/2.0) )+2
rt.Width = rx.Width
rt.Height = rx.Height
GdipDrawString(pGraphics,tout, len(tout), pFont, &rt, pStringFormat, pBrush)
snum++
next x


And finally, with the above we don't use the SIND function because we want the numbers to be drawn in a horizontal row.

That completes our scale number drawing segment. We'll now address the gage titles with the following starting structure:

'draw gage titles
select #g_dat.sStyle
case ROUND2PENV

case ROUND2PENH

default

endselect


The two ROUND2PEN types are exceptions because we have two separate instances of the title preceded by numbers.  That results in a total of four text items we need to draw.

case ROUND2PENV
'left side
ri.X=0
ri.Y=0
ri.Width=500
ri.Height=500
tout =L"#1"
GdipMeasureString(pGraphics,tout, len(tout), pFont, &ri, pStringFormat,&rx,0,0)
rt.X = 50 -rx.Width/2.0
rt.Y = 56
rt.Width = rx.Width
rt.Height = rx.Height
GdipDrawString(pGraphics,tout, len(tout), pFont, &rt, pStringFormat, pBrush)

tout =S2W(#g_dat.sName)
GdipMeasureString(pGraphics,tout, len(tout), pFont, &ri, pStringFormat,&rx,0,0)
rt.X = 50 -rx.Width/2.0
rt.Y = 70
rt.Width = rx.Width
rt.Height = rx.Height
GdipDrawString(pGraphics,tout, len(tout), pFont, &rt, pStringFormat, pBrush)

'right side
ri.X=0
ri.Y=0
ri.Width=500
ri.Height=500
tout =L"#2"
GdipMeasureString(pGraphics,tout, len(tout), pFont, &ri, pStringFormat,&rx,0,0)
rt.X = 150 -rx.Width/2.0
rt.Y = 56
rt.Width = rx.Width
rt.Height = rx.Height
GdipDrawString(pGraphics,tout, len(tout), pFont, &rt, pStringFormat, pBrush)

tout =S2W(#g_dat.sName)
GdipMeasureString(pGraphics,tout, len(tout), pFont, &ri, pStringFormat,&rx,0,0)
rt.X = 150 -rx.Width/2.0
rt.Y = 70
rt.Width = rx.Width
rt.Height = rx.Height
GdipDrawString(pGraphics,tout, len(tout), pFont, &rt, pStringFormat, pBrush)


In the above we first draw the number on the left side and then we draw the title on the left side.  We then repeat the process for the right side of the gage.  Notice a couple of things here. First, there is no requirement for a FOR loop. Second, we are setting the value of tout directly instead of indirectly as we did when we were using the snum variable. Third, since we are setting tout directly we can use the LEN(tout) function as our third parameter to the two GDI+ string functions. And finally, there are no calculations required for the containing rectangles location other than for the centering adjustment of the text.

For the ROUND2PENH we have:

case ROUND2PENH
'top side
ri.X=0
ri.Y=0
ri.Width=500
ri.Height=500
tout =L"#1"
GdipMeasureString(pGraphics,tout, len(tout), pFont, &ri, pStringFormat,&rx,0,0)
rt.X = 100 -rx.Width/2.0
rt.Y = 36
rt.Width = rx.Width
rt.Height = rx.Height
GdipDrawString(pGraphics,tout, len(tout), pFont, &rt, pStringFormat, pBrush)

tout =S2W(#g_dat.sName)
GdipMeasureString(pGraphics,tout, len(tout), pFont, &ri, pStringFormat,&rx,0,0)
rt.X = 100 -rx.Width/2.0
rt.Y = 50
rt.Width = rx.Width
rt.Height = rx.Height
GdipDrawString(pGraphics,tout, len(tout), pFont, &rt, pStringFormat, pBrush)

'bottom side
ri.X=0
ri.Y=0
ri.Width=500
ri.Height=500
tout =L"#2"
GdipMeasureString(pGraphics,tout, len(tout), pFont, &ri, pStringFormat,&rx,0,0)
rt.X = 100 -rx.Width/2.0
rt.Y = 120
rt.Width = rx.Width
rt.Height = rx.Height
GdipDrawString(pGraphics,tout, len(tout), pFont, &rt, pStringFormat, pBrush)

tout =S2W(#g_dat.sName)
GdipMeasureString(pGraphics,tout, len(tout), pFont, &ri, pStringFormat,&rx,0,0)
rt.X = 100 -rx.Width/2.0
rt.Y = 134
rt.Width = rx.Width
rt.Height = rx.Height
GdipDrawString(pGraphics,tout, len(tout), pFont, &rt, pStringFormat, pBrush)


With this block we simply duplicate the previous and change left and right to top and bottom.  And we change the location of the text rectangle.

Following is the default case:

default
ri.X=0
ri.Y=0
ri.Width=500
ri.Height=500
tout =S2W(#g_dat.sName)
GdipMeasureString(pGraphics,tout, len(tout), pFont, &ri, pStringFormat,&rx,0,0)
rt.X = 100 -rx.Width/2.0
rt.Y = 65
rt.Width = rx.Width
rt.Height = rx.Height
GdipDrawString(pGraphics,tout, len(tout), pFont, &rt, pStringFormat, pBrush)


A single draw setup that draws the text slightly above the center of the gage.  This completes the segment for the gage titles.

The last categories of text we need to draw are units of measure and range multiplier(where applicable).  The following is the structure we start with:

'draw range multipliers and units
select #g_dat.sStyle
default

case ROUNDCLOCK12

case ROUNDCLOCK24

case ROUND2PENV

case ROUND2PENH

endselect


We'll start with the default case:

default
if #g_dat.nDialMulti > 0
tout =S2W("x"+LTRIM$(STR$(#g_dat.nDialMulti))+" "+#g_dat.sMultiUnits)
GdipMeasureString(pGraphics,tout, len(tout), pFont, &ri, pStringFormat,&rx,0,0)
rt.X = 100 -rx.Width/2.0
rt.Y = 120
rt.Width = rx.Width
rt.Height = rx.Height
GdipDrawString(pGraphics,tout, len(tout), pFont, &rt, pStringFormat, pBrush)
else
tout =S2W(#g_dat.sMultiUnits)
GdipMeasureString(pGraphics,tout, len(tout), pFont, &ri, pStringFormat,&rx,0,0)
rt.X = 100 -rx.Width/2.0
rt.Y = 120
rt.Width = rx.Width
rt.Height = rx.Height
GdipDrawString(pGraphics,tout, len(tout), pFont, &rt, pStringFormat, pBrush)
endif


The default case contains an IF statement to see if the user has configured a multiplier for our gage scale.  If the multiplier is non zero then we prepend the corresponding string to our text ; otherwise we draw just the unit of measure.  The rest of the contents of the text drawing blocks should be self explanatory at this point.

Next are the two clocks.  There is no need to draw any units of measure (hours/minutes) on the clock faces. 

As should be expected the ROUND2PENV type needs to address both portions of the gage.

case ROUND2PENV
'left side
tout =S2W(#g_dat.sMultiUnits)
GdipMeasureString(pGraphics,tout, len(tout), pFont, &ri, pStringFormat,&rx,0,0)
rt.X = 50 -rx.Width/2.0
rt.Y = 115
rt.Width = rx.Width
rt.Height = rx.Height
GdipDrawString(pGraphics,tout, len(tout), pFont, &rt, pStringFormat, pBrush)
'right side
tout =S2W(#g_dat.sMultiUnits)
GdipMeasureString(pGraphics,tout, len(tout), pFont, &ri, pStringFormat,&rx,0,0)
rt.X = 150 -rx.Width/2.0
rt.Y = 115
rt.Width = rx.Width
rt.Height = rx.Height
GdipDrawString(pGraphics,tout, len(tout), pFont, &rt, pStringFormat, pBrush)


And it follows that the ROUND2PENH type follows the same pattern for its two portions.

case ROUND2PENH
'top side
tout =S2W(#g_dat.sMultiUnits)
GdipMeasureString(pGraphics,tout, len(tout), pFont, &ri, pStringFormat,&rx,0,0)
rt.X = 100 -rx.Width/2.0
rt.Y = 65
rt.Width = rx.Width
rt.Height = rx.Height
GdipDrawString(pGraphics,tout, len(tout), pFont, &rt, pStringFormat, pBrush)
'bottom side
tout =S2W(#g_dat.sMultiUnits)
GdipMeasureString(pGraphics,tout, len(tout), pFont, &ri, pStringFormat,&rx,0,0)
rt.X = 100 -rx.Width/2.0
rt.Y = 150
rt.Width = rx.Width
rt.Height = rx.Height
GdipDrawString(pGraphics,tout, len(tout), pFont, &rt, pStringFormat, pBrush)


That completes all the drawing that we need for the gage backgrounds.  We now need to do our necessary cleanup.  We'll add the following:

'gdi+ text cleanup
'delete pFont
if pFont then GdipDeleteFont(pFont)
'delete pBrush
IF pBrush THEN GdipDeleteBrush(pBrush)
'delete pStringFormat
if pStringFormat then GdipDeleteStringFormat(pStringFormat)


This goes right before:

GdipDeleteGraphics(pGraphics)
endif
ENDSUB


___________________________

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