Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

GDI Helper Classes


January 2000/GDI Helper Classes


How many times have you written code like this:

void Draw(CDC * pDC)
{
    CPen NewPen;
    NewPen.CreatePen(PS_SOLID,1,RGB(255,0,0));
    CPen * pOldPen=pDC->SelectObject(&NewPen);
    // draw with your pen
    // repeat the above for brushes, fonts, ...
    pDC->SelectObject(pOldPen); // Restore previous pen
}

If you don’t use MFC, you probably used a similar method:

void Draw(HDC hDC)
{
    HPEN hNewPen=CreatePen(PS_SOLID,1,RGB(255,0,0));
    HPEN hOldPen=SelectObject(hDC,hNewPen);   
    // draw with your pen
    // repeat the above for brushes, fonts, ...
    SelectObject(hDC,hOldPen);
    DeleteObject(hNewpen);
}

Most C/C++ Windows programs are probably full of code like this. What is the problem with this code? This code has several problems:

1) Verbosity. You must write four lines of code just to select a simple pen into the device context. Then you must repeat this approach for fonts, bitmaps, brushes, etc.

2) Errors. You must remember to select the old pen into the device context before leaving your function and perhaps destroy the new pen, too, if you use the Win32 API directly. Besides, in the presence of exceptions or of multiple return statements, you must go out of your way to ensure that each execution path in your function will end up selecting the original pen into the device context. In the preceding examples, if the commented area contains a return statement or encounters an exception, you must remember to call SelectObject() with the original pen and perhaps call DeleteObject().

3) Complexity. You have to pass many parameters to the functions that create pens, fonts, brushes, bitmaps etc., whereas C++ default arguments could help you by passing many of those parameters for you. (MFC does use some default arguments, but not often in members like CPen::CreatePen() and CFont::CreateFont().)

Wouldn’t it be nice if you could write MFC code like this instead:

void Draw(CDC * pDC)
{
    CSelPen SelPen(pDC,RGB(255,0,0)); // One line!
    // draw with your pen
    // the original pen will be selected
    // back in the DC automatically
}

or non-MFC code like this:

void Draw(HDC hDC)
{
    CSelPen SelPen(hDC,RGB(255,0,0)); // One line!
    // draw with your pen
    // the original pen will be selected
    // back in the DC automatically
}

The previous code has the following advantages:

1) Conciseness. One line of code creates your pen, selects it into the device context, deselects it when you are finished, and destroys the pen if necessary.

2) Robustness. However you exit your function, the original pen will be selected back into the device context by the destructor of the CSelPen object, even in the presence of exceptions or of multiple return statements.

3) Ease of use. Many default arguments are passed to the pen’s creation function for you. Many member functions, e.g., all those to create brushes, are overloaded to use different sets of arguments.

The classes presented in this article let you write code as in the previous example. I will show an example with the CSelPen class, but I have created similar classes for brushes (CSelBrush), fonts (CSelFont), bitmaps (CSelBitmap), palettes (CSelPalette), and stock objects (CSelStock). To be diligent and leave your device context exactly as you found it, you can also use classes for selecting ROP2 mode (CSelROP2), background mode (CSelBkMode), background color (CSelBkColor), text color (CSelTextColor), text align mode (CSelTextAlign), and map mode (CSelMapMode). These classes are made up only of small inline functions, so they should really be fast and easy to understand. The only price you pay is that each instance of these classes contains a variable storing the device context (it also stores the previous GDI object selected in the device context, but you would need to do the same even without these classes), so it uses an extra four bytes on the stack. Big deal. Figure 1 shows how I implemented a CSelPen class like the one used in the previous examples.

I use MFC in most of my Windows programs, so I created the above classes for MFC, but I also made a version to be used in straight Win32 API programs without MFC. The MFC version is in file DRAWGDIX.H, and the straight Win32 version is in DRAWGDIW.H. The classes are named the same way, so you cannot use both versions together (unless you rename some of them). You can download both header files from this month’s code archive, together with two sample applications that use them. There is no accompanying implementation file since all the code is inline.

