UML软件工程组织

在MFC中集成RAD .NET框架
转自:www.microsoft.com 作者:孙辉
 

MFC已经有十几年的历史了,然而直到今天,他仍然是Visual C++的关键组成部分。从1996年的Visual C++ 4.2至今将近8年的时间,MFC的主体特征没有出现明显的变化,依旧是“古老”的面孔,因此关于这个类库的种种评论自然是情理之中的事情了。从我个人的观点上看,MFC功能依旧健壮、强大,而且是业界少有的、稳定的、经过足够长历史考验的开发框架。深入研究这个类库,你会找到酒越酿越醇的感觉。MFC的一个成功因素之一就是提供了一套完整的Document/View框架,然而这一点也是许多针对MFC批评的矛头指向。也许是由于这个框架太经典,使人们看上去MFC不再“婷婷玉立”,而是“人老珠黄”,以至于打开今天的Visual Studio IDE的时候,多少会有点不协调的感觉:比起其它基于.NET框架的开发语言,用MFC开发会显得很“土气”、“孤独”——没有RAD机制,明显的缺乏与其它时髦对象的“互操作”能力,而且严格恪守自己的领地。每当生成一个基于文档的MFC程序,我们总是看到一张沧桑的面孔,好像刘姥姥进入大观园,与周围时髦的C#、VB.NET等存在明显的“代沟”与“不相容”。曾经有很多人问我MFC还有前途吗?是否已经行将就木?关于是MFC还是.NET的讨论时隐时现,不绝于耳。CLR是个充满魅力的世界,这种魅力,使得C#、VB.NET等变得光彩夺目。然而,MFC并没有衰老,如果你深入的了解MFC,你会发现,MFC完全可以与C#、VB.NET争奇斗艳……

本页内容
在MFC项目中使用托管扩展 在MFC项目中使用托管扩展
宿主.NET控件 宿主.NET控件
经典优雅与现代风流的完美结合 经典优雅与现代风流的完美结合

在MFC项目中使用托管扩展

支持托管扩展

.NET FrameWork提供的托管扩展支持确保了在MFC项目支持托管扩展(CLR),开发者可以使MFC工程(本文我们将使用Test作为工程名称)通过打开项目的托管扩展属性开关,来增加编译器的托管支持(如图1)。

art/radnet_001.jpg

(图1:打开托管编译支持开关)

对偶.NET对象

在打开托管扩展编译开关以后,您就可以在MFC项目中使用托管对象了,通常的做法是:为每个重要的MFC对象匹配一个托管对象以形成一个对偶对,彼此匹配的对象包含指向对方的指针,这样,其他.NET对象可以通过对偶对中的.NET对象操作MFC对象;而其他MFC对象可以通过对偶对中的MFC对象操作.NET对象(如图2)。

art/radnet_002.jpg

(图2:对偶托管对象)

在Visual Studio .NET 中,没有提供关于添加托管C++类对象的向导,因此,你可以先添加一个基于托管C++的Component对象(如图3)。

art/radnet_003.jpg

(图3. Add Class向导:增加托管C++ Component对象)

添加了该testDocObject托管组件对象之后,将该对象的基类改为Object,并删除一些代码得到一个最小托管类:

namespace test
{
    __gc public class testDocObject
        : public Object
    {
    public:
        testDocObject(void)
        {
        }
    };
}

经过以上步骤,Visual Studio.NET生成的代码被装进了MFC程序,当然完全可以手动创建.h文件和.cpp文件,输入相应的代码,然后把它们添加到当前工程。由于以上步骤在托管扩展编程中经常遇到,因此,将上述过程自动化是必要的,有鉴于此,我们在附赠的光盘中提供了完整的添加.NET对象的Wizard。

在MFC非托管类中定义托管成员变量

在MFC类中使用托管对象,提供对象的声明和初始化方法与传统的方法略有不同。以在文档类CtestDoc中添加一个托管成员变量为例,声明托管对象的代码如下:

public:
       gcroot<test::testDocObject*> m_ptestDocObj;

gcroot类型安全包装模板可以将托管参考类型指针作为成员变量嵌入到非托管类中,该变量就可以像其他类型的变量一样使用了。在CtestDoc的成员函数InitialDocument中创建这个对象,代码如下:

BOOL CtestDoc::InitialDocument()
{
    #pragma push_macro("new")
    #undef new
        m_ptestDocObj = new test::testDocObject();
    #pragma pop_macro("new")
}

由于testDocObject是一个托管参考类型,它总被分配在CLR堆上,所以自然不能使用在afx.h中定义的new操作符来直接初始化该对象以避免该托管对象在非托管的本地C++堆上创建导致的错误。在托管对象中声明MFC对象,与常规方法一致。

掌握了上述的基本托管类和对象在传统MFC项目中的对偶使用方法,就可以保证您充分使用.NET框架所提供的丰富的类库支持(引用相关的动态链接库并声明名称空间是必要的)。如果希望更多地了解托管和非托管C++代码混用的技术,可以参考Tom Archer与Nishant Sivakumar合著,由Addison Wesley出版社出版的《Extending MFC Applications with the .NET Framework》一书,相信会很有帮助。

宿主.NET控件

宿主.NET控件的理论基础

在.NET Framework的世界里,功能丰富的.NET控件无疑是光彩夺目的明珠,MFC程序自然想联姻这些华丽的事物。由于MFC框架不提供对.NET控件的直接支持,从而导致MFC后天的失落(缺乏类似C#、VB.NET特有的可视化设计机制以及自由的控件组织功能),这一点多少成为MFC远离.NET世界的一种合理的客观原因。但是,我们注意到:.NET控件本质上就是ActiveX控件,二者之间的重要区别是注册方式不同——ActiveX控件是全局的,在系统注册表中注册;而.NET控件既可以在全局AssemblyCache中注册,也可以放在局部的目录中,相应的,在程序中获取它们相关信息的方式是不同的。但是,一旦.NET控件的基本信息被我们“捕获”,我们就可以使用与创建ActiveX控件一致的方法将.NET控件创建到MFC项目中。

art\radnet_004.jpg

(图4:MFC框架中ActiveX控件的创建)

我们知道,MFC是通过COleControlSite类创建ActiveX控件的,由于针对用于ActiveX控件的COleControlSite类不适用于.NET控件,因此必须重新派生一个新类CWFControlSite来提供必要的支持,通过一个CWFControlWrapper类将一个.NET控件包装在一个CWnd窗体中,并将包装后的窗体“安置”在CWFControlSite内。CWFControlWrapper类代码如下:

class CWFControlWrapper : public CWnd
{
public:
    CWFControlWrapper();
    virtual ~CWFControlWrapper(void);
    IUnknown *pUnkControl;
    IUnknown *GetManagedControl()
    {
        return pUnkControl;
    }
    void SetControlSite(COleControlSite *pSite)
    {
        m_pCtrlSite = pSite;
    }
};

下一步,要设计一个通用的CUserCtrlView类(从CView类派生),使得在CWFControlSite中指定的.NET控件可以像在COleControlSite中指定的ActiveX控件一样显示给用户。正象每个ActiveX控件必需用一个CWnd对象进行创建一样,一个支持.NET控件的CView类需要一个对应的CWnd对象,CWFControlWrapper就是针对这个目的设计的,通过CWFControlWrapper对象,MFC程序可以得到.NET对象对应的IUnknow、IDispatch。稍后我们介绍CUserCtrlView类的具体设计和使用方法。

art\radnet_005.jpg

(图5:MFC框架中.NET控件的创建)

NET控件的消息处理

一般而言,控件的对话框消息处理是一个极为关键的问题,在网上能找到的MFC中宿主控件的解决方法中,均没有实现.NET控件的对话框消息处理,一个明显的特征是不能处理“Tab”键消息。为此,我们重载了CUserCtrlView的PreTranslateMessage函数:

BOOL CUserCtrlView::PreTranslateMessage(MSG *pMsg)
{
    BOOL bRet = FALSE;
    if(m_Control.pUnkControl != NULL)
    {
        CComQIPtr<IOleInPlaceActiveObject>
            spInPlace(m_Control.pUnkControl);
        if(spInPlace)
            bRet =(S_OK == spInPlace->
                TranslateAccelerator(pMsg)) ?
                    TRUE : FALSE;
    }
    if(CView::PreTranslateMessage(pMsg))
        return TRUE;
    CFrameWnd *pFrameWnd = GetTopLevelFrame();
    if(pFrameWnd != NULL
    && pFrameWnd->m_bHelpMode)
        return FALSE;
    // start with first parent frame
    pFrameWnd = GetParentFrame();
    while(pFrameWnd != NULL)
    {
        if(pFrameWnd->PreTranslateMessage(pMsg))
            return TRUE;
        pFrameWnd = pFrameWnd->GetParentFrame();
    }
    return bRet;
}

这样可以使得CUserCtrlView可以正确的处理.NET Control的对话框消息。

回归RAD世界

接下来我们看看如何在工程中插入一个.NET用户自定义控件。我们增加一个新的托管类testControl,代码如下:

#pragma once

...

namespace test
{
    public __gc class testControl :
        public System::Windows::Forms::UserControl
    {
    public:
        testControl(void)
        {
            InitializeComponent();
        }
    protected:
        void Dispose(Boolean disposing)
        {
            if(disposing && components)
                components->Dispose();
            __super::Dispose(disposing);
        }
    private:
        System::Windows::Forms::Label *label1;
        System::ComponentModel::Container
            *components;
        void InitializeComponent(void)
        {
            this->label1 = new
                System::Windows::Forms::Label();
            this->SuspendLayout();
            this->label1->Location =
                System::Drawing::Point(16, 24);
            this->label1->Name = S"label1";
            this->label1->Size =
                System::Drawing::Size(208, 16);
            this->label1->TabIndex = 0;
            this->label1->Text =
                S"Welcome to TZ MFC.NET!";
            this->Controls->Add(this->label1);
            this->Name = S"testControl";
            this->Size =
                System::Drawing::Size(240, 160);
            this->ResumeLayout(false);
        }
    };
}

注意,testControl类继承自UserControl类,用户控件是开发者创建的任何控件,您可以将多个.NET控件组织在一起,添加功能代码,然后把它作为一个更综合一些的控件来使用,使用每一个用户控件和使用其他的.NET标准控件的步骤都是没有区别的。在上面的代码中,我们自定义的用户控件仅包含了一个.NET Label控件。

到目前为止,我们已经可以在原生MFC项目中成功插入.NET控件。然而,因为上面的.NET控件的插入是纯手工方式的,不直观且很难驾驭,一个聪明的办法是实现一个集成在Visual Studio .NET IDE中的Wizard,以使得MFC工程中可以直接使用可视设计器,在随机光盘中,我们提供了相关的Wizard,安装后您就可以直接在MFC项目中插入并可视化设计.NET用户控件了。

通过集成的Wizard,传统的MFC可以与现代的.NET RAD机制完美的结合在一起,使得你既可以得到传统C++的优雅,又可以享有现代RAD机制的风韵,对资源的整合力度也极大地扩展了。

使用CUserCtrlView类创建、显示.NET控件

我们为每个MFC文档类增加一个与之对偶的托管对象类,从而得到了一对对偶对。这个与MFC文档对偶的托管对象维护一个托管对象字典,每一个需要在文档中创建的托管控件会根据一个别名添加到这个字典中备查。当文档对象被实例化的时候,其对偶的托管对象也将被实例化,而且有待创建的控件也会被实例化并插入到相关的字典中,同时该对偶托管对象被传递给MFC应用程序对象中的指针变量m_pManagedCnnObj,CUserCtrlView类在调用OnInitialUpdate时,会通过全局变量theApp得到m_pManagedCnnObj,m_pManagedCnnObj就是与MFC文档对偶的托管对象,然后用.NET机制根据别名检索从m_pManagedCnnObj得到所要创建的控件的实例,之后调用SetControl函数将该控件创建出来:

void CUserCtrlView::SetControl(
    System::Object *control
)
{
    m_Control.pUnkControl =
        reinterpret_cast<IUnknown*>
        (System::Runtime::InteropServices::
        Marshal::GetIUnknownForObject(
            control).ToPointer());
    CRect clientRect;
    GetClientRect(&clientRect);
    CLSID clsid =
        { 0, 0, 0, { 0, 0, 0, 0, 0, 0, 0, 0 } };
    m_Control.CreateControl(
        clsid, 0, WS_VISIBLE|WS_CHILD,
        clientRect, this, 0);
    m_Control.ModifyStyleEx(
        0, WS_EX_CONTROLPARENT);
}

CUserCtrlView的OnInitialUpdate代码如下:

void CUserCtrlView::OnInitialUpdate()
{
    if(!m_bCtrlCreated&&m_strWinCtrlID!=_T(""))
    {
        Type *t =
            theApp.m_pManagedCnnObj->GetType();
        MethodInfo *mi = t->GetMethod(S"Connect");
        Object *p[] = new Object*[2];
        try
        {
            Object *pObj = NULL;
            String *pString[] = new String*[1];
            pString[0] = m_strWinCtrlID;
            PropertyInfo *m_pPropertyInfo =
                t->GetProperty(S"PrjItem");
            pObj = m_pPropertyInfo->GetValue(
                theApp.m_pManagedCnnObj,pString);
            if(pObj)
            {
                p[0] = pObj;
                p[1] = pString[0];
                //?????o
                mi->Invoke(
                    theApp.m_pManagedCnnObj, p);
                SetControl(pObj);
                m_bCtrlCreated = true;
            }
        }
        catch (Exception *exp)
        {
            CString strInfo = exp->Message;
            AfxMessageBox(strInfo);
            return;
        }
    }
    CView::OnInitialUpdate();
}

现在,你可以在MFC程序中创建.NET控件了,我们的第一个宿主.NET控件的程序运行结果如图:

art/radnet_006.jpg

(图6:运行时.NET控件)

检索.NET用户控件

一个MFC应用中可能包含多个UserControl,这些控件一般以动态链接库的形式存在。由于.NET控件一般不在全局注册,因此,.NET程序需要一种组件检索机制,使得它们能够正确的发现运行时用到的组件。.NET框架支持为每一个应用程序提供一个XML格式的配置文件,配置文件的名称是:“程序名.exe.config”,例如,名为 test.exe 的应用程序的配置文件为:test.exe.config,配置文件相当于局部的注册表,该文件必须与可执行文件位于同一个目录中。开发者可以在这个文件中更改、保存程序的设置,设置程序集绑定策略和其他的应用程序配置信息。.NET框架提供了专门的类以帮助开发人员操作该文件。下面是一段典型的配置文件片段:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<runtime>
        <assemblyBinding
            xmlns=
                "urn:schemas-microsoft-com:asm.v1"
        >
            <probing privatePath=
                "bin;usercontrol;
component;doctemplate"
            />
        </assemblyBinding>
    </runtime>
</configuration>

<configuration>元素是CLR和.NET Framework 应用程序所使用的每个配置文件中的根元素。<runtime>包含有关程序集绑定和垃圾回收的信息。<assemblyBinding>包含有关程序集版本重定向和程序集位置的信息,<probing>元素使用privatePath来指定加载程序集时运行库搜索的子目录(比如这里指定bin、usercontrol等目录为搜索目录),如果希望了解有关配置文件的更多信息,请参考MSDN。

经典优雅与现代风流的完美结合

MFC曾经依靠其与Win32的良好的耦合性在Windows开发方面独领风骚。然而,时过境迁,在.NET大行其道尽显风流的今天,MFC的世界似乎已经感到风雨飘摇了。不容否认,十几年的积累,已经使得MFC的世界有了相当的厚度,因此,MFC过时之说还言之过早,Visual Studio 2005之后,Microsoft依然在后续的Visual Studio中给MFC留下了位置。.NET环境,给Windows程序开发带来了许多新鲜的感觉,例如多语言的交叉继承、一致的Form设计等等。曾几何时,这些新鲜事物看上去都是与MFC不相关的,现在你可以自豪地说,在MFC的世界里,你可以拥有所有这一切!你可以充分运用已有的MFC经验积累去驾驭今天的新鲜事物。当所有的新鲜感觉消失之后,你会发现,经典的总是最好的,只要这种经典的感觉可以进一步延续。如果你希望左手托起传统Windows开发世界而右手紧握托管的.NET世界,那么,我们向你推荐MFC,因为MFC可以整合经典的优雅与现代的风流。通过集成.NET机制,经典MFC设计哲学融入了新的活力,因此打开了一扇门,对传统的MFC开发人员来说孤独已经不存在,完全可以与C#、VB.NET等争奇斗艳了。然而,当MFC具备同C#、VB.NET等一样的RAD机制的时候,面对C#、VB.NET这些新贵们,MFC自然的传统优势就会充分的得以体现,这种优势是内蕴的,C#、VB.NET们无法效仿。

 

 

版权所有:UML软件工程组织