Introduction
Drawing in Windows isn't overly complex, but it does require understanding a few concepts.
- All drawing is done in a DC (Device Context)!
A DC is basically some area in memory (in RAM or in video RAM) where Windows is prepared to draw. Each DC has attributes, such as what font is selected into the DC, what color pen will be used for drawing, what brush (a small bitmap) will be used to fill in large areas, etc. There are many more attributes than this, such as the clipping area, viewport, mapmode, etc. If you stick with working with just pixels, then a number of attributes need not be concerned with.
- Drawing directly into a window DC (video memory through the GDI) can produce some flickering at times, so a common technique is to create a Memory DC, draw into it, and Bitblt the memory DC image into the window DC when needed (during
WM_PAINT
). This gives your image what is called "persistence". Once the image is drawn, there is no need to redraw it again unless it changes. You simply BitBlt the image during the WM_PAINT
message to refresh it when needed.
- A memory DC is created using the
CreateCompatibleDC
function. Then you must create a bitmap to store the image by using the CreateCompatibleBitmap
function. You must select the bitmap into the memory DC using the SelectObject
function. Make sure you store the return value of SelectObject
, since it is the handle to the original bitmap in the DC (a 1 x 1 pixel bitmap). When you destroy the DC (use DeleteDC
) (when no longer needed), you must select the old bitmap back into the memory DC and then delete the bitmap you created (use DeleteObject
).
- To draw into the memory DC, you can use any of the GDI (Graphics Device Interface) API functions. You should define the oen color using the
CreatePen
function. You should define the brush color using the CreateSolidbrush
function. There are a number of drawing functions to draw things like rectangles, circles, lines, etc. (such as the Rectangle
, Ellipse
, LineTo
, and the MoveToEx
functions). The pen and brush created must be selected into the DC before drawing with the GDI calls. Remember to store the return value of the SelectObject
function, and then select the old pen and brush back into the DC when you are finished.
Key concepts in drawing in Windows
Drawing in Windows requires an understanding of a number of basic concepts. First is what is called a DC.
- A DC (Device Context) is basically a defined area for drawing, with its own unique set of attributes. There are different kinds of DCs. One is a window DC, which ultimately means the area drawn on is the video memory of the video card, so the image will be visible to the user. Windows shields you from direct access to the video memory (unless you use DirectX), but the image nonetheless will be drawn on the video memory. The second type of DC is a memory DC. The programmer can create any memory DC they like, when needed. The area drawn on will be a bitmap which has been selected into the DC. A common technique is to create a memory DC, select a bitmap into it, and then draw on it, and once the image is done, then BitBlt (copy) that image from the memory DC to a window DC (so it is visible). Another type of DC is a printer DC, where the area drawn on is the printed page.
To get access to a window DC, one of three ways is used:
- Process the
WM_PAINT
message and get the DC using the BeginPaint
API function. Now you can draw on it until Endpaint
is executed.
- Use the
GetDC
API function to get the DC for the client area (inside the border) of any window. You can draw on this DC and the image will appear on the screen immediately. You finish by using the ReleaseDC
API function to free the DC.
- Lastly, you can use the
GetWindowDC
API function to get a DC for the entire window of any window. This includes the non-client area which is the border of a control. Again, once done drawing, you must use the ReleaseDC
function to free the DC.
- Every DC has a set of attributes. A newly created DC has those attributes set to the default settings (i.e., a memory DC). A shared DC (window DCs can be shared by many different windows) has the attributes which were defined the last time the DC was accessed. As a rule of thumb, when working with a shared DC (e.g., a window DC), you should always restore the DC's attributes to what they were before you start accessing the DC. This can be done by using the
SaveDC
and RestoreDC
functions.
What are the attributes of a DC?
There are many, but I will list just a few here that are commonly used:
Attribute |
Description |
Function to create it |
Function to delete it |
Function to set it |
Pen |
Line color, width and style for drawing |
CreatePen |
DeleteObject |
SelectObject |
Brush |
8 x 8 pixel pattern for filling shapes |
CreateSolidBrush , CreateHatchBrush , CreatePatternBrush |
DeleteObject |
SelectObject |
Font |
Font used for drawing text |
CreateFont |
DeleteObject |
SelectObject |
Text_FG_Color |
Color of text |
SetTextColor |
|
|
Text_BG_Color |
BG color behind drawn text |
SetBKColor |
|
|
BG fill mode |
Mode of how to fill background |
SetBKMode (solid [filled] or transparent) |
|
|
Draw Mode |
How a drawn object is mixed with an existing background image |
SetROP2 |
|
|
. |
. |
. |
. |
. |
Drawing dunctions (some affected by the above attributes):
Function |
Description |
MoveToEx |
Move pen position to X,Y location |
SetPixel |
Draw pixel by color and return previous color |
SetPixelV |
Draw pixel by color |
Ellipse |
Draw an ellipse |
Rectangle |
Draw a rectangle |
LineTo |
Draw a line from pen position to new position |
Arc |
Draw an arc |
PolyLineTo |
Draw multiple lines by values in array |
RoundRect |
Draw a rounded rectangle |
Polygon |
Draw a polygon |
Pie |
Draw a pie shaped object |
BitBlt |
Copy image from one DC to another (bitmap) |
StretchBlt |
Copy and stretch image from one DC to another (bitmap) |
One of the best methods of drawing is to use a memory DC to draw on, and when you need to display it on the screen, simply use the BitBlt
function to copy it from the memory DC to the window DC (during WM_PAINT
).
Here is how to create a memory DC:
hDC& = CreateCompatibleDC(%NULL) W& = 200 H& = 200 hBmp& = CreateCompatibleBitmap(hDC&, W&, H&)
OriginalBitmap& = SelectObject(hDC&, hBmp&)
SaveDC hDC&
SelectObject hDC&, OriginalBitmap&
DeleteObject hBmp&
RestoreDC hDC&, -1
DeleteDC hDC&
How to draw
Let's do some real drawing!
Windows has a lot of overhead when drawing. It's not like the old days (remember the Commodore 64) when you could literally peek and poke video RAM. Especially when you use API functions like SetPixel
can you see how slow it can be to draw in Windows. The larger an area an API command draws on, the faster the drawing is per pixel. As a test, use the Rectangle
API function to draw a large filled rectangle and then try drawing the same filled rectangle a pixel at a time using the SetPixelV
(faster than SetPixel
) API function. The speed difference will be dramatic. Both techniques accomplish the same thing, but drawing with SetPixel
demonstrates the huge amount of overhead Windows has in drawing.
The next part of the equation is the difference between drawing into RAM and drawing directly into video RAM. When you draw directly into a Windows DC (Device Context), you are drawing into video RAM. Video RAM drawing is terribly slow compared to drawing into regular RAM.
What slows Windows down is the Device Context (DC) arrangement. Using a DC has its purpose though. It allows Windows to draw into a variety of devices using the same GDI functions, such as video RAM, regular RAM, a printer, or any other device that could be drawn into. The use of a DC is very powerful, but the drawback is that there is a lot of extra overhead to keep track of all the stuff associated with a DC. The need to select objects into and out of a DC adds a significant amount of overhead. DCs are needed for Windows to do what it does. It just slows things down.
Now using the above information, we can develop better approaches to drawing in Windows for faster display rates. Here are a few techniques of how to speed things up.
- Drawing into RAM is significantly faster than drawing into video RAM (a Windows DC)!
This is where buffering comes in. By creating a memory DC and selecting a bitmap into it, you can now draw directly into RAM. When you must use the slower, lower level GDI functions (like SetPixel
) to draw with, then you should always draw into a memory DC rather than a window DC (video RAM). This will significantly speed up drawing. Now remember, when you draw into a memory DC, you can't see the results. You will have to somehow move the image from RAM (memory DC) to video RAM (window DC). This brings us to point #2.
- Use only high level, large area, GDI (API) functions when drawing into a Windows DC!
When drawing into the actual video RAM (window DC during WM_PAINT
), the more complex GDI functions which cover a larger area should be used. For example, PatBlt
is commonly used for filling the background of a Windows DC. These types of GDI functions (which cover a large area) are more highly optimized than the lower level ones. One of the more commonly used GDI functions used when using buffers (memory DC) is BitBlt
. BitBlt
is highly optimized, and it can move large amounts of data (pixels) back and forth between DCs. BitBlt
is commonly used to move data (pixels) from a memory DC where an image has been drawn to a window DC (which is the video RAM). Another GDI function that can be very useful is StretchBlt
, which can scale an image into any size window DC.
By using these very fast and highly optimized GDI (API) functions, you can significantly speed up drawing into a window DC.
Note: When drawing into a printer DC, things are a little different since many printer drivers don't support BitBlt
. In this case, DIBs (Device Independent Bitmaps) need to be used and GDI functions like StretchDiBits
should be used.
Putting the above two principles to use is the basis to using a memory DC as a buffer. Using a memory buffer is also useful in adding persistence to a window DC. Rather than have to redraw an image from scratch into a window DC, every time the WM_PAINT
message is processed, a single image can be copied (using BitBlt
) from memory into the window DC. Consider this technique as draw once, BitBlt
many.
There are other techniques other than this for drawing that can add speed, but they are a bit more advanced. One such is using DIBs. Simply put, a DIB is a memory image similar to a bitmap, but the difference is that it is device independent and you can actually choose the format of how the data is stored. For example, if the video display is 16 bit color, you could copy the data into a DIB which is stored as 32 bit color. The other difference, which is significant, is that with a DIB, you have direct access to each pixel in byte form. The GDI is not necessary for accessing the pixels. It's kind of like the old days where you can peek and poke directly into the video RAM. In this case, there are a few extra steps. You create a memory DC and memory bitmap which is compatible with the video mode (i.e., 256 color, 16 bit, etc.). You then move the data from the memory bitmap (and DC) into a DIB section (simply a block of RAM allocated to hold a bunch of bytes). Now you can peek and poke all you want directly into the DIB section data. You can write your own custom drawing functions which work directly on RAM data. Once you finish drawing, you now move the data in the DIB section back into the memory DC bitmap. Now you can BitBlt the memory DC into video ram. I think it is also possible to skip the memory DC step when using DIBs and to move data back and forth between a window DC and a DIB section, but I haven't tried it yet and I can't verify how it works.
Just to add to my graphics 101
I wasn't trying to get into the technical aspects of how Windows draws things. Of course, more is involved than what I mentioned. The video driver and the GDI are between your code and the direct video RAM, and you can't access it directly.
My point though is when you draw into a window DC, for all practical purposes, you are indirectly drawing into the video RAM (that's where what you draw is stored). The problem with what you draw in a window DC is that it doesn't have persistence. The memory where the window DC's pixels are stored (video RAM or whatever) is shared by all windows. The DC itself may be shared (it stores the current objects like pen, brush) or not, but the area where the pixels are stored is shared by all windows. Any other window can write over your window's pixel data in video RAM. Because of its lack of persistence because of sharing pixel space with any window that may draw in it, a window DC's pixel data should always be considered temporary.
A memory DC (with an associated bitmap, which is where the pixels are actually stored) can be isolated from access by other windows. You can create your own memory DC and memory bitmap to draw in and no one else can bother it. This produces persistence.
Memory DCs (with a memory bitmap selected into it) have two advantages. One is speed of drawing. Drawing into a memory DC is always faster than drawing into a window DC (for whatever reasons). Second, a memory DC (with its bitmap) has persistence, whereas a window DC does not.
To prove this concept of persistence, write a program that only processes the WM_PAINT
message once (the first time called) and see what happens when you move over it with another window.
A lesson in persistance!
Note: The code below uses the PowerBASIC DDT syntax which simplifies creating dialogs.
Given below is a program that demonstrates what happens with a window DC not being painted all the time. It demonstrates the lack of persistence when you draw into a window DC. If the window is prevented from processing the WM_PAINT
(or the WM_ERASEBKGND
) message, the pixels drawn in the window DC are not remembered.
#COMPILE EXE
#REGISTER NONE
#DIM ALL
#INCLUDE "win32api.inc"
DECLARE SUB LIB_InitColors()
DECLARE SUB LIB_DeleteBrushes()
DECLARE SUB ShowDialog_Form1(BYVAL hParent&)
DECLARE CALLBACK FUNCTION Form1_DLGPROC
DECLARE SUB ShowDialog_Form2(BYVAL hParent&)
DECLARE CALLBACK FUNCTION Form2_DLGPROC
DECLARE CALLBACK FUNCTION CBF_FORM2_BUTTON1()
%FORM2_BUTTON1 = 100
GLOBAL App_Brush&()
GLOBAL App_Color&()
GLOBAL App_Font&()
GLOBAL hForm1& GLOBAL hForm2& GLOBAL PaintFlag&
FUNCTION PBMAIN
LOCAL Count&
LIB_InitColors
PaintFlag&=1
ShowDialog_Form1 0
ShowDialog_Form2 hForm1&
DO
DIALOG DOEVENTS TO Count&
LOOP UNTIL Count&=0
LIB_DeleteBrushes
END FUNCTION
SUB ShowDialog_Form1(BYVAL hParent&)
LOCAL Style&, ExStyle&
Style& = %WS_POPUP OR %DS_MODALFRAME OR %WS_CAPTION OR %WS_MINIMIZEBOX
OR %WS_SYSMENU OR %DS_CENTER
ExStyle& = 0
DIALOG NEW hParent&, "WM_PAINT limited window", 0, 0, 267, 177,
Style&, ExStyle& TO hForm1&
DIALOG SHOW MODELESS hForm1& , CALL Form1_DLGPROC
END SUB
CALLBACK FUNCTION Form1_DLGPROC
SELECT CASE CBMSG
CASE %WM_PAINT
CASE %WM_ERASEBKGND
IF PaintFlag&=0 THEN
FUNCTION=1
EXIT FUNCTION
END IF
CASE %WM_CTLCOLORDLG
IF CBLPARAM=CBHNDL THEN
SetTextColor CBWPARAM, App_Color&(0)
SetBkColor CBWPARAM, App_Color&( 17)
FUNCTION=App_Brush&( 17)
END IF
CASE ELSE
END SELECT
END FUNCTION
SUB ShowDialog_Form2(BYVAL hParent&)
LOCAL Style&, ExStyle&
Style& = %WS_POPUP OR %DS_MODALFRAME OR %WS_CAPTION OR %WS_MINIMIZEBOX
OR %WS_SYSMENU OR %DS_CENTER
ExStyle& = 0
DIALOG NEW hParent&, "Click Button to Toggle painting of other window",
0, 0, 245, 59, Style&, ExStyle& TO hForm2&
CONTROL ADD "Button", hForm2&, %FORM2_BUTTON1, "Toggle WM_PAINT for
other Window", 37, 17, 176, 15, _
%WS_CHILD OR %WS_VISIBLE OR %BS_PUSHBUTTON OR %WS_TABSTOP CALL
CBF_FORM2_BUTTON1
DIALOG SHOW MODELESS hForm2& , CALL Form2_DLGPROC
END SUB
CALLBACK FUNCTION Form2_DLGPROC
SELECT CASE CBMSG
CASE %WM_CTLCOLORDLG
IF CBLPARAM=CBHNDL THEN
SetTextColor CBWPARAM, App_Color&(0)
SetBkColor CBWPARAM, App_Color&( 10)
FUNCTION=App_Brush&( 10)
END IF
CASE ELSE
END SELECT
END FUNCTION
SUB LIB_InitColors()
DATA 0, 8388608, 32768, 8421376, 196, 8388736,
16512, 12895428
DATA 8421504, 16711680, 65280, 16776960, 255, 16711935,
65535, 16777215
DATA 10790052, 16752768, 10551200, 16777120, 10526975, 16752895,
10551295, 13948116
DATA 11842740, 16768188, 14483420, 16777180, 14474495, 16768255,
14483455, 15000804
LOCAL T&, RGBVal&
REDIM App_Brush&(0 TO 31)
REDIM App_Color&(0 TO 31)
FOR T&=0 TO 31
RGBVal&=VAL(READ$(T&+1))
App_Brush&(T&)=CreateSolidBrush(RGBVal&)
App_Color&(T&)=RGBVal&
NEXT T&
END SUB
SUB LIB_DeleteBrushes()
LOCAL T&
FOR T&=0 TO 31
DeleteObject App_Brush&(T&)
NEXT T&
END SUB
CALLBACK FUNCTION CBF_FORM2_BUTTON1
IF CBCTLMSG=%BN_CLICKED THEN
IF PaintFlag&=0 THEN
PaintFlag&=1
ELSE
PaintFlag&=0
END IF
END IF
END FUNCTION