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

时间:2021-11-18 20:13:07

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)”。

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

除此之外,字符集请选择“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”,添加你想要的库吧。

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

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去继承,等等很多种方式,我就不再多说了。

完。