Programming Windows with MFC, Second Edition
Now you know how to write ActiveX controls. But what about control containers? Not just any window can host an ActiveX control; to do it, someone must implement the requisite COM interfaces. Fortunately, MFC will provide those interfaces for you. All you have to do is check a box in AppWizard and insert the control into the project. The control will then appear in the dialog editor's control toolbar, where it can be inserted into any MFC dialog.
Here are the steps required to use an ActiveX control in an MFC application:
- In AppWizard's Step 2 dialog box (for dialog-based applications) or Step 3 dialog box (for nondialog-based applications), check the ActiveX Controls box, as shown in Figure 21-23.
- When AppWizard is done, use Visual C++'s Project-Add To Project-Components And Controls command to insert the control into the project. This command displays the Components And Controls Gallery dialog box. The Registered ActiveX Controls folder contains a list of all the ActiveX controls installed on this system. (See Figure 21-24.)
- When the Confirm Classes dialog box (shown in Figure 21-25) appears, either edit the class name and file names or accept the defaults. Visual C++ will create a wrapper class that the container can use to interact with the control. Member functions in the wrapper class will provide access to the control's methods and properties. Visual C++ gets the information it needs to build the wrapper class from the control's type library.
- Close the Components And Controls Gallery dialog box.
Figure 21-23. Checking AppWizard's ActiveX Controls box makes any MFC dialog an ActiveX control container.
Figure 21-24. The Components And Controls Gallery dialog box showing a list of registered ActiveX controls.
Figure 21-25. The Confirm Classes dialog box.
If you now switch to ResourceView and open a dialog resource, the dialog editor's controls toolbar will contain a button representing the control. Adding the control to a dialog is a simple matter of clicking the button and drawing the control into the dialog. You can display the control's property sheet by right-clicking the control and selecting Properties from the context menu. Any changes you make to the control's properties will be serialized into the project's RC file and reapplied when the dialog is displayed.
Calling an ActiveX Control's Methods
Can it really be that easy? You bet. But that's not all. You can program the control—call its methods and read and write its properties programmatically—using the wrapper class generated when the control was added to the project. First, though, you must instantiate the wrapper class and connect it to a running control. Here's how to do it:
- Go to ClassWizard's Member variables page, and select the ActiveX control's ID in the Control IDs box.
- Click the Add Variable button, and choose the wrapper class's name (for example, CCalendar) in the Variable Type box. Enter a name for the instantiated class in the Member Variable Name box, too.
- Click OK.
After that, you can call a control method or access a control property by calling the corresponding member function on the object whose name you entered in the Member Variable Name box. For a calendar control object named m_ctlCalendar, the following statement calls the control's SetDate method to set the date to January 1, 2000:
m_ctlCalendar.SetDate (2000, 1, 1); |
The next statement sets the control's background color to light gray:
m_ctlCalendar.SetBackColor (OLE_COLOR (RGB (192, 192, 192))); |
It works because ClassWizard added a DDX_Control statement to the dialog's DoDataExchange function that connects m_ctlCalendar to the running ActiveX control. You could add this statement yourself, but regardless of how you choose to do it, the fact remains that accessing the control from your program's source code is now no more difficult than calling a C++ member function.
Processing Events
You might want to do one more thing with an ActiveX control in an MFC application: process events. In Chapter 1, you learned that MFC uses message maps to correlate messages to member functions. Similarly, it uses event sink maps to correlate events fired by ActiveX controls to member functions. Here's a simple event sink map that connects NewDay events fired by our calendar control to a CMyDialog member function named OnNewDay:
BEGIN_EVENTSINK_MAP (CMyDialog, CDialog) ON_EVENT (CMyDialog, IDC_CALENDAR, 1, OnNewDay, VTS_I2) END_EVENTSINK_MAP () |
The second parameter passed to ON_EVENT is the control ID. The third is the event's dispatch ID. The fourth is the member function that's called when the event is fired, and the final parameter specifies the types of parameters included in the event's parameter list—in this case, a 16-bit integer (VTS_I2). An event sink map can hold any number of ON_EVENT entries, making it a simple matter for an MFC container to respond to all manner of control events.
How do you write event sink maps? You don't have to write them by hand because ClassWizard will write them for you. To write an event handler, go to ClassWizard's Message Maps page, select the class that hosts the control in the Class Name box, and select the control's ID in the Object IDs box. You'll see a list of events that the control is capable of firing in the Messages box. (See Figure 21-26.) Select an event, and click the Add Function button. Enter a name for the member function you want to be called when the event is fired, and ClassWizard will add the member function to the class and an entry to the event sink map. When an event of that type is fired, the member function will be called just as if it were an ordinary message handler.
Figure 21-26. Adding an event handler with ClassWizard.
The CalUser Application
The CalUser application shown in Figure 21-1 is a dialog-based MFC application that hosts the MFC calendar control. Selecting a new month or year changes the calendar by calling its SetDate method. Clicking a square in the calendar pops up a message box that echoes the date that was clicked. The message box is displayed by an event handler named OnNewDay that's called each time the control fires a NewDay event. Relevant portions of CalUser's source code are reproduced in Figure 21-27.
Figure 21-27. The CalUser application.
CalUserDlg.h
// CalUserDlg.h : header file // //AFX_INCLUDES #if !defined( AFX_CALUSERDLG_H__85FDD589_470B_11D2_AC96_006008A8274D__INCLUDED_) #define AFX_CALUSERDLG_H__85FDD589_470B_11D2_AC96_006008A8274D__INCLUDED_ #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 /////////////////////////////////////////////////////////////////////////// // CCalUserDlg dialog class CCalUserDlg : public CDialog { // Construction public: CCalUserDlg(CWnd* pParent = NULL); // standard constructor // Dialog Data //{{AFX_DATA(CCalUserDlg) enum { IDD = IDD_CALUSER_DIALOG }; CComboBox m_ctlYear; CComboBox m_ctlMonth; CCalendar m_ctlCalendar; //}}AFX_DATA // ClassWizard generated virtual function overrides //AFX_VIRTUAL // Implementation protected: static const CString m_strMonths[]; void InitListOfYears (); void InitListOfMonths (); HICON m_hIcon; // Generated message map functions //AFX_MSG DECLARE_MESSAGE_MAP() }; // // Microsoft Visual C++ will insert additional declarations // immediately before the previous line. #endif // !defined( // AFX_CALUSERDLG_H__85FDD589_470B_11D2_AC96_006008A8274D__INCLUDED_) |
CalUserDlg.cpp
// CalUserDlg.cpp : implementation file // #include "stdafx.h" #include "CalUser.h" #include "CalUserDlg.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif /////////////////////////////////////////////////////////////////////////// // CCalUserDlg dialog CCalUserDlg::CCalUserDlg(CWnd* pParent /*=NULL*/) : CDialog(CCalUserDlg::IDD, pParent) { //AFX_DATA_INIT // Note that LoadIcon does not require a subsequent // DestroyIcon in Win32 m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME); } void CCalUserDlg::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); //AFX_DATA_MAP } BEGIN_MESSAGE_MAP(CCalUserDlg, CDialog) //AFX_MSG_MAP END_MESSAGE_MAP() const CString CCalUserDlg::m_strMonths[] = { _T ("January"), _T ("February"), _T ("March"), _T ("April"), _T ("May"), _T ("June"), _T ("July"), _T ("August"), _T ("September"), _T ("October"), _T ("November"), _T ("December") }; /////////////////////////////////////////////////////////////////////////// // CCalUserDlg message handlers BOOL CCalUserDlg::OnInitDialog() { CDialog::OnInitDialog(); SetIcon(m_hIcon, TRUE); // Set big icon SetIcon(m_hIcon, FALSE); // Set small icon // // Initialize the Month control. // COleDateTime date = m_ctlCalendar.GetDate (); int nMonth = date.GetMonth (); int nYear = date.GetYear (); InitListOfMonths (); m_ctlMonth.SetCurSel (nMonth - 1); // // Initialize the Year control. // InitListOfYears (); m_ctlYear.SetCurSel (nYear - 1970); return TRUE; } void CCalUserDlg::OnPaint() { if (IsIconic()) { CPaintDC dc(this); // device context for painting SendMessage(WM_ICONERASEBKGND, (WPARAM) dc.GetSafeHdc(), 0); // Center icon in client rectangle int cxIcon = GetSystemMetrics(SM_CXICON); int cyIcon = GetSystemMetrics(SM_CYICON); CRect rect; GetClientRect(&rect); int x = (rect.Width() - cxIcon + 1) / 2; int y = (rect.Height() - cyIcon + 1) / 2; // Draw the icon dc.DrawIcon(x, y, m_hIcon); } else { CDialog::OnPaint(); } } HCURSOR CCalUserDlg::OnQueryDragIcon() { return (HCURSOR) m_hIcon; } void CCalUserDlg::InitListOfMonths() { for (int i=0; i<12; i++) m_ctlMonth.AddString (m_strMonths[i]); } void CCalUserDlg::InitListOfYears() { for (int i=1970; i<=2037; i++) { CString string; string.Format (_T ("%d"), i); m_ctlYear.AddString (string); } } void CCalUserDlg::OnChangeDate() { int nMonth = m_ctlMonth.GetCurSel () + 1; int nYear = GetDlgItemInt (IDC_YEAR); ASSERT (nYear != 0); m_ctlCalendar.SetDate (nYear, nMonth, 1); } BEGIN_EVENTSINK_MAP(CCalUserDlg, CDialog) //AFX_EVENTSINK_MAP END_EVENTSINK_MAP() void CCalUserDlg::OnNewDay(short nDay) { static const CString strDays[] = { _T ("Sunday"), _T ("Monday"), _T ("Tuesday"), _T ("Wednesday"), _T ("Thursday"), _T ("Friday"), _T ("Saturday"), }; COleDateTime date = m_ctlCalendar.GetDate (); int nMonth = date.GetMonth (); int nYear = date.GetYear (); CTime time (nYear, nMonth, nDay, 12, 0, 0); int nDayOfWeek = time.GetDayOfWeek () - 1; CString string; string.Format (_T ("%s, %s %d, %d"), strDays[nDayOfWeek], m_strMonths[nMonth - 1], nDay, nYear); MessageBox (string); } |
Using ActiveX Controls in Nondialog Windows
MFC and ClassWizard make it wonderfully easy to use ActiveX controls in dialogs, but what about nondialog windows? It turns out that MFC allows any CWnd object to host ActiveX controls. You can create ActiveX controls just about anywhere in an MFC application, but outside of dialog windows, you have to do some manual coding to make it happen.
Here's how to add the MFC calendar control to a view:
- Insert the control into the project. Name the wrapper class CCalendar.
- Add a CCalendar member variable named m_ctlCalendar to the view.
- Add the following statement to the view's OnCreate handler:
m_ctlCalendar.Create (NULL, WS_VISIBLE, CRect (0, 0, 400, 300), this, IDC_CALENDAR); |
When the view is created, the calendar control will be created in the view's upper left corner and assigned the control ID IDC_CALENDAR. Most of the work is done by CCalendar::Create, which calls the CreateControl function CCalendar inherits from CWnd. CWnd::CreateControl indirectly calls COleControlSite::CreateControl, which creates an ActiveX control and wires it up to its container.
So far, so good. But what if you want the view to process control events, too? This is where it gets tricky. ClassWizard will add event handlers to dialogs, but not to nondialogs. So you code the event sink map by hand. That wouldn't be too bad if it weren't for the fact that an event's parameter list has to be coded into the ON_EVENT statement in the form of VTS flags. Some programmers get around this by doing the following:
- Add a dummy dialog to the application.
- Insert the ActiveX control into the dialog.
- Use ClassWizard to write event handlers into the dialog.
- Copy the event sink map from the dialog to the view.
- Delete the dummy dialog.
I didn't say it was pretty. But it works. If you use this technique, don't forget to copy the DECLARE_EVENTSINK_MAP statement from the dialog's header file to the view's header file. DECLARE_EVENTSINK_MAP declares an event sink map just as DECLARE_MESSAGE_MAP declares a message map.
All this assumes, of course, that you checked AppWizard's ActiveX Controls box when you created the project. If you didn't, you can add container support to the application after the fact by adding an
AfxEnableControlContainer (); |
statement to InitInstance.
Using ActiveX Controls in Web Pages
One of the reasons ActiveX controls exist is to make Web content more interactive. An <OBJECT> tag in an HTML page denotes an ActiveX control. The control's methods and properties are accessible from within the HTML code, and events can be processed as well. The following HTML page displays this chapter's calendar control and responds to NewDay events by popping up a message box announcing which date was clicked:
<HTML> <BODY> <OBJECT CLASSID="CLSID:ED780D6B-CC9F-11D2-9282-00C04F8ECF0C" WIDTH=400 HEIGHT=300 ID="Calendar" > <PARAM NAME="BackColor" VALUE=12632256> </OBJECT> </BODY> <SCRIPT LANGUAGE=VBScript> Sub Calendar_NewDay(day) dt = Calendar.GetDate yr = DatePart ("yyyy", dt) mon = DatePart ("m", dt) MsgBox (CStr (mon) + "/" + CStr (day) + "/" + CStr (yr)) End Sub </SCRIPT> </HTML> |
You can try out these statements by typing them into an HTML file and opening the file with Internet Explorer. You'll have to modify the CLSID in the <OBJECT> tag if you create the control yourself because your control's CLSID will differ from mine. And remember that for the page to display properly, the control must be installed on your system. (In real life, the <OBJECT> tag would include a CODEBASE attribute and a URL telling Internet Explorer where to find the control if it's not already installed.) Notice the WIDTH and HEIGHT statements that specify the size of the control and the PARAM statement that sets the control's background color to light gray. The VBScript code in the SCRIPT block is called whenever a NewDay event is fired. It calls the control's GetDate method and displays the resultant date.