看一段操作:
上面这个动画是我自己花了一周实现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下可跨语言交互,
- 使用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;
}
}
- 使用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();
}
}
- 在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++开源协程有 libgo
、libco
、boost coroutine2
、fibers
、C++20
、windows纤程等 ,对于做命令驱动程序而言,一般只能用有栈协程
, 无栈协程一般情况下协程函数一般情况下使用的是关键字来处理,有特定的返回值,也是通过语言语法糖来实现,局限性比较大(C++20的协程就是无栈协程)。
1. boost coroutine2
- 需要编译依赖boost的context模块,这里面没用到里面的fiber模块,fiber模块支持协程的调度。
- 封装到C#测试过,也支持协程的调用(某些协程跨语言调用就会出问题,如windows fiber在net2.0及以下版本可以用),也支持协程的调试。在net3.5版本中调试打断点会报调试异常(C#不支持协程调试)
- 这个库的使用相对来说还是不太友好,应用于交互系统中还是要写很多额外的代码。
#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