`
jgsj
  • 浏览: 960224 次
文章分类
社区版块
存档分类
最新评论

Designing ActiveX Components with the MFC Document/View Model

 
阅读更多

转自:http://blog.csdn.net/l12345678/article/details/1711521

W hether you're an experienced Win32® guru or a junior Windows® wanna-be, you've got to love MFC. It removes the drudgery from Windows development by encapsulating many of the menial parts of an application without sacrificing speed. In addition, it provides a very powerful, but often misunderstood, program model called the document/view model. The basic idea behind the document/view architecture is to separate data from its visual representation—the object-oriented programming model. A side benefit of its implementation in MFC is a multitude of built-in features, including toolbars, status bars, and File Save/Open.

The MFC framework provides three base classes that perform a lot of the redundant work of a typical application—CFrameWnd, CDocument, and CView. You create derived classes from each of those classes and override the appropriate virtual functions to tailor your application to your needs. To glue the three objects together, you create a CSingleDocTemplate or CMultiDocTemplate object and presto, MFC makes the thing fly. To make it even easier, AppWizard automatically generates the files containing the skeletons of your CFrameWnd, CDocument, and CView-derived classes.

Although the document/view approach is not appropriate for every problem, Figure 1 shows what it adds to your application with little or no effort.

Document/View, Meet ActiveX

You've probably been using MFC to write standalone apps for years. The real question is, how can you use the MFC document/view model to develop cool Web-based applications with active content? Not all of the features listed in Figure 1 are appropriate for an ActiveX™ control, but many of them are. If you want to do nothing more than separate an ActiveX control's data from its visual representation, using CDocument and CView-derived classes seems like a no-brainer. And if you could find a way to place a CFrameWnd-derived object inside an ActiveX control, you'd have quick and easy access to the added functionality provided by the CToolBar, CStatusBar, and CSplitterWnd classes.

The problem is that MFC does not provide a template class for jump-starting document/view inside an ActiveX control. In a standalone application, you simply create a CSingleDocTemplate or a CMultiDocTemplate, call it AddDocTemplate, and you're off to the races. But if you use these classes to create an ActiveX control, they create a parentless frame window with a menu bar and the WS_OVERLAPPED window style. Since you probably want to drop your ActiveX controls onto HTML pages and display them as child windows of your Web browser, neither of these template classes makes much sense.

To address this difficulty, I've developed two classes—CActiveXDocTemplate and CActiveXDocControl—that make it easy to take advantage of the document/view architecture inside an ActiveX control. In fact, I converted Scribble (the popular sample from the Visual C++® tutorials) into a document/view ActiveX control to show you how it works. You can download the full source code from the link at the top of this article.

Let's start with a brief review of how the document/view architecture works. If you're already a document/view hotshot, feel free to skip over the next section.

Document/View 101: The Basics

Since most of us at one time or another have worked through the scant but oh-so-informative MFC tutorials, let's use the trusty Scribble sample code as the basis for discussion. If you're new to MFC and want to follow along at home, you'll find step-by-step instructions for creating the Scribble sample in the Introducing Visual C++ manual that ships with Visual C++.

In the Scribble sample, the CScribbleApp::InitInstance method contains the following code:

CMultiDocTemplate* pDocTemplate; pDocTemplate = new CMultiDocTemplate( IDR_SCRIBBTYPE, RUNTIME_CLASS(CScribbleDoc), RUNTIME_CLASS(CChildFrame), RUNTIME_CLASS(CScribbleView)); AddDocTemplate(pDocTemplate); ••• if (!ProcessShellCommand(cmdInfo)) return FALSE;

The creation of a CMultiDocTemplate object basically serves two purposes:

  • It specifies the resource identifier of the menu, icon, accelerator table, and string resource associated with a document.
  • It defines the relationships among three classes: the document class, derived from CDocument, which is typically the information that the application will store on disk; the view class, derived from CView, which acts as the user interface to the data contained in the document; and the frame window class, derived from CFrameWnd or CMDIChildFrame, in which one or more views are placed.

The call to AddDocTemplate does not actually create instances of these three classes. All it does is associate the file extension specified in the IDR_SCRIBBTYPE string resource with the CScribbleDoc, CChildFrame, and CScribbleView classes. The real work happens later in the call to ProcessShellCommand. That function goes through some MFC gyrations and eventually ends up calling two virtual functions found in CDocTemplate (which is, of course, CMultiDocTemplate's base class). These are CreateNewDocument, which creates and initializes an instance of CScribbleDoc, and CreateNewFrame, which creates a CChildFrame window and places an instance of CScribbleView inside it.

The nice thing about the CScribbleApp code I just showed you is that AppWizard creates it all automatically. All you have to do is fill out the CScribbleDoc and CScribbleView classes and the handy-dandy Scribble app is done. It couldn't be any easier.

CActiveXDocTemplate and CActiveXDocControl

Now let's look at ActiveX controls. Even though there isn't a built-in document/view option in the OleControl Wizard to do the work for you, the CActiveXDocTemplate and CActiveXDocControl classes that I've whipped up make it easy to use the document/view architecture inside an ActiveX control. Here's how to do it:

1.Using the OleControl Wizard, create a new project workspace called MyOCX (or something like that). Use the default settings.

2.In the source files for your control, replace all references to COleControl with CActiveXDocControl so that your control class is derived from CActiveXDocControl, like this:

class CMyOCXCtrl : public CActiveXDocControl

  1. Remove the generic OnDraw method that the OleControl Wizard created for you. Since the entire client area of your control will contain a document/view frame window, this function isn't necessary.
  2. Add the ActivDoc.cpp file to the project and include ActivDoc.h in the control's CPP file:

#include "stdafx.h" #include "ActiveDoc.h" #include"MyOCX.h" #include "MyOCXCtrl.h" #include "MyOCXPpg.h"

  1. In the constructor for your class, create a CActiveXDocTemplate object and register it by calling AddDocTemplate, similar to this:

AddDocTemplate(new CActiveXDocTemplate( RUNTIME_CLASS(CMyDoc), RUNTIME_CLASS(CFrameWnd), RUNTIME_CLASS(CMyView)));

As shown below, a new CActiveXDocTemplate object requires three parameters: a CDocument-derived class, a CFrameWnd-derived class, and a CView-derived class. The constructor passes this information to the base class, CSingleDocTemplate, along with a dummy resource identifier. You don't pass in a resource identifier because ActiveX controls don't have a menu bar.

CActiveXDocTemplate::CActiveXDocTemplate(CRuntimeClass* pDocClass, CRuntimeClass* pFrameClass, CRuntimeClass* pViewClass) : CSingleDocTemplate(IDR_NOTUSED, pDocClass, pFrameClass, pViewClass) { ASSERT(pFrameClass); }

You must pass in a CFrameWnd-derived class so that your ActiveX control knows how to create the frame window that will be placed inside it. If you don't need to customize the behavior of the frame (if you have no toolbar, for example), you can use CFrameWnd directly without deriving your own class. You may pass in a NULL pointer for either the CDocument or CView-derived classes if your control doesn't use one of those objects. In a simple control, for example, you might dispense with the CDocument class and simply let the view keep track of the data. Conversely, you might let the document draw directly onto the CFrameWnd's client area without creating a view. In most cases you'll create your own document and view classes. Refer to the CSingleDocTemplate documentation for more information.

That's all there is to it. You don't even need to delete the CActiveXDocTemplate object because it's deleted automatically by the base class for your control, CActiveXDocControl.

A Look Under the Hood

How does it all work? Well, let's take a peek. When you place your control inside an OLE control container, such as Microsoft's Internet Explorer, the framework calls the CActiveXDocControl::OnCreate method, which contains the following code:

ASSERT(m_pDocTemplate); // Set in call to AddDocTemplate m_pFrameWnd = m_pDocTemplate->CreateDocViewFrame(this); ASSERT_KINDOF(CFrameWnd, m_pFrameWnd);

The call to CActiveXDocTemplate::CreateDocViewFrame kick-starts the document, frame, and view classes in much the same way that CWinApp::ProcessShellCommand does, except that CreateDocViewFrame always calls OpenDocumentFile with a NULL pointer. The reason for this is that an ActiveX control is just a DLL on steroids, so you can't pass it a file name as a command-line argument as you would with a regular application.

Thus, the control is initially created with an empty document. But since you obviously can pass information into an ActiveX control using one of its properties or methods, it's easy to pass in the file name of a document to load right after the control is created. As I'll show you in my Scribble example, you can add a property called something like DocumentName to the control and open that document whenever the property changes.

If you take a look at the header file for CDocTemplate (found in msdev/mfc/src/include/afxwin.h), you'll see that it contains quite a few virtual methods. I've listed a few of them to give you a flavor of what I'm talking about:

virtual CDocument* CreateNewDocument(); virtual CFrameWnd* CreateNewFrame(CDocument* pDoc, CFrameWnd* pOther); virtual void InitialUpdateFrame(CFrameWnd* pFrame, CDocument* pDoc, BOOL bMakeVisible = TRUE); virtual BOOL SaveAllModified(); // for all documents virtual void CloseAllDocuments(BOOL bEndSession); virtual CDocument* OpenDocumentFile(LPCTSTR lpszPathName, BOOL bMakeVisible = TRUE) = 0; virtual void SetDefaultTitle(CDocument* pDocument) = 0;

By making these functions virtual, the classes that inherit from CDocTemplate—namely CMultiDocTemplate and CSingleDocTemplate—can have significantly different functionality without changing the way the framework deals with them. What's even better is that you can keep all the code in CSingleDocTemplate that applies to an ActiveX control and only rewrite the functions that don't apply. As it turns out, there are only two functions that you need to override, as shown in Figure 2.

There's one more thing to mention before I unveil my super-duper Scribble control. In the traditional document/view model, the user opens and saves documents using the File Open and File Save menu commands in conjunction with the Open and Save As common dialogs. As I've already mentioned, it makes more sense to use an ActiveX control property to tell the control which document, if any, to open. To do this, you need a SaveDocumentFile function that does the same work as the ID_FILE_SAVE and ID_FILE_SAVE_AS menu handlers in a normal application.

void CActiveXDocTemplate::SaveDocumentFile() { if (m_pOnlyDoc != NULL) { if (!m_docFile.IsEmpty()) m_pOnlyDoc-> OnSaveDocument(m_docFile); else m_pOnlyDoc-> SetModifiedFlag(FALSE); } }

Note that SaveDocumentFile tries to save the document to disk only if the document was given a name in the call to OpenDocumentFile. Rather than prompting the user before saving the document as you do in a full-blown application, the CActiveXDocControl automatically calls SaveDocumentFile at the beginning of OpenDocumentFile, and then again right before the control is deleted:

void CActiveXDocControl::OnDestroy() { AfxGetApp()->m_pMainWnd = NULL; m_pDocTemplate->SaveDocumentFile(); COleControl::OnDestroy(); }

While this doesn't give your users the ability to discard any changes they've made to the current document, it seems like an appropriate choice. Users dabbling with the control while browsing your Web page won't want to have to confirm saving the control's document before being able to move to the next page!

Active Scribble

Now that I've given you all of the details, let's move on to the good stuff. As a proof of concept, I decided to convert Scribble into a document/view ActiveX control. I used this example for two reasons. First, it's a familiar sample, so you'll be able to see how it integrates with the CActiveXDocTemplate and CActiveXDocControl classes without getting bogged down in the implementation details. Second, it's a good demonstration of the many great features you get for little effort when you use the document/view architecture. As you can see in Figure 3, the control looks pretty awesome!

Figure 3: The Scribble ActiveX control

The code is straightforward, but I added a few things that are worth mentioning. As promised, I added a DocumentName property to the control using ClassWizard and tied it to a CString data member called m_documentName. This lets the control's container (the HTML for the Web page in this case) specify a name for the Scribble document. Whenever the DocumentName property changes, my OnDocumentNameChanged function is called:

void ScribbleCtrl::OnDocumentNameChanged() { GetDocTemplate()->OpenDocumentFile(m_documentName); SetModifiedFlag(); }

I enhanced the toolbar so that it functions like an office whiteboard. I made the buttons for the four marker colors act like a mutually-exclusive group of radio buttons by overriding the Create method of the toolbar:

BOOL CScribbleBar::Create(CWnd* pParentWnd) { if (!CToolBar::Create(pParentWnd) || !LoadToolBar(IDR_MAINFRAME)) return FALSE; SetBarStyle(GetBarStyle() | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_FIXED); SetButtonStyle(0, TBBS_CHECKGROUP); SetButtonStyle(1, TBBS_CHECKGROUP); SetButtonStyle(2, TBBS_CHECKGROUP); SetButtonStyle(3, TBBS_CHECKGROUP); GetToolBarCtrl().CheckButton(IDB_BLACK_MARKER); return TRUE; }

I wanted to make it easy to clear the whiteboard, so I added an eraser button to the toolbar and some message handlers to the CScribbleDoc class; when you click the eraser, it clears the current document and then disables the button until you've added more scribble strokes.

void CScribbleDoc::OnUpdateEraser(CCmdUI* pCmdUI) { pCmdUI->Enable(!m_strokeList.IsEmpty()); } void CScribbleDoc::OnEraser() { OnNewDocument(); UpdateAllViews(NULL); }

I ran into a bit of a snag when I noticed that the eraser button stayed enabled even when the document was empty. I stepped into the code with the debugger and determined that my OnUpdateEraser function was never getting called by the framework. After a little digging around, I realized that the Scribble control wasn't being sent any ON_COMMAND_ UPDATE_UI messages. As it turns out, that's because the OLE control container, not my control, owns the message loop, so no MFC idle-processing is ever done. Therefore, the following code in CWinThread::OnIdle was never called:

// send WM_IDLEUPDATECMDUI to the main window CWnd* pMainWnd = m_pMainWnd; if (pMainWnd != NULL && pMainWnd->m_hWnd != NULL && pMainWnd->IsWindowVisible()) { AfxCallWndProc(pMainWnd, pMainWnd->m_hWnd, WM_IDLEUPDATECMDUI, (WPARAM)TRUE, 0); pMainWnd->SendMessageToDescendants(WM_IDLEUPDATECMDUI, (WPARAM)TRUE, 0, TRUE, TRUE); }

My control wasn't receiving the WM_IDLEUPDATECMDUI message because it wasn't being sent.

Incidentally, the potential for this problem exists in any MFC code that is linked as a DLL because there's no guarantee that its calling process will send it any update messages. I worked around this problem by setting a timer in CActiveXDocControl::OnCreate and pumping the WM_IDLEUPDATECMDUI messages in the timer handler myself:

void CActiveXDocControl::OnTimer(UINT nIDEvent) { // Since we're in an OCX, we don't control the message loop, // so CWinThread::OnIdle is never called. That means we have // to periodically pump the ON_UPDATE_COMMAND_UI messages // by hand. SendMessageToDescendants(WM_IDLEUPDATECMDUI, TRUE); COleControl::OnTimer(nIDEvent); }

After solving that problem, I noticed that my toolbar wasn't receiving ToolTip messages, even though I had added the CBRS_TOOLTIPS and CBRS_FLYBY flags to its style bits. This symptom is part of a similar illness—since the ActiveX control doesn't own the message loop, CWnd::PreTranslateMessage is never called and the ToolTip messages don't get filtered properly. I'm not sure whether this is the most elegant solution, but I got around it by overriding the CToolBar::WindowProc function:

LRESULT CScribbleBar::WindowProc(UINT message, WPARAM wParam, LPARAM lParam) { MSG msg; msg.hwnd = m_hWnd; msg.message = message; msg.wParam = wParam; msg.lParam = lParam; GetCursorPos(&msg.pt); FilterToolTipMessage(&msg); return CToolBar::WindowProc(message,wParam,lParam); }

Looking Ahead

While the Scribble control is an eye-catching sample, I'm the first to admit that it doesn't serve any practical purpose. You can drop it onto a Web page, of course, and use it to store virtual marker strokes on a user's local hard drive. But that gets old in a hurry. In a follow-up article, I'll build on the Scribble example and show you how to layer the WinInet routines on top of the built-in document serialization routines you get with document/view. Then, instead of storing the scribble strokes on the local hard drive, the ActiveX control will send them to a Web server. I'll also write an ISAPI server DLL that broadcasts the strokes to all other running instances of the Scribble control so you end up with a World Wide Web virtual whiteboard. Now that has some practical implications, so don't miss it.

Conclusion

As you can see, there are definite advantages to utilizing MFC's document/view architecture inside an ActiveX control. Of course, there's a certain amount of overhead associated with the MFC runtime DLLs that you need to think about when you deploy your ActiveX controls on a Web site. For controls with a sophisticated user interface like the Scribble control, it's probably not a big price to pay. After all, even though the MFC DLLs will probably take several minutes to download at 14.4Kbps, they only have to be downloaded once. And since you can link to the DLLs instead of swallowing all that code whole, your ActiveX controls will stay relatively lean.

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics