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