在 初试 MDL 这篇文章中,认识了 MDL 文件结构、程序运行以及编译原理,下面就来讲讲 MDL 设计思路以及如何编写各部分。
设计思路
MDL 如何开始,并能使初设很容易转换成高层次的设计:
首先:
- 定义使用者在熟悉的术语中操作的对象类型
- 列出对象的属性
- 定义对象的放置和操作
- 其他需要的功能
进阶:
- 确定用于表示应用程序对象的 MicroStation 元素
- 确定必须从用户那里收集哪些设置
- 设计元素放置和操作工具
- 用于其他功能的设计工具和设置框
以上就是在编程应用程序的设计细节之前,需要首先拥有的功能规范,描述应用程序要执行的操作。
以下就是实际编程中 MDL 初始化逻辑:
为对话框、命令表和消息列表打开资源文件:
12RscFileHandle rfHandle;mdlResource_openFile (&rfHandle, NULL, FALSE);注册消息列表 id 以用于操作的提示或错误:
12//MSGLIST_commands、MSGLIST_prompts 分别为命令ID、命令提示IDmdlState_registerStringIds (MSGLIST_commands, MSGLIST_prompts);加载命令表:
1mdlParse_loadCommandTable (NULL);为系统事件(比如:【SYSTEM_UNLOAD_PROGRAM:程序尚未加载】、【SYSTEM_RELOAD_PROGRAM:程序重新加载】)设置用户的 hook 函数:
12// 在 SYSTEM_UNLOAD_PROGRAM 卸载时在 unloadFunction 函数中做一些事情,比如存储这是输入的值以便下次使用,不用再次输入,而 unloadFunction 就是 hook 函数,用于在指定的事件发生时触发自定义的操作mdlSystem_setFunction (SYSTEM_UNLOAD_PROGRAM, unloadFunction);使用 MicroStation 对话框管理器注册对话框 hook 函数:
1234567891011121314151617181920212223242526272829303132//注册对话框 hook 函数组,并关联hook函数ID和函数地址mdlDialog_hookPublish (sizeof(uHooks)/sizeof(DialogHookInfo), uHooks);// 将hook函数 myLevel_comboHook 和 对话框ID为 HOOKITEMID_MyLevelCombo的关联起来,以监测这个对话框条目生命周期的事件DialogHookInfo uHooks[] ={{HOOKITEMID_MyLevelCombo, (PFDialogHook)myLevel_comboHook },}//针对对话框条目比如:初始化、创建生成、清除等等进行监测,并触发相应的操作void myLevel_comboHook (DialogItemMessage *dimP){RawItemHdr *riP = dimP->dialogItemP->rawItemP;dimP->msgUnderstood = TRUE;switch (dimP->messageType){case DITEM_MESSAGE_CREATE:{//对话框条目生成时触发的操作break;}case DITEM_MESSAGE_DESTROY:{//对话框条目清除时触发的操作break;}.......default://对话框条目其他情况默认触发的操作break;}}打开应用程序的初始对话框,初始化完成后,主函数返回,应用程序等待它的对话框 hook 函数、用户 hook 函数或命令函数被调用:
12//其中 DIALOGID_Pal 为对话框资源文件中定义的对话框IDmdlDialog_open (NULL, DIALOGID_Pal);构建单元库,用于需要一组预定义图符的应用程序表示系统内置的单元库中的对象,而这个单元库由开发人员生成并打包成应用程序
创建命令表,注册命令表中的 command,并将命令ID与命令触发的方法对应:
123456789101112Private MdlCommandNumber commandNumbers [] ={{createALine, CMD_HELLOWORLD_CREATE_LINE},{createAComplexShape, CMD_HELLOWORLD_CREATE_COMPLEXSHAPE},{createAProjectedSolid, CMD_HELLOWORLD_CREATE_PROJECTEDSOLID},{createABsplineSurface, CMD_HELLOWORLD_CREATE_BSPLINESURFACE},{connectHitPoints, CMD_HELLOWORLD_CONNECT_HITPOINTS},{turnOnViewTransient, CMD_HELLOWORLD_TRANSIENT_ON},{turnOffViewTransient, CMD_HELLOWORLD_TRANSIENT_OFF},0};mdlSystem_registerCommandNumbers (commandNumbers);编写原始命令逻辑,编写应用程序的所有原始命令例程来处理元素放置和操纵。每一个原始的命令都需要设置状态函数来处理事件,比如数据点、重置和显示动态——当它被拖过屏幕时显示一个元素,有时被称为“弹性连接”。
为直接命令和实用命令编写逻辑,其中直接命令用于修改内部设置的值,比如:精确绘图快速旋转 RQ,但对话框是一种更优雅的方式来设置内部变量的值,因此编写直接命令的需求减少了。而实用命令不需要来自用户的交互输入,并且不只是设置应用程序变量,还有很多状态控制函数,比如会经常使用的:
12//根据事件规定使用的函数,下面的意思是动态绘制状态时调用函数 line_drawLine 进行动态绘制直线mdlState_setFunction (STATE_COMPLEX_DYNAMICS, line_drawLine);构建工具栏,工具栏是开始命令的图形图标。当用户单击一个工具时,就会启动适当的命令。如果向用户提供命令,MicroStation 类型的应用程序至少有一个工具箱。在开发的初始阶段,创建工具箱并不是绝对必要的,因为可以在命令行键入命令。然而,当您可以单击一个图标以启动一个命令而不是在命令行键入它时,测试就容易多了。
构建对话框,对话框显示并收集数据
创建在线帮助的文档
编程实践
要实现【 MDL 初始化逻辑】里的步骤,主要需要编写以下部分:
- 消息表(xxxmsg.r)
- 命令表(xxxcmd.r)
- 对话框(xxx.r)
- 源码(xxx.cpp)
- 编译规则(xxx.mke)
接下来如何编写以上文件,根据 初试 MDL 这篇文章中的示例程序 adrwdemo 的绘制直线的功能来讲解:
消息表
消息列表包含应用程序使用的消息。列表中的每条消息都有相应的消息编号。这些消息号必须是唯一的,并以升序的顺序出现在列表中。MessageList的资源标识符(称为resources ID)与消息编号相结合,惟一地标识列表中的给定消息。
消息表结构的语法如下:
|
|
根据语法在 adrwdemomsg.r 编写如下代码:
|
|
在 MS 操作时界面左下角的提示框就会显示【Line ->Enter point】,提示需要在视图中输入一个数据点。
命令表
命令表定义了应用程序命令的语法。命令表是分层的,其中一个主表向下分支到子命令树中。一个应用程序的命令表是在一个资源源文件中定义的,它被编译成产生两个输出文件,一个是用来解析和验证键的,另一个是唯一标识每个命令的无符号整数的列表。
命令表结构的语法如下:
|
|
根据语法在 adrwdemocmd.r 编写如下代码:
|
|
在 MS 中顶部菜单栏【实用工具 】-【命令行】键入画线命令【DEMO LINE】,就能启动画线的功能
对话框
工具栏和对话框的GUI相关信息都是存储在资源文件中,通过编写资源文件来控制列表条目、条目资源显示的位置和方式、将被发送到条目hook函数的消息列表以及相关对话框管理器功能的列表。标准对话框条目有:ColorPicker, Generic, GroupBox, IconCmdFrame, Label, LevelMap, ListBox, MenuBar, MLText, OptionButton, PushButton, ScrollBar, Text, ToggleButton, Sash, Scale, RadioButton, MenuBarX, LevelPicker, ToolBox, IconCmdX, ButtonGroup、IconCmdFrameX,这里只列举示例程序中用到的条目类型:
对话框
上图为对话框,其数据结构:
|
|
对话框通用条目
红色方框内为对话框通用条目,其数据结构:
|
|
图标命令框
红色方框内为图标命令框,其数据结构:
|
|
图标命令框条目
红色方框内为图标命令框条目,其数据结构:
|
|
开关状态按钮
红色方框内开关状态按钮,其数据结构:
|
|
命令列表条目
CmdItemListRsc 基于命令,(命令编号被用作ID)CmdItemListRsc 让 MicroStation 无论如何调用该命令,都可以找到当前命令的工具设置,以此解决由于工具设置项列表与图标相关联,(而不是图标执行的命令),只有当图标在内存中(显示)时才能找到工具设置。在检查 IconCmdRsc 资源中的条目列表之前,MicroStation 现在寻就会找与当前命令相关联的 CmdItemListRsc。
|
|
对话框管理访问字符串
假设开发按钮的字段 label 为变量 adrwdemoInfo.showAxe,如下:
|
|
因为访问字符串为检查和修改对话框条目定义变量、结构和结构的指针,所以必须通过发布变量、结构或指针, 使对话框条目能够管理我们的数据。实现分为两步:
xxxtyp.mt中定义结构和联合的说明,这个文件含有包含文件和publishStructures语句,publishStructures 语句识别在资源源文件中定义的结构。这个结构不能含有动态说明(分配空间的那些说明)或可执行语句:
12#include "adrwdemo.h"publishStructures (adrwdemoinfo);以此来发布定义在 adrwdemo.h 中的用于设置是否启用结构平面或者显示坐标轴的全局数据结构体:
12345typedef struct adrwdemoinfo{int showAxes;int useCPlane;} AdrwdemoInfo;定义符号集,符号集是一个指向内存区域的指针。在这个内存区域内对话条目可以找到应用的变量、结构和指针,为了建立并公布要存取的结构 AdrwdemoInfo,在 .cpp 程序入口函数中编写如下:
12345//使setP符号集初始化,可见标志被分配给每一个符号。在搜寻过程中检查可见标志来决定符号集是否包括在搜寻中。当这些符号用于对话框时, 我们规定VISIBILITY_DIALOG_BOX。这些符号如果用于计算器或预处理器,我们则指定VISIBILITY_CALCVLATOR。如果符号同时用于二者,则规定(VISIBILITY_DIALOG_BOX\ VISIBILITY_CALULATOR)。若符号与调试程序一起使用, 则我们规定VISIBILITY_DBUGGER。SymbolSet *setP = mdlCExpression_initializeSet (VISIBILITY_DIALOG_BOX, 0, 0);//发布要使用的符号mdlDialog_publishComplexVariable (setP, "adrwdemoinfo","adrwdemoInfo", &adrwdemoInfo);
根据各个对话框条目的语法,在 adrwdemo.r 编写如下代码:
|
|
源码
adrwdemo.cpp 文件的画线功能由以下三部分组成:
引入头文件,涉及用到的宏定义、全局变量以及函数
123456789#include <MicroStationAPI.h>#include <cmdlist.h>#include <dlogman.fdf>#include <AccuDraw.h>#include <mselmdsc.fdf>#include "adrwdemo.h"#include "adrwdemocmd.h"#include "adrwdtxt.h程序入口函数【 extern “C” DLLEXPORT int MdlMain (int argc, char *argv[]) { }】,编写参照【MDL初始化逻辑】
123456789101112131415161718192021222324252627282930313233343536373839extern "C" DLLEXPORT int MdlMain (int argc, char *argv[]){RscFileHandle rfHandle;SymbolSet *setP;//打开资源文件,载入内存mdlResource_openFile (&rfHandle, NULL, FALSE);//加载命令表mdlParse_loadCommandTable (NULL);//创建命令表,注册命令表中的 command,并将命令ID与命令触发的方法对应Private MdlCommandNumber commandNumbers [] ={{line, CMD_DEMO_LINE},{circle, CMD_DEMO_CIRCLE},{rect, CMD_DEMO_RECTANGLE},{changeCircle, CMD_DEMO_CHANGE},{rect2, CMD_DEMO_RECTANGLE2},0};mdlSystem_registerCommandNumbers (commandNumbers);//注册消息列表 id 以用于操作的提示或错误mdlState_registerStringIds (MSGLIST_commands, MSGLIST_prompts);/*使setP符号集初始化,并发布使用的符号,符号集是一个指向内存区域的指针。在这个内存区域内对话条目可以找到应用的变量、结构和指针,为了建立并公布要存取的结构 adrwdemoinfo*/setP = mdlCExpression_initializeSet (VISIBILITY_DIALOG_BOX, 0, 0);mdlDialog_publishComplexVariable (setP, "adrwdemoinfo","adrwdemoInfo", &adrwdemoInfo);adrwdemoInfo.showAxes = FALSE;打开应用程序的初始对话框mdlDialog_open (NULL, DIALOGID_Pal);return 0;}当程序运行完这个函数,视图中就会显示 adrwdemo 工具框。
画线功能函数,MS内部机制是事件驱动,由事件队列统一管理调度,所以当按下 adrwdemo 工具框中的画线图标命令,这个画线任务就会进入事件队列,由事件队列处理器调用画线功能函数,所以需要使用函数mdlState_startPrimitive编写一个原始命令,由原始命令例程来处理元素放置和操纵,参考【MDL初始逻辑】第9条
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071//画线初始命令Private void line (char *unparsed){/*调用mdlState_startPrimitive基本命令生成元素,并调用line_dataPt1开始画线,并配置宏定义字符串MSG_cmdLine 和 MSG_promptEnterPoint 组成"Line->enter point"的命令提示*/mdlState_startPrimitive (line_dataPt1, line, MSG_cmdLine, MSG_promptEnterPoint);}//画线输入第一个数据点,作为画线的起始点Private void line_dataPt1 (Dpoint3d *ptP, int view){//存储确定的第一个数据点ptStack[0] = *ptP;//设置事件触发函数,当输入数据点时触发函数line_dataPt2,动态绘制时触发函数line_drawLinemdlState_setFunction (STATE_DATAPOINT, line_dataPt2);mdlState_setFunction (STATE_COMPLEX_DYNAMICS, line_drawLine);//设置输出的命令提示mdlOutput_rscPrintf (MSG_PROMPT, NULL, MSGLIST_prompts,MSG_promptEnterPoint);}//画线输入第二个数据点,作为画线的终结点Private void line_dataPt2 (Dpoint3d *ptP, int view){double length; /* length of line placed */DVec3d xAxisVec; /* unit vector -- direction of line *///传入第二个数据点,根据两点绘制直线line_drawLine (ptP, view, NORMALDRAW);//求出两点法向量的距离length = mdlVec_computeNormal (&xAxisVec, ptP, &ptStack[0]);//设置精确绘图 AccuDraw 的位置、距离、方向等,以便更好地操作,提高工具的可用性和能力mdlState_setAccudrawContext (ACCUDRAW_SetDistance |ACCUDRAW_SetXAxis,NULL, /* origin: last point OK */NULL, /* delta (not used here) */&length, /* distance */NULL, /* angle (not used here) */&xAxisVec /* x axis: last line */);//将第二个数据点设置为第一个ptStack[0] = *ptP;mdlState_setFunction (STATE_COMPLEX_DYNAMICS, line_drawLine);mdlOutput_rscPrintf (MSG_PROMPT, NULL, MSGLIST_prompts,MSG_promptEnterPoint);}//结合line_dataPt1和line_dataPt2两个数据点画线Private void line_drawLine (Dpoint3d *ptP, int view, MstnDrawMode drawMode){MSElement elem;Dpoint3d points[2];points[0] = ptStack[0];points[1] = *ptP;//根据传入的两个点,创建元素mdlLine_create (&elem, NULL, points);//显示创建的直线元素mdlElement_display (&elem, drawMode);//当完成绘制,就将元素头部中的属性位设置为未锁定、新元素,而不修改,也就是在确定第二个点之后在视图生成固定位置的直线,而不动态改变了if (drawMode == NORMALDRAW)mdlElement_add (&elem);}
编译规则
MDL 的编译文件是 makefile 类型的文件,拥有自己的语法和变量,在 Linux 中也用来进行宏编译,它的好处:
- 简化编译时所需要执行的命令
- 若在编译完成之后,修改了某个源码文件,则 make 仅会针对被修改了的文件进行编译。其他的目标文件不会被更改
- 最后可以依照相依性来更新执行文件
adrwdemo.mke 编写如下:
|
|
编写完成,运行程序,实现下图红框中的画线功能: