Chapter 18. Working with Dialogs and Controls

WHAT YOU WILL LEARN IN THIS CHAPTER

  • How to create dialog resources

  • How to add controls to a dialog

  • The basic varieties of controls available

  • How to create a dialog class to manage a dialog

  • How to program the creation of a dialog box, and how to get information back from the controls in it

  • Modal and modeless dialogs

  • How to implement and use direct data exchange and validation with controls

  • How to implement view scaling

  • How to add a status bar to an application

Dialogs and controls are basic tools for user communication in the Windows environment. In this chapter you'll learn how to implement dialogs and controls by applying them to extend the Sketcher program.

UNDERSTANDING DIALOGS

Of course, dialog boxes are not new to you. Most Windows programs of consequence use dialogs to manage some of their data input. You click a menu item and up pops a dialog box with various controls that you use for entering information. Just about everything that appears in a dialog box is a control. A dialog box is actually a window and, in fact, each of the controls in a dialog is also a specialized window. Come to think of it, most things you see on the screen under Windows are windows.

There are two things needed to create and display a dialog box in an MFC program: the physical appearance of the dialog box, which is defined in a resource file, and a dialog class object, used to manage the operation of the dialog and its controls. MFC provides a class called CDialog for you to use after you have defined your dialog resource.

UNDERSTANDING CONTROLS

Many different controls are available to you in Windows, and in most cases there's flexibility to how they look and operate. Most of them fall into one of the six categories shown in the following table.

CONTROL TYPE

WHAT THEY DO

Static controls

These are used to provide titles or descriptive information.

Button controls

Buttons provide a single-click input mechanism. There are basically three flavors of button controls: simple push buttons, radio buttons (of which only one may be in a selected state at any one time), and checkboxes (of which several may be in a selected state at one time).

Scrollbars

Scrollbars are typically used to scroll text or images, either horizontally or vertically, within another control.

List boxes

These present a list of choices of which one or more selections can be in effect at one time.

Edit controls

Edit controls allow text input or editing of text that is displayed.

Combo boxes

Combo boxes present a list of choices from which you can select, combined with the option of entering text yourself.

A control may or may not be associated with a class object. Static controls don't do anything directly, so an associated class object may seem superfluous; however, there's an MFC class, CStatic, that provides functions to enable you to alter the appearance of static controls. Button controls can also be handled by the dialog object in many cases, but again, MFC does provide the CButton class for use in situations where you need a class object to manage a control. MFC also provides a full complement of classes to support the other controls. Because controls are windows, they are all derived from CWnd.

CREATING A DIALOG RESOURCE

Here's a concrete example. You could add a dialog to Sketcher to provide a choice of pen widths for drawing elements. This ultimately involves modifying the current pen width in the document, as well as in the CElement class, and adding or modifying functions to manage pen widths. You'll deal with all that, though, after you've gotten the dialog together.

Display the Resource View, expand the resource tree for Sketcher by clicking ⊳ twice, and right-click the Dialog folder in the tree; then click Insert Dialog from the pop-up to add a new dialog resource to Sketcher. This results in the Dialog Resource editor swinging into action and displaying the dialog in the Editor pane, and the Toolbox showing a list of controls that you can add; if the Toolbox is not displayed, click Toolbox in the right-hand sidebar or press Ctrl+Alt+X.

The dialog has OK and Cancel button controls already in place. Adding more controls to the dialog is simplicity itself: you can just drag the control from the list in the Toolbox window to the position at which you want to place it in the dialog. Alternatively, you can click a control from the list to select it, and then click in the dialog where you want the control to be positioned. When it appears you'll still be able to move it around to set its exact position, and you'll also be able to resize it by dragging handles on the boundaries.

The dialog has a default ID assigned, IDD_DIALOG1, but it would be better to have an ID that's a bit more meaningful. You can edit the ID by right-clicking the dialog name in the Resource View pane and selecting Properties from the pop-up; this displays the properties for the dialog node. Change the ID to something that relates to the purpose of the dialog, such as IDD_PENWIDTH_DLG. You can also display the properties for the dialog itself by right-clicking in the Dialog Editor pane and selecting from the pop-up. Here you can change the Caption property value that appears in the title bar of the dialog window to Set Pen Width.

Adding Controls to a Dialog Box

To provide a mechanism for entering a pen width, you can add controls to the basic dialog that is displayed initially until it looks like the one shown in Figure 18-1. The figure shows the grid that you can use to position controls. If the grid is not displayed, you can select the appropriate toolbar button to display it; the toolbar button toggles the grid on and off.

FIGURE 18-1

Figure 18.1. FIGURE 18-1

Alternatively, you can display rules along the side and top of the dialog, which you can use to create guide lines, by clicking the Toggle Guides button. You create a horizontal guide by clicking in the appropriate rule where you want the guide to appear. Controls placed in contact with a guide will be attached to it and move with the guide. You can reposition a guide line by dragging the arrow for it along the rule. You can use one or more guides when positioning a control. You can toggle guides on and off by clicking the Toggle Guides toolbar button.

The dialog shown has six radio buttons that provide the pen width options. These are enclosed within a group box with the caption Pen Widths. The group box serves to enclose the radio buttons and make them operate as a group, for which only one member of the group can be checked at any given time. Each radio button has an appropriate label to identify the pen width that is set when it is selected. There are also the default OK and Cancel buttons that close the dialog. Each of the controls in the dialog has its own set of properties that you can access and modify, just as for the dialog box itself. Let's press on with putting the dialog together.

The next step in the creation of the dialog shown in Figure 18-1 is to add the group box. As I said, the group box serves to associate the radio buttons in a group from an operational standpoint, and to provide a caption and a boundary for the group of buttons. Where you need more than one set of radio buttons, a means of grouping them is essential if they are to work properly. You can select the button corresponding to the group box from the common controls palette by clicking it; then click the approximate position in the dialog box where you want to place the center of the group box. This places a group box of default size onto the dialog. You can then drag the borders of the group box to enlarge it to accommodate the six radio buttons that you add. To set the caption for the group box, type the caption you want while the group box is selected (in this case, type Pen Widths).

The last step is to add the radio buttons. Select the radio button control by clicking it, and then clicking the position in the dialog where you want to place a radio button within the group box. Do the same for all six radio buttons. You can select each button by clicking it; then type in the caption to change it. You can also drag the border of the button to set its size, if necessary. To display the Properties window for a control, select it by right-clicking it; then select Properties from the pop-up. You can change the ID for each radio button in the Properties window for the control, in order to make the ID correspond better to its purpose: IDC_PENWIDTH0 for the one-pixel-width pen, IDC_PENWIDTH1 for the 0.01-inch-width pen, IDC_PENWIDTH2 for the 0.02-inch-pen, and so on.

You can position individual controls by dragging them around with the mouse. You can also select a group of controls by selecting successive controls with the Shift key pressed, or by dragging the cursor with the left button pressed to create an enclosing rectangle. To align a group of controls, or to space them evenly horizontally or vertically, select the appropriate button from the Dialog Editor toolbar. If the Dialog Editor toolbar is not visible, you can show it by right-clicking in the toolbar area and selecting it from the list of toolbars that is displayed. You can also align controls in the dialog by selecting from the Format menu.

Testing the Dialog

The dialog resource is now complete. You can test it by selecting the toolbar button that appears at the left end of the toolbar or by pressing Ctrl+T. This displays the dialog window with the basic operations of the controls available, so you can try clicking on the radio buttons. When you have a group of radio buttons, only one can be selected at a time. As you select one, any other that was previously selected is reset. Click either the OK or Cancel button, or even the close icon in the title bar of the dialog, to end the test. After you have saved the dialog resource, you're ready to add some code to support it.

PROGRAMMING FOR A DIALOG

There are two aspects to programming for a dialog: getting it displayed, and handling the effects of its controls. Before you can display the dialog corresponding to the resource you've just created, you must first define a dialog class for it. The Class Wizard helps with this.

Adding a Dialog Class

Right-click in the Resource Editor pane for the dialog and then select Add Class from the pop-up to display the Class Wizard dialog. You'll define a new dialog class derived from the MFC class CDialog, so select that class name from the "Base class" drop-down list box, if it's not already selected. You can enter the class name as CPenDialog in the Class name edit box. The Class Wizard dialog should look as shown in Figure 18-2. Click the Finish button to create the new class.

FIGURE 18-2

Figure 18.2. FIGURE 18-2

The CDialog class is a window class (derived from the MFC class CWnd) that's specifically for displaying and managing dialogs. The dialog resource that you have created automatically associates with an object of type CPenDialog because the IDD class member is initialized with the ID of the dialog resource:

class CPenDialog : public CDialog
{
  DECLARE_DYNAMIC(CPenDialog)

public:
  CPenDialog(CWnd* pParent = NULL);   // standard constructor
  virtual ~CPenDialog();

// Dialog Data
  enum { IDD = IDD_PENWIDTH_DLG };

protected:
  virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV support

  DECLARE_MESSAGE_MAP()
};

The boldfaced statement defines IDD as a symbolic name for the dialog ID in the enumeration. Incidentally, using an enumeration is one of two ways to get an initialized data member into a native C++ class definition. The other way is to define a static const integral member of the class, so the Class Wizard could have used the following in the class definition:

static const int IDD = IDD_PENWIDTH_DLG;

If you try putting an initial value for any regular non-static data member declaration in a class, it won't compile.

Having your own dialog class derived from CDialog means that you get all the functionality that that class provides. You can also customize the dialog class by adding data members and functions to suit your particular needs. You'll often want to handle messages from controls within the dialog class, although you can also choose to handle them in a view or a document class if this is more convenient.

Modal and Modeless Dialogs

There are two different types of dialogs, modal and modeless, and they work in completely different ways. While a modal dialog is displayed, all operations in the other windows in the application are suspended until the dialog box is closed, usually by the user clicking an OK or Cancel button. With a modeless dialog you can move the focus back and forth between the dialog window and other windows in your application just by clicking them, and you can continue to use the dialog at any time until you close it. The Class Wizard is an example of a modal dialog; the Properties window is modeless.

You can create a modeless dialog box by calling the Create() member of the CDialog class in your dialog class constructor. You create a modal dialog box by creating an object on the stack from your dialog class and calling its DoModal() function.

Displaying a Dialog

Where you put the code to display a dialog in your program depends on the application. In the Sketcher program, it will be convenient to add a menu item that, when selected, results in the pen width dialog's being displayed. You can put this item in the IDR_SketcherTYPE menu bar. As both the pen width and the drawing color are associated with a pen, you can rename the Color menu as Pen. You do this just by double-clicking the Color menu item in the Resource Editor pane to open its Properties window and changing the value of the Caption property to &Pen.

When you add the Width menu item to the Pen menu, it would be a good idea to separate it from the colors in the menu. You can add a separator after the last color menu item by right-clicking the empty menu item and selecting the Insert Separator menu item from the pop-up. You can then enter the new Width item as the next menu item after the separator. The Width menu item ends with an ellipsis (three periods) to indicate that it displays a dialog; this is a standard Windows convention. Double-click the menu to display the menu properties for modification, as shown in Figure 18-3.

FIGURE 18-3

Figure 18.3. FIGURE 18-3

The default ID, ID_PEN_WIDTH, is fine, so you don't need to change that. You can add a status bar prompt for the menu item, and because you'll also add a toolbar button, you can include text for the tooltip as well. Remember, you just put the tooltip text after the status bar prompt text, separated from it by . Here, the value for the Prompt property is "Change pen width Show pen width options."

You need to add toolbar buttons to both toolbars corresponding to the Width menu item. To add the toolbar button to the toolbar that is displayed when you check "Large icons" in the Customize dialog for the application toolbar, open the toolbar resource by extending the Toolbar folder in the Resource View and double-clicking IDR_MAINFRAME_256. You can add a toolbar button to represent a pen width. The one shown in Figure 18-4 tries to represent a pen drawing a line.

FIGURE 18-4

Figure 18.4. FIGURE 18-4

To associate the new button with the menu item that you just added, open the Properties box for the button and specify its ID as ID_PEN_WIDTH, the same as that for the menu item. You then need to repeat the process for the IDR_MAINFRAME toolbar.

Code to Display the Dialog

The code to display the dialog goes in the handler for the Pen

Code to Display the Dialog

Right-click the Width menu item in the Resource View pane for the IDR_SketcherTYPE menu and select Add Event Handler from the pop-up. You can then create a function for the COMMAND message handler corresponding to ID_PEN_WIDTH in the CSketcherDoc class. Now edit this handler and enter the following code:

// Handler for the pen width menu item
void CSketcherDoc::OnPenWidth()
{
   CPenDialog aDlg;                    // Create a local dialog object
   // Display the dialog as modal
   aDlg.DoModal();
}

There are just two statements in the handler at the moment. The first creates a dialog object that is automatically associated with your dialog resource. You then display the dialog by calling the DoModal() function for the aDlg object.

Because the handler creates a CPenDialog object, you must add a #include directive for PenDialog.h to the beginning of SketcherDoc.cpp (after the #include directives for stdafx.h and Sketcher.h); otherwise, you'll get compilation errors when you build the program. After you've done that, you can build Sketcher and try out the dialog. It should appear when you click the pen-width toolbar button. Of course, if the dialog is to do anything, you still have to add the code to support the operation of the controls; to close the dialog, you can use either of the buttons or the close icon in the title bar.

Code to Close the Dialog

The OK and Cancel buttons (and the close icon on the title bar) already close the dialog. The handlers to deal with the BN_CLICKED event handlers for the OK and Cancel button controls have been implemented for you. However, it's useful to know how the action of closing the dialog is implemented, in case you want to do more before the dialog is finally closed, or if you are working with a modeless dialog.

The CDialog class defines the OnOK() method that is called when you click the default OK button, which has IDOK as its ID. This function closes the dialog and causes the DoModal() method to return the ID of the default OK button, IDOK. The OnCancel() function is called when you click the default Cancel button in the dialog; this closes the dialog, and DoModal() returns the button ID, which is IDCANCEL. You can override either or both of these functions in your dialog class to do what you want. You just need to make sure you call the corresponding base class function at the end of your function implementation. You'll probably remember by now that you can add an override class by clicking the override button in the Properties window for the class.

For example, you could implement an override for the OnOK() function as follows:

void CPenDialog::OnOK()
{
  // Your code for data validation or other actions...

  CDialog::OnOK();                     // Close the dialog
}

In a complicated dialog, you might want to verify that the options selected, or the data that has been entered, is valid. You could put code here to check the state of the dialog and fix up the data, or even to leave the dialog open if there are problems.

Calling the OnOK()function defined in the base class closes the dialog and causes the DoModal() function to return IDOK. Thus, you can use the value returned from DoModal() to detect when the dialog was closed via the OK button.

As I said, you can also override the OnCancel() function in a similar way if you need to do extra cleanup operations before the dialog closes. Be sure to call the base class method at the end of your function implementation.