Brushes

Figure 2 contains the CSelBrush class. As you can see, it’s very similar to the CSelPen class. The main difference lies in the fact that whereas there is only one way to create a pen (CreatePen()), there are many ways to create a brush (CreateSolidBrush(), CreateHatchBrush(), CreatePatternedBrush(), and CreateDIBPatternBrush()). CSelBrush has overloaded constructors and overloaded Select() functions that differ only in the number and type of arguments and that allow the creation of all these types of different brushes. But if you feel more comfortable with the original function names, no one is stopping you from renaming my Select() functions into SelectSolidBrush(), etc. I use Select() because I have a Select() member function to match each possible constructor, with the arguments in the same order (except for the fact that Select() does not have the first argument, the device context pointer, which was set by the constructor).

Fonts

Figure 3 illustrates my implementation of CSelFont, which handles creating a font, selecting it into the device context, deselecting it later, and destroying the font. Creating a font is usually cumbersome; you must fill out the many fields in a LOGFONT structure to specify what kind of font you want. You can try to be a bit more concise by using CFont::CreateFont() instead of CFont::CreateFontIndirect(), but you would still have to pass 14 arguments and use constants such as FW_BOLD or FW_NORMAL, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY or DEFAULT_QUALITY, FF_MODERN or FF_SWISS, FIXED_PITCH or VARIABLE_PITCH, and many more. I’d like to meet the programmer who can create a font this way without using copy and paste from somewhere else and without opening up the online help or a book! You could also use memset() or ZeroMemory() to avoid specifying some options, but I don’t think that is a nice solution because it depends on the actual values of the constants used.

After including my inline classes, here is the constructor I use to create and select a font:

CSelFont(CDC * pDC,    int size,         LPCSTR face=NULL,
         BOOL bold=0,  BOOL italic=0,    BOOL underlined=0,
         BOOL fixed=0, BOOL hiquality=0, int angledegrees=0);

As you can see, the only necessary argument (besides the device context) is the size. All the rest have defaults. And to choose between normal and bold, you don’t have to remember FW_NORMAL and FW_BOLD; you just pass TRUE or FALSE to a variable named bold. A similar method is used to select DEFAULT_QUALITY or PROOF_QUALITY, FIXED_PITCH or VARIABLE_PITCH, etc. Besides, when you define a font height, you usually want to use a point height (height in 72nds of an inch), not a height in logical coordinates, so my member functions automatically calculate the height with the following call:

-MulDiv(size,pDC->GetDeviceCaps(LOGPIXELSY),72)

to convert point height to device height and then call DPtoLP to convert device height to logical height. The following code shows how I can create a 12-point, bold Arial font in one line of code:

void DrawWithFont(CDC * pDC)
{
    CSelFont MyFont(pDC,12,"Arial",TRUE);
    // ...use the font!
}

Now, I’m not saying this will change your life, but it’s 20 times more concise and is less error-prone. It can be argued that not all possible fonts can be created this way (where can you specify FF_ROMAN?), but that’s because I used my own font creation preferences and defaults. You can easily create your own options and defaults to let you create any font whatsoever. It can also be argued that my constructors, like all constructors, don’t let you return an error code, so I just call GDI functions inside a VERIFY macro. This never gave me problems; if you can’t create a simple pen, chances are you have a bigger problem than not knowing that the pen was not created. The Select() functions can easily be modified to return FALSE if they fail. Had any of them asserted in my face in the last few years, I would have done so.

Creating fonts this way was so convenient that I also created a new class, CMyFont inherited from CFont. CMyFont has an overloaded constructor and a MyCreateFont() member function that take the same arguments as my CSelFont class, but which you can use independently of CSelFont. This class is included in this month’s code archive.

Using the Classes

Using my classes is very easy. Here is a simple example using MFC (if you prefer the classes that don’t use MFC, just replace CDC* with HDC, and the rest of the code remains the same):

