The Ex08b Example: Advanced Common Controls
In this example, we'll build a dialog box that demonstrates how to create and program each type of advanced common control. The steps required to create the dialog box are as follows:
Run the MFC Application Wizard to generate a project named Ex08b. Choose New Project from the Visual Studio File menu. In the New Project dialog box, select the MFC Application template, type the name Ex08b, and click OK. In the MFC Application Wizard, accept all the defaults but one: On the Application Type page, select Single Document.
Create a new dialog resource with ID IDD_DIALOG1. Choose Add Resource from the Project menu and add a new dialog resource. Using the Toolbox, add controls to the dialog box. The following is a list of the control types, their IDs, and their tab order. After you set the Caption properties for the static text controls to the appropriate text, the dialog box should have the following controls and tab order:

Until we set some properties, your dialog box will not look exactly like the one shown earlier in Figure 8-2.
Control Type | ID | Tab Order |
---|---|---|
Group Box | IDC_STATIC | 1 |
Static | IDC_STATIC | 2 |
Date Time Picker | IDC_DATETIMEPICKER1 | 3 |
Static | IDC_STATIC1 | 4 |
Static | IDC_STATIC | 5 |
Date Time Picker | IDC_DATETIMEPICKER2 | 6 |
Static | IDC_STATIC2 | 7 |
Static | IDC_STATIC | 8 |
Date Time Picker | IDC_DATETIMEPICKER3 | 9 |
Static | IDC_STATIC3 | 10 |
Static | IDC_STATIC | 11 |
Date Time Picker | IDC_DATETIMEPICKER4 | 12 |
Static | IDC_STATIC4 | 13 |
Static | IDC_STATIC | 14 |
Month Calendar | IDC_MONTHCALENDAR1 | 15 |
Static | IDC_STATIC5 | 16 |
Group Box | IDC_STATIC | 17 |
Static | IDC_STATIC | 18 |
IP Address | IDC_IPADDRESS1 | 19 |
Static | IDC_STATIC6 | 20 |
Group Box | IDC_STATIC | 21 |
Static | IDC_STATIC | 22 |
Extended Combo Box | IDC_COMBOBOXEX1 | 23 |
Static | IDC_STATIC7 | 24 |
Static | IDC_STATIC | 25 |
Extended Combo Box | IDC_COMBOBOXEX2 | 26 |
Static | IDC_STATIC8 | 27 |
Button | IDOK | 28 |
Button | IDCANCEL | 29 |
Use the MFC Class Wizard to create a new class, CEx08bDialog, that is derived from CDialog. Choose Add Class from the Project menu to start the MFC Class Wizard. Select CDialog as the base class and IDD_DIALOG1 as the name of the Dialog ID, as shown here:

