Introduction
In reading the forums and answering the questions, I have found that many beginners
would like to learn the fundamental concepts of painting in WIN32 programs.
Developing applications for windows can be challenging and frustrating. it can also
be rewarding if you know some of the basic techniques that are required to take advantage
of the WIN32 operating system. This tutorial is for beginners, and will cover
the basic techniques and knowledge required to get started with painting in WIN32.
The code and the concepts are all at the SDK level, as most of the other techniques
that are used to paint to windows are based on the SDK. The WIN32 SDK is used
inside of the MFC and ATL/WTL objects that represent those frameworks. I believe the more
you know about the base technology, the SDK, the more you can take advantage of
the frameworks that are written to encapsulate the technology. In order to help developers
that are developing in MFC and WTL, the corresponding classes from those frameworks will
be explained in the appropriate sections.
This tutorial is the first in a series of five. This one is for beginners, the
next three will cover more advanced topics at the intermediate level, and the final
tutorial will cover WIN32 paint internals at the advanced level.
Device Context
At the simplest level, the device context (DC) is a surface that can be painted on.
However, the DC is also the operating system resource that coordinates
the brushes, pens and fonts that are used to render the images on the display. It is also
the layer of abstraction that the Windows Graphics Device Interface (GDI) uses to
abstract the details of a display device from a developer. This means
that it is possible to paint to the screen, a printer, or even a bitmap in memory with the
same code, initialized with a different DC that is created for a particular purpose.
This makes coding very simple compared to the development that is required to program
directly to a particular video card, or printer driver.
The key to creating solid and efficient graphics in Windows, is to know how to
create and utilize the DC that you want for a particular purpose. There are many
flavors of DCs, here is a short description of each:
Client DC
A client DC is associated with a particular window, and
will give the developer access to the client area of the target window.
This is the type of DC that is used most often by application programmers, and is
the easiest to handle. This is the type of DC that should be used when
the WM_PAINT
message is handled. This is also the only DC that will be
explained with any amount of detail.
Window DC
A window DC is associated with a particular window, and
allows the developer to draw on any part of the target window, including the
borders and the caption. This is the type of DC that is sent with WM_NCPAINT
message.
Memory DC
A Memory DC is a DC that exists only in memory, and is not
associated with any window. This is the only type of DC that
can hold a user defined bitmap (HBITMAP
). Memory DC's are very useful for
caching images, and for use with backbuffers on complicated displays.
General Device DC
For lack of a better name, this type of DC covers
all of the other devices that it is possible to get a DC from. For instance, a
printer or a plotter, the entire display monitor, or even custom device that
a cutting-edge technology company may invent that does not even exist yet. The
fact is, a DC can be created for any device that supports the required API
functions defined by Microsoft.
This guide will only demonstrate the Client DC in order to get the user started with basic
Windows graphics development. The tutorials later in this series will cover the other
types of DC.
Obtaining a Client Device Context
While working with the WIN32 Paint API, you will always obtain a handle to
a device context (HDC
). Any of the types of DCs that were described earlier can
be stored in a HDC. This guide will only describe how to obtain a client DC,
as the other DCs are used for more advanced purposes.
The frameworks represent their DCs with a class. The base class for the DC is CDC
.
A CDC
encapsulates the HDC itself. All of the functions that can be called
for a HDC are encapsulated as member functions. There are also a few classes derived from the
CDC
that will allow special DCs to be created and maintained. The
CPaintDC
and the CClientDC
will be explained later in this section.
BeginPaint
BeginPaint
is the most common way to create a DC. However, this function should
only be called in your WM_PAINT
message handler of your window. This is because BeginPaint
validates the invalid region of your window. WM_PAINT
messages are generated whenever an
invalid region is created on your window. If BeginPaint
is called outside of
the WM_PAINT
handler, any region that was previously invalid will be validated, and no
WM_PAINT
message will be generated for your window. This could cause major side-effects in your
control, especially if someone else would like to subclass your control in the future.
It is just as important to use BeginPaint
inside of the WM_PAINT
handler as it is
to not use it outside of WM_PAINT
. This is because inside of the BeginPaint
function
call, Windows may generate a WM_ERASEBKGND
message and a WM_NCPAINT
message if it is necessary. If you
do not call BeginPaint
inside of your WM_PAINT
handler, your windows borders may not
be updated properly.
In order to get a handle to a DC with BeginPaint
, you will need a handle to the
target window, and a PAINTSTRUCT
structure. PAINTSTRUCT
holds information
about the current painting session when you call BeginPaint
, that is unimportant at
this level. BeginPaint
also returns a handle to the DC that it creates, and this
is the value that we are interested in. Here is an example of how to call BeginPaint
.
PAINTSTRUCT ps;
HDC hdc;
hdc = ::BeginPaint(hWnd, &ps);
The function that needs to be used in order to free a HDC created with BeginPaint
is EndPaint
. It is important to call EndPaint
rather than one of the
other DC destroyers, because BeginPaint
calls HideCursor
in order
to prevent the cursor from being painted on during your paint operations, and EndPaint
calls ShowCursor
to make it visible again. If EndPaint
is not called,
after a BeginPaint
then you may experience some weird anomalies with the cursor.
Here is an example of EndPaint
:
::EndPaint(hWnd, &ps);
The call to BeginPaint
and EndPaint
are encapsulated in the class
CPaintDC
in MFC and WTL. By simply creating an instance of this DC on the stack,
the DC will automatically be created and destroyed for the developer. Here is the code for
the constructor and destructor in the MFC version of this class:
CPaintDC::CPaintDC(CWnd* pWnd)
{
...
if (!Attach(::BeginPaint(m_hWnd = pWnd->m_hWnd, &m_ps)))
AfxThrowResourceException();
}
CPaintDC::~CPaintDC()
{
...
::EndPaint(m_hWnd, &m_ps);
Detach();
}
GetDC / GetDCEx
GetDC
is the next most common way to create a DC. GetDC
will obtain a device context to the target windows client area. GetDCEx
will do the same thing, however it will allow you to specify a default clipping region.
GetDCEx
will be ignored until the next guide.
GetDC
has many uses outside of the WM_PAINT
handler. Use
GetDC
when a graphical effect needs to be created, that may not be part
of the permanent data of the target window. For instance, use GetDC to create the DC
for the rubber-banding effect on drawing tools, and when selecting multiple objects
in a window.
Here is an example of how to create and destroy a DC with a call to GetDC
.
HDC hdc;
hdc = GetDC(hWnd);
...
::ReleaseDC(hWnd, hdc);
The call to GetDC
and ReleaseDC
are encapsulated in the class
CClientDC
in MFC and WTL. By simply creating an instance of this DC on the stack,
the DC will automatically be created and destroyed for the developer. Here is the code for
the constructor and destructor in the MFC version of this class:
CClientDC::CClientDC(CWnd* pWnd)
{
...
if (!Attach(::GetDC(m_hWnd = pWnd->GetSafeHwnd())))
AfxThrowResourceException();
}
CClientDC::~CClientDC()
{
...
::ReleaseDC(m_hWnd, Detach());
}
Using a Device Context
Using a DC is very simple, and it can be quite complicated, it all depends on what painting
effect is to be accomplished. This guide will simply stay with the default pen and brush
that are selected in the DC when it is first created. Here is an example of how to call a number of
different GDI functions with the DC that we have created.
Rectangle(hdc, 100, 100, 200, 300);
Ellipse(hdc, 100, 100, 200, 300);
TCHAR szMessage[] = "Paint Beginner";
UINT nLen = _tcslen(szMessage);
TextOut(hdc, 100, 325, szMessage, nLen);
Here is a short example demonstrating how to use the CPaintDC
:
CPaintDC dc;
dc.Rectangle(10, 10, 150, 200);
...
Here is a short example demonstrating how to use the CClientDC
:
CClientDC dc;
dc.Rectangle(10, 10, 150, 200);
...
Getting Started
The short demo program that is provided will display an array of shapes that has been created
by the user. The user can create a shape by clicking and dragging the mouse, the same way that
objects are selected in windows explorer. The two methods for creating a client DC are demonstrated.
BeginPaint
: This function is used to paint the shapes that the user has created.
GetDC
: This function is used to create the rubberbanding effect that allows the user to drag and create the shapes.
The demo program is very simple in structure. It forsakes coding style and elegance for simplicity
and clarity. All of the state that is required for this program is stored in global variables. There
is a maximum of 5 shapes that can be created because they are stored in a statically allocated array.
If more than five shapes are created, then the oldest existing shape will be replaced with the new shape.
Here is the OnPaint handler that was created to handle the WM_PAINT
message:
LRESULT OnPaint (HWND hWnd)
{
PAINTSTRUCT ps;
HDC hdc;
hdc = ::BeginPaint(hWnd, &ps);
UINT index;
for (index = 0; index < SHAPE_COUNT; index++)
{
if (ID_SHAPE_RECTANGLE == Shapes[index].shapeID)
{
::Rectangle (
hdc,
Shapes[index].rect.left,
Shapes[index].rect.top,
Shapes[index].rect.right,
Shapes[index].rect.bottom
);
}
else
{
::Ellipse (
hdc,
Shapes[index].rect.left,
Shapes[index].rect.top,
Shapes[index].rect.right,
Shapes[index].rect.bottom
);
}
}
::EndPaint(hWnd, &ps);
return 0;
}
Notice the very simple paint structure encapsulated between the calls to BeginPaint
and
EndPaint
. More calls could be made between the BeginPaint
bracket. The same
principle would still apply, all painting for the main window should be done here.
The rubber banding effect is a little bit more complicated. This effect is created by modifying two
of the state variables in the DC. The first state that is changed is that the current drawing mode is
changed from a plain copy to a destination NOT pen, or R2_NOT
. This pen will allow the first
drawing instance of the pen to change all of the bits from display, making a line visible. By simply
Drawing the exact same line a second time, the line will disappear.
The second change in DC state, is to select an empty brush color into the DC, so that when the shape
is dragged, it does not paint the center of the shape. These two tricks are not the important point that
should be noticed from this code. The important point to notice is that the DC was
received from
a call to GetDC
and ended with a call to ReleaseDC
. Same as the drawing code,
any operations are contained in this DC creation bracket, and the window painting proceeds in between.
Here is the function that draws the rubberbanding effect:
void DrawRubberBand(HWND hWnd)
{
HDC hdc;
hdc = ::GetDC(hWnd);
::SetROP2(hdc, R2_NOT);
::SelectObject(hdc, ::GetStockObject(NULL_BRUSH));
HMENU hMenu = ::GetMenu(hWnd);
HMENU hShapeMenu = ::GetSubMenu(hMenu, 1);
if (::GetMenuState(hShapeMenu, ID_SHAPE_RECTANGLE, MF_BYCOMMAND) & MF_CHECKED)
{
::Rectangle(
hdc,
ptStart.x,
ptStart.y,
ptCurrent.x,
ptCurrent.y
);
}
else
{
::Ellipse(
hdc,
ptStart.x,
ptStart.y,
ptCurrent.x,
ptCurrent.y
);
}
::ReleaseDC(hWnd, hdc);
}
One final point that should be explained about the demo program is the call that
makes the rubber band work. This code is inside of the WM_MOUSEMOVE
message handler. The handler will first check if rubberbanding is currently
activated. If so it will first draw the current rectangle, effectively erasing it from
the screen, it will update the current coordinates, then it will draw the new coordinates
effectively updating the position of the rectangle. Here is the code:
LRESULT OnMouseMove (HWND hWnd, UINT nCtrl, UINT x, UINT y)
{
if (!isRubberBand)
{
return 0;
}
DrawRubberBand(hWnd);
ptCurrent.x = x;
ptCurrent.y = y;
DrawRubberBand(hWnd);
return 0;
}
Conclusion
Once again, this guide is to get people started down the path of painting in WIN32.
This article was written completely for the WIN32 SDK. I fully believe that MFC and
ATL are cleaner ways to develop code for Windows, but I also believe that if a developer
can get a firm grasp of the basic concepts at the SDK level, they will be able to flourish
as a framework developer, because you will be able to determine what is happening
behind the scenes, especially when problems arise.
The next article I post will be at an intermediate level with more information.
The next article will describe the other ways to create DCs. There will also be
information on the different fields of the PAINTSTRUCT
structure. Finally, the
update region will be explained with a great amount of detail.