The EX06B Example

I won't try to contrive a business-oriented example that uses all the custom controls. I'll just slap the controls in a modal dialog and trust that you'll see what's going on. The steps are shown below. After step 3, the instructions are oriented to the individual controls rather than to the Visual C++ components you'll be using.

  1. Run AppWizard to generate the EX06B project. Choose New from Visual C++'s File menu, and then select Microsoft AppWizard (exe) from the Projects page. Accept all the defaults but two: select Single Document and deselect Printing And Print Preview. The options and the default class names are shown here.

  2. Create a new dialog resource with ID IDD_DIALOG1. Place the controls as shown back in Figure 6-2.

    You can select the controls from the control palette. The following table lists the control types and their IDs.

    Don't worry about the other properties now—you'll set those in the following steps. (Some controls might look different than they do in Figure 6-2 until you set their properties.) Set the tab order as shown next.

    Click to view at full size.

    Tab Sequence Control Type Child Window ID
    1StaticIDC_STATIC
    2ProgressIDC_PROGRESS1
    3StaticIDC_STATIC
    4Trackbar (Slider)IDC_TRACKBAR1
    5StaticIDC_STATIC_TRACK1
    6StaticIDC_STATIC
    7Trackbar (Slider)IDC_TRACKBAR2
    8StaticIDC_STATIC_TRACK2
    9StaticIDC_STATIC
    10EditIDC_BUDDY_SPIN1
    11SpinIDC_SPIN1
    12StaticIDC_STATIC
    13StaticIDC_STATIC
    14List controlIDC_LISTVIEW1
    15StaticIDC_STATIC_LISTVIEW1
    16StaticIDC_STATIC
    17Tree controlIDC_TREEVIEW1
    18StaticIDC_STATIC_TREEVIEW1
    19PushbuttonIDOK
    20PushbuttonIDCANCEL

  3. Use ClassWizard to create a new class, CEx06bDialog, derived from CDialog. ClassWizard will automatically prompt you to create this class because it knows that the IDD_DIALOG1 resource exists without an associated C++ class. Map the WM_INITDIALOG message, the WM_HSCROLL message, and the WM_VSCROLL message.

  4. Program the progress control. Because ClassWizard won't generate a data member for this control, you must do it yourself. Add a public integer data member named m_nProgress in the CEx06bDialog class header, and set it to 0 in the constructor. Also, add the following code in the OnInitDialog member function:

    CProgressCtrl* pProg = 
        (CProgressCtrl*) GetDlgItem(IDC_PROGRESS1);
    pProg->SetRange(0, 100);
    pProg->SetPos(m_nProgress);
     

  5. Program the "continuous" trackbar control. Add a public integer data member named m_nTrackbar1 to the CEx06bDialog header, and set it to 0 in the constructor. Next add the following code in the OnInitDialog member function to set the trackbar's range, to initialize its position from the data member, and to set the neighboring static control to the tracker's current value.

    CString strText1;
    CSliderCtrl* pSlide1 = 
        (CSliderCtrl*) GetDlgItem(IDC_TRACKBAR1);
    pSlide1->SetRange(0, 100);
    pSlide1->SetPos(m_nTrackbar1);
    strText1.Format("%d", pSlide1->GetPos());
    SetDlgItemText(IDC_STATIC_TRACK1, strText1); 
    

    To keep the static control updated, you need to map the WM_HSCROLL message that the trackbar sends to the dialog. Here is the code for the handler:

    void CEx06bDialog::OnHScroll(UINT nSBCode, UINT nPos,
                                 CScrollBar* pScrollBar)
    {
        CSliderCtrl* pSlide = (CSliderCtrl*) pScrollBar;
        CString strText;
        strText.Format("%d", pSlide->GetPos());
        SetDlgItemText(IDC_STATIC_TRACK1, strText); 
    }
    

    Finally, you need to update the trackbar's m_nTrackbar1 data member when the user clicks OK. Your natural instinct would be to put this code in the OnOK button handler. You would have a problem, however, if a data exchange validation error occurred involving any other control in the dialog. Your handler would set m_nTrackbar1 even though the user might choose to cancel the dialog. To avoid this problem, add your code in the DoDataExchange function as shown below. If you do your own validation and detect a problem, call the CDataExchange::Fail function, which alerts the user with a message box.

    if (pDX->m_bSaveAndValidate) {
        TRACE("updating trackbar data members\n");
        CSliderCtrl* pSlide1 =
            (CSliderCtrl*) GetDlgItem(IDC_TRACKBAR1);
        m_nTrackbar1 = pSlide1->GetPos();
    
    }
     

  6. Program the "discrete" trackbar control. Add a public integer data member named m_nTrackbar2 to the CEx06bDialog header, and set it to 0 in the constructor. This data member is a zero-based index into the dValue, the array of numbers (4.0, 5.6, 8.0, 11.0, and 16.0) that the trackbar can represent. Define dValue as a private static double array member variable in ex06bDialog.h, and add to ex06bDialog.cpp the following line:

    double CEx06bDialog::dValue[5] = {4.0, 5.6, 8.0, 11.0, 16.0}; 
    

    Next add code in the OnInitDialog member function to set the trackbar's range and initial position.

    CString strText2;
    CSliderCtrl* pSlide2 = 
        (CSliderCtrl*) GetDlgItem(IDC_TRACKBAR2);
    pSlide2->SetRange(0, 4);
    pSlide2->SetPos(m_nTrackbar2);
    strText2.Format("%3.1f", dValue[pSlide2->GetPos()]);
    SetDlgItemText(IDC_STATIC_TRACK2, strText2); 
    

    If you had only one trackbar, the WM_HSCROLL handler in step 5 would work. But because you have two trackbars that send WM_HSCROLL messages, the handler must differentiate. Here is the new code:

    void CEx06bDialog::OnHScroll(UINT nSBCode, UINT nPos,
                                 CScrollBar* pScrollBar)
    {
        CSliderCtrl* pSlide = (CSliderCtrl*) pScrollBar;
        CString strText;
    
        // Two trackbars are sending
        //  HSCROLL messages (different processing)
        switch(pScrollBar->GetDlgCtrlID()) {
        case IDC_TRACKBAR1: 
            strText.Format("%d", pSlide->GetPos());
            SetDlgItemText(IDC_STATIC_TRACK1, strText);
            break;
        case IDC_TRACKBAR2:
            strText.Format("%3.1f", dValue[pSlide->GetPos()]);
            SetDlgItemText(IDC_STATIC_TRACK2, strText);
            break;
        } 
    }
    

    This trackbar needs tick marks, so you must check the control's Tick Marks and Auto Ticks properties back in the dialog editor. With Auto Ticks set, the trackbar will place a tick at every increment. The same data exchange considerations applied to the previous trackbar apply to this trackbar. Add the following code in the dialog class DoDataExchange member function inside the block for the if statement you added in the previous step:

    CSliderCtrl* pSlide2 =
        (CSliderCtrl*) GetDlgItem(IDC_TRACKBAR2);
    m_nTrackbar2 = pSlide2->GetPos(); 
    

    Use the dialog editor to set the Point property of both trackbars to Bottom/Right. Select Right for the Align Text property of both the IDC_STATIC_TRACK1 and IDC_STATIC_TRACK2 static controls.

  7. Program the spin button control. The spin control depends on its buddy edit control, located immediately before it in the tab order. Use ClassWizard to add a double-precision data member called m_dSpin for the IDC_BUDDY_SPIN1 edit control. We're using a double instead of an int because the int would require almost no programming, and that would be too easy. We want the edit control range to be 0.0 to 10.0, but the spin control itself needs an integer range. Add the following code to OnInitDialog to set the spin control range to 0 to 100 and to set its initial value to m_dSpin * 10.0:

    CSpinButtonCtrl* pSpin =
        (CSpinButtonCtrl*) GetDlgItem(IDC_SPIN1);
    pSpin->SetRange(0, 100);
    pSpin->SetPos((int) (m_dSpin * 10.0)); 
    

    To display the current value in the buddy edit control, you need to map the WM_VSCROLL message that the spin control sends to the dialog. Here's the code:

    void CEx06bDialog::OnVScroll(UINT nSBCode, UINT nPos, 
                                 CScrollBar* pScrollBar) 
    {
        if (nSBCode == SB_ENDSCROLL) {
            return; // Reject spurious messages
        }
        // Process scroll messages from IDC_SPIN1 only
        if (pScrollBar->GetDlgCtrlID() == IDC_SPIN1) {
            CString strValue;
            strValue.Format("%3.1f", (double) nPos / 10.0);
            ((CSpinButtonCtrl*) pScrollBar)->GetBuddy()
                                           ->SetWindowText(strValue);
        } 
    }
    

    There's no need for you to add code in OnOK or in DoDataExchange because the dialog data exchange code processes the contents of the edit control. In the dialog editor, select the spin control's Auto Buddy property and the buddy's Read-only property.

  8. Set up an image list. Both the list control and the tree control need an image list, and the image list needs icons.

    First use the graphics editor to add icons to the project's RC file. On the companion CD-ROM, these icons are circles with black outlines and different-colored interiors. Use fancier icons if you have them. You can import an icon by choosing Resource from the Insert menu and then clicking the Import button. For this example, the icon resource IDs are as follows.

    Resource ID Icon File
    IDI_BLACKIcon1
    IDI_BLUEIcon3
    IDI_CYANIcon5
    IDI_GREENIcon7
    IDI_PURPLEIcon6
    IDI_REDIcon2
    IDI_WHITEIcon0
    IDI_YELLOWIcon4

    Next add a private CImageList data member called m_imageList in the CEx06bDialog class header, and then add the following code to OnInitDialog:

    HICON hIcon[8];
    int n;
    m_imageList.Create(16, 16, 0, 8, 8); // 32, 32 for large icons
    hIcon[0] = AfxGetApp()->LoadIcon(IDI_WHITE);
    hIcon[1] = AfxGetApp()->LoadIcon(IDI_BLACK);
    hIcon[2] = AfxGetApp()->LoadIcon(IDI_RED);
    hIcon[3] = AfxGetApp()->LoadIcon(IDI_BLUE);
    hIcon[4] = AfxGetApp()->LoadIcon(IDI_YELLOW);
    hIcon[5] = AfxGetApp()->LoadIcon(IDI_CYAN);
    hIcon[6] = AfxGetApp()->LoadIcon(IDI_PURPLE);
    hIcon[7] = AfxGetApp()->LoadIcon(IDI_GREEN);
    for (n = 0; n < 8; n++) {
        m_imageList.Add(hIcon[n]);
    }
     

    About Icons

    You probably know that a bitmap is an array of bits that represent pixels on the display. (You'll learn more about bitmaps in Chapter 11.) In Windows, an icon is a "bundle" of bitmaps. First of all, an icon has different bitmaps for different sizes. Typically, small icons are 16-by-16 pixels and large icons are 32-by-32 pixels. Within each size are two separate bitmaps: one 4-bit-per-pixel bitmap for the color image and one monochrome (1-bit-per-pixel) bitmap for the "mask." If a mask bit is 0, the corresponding image pixel represents an opaque color. If the mask bit is 1, an image color of black (0) means that the pixel is transparent and an image color of white (0xF) means that the background color is inverted at the pixel location. Windows 95 and Windows NT seem to process inverted colors a little differently than Windows 3.x does—the inverted pixels show up transparent against the desktop, black against a Windows Explorer window background, and white against list and tree control backgrounds. Don't ask me why.

    Small icons were new with Windows 95. They're used in the task bar, in Windows Explorer, and in your list and tree controls, if you want them there. If an icon doesn't have a 16-by-16-pixel bitmap, Windows manufactures a small icon out of the 32-by-32-pixel bitmap, but it won't be as neat as one you draw yourself.

    The graphics editor lets you create and edit icons. Look at the color palette shown here.

    The top square in the upper-left portion shows you the main color for brushes, shape interiors, and so on, and the square under it shows the border color for shape outlines. You select a main color by left-clicking on a color, and you select a border color by right-clicking on a color. Now look at the top center portion of the color palette. You click on the upper "monitor" to paint transparent pixels, which are drawn in dark cyan. You click on the lower monitor to paint inverted pixels, which are drawn in red.

  9. Program the list control. In the dialog editor, set the list control's style attributes as shown in the next illustration.

    Make sure the Border style on the More Styles page is set. Next add the following code to OnInitDialog:

    static char* color[] = {"white", "black", "red",
                            "blue", "yellow", "cyan",
                            "purple", "green"};
    CListCtrl* pList = 
        (CListCtrl*) GetDlgItem(IDC_LISTVIEW1);
    pList->SetImageList(&m_imageList, LVSIL_SMALL); 
    for (n = 0; n < 8; n++) {
        pList->InsertItem(n, color[n], n); 
    }
    pList->SetBkColor(RGB(0, 255, 255)); // UGLY!
    pList->SetTextBkColor(RGB(0, 255, 255)); 
    

    As the last two lines illustrate, you don't use the WM_CTLCOLOR message with common controls; you just call a function to set the background color. As you'll see when you run the program, however, the icons' inverse-color pixels look shabby.

    If you use ClassWizard to map the list control's LVN_ITEMCHANGED notification message, you'll be able to track the user's selection of items. The code in the following handler displays the selected item's text in a static control:

    void CEx06bDialog::OnItemchangedListview1(NMHDR* pNMHDR,
                                              LRESULT* pResult)
    {
        NM_LISTVIEW* pNMListView = (NM_LISTVIEW*)pNMHDR;
        CListCtrl* pList =
            (CListCtrl*) GetDlgItem(IDC_LISTVIEW1);
        int nSelected = pNMListView->iItem;
        if (nSelected >= 0) {
            CString strItem = pList->GetItemText(nSelected, 0);
            SetDlgItemText(IDC_STATIC_LISTVIEW1, strItem);
        } 
        *pResult = 0;
    }
    

    The NM_LISTVIEW structure has a data member called iItem that contains the index of the selected item.

  10. Program the tree control. In the dialog editor, set the tree control's style attributes as shown here.

    Next, add the following lines to OnInitDialog:

    CTreeCtrl* pTree = (CTreeCtrl*) GetDlgItem(IDC_TREEVIEW1);
    pTree->SetImageList(&m_imageList, TVSIL_NORMAL);
    // tree structure common values
    TV_INSERTSTRUCT tvinsert;
    tvinsert.hParent = NULL;
    tvinsert.hInsertAfter = TVI_LAST;
    tvinsert.item.mask = TVIF_IMAGE | TVIF_SELECTEDIMAGE |
                         TVIF_TEXT;
    tvinsert.item.hItem = NULL; 
    tvinsert.item.state = 0;
    tvinsert.item.stateMask = 0;
    tvinsert.item.cchTextMax = 6;
    tvinsert.item.iSelectedImage = 1;
    tvinsert.item.cChildren = 0;
    tvinsert.item.lParam = 0;
    // top level
    tvinsert.item.pszText = "Homer";
    tvinsert.item.iImage = 2;
    HTREEITEM hDad = pTree->InsertItem(&tvinsert);
    tvinsert.item.pszText = "Marge";
    HTREEITEM hMom = pTree->InsertItem(&tvinsert);
    // second level     
    tvinsert.hParent = hDad;
    tvinsert.item.pszText = "Bart";
    tvinsert.item.iImage = 3;
    pTree->InsertItem(&tvinsert);
    tvinsert.item.pszText = "Lisa";
    pTree->InsertItem(&tvinsert);
    // second level
    tvinsert.hParent = hMom;
    tvinsert.item.pszText = "Bart";
    tvinsert.item.iImage = 4;
    pTree->InsertItem(&tvinsert);
    tvinsert.item.pszText = "Lisa";
    pTree->InsertItem(&tvinsert);
    tvinsert.item.pszText = "Dilbert";
    HTREEITEM hOther = pTree->InsertItem(&tvinsert);
    // third level
    tvinsert.hParent = hOther;
    tvinsert.item.pszText = "Dogbert";
    tvinsert.item.iImage = 7;
    pTree->InsertItem(&tvinsert);
    tvinsert.item.pszText = "Ratbert";
    pTree->InsertItem(&tvinsert); 
    

    As you can see, this code sets TV_INSERTSTRUCT text and image indexes and calls InsertItem to add nodes to the tree.

    Finally, use ClassWizard to map the TVN_SELCHANGED notification for the tree control. Here is the handler code to display the selected text in a static control:

    void CEx06bDialog::OnSelchangedTreeview1(NMHDR* pNMHDR,
                                             LRESULT* pResult)
    {
        NM_TREEVIEW* pNMTreeView = (NM_TREEVIEW*)pNMHDR;
        CTreeCtrl* pTree = (CTreeCtrl*) GetDlgItem(IDC_TREEVIEW1);
        HTREEITEM hSelected = pNMTreeView->itemNew.hItem;
        if (hSelected != NULL) {
            char text[31];
            TV_ITEM item;
            item.mask = TVIF_HANDLE | TVIF_TEXT;
            item.hItem = hSelected;
            item.pszText = text;
            item.cchTextMax = 30;
            VERIFY(pTree->GetItem(&item));
            SetDlgItemText(IDC_STATIC_TREEVIEW1, text);
        } 
        *pResult = 0;
    }
    

    The NM_TREEVIEW structure has a data member called itemNew that contains information about the selected node; itemNew.hItem is the handle of that node. The GetItem function retrieves the node's data, storing the text using a pointer supplied in the TV_ITEM structure. The mask variable tells Windows that the hItem handle is valid going in and that text output is desired.

  11. Add code to the virtual OnDraw function in file ex06bView.cpp. The following boldface code replaces the previous code:

    void CEx06bView::OnDraw(CDC* pDC)
    {
        pDC->TextOut(0, 0, "Press the left mouse button here."); 
    }
    

  12. Use ClassWizard to add the OnLButtonDown member function. Edit the AppWizard-generated code as follows:

    void CEx06bView::OnLButtonDown(UINT nFlags, CPoint point)
    {
        CEx06bDialog dlg;
    
        dlg.m_nTrackbar1 = 20;
        dlg.m_nTrackbar2 = 2; // index for 8.0
        dlg.m_nProgress = 70; // write-only
        dlg.m_dSpin = 3.2;
    
        dlg.DoModal(); 
    }
    

    Add a statement to include ex06bDialog.h in file ex06bView.cpp.

  13. Compile and run the program. Experiment with the controls to see how they work. We haven't added code to make the progress indicator functional; we'll cover that in Chapter 12.

Other Windows Common Controls

You've seen most of the common controls that appear on the dialog editor control palette. We've skipped the animation control because this book doesn't cover multimedia, and we've skipped the hot key control because it isn't very interesting. The tab control is interesting, but you seldom use it inside another dialog. Chapter 13 shows you how to construct a tabbed dialog, sometimes known as a property sheet. In Chapter 13, you'll also see an application that is built around the CRichEditView class, which incorporates the Windows rich edit control.