Working with Device Contexts in MFC

If you are new to MFC and interested in graphic operations, you probably need a structured tutorial to teach you how do to at least basic operations. This article, although not a complete MFC graphics tutorial, is intended to provide you with the core information to allow you to do (at least) simple painting operations.

Device Contexts

Any window has an associated device context that is used for painting. Such a device context is an object that on one hand defines a set of other graphic objects, together with their attributes; and on the other hand, defines the graphic modes that affect the output. The set of graphic objects that I mentioned include:

  • a pen: Used to draw lines
  • a brush: Used to paint and fill
  • a bitmap: Used to copy or scroll parts of the screen
  • a font: Used to draw text
  • a palette: Used to define the set of available colors
  • a region: Used for clipping and other operations
  • a path: Used for painting and drawing operations

You can think of this device context as a physical canvas on which you draw lines, shapes, or you write text. This "canvas" can hold one object of a type at a time. If you want to draw a line, you use the pen you want (solid red). If you want to draw a circle, you need a brush; select it before you draw the circle. Until a new selection is made, the last selected object (pen, brush, bitmap, font, and so forth) remains valid and all consecutive operations requiring an object of that type use it. Because of that, you should always use them like this:

  1. Select the object you need for the current operation and save the old selected object.
  2. Perform the operation.
  3. Select back the old object.

MFC provides a class called CDC that encapsulates a HDC, which is a handle to a Windows device context object. All operations with a device context should be made through such such a CDC object. This class provides functions for selecting objects, drawing lines, shapes (ellipses, polygons, rectangles), setting drawing attributes, working with regions, clipping, drawing texts, and so on.

In addition, MFC also provides several specialized classes, derived from CDC for handling device contexts:

  • CPaintDC: Handles the calls to BeginPaint() (in constructor) and EndPaint() (in destructor); when you handle the WM_PAINT message and need to paint a window, you have to call BeginPaint() to prepare the window for painting and filling a structure with information about the painting, and EndPaint() when painting is done. Basically, you need to do this:

    void CSomeWindow::OnPaint() 
    {
       PAINTSTRUCT ps;
       CDC* pDC = BeginPaint(&ps);
    
       // do the painting with pDC
    
       EndPaint(&ps);
    }
    

    With CPaintDC, all you have to do is this:

    void CSomeWindow::OnPaint() 
    {
       CPaintDC dc(this);
    
       // do the painting with dc
    }
    
  • CClientDC: Handles the device context associated with the client area of a window, by calling GetDC() in the constructor and ReleaseDC() in the destructor. If you want to draw in the client area, you have to call those functions like this:

    void CSomeWindow::DrawClient()
    {
       // acquire the device context associated with the
       // client area
       CDC* pDC = GetDC();
    
       // draw on the client area
    
       // release the client area
       ReleaseDC(pDC);
    }
    

    You must make sure to call ReleaseDC() for each call to GetDC(). To simplify the operations, you can use CClientDC:

    void CSomeWindow::DrawClient()
    {
       CClientDC dc(this);
    
       // draw on the client area
    }
    
  • CWindowDC: Manages a display context associated with an entire window, including its frame and controls. It calls GetWindowDC() in the constructor (to retreive a device context for the entire window) and ReleaseDC() in the destructor.

  • CMetaFileDC: Associates a device context with a metafile.

In this tutorial, you will use only CPaintDC.

Graphic Objects

MFC also provides a hierarchy of GDI (graphical device interface) objects: bitmaps, palletes, brushes, pens, regions, and fonts. CGdiObject, an abstract class, is the base of this hierarchy. It encapsulates a HANDLE that contains an attached object of type HBITMAP, HPALETTE, HBRUSH, HPEN, HRGN, or HFONT. The classes derived from this base class are:

  • CBitmap: Manipulates bitmaps
  • CPallete: Encapsulates a Windows pallete
  • CBrush: Encapsulates a brush; used for painting and filling
  • CPen: Encapsulates a pen; used for drawing lines
  • CRgn: Encapsulates a region (elliptical or polygonal area within a window); used for clipping
  • CFont: Encapsulates a font; used for drawing text

In the following pages, you will see how to use pens, brushes, bitmaps, and fonts to draw lines, shapes, images, or text.

Working with Device Contexts in MFC

Using Pens