When you are using a modeless dialog you must implement the OnOK() and OnCancel() function overrides so that they call the inherited DestroyWindow() to terminate the dialog. In this case, you must not call the base class OnOK() or OnCancel() functions, because they do not destroy the dialog window, but merely render it invisible.

SUPPORTING THE DIALOG CONTROLS

For the pen dialog you'll store the selected pen width in a data member, m_PenWidth, of the CPenDialog class. You can either add the data member by right-clicking the CPenDialog class name and selecting from the context menu, or you can add it directly to the class definition as follows:

class CPenDialog : public CDialog
{
// Construction
public:
   CPenDialog(CWnd* pParent = NULL);   // standard constructor

// Dialog Data
   enum { IDD = IDD_PENWIDTH_DLG };

   int m_PenWidth;                     // Record the current pen width

// Plus the rest of the class definition....

};

Note

If you do use the context menu for the class to add m_PenWidth, be sure to add a comment to the member variable definition. This is a good habit to get into, even when the member name looks self-explanatory.

You'll use the m_PenWidth data member to set as checked the radio button corresponding to the current pen width in the document. You'll also arrange for the pen width selected in the dialog to be stored in this member, so that you can retrieve it when the dialog closes. At this point you could arrange to initialize m_PenWidth to 0 in the CPenDialog class constructor.

Initializing the Controls

You can initialize the radio buttons by overriding the OnInitDialog() function defined in the base class, CDialog. This function is called in response to a WM_INITDIALOG message, which is sent during the execution of DoModal() just before the dialog box is displayed. You can add the function to the CPenDialog class by selecting OnInitDialog in the list of overrides in the Properties window for the CPenDialog class. The implementation for the new version of OnInitDialog() is:

BOOL CPenDialog::OnInitDialog()
{
   CDialog::OnInitDialog();
   // Check the radio button corresponding to the pen width
   switch(m_PenWidth)
   {
      case 1:
         CheckDlgButton(IDC_PENWIDTH1,1);
         break;
      case 2:
         CheckDlgButton(IDC_PENWIDTH2,1);
         break;
      case 3:
         CheckDlgButton(IDC_PENWIDTH3,1);
         break;
      case 4:
         CheckDlgButton(IDC_PENWIDTH4,1);
         break;
      case 5:
         CheckDlgButton(IDC_PENWIDTH5,1);
         break;
      default:
         CheckDlgButton(IDC_PENWIDTH0,1);
   }
   return TRUE;  // return TRUE unless you set the focus to a control
                 // EXCEPTION: OCX Property Pages should return FALSE
}

You should leave the call to the base class function there because it does some essential setup for the dialog. The switch statement checks one of the radio buttons, depending on the value set in the m_PenWidth data member. This implies that you must arrange to set m_PenWidth to a suitable value before you execute DoModal() because the DoModal() function causes the WM_INITDIALOG message to be sent, resulting in your version of OnInitDialog() being called.

The CheckDlgButton() function is inherited indirectly from CWnd through CDialog. The first argument identifies the button, and the second argument, of type UINT, sets its check status. If the second argument is 1, it checks the button corresponding to the ID you specify in the first argument. If the second argument is 0, the button is unchecked. This function works with both checkboxes and radio buttons.

Handling Radio Button Messages

After the dialog box is displayed, every time you click one of the radio buttons a message is generated and sent to the application. To deal with these messages, you can add handlers to the CPenDialog class. Return to the dialog resource that you created, right-click each of the radio buttons in turn, and select Add Event Handler from the pop-up to create a handler for the BN_CLICKED message. Figure 18-5 shows the event handler dialog window for the button that has IDC_PENWIDTH0 as its ID. Note that I have edited the name of the handler, as the default name was a little cumbersome.

FIGURE 18-5

Figure 18.5. FIGURE 18-5

The implementations of the BN_CLICKED event handlers for all of these radio buttons are similar because each just sets the pen width in the dialog object. As an example, the handler for IDC_PENWIDTH0 is as follows:

void CPenDialog::OnPenwidth0()
{
   m_PenWidth = 0;
}

You need to add the code for all six handlers to the CPenDialog class implementation, setting m_PenWidth to 1 in OnPenwidth1(), to 2 in OnPenwidth2(), and so on.

COMPLETING DIALOG OPERATIONS

You must now modify the OnPenWidth() handler in CSketcherDoc to make the dialog effective. Add the following code to the function:

// Handler for the pen width menu item
void CSketcherDoc::OnPenWidth()
{
   CPenDialog aDlg;     // Create a local dialog object
   // Set the pen width in the dialog to that stored in the document
   aDlg.m_PenWidth = m_PenWidth;
   // Display the dialog as modal
   // When closed with OK, get the pen width
   if(aDlg.DoModal() == IDOK)
   {
      m_PenWidth = aDlg.m_PenWidth;
   }
}

The m_PenWidth member of the aDlg object is passed a pen width stored in the m_PenWidth member of the document; you still have to add this member to CSketcherDoc. The call of the DoModal() function now occurs in the condition of the if statement, which is true if the DoModal() function returns IDOK. In this case you retrieve the pen width stored in the aDlg object and store it in the m_PenWidth member of the document. If the dialog box is closed by means of the Cancel button, the escape key on the keyboard, or the close icon, IDOK won't be returned by DoModal(), and the value of m_PenWidth in the document will not be changed.

Note that even though the dialog box is closed when DoModal() returns a value, the aDlg object still exists, so you can call its member functions without any problem. The aDlg object is destroyed automatically, on return from OnPenWidth().

All that remains to do to support variable pen widths in your application is to update the affected classes: CSketcherDoc, CElement, and the four shape classes derived from CElement.

Adding Pen Widths to the Document

You need to add the m_PenWidth member to the document class, and the GetPenWidth() function to allow external access to the value stored. You should add the following bolded statements to the CSketcherDoc class definition:

class CSketcherDoc : public CDocument
{
// the rest as before...

protected:
// the rest as before...
   int m_PenWidth;                     // Current pen width

// Operations
public:
// the rest as before...
   int GetPenWidth() const             // Get the current pen width
      { return m_PenWidth; }

// the rest as before...
};

Because it's trivial, you can define the GetPenWidth() function in the definition of the class and gain the benefit of its being implicitly inline. You still need to add initialization for m_PenWidth to the constructor for CSketcherDoc, so modify the constructor in SketcherDoc.cpp to initialize m_PenWidth to 0.

Adding Pen Widths to the Elements

You have a little more to do to the CElement class and the shape classes that are derived from it. You already have a member m_PenWidth in CElement to store the width to be used when you are drawing an element, and you must extend each of the constructors for elements to accept a pen width as an argument, and set the member in the class accordingly. The GetBoundRect() function in CElement must be altered to deal with a pen width of 0. You can modify the CElement class first. The new version of the GetBoundRect() function in the CElement class is as follows:

// Get the bounding rectangle for an element
CRect CElement::GetBoundRect() const
{
   CRect boundingRect(m_EnclosingRect);      // Object to store bounding rectangle
   //Increase the bounding rectangle by the pen width
   int Offset = m_PenWidth == 0 ? 1 : m_PenWidth; // Width must be at least 1
   boundingRect.InflateRect(Offset, Offset);
   return BoundingRect;
}

You use the local variable Offset to ensure that you pass the InflateRect() function a value of 1 if the pen width is 0 (a pen width of 0 always draws a line one pixel wide), and that you pass the actual pen width in all other cases.

Each of the constructors for CLine, CRectangle, CCircle, and CCurve must be modified to accept a pen width as an argument, and to store it in the inherited m_PenWidth member of the class. The declaration for the constructor in each class definition needs to be modified to add the extra parameter. For example, in the CLine class, the constructor declaration becomes:

CLine(const CPoint& start, const CPoint& end, COLORREF aColor, int penWidth);

And the constructor implementation should be modified to this:

CLine::CLine(const CPoint& start, const CPoint& end, COLORREF aColor, int penWidth)
 :m_StartPoint(start), m_EndPoint(end)
{
   m_PenWidth = penWidth;              // Set pen width
   m_Color = aColor;                   // Set line color

   // Define the enclosing rectangle
   m_EnclosingRect = CRect(Start, End);
   m_EnclosingRect.NormalizeRect();
}

You should modify each of the class definitions and constructors for the shapes in the same way, so that each initializes m_PenWidth with the value passed as the last argument.

Creating Elements in the View

The last change you need to make is to the CreateElement() member of CSketcherView. Because you have added the pen width as an argument to the constructors for each of the shapes, you must update the calls to the constructors to reflect this. Change the definition of CSketcherView::CreateElement() to the following:

CElement* CSketcherView::CreateElement()
{
   // Get a pointer to the document for this view
CSketcherDoc* pDoc = GetDocument();
   ASSERT_VALID(pDoc);                       // Verify the pointer is good

   // Now select the element using the type stored in the document
   switch(pDoc->GetElementType())
   {
      case RECTANGLE:
         return new CRectangle(m_FirstPoint, m_SecondPoint,
                            pDoc->GetElementColor(), pDoc->GetPenWidth());
      case CIRCLE:
         return new CCircle(m_FirstPoint, m_SecondPoint,
                            pDoc->GetElementColor(), pDoc->GetPenWidth());
      case CURVE:
         return new CCurve(m_FirstPoint, m_SecondPoint,
                            pDoc->GetElementColor(), pDoc->GetPenWidth());
      case LINE:
         return new CLine(m_FirstPoint, m_SecondPoint,
                            pDoc->GetElementColor(), pDoc->GetPenWidth());
      default:                   // Something's gone wrong
         AfxMessageBox(_T("Bad Element code"), MB_OK);
         AfxAbort();
         return nullptr;
   }
}

Each constructor call now passes the pen width as an argument. This is retrieved from the document with the GetPenWidth() function that you added to the document class.

Exercising the Dialog

You can now build and run the latest version of Sketcher to see how the pen dialog works out. Selecting the Pen

Exercising the Dialog
FIGURE 18-6

Figure 18.6. FIGURE 18-6

Note that the dialog box is a completely separate window. You can drag it around to position it where you want. You can even drag it outside the Sketcher application window.

USING A SPIN BUTTON CONTROL

Now you can move on to looking at how the spin button can help in the Sketcher application. The spin button is particularly useful when you want to constrain an input within a given integer range. It's normally used in association with another control, called a buddy control, that displays the value that the spin button modifies. The associated control is usually an edit control, but it doesn't have to be.

It would be nice to be able to draw at different viewing scales in Sketcher. If you had a way to change the scale, you could scale up whenever you wanted to fill in the fine detail in your masterpiece, and scale down again when working across the whole vista. You could apply the spin control to managing scaling in a document view. A drawing scale would be a view-specific property, and you would want the element drawing functions to take the current scale of a view into account. Altering the existing code to deal with view scaling requires rather more work than setting up the control, so first look at how you create a spin button and make it work.

Adding the Scale Menu Item and Toolbar Button

The first step is to provide a means of displaying the scale dialog. Go to Resource View and open the IDR_SketcherTYPE menu. You are going to add a Scale menu item to the end of the View menu. Enter the caption for the unused menu item as Scale. . . . This item will bring up the scale dialog, so you end the caption with an ellipsis (three periods) to indicate that it displays a dialog. Next you can add a separator before the new menu item by right-clicking it and selecting Insert Separator from the pop-up. The menu should now look as shown in Figure 18-7.

FIGURE 18-7

Figure 18.7. FIGURE 18-7

You can also add toolbar buttons to both toolbars for this menu item. All you need to do is make sure that the ID for each new button is also set to ID_VIEW_SCALE.

Creating the Spin Button

You've got the menu item; you better have a dialog to go with it. In Resource View, add a new dialog by right-clicking the Dialog folder on the tree and selecting Insert Dialog from the pop-up. Change the ID to IDD_SCALE_DLG and the Caption property to Set Drawing Scale.

Click the spin control in the palette, and then click on the position in the dialog where you want it to be placed. Next, right-click the spin control to display its properties. Change its ID to something more meaningful than the default, such as IDC_SPIN_SCALE. Now take at look at the properties for the spin button. They are shown in Figure 18-8.

FIGURE 18-8

Figure 18.8. FIGURE 18-8

The Arrow Keys property is already set to True, enabling you to operate the spin button by using arrow keys on the keyboard. You should also set to true the value for both the Set Buddy Integer property, which specifies the buddy control value as an integer, and Auto Buddy, which provides for automatic selection of the buddy control. The effect of the latter is that the control selected as the buddy is automatically the previous control defined in the dialog. At the moment this is the Cancel button, which is not exactly ideal, but you'll see how to change this in a moment. The Alignment property determines how the spin button is displayed in relation to its buddy. You should set this to Right Align so that the spin button is attached to the right edge of its buddy control.

Next, add an edit control at the left side of the spin button by selecting the edit control from the list in the toolbox pane and clicking in the dialog where you want it positioned. Change the ID for the edit control to IDC_SCALE.

To make the contents of the edit control quite clear, you could add a static control just to the left of the edit control in the dialog and enter View Scale: as the caption. You can select all three controls by clicking them while holding down the Shift key. Pressing the F9 function key aligns the controls tidily, or you can use the Format menu.

The Controls' Tab Sequence

Controls in a dialog have what is called a tab sequence. This is the sequence in which the focus shifts from one control to the next when you press the tab key, determined initially by the sequence in which controls are added to the dialog. You can see the tab sequence for the current dialog box by selecting Format

The Controls' Tab Sequence
FIGURE 18-9

Figure 18.9. FIGURE 18-9

If the tab order you see is different, you have to change it. You really want the edit control to precede the spin button in the tab sequence, so you need to select the controls by clicking them in the order in which you want them to be numbered: OK button; Cancel button; edit control; spin button; and finally the static control. So, the tab order will be as shown in Figure 18-9. Now the edit control is selected as the buddy to the spin button.

Generating the Scale Dialog Class

After saving the resource file, you can right-click the dialog and select Add Class from the pop-up at the cursor. You'll then be able to define the new class associated with the dialog resource that you have created. You should name the class CScaleDialog and select the base class as CDialog. Clicking the Finish button adds the class to the Sketcher project.

You need to add a variable to the dialog class that stores the value returned from the edit control, so right-click the CScaleDialog class name in the Class View and select Add

Generating the Scale Dialog Class
FIGURE 18-10

Figure 18.10. FIGURE 18-10

Figure 18-10 shows how the window for the Add Member Variable Wizard should look when you are done.

When you click the Finish button, the wizard takes care of entering the code necessary to support your new control variable.

You will also need to access the spin control in the dialog, and you can use the Add Member Variable Wizard to create that, too. Right-click CScaleDialog in Class View once again and click the "Control variable" checkbox. Leave the Category selection as Control and select IDC_SPIN_SCALE as the control ID: this corresponds to the spin control. You can now enter the variable name as m_Spin and add a suitable comment. When you click the Finish button, the new variable will be added to the CScaleDialog class.

The class definition you'll end up with after the wizard has added the new members is as follows:

class CScaleDialog : public CDialog
{
  DECLARE_DYNAMIC(CScaleDialog)

public:
  CScaleDialog(CWnd* pParent = NULL);   // standard constructor
  virtual ~CScaleDialog();

// Dialog Data
  enum { IDD = IDD_SCALE_DLG };

protected:
  virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV support

  DECLARE_MESSAGE_MAP()
public:
  // Stores the current drawing scale
  int m_Scale;
  // Spin control for view scale
  CSpinButtonCtrl m_Spin;

};

The interesting bits of the class definition are bolded. The class is associated with the dialog resource through the enum statement, initializing IDD with the ID of the resource. It contains the variable m_Scale, which is specified as a public member of the class, so you can set and retrieve its value in a CScaleDialog object directly. There's also some special code in the implementation of the class to deal with the new m_Scale member. The m_Spin variable references the CSpinButtonCtrl object so you can call functions for the spin control.

Dialog Data Exchange and Validation

A virtual function called DoDataExchange() has been included in the class by the Class Wizard. If you look in the ScaleDialog.cpp file, you'll find that the implementation looks like this:

void CScaleDialog::DoDataExchange(CDataExchange* pDX)
{
   CDialog::DoDataExchange(pDX);
   DDX_Text(pDX, IDC_SCALE, m_Scale);
   DDV_MinMaxInt(pDX, m_Scale, 1, 8);
   DDX_Control(pDX, IDC_SPIN_SCALE, m_Spin);
}

This function is called by the framework to carry out the exchange of data between variables in a dialog and the dialog's controls. This mechanism is called dialog data exchange, usually abbreviated to DDX. This is a powerful mechanism that can provide automatic transfer of information between a dialog and its controls in most circumstances, thus saving you the effort of programming to get the data yourself, as you did with the radio buttons in the pen width dialog.

In the scale dialog, DDX handles data transfers between the edit control and the variable m_Scale in the CScaleDialog class. The variable pDX, passed to the DoDataExchange() function, controls the direction in which data is transferred. After the base class DoDataExchange() function is called, the DDX_Text() function is called. The latter actually moves data between the variable m_Scale and the edit control.

The call to the DDV_MinMaxInt() function verifies that the value transferred is within the limits specified. This mechanism is called dialog data validation, or DDV. The DoDataExchange() function is called automatically before the dialog is displayed to pass the value stored in m_Scale to the edit control. When the dialog is closed with the OK button, it is automatically called again to pass the value in the control back to the variable m_Scale in the dialog object. All this is taken care of for you. You need only to ensure that the right value is stored in m_Scale before the dialog box is displayed, and arrange to collect the result when the dialog box closes.

Initializing the Dialog

You'll use the OnInitDialog() function to initialize the dialog, just as you did for the pen width dialog. This time you'll use it to set up the spin control. You'll initialize the m_Scale member a little later when you create the dialog in the handler for a Scale menu item, because it should be set to the value of the scale stored in the view. For now, add an override for the OnInitDialog() function to the CScaleDialog class, using the same mechanism you used for the previous dialog, and add code to initialize the spin control as follows:

BOOL CScaleDialog::OnInitDialog()
{
   CDialog::OnInitDialog();

   // If you have not checked the auto buddy option in
   // the spin control's properties, you can set the buddy control here

   // Set the spin control range
   m_Spin.SetRange(1, 8);

   return TRUE;  // return TRUE unless you set the focus to a control
                 // EXCEPTION: OCX Property Pages should return FALSE
}

There is only one line of code to add. This sets the upper and lower limits for the spin button by calling the SetRange() member of the spin control object. Although you have set the range limits for the edit control, this doesn't affect the spin control directly. If you don't limit the values in the spin control here, you allow the spin control to insert values outside the limits in the edit control, and there will be an error message from the edit control. You can demonstrate this by commenting out the statement that calls SetRange() here and trying out Sketcher without it.

If you want to set the buddy control using code, rather than by setting the value of Auto buddy in the spin button's properties to True, the CSpinButtonCtrl class has a function member to do this. You need to add the statement

mSpin.SetBuddy(GetDlgItem(IDC_SCALE));

at the point indicated by the comments.

Note

You can also access controls in a dialog programmatically. The function GetDlgItem() is inherited from CWnd via CDialog, and you can use it to retrieve the address of any control from the ID you pass as the argument. Thus, calling GetDlgItem() with IDC_SPIN_SCALE as the argument would return the address of the spin control. As you saw earlier, a control is just a specialized window, so the pointer returned is of type CWnd*; you therefore have to cast it to the type appropriate to the particular control, which would be CSpinButtonCtrl* in this case.

Displaying the Spin Button

The dialog is to be displayed when the Scale menu option (or its associated toolbar button) is selected, so you need to add a COMMAND event handler to the CSketcherView class corresponding to the ID_VIEW_SCALE message through the Properties window for the class. You can then add code as follows:

void CSketcherView::OnViewScale()
{
  CScaleDialog aDlg;                   // Create a dialog object
  aDlg.m_Scale = m_Scale;              // Pass the view scale to the dialog
  if(aDlg.DoModal() == IDOK)
{
    m_Scale = aDlg.m_Scale;            // Get the new scale
    InvalidateRect(0);                 // Invalidate the whole window
  }
}

You create the dialog as a modal dialog, just as you did the pen width dialog. Before the dialog box is displayed by the DoModal() function call, you store the scale value provided by the m_Scale member of CSketcherView in the dialog member with the same name; this ensures that the control displays the current scale value when the dialog is displayed. If the dialog is closed with the OK button, you store the new scale from the m_Scale member of the dialog object in the view member with the same name. Because you have changed the view scale, you need to get the view redrawn with the new scale value applied. The call to InvalidateRect() does this. Don't forget to add an #include directive for ScaleDialog.h to SketcherView.cpp.

Of course, you must not forget to add the m_Scale data member to the definition of CSketcherView, so add the following line at the end of the other data members in the class definition:

int m_Scale;                         // Current view scale

You should also modify the CSketcherView constructor to initialize m_Scale to 1. This results in a view always starting out with a scale of one to one.

That's all you need to get the scale dialog and its spin control operational. You can build and run Sketcher to give it a trial spin before you add the code to use a view scale factor in the drawing process.

USING THE SCALE FACTOR

Scaling with Windows usually involves using one of the scalable mapping modes, MM_ISOTROPIC or MM_ANISOTROPIC. By using one of these mapping modes you can get Windows to do most of the work. Unfortunately, it's not as simple as just changing the mapping mode, because neither is supported by CScrollView. If you can get around that, however, you're home and dry. You'll use MM_ANISOTROPIC for reasons that you'll see in a moment, so let's first understand what's involved in using this mapping mode.

Scalable Mapping Modes

As I've said, there are two mapping modes that allow the mapping between logical coordinates and device coordinates to be altered, and these are the MM_ISOTROPIC and MM_ANISOTROPIC modes. The MM_ISOTROPIC mode has a property that forces the scaling factor for both the x- and y-axes to be the same, which has the advantage that your circles will always be circles. The disadvantage is that you can't map a document to fit into a rectangle of a different aspect ratio. The MM_ANISOTROPIC mode, on the other hand, permits scaling of each axis independently. Because it's the more flexible mode of the two, you'll use MM_ANISOTROPIC for scaling operations in Sketcher.

The way in which logical coordinates are transformed to device coordinates is dependent on the following parameters, which you can set:

PARAMETER

DESCRIPTION

Window Origin

The logical coordinates of the upper left corner of the window. You set this by calling the function CDC::SetWindowOrg().

Window Extent

The size of the window specified in logical coordinates. You set this by calling the function CDC::SetWindowExt().

Viewport Origin

The coordinates of the upper left corner of the window in device coordinates (pixels). You set this by calling the function CDC::SetViewportOrg().

Viewport Extent

The size of the window in device coordinates (pixels). You set this by calling the function CDC::SetViewportExt().

The viewport referred to here has no physical significance by itself; it serves only as a parameter for defining how coordinates are transformed from logical coordinates to device coordinates.

Remember the following:

  • Logical coordinates (also referred to as page coordinates) are determined by the mapping mode. For example, the MM_LOENGLISH mapping mode has logical coordinates in units of 0.01 inches, with the origin in the upper left corner of the client area, and the positive y-axis direction running from bottom to top. These are used by the device context drawing functions.

  • Device coordinates (also referred to as client coordinates in a window) are measured in pixels in the case of a window, with the origin at the upper left corner of the client area, and with the positive y-axis direction from top to bottom. These are used outside a device context — for example, for defining the position of the cursor in mouse message handlers.

  • Screen coordinates are measured in pixels and have the origin at the upper left corner of the screen, with the positive y-axis direction from top to bottom. These are used for getting or setting the cursor position.

The formulae used by Windows to convert from logical coordinates to device coordinates are:

Scalable Mapping Modes

With coordinate systems other than those provided by the MM_ISOTROPIC and MM_ANISOTROPIC mapping modes, the window extent and the viewport extent are fixed by the mapping mode and can't be changed. Calling the functions SetWindowExt() or SetViewportExt() in the CDC object to change them has no effect, although you can still move the position of (0,0) in your logical reference frame by calling SetWindowOrg() or SetViewportOrg(). However, for a given document size that is expressed by the window extent in logical coordinate units, you can adjust the scale at which elements are displayed by setting the viewport extent appropriately. By using and setting the window and viewport extents, you can get the scaling done automatically.

Setting the Document Size

You need to maintain the size of the document in logical units in the document object. You can add a protected data member, m_DocSize, to the CSketcherDoc class definition to store the size of the document:

CSize m_DocSize;                     // Document size

You will also want to access this data member from the view class, so add a public function to the CSketcherDoc class definition as follows:

CSize GetDocSize() const            // Retrieve the document size
    { return m_DocSize; }

You must initialize the m_DocSize member in the constructor for the document; modify the implementation of CSketcherDoc() as follows:

CSketcherDoc::CSketcherDoc()
: m_Element(LINE)
, m_Color(BLACK)
, m_PenWidth(0)
, m_DocSize(CSize(3000,3000))
{
  // TODO: add one-time construction code here
}

You'll be using notional MM_LOENGLISH coordinates, so you can treat the logical units as increments of 0.01 inches, and the value set gives you an area of 30 square inches to draw on.

Setting the Mapping Mode

You can set the mapping mode to MM_ANISOTROPIC in an override for the inherited OnPrepareDC() function in the CSketcherView class. This function is always called for any WM_PAINT message, and you have arranged to call it when you draw temporary objects in the mouse message handlers; however, you have to do a little more than just set the mapping mode.

You'll need to create the function override in CSketcherView before you can add the code. Just open the Properties window for the CSketcherView class and click the Overrides toolbar button. You can then add the override by selecting OnPrepareDC from the list and clicking on <Add> OnPrepareDC in the adjacent column. You are now able to type the code directly into the Editor pane. The implementation of OnPrepareDC() is as follows:

void CSketcherView::OnPrepareDC(CDC* pDC, CPrintInfo* pInfo)
{
  CScrollView::OnPrepareDC(pDC, pInfo);
  CSketcherDoc* pDoc = GetDocument();
  pDC->SetMapMode(MM_ANISOTROPIC);     // Set the map mode
  CSize DocSize = pDoc->GetDocSize();  // Get the document size
  pDC->SetWindowExt(DocSize);          // Now set the window extent
  // Get the number of pixels per inch in x and y
  int xLogPixels = pDC->GetDeviceCaps(LOGPIXELSX);
  int yLogPixels = pDC->GetDeviceCaps(LOGPIXELSY);
  // Calculate the viewport extent in x and y
  int xExtent = (DocSize.cx*m_Scale*xLogPixels)/100;
  int yExtent = (DocSize.cy*m_Scale*yLogPixels)/100;
  pDC->SetViewportExt(xExtent,yExtent); // Set viewport extent
}

The override of the base class function is unusual here in that you have left in the call to CScrollView::OnPrepareDC() and added the modifications after it, rather than where the comment in the default code suggests. If the class was derived from CView, you would replace the call to the base class version because it does nothing, but in the case of CScrollView, this isn't the case. You need the base class function to set some attributes before you set the mapping mode. Don't make the mistake of calling the base class function at the end of the override version, though — if you do, scaling won't work.

The CDC member function GetDeviceCaps() supplies information about the device with which the device context is associated. You can get various kinds of information about the device, depending on the argument you pass to the function. In this case the arguments LOGPIXELSX and LOGPIXELSY return the number of pixels per logical inch in the x and y directions, respectively. These values are equivalent to 100 units in your logical coordinates.

You use these values to calculate the x and y values for the viewport extent, which you store in the local variables xExtent and yExtent, respectively. The document extent along an axis in logical units, divided by 100, gives the document extent in inches. If this is multiplied by the number of logical pixels per inch for the device, you get the equivalent number of pixels for the extent. If you then use this value as the viewport extent, you get the elements displayed at a scale of one to one. If you simplify the equations for converting between device and logical coordinates by assuming that the window origin and the viewport origin are both (0,0), they become the following:

Setting the Mapping Mode

If you multiply the viewport extent values by the scale (stored in m_Scale), the elements are drawn according to the value of m_Scale. This logic is exactly represented by the expressions for the x and y viewport extents in your code. The simplified equations, with the scale included, are as follows:

Setting the Mapping Mode

You should be able to see from this that a given pair of device coordinates varies in proportion to the scale value. The coordinates at a scale of three are three times the coordinates at a scale of one. Of course, as well as making elements larger, increasing the scale also moves them away from the origin.

That's all you need in order to scale the view. Unfortunately, at the moment scrolling won't work with scaling, so you need to see what you can do about that.

Implementing Scrolling with Scaling

CScrollView just won't work with the MM_ANISOTROPIC mapping mode, so clearly you must use another mapping mode to set up the scrollbars. The easiest way to do this is to use MM_TEXT, because in this case the units of logical coordinates are the same as the client coordinates — pixels, in other words. All you need to do, then, is figure out how many pixels are equivalent to the logical document extent for the scale at which you are drawing, which is easier than you might think. You can add a function to CSketcherView to take care of the scrollbars and implement it to work out the number of pixels corresponding to the logical document extent. Right-click the CSketcherView class name in Class View and add a public function, ResetScrollSizes(), with a void return type and no parameters. Add the code to the implementation, as follows:

void CSketcherView::ResetScrollSizes(void)
{
  CClientDC aDC(this);
  OnPrepareDC(&aDC);                             // Set up the device context
  CSize DocSize = GetDocument()->GetDocSize();   // Get the document size
  aDC.LPtoDP(&DocSize);                          // Get the size in pixels
  SetScrollSizes(MM_TEXT, DocSize);              // Set up the scrollbars
}

