LearnDgnTool-05_DgnElementSetTool详解


前面我们已经介绍,通过DgnPrimitiveTool我们可以实现交互式放置图形元素的工具,不管操作流程多么复杂,我们只要按照操作流程,去重写DgnPrimitiveTool中相关的虚函数,就能一步一步完成想要的功能。大多数情况下,DgnPrimitiveTool提供的接口已经足够使用了,但是如果我们在放置图形元素时,需要用户选择一些已有的元素从而获取一些特定信息才能放置我们的图形元素时(例如交互式放置路灯或者路牌,需要用户选取一条道路中心线),DgnPrimitiveTool提供的接口显然已经不能满足我们的要求了。因为DgnPrimitiveTool中没有提供定位元素的接口,当然如果你已经有一些Mstn二次开发相关经验的话,你完全可以在重写DgnPrimitiveTool的虚函数时,调用mdlLocate_XXX系列函数自己实现定位元素的功能。但是如果我告诉你DgnElementSetTool除了提供通过鼠标点选元素的功能外,还提供了框选、划选、选择集以及围栅获取元素的功能,你还愿意自己在DgnPrimitiveTool的虚函数里边去完成这些功能么?而且在使用DgnElementSetTool提供的这些功能时,大多数情况下我们也只是简单重写少数几个相关的虚函数即可。当然,要想完全理清DgnElementSetTool各个成员函数的用法也不是一件容易的事情,我粗略地数了一下,DgnElementSetTool的成员函数,包括从基类继承下来的大概有二百个左右。我们也不用害怕,只要花一点时间捋清各个函数之间是如何配合的,真正使用的时候,只用重写用到的相关函数即可,大部分不需要我们重写,DgnElementSetTool已经帮忙我们实现了。接下来就让我们进入DgnElementSetTool的世界吧!

DgnElementSetTool的基类除了DgnPrimitiveTool以外还有IRedrawOperation和ModifyOp这两个基类,从DgnPrimitiveTool继承了视图及鼠标键盘等交互事件的响应功能,从IRedrawOperation继承实现了元素动态重绘的功能,从ModifyOp继承实现了元素修改逻辑的功能,而DgnElementSetTool自身又添加了元素选取(包括点选、划选框选、选择集以及围栅)的功能。我们首先从工具启动后DgnElementSetTool的成员函数调用顺序看一下DgnElementSetTool的各个成员函数的调用执行流程。

当我们实现了一个从DgnElementSetTool派生下来的类以后,与DgnPrimitiveTool一样,我们动态创建一个类的实例化对象,调用基类的成员函数InstallTool来启动我们的工具类。我们在介绍DgnPrimitiveTool的时候InstallTool后台调用的函数只提到了_OnInstall和_OnPostInstall函数,实际上InstallTool只是调用了_InstallToolImplementation,_InstallToolImplementation进而调用了_OnInstall和_OnPostInstall函数,这一点我们从…\DgnView\DgnTool.h头文件中就能看到。DgnElementSetTool重写了这三个函数,我们先看一下重写的_InstallToolImplementation函数。这个函数里边会调用_SetElemSource设置DgnElementSetTool:: m_elemSource的值,这个值是调用_GetPreferredElemSource获得的。_GetPreferredElemSource通过调用_AllowFence、_AllowSelection以及当前围栅、选择集的激活状态判断返回什么值。如果_AllowFence返回的是USES_FENCE_Required,表明必须通过围栅获取元素,则_GetPreferredElemSource返回SOURCE_Fence,如果_AllowFence返回的是USES_FENCE_Check,表明可以但不是必须要通过围栅选择元素,这个时候再查看当前是否有围栅激活。如果有的话也是返回SOURCE_Fence,否则就继续往后判断。接下来就是通过_AllowSelection的返回值判断是否支持选择集获取元素,流程跟围栅的判断流程类似。最后如果围栅和选择集都不支持的话就返回SOURCE_Pick。_GetPreferredElemSource的伪代码如下所示:

DgnElementSetTool::ElemSource  DgnElementSetTool::_GetPreferredElemSource ()
    {
    switch (_AllowFence ())
        {
        case USES_FENCE_Required:
            return SOURCE_Fence;

        case USES_FENCE_Check:
            if (_UseActiveFence () && FenceManager::GetManager().IsFenceActive ())
                return SOURCE_Fence;
            break;
        }

    switch (_AllowSelection ())
        {
        case USES_SS_Required:
            return SOURCE_SelectionSet;

        case USES_SS_Check:
            if (SelectionSetManager::GetManager().IsActive ())
                return SOURCE_SelectionSet;
            break;

        default:
            SelectionSetManager::GetManager().EmptyAll ();
            break;
        }

    return SOURCE_Pick;
    }