A pen is a graphical object used to draw lines. You can create several types of pens:

  • solid (PS_SOLID)
  • dashed (PS_DASH)
  • dotted (PS_DOT)
  • dash + dot (PS_DASHDOT): Alternates dashes and dots
  • dash + 2 dots (PS_DASHDOTDOT): Alternates one dash with two dots
  • cosmetic (PS_COSMETIC): Has three attributes: width (in device units), style, and color
  • geometric (PS_GEOMETRIC): Has all the attributes of a cosmetic pen and pattern, optional hatch, end style, and join style

The first example shows how to create a simple solid red pen that's 1 pixel wide and draw a rectangle with it (by joining 4 lines). Notice that, to select the object, a call to SelectObject() is made. This can take any type of GDI object, and returns a pointer to the old object of the same type, that it just replaced. You must keep this and restore it when you no longer need the new object.

void CChildView::OnPaint()
{
   CPaintDC dc(this);

   CRect rc;
   GetClientRect(rc);
   rc.DeflateRect(50, 50, 50, 50);

   // create a solid, red, 1 pixel pen
   CPen pen(PS_SOLID, 1, RGB(255, 0, 0));
   // select the pen into the device context
   CPen* oldpen = (CPen*)dc.SelectObject(&pen);

   // draw a rectangle
   dc.MoveTo(rc.left, rc.top);
   dc.LineTo(rc.right, rc.top);
   dc.LineTo(rc.right, rc.bottom);
   dc.LineTo(rc.left, rc.bottom);
   dc.LineTo(rc.left, rc.top);

   // select the old pen back
   dc.SelectObject(oldpen);
}

[pen_1.png]

The second example shows how to create other types of pens. For all the other pens except the solid pen, only the width of 1 device unit is valid. Setting any other value will create a solid pen.

void CChildView::OnPaint()
{
   CPaintDC dc(this);

   CRect rc;
   GetClientRect(rc);

   rc.DeflateRect(50, 50, 50, 50);

   // create a solid, red, 1 pixel pen
   CPen pen_solid      (PS_SOLID,      1, RGB(255, 0, 0));
   CPen pen_dash       (PS_DASH,       1, RGB(255, 0, 0));
   CPen pen_dot        (PS_DOT,        1, RGB(255, 0, 0));
   CPen pen_dashdot    (PS_DASHDOT,    1, RGB(255, 0, 0));
   CPen pen_dashdotdot (PS_DASHDOTDOT, 1, RGB(255, 0, 0));

   // select the pen into the device context
   CPen* oldpen = (CPen*)dc.SelectObject(&pen_solid);
   dc.MoveTo(rc.left, rc.top);
   dc.LineTo(rc.right, rc.top);

   dc.SelectObject(&pen_dash);
   dc.MoveTo(rc.left,  rc.top+25);
   dc.LineTo(rc.right, rc.top+25);

   dc.SelectObject(&pen_dot);
   dc.MoveTo(rc.left,  rc.top+50);
   dc.LineTo(rc.right, rc.top+50);

   dc.SelectObject(&pen_dashdot);
   dc.MoveTo(rc.left,  rc.top+75);
   dc.LineTo(rc.right, rc.top+75);

   dc.SelectObject(&pen_dashdotdot);
   dc.MoveTo(rc.left,  rc.top+100);
   dc.LineTo(rc.right, rc.top+100);

   // select the old pen back
   dc.SelectObject(oldpen);
}

[pen_2.png]

In this last example, you create a geometric pen, alternating dashes and dots, having 5 pixels width and rounded end caps.

void CChildView::OnPaint()
{
   CPaintDC dc(this);

   CRect rc;
   GetClientRect(rc);
   rc.DeflateRect(50, 50, 50, 50);

   // compute the width for the pen in device units
   int width = -MulDiv(5, dc.GetDeviceCaps(LOGPIXELSY), 72);
   // create a geometric pen
   LOGBRUSH logBrush;
   logBrush.lbStyle = BS_SOLID;
   logBrush.lbColor = RGB(255, 0, 0);
   CPen pen(PS_DASHDOT|PS_GEOMETRIC|PS_ENDCAP_ROUND, width,
      &logBrush);

   // select the pen into the device context
   CPen* oldpen = (CPen*)dc.SelectObject(&pen);

   // draw a rectangle
   dc.MoveTo(rc.left, rc.top);
   dc.LineTo(rc.right, rc.top);
   dc.LineTo(rc.right, rc.bottom);
   dc.LineTo(rc.left, rc.bottom);
   dc.LineTo(rc.left, rc.top);

   // select the old pen back
   dc.SelectObject(oldpen);
}