After creating a local CClientDC object for the view, you call OnPrepareDC() to set up the MM_ANISOTROPIC mapping mode. Because this takes scaling into account, the LPtoDP() member of the aDC object converts the document size stored in the local variable DocSize to the correct number of pixels for the current logical document size and scale. The total document size in pixels defines how large the scrollbars must be in MM_TEXT mode — remember, MM_TEXT logical coordinates are in pixels. You can then get the SetScrollSizes() member of CScrollView to set up the scrollbars based on this by specifying MM_TEXT as the mapping mode.

It may seem strange that you can change the mapping mode in this way, but it's important to keep in mind that the mapping mode is nothing more than a definition of how logical coordinates are to be converted to device coordinates. Whatever mode (and therefore coordinate conversion algorithm) you've set up applies to all subsequent device context functions until you change it, and you can change it whenever you want. When you set a new mode, subsequent device context function calls just use the conversion algorithm defined by the new mode. You figure out how big the document is in pixels with MM_ANISOTROPIC because this is the only way you can get the scaling into the process; you then switch to MM_TEXT to set up the scrollbars because you need units for this in pixels for it to work properly. Simple really, when you know how.

Setting Up the Scrollbars

You must set up the scrollbars initially for the view in the OnInitialUpdate() member of CSketcherView. Change the previous implementation of the function to the following:

void CSketcherView::OnInitialUpdate()
{
  ResetScrollSizes();                  // Set up the scrollbars
  CScrollView::OnInitialUpdate();
}

All you do is call the ResetScrollSizes() function that you just added to the view. This takes care of everything — well, almost. The CScrollView object needs an initial extent to be set in order for OnPrepareDC() to work properly, so you need to add one statement to the CSketcherView constructor:

CSketcherView::CSketcherView()
: m_FirstPoint(CPoint(0,0))           // Set 1st recorded point to 0,0
, m_SecondPoint(CPoint(0,0))          // Set 2nd recorded point to 0,0
, m_pTempElement(NULL)                // Set temporary element pointer to 0
, m_pSelected(NULL)                   // No element selected initially
, m_MoveMode(FALSE)                   // Set move mode off
, m_CursorPos(CPoint(0,0))            // Initialize as zero
, m_FirstPos(CPoint(0,0))             // Initialize as zero
, m_Scale(1)                          // Set scale to 1:1
{
   SetScrollSizes(MM_TEXT, CSize(0,0));     // Set arbitrary scrollers
}

The additional statement just calls SetScrollSizes() with an arbitrary extent to get the scrollbars initialized before the view is drawn. When the view is drawn for the first time, the ResetScrollSizes() function call in OnInitialUpdate() sets up the scrollbars properly.

Of course, each time the view scale changes, you need to update the scrollbars before the view is redrawn. You can take care of this in the OnViewScale() handler in the CSketcherView class:

void CSketcherView::OnViewScale()
{
  CScaleDialog aDlg;                   // Create a dialog object
  aDlg.m_Scale = m_Scale;              // Pass the view scale to the dialog
  if(aDlg.DoModal() == IDOK)
{
    m_Scale = aDlg.m_Scale;            // Get the new scale
    ResetScrollSizes();                // Adjust scrolling to the new scale
    InvalidateRect(0);                 // Invalidate the whole window
  }
}

With the ResetScrollSizes() function, taking care of the scrollbars isn't complicated. Everything is covered by the one additional line of code.

Now you can build the project and run the application. You'll see that the scrollbars work just as they should. Note that each view maintains its own scale factor, independently of the other views.

USING THE CTASKDIALOG CLASS

The CTaskDialog class is a new feature of Visual C++ 2010 that enables you to create and display a wide range of message boxes and input dialogs programmatically. Although CTaskDialog is easy to use, it has a couple of limitations. First, it works only if your application is running under Windows Vista or later. If you want to use it, and also support earlier versions of Windows, you have to program the old-fashioned way as an alternative, so it doesn't save you any effort. Second, you have much more flexibility in how your input dialogs look and work if you create dialog resources for them in the way you have seen. Because of these limitations, I'll only discuss the class briefly, and we won't be integrating it into Sketcher.

To use the CTaskDialog class in a source file, you need an #include directive for afxTaskDialog.h. You can program to use this class, but only when it is supported, like this:

if(CTaskDialog::IsSupported)
{
  // Use CTaskDialog
}
else
{
  // Use a dialog derived from CDialog or use AfxMessageBox()
}

The static IsSupported() function in CTaskDialog returns TRUE if the operating system environment on which the application is running supports it.

Displaying a Task Dialog

The simplest way to create a CTaskDialog dialog and display it is to call the static ShowDialog() member of the class. Because you have no explicit CTaskDialog object, you can't do anything beyond the basic options offered by the ShowDialog() function, so this is primarily a more sophisticated alternative to calling AfxMessageBox(). The prototype of the ShowDialog() function is as follows:

static INT_PTR ShowDialog(
const CString& content,                          // Content of the dialog
const CString& mainInstr,                        // Main instruction in the dialog
const CString& title,                            // Title bar text
int nIDCommandFirst,                             // String ID of the first command
int nIDCommandLast,                              // String ID of the last command
int buttons = TDCBF_YES_BUTTON|TDCBF_NO_BUTTON,  // Buttons in the dialog
int nOptions = TDF_ENABLE_HYPERLINKS|TDF_USE_COMMAND_LINKS,  // Dialog options
const CString& footer=_T(""));                   // Footer string in the dialog

The nIDCommandFirst and nIDCommandLast parameters specify a range of IDs that should identify entries in the string table resource in your application. For each valid ID, a command button will be created, and the string from the table will appear as the caption for the command button. You need to make sure that the value for the nIDCommandFirst parameter is greater than the IDs for any other buttons you may be using, such as IDOK and IDCANCEL. If you create the IDs through the resource editor for the string table, this should not be a problem.

The buttons parameter specifies the common buttons that the user can click to close the dialog. In addition to the defaults, you can also use TDCBF_OK_BUTTON, TDCBF_CANCEL_BUTTON, TDCBF_RETRY_BUTTON, and TDCBF_CLOSE_BUTTON. These are single-bit masks that identify the buttons. When you want to have two or more buttons displayed in the dialog, you OR them together.

There are many standard options you can specify via the nOptions parameter, in addition to the default values shown. You can find these in the documentation for the SetOptions() function in the dialog class.

The integer value that is returned reflects the selection made by the user to close the dialog. This will be the ID for the control that was clicked to close the dialog. In the case of a command button being clicked, the ID will be the ID that identifies the entry in the string table. If a common button is clicked to close the dialog, the value returned will be one of IDYES, IDNO, IDOK, IDCANCEL, IDRETRY, or IDCLOSE.

Here's a code fragment showing how you might use the ShowDialog() function:

CString content(_T("Modal Line Type Options"));
    CString mainInst(
              _T("Choose the line type you want to use or click Cancel to exit:"));
    CString title(_T("Line Type Chooser"));
    int result = CTaskDialog::ShowDialog(
      content,
      mainInst,
      title,
      IDS_SOLID_LINE,
      IDS_DASHED_LINE,
      TDCBF_CANCEL_BUTTON
      );

For this fragment to compile and execute successfully, you would need to have defined the symbols IDS_SOLID_LINE, IDS_DOTTED_LINE, IDS_DOTDASH_LINE, and IDS_DASHED_LINE with consecutive values. You can create new symbols by clicking on the string table resource on the Resources pane and pressing the insert key to add a new string. You will then be able to edit the Caption, ID, and Value properties for the new entry.

The dialog that is displayed is shown in Figure 18-11.

FIGURE 18-11

Figure 18.11. FIGURE 18-11

To get the data from this dialog when it closes, you could use the following code:

switch(result)
    {
      case IDS_SOLID_LINE:
       m_LineType = SOLID;
       break;
      case IDS_DOTTED_LINE:
       m_LineType = DOTTED;
       break;
      case IDS_DOTDASH_LINE:
       m_LineType = DOTDASH;
       break;
      case IDS_DASHED_LINE:
       m_LineType = DASHED;
       break;
      case IDCANCEL:
       break;
      default:
        CTaskDialog::ShowDialog(
         _T("Error Choosing Line Type"),
         _T("Invalid return from dialog!"),
         _T("Error"),
         0,
         0,
         TDCBF_OK_BUTTON
         );
        break;
    }

This fragment sets the value of a member variable, m_LineType, depending on the value returned from the ShowDialog() function. The function returns the symbol value corresponding to the command or common button that was clicked to terminate the dialog. There's a provision in the default case for displaying an error dialog using the ShowDialog() function. This supplies string arguments as literals rather than creating CString objects.

Creating CTaskDialog Objects

When you create a CTaskDialog object using the class constructor, you have increased flexibility in what you can do with the dialog because you have functions available that enable you to customize the dialog object. There are two constructors that have parameters similar to those of the ShowDialog() function. One has parameters for specifying a range of IDs for command controls, and the other doesn't. The prototype for the constructor without command control parameters is as follows:

CTaskDialog(
const CString& content,                          // Content of the dialog
const CString& mainInstr,                        // Main instruction in the dialog
const CString& title,                            // Title bar text
int buttons = TDCBF_OK_BUTTON|TDCBF_CANCEL_BUTTON,   // Buttons in the dialog
int nOptions = TDF_ENABLE_HYPERLINKS|TDF_USE_COMMAND_LINKS,  // Dialog options
const CString& footer=_T("")                     // Footer string in the dialog
};

Note that the default button values are different from those of the ShowDialog() function. Here the defaults are for OK and Cancel buttons rather than Yes and No buttons. Here's how you could use this constructor:

CTaskDialog scaleDlg(
_T("Choose the View Scale"),
_T("Click on the view scale you want:"),
_T("View Scale Selector") );

When you use this constructor, you typically customize the object by calling member functions before displaying the dialog.

The more comprehensive constructor adds two parameters that specify the IDs for a range of command controls:

CTaskDialog(
const CString& content,                          // Content of the dialog
const CString& mainInstr,                        // Main instruction in the dialog
const CString& title,                            // Title bar text
int nIDCommandFirst,                             // String ID of the first command
int nIDCommandLast,                              // String ID of the last command
int buttons,                                     // Buttons in the dialog
int nOptions = TDF_ENABLE_HYPERLINKS|TDF_USE_COMMAND_LINKS,  // Dialog options
const CString& footer=_T("")                     // Footer string in the dialog
};

As you see, there are no default button values in this case. This constructor produces an object that encapsulates a dialog that is similar to the one produced by the ShowDialog() function.

Adding Radio Buttons

You can add radio buttons to a CTaskDialog object using the AddRadioButton() function. Here's how you could add radio buttons to the scaleDlg object that was created in the previous section:

const int SCALE_1(1001), SCALE_2(1002), SCALE_3(1003), SCALE_4(1004);
scaleDlg.AddRadioButton(SCALE_1, _T("View scale 1"));
scaleDlg.AddRadioButton(SCALE_2, _T("View scale 2"));
scaleDlg.AddRadioButton(SCALE_3, _T("View scale 3"));
scaleDlg.AddRadioButton(SCALE_4, _T("View scale 4"));
int result = scaleDlg.DoModal();

The first statement defines four integer constants that you use to identify the radio buttons. The AddRadioButton() function calls add four radio buttons, each identified by the first argument, with the annotation for each radio button specified by the second argument. Executing this fragment, following the statement in the previous section that creates scaleDlg, will display the dialog shown in Figure 18-12.

FIGURE 18-12

Figure 18.12. FIGURE 18-12

The first radio button is selected by default. If you wanted to have a different radio button selected by default, you could call the SetDefaultRadioButton() function for the dialog before you display it. For example:

scaleDlg.SetDefaultRadioButton(SCALE_3);

The argument to the function is the ID of the button you want to set as the default selection.

You can remove all the radio buttons from a CTaskDialog object by calling its RemoveAllRadioButtons() function. You would do this if you wanted to add a different set of radio buttons before you re-display the dialog.

Getting Output from Radio Buttons

You can obtain the ID of the radio button that is selected when the dialog closes by calling the GetSelectedRadioButtonID() function for the dialog object. Here's how you can get output from scaleDlg:

if(scaleDlg.DoModal() == IDOK)
  {
    switch(scaleDlg.GetSelectedRadioButtonID())
    {
    case SCALE_1:
      m_Scale = 1;
      break;
    case SCALE_2:
      m_Scale = 2;
      break;
    case SCALE_3:
      m_Scale = 3;
      break;
    case SCALE_4:
      m_Scale = 4;
      break;
    }
  }

You display the dialog by calling DoModal() for scaleDlg. You want to check the state of the radio buttons in the dialog only when the OK button is used to close the dialog, and the if statement verifies that the return from DoModal() corresponds to the OK button. The GetSelectedRadioButtonID() function returns the ID of the radio button that is selected, so the switch statement sets the value of the m_Scale member, depending on the ID that is returned.

The CTaskDialog class is a convenient and more flexible alternative to the AfxMessageBox() function, especially when you want to get user input in addition to communicating a message. There are also many more functions that the CTaskDialog class defines for manipulating and updating the dialog that I have described here. However, creating a dialog resource using the toolbox provides you with a much wider range of controls that you can use and gives you complete flexibility in how the dialog is laid out. Furthermore, when you do use the CTaskDialog class in an application that you want to run with Windows XP or earlier operating systems, you must call the IsSupported() function to verify that the class is supported before you attempt to use it, and you will need to program for an alternative dialog creation process when it isn't. For these reasons, you will find that most dialogs are best created as a dialog resource or through AfxMessageBox().

WORKING WITH STATUS BARS

With each view now being scaled independently, there's a real need to have some indication of what the current scale in a view is. A convenient way to do this would be to display the scale in the status bar for each view window. A status bar was created by default in the Sketcher main application window. Usually, the status bar appears at the bottom of an application window, below the horizontal scrollbar, although you can arrange for it to be at the top of the client area. The status bar is divided into segments called panes; the status bar in the main application window in Sketcher has four panes. The one on the left contains the text "Ready," and the other three are the recessed areas on the right that are used as indicators to record when Caps Lock, Num Lock, and Scroll Lock are in effect.

It's possible for you to write to the status bar that the Application Wizard supplied by default, but you need access to the m_wndStatusBar member of the CMainFrame object for the application, as this represents it. As it's a protected member of the class, you must either add a public member function to modify the status bar from outside the class, or add a member to return a reference to m_wndStatusBar.

You may well have several views of the same sketch, each with its own view scale, so you really want to associate displaying the scale with each view. Our approach will be to give each child window its own status bar. The m_wndStatusBar object in CMainFrame is an instance of the CMFCStatusBar class. You can use the same class to implement your own status bars in the view windows.

Adding a Status Bar to a Frame

The CMFCStatusBar class defines a control bar with multiple panes in which you can display information. We will be using this in a very simple way, but the CMFCStatusBar class provides a great deal of advanced capability, including the ability to display icons in status bar panes. Consult the Visual C++ 2010 documentation for the CMFCStatusBar class for more information on this.

The first step to using CMFCStatusBar is to add a data member for the status bar to the definition of CChildFrame, which is the frame window for a view. Add the following declaration to the public section of the class:

CMFCStatusBar m_StatusBar;              // Status bar object

Note

Status bars should be part of the frame, not part of the view. You don't want to be able to scroll the status bars or draw over them. They should just remain anchored to the bottom of the window. If you added a status bar to the view, it would appear inside the scrollbars and would be scrolled whenever you scrolled the view. Drawing over the part of the view containing the status bar would cause the bar to be redrawn, leading to an annoying flicker. Having the status bar as part of the frame avoids these problems.

Creating Status Bar Panes

To create the panes in the status bar, you call the SetIndicators() function for the status bar object. This function requires two arguments, a const array of indicators of type UINT and the number of elements in the array. Each element in the array is a resource symbol that will be associated with a pane in the status bar, and each resource symbol must have an entry in the resource string table that will be the default text in the pane. There are standard resource symbols, such as ID_INDICATOR_CAPS and ID_INDICATOR_NUM, which are used to identify indicators for the Caps Lock and Num Lock keys respectively, and you can see these in use if you look at the implementation of the OnCreate() function in the CMainFrame class. They also appear in the string table resource.

You can create your own indicator resource symbol by extending the String Table resource in Resource View: click the ⊳ symbol, double-click the String Table entry to display the table, and then press the insert key. If you right-click the new entry in the table and select Properties from the menu, you will be able to change the ID to ID_INDICATOR_SCALE and the caption to View Scale: 1.

You should initialize the m_StatusBar data member just before the visible view window is displayed. So, using the Properties window for the CChildFrame class, you can add a function to the class that will be called in response to the WM_CREATE message that is sent to the application when the window is to be created. Add the following code to the OnCreate() handler:

int CChildFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
  if(CMDIChildWndEx::OnCreate(lpCreateStruct) == -1)
    return −1;

  // Create the status bar
  m_StatusBar.Create(this);
  static UINT indicators[] = {ID_INDICATOR_SCALE};
  m_StatusBar.SetIndicators(indicators, sizeof(indicators)/sizeof(UINT));
  m_StatusBar.SetPaneStyle(0, SBPS_STRETCH);     // Stretch the first pane
  return 0;
}

The generated code isn't bolded. There's a call to the base class version of the OnCreate() function, which takes care of creating the definition of the view window. It's important not to delete this function call; otherwise, the window is not created.

Calling the Create() function for the CMFCStatusBar object creates the status bar. You pass the this pointer for the current CChildFrame object to the Create() function, setting up a connection between the status bar and the window that owns it. The indicators that define the panes in the status bar are typically defined as an array of UINT elements. Here you are interested only in setting up a single pane, so you define indicators as an array that is initialized just with the symbol for your status pane. When you want to add multiple panes to a status bar, you can separate them by including ID_SEPARATOR symbols between your symbols in the indicators array. You call the SetIndicators() function for the status bar object with the address of indicators as the first argument and a count of the number of elements in the array as the second argument. This will create a single pane to the left in the status bar. The call to SetPaneStyle() for the first pane causes this pane to be stretched as necessary, the first argument being the pane index and the second a UINT value specifying the style or styles to be set. Without this, the sizing grip would not remain in the correct position when you resized the window. Only one pane can have the SBPS_STRETCH style. Other styles you can set for a pane are as follows:

SBPS_NORMAL

No stretch, borders, or pop-out styles set.

SBPS_POPOUT

Border reversed, so text pops out.

SBPS_NOBORDERS

No 3D borders.

SBPS_DISABLED

No text drawn in the pane.

You just OR the styles together when you want to set more than one style for a pane.

Updating the Status Bar

If you build and run the code now, the status bars appear, but they show only a scale factor of one, grayed out, no matter what scale factor is actually being used — not very useful. This is because there is no mechanism in place for updating the status bar. What you need to do is add code somewhere that changes the text in the status bar pane each time a different scale is chosen. The obvious place to do this is in the CSketcherView class because that's where the current view scale is recorded.

You can update a pane in the status bar by adding an UPDATE_COMMAND_UI handler that is associated with the indicator symbol for the pane. Right-click CSketcherView in Class View, and select Class Wizard from the pop-up. This will display the MFC Class Wizard dialog. As you can see, this enables you to create and edit a whole range of functions in a class and provides an alternative way to access any class member.

If it is not already visible, select the Commands tab in the dialog: this enables you to add command handlers to the class. Select ID_INDICATOR_SCALE in the Object IDs pane; this ID identifies the status bar pane you want to update. Then, select UPDATE_COMMAND_UI in the Messages pane. The dialog should look like Figure 18-13.

FIGURE 18-13

Figure 18.13. FIGURE 18-13

Click the Add Handler button to add the command handler to the class. Another dialog will be displayed that gives you the opportunity to change the handler function name to OnUpdateScale, which is a little more concise than the default. You can then click OK to close the dialog, and OK again to close the MFC Class Wizard dialog.

All you need is to complete the definition for the handler that was added, so add the following code to the function in SketcherView.cpp:

void CSketcherView::OnUpdateScale(CCmdUI *pCmdUI)
{
    pCmdUI->Enable();
    CString scaleStr;
    scaleStr.Format(_T(" View Scale : %d"), m_Scale);
    pCmdUI->SetText(scaleStr);
}

The parameter is a pointer to a CCmdUI object that encapsulates the status bar pane as a command target. You create a CString object and call its Format() function to generate the text string to be displayed in the pane. The first argument to Format() is a format control string in which you can embed conversion specifiers for subsequent arguments. The format string with the embedded specifiers is the same as for the C function, printf(). Each of the subsequent arguments is converted according to the corresponding format specifier in the first argument, so there must be one format specifier in the format string for each argument after the first. Here the %d specifier converts the value of m_Scale to a decimal string, and this is incorporated into the format string. Other common specifiers you can use are %f for floating point values and %s for string values. Calling SetText() for the CCmdUI object sets the string you supply as the argument in the status bar pane. Calling Enable() for the CCmdUI object causes the pane text to be displayed normally because the BOOL parameter has a default value of TRUE. An explicit argument of FALSE would make the pane text grayed out.

That's all you need for the status bar. If you build Sketcher again, you should have multiple, scrolled windows, each at different scales, with the scale displayed in the status bar in each view.

USING A LIST BOX

Of course, you don't have to use a spin button to set the scale. You could also use a list box control, for example. The logic for handling a scale factor would be exactly the same, and only the dialog box and the code to extract the value for the scale factor from it would change. If you want to try this out without messing up the development of the Sketcher program, you can copy the complete Sketcher project to another folder and make the modifications to the copy. Deleting part of a Class Wizard–managed program can be a bit messy, so it's a useful experience to have before you really need to do it.

Removing the Scale Dialog

You first need to delete the definition and implementation of CScaleDialog from the copy of the Sketcher project, as well as the resource for the scale dialog. To do this, go to the Solution Explorer pane, select ScaleDialog.cpp, and press the delete key. Click the Delete button in the dialog that is displayed, unless you want to keep the file; then select ScaleDialog.h and remove it from the project in the same way. Go to Resource View, expand the Dialog folder, click IDD_SCALE_DLG, and press the delete key to remove the dialog resource. Delete the #include directive for ScaleDialog.h from SketcherView.cpp.

At this stage, all references to the original dialog class have been removed from the project. Are you all done yet? Almost. The IDs for the resources should have been deleted for you. To verify this, right-click Sketcher.rc in Resource View and select the Resource Symbols menu item from the pop-up; you can check that IDC_SCALE and IDC_SPIN_SCALE are no longer in the list. Of course, the OnViewScale() handler in the CSketcherView class still refers to CScaleDialog, so the Sketcher project won't compile yet. You'll fix that when you have added the list box control.

Select the Build

Removing the Scale Dialog

Creating a List Box Control

Right-click the Dialog folder in Resource View, and add a new dialog with a suitable ID and caption. You could use the same ID as before, IDD_SCALE_DLG.

Select the list box button in the list of controls, and click where you want the list box to be positioned in the dialog box. You can enlarge the list box and adjust its position in the dialog by dragging it appropriately. Right-click the list box and select Properties from the pop-up. You can set the ID to something suitable, such as IDC_SCALE_LIST, as shown in Figure 18-14.

FIGURE 18-14

Figure 18.14. FIGURE 18-14

The Sort property will be True by default, so make sure you set it to False. This means that strings that you add to the list box are not automatically sorted. Instead, they're appended to the end of the list in the box, and are displayed in the sequence in which you enter them. Because you will be using the position of the selected item in the list to indicate the scale, it's important not to have the sequence changed. The list box has a vertical scrollbar for the list entries by default, and you can accept the defaults for the other properties. If you want to look into the effects of the other properties, you can click each of them in turn to display text at the bottom of the Properties window explaining what the property does.

Now that the dialog is complete, you can save it, and you're ready to create the class for the dialog.

Creating the Dialog Class

Right-click the dialog and select Add Class from the pop-up. Again, you'll be taken to the dialog to create a new class. Give the class an appropriate name, such as the one you used before, CScaleDialog, and select CDialog as the base class. If, when you click Finish, you get a message box saying that ScaleDialog.cpp already exists, you forgot to explicitly delete the .h and .cpp files. Go back and do that now, or rename the files if you want to keep them. Everything should then work as it's supposed to. After you've completed that, all you need to do is add a public control variable called m_Scale to the class, corresponding to the list box ID, IDC_SCALE_LIST. The type should be int and the limits should be 0 and 7. Don't forget to set the Category as Value; otherwise, you won't be able to enter the limits. Because you have created it as a control variable, DDX is implemented for the m_Scale data member, and you will use the variable to store a zero-based index to one of the eight entries in the list box.

You need to initialize the list box in the OnInitDialog() override in the CScaleDialog class, so add an override for this function using the Properties window for the class. Add code to the function as follows:

BOOL CScaleDialog::OnInitDialog()
{
  CDialog::OnInitDialog();

  CListBox* pListBox = static_cast<CListBox*>(GetDlgItem(IDC_SCALE_LIST));
  CString scaleStr;
  for(int i = 1 ; i <= 8 ; ++i)
  {
    scaleStr.Format(_T("Scale %d"), i);
    pListBox->AddString(scaleStr);
  }
  pListBox->SetCurSel(m_Scale);        // Set current scale

  return TRUE;  // return TRUE unless you set the focus to a control
                // EXCEPTION: OCX Property Pages should return FALSE
}

The first line that you have added obtains a pointer to the list box control by calling the GetDlgItem() member of the dialog class. This is inherited from the MFC class, CWnd. It returns a pointer of type CWnd*, so you cast this to type CListBox*, which is a pointer to the MFC class defining a list box.

Using the pointer to the dialog's CListBox object, you then use the AddString() member repeatedly in the for loop to add the lines defining the list of scale factors. Each entry in the list box is formed by a call to the Format() member of the scaleStr object. The statement following the for loop sets the currently selected entry in the list box to correspond to the value of m_Scale. These entries appear in the list box in the order in which you enter them, so that the dialog is displayed as shown in Figure 18-15.

FIGURE 18-15

Figure 18.15. FIGURE 18-15

Each entry in the list is associated with a zero-based index value that is automatically stored in the m_Scale member of CScaleDialog through the DDX mechanism. Thus, if you select the third entry in the list, m_Scale is set to 2.

Displaying the Dialog

The dialog is displayed by the OnViewScale() handler that you added to CSketcherView in the previous version of Sketcher. You need only to amend this function to deal with the new dialog, using a list box, the code for it is as follows:

void CSketcherView::OnViewScale()
{
  CScaleDialog aDlg;                   // Create a dialog object
  aDlg.m_Scale = m_Scale - 1;          // Pass the view scale to the dialog
  if(aDlg.DoModal() == IDOK)
  {
    m_Scale = 1 + aDlg.m_Scale;        // Get the new scale

    ResetScrollSizes();                // Adjust scrolling to the new scale
    InvalidateRect(0);                 // Invalidate the whole window
  }
}

Because the index value for the entry selected from the list is zero-based, you must decrement the current scale value by one when setting the value in the CScaleDialog object. You also need to add one to the value obtained from the dialog to get the actual scale value to be stored in the view. The code to display this value in the view's status bar is exactly as before. The rest of the code to handle scale factors is already complete and requires no changes. After you've added back the #include directive for ScaleDialog.h, you can build and execute this version of Sketcher to see the list box in action.

USING AN EDIT BOX CONTROL

You could use an edit box control to add annotations to a sketch in Sketcher. You'll need a new element type, CText, that corresponds to a text string, and an extra menu item to set a TEXT mode for creating elements. Because a text element needs only one reference point, you can create it in the OnLButtonDown() handler in the view class. You'll also need a new item in the Element menu to set TEXT mode. You'll add this text capability to Sketcher in the following sequence:

  1. Create a dialog resource and its associated class with an edit box control for input.

  2. Add the new menu item.

  3. Add the code to open the dialog for creating an element.

  4. Add the support for a CText class.

Creating an Edit Box Resource

Create a new dialog resource in Resource View by right-clicking the Dialog folder and selecting Insert Dialog from the pop-up. Change the ID for the new dialog to IDD_TEXT_DLG, and the caption text to Enter Text.

To add an edit box, select the edit control icon from the list of controls and then click the position in the dialog where you want to place it. You can adjust the size of the edit control by dragging its borders, and you can alter its position in the dialog by dragging the whole thing around. You can display the properties for the edit box by right-clicking it and selecting Properties from the pop-up. You could first change its ID to IDC_EDIT_TEXT, as shown in Figure 18-16.

FIGURE 18-16

Figure 18.16. FIGURE 18-16

Some of the properties for this control are of interest at this point. First, select the Multiline property. Setting the value for this as True creates a multiline edit box in which the text you enter can span more than one line. This enables you to enter a long line of text that will still remain visible in its entirety in the edit box. The Align text property determines how the text is to be positioned in the multiline edit box. The value Left is fine here, but you also have the options for Center and Right.

If you were to change the value for the Want return property to True, pressing Enter on the keyboard while entering the text in the control would insert a return character into the text string. This enables you to analyze the string if you want to break it into multiple lines for display. You don't want this effect, so leave the property value as False. In this state, pressing enter has the effect of selecting the default control (which is the OK button), so pressing enter closes the dialog.

If you set the value of the Auto HScroll property to False, there is an automatic spill to the next line in the edit box when you reach the edge of the control while entering text. However, this is just for visibility in the edit box — it has no effect on the contents of the string. You could also change the value of the Auto VScroll property to True to allow text to continue beyond the number of lines that are visible in the control. If you set the Vertical Scroll property to True, the edit control will be supplied with a scrollbar that will allow you to scroll the text.

When you've finished setting the properties for the edit box, close its Properties window. Make sure that the edit box is first in the tab order by selecting the Format

FIGURE 18-16
FIGURE 18-17

Figure 18.17. FIGURE 18-17

You can even enter text into the dialog in test mode to see how it works. Clicking the OK or Cancel button closes the dialog.

Creating the Dialog Class

After saving the dialog resource, you can create a suitable dialog class corresponding to the resource, which you could call CTextDialog. To do this, right-click the dialog in Resource View and select Add Class from the pop-up. The base class should be CDialog. Next you can add a control variable to the CTextDialog class by right-clicking the class name in Class View and selecting Add

Creating the Dialog Class
FIGURE 18-18

Figure 18.18. FIGURE 18-18

A length of 100 is more than adequate for your needs. The variable that you have added here is automatically updated from the data entered into the control by the DDX mechanism. You can click Finish to create the variable in the CTextDialog class, and close the Add Member Variable wizard.

The CString Class

You have already used CString objects a couple of times in Sketcher, but there's more to it than you have seen so far. The CString class provides a very convenient and easy-to-use mechanism for handling strings that you can use just about anywhere a string is required. To be more precise, you can use a CString object in place of strings of type const char*, which is the usual type for a character string in native C++, or of type LPCTSTR, which is a type that comes up frequently in Windows API functions.

The CString class provides several overloaded operators, as shown in the following table, that make it easy to process strings.

OPERATOR

USAGE

=

Copies one string to another, as in:

str1 = str2;
str1 = _T("A normal string");

+

Concatenates two or more strings, as in:

str1 = str2 + str3 + _T(" more");

+=

Appends a string to an existing CString object, as in:

str1 += str2;

==

Compares two strings for equality, as in:

if(str1 == str2) // do something...

<

Tests if one string is less than another.

<=

Tests if one string is less than or equal to another.

>

Tests if one string is greater than another.

>=

Tests if one string is greater than or equal to another.

The variables str1 and str2 in the preceding table are CString objects.

CString objects automatically grow as necessary, such as when you add an additional string to the end of an existing object. For example:

CString str = _T("A fool and your money ");
str += _T("are soon partners.");

The first statement declares and initializes the object str. The second statement appends an additional string to str, so the length of str automatically increases.

Adding the Text Menu Item

Adding a new menu item should be easy by now. You just need to open the menu resource with the ID IDR_SketcherTYPE in Resource View by double-clicking it, and add a new menu item, Text, to the Element menu. The default ID, ID_ELEMENT_TEXT, that appears in the Properties window for the item is fine, so you can leave that as it is. You can add a prompt to be displayed on the status bar corresponding to the menu item, and because you'll also want to add an additional toolbar button corresponding to this menu item, you can add a tooltip to the end of the prompt line, using to separate the prompt and the tooltip.

Don't forget the context menu. You can copy the menu item from IDR_SketcherTYPE. Right-click the Text menu item and select Copy from the pop-up. Open the menu IDR_NOELEMENT_MENU, right-click the empty item at the bottom, and select Paste. All you need to do then, is drag the item to the appropriate position — above the separator — and save the resource file.

Add the toolbar button to the IDR_MAINFRAME_256 toolbar and set its ID to the same as that for the menu item, ID_ELEMENT_TEXT. You can drag the new button so that it's positioned at the end of the block defining the other types of elements. Do the same for the IDR_MAINFRAME toolbar that is used for small toolbar icons. When you've saved the resources, you can add an event handler for the new menu item.

In the Class View pane, right-click CSketcherDoc and display its Properties window. Add a COMMAND handler for the ID_ELEMENT_TEXT ID and add code to it as follows:

void CSketcherDoc::OnElementText()
{
  m_Element = TEXT;
}

Only one line of code is necessary to set the element type in the document to TEXT.

You also need to add a function to check the menu item if it is the current mode, so add an UPDATE_COMMAND_UI handler corresponding to the ID_ELEMENT_TEXT ID, and implement the code for it as follows:

void CSketcherDoc::OnUpdateElementText(CCmdUI* pCmdUI)
{
  // Set checked if the current element is text
  pCmdUI->SetCheck(m_Element == TEXT);
}

This operates in the same way as the other Element pop-up menu items. Of course, you could also have added both of these handlers through the Class Wizard dialog that you display by selecting Class Wizard from the context menu in Class View.

You must also add an additional entry to the ElementType enum in the SketcherConstants.h header file:

enum ElementType{LINE, RECTANGLE, CIRCLE, CURVE, TEXT};

The next step is to define the CText class for an element of type TEXT.

Defining a Text Element

You can derive the class CText from the CElement class as follows:

// Class defining a text object
class CText: public CElement
{
  public:
    // Function to display a text element
    virtual void Draw(CDC* pDC, CElement* pElement=0);

    // Constructor for a text element
    CText(const CString& aString, const CRect& rect, COLORREF aColor);
    virtual void Move(const CSize& size);  // Move a text element

  protected:
    CString m_String;                      // Text to be displayed
    CText(){}                              // Default constructor
};

I added this manually, but I'll leave it to you to decide how you want to do it. This definition should go at the end of the Elements.h file, following the other element types. This class definition declares the virtual Draw() and Move() functions, as the other element classes do. The data member m_String of type CString stores the text to be displayed.

The CText constructor declaration defines three parameters: the string to be displayed, the rectangle in which the string is to be displayed, and the color. The pen width doesn't apply to an item of text, because the appearance is determined by the font. Although you do not need to pass a pen width as an argument to the constructor, the constructor needs to initialize the m_PenWidth member inherited from the base class because it is used in the computation of the bounding rectangle for the text.

Implementing the CText Class

You have three functions to implement for the CText class:

  • The constructor for a CText object

  • The virtual Draw() function to display it

  • The Move() function to support moving a text object by dragging it with the mouse

I added these directly to the Elements.cpp file.

The CText Constructor

The constructor for a CText object needs to initialize the class and base class data members:

// CText constructor
CText::CText(const CString& aString, const CRect& rect, COLORREF aColor)
{
  m_PenWidth = 1;                      // Set the pen width
m_Color = aColor;                    // Set the color for the text
  m_String = aString;                  // Make a copy of the string

  m_EnclosingRect = rect;
  m_EnclosingRect.NormalizeRect();
}

You set the pen width to 1, and store the color and the text string to be displayed. The second parameter is the rectangle in which the text is to be displayed, so this is stored as the enclosing rectangle.

Creating a Text Element

After the element type has been set to TEXT, a text object should be created at the cursor position whenever you click the left mouse button and enter the text you want to display. You therefore need to display the dialog that permits text to be entered in the OnLButtonDown() handler, but only when the element type is TEXT. Add the following code to this handler in the CSketcherView class:

void CSketcherView::OnLButtonDown(UINT nFlags, CPoint point)
{
  CClientDC aDC(this);           // Create a device context
  OnPrepareDC(&aDC);             // Get origin adjusted
  aDC.DPtoLP(&point);            // convert point to Logical
  // In moving mode, so drop the element
  if(m_MoveMode)
  {
    m_MoveMode = false;                      // Kill move mode
    m_pSelected = nullptr;                   // De-select element
    GetDocument()->UpdateAllViews(nullptr);  // Redraw all the views
    return;
  }
  CSketcherDoc* pDoc = GetDocument();// Get a document pointer
  if(pDoc->GetElementType() == TEXT)
  {
    CTextDialog aDlg;
    if(aDlg.DoModal() == IDOK)
    {
      // Exit OK so create a text element
      CSize textExtent = aDC.GetTextExtent(aDlg.m_TextString);
      CRect rect(point, textExtent);      //Create enclosing rectangle
      CText* pTextElement = new CText(
                              aDlg.m_TextString, rect, pDoc->GetElementColor());
      // Add the element to the document
      pDoc->AddElement(pTextElement);
      // Get all views updated
      pDoc->UpdateAllViews(nullptr, 0, pTextElement);
    }
    return;
  }
   m_FirstPoint = point;             // Record the cursor position
   SetCapture();                     // Capture subsequent mouse messages
}

The code to be added is bolded. It creates a CTextDialog object and then opens the dialog using the DoModal() function call. The m_TextString member of aDlg is automatically set to the string entered in the edit box, so you can use this data member to pass the string entered back to the CText constructor if the OK button is used to close the dialog. The color is obtained from the document using the GetElementColor()member that you have used previously.

You also need to determine the rectangle that bounds the text. Because the size of the rectangle for the block of text depends on the font used in a device context, you use the GetTextExtent() function in the CClientDC object, aDC, to initialize the CSize object, textExtent, with the width and height of the text string in logical coordinates. You then use a CRect constructor that creates a rectangle from a point that is the top right corner of the rectangle, and a CSize object that specifies the width and height of the rectangle.

The CText object is created on the heap because the list in the document maintains only pointers to the elements. You add the new element to the document by calling the AddElement() member of CSketcherDoc, with the pointer to the new text element as an argument. Finally, UpdateAllViews() is called with the first argument 0, which specifies that all views are to be updated.

Drawing a CText Object

Drawing text in a device context is different from drawing a geometric figure. The implementation of the Draw() function for a CText object is as follows:

void CText::Draw(CDC* pDC, CElement* pElement)
{
  COLORREF Color(m_Color);             // Initialize with element color

  if(this==pElement)
    Color = SELECT_COLOR;              // Set selected color

  // Set the text color and output the text
  pDC->SetTextColor(Color);
  pDC->DrawText(m_String, m_EnclosingRect, DT_CENTER|DT_VCENTER|

DT_SINGLELINE|DT_NOCLIP);
}

You don't need a pen to display text. You just need to specify the text color using the SetTextColor() function member of the CDC object, and then use the DrawText() member to output the text string. This displays the string, specified by the first argument, in the rectangle specified by the second argument, using the default font. The third argument specifies how the text is to be formatted. You specify the third argument as one or more standard formatting constants of type UINT combined together with logical ORs.

Many of these constants correspond to the ones defined for use with the Windows API DrawText() function. The following table lists some of the more common constants:

CONSTANT

MEANING

DT_LEFT

Text is aligned with the left of the rectangle.

DT_CENTER

Text is centered horizontally in the rectangle.

DT_RIGHT

Text is aligned with the right of the rectangle.

DT_TOP

Text is aligned with the top of the rectangle.

DT_VCENTER

Text is centered vertically in the rectangle. This is used only with DT_SINGLELINE.

DT_BOTTOM

Text is aligned with the bottom of the rectangle. This is used only with DT_SINGLELINE.

DT_SINGLELINE

Text is displayed on a single line. CR and LF do not break the line.

DT_NOCLIP

Inhibits clipping of the text, and drawing is faster.

DT_WORDBREAK

The string will be broken between words if it would otherwise extend beyond the sides of the rectangle.

Because the TextOut() function doesn't use a pen, it isn't affected by setting the drawing mode of the device context. This means that the raster operations (ROP) method that you use to move the elements leaves temporary trails behind when applied to text. Remember that you used the SetROP2() function to specify the way in which the pen would logically combine with the background. By choosing R2_NOTXORPEN as the drawing mode, you could cause a previously drawn element to disappear by redrawing it — it would then revert to the background color and thus become invisible. Fonts aren't drawn with a pen, so it won't work properly with the text elements. You'll see how to fix this problem a little later in this chapter.

Moving a CText Object

The Move() function for a CText object is simple:

void CText::Move(const CSize& size)
{
  m_EnclosingRect += size;            // Move the rectangle
}

All you need to do is alter the enclosing rectangle by the distance specified in the size parameter.

To fix the trails problem when moving a text element, you have to treat it as a special case in the MoveElement() function in the CSketcherView class. Providing an alternative move mechanism for text elements requires that you can discover when the m_pSelected pointer contains the address of a CText object. The typeid operator in native C++ that you met back in Chapter 2 can help with this.

By comparing the expression typeid(*m_pSelected) with typeid(CText), you can determine whether or not m_pSelected is pointing to a CText object. To be able to use the typeid operator in your code, you must change the Enable Run-Time Type Information property value for the project to Yes(/GR). You can open the project's Properties dialog by pressing Alt+F7; you will find the property to change by selecting Configuration Properties

Moving a CText Object

Here's how you can update the MoveElement() function to eliminate the trails when moving text:

void CSketcherView::MoveElement(CClientDC& aDC, CPoint& point)
{
  CSize distance = point - m_CursorPos;   // Get move distance
  m_CursorPos = point;          // Set current point as 1st for next time

  // If there is an element, selected, move it
  if(m_pSelected)
  {
    // If the element is text use this method...
    if (typeid(*m_pSelected) == typeid(CText))
    {
      CRect oldRect=m_pSelected->GetBoundRect(); // Get old bound rect
      aDC.LPtoDP(oldRect);                          // Convert to client coords
      m_pSelected->Move(distance);               // Move the element
      InvalidateRect(&oldRect);                 // Invalidate combined area
      UpdateWindow();                               // Redraw immediately
      m_pSelected->Draw(&aDC,m_pSelected);   // Draw highlighted
      return;
    }
    // ...otherwise, use this method
    aDC.SetROP2(R2_NOTXORPEN);
    m_pSelected->Draw(&aDC,m_pSelected);    // Draw the element to erase it
    m_pSelected->Move(distance);            // Now move the element
    m_pSelected->Draw(&aDC,m_pSelected);    // Draw the moved element
  }
}

When m_pSelected points to a text element, you obtain the bounding rectangle for the old element before moving the element to its new position, and convert the rectangle to client coordinates, because that's what InvalidateRect() expects. After the move, you call InvalidateRect() to redraw the rectangle where the element used to be.

You can see that the code for invalidating the rectangles that you must use for moving the text is a little less elegant than the ROP code that you use for all the other elements. It works, though, as you'll see for yourself if you make this modification, and then build and run the application. Because you use the typeid operator for testing the type, you must add an #include directive for the typeinfo header to SketcherView.cpp. This header provides the definition for the type_info class, and you need this because the typeid operator returns a reference to a const type_info object.

For the program to compile successfully, you need to add a #include directive for TextDialog.h to the SketcherView.cpp file. You should now be able to produce annotated sketches using multiple scaled and scrolled views, such as the ones shown in Figure 18-19.

FIGURE 18-19

Figure 18.19. FIGURE 18-19

You can see that the text does not scale along with the shapes. You could scale the text by adjusting the font size in the device context based on the scale. Of course, you would have to adjust the rectangle enclosing the text when you draw it, too.

DIALOGS AND CONTROLS IN CLR SKETCHER

There is a full range of dialogs and controls available for use in a CLR program, but the procedure for adding them to an application is often different from what you have seen in the MFC version of Sketcher. Extending CLR Sketcher will allow you to try out some of the ways in which you can use dialogs and controls in a Windows Forms application. You will not necessarily end up with the GUI for CLR Sketcher that you would want in practice because you will inevitably duplicate some functions in a way that is not needed in the application.

Adding a Dialog

It would be useful to add a dialog that you can use to enter pen widths, to allow elements to be drawn with lines of different thicknesses. Your first thought as to how to do this is likely to involve the Design window for the form — and indeed, for some predefined dialogs this is the place to start. However, when you want to create your own dialog from the ground up, the starting point is Solution Explorer.

To add a dialog, you actually add a form to the project that you then modify to make it a dialog window. In Solution Explorer, right-click your project, click the Add menu item, then click New Item. In the dialog that displays, select the UI group in the left pane; then select Windows Form in the right pane. Enter the name as PenDialog.cs and click the OK button. The .cs extension identifies that the dialog is for a C++/CLI program. A new Design window will display showing the dialog. In the Properties window for the dialog, change the FormBorderStyle property in the Appearance group to FixedDialog and set the ControlBox, MinimizeBox, and MaximizeBox properties in the Window Style group to false. Change the value of the Text property to Set Pen Width. You now have a modal dialog for setting the pen width, so next you can add the controls you need to the dialog.

Customizing the Dialog

You'll use radio buttons to allow a pen width to be chosen, and place them within a group box that serves to keep them in a group in which only one radio button can be checked at one time. Drag a GroupBox control from the Containers group in the Toolbox window onto the dialog you have created. Change the value of the Text property for the group box to Select Pen Width and change the value of the (name) property to penWidthGroupBox. Adjust the size of the group box by dragging its border so that it fits nicely within the dialog frame. Drag six RadioButton controls from the Toolbox window to the group box and arrange them in a rectangular configuration as you did in the MFC Sketcher program. You'll notice that alignment guides to help you position the controls display automatically for each RadioButton control after the first. Change the text properties for the radio button controls to Pen Width 1 through Pen Width 6, and the corresponding (name) property values to penWidthButton1 through penWidthButton6. Change the value of the Checked property for penWidthButton1 to true.

Next you can add two buttons to the dialog and change their Text properties to OK and Cancel, and the (name) properties to penWidthOK and penWidthCancel. Return to the dialog's Properties window and set the AcceptButton and CancelButton property values to penWidthOK and penWidthCancel by selecting from the list in the values column. Verify that the DialogResult property values for the buttons are OK and Cancel; this will cause the appropriate DialogResult value to be returned when the dialog is closed by the user clicking one button or the other. You should now have a dialog in the Design window that looks similar to Figure 18-20.

FIGURE 18-20

Figure 18.20. FIGURE 18-20

The code for the dialog class is part of the project and is defined in the PenDialog.h header file. However, there are no instances of the PenDialog class anywhere at the moment, so to use the dialog you must create one. You will create a PenDialog object in the Form1 class; add an #include directive for PenDialog.h to Form1.h. Add a new private variable of type PenDialog^ to the Form1 class with the name penDialog and initialize it in the Form1 constructor to gcnew PenDialog().

You need a way to get information about which radio button is checked from the PenDialog object. One way to do this is to add a public property to the class to make the information available. It can be a read-only property, so you need only to implement get() for the property. The code that the Windows Form Designer generates is delimited by a #pragma region and a #pragma endregion directive, and you should not modify this manually or add in your own code. Add the following code to the PenDialog class definition immediately after the #pragma endregion directive:

public: property float PenWidth
{
  float get()
  {
    if(penWidthButton1->Checked)
      return 1.0f;
    if(penWidthButton2->Checked)
      return 2.0f;
    if(penWidthButton3->Checked)
      return 3.0f;
    if(penWidthButton4->Checked)
      return 4.0f;
    if(penWidthButton5->Checked)
      return 5.0f;
    return 6.0f;
  }
}

The System::Drawing::Pen class defines a pen where the pen width is a floating-point value; hence, the PenWidth property is floating-point.

Now that the dialog is complete, and you have a Form1 member that references a dialog object, you need a mechanism to open the dialog. Another toolbar button on the Form1 window is a good choice.

Displaying the Dialog

You need a bitmap to display on the new toolbar button. Switch to the Resource View, right-click the Bitmap folder, and click Insert Bitmap. Set the Height and Width property values for the bitmap to 16, the Filename property value to penwidth.bmp, and the ID to IDB_PENWIDTH. You can now create a bitmap of your choosing to represent a line width; I'll use a symbol that looks like a pen drawing a line, as in MFC Sketcher.

In the Design window for Form1.h, add a separator to the toolbar, followed by a new toolbar button. Change the (name) property value to penWidthButton and the ToolTipText property value to Change pen width. Set the image for the new button to penwidth.bmp, and then create a Click event handler for the toolbar button by double-clicking the Click event in its Properties window. You can add the following code to the new event handler to display the pen dialog:

private: System::Void penWidthButton _Click(
                                    System::Object^  sender, System::EventArgs^  e)
{
  if(penDialog->ShowDialog() == System::Windows::Forms::DialogResult::OK)
  {
    // Get the pen width ...
  }
}

Calling ShowDialog() for the PenDialog object displays the dialog. Because it is a modal dialog, it remains visible until a button is clicked to close it. The ShowDialog() function returns a value that is an enumerator from the System::Windows::Forms::DialogResult enumeration. This enumeration defines the following enumerator values:

None, OK, Cancel, Abort, Retry, Ignore, Yes, No

These provide for a variety of buttons being identified to close a dialog, and you can set any of these values from the drop-down list of values for the DialogResult property of a button. You must fully qualify the DialogResult type name here because the Form1 class has an inherited member with the name DialogResult.

You need somewhere to record the current pen width in the Form1 class, so add a private penWidth variable of type float to the class and initialize it to 1.0f in the constructor. You can now replace the comment in the preceding Click event handler with the following:

penWidth = penDialog->PenWidth;

This will set the current pen width to the value returned by the PenWidth property for the dialog.

All that remains to do is to implement creating and drawing elements with a given pen width.

Creating Elements with a Pen Width

This will involve modifying the constructor in each of the derived element classes. The modifications are essentially the same in each class, so I'll just show how the Line class constructor changes, and you can do the rest.

The changes to the Line class constructor to allow for different pen widths are as follows:

Line(Color color, Point start, Point end, float penWidth)
      {
        pen = gcnew Pen(color, penWidth);
      this->color = color;
      position = start;
      this->end = end - Size(start);
      boundRect = System::Drawing::Rectangle(Math::Min(position.X, end.X),
                                             Math::Min(position.Y, end.Y),
                    Math::Abs(position.X - end.X), Math::Abs(position.Y - end.Y));
boundRect.Inflate(safe_cast<int>(penWidth), safe_cast<int>(penWidth));

      }

The only changes are to add an extra parameter to the Line constructor, to use the Pen constructor that accepts a second argument of type float that specifies the pen width, and to increase the size of the bounding rectangle by the penWidth argument value. Make the same change to the constructors for the other element classes.

Because you have changed the parameter list for the element class constructors, you must change the MouseMove event handler that creates elements:

