实现类CAD的交互式命令系统

看一段操作:
nfiee-z4zp6.gif
上面这个动画是我自己花了一周实现CAD绘图系统,里面实现了类CAD的交互命令
交互式命令操作流程

通过在命令栏或者菜单触发命令来启动相关功能,在命令执行过程中,命令栏提示当前命令的输入状态,当用户根据命令提示中的输入信息完成相应的操作(如在地图中点击某个点或者输入某个字符串)之后继续命令的下一步操作,直到命令的结束。
这种操作方式在设计类、地图类软件中经常用到,如AutoCAD,ARCGIS软件中,实现方式也有很多种。也各有优缺点,本文主要介绍类CAD系统中的交互命令实现。

在AutoCAD的二次开发中,有类似的提示用户等待函数

acedGetString函数暂停程序执行,等待用户输入一个字符串,该函数定义为:
int acedGetString(int cronly, const char * prompt, char * result);
acedGetString函数暂停程序执行,等待用户输入一个坐标,该函数定义为:
int acedGetPoint(const char* prompt,AcDbPoint* result);
//命令函数,以下是伪代码;
void cmd_drawline()
{
    AcDbPoint result_point;
    auto es= acedGetPoint("请输入起点坐标",&result_point);
    //在这个函数中,中断了命令drawline的执行,在不卡住主界面的情况下让用户输入所需要的值;
    AcDbPolylinePtr line=AcDbPolyline::createObject();
    if(es==eOk)
    {
         line->addPoint(result_point);
    }
    do {
       es= acedGetPoint("请输入下一个点坐标",&result_point);
       if(es==eNone)
       {
           break;
       }
       line->addPoint(result_point);
    } while(1);
}

常用交互式系统的解决办法

基于消息循环实现(C++)

消息循环的方式在各种语言中的都有类似的方法,比较容易找到解决办法。
优点:比较容易实现,在windows下可跨语言交互,

  1. 使用Qt的消息循环
//伪代码;
//用户输入类型;
enum InputType
{
    inputNothing,
    inputPoint,
    inputString,
};
struct CommandStatus
{
    //表示已经准备好输入;
    bool _has_read_get;
    char* _command_name;
    //输入结果;
    QVariant _result_value;
    //输入类型;
    InputType _inut_type=inputNothing;
};

//等待用户输入函数;
int acedGetPoint(const char* prompt,AcDbPoint* result)
{
    //获取当前的命令状态;
    CommandStatus&  command_status=getCurrentCommandStatus();
    //标记状态;
    command_status._inut_type=inputString;
    command_status._has_read_get=false;
    do {
       //最为关键的一步,Qt中防止阻塞的办法; 
       qApp->processEvents();
       if(command_status._has_read_get)
       {
           break;
       }
    } while(true);
    
    //获取结果数据;
    result=command_status._value.toPoint();
}

//地图中鼠标按下或者命令栏中的输入完成事件

void Canvas::mousePress(QMouseEvent* event)
{
   if(event->inputType()==InputPoint)
   {
       //当这个事件结束之后就会退出acedGetPoint函数中的死循环;
       CommandStatus&  command_status=getCurrentCommandStatus();
       auto point=toScreen(event->pos());//将像素坐标转为地理坐标;
       command_status._has_read_get=true;
       command_status._result_value=point;
   }
}
  1. 使用Qt中的QEventLoop