void Draw(CDC * pDC)
{
    CSelPen         Pen(pDC,RGB(255,0,0));
    CSelBrush       Brush(pDC,HS_BDIAGONAL,RGB(0,255,0));
    CSelFont        Font(pDC,18,"Arial",TRUE);
    CSelTextColor   TxtCol(pDC,RGB(0,0,255));
    CSelTextAlign   TxtAlg(pDC,TA_CENTER|TA_BASELINE);
    CSelBkMode      BkMode(pDC,TRANSPARENT);

    // Draw!
    // The DC will be restored automatically!
}

The objects above set up the device context as you want them to and will restore it upon destruction.

Sometimes, though, you won’t know which brush to create until you are in a body of code where you can’t put a CSelBrush object, for instance:

if (Condition1)
    {
    // Select Hatch Brush
    }
else
    {
    // Select Solid Brush
    }
// Use the brush

I cannot put a CSelBrush object where I know what brush to create, because the brush would be deselected by the time I exit the if statement. (The scope of the CSelBrush object would be within the braces.) For these and other occasions, you have the constructors that take only the device context as a parameter and the Select() member functions to change the GDI object later:

CSelBrush SelBrush(pDC);

if (Condition1)
    {
    SelBrush.Select(HS_BDIAGONAL,RGB(255,0,0));
    }
else
    {
    SelBrush.Select(RGB(255,0,0));
    }
// Use the brush

In this example, the constructor of the SelBrush object just stores the device context pointer for later use. The overloaded CSelBrush::Select() member functions help you in creating or selecting the appropriate brushes and can be used also if you want to change the brush repeatedly. You can use Restore() to tell the CSelBrush object to restore the device context to its previous brush before the destructor is called. Another simple member function returns the previous brush.

Until now, you have seen my classes create the GDI objects they need to select into the device context. Sometimes this will not be what you want; you might have, for instance, a CFont object m_Font as a member of one of your window classes, and you want to use that font repeatedly. There is an overloaded constructor and Select() in CSelFont just for that. For example:

void CMyWindow::OnDraw(CDC *pDC)
{
    CSelFont Font(pDC,&m_Font);
    // Use the font
}

All these constructors and select functions can be combined as you wish.

All classes, be they for brushes, pens, fonts, or map modes, work in much the same way. They are all very easy to use. Just be careful of a possible erroneous use, like the following:

CSelPen(pDC,RGB(255,0,0));
CSelBrush(pDC, HS_BDIAGONAL,RGB(255,0,0));
CSelTextColor(pDC,RGB(255,0,0));

The code will compile and run, but it is creating temporary objects that will be destroyed in the same line where they are created, leaving the device context exactly as it was before calling them. You must create named objects if you want them to stay alive until after you have used them.

Keep in mind also that each type of GDI object (pen, brush, etc.) or characteristic of the device context (text color, map mode, etc.) should be handled by only one object of the classes in each one of your functions, and you should use that object to modify the relative type of resource.

If you don’t want to fiddle with these classes and you will accept a slightly worse performance, you can just use class CSaveDC. It saves the whole device context upon creation and restores it upon destruction.

Conclusions

I created these classes a few years ago, and since then, I have never used a different method to set up device contexts. I never forget to restore the device context to its previous state; it is always done automatically by these classes. I can also create fonts without using copy and paste and without consulting the online help. This might not be a big deal, but it’s a simple solution to a simple but common problem and can reduce the number of lines and clear up the logic of many parts of your programs, increasing robustness.

Giovanni Bavestrelli lives in Milan and is a software engineer for Techint S.p.A., Castellanza, Italy. He has a degree in Electronic Engineering from the Politecnico di Milano and writes automation software for Pomini Roll Grinding machines. He has been working in C++ under Windows since 1992, specializing in the development of reusable object oriented libraries. He can be reached at [email protected].

Get Source Code


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.