In this hour you will look at pens and brushes. Specifically, you will learn
You also modify the DCTest sample program from Hour 11, "Device Contexts," to draw a variety of figures using pens and brushes.
New Term: A pen is a Windows GDI object used to draw lines and figures.
Think of a Windows pen as being like an ink pen at your desk. A Windows pen object has three attributes:
Programs written for Windows use two types of pens:
You use a cosmetic pen when you must always draw lines with a fixed size. For example, rulers and grid lines are often drawn using cosmetic pens. You use geometric pen lines to reflect the scaling provided by the current mapping mode.
Just a Minute: A pen is perfect in situations in which you must draw a geometric shape or line. Although you can use a bitmap for complicated images, you easily can draw squares, rectangles, circles, and other basic shapes using GDI objects.
You can create and use pens with a variety of styles. Cosmetic pens are extremely quick and are mapped directly into device units. This makes them useful for drawing things like frames, borders, grid lines, and other screen objects that should not be affected by the current device context-mapping mode. Geometric pens require more CPU power but offer more styles. You can manipulate geometric pens using any of the available mapping modes.
Pens are also useful for drawing three-dimensional highlighting or other effects. It's not uncommon for pens and other GDI objects to be used to simulate controls in Windows; before Windows 95 was released, early versions of property pages used pens to draw simulated "tabs."
Like other GDI objects, you normally use a pen by creating an MFC object. Use the CPen class to create and manage both cosmetic and geometric pens. When creating a pen, you must specify at least three things:
Time Saver: The number of styles available for geometric pens is much larger than for cosmetic pens. However, cosmetic pens have much less overhead. You should use cosmetic pens whenever possible. The next few sections discuss the various options available for cosmetic and geometric pens.
Cosmetic pens are not affected by the current mapping mode's scaling factor because they are always drawn in device units. Therefore, they are useful where a line must overlay another view that may be scaled. These basic styles are available for cosmetic pens:
Figure 12.1 shows examples of each of the pen styles.
Figure 12.1.
Examples of the styles available for pens.
Geometric pens can use all the styles available for cosmetic pens except for the PS_ALTERNATE style, and they also have access to four additional attributes:
The CPen class is simple because there really are only a few things that can be done to a pen object; most of the fun occurs when the pen object is selected into a device context. The CPen class provides three constructors: two simple constructors primarily for cosmetic pens and another extremely flexible constructor primarily for geometric pens.
The first constructor has no arguments:
CPen aGreenPen; aGreenPen.Create( PS_SOLID, 1, RGB(0,255,0);
If you use this constructor, use the Create member function to actually create the pen and make it ready for use.
The second constructor provided for CPen also is used for cosmetic pens:
CPen penDottedAndRed( PS_DOT, 1, RGB(255,0,0) );
This version of the constructor accepts three parameters: the pen style, width, and color. In this case, the CPen instance is a dotted red pen.
The third constructor used for CPen objects enables any type of pen to be created. It also uses more parameters, as shown in Listing 12.1.
LOGBRUSH lbrGrnHatch; lbrGrnHatch.lbStyle = BS_HATCHED; lbrGrnHatch.lbColor = RGB(0,255,0); lbrGrnHatch.lbHatch = HS_DIAGCROSS; CPen penGeometric( PS_DOT | PS_GEOMETRIC | PS_ENDCAPROUND, 50, &lbrGrnHatch, 0, NULL );
The constructor's first parameter is the pen's style, with the C++ OR operator, |, used to combine all styles that are applied to the pen. The second parameter for the constructor is the width; if the pen is cosmetic, it must be set to 1. The third parameter is a pointer to a LOGBRUSH structure. In Listing 12.1, lbrGrnHatch is defined as a diagonally cross-hatched green brush.
The last two parameters are rarely used; they define a user-supplied pattern for the pen. These two parameters are used only if the pen is created with the PS_USERSTYLE attribute. The fourth parameter is the number of elements in the style array, whereas the fifth parameter is an array of DWORD values, each used to define the length of a dash or space in the pen's pattern.
The simplest pens to use are known as stock objects. Stock objects were discussed in Hour 11; they are GDI objects that belong to the operating system. Windows provides three stock pens:
Each of these pens is exactly one unit wide. If you need a wider pen, you must create one using the CPen class. These pens are used through a CDC object by calling the SelectStockObject function, passing the stock object as a parameter, as follows:
CPen* pOldPen = pDC->SelectStockObject( BLACK_PEN );
After a pen has been selected into a device context, several different drawing functions can be performed with the device context. The CDC class used to represent device contexts, as you learned in Hour 11, includes these drawing functions often used with pens:
As an example using pens, you can modify the DCTest program you created in Hour 11. The changes use three pens to draw three figures in the view window in a variety of styles and colors. Modifying the existing project also gives you a chance to see how different mapping modes affect the figures drawn using pens. Although some of the listings might look long, most of them require only a few changes in the source code that is already present.
As the first step in the sample program, modify the Mapping Mode dialog box and the CMapModeDlg class to support extra options used when drawing with pens. The new version of the IDD_MAP_MODE dialog box is shown in Figure 12.2.
Figure 12.2.
The new version of the Mapping Mode dialog box.
Use the values from Table 12.1 for the new controls you add to the Mapping Mode dialog box, using ClassWizard to add member variables to the CMapModeDlg class. All existing controls should remain as they are.
Edit Control | Resource ID | Variable Name | Type |
Pen Width | IDC_WIDTH | m_nPenWidth | int |
Ellipse Width | IDC_CXELLIPSE | m_cxEllipse | int |
Ellipse Height | IDC_CYELLIPSE | m_cyEllipse | int |
You must modify the CDCTestView class slightly to add three new member variables that store the pen height and the ellipse variables you just added to the dialog box class. Add three new member variables to the attributes section of the CDCTestView class declaration, as shown in Listing 12.2.
// Attributes private: // Variables added for Hour 11 CMap< int, int, CString, CString > m_map; int m_nMapMode; // Variables added for Hour 12 - pens int m_cxEllipse; int m_cyEllipse; int m_nPenWidth;
Do not modify the declarations for existing member variables.
You must modify three CDCTestView member functions:
Each of these views is modified in the following sections. None of these member functions are new; all are used in the current DCTest project.
Add three new lines to the CDCTestView constructor to initialize the new variables added to the CDCTestView class. Listing 12.3 is the new version of the CDCTestView constructor. Most of the function should already be entered; you must add only the last three lines before the closing parenthesis.
CDCTestView::CDCTestView() { m_nMapMode = MM_TEXT; m_map.SetAt( MM_ANISOTROPIC, "MM_ANISOTROPIC" ); m_map.SetAt( MM_HIENGLISH, "MM_HIENGLISH" ); m_map.SetAt( MM_HIMETRIC, "MM_HIMETRIC" ); m_map.SetAt( MM_ISOTROPIC, "MM_ISOTROPIC" ); m_map.SetAt( MM_LOENGLISH, "MM_LOENGLISH" ); m_map.SetAt( MM_LOMETRIC, "MM_LOMETRIC" ); m_map.SetAt( MM_TEXT, "MM_TEXT" ); m_map.SetAt( MM_TWIPS, "MM_TWIPS" ); m_nPenWidth = 1; m_cxEllipse = 100; m_cyEllipse = 200; }
Modify the CDCTestView::OnViewMapMode function to handle the changes in the Mapping Mode dialog box. Listing 12.4 provides the source code for the new version of OnViewMapMode. There are a total of six new source code lines, each marked with a comment. You should need to add only these six lines; the rest of the function should have been created already.
void CDCTestView::OnViewMapMode() { CMapModeDlg dlg; // The next three lines are added for Hour 12 - pens dlg.m_nPenWidth = m_nPenWidth; // 1 dlg.m_cxEllipse = m_cxEllipse; // 2 dlg.m_cyEllipse = m_cyEllipse; // 3 if( dlg.DoModal() == IDOK ) { // The next three lines are added for Hour 12 - pens m_nPenWidth = dlg.m_nPenWidth; // 4 m_cxEllipse = dlg.m_cxEllipse; // 5 m_cyEllipse = dlg.m_cyEllipse; // 6 POSITION pos; pos = m_map.GetStartPosition(); while( pos != NULL ) { CString szMapMode; int nMapMode; m_map.GetNextAssoc( pos, nMapMode, szMapMode ); if( szMapMode == dlg.m_szCombo ) { m_nMapMode = nMapMode; break; } } InvalidateRect( NULL ); } }
Last but not least, you must create a new version of the OnDraw function. Most of this version of OnDraw is new because you are now drawing with pens instead of listing device attributes. Use the source code provided in Listing 12.5 for the new version of CDCTestView::OnDraw.
void CDCTestView::OnDraw(CDC* pDC) { pDC->SetMapMode( m_nMapMode ); // Draw an ellipse based on the current map-mode and values // supplied by the user. CRect rcClient; GetClientRect( rcClient ); pDC->DPtoLP( rcClient ); // Covert device units to logical CPoint ptCenter( rcClient.Width()/2, rcClient.Height()/2 ); CRect rcEllipse( ptCenter.x - ( m_cxEllipse/2 ), ptCenter.y - ( m_cyEllipse/2 ), ptCenter.x + ( m_cxEllipse/2 ), ptCenter.y + ( m_cyEllipse/2 ) ); CPen penRed( PS_SOLID, m_nPenWidth, RGB(255,0,0) ); CPen* pOldPen = pDC->SelectObject( &penRed ); pDC->Ellipse( rcEllipse ); // Draw a black box around the ellipse, using one of the stock // pens. pDC->SelectStockObject( BLACK_PEN ); pDC->MoveTo( rcEllipse.TopLeft() ); pDC->LineTo( rcEllipse.right, rcEllipse.top ); pDC->LineTo( rcEllipse.BottomRight() ); pDC->LineTo( rcEllipse.left, rcEllipse.bottom ); pDC->LineTo( rcEllipse.left, rcEllipse.top ); // Draw an arc using the client area as a bounding rectangle. // Clip the arc so that only the lower-left half is displayed. CPen penDottedAndGreen( PS_DOT, 1, RGB(0,255,0) ); pDC->SelectObject( &penDottedAndGreen ); pDC->Arc(rcClient, rcClient.TopLeft(),rcClient.BottomRight()); pDC->SelectObject( &pOldPen ); }
Compile and run the DCTest project, and experiment by changing the values in the Mapping Mode dialog box. Figure 12.3 shows the DCTest project running with the mapping mode set to MM_TEXT.
Figure 12.3.
The DCTest example after adding pen GDI objects.
A brush is a GDI object used to fill a control, window, or other area when programming for Windows. A brush is much like a pen; both are selected the same way, some of the attributes are similar, and you can use a series of stock objects without much overhead. However, you use a brush to fill an area rather than draw a line or a figure. A common use for brushes is to color windows, controls, or dialog boxes.
Every brush has several attributes:
By choosing different values for attributes given to a brush, you can achieve a wide variety of effects.
In a program you write with Visual C++, you normally create and use CBrush objects the way you create and use CPen objects. Every brush has a set of attributes that define how it appears and behaves. Just as with pens, Windows has several stock brushes that are available by calling the SelectStockObject function.
Your Windows program can create four basic types of brushes:
Figure 12.4.
Examples of styles available for brushes.
You create each of these brush types using a different function call. For example, a solid brush is created using CreateSolidBrush, whereas a hatched brush is created using CreateHatchBrush. When using the CBrush class, it's also possible to call a specialized CBrush constructor to construct the CBrush object in the desired style. You will use the CBrush class later in this hour.
You can create solid and hatch brushes with a color attribute that specifies the color used when the brush fills an area. In the case of a hatched brush, the color specifies the color of the hatching lines.
Six hatch styles are available for hatch brushes. You create a hatch style by passing one of the following styles as a parameter when you create the brush:
Figure 12.5 shows examples of these six hatching styles.
Figure 12.5.
Examples of brush hatching styles.
To use a brush in an MFC program, create a CBrush object and select it into a device context. You can create the brush using single-step construction, like this:
CBrush brBlack( RGB(0,0,0) );
Alternatively, use two-step construction, where the brush object is constructed and then explicitly created, like this:
CBrush brBlack(); brBlack.CreateSolidBrush( RGB(0,0,0) );
The advantage of using two-step construction is that the function used to create a brush returns FALSE if the function fails.
Unlike pens, which use style bits to determine the type of pen to be created, separate functions are used for different brush types. In two-step construction, you can use three functions to create a brush after you construct the CBrush object:
Four different constructors are provided for CBrush. In addition to the default constructor, you can use three constructors to create a specific type of brush in one step. The second constructor is used to create a solid brush and takes one COLORREF parameter, indicating the color used for the brush.
CBrush brGreen( RGB(0,0,255) );
Using this brush is equivalent to using the default constructor and then calling CreateSolidBrush.
The third form of the CBrush constructor is used to create a hatched brush, and takes the hatching style and hatch color as parameters:
CBrush brGray( HS_CROSS, RGB(192,192,192) );
This constructor is equivalent to using the default constructor and then calling CreateHatchBrush.
Use the fourth and final constructor for CBrush to create brushes that have bitmap patterns. You will learn more about bitmaps in Hour 15, "Using Bitmaps"; for now, just remember that a bitmap can be used as a pattern for a brush. The constructor takes a pointer to a CBitmap object as a parameter:
CBrush brArrow( &bmpArrow );
The CBitmap object can be up to 8x8 pixels. If the bitmap is larger, only the upper-left eight pixel squares are used for the brush pattern.
Logical brushes are defined using the LOGBRUSH structure. A logical brush often is used when specifying how a brush should be constructed. For example, earlier in this hour you used a LOGBRUSH structure to specify the characteristics of a geometric pen. Think of a LOGBRUSH as a recipe for a brush that might be created: It's not a brush yet, but it might help build a brush in the future.
The LOGBRUSH structure has three data members:
Each of the three LOGBRUSH data members corresponds to one of the style attributes available for brushes, discussed earlier in this hour. To create a logical brush, just assign values to the three data members, as with any structure. Listing 12.6 uses a logical brush to create a red hatched brush.
LOGBRUSH lbrRed; lbrRed.lbrStyle = BS_HATCH; lbrRed.lbrColor = RGB(255,0,0); lbrRed.lbrHatch = HS_CROSS; CBrush theRedBrush; theRedBrush.CreateBrushIndirect( &lbrRed );
Just like stock pens discussed earlier in this hour, Windows maintains a set of stock brushes. Windows provides seven stock brushes:
As with other stock objects, these brushes are used through a CDC object by calling the SelectStockObject function, passing the stock object as a parameter, as follows:
CPen* pOldBrush = pDC->SelectStockObject( BLACK_BRUSH );
The Windows operating system includes a series of dialog boxes as part of the operating system. These dialog boxes are guaranteed to be present, and using them requires just a few lines of code. Use these dialog boxes for common operations where it's beneficial for all Windows programs to have a similar look and feel. The common dialog boxes shipped with Windows can help you
To use the Common Color dialog box, just create a CColorDialog object and call DoModal, just as with any other dialog box:
CColorDialog dlgColor; if( dlgColor.DoModal() ) { //....
If IDOK is returned from the dialog box, the CColorDialog::GetColor function gets the selected color value. The example in the next section uses the Common Color dialog box to choose a brush color. You will use other common dialog boxes in later hours. (For example, the font-selection dialog box is used in Hour 13, "Fonts.")
As an example of how to use brushes, continue to modify the DCTest project that you worked with earlier this hour. The new version of the project displays a colored ellipse on a gray view background. Both the ellipse and background color are filled using CBrush objects. You can change the color of the ellipse using the Common Color dialog box; as a bonus, the Mapping Mode dialog box color changes to match the ellipse.
Modify the Mapping Mode dialog box to allow the user to choose a color for the dialog box and a brush used for the view. The CMapModeDlg class needs two new variables: a COLORREF for the currently selected color, and a CBrush object that has been created using the current color. Listing 12.7 contains the changes to the CMapModeDlg class declaration. Add the new code in the Dialog Data section, just after the AFX_DATA comments.
// Dialog Data //{{AFX_DATA(CMapModeDlg) enum { IDD = IDD_MAP_MODE }; CString m_szCombo; int m_cyEllipse; int m_cxEllipse; int m_nPenWidth; //}}AFX_DATA // Variable added in Hour 12 public: COLORREF m_clrChoice; private: CBrush m_brControl;
You must change the Mapping Mode dialog box slightly for this example. Remove the pen-width edit control and add a pushbutton control, as shown in Figure 12.6. Use ClassWizard to remove the m_nPenWidth member variable from the CMapModeDlg class.
Figure 12.6.
The new version of the Mapping Mode dialog box.
Use the values from Table 12.2 for the new button control.
Resource ID | Caption | Function |
IDC_COLOR | &Color... | CMapModeDlg::OnColor |
void CMapModeDlg::OnColor() { CColorDialog dlgColor; if( dlgColor.DoModal() == IDOK ) { m_clrChoice = dlgColor.GetColor(); // If the brush already exists, delete the current // GDI object before calling CreateSolidBrush if( m_brControl.Detach() ) m_brControl.DeleteObject(); m_brControl.CreateSolidBrush( m_clrChoice ); InvalidateRect( NULL ); } }
The OnColor function creates a Common Color dialog box and displays it using DoModal. If the user selects a new color, the color is collected and the brush is updated. If the brush has previously been created, the Detach and DeleteObject functions must be called to destroy the current brush before creating a new brush.
Before displaying any control or dialog box, Windows asks for the control's color by sending a WM_CTLCOLOR message to the owner of the control. To specify a color to be used for the control or dialog box, return a solid brush containing the color in response to this message, as shown in Listing 12.9. The m_brControl brush is a class member variable because it must survive for the life of the control.
HBRUSH CMapModeDlg::OnCtlColor(CDC* pDC,CWnd* pWnd,UINT nCtlColor) { if( nCtlColor == CTLCOLOR_DLG || nCtlColor == CTLCOLOR_STATIC ) { pDC->SetBkMode( TRANSPARENT ); return (HBRUSH)m_brControl.GetSafeHandle(); } else return CDialog::OnCtlColor(pDC, pWnd, nCtlColor); }
Time Saver: The easiest way to deal with colored dialog boxes is shown in Listing 12.9, where the text-drawing mode is set to transparent by calling SetBkMode. If this line is commented out, you will see that the static text has colored areas around each color. By setting the drawing mode to transparent, the text is drawn without including the text background color, allowing the dialog box color to show through.
The GetSafeHandle function is used with all GDI objects to return a handle to the underlying object. A CBrush object returns an HBRUSH handle; a CPen object returns an HPEN, and so on.
The WM_CTLCOLOR message is sent for every control type found in the dialog box. It's possible to set different colors for each control type by testing for the values found in Table 12.3. If a brush is not returned, determine the return value by calling CDialog::OnCtlColor.
Control Message Value | Control Type |
CTLCOLOR_BTN | Button control |
CTLCOLOR_DLG | Dialog box |
CTLCOLOR_EDIT | Edit control |
CTLCOLOR_LISTBOX | List box control |
CTLCOLOR_MSGBOX | Message box |
CTLCOLOR_SCROLLBAR | Scrollbar control |
CTLCOLOR_STATIC | Static control |
The CDCTestView class must store the color and brush selected by the user to color the ellipse and Mapping Mode dialog box. One new variable is added to the attributes section of the CDCTestView class, as shown in Listing 12.10: The m_clrChoice variable stores the currently selected color for the ellipse.
// Attributes private: // Variables added for Hour 11 CMap< int, int, CString, CString > m_map; int m_nMapMode; // Variables added for Hour 12 - pens int m_cxEllipse; int m_cyEllipse; // Variable added for Hour 12 - brushes COLORREF m_clrChoice;
To update the DCTest project to use brushes instead of pens, you must make four basic changes to the CDCTestView member functions:
Edit the constructor for CDCTestView so it looks like the source code provided in Listing 12.11. The m_nPenWidth variable has been removed, and one line has been added to initialize the m_clrChoice variable.
CDCTestView::CDCTestView() { m_nMapMode = MM_TEXT; m_map.SetAt( MM_ANISOTROPIC, "MM_ANISOTROPIC" ); m_map.SetAt( MM_HIENGLISH, "MM_HIENGLISH" ); m_map.SetAt( MM_HIMETRIC, "MM_HIMETRIC" ); m_map.SetAt( MM_ISOTROPIC, "MM_ISOTROPIC" ); m_map.SetAt( MM_LOENGLISH, "MM_LOENGLISH" ); m_map.SetAt( MM_LOMETRIC, "MM_LOMETRIC" ); m_map.SetAt( MM_TEXT, "MM_TEXT" ); m_map.SetAt( MM_TWIPS, "MM_TWIPS" ); // The next two lines are added for Hour 12 - pens m_cxEllipse = 100; m_cyEllipse = 200; // The next line is added for Hour 12 - brushes m_clrChoice = RGB(0,0,0); }
Modify the CDCTestView::OnViewMapMode function as shown in Listing 12.12. The code in this listing removes all references to the m_nPenWidth variable, and the function now tracks the color selected by the user. A total of two lines have been removed and one line added to the existing function.
void CDCTestView::OnViewMapMode() { CMapModeDlg dlg; // The next two lines are added for Hour 12 - pens dlg.m_cxEllipse = m_cxEllipse; dlg.m_cyEllipse = m_cyEllipse; // The next line is added for Hour 12 - brushes dlg.m_clrChoice = m_clrChoice; if( dlg.DoModal() == IDOK ) { // The next two lines are added for Hour 12 - pens m_cxEllipse = dlg.m_cxEllipse; m_cyEllipse = dlg.m_cyEllipse; // The next line is added for Hour 12 - brushes m_clrChoice = dlg.m_clrChoice; POSITION pos; pos = m_map.GetStartPosition(); while( pos != NULL ) { CString szMapMode; int nMapMode; m_map.GetNextAssoc( pos, nMapMode, szMapMode ); if( szMapMode == dlg.m_szCombo ) { m_nMapMode = nMapMode; break; } } InvalidateRect( NULL ); } }
Modify the CDCTestView::OnDraw function as shown in Listing 12.13. The new version of OnDraw uses a CBrush object to fill the view window with a red brush. Another CBrush object is used to draw an ellipse in the center of the view using a user-defined color to fill the figure.
void CDCTestView::OnDraw(CDC* pDC) { CRect rcClient; GetClientRect( rcClient ); pDC->DPtoLP( rcClient ); CBrush brBackground( RGB( 255, 255, 100 ) ); pDC->FillRect( rcClient, &brBackground ); CPoint ptCenter( rcClient.Width()/2, rcClient.Height()/2 ); CRect rcEllipse( ptCenter.x - ( m_cxEllipse/2 ), ptCenter.y - ( m_cyEllipse/2 ), ptCenter.x + ( m_cxEllipse/2 ), ptCenter.y + ( m_cyEllipse/2 ) ); CBrush brEllipse( m_clrChoice ); CBrush* pOldBrush = pDC->SelectObject( &brEllipse ); pDC->Ellipse( rcEllipse ); pDC->SelectObject( &pOldBrush ); }
Compile and run the DCTest project, and experiment by changing the values in the Mapping Mode dialog box. Also experiment with different colors for the dialog box and ellipse by clicking the Color button. Figure 12.7 shows an example of the DCTest project running.
Figure 12.7.
The DCTest example after adding brush GDI objects.
In this hour you learned about two GDI objects--pens and brushes--and how they are used to draw figures and fill shapes in Windows programs. You also learned about the MFC classes CPen and CBrush that are used to manage pen and brush objects. Finally, you modified the DCTest program to use the MFC CPen and CBrush classes.
The Workshop is designed to help you anticipate possible questions, review what you've learned, and begin thinking ahead to putting your knowledge into practice. The answers to the quiz are in Appendix B, "Quiz Answers."
© Copyright, Macmillan Computer Publishing. All rights reserved.