Windows Graphics Principles Erasing the Background There is a Windows message WM_ERASEBKGND. When this message is received by a window, the default processing erases the background, which means that the client area of the window is filled with the window's background color. Each window has a background color. If you do nothing about changing it, your view class's window will have background color white, if you have derived it from CFormView, or possibly gray if you derived it from CFormView. Normally, each WM_PAINT message is preceded by a WM_ERASEBKGND, so that your OnDraw code draws into a nice clean window (or more precisely, a nice clean clipping rectangle, since the erasing of the background is also affected by clipping). But, sometimes you may want to prevent erasing the background, as in animation or dragging code it can sometimes cause flicker. One way to do that is to use Invalidate(FALSE) instead of just Invalidate(). If that doesn't do the job you can add a message handler for WM_ERASEBKGND using Class Wizard, and just put in one line of code: return FALSE; // Do nothing and don't call the default handler for this message. CWnd::Invalidate void Invalidate( BOOL bErase = TRUE ); Parameters bErase Specifies whether the background within the update region is to be erased. Remarks Invalidates the entire client area of CWnd. The client area is marked for painting when the next WM_PAINT message occurs. The region can also be validated before a WM_PAINT message occurs by the ValidateRect or ValidateRgn member function. The bErase parameter specifies whether the background within the update area is to be erased when the update region is processed. If bErase is TRUE, the background is erased when the BeginPaint member function is called; if bErase is FALSE, the background remains unchanged. If bErase is TRUE for any part of the update region, the background in the entire region, not just in the given part, is erased. Windows sends a WM_PAINT message whenever the CWnd update region is not empty and there are no other messages in the application queue for that window. Drawing Lines The CDC class maintains as a data member the "current position". CDC::MoveToMoveTo This member function moves the current position to the point specified by x and y (or by point). Syntax CPoint MoveTo( int x, int y ); CPoint MoveTo( POINT point ); CDC::LineTo This member function draws a line from the current position up to, but not including, the point specified by x and y (or point). Syntax BOOL LineTo( int x, int y ); BOOL LineTo( POINT point ); Thus a complex figure (such as a polygon) would be drawn by one MoveTo to set the starting position, followed by a sequence of LineTo calls, possibly controlled by a for-loop. Remember that the coordinates (by default) are "client coordinates", measured in pixels with (0,0) at the upper left of the client area. You may have to compute coordinates using mathematical functions such as cosine or sine. If so, in C++ you need to put the line #include <math.h> at the top of the file to get the functions cos(x) and sin(x), etc., recognized. (In Java you would have to mention or import the math package; in C++ you just use this include command.) Selecting and deselecting tools Drawing is done with "pens" and painting is done with "brushes". The device context holds one pen and one brush at any given time. This is called the "selected" pen or brush. The selected pen and brush will be used by the graphics commands. By default, you get a one-pixel-wide black pen and a solid black brush. If you want any other width or color, you must do something about getting the right pen or brush selected. Pens and brushes occupy memory and hence have to be created and destroyed. The destructors of pens and brushes delete the C++ wrappers for the Windows objects and also delete the underlying Windows object, by calling the Windows API function DeleteObject: You don't call this function directly. CGdiObject::DeleteObject BOOL DeleteObject( ); Return Value Nonzero if the GDI object was successfully deleted; otherwise 0. Remarks Deletes the attached Windows GDI object from memory by freeing all system storage associated with the Windows GDI object. The storage associated with the CGdiObject object is not affected by this call. An application should not call DeleteObject on a CGdiObject object that is currently selected into a device context. Note the last line of that entry: one cardinal rule of Windows programming is that you may not destroy an object that is currently selected into a device context. The correct coding sequence: Create a pen (or get one that exists already) Select the pen do your drawing Select another pen (perhaps the original one) Delete the pen Here it is in code (which belongs in OnDraw; pDC is passed to OnDraw). CPen *RedPen = new CPen(PS_SOLID, 1, RGB(255,0,0)); // create CPen *OldPen = pDC->SelectObject(RedPen); ...// do some drawing with pDC->MoveTo and pDC->LineTo pDC->SelectObject(OldPen); // deselect delete RedPen; Alternately: CPen RedPen(PS_SOLID,1,RGB(255,0,0)); //create local object CPen *OldPen = pDC->SelectObject(&RedPen); // draw pDC->SelectObject(OldPen); // RedPen deleted automatically on exit from OnDraw Failure to delete the objects when you should will clog up memory and might even bring your application to a grinding halt. (In Win16 it used to bring the whole operating system to a grinding halt!) For inexperienced C++ programmers: Let's take a closer look at these two code samples. CPen *RedPen = new CPen(PS_SOLID, 1, RGB(255,0,0)); // create CPen *OldPen = pDC->SelectObject(RedPen); ...// do some drawing with pDC->MoveTo and pDC->LineTo pDC->SelectObject(OldPen); // deselect delete RedPen; Alternately: CPen RedPen(PS_SOLID,1,RGB(255,0,0)); //create local object CPen *OldPen = pDC->SelectObject(&RedPen); // draw pDC->SelectObject(OldPen); // RedPen deleted automatically on exit from OnDraw What is the difference between these two code samples? In the first one, RedPen, which lives on the stack, is only a pointer (4 bytes of space) containing the address of the actual Pen data structure, which is allocated on the heap by the new command. This data allocated on the heap will remain "in use" until it is freed by the delete command. In the second coding example, RedPen is not just a pointer, but the actual CPen object. It lives on the stack, and the space it occupies will be considered available again when control exits from OnDraw. We say that the "stack frame" of OnDraw has been popped from the stack. The next function call will overwrite the space used by RedPen. This is probably an oversimplification: probably the MFC class CPen contains a pointer to a Win32 PEN object. We don't care, because when OnDraw exits, the C++ code generated by the compiler will call the destructor of CPen, since RedPen is a class variable, and not just a pointer. This destructor will delete the GDI object (pen) "wrapped" by the CPen object. CDC::Ellipse This member function draws an ellipse. Syntax BOOL Ellipse( int x1, int y1, int x2, int y2 ); BOOL Ellipse( LPCRECT lpRect ); The rectangle specifies the bounding box of the ellipse. A circle is a special case of an ellipse. But because pixels are not necessarily square, you are not guaranteed to get a circle by specifying the height and width of the rectangle to be equal. That is, a rectangle with equal height and width (in pixels) is not necessarily geometrically square, though on many monitors it will be, in particular in our lab. Preparation for Friday's lab Let's take a simple exercise: Draw a big red (filled) circle on the screen. We will need a red brush. There are two plans: create the brush in OnDraw and let it be destroyed automatically on exit from OnDraw. Or, create the brush as a member variable of the view class, initialize it in the constructor, and destroy it in the destructor. We will illustrate both methods. First, making the brush a member variable. Then we add a member variable m_RedBrush to the view class. In the constructor of the view class we then call m_RedBrush.CreateSolidBrush(RGB(255,0,0)); Then we must put m_RedBrush.DeleteObject(); in the destructor of the view class. In OnDraw we write CBrush *oldBrush = pDC->SelectObject(&m_RedBrush); pDC->Ellipse(10,10,50,50); pDC->SelectObject(oldBrush); for example to draw a circle. (Remember, it's not guaranteed to be circular on all monitors, but it will do for now.) To prepare for the lab, you should create a project and actually write and run this code. Inexperienced C++ programmers should carefully note the use of * and & in this code. The declaration CBrush *oldBrush should be read as, oldBrush is of type, pointer to CBrush. That is, the * goes with CBrush rather than with oldBrush. Since SelectObject needs a pointer variable, we have to pass &m_RedBrush, but oldBrush is already a pointer variable, so &oldBrush would be a mistake. Now for Plan B. Let's use a green brush to demonstrate that. This time the code is entirely in OnDraw: CBrush greenBrush; greenBrush.CreateSolidBrush(RGB(0,255,0)); CBrush *oldBrush = pDC->SelectObject(&greenBrush); pDC->Ellipse(10,10,50,50); pDC->SelectObject(oldBrush); // greenBrush.deleteObject() is not necessary Why is it not necessary to delete the Win32 brush attached to greenBrush? Because (1) C++ calls the destructor of class variables allocated on the stack, when the function exits (i.e., when the variable goes out of scope), and (2) The destructor of the CBrush class calls deleteObject. The first two lines above can be replaced by using one of the constructors of the CBrush class: CBrush greenBrush(RGB(0,255,0)); // correct C++ syntax for constructor as a minor variation on Plan B. Note that you don't say CBrush greenBrush = CBrush(RGB(0,255,0)); // wrong Now here's Plan C: allocate the brush locally, but on the heap. CBrush *pGreenBrush = new CBrush(); pGreenBrush->CreateSolidBrush(RGB(0,255,0)); CBrush *oldBrush = pDC->SelectObject(pGreenBrush); pDC->Ellipse(10,10,50,50); pDC->SelectObject(oldBrush); delete pGreenBrush; // this calls greenBrush->deleteObject for you. Again, inexperienced C++ programmers should pay close attention to the use of * and & in the above code samples. Type in and run all these three approaches. CDC::FillRect This member function fills a given rectangle using the specified brush. Syntax void FillRect( LPCRECT lpRect, CBrush* pBrush ); CDC::FillSolidRect This member function fills the given rectangle with the specified solid color. Syntax void FillSolidRect( LPCRECT lpRect, COLORREF clr ); void FillSolidRect( int x, int y, int cx, int cy, COLORREF clr ); CDC::Rectangle This member function draws a rectangle using the current pen. The interior of the rectangle is filled using the current brush. Syntax BOOL Rectangle( int x1, int y1, int x2, int y2 ); BOOL Rectangle( LPCRECT lpRect ); Drawing an unfilled rectangle is done by selecting a NULL_BRUSH, which is transparent. CBrush * pOldBrush = (CBrush *) pDC->SelectStockObject(NULL_BRUSH); pDC->Rectangle(10,10,100,100); pDC->SelectObject(pOldBrush); This illustrates the fact that there are some brushes and pens you do NOT have to create. The explicit cast (Cbrush *) is needed because SelectStockObject returns a pointer to CGDIObject, which could be a pointer to a font, brush, or pen. Without the cast the code won't compile. CDC::SelectStockObject virtual CGdiObject* SelectStockObject( int nIndex ); Return Value A pointer to the CGdiObject object that was replaced if the function is successful. The actual object pointed to is a CPen, CBrush, or CFont object. If the call is unsuccessful, the return value is NULL. Parameters nIndex Specifies the kind of stock object desired. It can be one of the following values: BLACK_BRUSH Black brush. DKGRAY_BRUSH Dark gray brush. GRAY_BRUSH Gray brush. HOLLOW_BRUSH Hollow brush. [synonym for NULL_BRUSH] LTGRAY_BRUSH Light gray brush. NULL_BRUSH Null brush. [does nothing] WHITE_BRUSH White brush. BLACK_PEN Black pen. NULL_PEN Null pen. [does nothing] WHITE_PEN White pen. [font objects omitted here..] If you just want to create a stock object but not select it into your device context, you need CreateStockObject. There is a function in the SDK called GetStockObject, which retrieves a handle to the stock object you want, but the handle still needs to be wrapped in a CBrush (or CPen, etc.) class. Here's sample code: CRect r; r.SetRect(10,10,100,100); CBrush BlackBrush; BlackBrush.CreateStockObject(BLACK_BRUSH); pDC->FillRect(&r,&BlackBrush); Text Output Text is not drawn with pens and brushes. The currently selected pen and brush have no effect on text output. The workhorse function for text output is TextOut: CDC::TextOut virtual BOOL TextOut( int x, int y, LPCTSTR lpszString, int nCount ); BOOL TextOut( int x, int y, const CString& str ); Return Value Nonzero if the function is successful; otherwise 0. Parameters x Specifies the logical x-coordinate of the starting point of the text. y Specifies the logical y-coordinate of the starting point of the text. lpszString Points to the character string to be drawn. nCount Specifies the number of bytes in the string. str A CString object that contains the characters to be drawn. Remarks Writes a character string at the specified location using the currently selected font. Character origins are at the upper-left corner of the character cell. By default, the current position is not used or updated by the function. To change the size or style of text, you have to change the font. This will be the subject of a later lecture. To change the color is easy: CDC::SetTextColor virtual COLORREF SetTextColor( COLORREF crColor ); Return Value An RGB value for the previous text color. Parameters crColor Specifies the color of the text as an RGB color value. Remarks Sets the text color to the specified color. The system will use this text color when writing text to this device context and also when converting bitmaps between color and monochrome device contexts. If the device cannot represent the specified color, the system sets the text color to the nearest physical color. The background color for a character is specified by the SetBkColor and SetBkMode member functions.