[pen_3.png]

Working with Device Contexts in MFC

Using Brushes

A brush is used to paint or fill. You can create several types of brushes:

  • Solid, from a specified color (CreateSolidBrush and CreateSysColorBrush)
  • Pattern, from a bitmap (CreatePatternBrush)
  • DIB pattern, from a device independent bitmap (CreateDIBPatternBrush)
  • Hatched, with a given pattern and color (CreateHatchBrush)
    • Downward hatch (left to right) at 45 degrees (HS_BDIAGONAL)
    • Upward hatch (left to right) at 45 degrees (HS_FDIAGONAL)
    • Horizontal and vertical crosshatch (HS_CROSS)
    • Crosshatch at 45 degrees (HS_DIAGCROSS)
    • Horizontal hatch (HS_HORIZONTAL)
    • Vertical hatch (HS_VERTICAL)
  • Any of the above are created from a LOGBRUSH structure (CreateBrushIndirect).

The first example shows how to draw a rectangle with a red, solid brush. Method Rectangle() requires the brush to be selected in the device context. It also draws margin around the rectangle, using the currently selected pen (by default it's 1 pixel, solid, and black).

void CChildView::OnPaint()
{
   CPaintDC dc(this);

   CRect rc;
   GetClientRect(rc);
   rc.DeflateRect(50, 50, 50, 50);

   // create a solid red brush
   CBrush br(RGB(255, 0, 0));
   // select the brush into the device context
   CBrush* oldbrush = dc.SelectObject(&br);

   // draw a rectangle using the current brush for filling and
   // current pen for border
   dc.Rectangle(rc);

   // select back the old brush
   dc.SelectObject(oldbrush);
}

[brush_1.png]

The second example shows how to create a solid brush from a COLORREF color or from a system color (COLOR_BTNFACE). Method FillRect draws a rectangle using the provided brush (and does not require it to be previously selected into the CDC object). Another differece from Rectangle() is that it does not draw a margin around the rectangle.

void CChildView::OnPaint()
{
   CPaintDC dc(this);

   CRect rc;
   GetClientRect(rc);

   // create a red brush
   CBrush br1;
   br1.CreateSolidBrush(RGB(255, 0, 0));

   // create a brush with a system color
   CBrush br2;
   br2.CreateSysColorBrush(COLOR_BTNFACE);

   // fill a left rect with red brush
   CRect rc1 = rc;
   rc1.DeflateRect(50, 50, rc.Width()/2+10, 50);
   dc.FillRect(&rc1, &br1);

   // fill a right rect with system color brush
   CRect rc2 = rc;
   rc2.DeflateRect(rc.Width()/2+10, 50, 50, 50);
   dc.FillRect(&rc2, &br2);
}

[brush_2.png]

Filling with a pattern requires the use of a bitmap. You can load a bitmap from the resources and create a brush with it. Method RoundRect() draws a rectangle with rounded margins. This example uses a black and white tiles pattern.

void CChildView::OnPaint()
{
   CPaintDC dc(this);

   CRect rc;
   GetClientRect(rc);
   rc.DeflateRect(50, 50, 50, 50);

   CBitmap bmp;
   bmp.LoadBitmap(IDB_PATTERN);

   // Create a pattern brush from the bitmap.
   CBrush brush;
   brush.CreatePatternBrush(&bmp);

   // Select the brush into a device context, and draw.
   CBrush* pOldBrush = (CBrush*)dc.SelectObject(&brush);

   // draw a rounded rectangle
   dc.RoundRect(rc, CPoint(10,10));

   // Restore the original brush.
   dc.SelectObject(pOldBrush);
}

[brush_3.png]

As enumerated at the beginning of this section, you can create six types of hatched brushes. Below is a sample that shows how to use them all.

void CChildView::OnPaint()
{
   CPaintDC dc(this);

   CRect rc;
   GetClientRect(rc);
   rc.DeflateRect(10, 10, 10, 10);

   CBrush brush1;
   brush1.CreateHatchBrush(HS_BDIAGONAL, RGB(255, 0, 0));
   dc.FillRect(
      CRect(rc.left, rc.top, rc.Width()/2-10, rc.Height()/3-5),
      &brush1);

   CBrush brush2;
   brush2.CreateHatchBrush(HS_FDIAGONAL, RGB(255, 0, 0));
   dc.FillRect(
      CRect(rc.Width()/2+10, rc.top, rc.right, rc.Height()/3-5),
      &brush2);

   CBrush brush3;
   brush3.CreateHatchBrush(HS_DIAGCROSS, RGB(255, 0, 0));
   dc.FillRect(
      CRect(rc.left, rc.Height()/3+5, rc.Width()/2-10,
            2*rc.Height()/3-5),
      &brush3);

   CBrush brush4;
   brush4.CreateHatchBrush(HS_CROSS, RGB(255, 0, 0));
   dc.FillRect(
      CRect(rc.Width()/2+10, rc.Height()/3+5, rc.right,
            2*rc.Height()/3-5),
      &brush4);

   CBrush brush5;
   brush5.CreateHatchBrush(HS_HORIZONTAL, RGB(255, 0, 0));
   dc.FillRect(
      CRect(rc.left, 2*rc.Height()/3+5, rc.Width()/2-10,
            rc.bottom),
      &brush5);

   CBrush brush6;
   brush6.CreateHatchBrush(HS_VERTICAL, RGB(255, 0, 0));
   dc.FillRect(
      CRect(rc.Width()/2+10, 2*rc.Height()/3+5, rc.right,
            rc.bottom),
      &brush6);
}

[brush_4.png]

Working with Device Contexts in MFC

Using Bitmaps

Bitmaps are used to copy or scroll parts of the screen. Class CBitmap encapsulates a bitmap object and provides functionality for manipulating the bitmap.

This first example shows how to draw a bitmap on the device context of a window. There are several steps to follow:

  1. Load the bitmap from the resources.
  2. You can use a BITMAP object to retrieve information about the bitmap (such as width and height).
  3. Creatie an in-memory device context, compatible with the painting device context (see CreateCompatibleDC).
  4. Select the bitmap into this in-memory device context.
  5. Copy a portion of the in-memory device context to the painting device context (see BitBlt).
void CChildView::OnPaint()
{
   CPaintDC dc(this);

   CRect rc;
   GetClientRect(rc);

   // load the bitmap from the resources
   CBitmap bmp;
   bmp.LoadBitmap(IDB_MARIUS);

   // get information about the bitmap
   BITMAP tagbmp;
   bmp.GetBitmap(&tagbmp);

   // create a in-memory device context,
   // compatible with the painting device context
   CDC memDC;
   memDC.CreateCompatibleDC(&dc);

   // select the loaded bitmap into the in-memory device context
   CBitmap* oldbitmap = (CBitmap*)memDC.SelectObject(&bmp);

   //copy the bitmap from the in-memory DC to the painting DC
   dc.BitBlt(
      // position on the destination: center
      (rc.Width() - tagbmp.bmWidth)/2, (rc.Height()
                  - tagbmp.bmHeight)/2,
      // height of the bitmap to copy: same as loaded bitmap
      tagbmp.bmWidth, tagbmp.bmHeight,
      // source device context: in-memory DC
      &memDC,
      // position from the source: top-left corner
      0, 0,
      // raster operation to perform: direct copy
      SRCCOPY);

   // select the old rectangle back
   memDC.SelectObject(oldbitmap);
}

[bitmap_1.png]

You also can create a bitmap programatically, with CreateBitmap(). This example shows how to create a 32-bit bitmap, with 1 pixel width and 255 pixels height. The color for each pixels start from RGB(255, 255, 255) and end at RGB(0, 0, 0). This is used to create a gradient pattern brush for filling a rectangle.

void CChildView::OnPaint()
{
   CPaintDC dc(this);

   CRect rc;
   GetClientRect(rc);
   rc.DeflateRect(50, 50, 50, 50);

   // creat an array of 255 colors
   int segments = 255;
   DWORD* dwBits = new DWORD[segments];
   for(int i = 0; i < segments; ++i)
   {
      dwBits[i] = (DWORD)RGB(0xFF - i, 0xFF - i, 0xFF - i);
   };

   // create a 32-bit bitmap of 1 pixel width and 255 pixels
   // height
   HBITMAP hBitmap = ::CreateBitmap(1, segments, 1, 32, dwBits);
   delete [] dwBits;

   // create a pattern brush
   CBrush brush;
   brush.CreatePatternBrush(CBitmap::FromHandle(hBitmap));

   CBrush* oldbrush = (CBrush*)dc.SelectObject(&brush);

   // draw a rectangle with the gradient pattern brush
   dc.Rectangle(rc);

   dc.SelectObject(oldbrush);
}

[bitmap_2.png]

Working with Device Contexts in MFC

Using Fonts

Class CFont encapsulates a font and is used to manipulate fonts. Fonts are used for drawing texts. Class CDC has two methods for drawing text:

  • DrawText: Formats and draws text in a given rectangle
  • DrawTextEx: Provides additional formatting options

This sample creates an Arial font, of 16 pixels height (the value specified to CreateFont is in logical units, so a transformation must be performed from pixels to logical units). This font is used to draw a single-line text in the middle (vertical and horizontal) of the client area of the window.

void CChildView::OnPaint()
{
   CPaintDC dc(this);

   CRect rc;
   GetClientRect(rc);

   // compute the hight in logical units
   int height = -MulDiv(16, dc.GetDeviceCaps(LOGPIXELSY), 72);

   // create the font
   CFont font;
   font.CreateFont(
      height,                    // nHeight
      0,                         // nWidth
      0,                         // nEscapement
      0,                         // nOrientation
      FW_NORMAL,                 // nWeight
      FALSE,                     // bItalic
      FALSE,                     // bUnderline
      0,                         // cStrikeOut
      ANSI_CHARSET,              // nCharSet
      OUT_DEFAULT_PRECIS,        // nOutPrecision
      CLIP_DEFAULT_PRECIS,       // nClipPrecision
      DEFAULT_QUALITY,           // nQuality
      DEFAULT_PITCH | FF_SWISS,  // nPitchAndFamily
      _T("Arial"));

   // select the font into the device context
   CFont* oldfont = (CFont*)dc.SelectObject(&font);

   // set the color to red
   COLORREF oldcolor = dc.SetTextColor(RGB(255, 0, 0));

   // draw the text
   dc.DrawText(
      // text
      _T("This is a sample text"),
      // rectangle in which to draw the text
      rc,
      // center the text in the middle of the rectangle
      DT_CENTER|DT_VCENTER|DT_SINGLELINE);

   // select the old font
   dc.SelectObject(oldfont);
}

[font_1.png]

Where to Do Printing

If you are doing painting in a dialog, do it in the OnPaint() method (called by the framework when Windows or an application makes a request to repaint a portion of an application's window). If you are using the doc/view architecture and want to do painting in the view, do it in the OnDraw() method (called by the framework to perform screen display, printing, and print preview, with a different device context in each case). Please refer to MSDN for more information about these methods.

Where to Do Painting

If you have to do custom painting for a dialog, you should do that in OnPaint(), which is called by the framework when Windows or an application makes a request to repaint a portion of the window, in this case the dialog (you should make sure that kind of painting happens only when the window is not minimized—see IsIconic()). If you have an SDI or MDI application and need to do painting for a view, you should do it in OnDraw(), which is called by the framework to render an image of the document. Please refer to MSDN for more information about these methods.

Conclusions

This article provided information about basic graphic operations in MFC, such as drawing lines, filling shapes with a brush, displaying a bitmap, or drawing text. Hopefully, this can be a good start for you. In a future article, you will see more advanced operations.



About the Author

Marius Bancila

Marius Bancila is a Microsoft MVP for VC++. He works as a software developer for a Norwegian-based company. He is mainly focused on building desktop applications with MFC and VC#. He keeps a blog at www.mariusbancila.ro/blog, focused on Windows programming. He is the co-founder of codexpert.ro, a community for Romanian C++/VC++ programmers.

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Live Event Date: August 20, 2014 @ 1:00 p.m. ET / 10:00 a.m. PT When you look at natural user interfaces as a developer, it isn't just fun and games. There are some very serious, real-world usage models of how things can help make the world a better place – things like Intel® RealSense™ technology. Check out this upcoming eSeminar and join the panel of experts, both from inside and outside of Intel, as they discuss how natural user interfaces will likely be getting adopted in a wide variety …

  • Java developers know that testing code changes can be a huge pain, and waiting for an application to redeploy after a code fix can take an eternity. Wouldn't it be great if you could see your code changes immediately, fine-tune, debug, explore and deploy code without waiting for ages? In this white paper, find out how that's possible with a Java plugin that drastically changes the way you develop, test and run Java applications. Discover the advantages of this plugin, and the changes you can expect to see …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds