特征(Action)执行流程
LibreCAD
界面的菜单栏或工具栏上的按钮代表着一个功能,例如画一个直线段,标注一个角度。这些功能在LibreCAD
中被称为Action
。因工作习惯的原因,更习惯于将这些功能称为特征,也有人将其叫成命令,这些说法都与LibreCAD
中的Action
意义相同,后文以特征为准。
特征类
特征类的基类是RS_ActionInterface
类,其重要函数和变量说明如下:
class RS_ActionInterface : public QObject, public RS_Snapper {
Q_OBJECT
public:
virtual RS2::ActionType rtti() const; // 获取当前特征的实际类型(动态类型识别的意思)
virtual void keyReleaseEvent(QKeyEvent* e);
virtual void coordinateEvent(RS_CoordinateEvent*); // 在视图中点击鼠标的响应函数
virtual void commandEvent(RS_CommandEvent*); // 从命令行启动当前特征的响应函数
virtual void trigger(); // 特征执行完成的响应函数
private:
int status = 0; // 表示特征执行到哪一步。在派生类中定义了枚举变量来表示各种状态。
protected:
bool finished = false;
RS_Graphic *graphic = nullptr; // 特征所在视图对象的指针
RS_Document *document = nullptr; // 特征所在文档对象的指针
RS2::ActionType actionType = RS2::ActionNone; // 存储当前特征的实际类型
};
特征类基类有一个重要的派生类是RS_PreviewActionInterface
类,该类表示可以预览的特征。预览的意思是在特征执行过程中,可以根据鼠标移动的位置,实时更新要生成的特征。例如绘制一个直线段时,当鼠标按下第一个点后,确定了直线段的起点,然后移动鼠标,会有一条从起点到鼠标当前为止的直线段,该直线段会跟随鼠标位置变化,这种效果称为预览。
可预览特征类中主要是添加了一个预览对象容器和一些针对预览的方法。
class RS_PreviewActionInterface : public RS_ActionInterface {
...
void drawPreview(); // 绘制预览容器中的实体
void deletePreview(); // 删除预览相关
protected:
std::unique_ptr<RS_Preview> preview; // 预览对象容器(RS_Preview是一个实体容器)
bool hasPreview = true; //whether preview is in use
};
在具体的特征类中,例如两点直线特征类,它继承于RS_PreviewActionInterface
,然后添加了用于后续创建两点实体的数据(起点和终点坐标值):
class RS_ActionDrawLine : public RS_PreviewActionInterface
{
Q_OBJECT
public:
enum Status { // 特征状态,表示特征执行到了哪一步
SetStartpoint, // 当前鼠标点击的位置将会被设置为两点直线的起点
SetEndpoint // 当前鼠标点击的位置将会被设置为两点直线的终点
};
...
protected:
struct Points; // 该数据结构每个具体的特征类都会定一个,记录实体的坐标参数,用于后续创建实体
std::unique_ptr<Points> pPoints; // 两点直线实体的数据
};
选中特征
在界面上的菜单或工具条上点击按钮即可选中一个特征,后续的鼠标在视图区域的操作都会被传递给特征的响应函数。
在LC_ActionFactory::fillActionContainer()
函数中,将界面上的每一个按钮(QAction
)与一个槽函数绑定,例如两点线段的按钮和槽函数绑定如下:
void LC_ActionFactory::fillActionContainer(QMap<QString, QAction*>& a_map, LC_ActionGroupManager* agm)
{
QAction* action;
...
action = new QAction(tr("&2 Points"), agm->line);
action->setIcon(QIcon(":/icons/line_2p.svg"));
connect(action, SIGNAL(triggered()), action_handler, SLOT(slotDrawLine()));
action->setObjectName("DrawLine");
a_map["DrawLine"] = action;
...
}
被绑定的槽函数是QG_ActionHandler
类的一个成员函数(槽函数),这个类的类似的槽函数里面只是简单地调用QG_ActionHandler::setCurrentAction()
函数设置当前选中的特征。
然后在QG_ActionHandler::setCurrentAction()
函数里面,会根据按钮的类型(传入的id
),创建一个特征对象,然后设置当前视图的当前特征为选中的特征。
RS_ActionInterface* QG_ActionHandler::setCurrentAction(RS2::ActionType id) {
...
switch (id) {
case RS2::ActionDrawLine:
a = new RS_ActionDrawLine(*document, *view); // 创建两点线段特征
break;
...
}
if (a) {
view->setCurrentAction(a); // 视图根据当前选中特征进行设置
}
...
}
在视图类里,借助事件处理类(RS_EventHandler
)来处理事件。
void RS_GraphicView::setCurrentAction(RS_ActionInterface* action) {
if (eventHandler) {
eventHandler->setCurrentAction(action);
}
}
事件处理类RS_EventHandler
是一个专门处理视图事件的类,相当于MVC
设计模式中的"Controller
",视图将用户的操作(点击鼠标、拿下键盘等)发送给文档(document
,也称为模型、数据库)。需要注意的是,每一个视图对象中都拥有一个事件处理类对象。
在RS_EventHandler::setCurrentAction()
函数中,首先是将当前特征加入到特征容器中,然后执行了被选中特征的init()
函数,对特征进行初始化。
void RS_EventHandler::setCurrentAction(RS_ActionInterface* action) {
...
currentActions.push_back(std::shared_ptr<RS_ActionInterface>(action));
action->init(); // 初始化选中特征
...
}
需要注意的是,没有专门设置一个变量来存储当前特征,而是通过访问特征容器currentActions
最后一个元素,作为当前特征。
特征接受输入
点击界面按钮后,激活了一个特征,后续在视图中的操作,都会调用该特征相应的响应函数。后续主要以椭圆(轴)特征为例进行说明。
在视图中的每一个操作,例如鼠标点击(主要)或按下按键等,都会转变成一个事件(这是利用Qt
的特性),例如鼠标按键弹起的事件传递过程为:QG_GraphicView->RS_EventHandler->RS_ActionDrawEllipseAxis
;
QG_GraphicView::mouseReleaseEvent(QMouseEvent* event)
RS_EventHandler::mouseReleaseEvent(QMouseEvent* e)
RS_ActionDrawEllipseAxis::mouseReleaseEvent(QMouseEvent* e) // 椭圆(轴)特征
进入到特征的mouseReleaseEvent()
函数中,将Qt
的鼠标事件转换成RS
坐标事件,实际上是为了提取鼠标点击的坐标值。然后进入到coordinateEvent()
函数中,在该函数中一步一步地将特征的参数设置完毕。
如果是在执行特征过程中(特征还未构建完毕),点击了鼠标右键,特征执行流程就回退一步。
void RS_ActionDrawEllipseAxis::mouseReleaseEvent(QMouseEvent* e) {
if (e->button()==Qt::LeftButton) { // 左键,将当前点作为Action输入
RS_CoordinateEvent ce(snapPoint(e)); // 事件转换。主要是为了提取鼠标点击坐标
coordinateEvent(&ce);
} else if (e->button()==Qt::RightButton) { // 右键,取消执行当前Action
deletePreview();
init(getStatus()-1); // 特征执行流程回退一步
}
}
特征执行
一个典型的coordinateEvent()
函数的结构如下:
void RS_ActionInterface::coordinateEvent(RS_CoordinateEvent* e) {
switch (getStatus()) {
case Status_1:
// do about Status_1 ...
setStatus(Status_2); // 切换为下一个状态Status_2
break;
case Status_2:
// do about Status_2 ...
setStatus(Status_3); // 切换为下一个状态Status_3
break;
case ...
case Status_end:
// do about Status_end ...
trigger(); // 特征构建完成,添加特征到数据库
//setStatus(Status_1); // 切换为初始个状态Status_1,连续执行同一个action
break;
default:
break;
}
}
首先在每个特征里面都会定义一些状态枚举变量,用于表示特征执行过程中的各个阶段。椭圆(轴)特征的状态枚举变量为:
enum Status {
SetCenter, // 设置椭圆的中心
SetMajor, // 设置椭圆的长轴
SetMinor, // 设置椭圆的短轴
SetAngle1, // 设置椭圆的起始角度(执行椭圆弧特征时使用)
SetAngle2 // 设置椭圆的终止角度(执行椭圆弧特征时使用)
};
同样在椭圆特征(长轴和短轴)中,首先是确定中点,然后确定长轴,最后确定短轴。确定短轴后,就会生成了一椭圆实体(具体是在trigger()
函数中完成)。
void RS_ActionDrawEllipseAxis::coordinateEvent(RS_CoordinateEvent* e) {
if (!e) return;
RS_Vector const& mouse = e->getCoordinate();// 鼠标点坐标转换
switch (getStatus()) {
case SetCenter:
pPoints->center = mouse; // 设置椭圆中心点
graphicView->moveRelativeZero(mouse);
setStatus(SetMajor);
break;
case SetMajor:
pPoints->m_vMajorP = mouse- pPoints->center;// 设置椭圆长轴
setStatus(SetMinor);
break;
case SetMinor: {
RS_Line line{pPoints->center-pPoints->m_vMajorP,
pPoints->center+pPoints->m_vMajorP}; // 构建短轴线
double d = line.getDistanceToPoint(mouse);
pPoints->ratio = d/(line.getLength()/2); // 设置椭圆离心率
if (!pPoints->isArc) {
trigger(); // 生成椭圆
setStatus(SetCenter); // 初始化状态枚举变量
} else {
setStatus(SetAngle1);
}
}
break;
...
default:
break;
}
}
完成特征
当一个特征的操作完成后,就会执行特征的trigger()
函数。在trigger()
函数中,首先会调用基类的trigger()
函数,做一些清理的工作。然后根据输入的特征数据,创建一个特征实体对象,并将该实体对象添加到实体容器中。紧接着刷新视图,将该实体显示在视图中。最后将特征执行状态设置为起始状态(默认行为),这样就可以继续执行当前的特征。
void RS_ActionDrawEllipseAxis::trigger()
{
RS_PreviewActionInterface::trigger(); // 调用基类方法,主要是清理预览图层
RS_Ellipse* ellipse = new RS_Ellipse { container, // 创建椭圆实体对象
{pPoints->center, pPoints->m_vMajorP, pPoints->ratio,
pPoints->angle1, pPoints->angle2, false}
};
...
container->addEntity(ellipse); // 将椭圆实体对象添加到容器中
graphicView->redraw(RS2::RedrawDrawing); // 重绘视图
setStatus(SetCenter); // 设置特征执行状态为起始状态,连续执行当前特征
...
}
总结
特征执行流程总结如下:
- 按下界面的一个按钮,启用一个特征(功能),后续鼠标、键盘的操作都是在逐步确定特征的参数。
- 界面交互采用
MVC
设计模式,鼠标在视图中的点击事件会先传递给事件处理类,由事件处理类分发给活动的特征。 - 在特征内部,记录了特征当前执行到了哪一步。特征执行完成后,就会生成一个与该特征对应的实体对象,并添加到实体容器中,然后重绘视图,即可显示所绘制的特征。