ActiveX Automation is about writing code that other programs can call. Other programs call your code, not in the insulated manner of a DLL, but directly. The jargon is that your code exposes both methods (functions) and properties (variables) to other applications. The good part is that you don't have to create a macro language for your application; you only have to make hooks for a more universal macro language, Visual Basic for Applications, to grab onto.
If you've been building the sample applications throughout this book, you can probably design ShowString in your sleep by now, but it's time to do it once again. This time, ShowString is not going to have a Tools, Options menu-instead, other programs will directly set the string and other display options. The member variables in the document will be the same, and the code in OnDraw() will be the same as in all the other implementations of ShowString.
To build the version of ShowString that is an ActiveX Automation server, first use AppWizard to create an empty shell. Run AppWizard as usual, but in a different directory from your other versions of ShowString. Make almost exactly the same AppWizard choices as before: call it ShowString and then choose an MDI application and no database support. In AppWizard's Step 4, choose no support for OLE compound documents (the radio buttons at the top of the dialog box) but turn on support for OLE Automation. Continue through the AppWizard process, selecting a docking toolbar, status bar, printing and print preview, and 3-D controls. Finally, select source file comments and a shared DLL.
NOTE |
Even though the technology is now called ActiveX, the AppWizard dialogs still refer to OLE. Many of the class names that are used throughout this chapter have Ole in their names, and comments refer to OLE. While Microsoft has changed the name of the technology, it has not propagated that change throughout Visual C++ yet. You will have to live with these contradictions until the next release of Visual C++. |
There are just a few differences in this application from the do-nothing application without ActiveX Automation support, primarily in the application object and the document.
CShowStringApp The application object, CShowStringApp, has a number of changes. In the source file, just before InitInstance(), the code shown in Listing 25.1 has been added:
Listing 25.1 ShowString.cpp-CLSID
// This identifier was generated to be statistically unique for your app. // You may change it if you prefer to choose a specific identifier. // {4E28FA6A-E3C0-11CF-B5C2-0080C81A397C} static const CLSID clsid = { 0x4e28fa6a, 0xe3c0, 0x11cf, { 0xb5, 0xc2, 0y0, 0x80, 0xc8, 0x1a, 0x39, 0x7c } };
The numbers will be different in your code. This class ID identifies your ActiveX Automation application.
CShowStringApp::InitInstance() has several changes. The lines of code in Listing 25.2 initialize the ActiveX (OLE) libraries.
Listing 25.2 ShowString.cpp-Initializing Libraries
// Initialize OLE libraries if (!AfxOleInit()) { AfxMessageBox(IDP_OLE_INIT_FAILED); return FALSE; }
As with the server application, InitInstance() goes on to connect the document template to the COleTemplateServer, after the document template has been initialized:
m_server.ConnectTemplate(clsid, pDocTemplate, FALSE);
Then InitInstance() checks to see if the server is being launched to edit an embedded object or as an automation server; if so, there is no need to display the main window, so the function returns early, as shown in Listing 25.3.
Listing 25.3 ShowString.cpp-How the app Was Launched
// Check to see if launched as OLE server if (cmdInfo.m_bRunEmbedded || cmdInfo.m_bRunAutomated) { // Application was run with /Embedding or /Automation. Don't show the // main window in this case. return TRUE; } // When a server application is launched stand-alone, it is a good idea // to update the system registry in case it has been damaged. m_server.UpdateRegistry(OAT_DISPATCH_OBJECT); COleObjectFactory::UpdateRegistryAll();
If ShowString is being run as a stand-alone application, the code in Listing 25.3 updates the Registry as discussed in Chapter 24, "Building an ActiveX Server Application."
CShowStringDoc The document class, CShowStringDoc, still inherits from CDocument rather than from any OLE document class, but that's where the similarities to the old non-OLE CShowStringDoc end. The first block of new code in ShowStringDoc.cpp is right after the message map and is shown in Listing 25.4
Listing 25.4 ShowStringDoc.cpp-Dispatch Map
BEGIN_DISPATCH_MAP(CShowStringDoc, CDocument) //{{AFX_DISPATCH_MAP(CShowStringDoc) // NOTE - the ClassWizard will add and remove mapping macros here. // DO NOT EDIT what you see in these blocks of generated code! //}}AFX_DISPATCH_MAP END_DISPATCH_MAP()
This is an empty dispatch map. A dispatch map is like a message map, in that it maps events in the real world into function calls within this C++ class. When you expose methods and properties of this document with ClassWizard, the dispatch map will be updated.
After the dispatch map is another unique identifier, the IID (interface identifier).As Listing 25.5 shows, the IID is added as a static member, like the CLSID.
Listing 25.5 ShowStringDoc.cpp-IID
// Note: we add support for IID_IShowSt to support typesafe binding // from VBA. This IID must match the GUID that is attached to the // dispinterface in the .ODL file. // {4E28FA6C-E3C0-11CF-B5C2-0080C81A397C} static const IID IID_IShowSt = { 0x4e28fa6c, 0xe3c0, 0x11cf, { 0xb5, 0xc2, 0x0, 0x80, 0xc8, 0x1a, 0x39, 0x7c } };
Then the interface map looks like this:
BEGIN_INTERFACE_MAP(CShowStringDoc, CDocument) INTERFACE_PART(CShowStringDoc, IID_IShowSt, Dispatch) END_INTERFACE_MAP()
An interface map hides ActiveX functions like QueryInterface() from you, the programmer, and, like a message map, allows you to think at a more abstract level. ShowString will not have multiple entries in the interface map, but many applications do. Entries in the interface map are managed for you by ClassWizard.
The document constructor has some setting up to do. The AppWizard code is in Listing 25.6.
Listing 25.6 ShowStringDoc.cpp-Constructor
CShowStringDoc::CShowStringDoc() { // TODO: add one-time construction code here EnableAutomation(); AfxOleLockApp(); }
EnableAutomation() does just what its name suggests: it enables ActiveX Automation for this document. AfxOleLockApp() is used to ensure that an application is not closed while one of its documents is still in use elsewhere. Imagine that a user has two applications open that use ShowString objects. When the first application is closed, ShowString should not be closed because it is needed by the other application. ActiveX technology implements this by keeping a count, within the framework, of the number of active objects. AfxOleLockApp() increases this count. If it is nonzero when the user tries to close an application, the application is hidden but not actually closed.
It shouldn't be surprising, then, to see the destructor for ShowString's document:
CShowStringDoc::~CShowStringDoc() { AfxOleUnlockApp(); }
AfxOleUnlockApp() decreases the count of active objects so that eventually the application can be closed.
At this point, you have an ActiveX Automation server that does not expose any methods or properties. Also, the four member variables of the document that have been in all the previous versions of ShowString have not been added to this version. These member variables are the following:
These variables will be added as ActiveX Automation properties, so you will not type their names into the class definition for CShowStringDoc. Bring up ClassWizard by clicking on its toolbar button or choosing View, ClassWizard. Click on the OLE Automation tab, shown in Figure 25.1, to add properties and methods.
The first step in restoring the old ShowString functionality is to add member variables to the document class that will be exposed as properties of the ActiveX Automation server. There are two ways to expose properties: as a variable and with functions. Exposing a property as a variable is rather like declaring a public member variable of a C++ class: other applications can look at the value of the property and change it directly. A notification function within your server is called when the variable is changed from the outside. Exposing with Get and Set functions is like implementing a private member variable with public access functions. Other applications appear to access the variable directly, but the framework arranges for a call to your functions to Get and Set the property. Your Get may make sure that the object is in a valid state (for example, that a sorted list is currently sorted or that a total has been calculated) before returning the property value. Your Set function may do error checking (validation) or may calculate other variables that depend on the property that the outside application is changing. To make a property read-only, you add it as a Get/Set function property and then do not implement a Set function.
For the purposes of this chapter, you will add the two centering flags to the CShowStringDoc class with Get and Set functions, and the string and color properties as direct-access properties. To do so, follow these steps:
CAUTION |
Once you have clicked OK to add a property, you cannot change the type, external name or other properties of the property. You will have to delete it and then add one that has the new type or external name or whatever. Always look over the Add Property Dialog before clicking OK. |
Figure 25.5 shows the ClassWizard summary of exposed properties and methods. The details of each property are shown in the Implementation box below the list of properties. In Figure 25.5, VertCenter is highlighted, and the Implementation box reminds you that VertCenter has a Get function and a Set function, showing their declarations.
Figure 25.5 : ClassWizard provides a summary of the properties you have added.
It should come as no surprise that as a result of these additions, ClassWizard has made changes to the header and source files for CShowStringDoc. The new dispatch map in the header file is in Listing 25.7.
Listing 25.7 ShowStringDoc.cpp-Dispatch Map
/{{AFX_DISPATCH(CShowStringDoc) CString m_string; afx_msg void OnStringChanged(); short m_color; afx_msg void OnColorChanged(); afx_msg BOOL GetHorizCenter(); afx_msg void SetHorizCenter(BOOL bNewValue); afx_msg BOOL GetVertCenter(); afx_msg void SetVertCenter(BOOL bNewValue); //}}AFX_DISPATCH DECLARE_DISPATCH_MAP()
Two new member variables have been added: m_string and m_color.
NOTE |
It's natural to wonder if these are actually public member variables; they are not. Just above this dispatch map is this line: DECLARE_MESSAGE_MAP() And that macro, when it expands, declares a number of protected variables. Since these declarations are immediately afterward, they are protected member variables and protected functions. They are accessed in just the same way that protected message-catching functions are: they are called by a member function hidden in the class that directs traffic using these maps. |
A block of code has been added in the source file, but it's pretty boring, as you can see by looking at Listing 25.8.
Listing 25.8 ShowStringDoc.cpp-Notification, Get, and Set Functions
///////////////////////////////////////////////////////// // CShowStringDoc commands void CShowStringDoc::OnColorChanged() { // TODO: Add notification handler code } void CShowStringDoc::OnStringChanged() { // TODO: Add notification handler code } BOOL CShowStringDoc::GetHorizCenter() { // TODO: Add your property handler here return TRUE; } void CShowStringDoc::SetHorizCenter(BOOL bNewValue) { // TODO: Add your property handler here } BOOL CShowStringDoc::GetVertCenter() { // TODO: Add your property handler here return TRUE; } void CShowStringDoc::SetVertCenter(BOOL bNewValue) { // TODO: Add your property handler here }
The class still does not have member variables for the centering flags. Add them by hand to the header file, as private member variables:
// Attributes private: BOOL m_horizcenter; BOOL m_vertcenter;
Now you can write their Get and Set functions; Listing 25.9 shows the code.
Listing 25.9 ShowStringDoc.cpp-Get and Set Functions for the Centering Flags
BOOL CShowStringDoc::GetHorizCenter() { return m_horizcenter; } void CShowStringDoc::SetHorizCenter(BOOL bNewValue) { m_horizcenter = bNewValue; } BOOL CShowStringDoc::GetVertCenter() { return m_vertcenter; } void CShowStringDoc::SetVertCenter(BOOL bNewValue) { m_vertcenter = bNewValue; }
Restoring the member variables takes you halfway to the old functionality of ShowString. Changing the view's OnDraw() function will take you most of the rest of the way.
To write a version of OnDraw() that shows a string properly, open an old version of ShowString-either from your own work in Chapter 17, "Building Menus and Dialogs,"or from the CD that comes with this book-and then paste in the following bits ofcode. (If any of this code is unfamiliar to you, Chapter 17 explains it fully.) First, CShowStringDoc::OnNewDocument() (Listing 25.10) should initialize the member variables.
Listing 25.10 ShowStringDoc.cpp-Get and Set Functions for the Centering Flags
BOOL CShowStringDoc::OnNewDocument() { if (!CDocument::OnNewDocument()) return FALSE; m_string = "Hello, world!"; m_color = 0; //black m_horizcenter = TRUE; m_vertcenter = TRUE; return TRUE; }
Don't forget to change the member variable names; in this version of ShowString, you should use Microsoft's Hungarian notation, with m_ indicating that these are member variables, so as to be compatible with the code generated by ClassWizard. Next, copy in the document's Serialize function, shown in Listing 25.11.
Listing 25.11 ShowStringDoc.cpp- CShowStringDoc::Serialize()
void CShowStringDoc::Serialize(CArchive& ar) { if (ar.IsStoring()) { ar << m_string; ar << m_color; ar << m_horizcenter; ar << m_vertcenter; } else { ar >> m_string; ar >> m_color; ar >> m_horizcenter; ar >> m_vertcenter; } }
Finally, the view's OnDraw() function (Listing 25.12) actually shows the string.
Listing 25.12 ShowStringDoc.cpp- CShowStringView::OnDraw()
void CShowStringView::OnDraw(CDC* pDC) { CShowStringDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); COLORREF oldcolor; switch (pDoc->GetColor()) { case 0: oldcolor = pDC->SetTextColor(RGB(0,0,0)); //black break; case 1: oldcolor = pDC->SetTextColor(RGB(0xFF,0,0)); //red break; case 2: oldcolor = pDC->SetTextColor(RGB(0,0xFF,0)); //green break; } int DTflags = 0; if (pDoc->GetHorizcenter()) { DTflags |= DT_CENTER; } if (pDoc->GetVertcenter()) { DTflags |= (DT_VCENTER|DT_SINGLELINE); } CRect rect; GetClientRect(&rect); pDC->DrawText(pDoc->GetString(), &rect, DTflags); pDC->SetTextColor(oldcolor); }
In order for this code to work, you need to add public functions to the document class to get the private member variables m_string, m_color, m_horizcenter, and m_vertcenter. The best names are already taken and are protected functions. You could either make the view a friend to the document so that it can access the member variables directly, or you could settle for not-so-good names for the access functions. You can add the functions inline to the header file, pasting from the old ShowString.
public: CString GetDocString() {return m_string;} int GetDocColor() {return m_color;} BOOL GetHorizcenter() {return m_horizcenter;} BOOL GetVertcenter() {return m_vertcenter;}
In CShowStringView::OnDraw(), change the call to GetColor() to a call to GetDocColor(), and change the call to GetString() to a call to GetDocString(). Build the project to check for any typing mistakes or forgotten changes. While it may be tempting to run ShowString now, it's not going to do what you expect until you make a few more changes.
By default, ActiveX Automation servers do not have a main window. Remember the little snippet from CShowStringApp::InitInstance() in Listing 25.13.
Listing 25.13 ShowString.cpp- How the app Was Launched
// Check to see if launched as OLE server if (cmdInfo.m_bRunEmbedded || cmdInfo.m_bRunAutomated) { // Application was run with /Embedding or /Automation. Don't show the // main window in this case. return TRUE; }
This code returns before showing the main window. While you could remove this test so that ShowString always shows its window, it's more common to add a ShowWindow() method for the controller application to call. You'll also need to add a RefreshWindow() method that updates the view after a variable is changed; ClassWizard makes it simple to do this. Bring up ClassWizard, click the OLE Automation tab, and then click Add Method. Fill in the external name as ShowWindow(). ClassWizard fills in the internal name for you, and there's no need to change it. Choose void from the Return type drop-down list box. Figure 25.6 shows the dialog box after it's been filled in.
Figure 25.6 : ClassWizard makes it simple to add a ShowWindow() method.
Click OK on the dialog box, and ShowWindow() appears in the middle of the list of properties, which turns out to be a list of properties and methods in alphabetical order. The C next to the properties reminds you that these properties are custom properties (other types of properties are discussed in the "Displaying the Current Value" section of the next chapter, "Building an ActiveX Control"). The M next to the methods reminds you that these are methods. With ShowWindow() highlighted, click Edit Code and then type in the function, as shown inListing 25.14.
Listing 25.14 ShowStringDoc.cpp-CShowStringDoc::ShowWindow()
void CShowStringDoc::ShowWindow() { POSITION pos = GetFirstViewPosition(); CView* pView = GetNextView(pos); if (pView) { CFrameWnd* pFrameWnd = pView->GetParentFrame(); pFrameWnd->ActivateFrame(SW_SHOW); } }
This code activates the view and asks for it to be shown. Bring up ClassWizard again, click Add Method, and add RefreshWindow(), returning void. Click OK and then Edit Code. The code for RefreshWindow(), shown in Listing 25.15, is even simpler.
Listing 25.15 ShowStringDoc.cpp-CShowStringDoc::RefreshWindow()
void CShowStringDoc::RefreshWindow() { UpdateAllViews(NULL); SetModifiedFlag(); }
This arranges for the view (now that it's active) to be redrawn. And, because a change to the document is almost certainly the reason for the redraw, this is a handy place to put the call to SetModifiedFlag(), though if you prefer, you can put it in each Set function and the notification functions for the direct-access properties. You will add a call to RefreshWindow() to each of those functions now. For example, SetHorizCenter() is shown in Listing 25.16.
Listing 25.16 ShowStringDoc.cpp-CShowStringDoc::SetHorizCenter()
void CShowStringDoc::SetHorizCenter(BOOL bNewValue) { m_horizcenter = bNewValue; RefreshWindow(); } And OnColorChanged() looks like this: void CShowStringDoc::OnColorChanged() { RefreshWindow(); }
Add the same RefreshWindow() call to SetVertCenter() and OnStringChanged(). Now you are ready to build and test. Build the project and correct any typing errors. Run ShowString as a stand-alone application, both to register it and to test your drawing code. You cannot change the string, color, or centering as you could with older versions of ShowString, because this version does not implement the Tools, Options menu item and its dialog box. The controller application is going to do that for this version.
This chapter has mentioned a controller application several times, and you may have wondered where it's going to come from. You are going to put it together in DispTest, a watered-down version of Visual Basic that comes with Visual C++. It's not added to the Start menu, but you can run DISPTEST.EXE from the C:\MSDEV\BIN folder or from your Visual C++ CD-ROM's \MSDEV\BIN folder. Figure 25.7 shows the DispTest interface.
Figure 25.7 : At first glance, DispTest looks just like Visual Basic.
TIP |
If you have Visual Basic, you may choose to use it instead of DispTest. For testing OLE Automation servers, it doesn't matter which you choose. If you've written VBA macros in Excel and have a copy of Excel, you can use that, too. |
To build a controller application for the ShowString Automation server, start by running DispTest. In the window at the upper-right labeled Project1, click the View Code button. Choose Form from the right-hand drop-down list box in the new window that appears, and the Form_Load() subroutine is displayed. Enter the code in Listing 25.17 into that subroutine.
Listing 25.17 Form1.frm-DispTest code
Sub Form_Load () Set ShowTest = CreateObject("ShowString.Document") ShowTest.ShowWindow ShowTest.HorizCenter = False ShowTest.Color = 1 ShowTest.String = "Hello from DispTest" Set ShowTest = Nothing End Sub
Choose (general) from the left-hand drop-down list box and then enter this line of code:
Dim ShowTest As Object
For those of you who don't read Visual Basic, this code makes more sense if you go through it a line at a time. Choose Debug Single Step to execute the first line of code. Then repeatedly press f8 to move through the routine. (Wait after each press until the cursor is back to normal.) The line in the general code sets up an object called ShowTest. When the form is loaded (which is whenever you run this little program), an instance of the ShowString object is created. The next line calls the ShowWindow method to display the main window on the screen. Whenever the debugger pauses, the dashed box is around the line of code that will run next. You should see something like Figure 25.8 with the default ShowString behavior.
Figure 25.8 : The ShowWindow method displays the main ShowString window.
Press f8 again to run the line that turns off horizontal centering. Notice that you do not call the function SetHorizCenter. You exposed HorizCenter as a property of the OLE Automation server, and from Visual Basic, you access it as a property. The difference is that the C++ framework code calls SetHorizCenter to make the change, rather than just making the change and then calling a notification function to tell you that it was changed. After this line has executed, your screen will resemble Figure 25.9, because the SetHorizCenter method calls RefreshWindow() to immediately redraw the screen.
Figure 25.9 : The Visual Basic program has turned off centering.
As you continue through this program, pressing f8 to move a step at a time, the string will turn red and then change to Hello from DispTest. Notice that the change to these directly exposed properties looks no different than the change to the Get/Set method property, HorizCenter. When the program finishes, the window goes away. You have successfully controlled your ActiveX Automation server from DispTest.
Many programmers are intimidated by ActiveX, and the last thing they want is to know what's happening under the hood. There's nothing wrong with that attitude at all. It's quite object-oriented, really, to trust the already-written ActiveX framework to handle the black magic of translating ShowTest.HorizCenter = False into a call to CShowStringDoc::SetHorizCenter(). But if you want to know how that "magic" happens, or what to do if it doesn't, you need to add one more piece to the puzzle. You have already seen the dispatch map for ShowString, but you haven't seen the type library. It is not meant for humans to read, but it's for ActiveX and the Registry. It is generated for you as part of a normal build from your Object Definition Language (ODL) file. This file was generated by AppWizard and is maintained by ClassWizard. ShowString.odl looks like Listing 25.18, which follows.
Listing 25.18 ShowString.odl-ShowString Type Library
// ShowString.odl : type library source for ShowString.exe // This file will be processed by the Make Type Library (mktyplib) tool to // produce the type library (ShowString.tlb). [ uuid(4E28FA6B-E3C0-11CF-B5C2-0080C81A397C), version(1.0) ] library ShowString { importlib("stdole32.tlb"); // Primary dispatch interface for CShowStringDoc [ uuid(4E28FA6C-E3C0-11CF-B5C2-0080C81A397C) ] dispinterface IShowSt { properties: // NOTE - ClassWizard will maintain property information here. // Use extreme caution when editing this section. //{{AFX_ODL_PROP(CShowStringDoc) [id(1)] BSTR String; [id(2)] short Color; [id(3)] boolean HorizCenter; [id(4)] boolean VertCenter; //}}AFX_ODL_PROP methods: // NOTE - ClassWizard will maintain method information here. // Use extreme caution when editing this section. //{{AFX_ODL_METHOD(CShowStringDoc) [id(5)] void ShowWindow(); [id(6)] void RefreshWindow(); //}}AFX_ODL_METHOD }; // Class information for CShowStringDoc [ uuid(4E28FA6A-E3C0-11CF-B5C2-0080C81A397C) ] coclass Document { [default] dispinterface IShowSt; }; //{{AFX_APPEND_ODL}} };
This explains why Visual Basic just thought of all four properties as properties: that's how they are listed in this ODL file. The two methods are here, too, in the methods section. The reason you passed "ShowString.Document" to CreateObject() is that there is a coclass Document section here. It points to a dispatch interface (dispinterface) called IShowSt. Here's the interface map from ShowStringDoc.cpp:
BEGIN_INTERFACE_MAP(CShowStringDoc, CDocument) INTERFACE_PART(CShowStringDoc, IID_IShowSt, Dispatch) END_INTERFACE_MAP()
So a call to CreateObject("ShowString.Document") leads to the coclass section, which points to IShowSt. The interface map points from IShowSt to CShowStringDoc, which has a dispatch map that connects the properties and methods in the outside world to C++ code. You can see that editing any of these sections by hand could have disastrous results. Trust the wizards to do this for you.
In this chapter, you built an ActiveX Automation server and controlled it from DispTest. ActiveX Automation servers are far more powerful than older ways of application interaction, but your server doesn't have any user interaction. If the Visual Basic program wanted to allow the user to choose the color, that would have to be built into the Visual Basic program. The next logical step is to allow the little embedded object to react to user events like clicks and drags, and to report to the controller program what has happened. That's what ActiveX controls do, as you'll see in the next chapter. Some chapters you may want to read include: