MSCE C#官网一步步学习搬运9 第九章、用C++/CLI编写Addins

第九章、用C++/CLI编写Addins

上一章中简单演示了如何在Addins端封装调用NativeCode端导出的函数,虽然通过PInvoke可以调用到大部分NativeCode端导出的函数,但是C/C++中一些特殊的函数是无法通过PInvoke进行封装的,例如构造函数,本章演示的例子就属于这种情况。在第零章中已经提到过CE版本上Addins中有两套开发框架,在第二章中简单演示了分别在两套开发框架下如何创建元素。在实际开发过程中,我们有时候会遇到要将元素转换到另一套开发框架下的情况。这个时候您会发现,Addins中并没有接口去完成这样的功能。但是如果您仔细研究的话,会发现两套开发框架下的Element对象都留有接口能获取到NativeCode端与元素构造相关的对象,以及从NativeCode端元素相关的对象构造其对象实例。COM框架下的Element对象有MdlElementRef和MdlElementDescrP两个成员函数可以获取到NativeCode端元素的ElementRefP和MSElementDescrP指针。Bentley.Interop.MicroStationDGN.Application 下的MdlCreateElementFromElementDescrP成员函数可以从MSElementDescrP构造出COM框架下的Element。而新的开发框架下的Element有一个ElementHandle的属性能获取到NativeCode端ElementHandle的指针。其有一个保护类型的成员函数InitializeFromElementHandle可以从NativeCode端ElementHandle的指针构造出其对象实例。在NativeCode端,ElementHandle的几个重载构造函数中有接收ElementRefP或者MSElementDescrP为参数的构造函数,且ElementHandle也有获取ElementRefP和MSElementDescrP的成员函数。所以以NativeCode的对象ElementHandle为桥梁我们就可以完成两套编程框架下的元素相互转换。如果我们用PInvoke的技术来实现的话,就需要在NativeCode端导出这样两个转换函数,一个以ElementRefP或者MSElementDescrP指针为参数以ElementHandleP指针为返回值的转换函数实现从COM框架下的Element到新的编程框架下的Element的转换。另外一个是以ElementHandleP指针为参数,以MSElementDescrP指针为返回值的转换函数实现从新的编程框架下的Element到COM框架下的Element的转换。不管是哪个方向的转换,我们都需要在Addins端有一个接收返回的指针并将其最终转换为Element对象实例。如果在C++/CLI中我们可以在一个代码块里边同时使用托管和非托管的对象,所以不管是哪个方向的转换我们只需要一个函数就能实现。

在学习本章例子之前,需要安装好Mstn CE SDK。如何获取Mstn CE SDK请参考另一篇NativeCode的学习博客。接下来就让我们来一步一步实现这两个转换函数吧。

1、在D盘建立:D:\Files\BentleyMstn\MstnCE\Mixed\SampleMixed目录。

2、启动一个文本编辑器(当然可以启动VS2015用作编辑器),在其中键入如下内容并保存为文件D:\Files\BentleyMstn\MstnCE\Mixed\SampleMixed\SampleMixed.h。该文件中含有转换函数所在类的类型声明。以及一个继承与Bentley.DgnPlatformNET.Elements.Element的类ElementDerive。

#pragma once

#pragma managed

#using <Bentley.MicroStation.dll>
#using <Bentley.GeometryNET.Structs.dll>
#using <Bentley.DgnPlatformNET.dll>
#using <Bentley.Interop.MicroStationDGN.dll>

#include <Mstn\MdlApi\MdlApi.h>
#include <Mstn\ISessionMgr.h>
#include <Mstn\MdlApi\mselems.h>
#include <msclr/gcroot.h>
#include <Mstn\MdlApi\dloadlib.h>

USING_NAMESPACE_BENTLEY_DGNPLATFORM;
USING_NAMESPACE_BENTLEY_MSTNPLATFORM;
USING_NAMESPACE_BENTLEY_MSTNPLATFORM_ELEMENT;
using namespace msclr;
namespace GeoNet = Bentley::GeometryNET;
namespace DgnNetEle = Bentley::DgnPlatformNET::Elements;
namespace InteropMstn = Bentley::Interop::MicroStationDGN;