QEventLoop类为我们提供了一种进入和退出一个事件循环的方法,模态对话框就是类似的实现方式(也可以使用windows的消息循环,但是因为不能跨平台,windows的消息应用范围不高;

//等待用户输入函数;
QEventLoop _loop;
int acedGetPoint(const char* prompt,AcDbPoint* result)
{
    //获取当前的命令状态;
    CommandStatus&  command_status=getCurrentCommandStatus();
    //标记状态;
    command_status._inut_type=inputString;
    command_status._has_read_get=false;
    //开启独立的事件循环;
    _loop.exec();
}

//地图中鼠标按下或者命令栏中的输入完成事件

void Canvas::mousePress(QMouseEvent* event)
{
   if(event->inputType()==InputPoint)
   {
       //当这个事件结束之后就会退出acedGetPoint函数中的死循环;
       CommandStatus&  command_status=getCurrentCommandStatus();
       auto point=toScreen(event->pos());//将像素坐标转为地理坐标; 
       command_status._result_value=point;
       //退出循环;
       _loop.quit();
   }
}
  1. 在WPF中使用消息循环

当主程不是Qt程序时,上面的方法将会无效(当然也可以将Qt做成组件提供给C#中使用,具体方法在以前的文章<<C#的winform中嵌入Qt界面库>>。主要用用到了WPF的这个类 DispatcherFrame,这个类具体有什么作用,可以自行百度
ps:这个方法找了好久,才发现WPF有这样一个东西;

//其他的代码与C++的类似,最主要的时进入循环和退出循环中
class MessageCall
{
     private Stack<DispatcherFrame> _dispathcer_frame_list = new Stack<DispatcherFrame>();
    //挂起,进入消息循环,在
    public void yeild()
    {
        ComponentDispatcher.PushModal();
        var dispathcer_frame = new DispatcherFrame(true);
        _dispathcer_frame_list.Push(dispathcer_frame);
        Dispatcher.PushFrame(dispathcer_frame);
    }
    //退出消息循环;
    public void resume()
    {
         var dis_frame = _dispathcer_frame_list.Pop();
        if (dis_frame != null)
        {
            dis_frame.Continue = false;
        }

        ComponentDispatcher.PopModal();
    }
}

MessageCall message_call=new MessageCall();
int acedGetPoint(String prompt,ref AcDbPoint result)
{
    //获取当前的命令状态;
    CommandStatus  command_status=getCurrentCommandStatus();
    //标记状态;
    command_status._inut_type=inputString;
    command_status._has_read_get=false;
    //挂起,等待其他地方调用了resume才进行往下执行;
    message_call.yeild();
}

//地图中鼠标按下或者命令栏中的输入完成事件

void Canvas_mousePress(MouseEvent event)
{
   if(event->inputType()==InputPoint)
   {
       //当这个事件结束之后就会退出acedGetPoint函数中的死循环;
       CommandStatus  command_status=getCurrentCommandStatus();
       auto point=toScreen(event->pos());//将像素坐标转为地理坐标; 
       command_status._result_value=point;
       message_call.resume();
   }
}
使用有栈协程解决

是一种比线程更加轻量级的存在,正如一个进程可以拥有多个线程一样,一个线程可以拥有多个协程.很多语言如go自带了协程的支持,C++20支持无栈协程,在网络通信中应用比较多。

常用的C++开源协程有 libgolibcoboost coroutine2fibersC++20、windows纤程等 ,对于做命令驱动程序而言,一般只能用有栈协程, 无栈协程一般情况下协程函数一般情况下使用的是关键字来处理,有特定的返回值,也是通过语言语法糖来实现,局限性比较大(C++20的协程就是无栈协程)。

1. boost coroutine2
  1. 需要编译依赖boost的context模块,这里面没用到里面的fiber模块,fiber模块支持协程的调度。
  2. 封装到C#测试过,也支持协程的调用(某些协程跨语言调用就会出问题,如windows fiber在net2.0及以下版本可以用),也支持协程的调试。在net3.5版本中调试打断点会报调试异常(C#不支持协程调试)
  3. 这个库的使用相对来说还是不太友好,应用于交互系统中还是要写很多额外的代码。
#include "boost/coroutine2/all.hpp"
#include <stdio.h>
#include <iostream>
using namespace boost::coroutines2;
TCallback* _callback;
void cooperative(coroutine<void>::push_type &sink,int num)
{
	std::cout << "Hello";

	//之所以能够执行是因为重载了操作符()
	//返回main()函数继续运行
    //执行挂起操作;如果在交互系统中,这个sink是需要有成员变量接收的,
    //可以保留为指针,在命令中调用用户等待函数的时候在挂起;
	sink();  
   

	std::cout << "world";

	//执行完毕,返回main继续执行
}

 
void Test::testone()
{
	coroutine<void>::pull_type source{ std::bind(cooperative,std::placeholders::_1, 1)};

	std::cout << ", ";
	_callback->showMessage();
	//返回cooperative函数继续执行
	source();

	std::cout << "!";

	std::cout << "\n";
}
输出结果是: hello,world!                                               
2. 简单的协程库

https://github.com/tonbit/coroutine 是一个单文件协程库,在windows下使用的fiber实现,linux下是用的ucontext函数来实现的。非常简单易用.

thread_local static Ordinator ordinator
//coroutine.h文件中的101有一个这样的定义,需要去掉thread_local,否则多处调用很容易崩溃.
void cmd_drawline() //命令;
{
    AcDbPoint result_point;
    auto es= acedGetPoint("请输入起点坐标",&result_point);
    //在这个函数中,中断了命令drawline的执行,在不卡住主界面的情况下让用户输入所需要的值;
    AcDbPolylinePtr line=AcDbPolyline::createObject();
    if(es==eOk)
    {
         line->addPoint(result_point);
    }
    do {
       es= acedGetPoint("请输入下一个点坐标",&result_point);
       if(es==eNone)
       {
           break;
       }
       line->addPoint(result_point);
    } while(1);
}



enum InputType
{
    inputNothing,
    inputPoint,
    inputString,
};
struct CommandStatus
{
    //协程ID;
    int _coroutine_id;
    char* _command_name;
    //输入结果;
    QVariant _result_value;
    //输入类型;
    InputType _inut_type=inputNothing;
};

int acedGetPoint(const char* prompt,AcDbPoint* result)
{
    //获取当前的命令状态;
    CommandStatus&  command_status=getCurrentCommandStatus();
    //标记状态;
    command_status._inut_type=inputString;
    //挂起协程;
     coroutine::yield();
}

void Canvas::mousePress(QMouseEvent* event)
{
   if(event->inputType()==InputPoint)
   {
       //当这个事件结束之后就会退出acedGetPoint函数中的死循环;
       CommandStatus&  command_status=getCurrentCommandStatus();
       auto point=toScreen(event->pos());//将像素坐标转为地理坐标; 
       command_status._result_value=point;
       //恢复命令,coroutine_id是我们创建的协程ID;
       coroutine::resume(command_status._coroutine_id); 
   }
}

void execCommand(const char* command_name)
{
    //根据命令名,找到命令的回调函数执行;
    auto call_fun=CommandManager::instance().findCommand(command_name);
    call_fun();
}

//执行命令的主函数,非入口函数,这只是举个例子;
void main()
{
    auto rt1 = coroutine::create(std::bind(&execCommand, command_name));
    CommandStatus&  command_status=getCurrentCommandStatus();
    command_status._coroutine_id=rt1;
}

在C#中使用task await解决

在AutoCAD 2015版本之后协程的使用方式被放弃了,主要的原因是在C#中对协程的支持不好,不可调试,我自己使用是很容易崩溃。AutoCAD.Net使用的是Task await的C#关键字来实现,网上有一个C#实现的CAD系统就是这种方式,有兴趣的可以学习下.

simpleCAD https://github.com/oozcitak/SimpleCad
image.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

揽月凡尘

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值