private: System::Void Form1_MouseMove(System::Object^  sender,
System::Windows::Forms::MouseEventArgs^  e) {
   if(drawing)
  {
    if(tempElement)
      Invalidate(tempElement->Bound);  // The old element region
    switch(elementType)
    {
      case ElementType::LINE:
        tempElement = gcnew Line(color, firstPoint, e->Location, penWidth);
        break;
      case ElementType::RECTANGLE:
        tempElement = gcnew Rectangle(color, firstPoint, e->Location, penWidth);
        break;
      case ElementType::CIRCLE:
        tempElement = gcnew Circle(color, firstPoint, e->Location, penWidth);
        break;
      case ElementType::CURVE:
        if(tempElement)
          safe_cast<Curve^>(tempElement)->Add(e->Location);
        else
          tempElement = gcnew Curve(color, firstPoint, e->Location, penWidth);
        break;
    }
    // Rest of the code as before...
 }

You should now have CLR Sketcher with pen widths fully working, as Figure 18-21 illustrates.

FIGURE 18-21

Figure 18.21. FIGURE 18-21

Using a Combo Box Control

You can add a combo box to the toolbar in the Form1 Design window to provide a way to enter a line style. ComboBox is an option in the list that displays when you click the down arrow for the toolbar item that adds new entries in the Design window. You can customize this option to do what you want.

Set the DropDownStyle property to DropDownList; the effect of this is to only allow values to be selected from the list. A value of DropDown allows any value to be typed in the combo box. Change (name) to something more relevant, such as lineStyleComboBox. You want to add a specific set of items to be displayed in the combo box, and the Items property holds these. Select the Items property, click (Collection) in the value column, and select the ellipsis at the right to open the String Collection Editor dialog. You can then enter strings, as shown in Figure 18-22.

FIGURE 18-22

Figure 18.22. FIGURE 18-22

These are the entries that will be displayed in the drop-down for the combo box; they are indexed from 0 to 4.

Because the default size for the combo box is not as wide as you need for the longest string, change the value of the Size property to 150,25. You can also change the FlatStyle property to Standard, so that the combo box will be more visible on the toolbar. You can set the ToolTipText property to Choose line style.

At present the combo box will not show anything when it is first displayed, but you can fix that in the Form1 constructor. Add the following line of code to the constructor after the // TODO comment:

lineStyleComboBox->SelectedIndex = 0;

The SelectedIndex property determines which of the entries in the Items collection is displayed in the combo box, and because the entries in the combo box Items property collection are indexed from 0, this causes the first entry to be displayed initially. That will continue to be the entry displayed until you select a new entry from the combo box, so the combo box will always show the currently selected line width.

You need somewhere to store the current line style, so add a private member, lineStyle, to the Form1 class of type System::Drawing::Drawing2D::DashStyle. This is an enum type used by the Pen class to specify a style for drawing lines. If you add a using directive for the System::Drawing::Drawing2D namespace, you can specify the type for lineStyle as just DashStyle.

The DashStyle enum defines the members Solid, Dash, Dot, DashDot, DashDotDot, and Custom. The meaning for the first five are obvious. The Custom style gives you complete flexibility in specifying a line style as virtually any sequence of line segments and spaces of differing lengths that you want. You specify how the line style is to appear by setting the DashPattern property for the Pen object. You specify the pattern with an array of float values that specify the lengths of the line segments and intervening spaces. Here's an example:

array<float>^ pattern = {5.0f, 3.0f};
pen->DashPattern = pattern;

This specifies a dashed line for the Pen object pen, consisting of segments of length 5.0f separated by spaces of length 3.0f. The pattern you specify is repeated as often as necessary to draw a given line. Obviously, by specifying more elements in the array, you can define patterns as complicated as you like.

You need to know when an entry from the combo box is selected so that you can update the lineStyle member of the Form1 class. An easy way to arrange this is to add a handler for the SelectedIndexChanged event for the ComboBox object; add this handler through the Properties window and implement it like this:

private: System::Void lineStyleComboBox_SelectedIndexChanged(
                            System::Object^  sender, System::EventArgs^ e)
{
  switch(lineStyleComboBox->SelectedIndex)
  {
    case 1:
     lineStyle = DashStyle::Dash;
     break;
    case 2:
     lineStyle = DashStyle::Dot;
     break;
    case 3:
     lineStyle = DashStyle::DashDot;
     break;
    case 4:
     lineStyle = DashStyle::DashDotDot;
     break;
    default:
     lineStyle = DashStyle::Solid;
     break;
  }
}

This just sets the value of lineStyle to one of the DashStyle enum values based on the value of the SelectedIndex property for the combo box object.

To draw elements with different line styles you must add another parameter of type DashStyle to each of the element constructors. First, add the following using directive to Elements.h:

using namespace System::Drawing::Drawing2D;

Here's how you update the Line class constructor to support drawing lines in different styles:

Line(Color color, Point start, Point end, float penWidth, DashStyle style)
    {
      pen = gcnew Pen(color, penWidth);
      pen->DashStyle = style;
      // Rest of the code for the function as before...
    }

There's just one new line of code, which sets the DashStyle property for the pen to the property passed as the last argument to the constructor. Change the other element class constructors in the same way.

The last thing you must do is update the constructor calls in the mouse move event handler in the Form1 class:

private: System::Void Form1_MouseMove(System::Object^  sender,
System::Windows::Forms::MouseEventArgs^  e)
{
  if(drawing)
  {
    if(tempElement)
      Invalidate(tempElement->bound);               // Invalidate old element area
    switch(elementType)
    {
      case ElementType::LINE:
        tempElement = gcnew Line(color, firstPoint, e->Location, penWidth,
                                                                        lineStyle);
        break;
      case ElementType::RECTANGLE:
        tempElement = gcnew Rectangle(color, firstPoint, e->Location, penWidth,
                                                                        lineStyle);
        break;
      case ElementType::CIRCLE:
        tempElement = gcnew Circle(color, firstPoint, e->Location, penWidth,
                                                                        lineStyle);
        break;
      case ElementType::CURVE:
        if(tempElement)
          safe_cast<Curve^>(tempElement)->Add(e->Location);
        else
          tempElement = gcnew Curve(color, firstPoint, e->Location, penWidth,
                                                                        lineStyle);
        break;
    }

   // Rest of the code for the function as before...
}

If you recompile CLR Sketcher and run it again, the combo box will enable you to select a new line style for drawing elements. The line styles don't really work for curves because of the way they are drawn — as a series of very short line segments. With the other elements, the line styles look better if you draw with a pen width greater than the default.

Creating Text Elements

Drawing text in CLR Sketcher will be similar to drawing text in MFC Sketcher. Selecting a Text menu item or clicking a text toolbar button will set Text as the element drawing mode, and in this mode clicking anywhere on the form will display a modal dialog that allows some text to be entered; closing the dialog with the OK button will display the text at the cursor position. There are many ramifications to drawing text, but in the interest of keeping the book to a modest weight, I'll limit this discussion to the basics.

Add a TEXT enumerator to the ElementType enum class, then add a Text menu item to the Elements menu, and a corresponding toolbar button in the Form1 Design window; you will need to create a bitmap resource for the button. Change the value of the (name) property for the menu item to textToolStripMenuItem if necessary, and create a Click event handler for it. You can also add a value for the ToolTipText property for the menu item and the toolbar button. You can create a bitmap for the toolbar button to indicate text mode, and select the Click event handler for the toolbar button to be the Click event handler for the menu item. You can implement the Click event handler like this:

private: System::Void textToolStripMenuItem_Click(
                                    System::Object^  sender, System::EventArgs^  e)
{
   elementType = ElementType::TEXT;
   SetElementTypeButtonState();
}

The function just sets elementType to the ElementType enumerator that represents text drawing mode.

Update the SetElementTypeButtonsState() function to check the text button. You will need to update the DropDownOpening event handler for the tool strip to ensure that the new text menu item gets checked. Don't forget to add a Text menu item to the context menu, too. This will involve amending the handler for the Opening event for the context menu to add the Text menu item, and set it as checked when appropriate.

Drawing Text

You need to look into several things before you can make CLR Sketcher create and draw text. Let's start with how you draw text. The Graphics class defines a DrawString() function for drawing text. There are several overloaded versions of this function, as described in the following table.

FUNCTION

DESCRIPTION

DrawString(
 String^ str,
 Font^ font,
 Brush^ brush,
 PointF point)

Draws str at the position point using font, with the color determined by brush. Type Windows::Drawing::PointF is a point represented by coordinates of type float. You can use a Point object as an argument in any of the functions for a PointF parameter.

DrawString(
 String^ str,
 Font^ font,
 Brush^ brush,
 float X,
 float Y)

Draws str at the position (X,Y) using font, with the color determined by brush. You can also use coordinate arguments of type int when you call this function.

DrawString(
 String^ str,
 Font^ font,
 Brush^ brush,
 RectangleF rect)

Draws str within the rectangle rect using font, with the color determined by brush. Type Windows::Drawing::RectangleF defines a rectangle with its position width and height specified by values of type float. You can use a Rectangle object as an argument in any of the functions for a RectangleF parameter.

DrawString(
 String^ str,
 Font^ font,
 Brush^ brush,
 PointF point,
 StringFormat^ format)

Draws str at the position point using font, with the color determined by brush and the formatting of the string specified by format. A StringFormat object determines the alignment and other formatting properties for a string.

DrawString(
 String^ str,
 Font^ font,
 Brush^ brush,
 float X,
 float Y,
 StringFormat^ format)

Draws str at the position (X,Y) using font, with the color determined by brush and the formatting of the string specified by format.

DrawString(
 String^ str,
 Font^ font,
 Brush^ brush,
 RectangleF rect,
 StringFormat^ format)

Draws str within the rectangle rect using font, with the color determined by brush and the formatting of the string determined by format.

Clearly, you need to understand a bit about fonts and brushes in order to use the DrawString() function, so I'll briefly introduce those before returning to how you can create and display Text elements.

Creating Fonts

You specify the font to be used when you draw a string by a System::Drawing::Font object that defines the typeface, style, and size of the drawn characters. An object of type System::Drawing::FontFamily defines a group of fonts with a given typeface, such as "Arial" or "Times New Roman". The System::Drawing::FontStyle enumeration defines possible font styles, which can be any of the following: Regular, Bold, Italic, Underline, and Strikeout.

You can create a Font object like this:

FontFamily^ family = gcnew FontFamily(L"Arial");
System::Drawing::Font^ font = gcnew System::Drawing::Font(family, 10,
                                            FontStyle::Bold,  GraphicsUnit::Point);

You create the FontFamily object by passing the name of the font to the constructor. The arguments to the Font constructor are the font family, the size of the font, the font style, and an enumerator from the GraphicUnit enumeration that defines the units for the font size.

The possible enumerator values are as follows:

World

The world coordinate system is the unit of measure.

Display

The unit of measure is that of the display device: pixels for monitors, and 1/100 inch for printers.

Pixel

A device pixel is the unit of measure.

Point

The unit of measure is a point (1/72 inch).

Inch

The unit of measure is the inch.

Document

The unit of measure is the document unit, which is 1/300 inch.

Millimeter

The millimeter is the unit of measure.

Thus, the previous code fragment defines a 10-point bold Arial font. Note that the Font type name is fully qualified in the fragment because a form object in a Windows Forms application inherits a property with the name Font from the Form base class that identifies the default font for the form. Windows Forms applications support TrueType fonts primarily, so it is best not to choose Open Type fonts. If you attempt to use a font that is not supported, or the font is not installed on your computer, the Microsoft Sans Serif font will be used.

Creating Brushes

A System::Drawing::Brush object determines the color used to draw a string with a given font; it is also used to specify the color and texture used to fill a shape. You can't create a Brush object directly because Brush is an abstract class. You create brushes using the SolidBrush, the TextureBrush, and the LinearGradientBrush class types derived from Brush. A SolidBrush object is a brush of a single color, a TextureBrush object is a brush that uses an image to fill the interior of a shape, and a LinearGradientBrush is a brush defining a color gradient blend, usually between two colors, but also possibly among several colors. You will use a SolidBrush object to draw text, which you create like this:

SolidBrush^ brush = gcnew SolidBrush(Color::Red);

This creates a solid brush that will draw text in red.

Choosing a Font

You can store a reference to the current Font object, to be used when CLR Sketcher is creating Text elements, by adding a private variable of type System::Drawing::Font^, and with the name textFont, to the Form1 class. You must use the fully qualified type name to avoid confusion with the inherited property with the name Font. Initialize textFont in the Form1 constructor to Font, which is a form property specifying the default font for the form. Be sure to do this in the body of the constructor following the // TODO: comment; if you attempt to initialize it in the initialization list, the program will fail because Font is not defined at this point.

You can add a toolbar button to allow the user to select a font for entering text, perhaps a button with a bitmap representation of F for font. Give the button a suitable value for the (name) property, such as toolStripFontButton, and add a Click event handler for it.

The Toolbox window has a standard dialog for choosing a font that will display all the fonts available on your system and allow selection of the font style and size. Select the Design window for Form1.h and drag a FontDialog control from the Toolbox window to the form. The Click event handler for the toolStripFontButton can display the font dialog:

private: System::Void toolStripFontButton_Click(
                                   System::Object^  sender, System::EventArgs^  e)
{
  if(fontDialog1->ShowDialog() == System::Windows::Forms::DialogResult::OK)
  {
    textFont = fontDialog1->Font;
  }
}

You display the dialog by calling its ShowDialog() function, as you did for the pen dialog. If the function returns DialogResult::OK, you know the dialog was closed with the OK button. In this case, you retrieve the chosen font from the Font property for the dialog object, and store it in the textFont variable in the Form1 class object. You can try this out if you want. The Font dialog will look like Figure 18-23.

FIGURE 18-23

Figure 18.23. FIGURE 18-23

You need a class to represent a Text element, so let's define that next.

Defining the Text Element Class

The TextElement element type will have Element as a base class like the other element types, so you can call the Draw() function polymorphically. Basic information about the location and color of the text element will be recorded in the members inherited from the base class, but you need to add extra members to store the text and information relating to the font. Here's the initial definition of the class:

public ref class TextElement : Element
  {
    protected:
      String^ text;
      SolidBrush^ brush;
      Font^ font;
public:
      TextElement(Color color, Point p, String^ text, Font^ font)
      {
        this->color = color;
        brush = gcnew SolidBrush(color);
        position = p;
        this->text = text;
        this->font = font;
        int height = font->Height;           // Height of text string
        int width = static_cast<int>(font->Size*text->Length); // Width of string
        boundRect = System::Drawing::Rectangle(position, Size(width, height));
        boundRect.Inflate(2,2);              // Allow for descenders
      }

      virtual void Draw(Graphics^ g) override
      {
        brush->Color = highlighted ? highlightColor : color;
        g->TranslateTransform(safe_cast<float>(position.X),
                              safe_cast<float>(position.Y));
        g->DrawString(text, font, brush, Point(0,0));
        g->ResetTransform();
      }
  };

I chose the TextElement type name, rather than just Text, to avoid confusion with a member of the Form class with the name Text. A TextElement object has members to store the text string, the font, and the brush to be used to draw the text.

Determining the bounding rectangle for a text element introduces a slight complication in that it depends on the point size of the font, and you have to figure out the width and height from the font size. The height is easy because the font object has a Height property that makes the font's line spacing available, which is a good estimate for the height of the text string. The Size property returns the em size of the font (which is the width of the letter M), so you can get a generous estimate of the width of the rectangle the string will occupy by multiplying the em size by the number of characters in the string. I have inflated the rectangle by two because the line spacing doesn't always result in a bounding rectangle that takes account of descenders such as in lowercase ps and qs.

Creating the Text Dialog

You'll want to enter text from the keyboard when you create a TextElement element, so you need a dialog to manage this. Go to the Solution Explorer window, and add a new form to CLR Sketcher by right-clicking the project name and clicking Add

Creating the Text Dialog

The next step is to add buttons to close the dialog. Add an OK button and a Cancel button to the text dialog with the DialogResult property values set appropriately. Change the (name) property values to textOKButton and textCancelButton respectively. Set the AcceptButton property value for TextDialog to textOKButton and the CancelButton property value to textCancelButton.

Using a Text Box

A TextBox control allows a single line or multiple lines of text to be entered, so it certainly covers what you want to do here. Add a TextBox to the text dialog by dragging it from the Toolbox window to the dialog in the Design window. By default, a TextBox allows a single line of text to be entered, and you can stick with that here. When you want to allow multiple lines of input, you click the arrow to the right on the TextBox in the Design window, and click the checkbox to enable Multiline. The Text property for the TextBox provides access to the input and makes it available as a String object.

Ideally, you want the text dialog to open with the focus on the box for entering text. Then you can enter the text immediately after the dialog opens, and press enter to close it as if you had selected the OK button. The control that has the focus initially is determined by the tab order of the controls on the dialog, which depends on the value of the TabIndex property for the controls. If you set the TabIndex property for the text box to 0 and the OK and Cancel buttons to 1 and 2 respectively, this will result in the text box having the focus when the dialog initially displays. The tab order also defines the sequence in which the focus changes when you press the tab key. You can display the tab order for the controls in the Design window for the dialog by selecting View

Using a Text Box

The textBox1 control will store the string you enter, but because this is a private member of the dialog object, it is not accessible directly. You must add a mechanism to retrieve the string from the dialog object. Another problem is that the textBox1 control will retain the text you enter and display it the next time the dialog is opened. You probably don't want this to occur, so you need a way to reset the Text property of textBox1. You can add a public property to the TextDialog class that will deal with both difficulties. Add the following code to the TextDialog class definition, following the #pragma endregion directive:

// Property to access the text in the edit box
    public: property String^ TextString
    {
      String^ get() {return textBox1->Text; }
      void set(String^ text) { textBox1->Text = text; }
    }

The get() function for the TextString property makes the string you enter available, and the set() function enables you to reset it.

It would be nice if the text appeared in the edit box in the current font so that the user could tell whether it's what he or she wants. This will provide an opportunity to cancel the text entry and change to another font. Add the following property to the TextDialog class, following the preceding one:

// Set the edit box font
public: property System::Drawing::Font^ TextFont
{
  void set(System::Drawing::Font^ font) { textBox1->Font = font; }
}

This is a set-only property that you can use to set the font for the edit box object textBox1.

You need an extra data member in the Form1 class to help you create text elements. Add an #include directive for TextDialog.h to Form1.h, then add a textDialog member of type TextDialog^. You can initialize it to gcnew TextDialog() in the initialization list for the Form1 class constructor.

Displaying the Dialog and Creating a Text Element

The process for creating an element that is text is different from the process for geometric elements, so to understand the sequence of events in the code, let's describe the interactive process. Text element mode is in effect when you select the Text menu item or toolbar button. To create an element, you click at the position on the form where you want the top left corner of the text string to be. This will display the text dialog; you type the text you want in the text box, and press Enter to close the dialog. The MouseMove handler is not involved in the process at all. Clicking the mouse to define the position of the element embodies the whole process. This implies that you must display the dialog and create the text element in the MouseDown event handler. Here's the code to do that:

private: System::Void Form1_MouseDown(System::Object^  sender,
System::Windows::Forms::MouseEventArgs^  e) {
  if(e->Button == System::Windows::Forms::MouseButtons::Left)
  {
    firstPoint = e->Location;
    if(mode == Mode::Normal)
    {
      if(elementType == ElementType::TEXT)
      {
        textDialog->TextString = L"";        // Reset the text box string
        textDialog->TextFont = textFont;     // Set the font for the edit box
        if(textDialog->ShowDialog() == System::Windows::Forms::DialogResult::OK)
        {
          tempElement = gcnew TextElement(color, firstPoint,
                                                 textDialog->TextString, textFont);
          sketch += tempElement;
          Invalidate(tempElement->bound);    // The text element region
          tempElement = nullptr;
          Update();
        }
        drawing = false;
      }
      else
      {
        drawing = true;
      }
    }
  }
}

You create text elements only when the elementType member of the form has the value ElementType::TEXT and the mode is Mode::Normal; the second condition is essential to avoid displaying the dialog when you are in move mode. When a text element is being created, the first action is to reset the Text property for the textBox1 control to an empty string by setting it as the value for the TextString property for the dialog. You also set the font for the edit box to the value stored in the textFont member of the form.

You display the dialog by calling ShowDialog() in the if condition expression. If the ShowDialog() function returns DialogResult::OK, you retrieve the string from the dialog, use it to create the TextElement object, and add the object to the sketch. You then invalidate the region occupied by the new element and call Update() to display it. You also reset tempElement to nullptr when you are done with it. Finally, you set drawing to false to prevent the MouseMove handler from attempting to create an element.

FIGURE 18-24

Figure 18.24. FIGURE 18-24

If you recompile Sketcher, you should be able to create text elements using a font of your choice. Not only that, but you can move and delete them too. Figure 18-24 shows Sketcher displaying text elements.

SUMMARY

In this chapter you've seen several different dialogs using a variety of controls. Although you haven't created dialogs involving several different controls at once, the mechanism for handling them is the same as what you have seen, because each control can operate independently of the others. Dialogs are a fundamental tool for managing user input in an application. They provide a way for you to manage the input of multiple related items of data. You can easily ensure that an application only receives valid data. Judicious choice of the controls in a dialog can force the user to choose from a specific set of options. You can also check the data after it has been entered in a dialog and prompt the user when it is not valid.

WHAT YOU LEARNED IN THIS CHAPTER

TOPIC

CONCEPT

Dialogs

A dialog involves two components: a resource defining the dialog box and its controls, and a class that is used to display and manage the dialog.

Extracting data from a dialog

Information can be extracted from controls in a dialog by means of the DDX mechanism. The data can be validated with the DDV mechanism. To use DDX/DDV you need only use the Add Control Variable option for the Add Member Variable wizard to define variables in the dialog class associated with the controls.

Modal dialogs

A modal dialog retains the focus in the application until the dialog box is closed. As long as a modal dialog is displayed, all other windows in an application are inactive.

Modeless dialogs

A modeless dialog allows the focus to switch from the dialog box to other windows in the application and back again. A modeless dialog can remain displayed as long as the application is executing, if required.

Common controls

Common Controls are a set of standard Windows controls supported by MFC and the resource editing capabilities of Developer Studio.

Adding controls

Although controls are usually associated with a dialog, you can add controls to any window.

Adding a new form in a Windows Forms application

You add a form to a Windows Forms application using the Solutions Explorer pane; you right-click the project name and select Add

EXERCISES

Converting a form into a modal dialog

You convert a form into a modal dialog window by changing the value of the FormBorderStyle property to FixedDialog, and changing the values of the MaximizeBox, MinimizeBox, and ControlBox properties to false.

Adding controls to a dialog

You populate a dialog with controls by dragging them from the Toolbox window onto the dialog in the Design pane.

Displaying a dialog

You display a dialog by calling its ShowDialog() function.

The DialogResult property for a button

The value that you set for the DialogResult property for a button in a dialog determines the value returned by the ShowDialog() function when that button is used to close the dialog.

Using standard dialogs

The toolbox has several complete standard dialogs available, including a dialog for choosing a font.

Drawing test on a form

You can draw a string on a form by calling the DrawString() function for the Graphics object that encapsulates the drawing surface of a form. A Font object argument to the function determines the typeface, style, and size of the characters drawn, and a Brush object argument determines the color.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.17.79.20