设置好DgnElementSetTool:: m_elemSource的值后,会调用基类的_InstallToolImplementation函数,即DgnPrimitiveTool:: _InstallToolImplementation,这个函数最终调用了我们先前介绍的_OnInstall和_OnPostInstall函数。而DgnElementSetTool重写了这两个函数,所以实际上调用了DgnElementSetTool:: _OnInstall和DgnElementSetTool:: _OnPostInstall。我们先介绍一下DgnElementSetTool:: _OnInstall里边做了哪些操作。这个函数里边通过调用_GetElemSource获取DgnElementSetTool:: m_elemSource的值,判断这个值如果是SOURCE_Fence,但是当前没有围栅激活的话,就返回false,工具就无法激活。同样的如果是SOURCE_SelectionSet,而当前没有选择集激活的话也是返回false。如果通过这两步的话就返回true,工具就会被激活真正成为当前的工具,继而调用DgnElementSetTool:: _OnPostInstall。主要的调用顺序如下所示:

DgnTool ::InstallTool

               DgnElementSetTool:: _InstallToolImplementation

                              DgnElementSetTool:: _SetElemSource

                                             DgnElementSetTool:: _GetPreferredElemSource

                                                            DgnElementSetTool:: _AllowFence

                                                            DgnElementSetTool:: _AllowSelection

                              DgnPrimitiveTool:: _InstallToolImplementation

                                             DgnElementSetTool:: _OnInstall

                                             DgnElementSetTool:: _OnPostInstall

接下来我们看一下DgnElementSetTool:: _OnPostInstall函数。函数里边首先判断如果_GetElemSource返回的结果是SOURCE_Pick的话就调用_BeginPickElements函数。_BeginPickElements会设置点选元素时搜寻哪些Model,以及设置光标为点选元素状态。如果_GetElemSource返回的是SOURCE_SelectionSet的话,接下来会根据_NeedPointForSelection返回的结果判断是否需要用户输入额外的确认点才开始处理选择集元素。如果我们重写了_NeedPointForSelection并且返回false(DgnElementSetTool:: _NeedPointForSelection返回true),则相继调用_BuildAgenda、_ProcessAgenda、_OnModifyComplete对选择集中的元素进行处理。如果返回true的话,则继续往下执行,调用_NeedPointForDynamics判断是否需要用户输入额外的确认点来启动动态绘制。如果重写了_NeedPointForDynamics并且返回false(DgnElementSetTool:: _NeedPointForDynamics返回true),就会调用_BeginDynamics启动动态绘制。调用_BeginDynamics之前会调用_GetElemSource判断当前是否是通过选择集选择的元素(返回SOURCE_Pick),如果是会调用_BuildAgenda来获取选择集中的元素。_OnPostInstall的伪代码如下所示:

void            DgnElementSetTool::_OnPostInstall ()
{
…
    if (SOURCE_Pick == _GetElemSource ())
        {
        _BeginPickElements ();
        }
    else if (SOURCE_SelectionSet == _GetElemSource ())
        {
        if (!_NeedPointForSelection ())
            {
	…
            _BuildAgenda (ev);

            _ProcessAgenda (ev)
                _OnModifyComplete (ev);

            return;
            }
        else
            {
…
        }
    if (!_NeedPointForDynamics ())
        {
        if (SOURCE_Pick != _GetElemSource ())
            {
	…
            _BuildAgenda (ev);
            }

        _BeginDynamics ();
        }
}

DgnElementSetTool::_ OnPostInstall函数里边有两个函数我们需要更深入地研究一下,_ BuildAgenda和_ ProcessAgenda,_ ProcessAgenda 我们放到后边介绍_OnDataButton函数的时候再介绍,我们先看一下_BuildAgenda。DgnElementSetTool定义了一个ElementAgenda的私有成员变量m_agenda,无论是围栅、选择集还是点选的元素,使用DgnElementSetTool提供的拾取元素的接口最终获取到的元素都会放到这个变量里边,由于是私有成员变量,所以DgnElementSetTool的派生类里边只能通过GetElementAgenda函数访问这个成员变量。_ BuildAgenda根据_GetElemSource返回的结果判断是通过围栅还是选择集来填充m_agenda,填充后会继续调用_ModifyAgendaEntries对m_agenda中的元素进行修改筛选。_ModifyAgendaEntries会调用_FilterAgendaEntries,如果重写了_FilterAgendaEntries并且返回true(DgnElementSetTool:: _FilterAgendaEntries返回false)的话,会将m_agenda中无效的元素(指调用了EditElementHandle:: Invalidate的元素)剔除掉,所以这就给了我们一个机会去筛选DgnElementSetTool为我们拾取的元素。最后调用_HiliteAgendaEntries高亮最终的ElementAgenda,如果是通过围栅获取的元素,_HiliteAgendaEntries 会调用_HiliteFenceElems判断是否高亮围栅选中的元素。

_OnInstall和_OnPostInstall执行之后,工具处与激活状态,等待用户的输入。这个时候我们的鼠标或者键盘在视图中发生的一些事件会触发DgnElementSetTool对应的成员函数,这些成员函数都是虚函数,我们可以在自己的类中重写这些虚函数从而响应这些事件,我们首先介绍一下_OnPostLocate函数。当光标在视图中移动并且定位到某个元素时,这个函数就会被调用。_OnPostLocate的函数原型如下所示:

virtual bool    _OnPostLocate (HitPathCP path, WStringR cantAcceptReason);

通过path参数我们可以获取到当前光标定位到的元素,我们可以在重写的_OnPostLocate函数中获取元素的属性,然后判断我们的工具是否支持处理定位到的元素。如果不支持,就返回false,这样用户就没办法选中这个元素。我们还可以设置cantAcceptReason参数的值,从而提醒用户是什么原因不能选中这个元素,接下来我们看一下DgnElementSetTool ::_OnDataButton函数。

前面已经提到过单击鼠标左键时_OnDataButton函数会被调用,DgnElementSetTool中为我们实现了_OnDataButton函数,这个函数与_DecorateScreen、_OnModifierKeyTransition、_OnModelStartDrag、_OnModelEndDrag、_OnModelMotion等函数配合完成了支持点选、划选以及框选的功能。DgnElementSetTool::_OnDataButton函数中首先会根据m_agenda中元素个数是否为0或者_WantAdditionalLocate的返回结果是否为true,判断当前是否需要拾取元素。如果需要拾取元素的话会通过_GetElemSource的返回的结果判断是要点选元素还是要通过选择集或者围栅来拾取元素。如果是点选元素的话,会调用_LocateOneElement来定位拾取元素,否则会调用前面提到过的_BuildAgenda来拾取元素。如果没有拾取到新的元素的话,会根据_AllowDragSelect的返回结果判断是否要启用框选或者划选。如果启用框选或者划选的话,那么当光标停止拖拽在_OnModelEndDrag函数中,_BuildDragSelectAgenda会被调用。_BuildDragSelectAgenda会把框选或者划选的元素加入到m_agenda中,如果当前处于多选状态下的话,框选或者划选的元素已经在m_agenda中,这些元素会被反选掉。如果不是在多选状态下的话,m_agenda首先会被清空,然后再将框选或者划选的元素插入到m_agenda中,调用_BuildDragSelectAgenda之后会调用_ModifyAgendaEntries,前面我们提到过_ModifyAgendaEntries函数,给了我们一个过滤元素的机会。我们继续回到_OnDataButton函数中,如果拾取到新的元素的话,会调用_WantDynamics判断是否要启动动态绘制。然后会调用_NeedAcceptPoint判断是否需要用户再点击左键才开始处理元素,如果返回true的话,_OnDataButton会直接返回。如果_NeedAcceptPoint返回false的话,会直接调用_ProcessAgenda开始执行元素修改的流程。_ProcessAgenda调用以后,最后调用_OnModifyComplete。其中DgnElementSetTool::_NeedAcceptPoint的实现如下所示,可以看到如果_WantDynamics返回true,的话其直接返回true,否则会根据m_agenda中元素的来源判断返回true还是false。

bool            DgnElementSetTool::_NeedAcceptPoint ()
    {
    if (_WantDynamics ())
        return true;

    return (SOURCE_Pick != _GetElemSource () || AccuSnap::GetInstance().UserWantsLocates() ? false : true);
    }

_OnDataButton函数的流程图如下所示:

_OnDataButton函数中调用的_LocateOneElement、_ProcessAgenda、_OnModifyComplete我们需要再深入探讨一下。_LocateOneElement中通过调用_WantAdditionalLocate判断是否支持多选,如果_WantAdditionalLocate返回false,则会清空m_agenda。如果返回true的话,会判断第二个参数newSearch的值。newSearch这个参数是用来控制定位元素时是否继续从上次定位到元素的位置往后查找,我们知道虽然在Mstn中我们看到的元素在空间下是杂乱无章的,但是Dgn文件在存储到磁盘上时,是按顺序一个挨着一个存储的。_LocateOneElement函数中是通过调用_DoLocate来定位元素的,_DoLocate在搜寻到元素时,会记录查找到元素的位置,_DoLocate有一个参数可以让我们从这个位置继续往后定位搜寻元素。在调用_DoLocate之前,如果newSearch的值为false的话,表明用户最近拾取的元素不是想要的,需要继续往后定位其他元素,所以这里会调用_RemoveAgendaElement从m_agenda中移除最近一次定位到的元素。之后就会调用_DoLocate来定位元素了,_DoLocate返回定位到的元素,_LocateOneElement中通过调用_BuildLocateAgenda把定位到的元素插入到m_agenda中。_BuildLocateAgenda也是先调用_WantAdditionalLocate判断是否支持多选,如果支持多选且当前Ctrl键处于按下的状态(通过参数ev->IsControlKey()判断)的话会先查看m_agenda中是否已经存在要插入的元素,如果存在会调用__RemoveAgendaElement将其移除然后马上返回,这样就实现了反选的功能。如果不存在的话,最后将定位到的元素插入到m_agenda中,这个时候会调用_IsModifyOriginal判断是插入元素本身还是元素的拷贝。其实我们前面介绍的_BuildAgenda以及框选划选的时候都会调用_IsModifyOriginal判断是插入元素本身还是元素的拷贝。另外在点选元素时还会调用_DoGroups判断是否与要插入元素同一组的元素也要插入到m_agenda中,在调用_RemoveAgendaElement时,也是会通过_DoGroups判断是否要移除同一组中的其他元素。 _LocateOneElement调用_BuildLocateAgenda之后也会调用_ModifyAgendaEntries,如前面所述,在这里同样给了我们一个机会去筛选m_agenda中的元素。

接下来我们看一下_ProcessAgenda函数,_ProcessAgenda中会依次调用_SetupForModify、_PreModifyAgenda、_ModifyAgendaGroup、_PostModifyAgenda。在_PreModifyAgenda中,会调用_IsFenceClip判断是否用围栅对元素进行剪切,如果需要的话,会调用_DoFenceClip来完成。在_ModifyAgendaGroup函数中,经过层层调用最终会调用到_DoOperationForModify、_OnPreElementModify、_OnElementModify。在_OnElementModify函数中如果我们对通过参数传递进来的元素进行修改,并且返回SUCCESS的话,在_DoOperationForModify中会用修改后的元素替换掉原来的元素,当然前提是_IsModifyOriginal返回true。否则的话会直接把修改后的元素重新添加到Dgn文件中。

_OnDataButton函数中调用_ProcessAgenda之后会调用_OnModifyComplete。_OnModifyComplete会先调用_CheckSingleShot,如果_CheckSingleShot返回true,则_OnModifyComplete直接返回。否则会根据_GetElemSource的返回结果做不同的操作,若果返回SOURCE_Pick,则会调用_NeedAcceptPoint和_AcceptIdentifiesNext,两者都返回ture的话会清空m_agenda,并调用_OnDataButton。如果_GetElemSource的返回结果是SOURCE_SelectionSet或者SOURCE_Fence则直接调用_ExitTool退出当前工具。

接下来我们看一下DgnElementSetTool封装的动态绘制功能是如何实现的。前面在介绍DgnPrimitiveTool的时候已经提到只要调用了_BeginDynamics之后,当我们的光标在视图中移动时,_OnDynamicFrame会被不停地调用,_OnPostInstall和_OnDataButton函数中在适当的时机都会调用到_BeginDynamics。DgnElementSetTool::_OnDynamicFrame中首先会判断m_agenda中是否有元素,如果没有就直接返回。否则继续调用_SetupForModify,这一点与_ProcessAgenda类似,区别是调用时第二个参数为true。接下来后台会调用一系列函数,经过层层调用后_OnRedrawInit、_OnRedrawOperation、_OnResymbolize、_OnRedrawFinish和_OnRedrawComplete会先后被调用到,其中_OnRedrawOperation又会调用到_OnElementModify。所以我们在_OnElementModify对元素所做的修改,同样的在动态绘制里边也会起作用。在这个过程中m_agenda中的元素会被复制拷贝一份出来传递给_OnElementModify,所以动态过程中并不会修改m_agenda中原有的元素。

目前为止,我们使用了大部分的鼠标相关的事件,其实键盘按键相关的事件处理函数主要有两个_OnModifierKeyTransition和_OnKeyTransition。_OnModifierKeyTransition在Ctrl、Alt或者Shift按下时触发,_OnKeyTransition是在VK_TAB, VK_RETURN, VK_END, VK_HOME, VK_LEFT, VK_UP, VK_RIGHT, VK_DOWN这些按键的其中一个按下时触发。并不是每个交互式工具都需要按键功能,根据情况而定。我们上边提到的框选和划选就是在_OnModifierKeyTransition函数中通过Alt键来切换的。