第8章、在Addins中调用C/C++函数


在第零章中我介绍过在MSTN CE版本上Addins开发方式新增加了一套与NativeCode架构几乎“平行”的编程框架。这里用了“几乎” 是因为C#C/C++是两种完全不同的语言,虽然有很多相似的地方,但是要做到完全平行是不可能的。所以如果我们选择Addins开发方式的话,开发过程中会偶尔遇到NativeCode能实现的功能,在Addins的编程框架下找不到对应的接口。这个时候我们就要想办法自己动手在Addins中调用C/C++的接口了。有两种方式可以实现,一种是通过C#PInvoke调用NativeCode端导出的函数,另一种是通过C++/CLI直接实现C#C/C++的混合编程。本章简单介绍一下PInvoke,下一章介绍C++/CLI

Level是Mstn中对元素分组的一种方式。通过把元素放到不同Level下,可以对元素分组管理。Addins和NativeCode中都有获取当前Dgn文件中Level名字的接口。但是当你使用Addins中的接口获取Level名字的时候你会发现有些Level的名字获取不到。但是NativeCode下却能全部得到。原因是Dgn文件中的Level是由两部分组成的,一部分是存储在当前Dgn文件中的Level,另外一部分是从WorkSpace下的DgnLib中加载的Level。NativeCode下的接口这两部分Level的名字都可以获取到,而Addins下只能获取到当前Dgn文件中Level的名字。本章我们要在C#端通过PInvoke调用已经在NativeCode端封装好的一个函数来获取Dgn文件中所有Level的名字。

本文主要介绍Addins开发,所以NativeCode端如何获取Level名字不再详细讲解,这里只把相关代码以及编译好的DLL上传上来。具体如何编译请参考另一篇NativeCode的学习博客。在NativeCode端我们导出了两个函数GetDgnlibLevelNames和ReleaseDgnlibLevelNames。GetDgnlibLevelNames获取Level的名字,ReleaseDgnlibLevelNames负责释放动态申请的内存。我们将在Addins中封装调用这两个函数。请将SampleNative.dll下载后拷贝到…\ Bentley\MicroStation CONNECT Edition\MicroStation\Mdlapps下。

communities.bentley.com/.../SampleNative.7zcommunities.bentley.com/.../SampleNative_5F00_DLL.7z

下面就让我们一步步来实现如何在C#的Addin中调用这两个函数。

1. 打开commands.xml文件,新增命令csAddins DemoForm ShowLevelNames并指定其处理函数为csAddins.DemoForm.ShowLevelNames。如果您对XML格式的命令表文件还不熟悉,请参考第四章的相关主题。

2. 选择VS菜单Project->csAddins Properties…,切换到Build下,选中“Allow Unsafe Code”复选框,如下图所示。

3. 打开DemoForm.cs文件,在代码开头部分增加如下的using语句。

using System.Runtime.InteropServices; 
using Bentley.DgnPlatformNET;
using Bentley.MstnPlatformNET;

4. 翻到该文件的尾部,增加命令处理函数ShowLevelNames以及调用外部函数所需要的DllImport属性声明,最终的源代码如下。

unsafe public static void ShowLevelNames(string unparsed)
        {
            List<string> namesList = new List<string>();
            LevelHandleCollection lvlHanCol = Session.Instance.GetActiveDgnFile().GetLevelCache().GetHandles();
            foreach (LevelHandle lvlHan in lvlHanCol)
            {
                namesList.Add(lvlHan.Name);
            }
            int namesCnt = 0;
            //GetDgnlibLevelNames返回的是一个字符串指针数组,即数组中每一项都是一个字符串指针,参数中返回了数组的维度
            void** namesvpp = GetDgnlibLevelNames(ref namesCnt);
            IntPtr ptr = new IntPtr(namesvpp);
            //迭代数组中的每一个字符串,将字符串转换为托管字符串添加到list中
            for (int i = 0; i < namesCnt; i++)
            {
                IntPtr ptr1 = new IntPtr(ptr.ToInt64() + 8 * i);
                string lvlName = Marshal.PtrToStringUni(new IntPtr(*(void**)ptr1.ToPointer()));
                namesList.Add(lvlName);

            }
            //释放非托管字符串占用的内存
            ReleaseDgnlibLevelNames(namesvpp, namesCnt);
            foreach(string lvlName in namesList)
            {
                MessageCenter.Instance.ShowInfoMessage(lvlName, lvlName, false);
            }
        }

        [DllImport("SampleNative.dll")]
        public static unsafe extern void** GetDgnlibLevelNames(ref int namesCnt);


        [DllImport("SampleNative.dll")]
        public static unsafe extern void ReleaseDgnlibLevelNames(void** namesPP, int namesCnt);

5. 在VS中重新生成csAddins。然后启动Mstn并打开一个dgn文件,键入mdl load csAddins装载csAddins,再键入csaddins demoform showlevelnames,在Mstn的消息中心(双击Mstn底部的消息栏可以打开)就会显示出当前dgn文件中的所有Level的名字,包括从Dgnlib中加载的Level。如下所示是在我的机器上运行的结果。

这里我们封装的两个函数的参数及返回值类型都是很简单的基本类型,实际开发过程中参数类型是很复杂的,例如参数类型是C++的类或者结构体。这样的函数封装的时候我们需要先在Addins端声明定义一个在二进制布局上与C++端的类或者结构体一致的类型,然后才能通过DllImport属性声明去调用NativeCode端的函数。具体如何在C#中声明定义与C++中的类或者结构体一致的类型不在本文的讨论范围,网上有很多相关的资料,读者可以自行到网上去搜索。

如下链接为最终版本的csAddins源代码供您参考。

communities.bentley.com/.../2086.csAddins_5F00_chapter8.7z