Override the OnInitDialog function. Select the CEx08bDialog class in Class View. Click the Overrides button at the top of the Properties window and add the OnInitDialog function.
Set the properties for the dialog box's controls. To demonstrate the full range of controls, we'll need to set a variety of properties for each of the common controls in this example. Here's a brief overview of each property you'll need to set:
The Short Date and Time Picker For the first date and time picker control (IDC_DATETIMEPICKER1), be sure the Format property is set to Short Date (the default).
The Long Date and Time Picker For the second date and time picker control (IDC_DATETIMEPICKER2), set the Format property to Long Date.
The Short and NULL Date and Time Picker For the third date and time picker control (IDC_DATETIMEPICKER3), be sure the Format property is set to Short Date and set the Allow Edit, Show None, and Use Spin Control properties to True.
The Time Picker The fourth date and time picker control (IDC_DATETIMEPICKER4) is configured to let the user select a time. Set the Format property to Time, and set the Use Spin Control property to True.
The Month Calendar To configure the month calendar, you must set a few properties. First set the Day States property to True. With the default properties, the month calendar does not look like a control in the dialog box. No borders are drawn. To make the control fit in with the other controls in the dialog box, set the Client Edge and Static Edge properties to True.
The IP Address This control (IDC_IPADDRESS1) does not require any special properties.
Extended Combo Boxes These controls (IDC_COMBOBOXEX1 and IDC_COMBOBOXEX2) do not require any special properties.
Add the variables to CEx08bDialog. Use the Add Member Variable Wizard to add member variables to CEx08bDialog. To start the wizard, select the CEx08bDialog class in Class View and then choose Add Variable from the Project menu. Enter the following member variables for each control listed.
Control ID | Category | Variable Type | Variable Name |
---|---|---|---|
IDC_DATETIMEPICKER1 | Control | CDateTimeCtrl | m_MonthCal1 |
IDC_DATETIMEPICKER2 | Control | CDateTimeCtrl | m_MonthCal2 |
IDC_DATETIMEPICKER3 | Control | CDateTimeCtrl | m_MonthCal3 |
IDC_DATETIMEPICKER4 | Control | CDateTimeCtrl | m_MonthCal4 |
IDC_IPADDRESS1 | Control | CIPAddressCtrl | m_ptrIPCtrl |
IDC_MONTHCALENDAR1 | Control | CMonthCalCtrl | m_MonthCal5 |
IDC_STATIC1 | Value | CString | m_strDate1 |
IDC_STATIC2 | Value | CString | m_strDate2 |
IDC_STATIC3 | Value | CString | m_strDate3 |
IDC_STATIC4 | Value | CString | m_strDate4 |
IDC_STATIC5 | Value | CString | m_strDate5 |
IDC_STATIC6 | Value | CString | m_strIPValue |
IDC_STATIC7 | Value | CString | m_strComboEx1 |
IDC_STATIC8 | Value | CString | m_strComboEx2 |
Program the short date and time picker. In this example, we don't mind if the first date and time picker starts with the current date, so we don't have any OnInitDialog handling for this control. (If we did want to change the date, we could make a call to SetTime for the control in OnInitDialog.) At run time, when the user selects a new date in the first date and time picker, the companion static control will be automatically updated. To achieve this, we need to add a handler for the DTN_DATETIMECHANGE message. Select the CEx08bDialog class in Class View, click the Events button in the Properties window, expand the IDC_DATETIMEPICKER1 item, select the DTN_DATETIMECHANGE message, and add the OnDtnDatetimechangeDatetimepicker1 handler. Repeat this step for each of the other three IDC_DATETIMEPICKER IDs. Next, add the following boldface code to the handler for Datetimepicker1 created by Visual Studio:
void CEx08bDialog::OnDtnDatetimechangeDatetimepicker1 (NMHDR* pNMHDR,
LRESULT* pResult)
{
LPNMDATETIMECHANGE pDTChange =
reinterpret_cast<LPNMDATETIMECHANGE>(pNMHDR);
CTime ct;
m_MonthCal1.GetTime(ct);
m_strDate1.Format(_T("%02d/%02d/%2d"),
ct.GetMonth(),ct.GetDay(),ct.GetYear());
UpdateData(FALSE);
*pResult = 0;
}
This code uses the m_MonthCal1 data member that maps to the first date and time picker to retrieve the time into the CTime object variable ct. It then calls the CString::Format member function to set the companion static string. Finally, the call to UpdateData(FALSE) triggers MFC's DDX and causes the static to be automatically updated to m_strDate1.
Program the long date and time picker. Now we need to provide a similar handler for the second date and time picker:
void CEx08bDialog::OnDtnDatetimechangeDatetimepicker2(NMHDR* pNMHDR,
LRESULT* pResult)
{
LPNMDATETIMECHANGE pDTChange =
reinterpret_cast<LPNMDATETIMECHANGE>(pNMHDR);
CTime ct;
m_MonthCal2.GetTime(ct);
m_strDate2.Format(_T("%02d/%02d/%2d"),
ct.GetMonth(),ct.GetDay(),ct.GetYear());
UpdateData(FALSE);
*pResult = 0;
}
Program the third date and time picker. The third date and time picker needs a similar handler, but because we set the Show None style in the dialog box properties, it is possible for the user to specify a NULL date by selecting the inline check box. Instead of blindly calling GetTime, we have to check the return value. If the return value of the GetTime call is nonzero, the user has selected a NULL date. If the return value is zero, a valid date has been selected. As in the previous two handlers, when a CTime object is returned, it is converted into a string and automatically displayed in the companion static control.
void CEx08bDialog::OnDtnDatetimechangeDatetimepicker3(NMHDR* pNMHDR,
LRESULT* pResult)
{
LPNMDATETIMECHANGE pDTChange =
reinterpret_cast<LPNMDATETIMECHANGE>(pNMHDR);
//NOTE: this one can be null!
CTime ct;
int nRetVal = m_MonthCal3.GetTime(ct);
if (nRetVal) //If not zero, it's null; and if it is,
// do the right thing.
{
m_strDate3 = "NO DATE SPECIFIED!!";
}
else
{
m_strDate3.Format(_T("%02d/%02d/%2d"),ct.GetMonth(),
ct.GetDay(),ct.GetYear());
}
UpdateData(FALSE);
*pResult = 0;
}
Program the time picker. The time picker needs a similar handler, but this time the format displays hours/minutes/seconds instead of months/days/years:
void CEx08bDialog::OnDtnDatetimechangeDatetimepicker4(NMHDR* pNMHDR,
LRESULT* pResult)
{
LPNMDATETIMECHANGE pDTChange =
reinterpret_cast<LPNMDATETIMECHANGE>(pNMHDR);
CTime ct;
m_MonthCal4.GetTime(ct);
m_strDate4.Format(_T("%02d:%02d:%2d"),
ct.GetHour(),ct.GetMinute(),ct.GetSecond());
UpdateData(FALSE);
*pResult = 0;
}
Program the month selector. You might think that the month selector handler is similar to the date and time picker's handler, but they're actually somewhat different. First of all, the message you need to handle for detecting when the user has selected a new date is the MCN_SELCHANGE message. Select the CEx08bDialog class in Class View, click the Events button in the Properties window, expand the IDC_MONTHCALENDER1 item, select the MCN_SELCHANGE message, and add the OnMcnSelchangeMonthcalender1 handler. In addition to the different message handler, this control uses GetCurSel as the date and time picker instead of GetTime. The following code shows the MCN_SELCHANGE handler for the month calendar control.
void CEx08bDialog::OnMcnSelchangeMonthcalendar1(NMHDR* pNMHDR,
LRESULT* pResult)
{
LPNSELCHANGE pSelChange =
reinterpret_cast<LPNMSELCHANGE>(pNMHDR);
CTime ct;
m_MonthCal5.GetCurSel(ct);
m_strDate5.Format(_T("%02d/%02d/%2d"),
ct.GetMonth(),ct.GetDay(),ct.GetYear());
UpdateData(FALSE);
*pResult = 0;
}
Program the IP control. First, we need to make sure the control is initialized. In this example, we initialize the control to 0 by giving it a 0 DWORD value. If you don't initialize the control, each segment will be blank. To initialize the control, add this call to the CEx08bDialog::OnInitDialog function:
// Initialize the IP control
m_ptrIPCtrl.SetAddress(0L);
Now we need to add a handler to update the companion static control whenever the IP address control changes. First, we need to add a handler for the IPN_FIELDCHANGED notification message. Select the CEx08bDialog class in Class View, click the Events button in the Properties window, expand the IDC_IPADDRESS1 item, select the IPN_FIELDCHANGED message, and add the OnIpnFieldchangedIpaddress1 handler.Next, we need to implement the handler as follows:
void CEx08bDialog::OnIpnFieldchangedIpaddress1(NMHDR* pNMHDR,
LRESULT* pResult)
{
LPNMIPADDRESS pIPAddr =
reinterpret_case<LPNMIADDRESS>(pNMHDR);
DWORD dwIPAddress;
m_ptrIPCtrl.GetAddress(dwIPAddress);
m_strIPValue.Format("%d.%d.%d.%d %x.%x.%x.%x",
HIBYTE(HIWORD(dwIPAddress)),
LOBYTE(HIWORD(dwIPAddress)),
HIBYTE(LOWORD(dwIPAddress)),
LOBYTE(LOWORD(dwIPAddress)),
HIBYTE(HIWORD(dwIPAddress)),
LOBYTE(HIWORD(dwIPAddress)),
HIBYTE(LOWORD(dwIPAddress)),
LOBYTE(LOWORD(dwIPAddress)));
UpdateData(FALSE);
*pResult = 0;
}
The first call to CIPAddressCtrl::GetAddress retrieves the current IP address into the local dwIPAddress DWORD variable. Next, we make a fairly complex call to CString::Format to deconstruct the DWORD into the various fields. This call uses the LOWORD macro to first get to the bottom word of the DWORD and then uses the HIBYTE/LOBYTE macros to further deconstruct the fields in order from field 0 to field 3.
Add Items to the first extended combo box. Add this code to OnInitDialog to programmatically add three items ("George", "Sandy", and "Teddy") to the first extended combo box. Can you spot how this differs from a "normal" combo box control?
// Initialize IDC_COMBOBOXEX1
CComboBoxEx* pCombo1 =
(CComboBoxEx*) GetDlgItem(IDC_COMBOBOXEX1);
CString rgstrTemp1[3];
rgstrTemp1[0] = "George";
rgstrTemp1[1] = "Sandy";
rgstrTemp1[2] = "Teddy";
COMBOBOXEXITEM cbi1;
cbi1.mask = CBEIF_TEXT;
for (int nCount = 0; nCount < 3; nCount++)
{
cbi1.iItem = nCount;
cbi1.pszText = (LPTSTR)(LPCTSTR)rgstrTemp1[nCount];
cbi1.cchTextMax = 256;
pCombo1->InsertItem(&cbi1);
}
The first thing you probably noticed is the use of the COMBOBOXEXITEM structure for the extended combo box instead of the plain integers used for items in an older combo box.
Add a handler for the first extended combo box. We need to handle the CBN_SELCHANGE message for the first extended combo box. Select the CEx08bDialog class in Class View, click the Events button in the Properties window, expand the IDC_COMBOBOX1 item, select the CBN_SELCHANGE message, and add the OnCbnSelchangeComboboxex1 handler. The following code shows the extended combo box handler.
void CEx08bDialog::OnCbnSelchangeComboboxex1 ()
{
COMBOBOXEXITEM cbi;
CString str ("dummy_string");
CComboBoxEx * pCombo = (CComboBoxEx *)GetDlgItem(IDC_COMBOBOXEX1);
int nSel = pCombo->GetCurSel();
cbi.iItem = nSel;
cbi.pszText = (LPTSTR)(LPCTSTR)str;
cbi.mask = CBEIF_TEXT;
cbi.cchTextMax = str.GetLength();
pCombo->GetItem(&cbi);
SetDlgItemText(IDC_STATIC7,str);
return;
}
Once the handler retrieves the item, it extracts the string and calls SetDlgItemText to update the companion static control.
Add Images to the items in the second extended combo box. The first extended combo box does not need any special programming. It simply demonstrates how to implement a simple extended combo box that is similar to the older, nonextended combo box. The second combo box requires a good bit of programming. First, we created six bitmaps and eight icons that we need to add to the resources for the project. Of course, you're free to grab these images from the companion CD instead of recreating them all by hand, or you can choose to use any bitmaps and icons. Add these bitmaps and icons to Resource View and set their IDs as shown here:

Before we start adding graphics to the extended combo box, let's create a public CImageList data member in the CEx08bDialog class named m_imageList. Add the following code to

CImageList m_imageList;
Now we can add some of the bitmap images to the image list and then "attach" the images to the three items already in the extended combo box. Add the following code to your CEx08bDialog's OnInitDialog method to achieve this:
// Initialize IDC_COMBOBOXEX2
CComboBoxEx* pCombo2 =
(CComboBoxEx*) GetDlgItem(IDC_COMBOBOXEX2);
// First let's add images to the items there.
// We have six images in bitmaps to match to our strings:
m_imageList.Create(32,16,ILC_MASK,12,4);
CBitmap bitmap;
bitmap.LoadBitmap(IDB_BMBIRD);
m_imageList.Add(&bitmap, (COLORREF)0xFFFFFF);
bitmap.DeleteObject();
bitmap.LoadBitmap(IDB_BMBIRDSELECTED);
m_imageList.Add(&bitmap, (COLORREF)0xFFFFFF);
bitmap.DeleteObject();
bitmap.LoadBitmap(IDB_BMDOG);
m_imageList.Add(&bitmap, (COLORREF)0xFFFFFF);
bitmap.DeleteObject();
bitmap.LoadBitmap(IDB_BMDOGSELECTED);
m_imageList.Add(&bitmap, (COLORREF)0xFFFFFF);
bitmap.DeleteObject();
bitmap.LoadBitmap(IDB_BMFISH);
m_imageList.Add(&bitmap, (COLORREF)0xFFFFFF);
bitmap.DeleteObject();
bitmap.LoadBitmap(IDB_BMFISHSELECTED);
m_imageList.Add(&bitmap, (COLORREF)0xFFFFFF);
bitmap.DeleteObject();
// Set the imagelist
pCombo2->SetImageList(&m_imageList);
CString rgstrTemp2[3];
rgstrTemp2[0] = "Tweety";
rgstrTemp2[1] = "Mack";
rgstrTemp2[2] = "Jaws";
COMBOBOXEXITEM cbi2;
cbi2.mask = CBEIF_TEXT|CBEIF_IMAGE|CBEIF_SELECTEDIMAGE|CBEIF_INDENT;
int nBitmapCount = 0;
for (int nCount = 0; nCount < 3; nCount++)
{
cbi2.iItem = nCount;
cbi2.pszText = (LPTSTR)(LPCTSTR)rgstrTemp2[nCount];
cbi2.cchTextMax = 256;
cbi2.iImage = nBitmapCount++;
cbi2.iSelectedImage = nBitmapCount++;
cbi2.iIndent = (nCount & 0x03);
pCombo2->InsertItem(&cbi2);
}
The extended combo box initialization code first creates a pointer to the control using GetDlgItem. Then it calls Create to create memory for the images to be added and to initialize the image list. The next series of calls loads each bitmap, adds them to the image list, and then deletes the resource allocated in the load.CComboBoxEx::SetImageList is called to associate the m_imageList with the extended combo box. Then a COMBOBOXEXITEM structure is initialized with a mask, and the for loop iterates from 0 through 2, setting the selected and unselected images with each pass through the loop. There's an array of strings named rgstrTemp that's associated with each picture. The rgstrTemp array includes the strings "Tweety", "Mack", and "Jaws". The variable nBitmapCount is used to set the string in the extended combo box. The variable nBitmapCount also increments through the image list to ensure that the correct image ID is put into the COMBOBOXEXITEM structure. Then the loop sets up the images for the list item and finally calls CComboBoxEx::InsertItem to put the COMBOBOXEXITEM structure back into the extended combo box and complete the association of images with the existing items in the list.
Add items to the second extended combo box. The other technique available for putting images into an extended combo box is to add them dynamically, as shown in the following code. Add this code to OnInitDialog:
HICON hIcon[8];
int n;
// Now let's insert some color 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]);
}
static char* color[] = {"white", "black", "red",
"blue", "yellow", "cyan",
"purple", "green"};
cbi2.mask = CBEIF_IMAGE|CBEIF_TEXT|CBEIF_OVERLAY|
CBEIF_SELECTEDIMAGE;
for (n = 0; n < 8; n++) {
cbi2.iItem = n;
cbi2.pszText = color[n];
cbi2.iImage = n+6; // 6 is the offset into the image list from
cbi2.iSelectedImage = n+6; // the first six items we added...
cbi2.iOverlay = n+6;
int nItem = pCombo2->InsertItem(&cbi2);
ASSERT(nItem == n);
}
The addition of the icons above is similar to the Ex08a list control example shown earlier in this chapter. The for loop fills out the COMBOBOXEXITEM structure and then calls CComboBoxEx::InsertItem with each item to add it to the list.
Add a handler for the second extended combo box. Select the CEx08bDialog class in Class View, click the Events button in the Properties window, expand the IDC_COMBOBOX2 item, select the CBN_SELCHANGE message, and add the OnCbnSelchangeComboboxex2 handler. Add the following boldface code to the second extended combo box handler. The handler is essentially the same as the first.
void CEx08bDialog::OnCbnSelchangeComboboxex2()
{
COMBOBOXEXITEM cbi;
CString str ("dummy_string");
CComboBoxEx * pCombo = (CComboBoxEx *)GetDlgItem(IDC_COMBOBOXEX2);
int nSel = pCombo->GetCurSel();
cbi.iItem = nSel;
cbi.pszText = (LPTSTR)(LPCTSTR)str;
cbi.mask = CBEIF_TEXT;
cbi.cchTextMax = str.GetLength();
pCombo->GetItem(&cbi);
SetDlgItemText(IDC_STATIC8,str);
return;
}
Connect the view and the dialog box. Add code to the virtual OnDraw function in

void CEx08bView::OnDraw(CDC* pDC)
{
CEx08vDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
pDC->TextOut(0, 0, "Press the left mouse button here.");
}
Add the OnLButtonDown member function to the CEx08aView class. Select the CEx08bView class in Class View, click the Messages button in the Properties window, select the WM_LBUTTONDOWN message, and add the OnLButtonDown handler. Edit the code as follows:
void CEx08aView::OnLButtonDown(UINT nFlags, CPoint point)
{
CEx08bDialog dlg;
dlg.DoModal();
CView::OnLButtonDown(nFlags, point);
}
Add a statement to include


#include "Ex08bDialog.h"
Compile and run the program. Now you can experiment with the various common controls to see how they work.