namespace SampleMixed
{
		private ref class ElementDerive :public DgnNetEle::Element
		{
		public:
			ElementDerive(ElementHandleP);
		};

		public ref class ElementOperation
		{
		public:
			static DgnNetEle::Element^  ConvertToDgnNetEle(InteropMstn::Element^ ele);
			
			static InteropMstn::Element^ ElementOperation::ConvertToInteropEle(DgnNetEle::Element^ ele);
		};
}

这里定义了一个ElementDerive类,是因为我们要借助于Bentley.DgnPlatformNET.Elements.Element的InitializeFromElementHandle函数来初始化构造Bentley.DgnPlatformNET.Elements.Element。因为InitializeFromElementHandle是保护类型的成员函数,不能从类的外边访问此成员函数,所以从Bentley.DgnPlatformNET.Elements.Element派生出ElementDerive类。在ElementDerive的成员函数中我们可以访问到基类的保护类型成员,所以可以通过InitializeFromElementHandle初始化基类Bentley.DgnPlatformNET.Elements.Element的成员。而ElementDerive是从Bentley.DgnPlatformNET.Elements.Element派生出来的,当然可以作为Bentley.DgnPlatformNET.Elements.Element来使用了。

3. 在文本编辑器中再另建一个新文件,在其中键入如下内容并保存为文件D:\Files\BentleyMstn\MstnCE\Mixed\SampleMixed\SampleMixed.cpp。该文件中包含有ElementDerive和两个转换函数的实现。

using namespace System::Reflection;

#include <Mstn\MdlApi\mselemen.fdf>
#include <vcclr.h>
#include "SampleMixed.h"


namespace SampleMixed
{

[assembly:AssemblyDelaySignAttribute(false)];

	ElementDerive::ElementDerive(ElementHandleP eehp)
	{
		InitializeFromElementHandle(*eehp);
	}

	DgnNetEle::Element^ ElementOperation::ConvertToDgnNetEle(InteropMstn::Element^ ele)
	{
		ElementDerive^ rtnEle = nullptr;
		if ((long)ele->MdlElementRef() != 0)
		{
			EditElementHandle eeh((ElementRefP)(void*)ele->MdlElementRef(), (DgnModelRefP)(void*)ele->ModelReference->MdlModelRefP());
			rtnEle = gcnew ElementDerive(&eeh);
			return rtnEle;
		}
		MSElementDescrP elmdscrP = (MSElementDescrP)(void*)ele->MdlElementDescrP(false);
		EditElementHandle eeh(elmdscrP, true, false, (DgnModelRefP)(void*)ele->ModelReference->MdlModelRefP());
		rtnEle = gcnew ElementDerive(&eeh);
		return rtnEle;
	}

	InteropMstn::Element^ ElementOperation::ConvertToInteropEle(DgnNetEle::Element^ ele)
	{
		cli::pin_ptr<unsigned char> pb = &(ele->ElementHandle[0]);
		ElementHandleP ehp = reinterpret_cast<ElementHandleP>(pb);
		InteropMstn::Application^ msApp = Bentley::MstnPlatformNET::InteropServices::Utilities::ComApp;
		MSElementDescrP elmdscrp = const_cast<MSElementDescrP>(ehp->GetElementDescrCP());
		long long* lp = reinterpret_cast<long long*>(&*elmdscrp);
		long long elmDscrp = *reinterpret_cast<long long*>(&lp);
		InteropMstn::Element^ rtnEle = msApp->MdlCreateElementFromElementDescrP(elmDscrp);
		return rtnEle;
	}
}

