• 浅尝辄止——使用ActiveX装载WPF控件


    1 引言

    使用VC编写的容器类编辑器,很多都可以挂接ActiveX控件,因为基于COM的ActiveX控件不仅封装性不错,还可以显示一些不错的界面图元。

    但是随着技术不断的进步,已被抛弃的ActiveX早已无法满足现代客户对审美的新需求,所以我们需要在这条道路上不断的独辟蹊径,今天提到的使用ActiveX装载WPF控件就是其中一条思路。

    WPF(Windows Presentation Foundation)是微软推出的基于Windows Vista的用户界面框架,属于.NET Framework 3.0的一部分。它提供了统一的编程模型、语言和框架,真正做到了分离界面设计人员与开发人员的工作;同时它提供了全新的多媒体交互用户图形界面。与传统的VC界面库相比,WPF不需要额外的界面库,通过xml格式配置显示页面,通过C#实现内部业务逻辑,在图形化方面WPF基于DirectX引擎,支持GPU硬件加速,可以给用户完美的体验。关于WPF的优点我就不多说了,读者可以自行查资料补脑。

    虽然我对WPF技术也是一知半解(不过可以肯定的是与COM技术相比,WPF还是相当简单的),本文的主要目的是让ActiveX与WPF结合起来,让我们原始的VC工程重新复活起来,下面切入正题吧。

    2 托管编程

    有人可能用到过在C#调用C++代码或者COM对象,非常的简单,C#已经为用户做好的完善的封装的转换,直接导进来用就行,但在C++中使用C#还是比较苦难的,需要使用托管编程的方式。

    C++托管编程的语法与传统C++和C#都有不同,所以这里需要简单介绍一下如何创建并修改ActiveX控件的代码。

    2.1 启动托管编程

    启动托管编程就是告诉编译器,我的C++代码里有托管代码,你编译的时候注意点。

    在ActiveX工程的属性页面里,General页的“Common Language Runtime Support”,设置成“Common Language Runtime Support (/clr)”。

    除此之外,字符集请选择“Unicode”,我们发现从VS2013之后,多字符集已经默认不支持了,需要单独安装一个补丁才能支持,这可以理解为微软的一个信号,建议大家创建工程时最好使用unicode

    其他的差别我就不多说了。

    2.2 编程语法

    托管类:ref class CMyClass{...。“ref”表示后边声明的类是一个托管类,托管类被创建之后,内存是放在托管堆上的,也就是会自动释放(也可以手动释放delete p; p=nullptr;),不需要我们考虑如何删除。代码中并不是所有类都需要设置成托管类,原始的继承自CWnd等MFC类的子类最好还是保留原始结构。我们在托管类中可以使用一些C#提供的东西,引入C#C#的各类资源。

    托管对象创建:在非托管类中,通过 gcroot<CMyClass^> m_pMyClass 定义一个托管对象指针;在托管类中,不需要使用gcroot,可以直接只用 “Class^”。

    初始化托管对象:创建对象代码为 m_pMyClass = gcnew CMyClass();,只要需要创建托管对象指针,都需要使用 “gcnew”关键字。

    指针:指向托管对象的指针为 “^”,所有从C#一侧得到的对象都是指针,都需要使用 “^”指向它。

    数组:与C#之间传递数组或链表时,C++中只能使用“array^”,例如定义一个int数组 “array<int>^ m_arrInt;”,初始化代码 “m_arrInt = gcnew array<int>(500)”。

    2.3 引入类库

    C#提供了丰富的类库,使用前需要引入,在工程属性页最上面的 “Common Properties”,点击子节点 “References”,添加你想要的库吧。

    3 开始编程吧

    如果没有ActiveX基础或COM基础或WPF基础的童鞋,可以先去学习,有兴趣再来看。

    3.1 准备WPF工程

    首先需要编写一个WPF的工程,输出dll格式,我们假设这个WPF的工程中,有一个WPF界面类,比如叫WPFSample,那么将会有WPFSample.xaml和WPFSample.xaml.cs等文件,其中xaml是界面,cs是后台处理代码,具体怎么实现的我就不多说了。

    3.2 创建ActiveX工程

    创建ActiveX工程,输出ocx格式,比如这个ActiveX叫WPFShow,那么创建好后,就会有WPFShowCtrl文件,这个里边是控件的类CWPFShowCtrl。

    除CWPFShowCtrl外,我们还需要提供一个用于管理WPF对象的容器,我们称其为WPFContainer类。

    3.3 DesignTime(DT)

    为了在DesignTime下能够看到WPF控件,我们需要使用一个Wnd对象,我们称其为CMyWnd吧,先让WPF画在这个Wnd上,再从Wnd上获得图像画到CDC上。

    这部分是最难的,在编辑模式下,ActiveX对象并没有被创建出来,但有些时候我们却想要显示出来这个控件的样子,我的做法是这样的。

    (1)先装载WPF

    最好使用一个类去加载WPF。

    .h实现——示意代码

    using namespace "System","System::Windows::Interop","System::Reflection","WPF文件里的命名空间"

    class CMyWnd:pubilc CWnd(){

      WPFContainer m_WpfDT;  // 在Design Time下管理WPFContainer

    };

    ref class WPFContainer{  // 这个类需要在Ctrl类里通过gcnew创建出来

      HwndSource^ m_pHwndSource;  // WPF对象会显示在这上面

      HwndSourceParameters^ m_sourceParams;  // 用于初始化HwndSource

      Assembly^ m_pCtrlAssem;    // WPF对象的Assembly

      WPFSample^ m_WpfObj;    // WPF对象

      ...  // 还有一些属性,我们后续再说

    };

     .cpp实现——创建WPF对象,示意代码

    m_pCtrlAssem = Assembly::LoadFrom(strWpfFilePathName);  // 加载WPF的dll文件

    Type^ wpfType = m_pCtrlAssem->GetType(_T("WPFSample"));

    if (wpfType == nullptr){...}  // 错误处理

    m_ObjWpf = (WFSample^)Activator::CreateInstance(wpfType);  // 创建WPF对象

    (2)OnDraw画处理

    ActiveX的主类CWPFShowCtrl,在Design time状态下,只有OnDraw方法被调用,这个方法是“OnDraw(CDC* pdc, const CRect& rcBounds, const CRect& rcInvalid)”

    我们现在就是在这里将WPF画在CDC上,从而上用户能够看得到

    .cpp实现——CWPFShowCtrl::OnDraw,示意代码

    CDC memDC;

    CClientDC dc(NULL);

    CRect rcBoundsDP(rcBounds);

    CRect rcBitmap(0, 0, rcBoundsDP.Width(), rcBoundsDP.Height());

    if (!memDC.CreateCompatibleDC(&dc)){return;}  // 错误处理

    // 创建一个Bitmap

    bool bSizeChanged = (m_rectLast!=rcBitmap);  // 判断是否改变大小了,是否需要resize

    if (!m_Bitmap.GetSafeHandle() || bSizeChanged){

      if (m_Bitmap.GetSafeHandle()){m_Bitmap.DeleteObject();}

      m_Bitmap.CreateCompatibleBitmap(&dc, rcBitmap.Width(), rcBitmap.Height());

      m_rectLast = rcBitmap;

    }

    CBitmap *pOldBitmap = memDC.SelectObject(&m_Bitmap);

    CBrush brush(RGB(255, 255, 255));

    memDC.SetBkColor(0); SetTextColor(xx); FillRect(&rcBitmap, &brush);

    memDC.SaveDC();

    // 从WPF里获得CDC

    CMyWnd对象->DrawWPF(&memDC, rcBounds);  // 这个对象还会再调WPFContainer去画

    memDC.RestoreDC(nSaveDC);

    pdc->BitBlt(rcBoundsDP.left, rcBoundsDP.top, rcBoundsDP.Width(), rcBoundsDP.Height(), &memDC, 0, 0, SRCCOPY);

    memDC.SelectObject(pOldBitmap);

     .cpp实现——CWPFContainer::OnDraw,示意代码

    int w = rcBounds.Width(), h = rcBounds.Height();

    RenderTargetBimap^ bmpRen;  // usercontrol to bitmapencoder

    BitmapEncoder^ encoder;    // bitmapencoder

    MemoryStream^ stream;    // bitmapencoder to stream

    Bitmap^ bitmap;        // get bitmap(c#) from stream

    try{  // c# try catch

      m_WpfObj->Resize(w, h);  // 这个函数是要你们自己实现的,放大缩小功能是必要的,除非大小已经定死了

      // get render

      System::Windows::Controls::UserControl^ pUC = (System::Windows::Controls::UserControl^)m_WpfObj;

      pUC->Width = w; ->Height = h;

      pUC->UpdateLayout()  // let it redraw

      bmpRen = gcnew RenderTargetBimap(w, h, 0, 0, PixelFormats::Pbgra32);

      bmpRen->Render(pUC);

      // get C# bitmap

      encoder = gcnew BmpBitmapEncoder();

      stream = gcnew MemoryStream();

      encoder->Frames->Add(BitmapFrame::Create(bmpRen));

      encoder->Save(stream);

      bitmap = gcnew Bitmap(stream);

      // bitmap from C# to C++

      if (pDC){

        IntPtr bitmapPtr = bitmap->GetHbitmap();

        HBITMAP hBitmap = static_cast<HBITMAP>(bitmapPtr.ToPointer());

        // draw cdc

        pDC->DrawState(CPoint(0,0), CSize(w, h), hBitmap, DST_BITMAP|DSS_NORMAL);

        DleleteObject(hBitmap);

      }

    }

    catch (Exception^ ex){}  // 异常处理

    finally{  // 释放资源

      if (bmpRen !=nullptr) delete bmpRen;

      if (encoder != nullptr) delete encoder;

      if (stream != nullptr) delete stream;

      if (bitmap != nullptr) delete bitmap;

    }

    ok了,看看design time状态下是否可以显示WPF了。

    此时虽然能看到,但不能够编辑,ActiveX为编辑状态提供了PropertyPage(属性页)功能,可以设置一些信息,这一点我们也可以实现,请看后面的PropertySet一节。

    3.4 RunTime(RT)

    参考DesighTime的方法,运行模式下显示WPF对象已经非常简单了,此时我们不需要CMyWnd参与,而是在CWPFShowCtrl里直接控制一个WPFContainer。

     CWPFShowCtrl::OnCreate(LPCREATESTRUCT lpCreateStruct)方法

    示意代码

    m_WPFCtrl = gcnew WPFContainer(this);  // 传递this指针,因为自己就是wnd,让WPF画在自己身上

    m_WPFCtrl->LoadFile(strWpfFilePathName);  // 让WPF加载并创建出WPF对象,这个过程上文已经提过了

    示意代码,在WPFContainer里创建WPF对象

    System::Windows::Controls::UserControl^ uc = (System::Windows::Controls::UserControl^)m_WpfObj;

    m_sourceParams = gcnew HwndSourceParameters("WPFSample");  // 根据名称创建对象

    m_sourceParams->SetPosition(x, y);  // 设定位置和大小

    m_sourceParams->SetSize(w, h);

    m_sourceParams->ParentWindow = IntPtr(m_pParentWnd->GetSafeHwnd());  // m_pParentWnd就是外层的Wnd对象,DT和RT不是一个

    m_sourceParams->WindowsStyle = WS_VISIBLE|WS_CHILD;

    m_pHwndSource = gcnew HwndSource(*m_sourceParams);

    m_pHwndSource->SizeToContent = SizeToContent::WidthAndHeight;

    FrameworkElement^ fe = (FrameworkElement^)m_WpfObj;

    // 立即在wnd上显示出来

    m_pHwndSournce->RootVisual = fe;

    m_WpfObj->Resize(w, h);

    3.5 PropertySet

    上文我们分别在DT和RT下创建了WPF对象并显示出来,本节将提供如何显示出PropertyPage的方法。

    如果你就打算在ActiveX里自己画出来PropertyPage,用户配置完参数后,再通过接口传递给WPF,那么不需要看本节了,本节的工作是WPF控件里有PropertyPage(与WPF control本身一样的界面代码),想在ActiveX的PropertyPage里显示。

    这部分可能需要调整一下代码了,需要ActiveX和WPF相互配合。

    ActiveX需要在编译器之前就提供PropertyPage的数量,通过MFC的宏设定出来:

    BEGIN_PROPPAGEEIDS(CWPFShowCtrl, 2)

      PROPPAGEID(guid1)

      PROPPAGEID(guid2)

    END_PROPPAGEIDS

    但是,如果我们不想再修改了WPF后还要花时间改ActiveX,或者ActiveX根本就不知道加载的WPF有哪些属性框,怎么办。

    我的想法可能比较傻,就是先创建十个八个的假propertypage,然后然后WPF有多少就显示多少个,如果我们创建了十个PropertyPage容器,那么所有加载的WPF最多只能有十个属性页,可少。

    创建一个用于显示PropertyPage的子类

    .h,示意代码

    // 定义用于创建多个page对象的宏

    #define NEW_PROPPAGE_CLASS(n, caption)

      calss CWPFPropPage##n : public CPropertyPageBase {

        enum {IDD = IDD_PROPPAGE_WPFHOLD};

        DECLARE_DYNCREATE(CWPFPropPage##n)

        DECLARE_OLECREATE_EX(CWPFPropPage##n)

        public: CWPFPropPage##n():CPropertyPageBase(IDD, caption, n){;}}

    #define NEW_PROPPAGE_CLASS_CPP(n, name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8, ids)

      IMPLEMENT_DYNCREATE(CWPFPropPage##n, CPropertyPageBase)

      IMPLEMENT_OLECREATE_EX(CWPFPr4opPage##n, name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8)

      BOOL CWPFPropPage##n::CWPFPropPage#nn##Factory::UpdateRegistry(BOOL bRegister) {

        if (bRegister) return AfxOleRegisterPropertyPageClass(AfxGetInstanceHandle(), m_clsid, ids);

        else     return AfxOleUnregisterClass(m_clsid, NULL); }

     

    // 创建propertypage子类代码

    class CPropertyPageBase : public COlePropertyPage

    {

      UINT m_nPropID;

      ...

    };

     

    // 定义多个page页

    NEW_PROPPAGE_CLASS(0, IDS_WPFCTRL_PPG_CAPTION0);

    NEW_PROPPAGE_CLASS(1, IDS_WPFCTRL_PPG_CAPTION1);

    NEW_PROPPAGE_CLASS(2, IDS_WPFCTRL_PPG_CAPTION2);

    ...

     .cpp示意代码

    // 创建多个page页

    NEW_PROPPAGE_CLASS(0, "WPFCtrl.WPFCtrlPropPage0", 0xb3d4a12c, 0x0251, 0x4301, 0xa1, 0xb5, 0x33, 0x32, 0x12, 0x44, 0x4e, 0xc1, IDS_WPFCTRL_PPG0);

    ... // 太多了就不打了,guid是通过生成器生成好的

    下面列几个关键函数

    CPropertyPageBase::DoDataExchange(CDataExchange* pDX){

      if (pDX->m_bSaveAndValidate) OnApplyNow();  // 当用户点击 "Apply"按钮时,这里要触发保存函数,告诉WPF控件,保存一下你的所有属性

      DDP_PostProcessing(pDX);

    }

    CPropertyPageBase::OnPropertyModify(WPARAM, LPARAM){SetModifyFlag(TRUE); return S_OK;}  // 数据改变后通知上层使能Apply按钮

    CPropertyPageBase::OnSetPageSite(){

      SetpageName(g_WpfObj->GetPageName(m_nPropID));  // 每个页面有一个名字(显示在标签页上),这个名字需要从WPF控件里得到

    }

    CPropertyPageBase::OnInitDialog(){

      // 参考之前RT和DT代码,将WPF控件中的Page页面显示在CPropertyPageBase上

    }

    CPropertyPageBase::OnDestroy(){

      COlePropertyPage::OnDestroy();

      // 释放WPF相应的资源

    }

      

    在WPF的代码里,除了需要创建主页外,还需要创建对应的PropertyPage页面,每个Page页面都需要知道自己的ID和Name.

    4 总结

    文本介绍如何通过ActiveX显示WPF控件的方法,其中最关键的技术主要是:DT下如果得到WPF的bitmap然后draw到ActiveX里;RT下将WPF控件创建出来并贴到ActiveX上。

    结尾又介绍了关于在ActiveX控件里显示出来WPF的属性页,这个工作主要针对那些想做通用ActiveX的朋友,如果你想做一个通用的用于显示WPF的ActiveX控件,也就是在加载WPF之前根本就不知道具体的WPF控件都有说明时,可以试试这个方法,前提是需要WPF配合你提供相应的页面,能够通知ActiveX有多个page,每个page的name是什么等等。

    了解了托管编程的语法后,剩下的在ActiveX里调用WPF的类,属性,方法等,则可以通过反射,或提供一个接口类,让WPF去继承,等等很多种方式,我就不再多说了。

    完。

  • 相关阅读:
    如何使用Remoting实现双工(转自Artech)
    解决自定义代码启动Approver Sharepoint 2010 Workflow,出现Failed on Start
    设计模式学习(目录)
    C#面向对象分析
    使用Sharepoint 2010 Client Object Model 通过SSL验证
    .NET 4.0 Location : 查看传感器状态变化
    OLAP和OLTP的 概念和区别
    组织结构及权限模型设计
    维度关系
    PHP正则替换中文让中文无处可躲
  • 原文地址:https://www.cnblogs.com/atlaser/p/6118670.html
Copyright © 2020-2023  润新知