ConvertToDgnNetEle实现从COM框架下的Element到新的编程框架下的Element的转换。这个函数通过COM框架下Element的MdlElementRef或者MdlElementDescrP获取到ElementRefP或者MSElementDescrP指针,通过这两个指针可以初始化构造非托管类型EditElementHandle。ElementDerive的构造函数需要一个ElementHandle的指针作为参数,而EditElementHandle派生于ElementHandle。所以我们通过EditElementHandle就可以初始化构造ElementDerive对象了,最后返回ElementDerive对象实例即可。ConvertToInteropEle实现从新的编程框架下的Element到COM框架下的Element的转换。函数中通过新的编程框架下的Element的ElementHandle属性获取到ElementHandle的指针。因为.Net中的对象实例在垃圾回收过程中会被重定位,所以我们使用C++/CLI中的类型cli::pin_ptr告诉编译器,在此cli::pin_ptr的作用域中ele->ElementHandle在垃圾回收时不被重新定位。这样就可以保证我们的ElementHandle指针一直有效。然后我们通过ElementHandle获取到元素的MSElementDesrP指针,进而去调用Bentley.Interop.MicroStationDGN.Application 下的MdlCreateElementFromElementDescrP成员函数的到COM框架下的Element实例。

4. 在文本编辑器中再另建一个新文件,在其中键入如下内容并保存为文件D:\Files\BentleyMstn\MstnCE\Mixed\SampleMixed\SampleMixed.mke。该文件是SDK下的bmake工具用来编译源码来使用的,.mke文件的格式可以参考另一篇NativeCode的学习博客。

DemoSrcDir   = $(_MakeFilePath)
PolicyFile = MicroStationPolicy.mki
appName    = SampleMixed
ASSEMBLY_NAME       = $(appName)
RIGHTSCOMPLIANT     = false

MDLMKI = $(MSMDE)mki/

mdlLibs = $(MSMDE)library/

%include        mdl.mki

outputDir = $(MS)Mdlapps/

always:
    ~mkdir $(o)

objList = $(o)SampleMixed$(oext)

%include compileForCLRStart.mki

CCompDebugOptions = $(CCompDebugOffSwitch)

CCompDebugOptions =% $[CCompDebugDefault]

CCompOpts + -AI$(MS)Assemblies -AI$(MS)Assemblies/ECFramework -AI$(MS) -AI$(o)

dirToSearch = ${msPrivInc}

$(o)SampleMixed$(oext) : $(baseDir)SampleMixed.cpp

%include compileForCLRStop.mki

DLM_OBJECT_DEST         = $(o)
DLM_NAME                = $(appname)
RIGHTSCOMPLIANT         = true
DLM_DEST                = $(outputDir)
DLM_OBJECT_FILES        = $(objList)
DLM_NO_DEF              = 1
DLM_NO_DLS              = 1
DLM_NO_IMPLIB           = 1
DLM_NO_SIGN = 1

ASSEMBLY_VERSION        = 1.0.0.0
ASSEMBLY_TITLE          = $(appName)
ASSEMBLY_DESCRIPTION    = MixedMode Test Application
ASSEMBLY_PRODUCT_NAME   = $(appName)
ASSEMBLY_FILE_VERSION   = 1.0.0.0
ASSEMBLY_COMPANY_NAME   = Bentley Systems
ASSEMBLY_COPYRIGHT      = Copyright: (c) 2019 Bentley Systems, Incorporated. All rights reserved.

LINKER_LIBRARIES            + $(mdlLibs)BentleyAllocator.lib
LINKER_LIBRARIES            + $(mdlLibs)DgnPlatform.lib

%include linkMixedAssembly.mki

4. 右键选择“开始>Bentley>MicroStation CONNECT Edition SDK”,选择“More>Run as administrator”启动bmake工具。在命令提示符后键入cd /d D:\Files\BentleyMstn\MstnCE\Mixed\SampleMixed并回车进入我们的源码所在目录,然后再键入bmake –a来生成SampleMixed.dll。生成的文件位于…\MicroStation\mdlapps目录下。

MSCE C#官网一步步学习搬运9 第九章、用C++/CLI编写Addins

至此我们的两个转换函数已经封装好了,接下来我们在csAddins项目中去验证我们这两个函数。在csAddins项目中添加引用SmaleMixed.dll,具体添加方法参考第一章。打开commands.xml文件,新增命令两个命令csAddins CreateElement TestConvertToDgnNetEle和csAddins CreateElement TestConvertToInteropEle并指定其处理函数为csAddins.CreateElement. TestConvertToDgnNetEle和csAddins.CreateElement. TestConvertToInteropEle。如果您对XML格式的命令表文件还不熟悉,请参考第四章的相关主题。打开CreateElement.cs文件,CreateElement类的最后加入如下两个函数。

public static void TestConvertToDgnNetEle(string unparsed)
        {
            BIM.Application app = Bentley.MstnPlatformNET.InteropServices.Utilities.ComApp;
            BIM.Point3d ptStart = app.Point3dZero();
            BIM.Point3d ptEnd = ptStart;
            ptStart.X = 10;
            BIM.LineElement lineEle = app.CreateLineElement2(null, ref ptStart, ref ptEnd);
            Element ele = SampleMixed.ElementOperation.ConvertToDgnNetEle(lineEle);
            ElementPropertiesSetter elePropSetter = new ElementPropertiesSetter();
            elePropSetter.SetColor(1);
            elePropSetter.SetWeight(2);
            elePropSetter.Apply(ele);
            ele.AddToModel();
        }

        public static void TestConvertToInteropEle(string unparsed)
        {
            DgnModel dgnModel = Session.Instance.GetActiveDgnModel();
            ModelInfo modelInfo = dgnModel.GetModelInfo();
            DPoint3d[] ptArr = new DPoint3d[5];
            ptArr[0] = new DPoint3d(0 * UorPerMas, 10 * UorPerMas, 0 * UorPerMas);
            ptArr[1] = new DPoint3d(1 * UorPerMas, 12 * UorPerMas, 0 * UorPerMas);
            ptArr[2] = new DPoint3d(3 * UorPerMas, 8 * UorPerMas, 0 * UorPerMas);
            ptArr[3] = new DPoint3d(5 * UorPerMas, 12 * UorPerMas, 0 * UorPerMas);
            ptArr[4] = new DPoint3d(6 * UorPerMas, 10 * UorPerMas, 0 * UorPerMas);
            CurvePrimitive curPri = CurvePrimitive.CreateLineString(ptArr);
            Element ele = DraftingElementSchema.ToElement(dgnModel, curPri, null);
            BIM.Element eleInterop = SampleMixed.ElementOperation.ConvertToInteropEle(ele);
            eleInterop.Color = 3;
            eleInterop.LineWeight = 4;
            ele.AddToModel();
        }

选VS菜单Build > Build Solution来生成本解决方案。启动Mstn,打开key-in窗口,输入csAddins CreateElement TestConvertToDgnNetEle,可以看到在生成了如图所示的元素。

MSCE C#官网一步步学习搬运9 第九章、用C++/CLI编写Addins

元素我们是通过COM框架下的接口生成的,然后转换成新的编程框架下的元素,然后设置其颜色及线宽属性,最后添加到dgn文件中。接下来验证另一个函数,在key-in窗口,输入csAddins CreateElement TestConvertToInteropEle,可以看到在生成了如图所示的元素。

MSCE C#官网一步步学习搬运9 第九章、用C++/CLI编写Addins

元素我们是通过新的编程框架下的接口生成的,颜色及线宽我们是通过COM框架下的接口设置的。本章例子只是简单演示了C++/CLI的用途,实际上C++/CLI能为我们做很多东西。当然其难度也是Mstn平台主要的三种开发语言中最高的,要想掌握C++/CLI必须先对C#和C/C++中对象的生命周期管理以及内存回收机制有很深的了解才行。建议读者要先熟练使用C#和C/C++之后,再开始利用C++/CLI进行开发。完整的csAddins解决方案源文件下载链接如下。非常欢迎大家批评指正,非常欢迎与他人分享该博客。