实体绘制流程
以绘制两点直线为例进行说明。两点直线的特征类是RS_ActionDrawLine
,在这个类里面实现了直线对象的创建。创建完成后就会通知视图进行重绘,然后就将实体显示在界面上了。
创建实体
进入绘制两点直线命令后,在void RS_ActionDrawLine::coordinateEvent(RS_CoordinateEvent* e)
中,当接收到第二个点的事件后,就会调用trigger()
函数,表示当前这个实体已经绘制完毕,需要生成实体,然后添加到文档(Document)中,最后进行重绘,以显示在桌面上。
void RS_ActionDrawLine::coordinateEvent(RS_CoordinateEvent* e)
{
...
switch (getStatus()) {
case SetStartpoint: // 起点,第一个点
...
break;
case SetEndpoint: // 终点,第二个点
if ((mouse-pPoints->data.startpoint).squared() > RS_TOLERANCE2) {
...
trigger(); // 绘制完毕,生成实体
...
}
break;
}
...
}
绘制事件
在trigger()
函数中,最重要的是调用了视图的redraw()
函数进行视图重绘。重绘函数中实际上只调用了update()
函数,这个函数是Qt
的QWidget
提供的界面刷新函数,调用这个函数后,会自动执行对应窗口的paintEvent()
函数。
void RS_ActionDrawLine::trigger()
{
RS_PreviewActionInterface::trigger();
RS_Line* line = new RS_Line(container, pPoints->data); // 直线实体对象
line->setLayerToActive();
line->setPenToActive();
container->addEntity(line); // 添加到容器中
...
graphicView->redraw(RS2::RedrawDrawing); // 重绘视图
...
}
paintEvent函数
首先是一个窗口中有三种类别的图层,实体在PixmapLayer2
图层上。在绘制实体前,需要将绘图设备(实际上是一个QPainter
)传递给绘图函数。这里的绘图设备还是RS_PainterQt
对象。
void QG_GraphicView::paintEvent(QPaintEvent *)
{
getPixmapForView(PixmapLayer1); // 背景,包括栅格
getPixmapForView(PixmapLayer2); // 实体
getPixmapForView(PixmapLayer3); // 预览,例如橡皮线
if (redrawMethod & RS2::RedrawGrid) // Draw Layer 1
{ ... }
if (redrawMethod & RS2::RedrawDrawing) // DRaw layer 2
{
...
PixmapLayer2->fill(Qt::transparent);
RS_PainterQt painter2(PixmapLayer2.get()); // 获取绘图对象
...
painter2.setDrawingMode(drawingMode);
painter2.setDrawSelectedOnly(false);
drawLayer2((RS_Painter*)&painter2); // 将绘图设备传递给实体绘制函数
painter2.setDrawSelectedOnly(true);
drawLayer2((RS_Painter*)&painter2);
painter2.end();
}
if (redrawMethod & RS2::RedrawOverlay) // Draw Layer 3
{ ... }
// Finally paint the layers back on the screen, bitblk to the rescue!
RS_PainterQt wPainter(this);
wPainter.drawPixmap(0,0,*PixmapLayer1);
wPainter.drawPixmap(0,0,*PixmapLayer2);
wPainter.drawPixmap(0,0,*PixmapLayer3);
wPainter.end();
redrawMethod=RS2::RedrawNone;
}
实际的绘图函数是drawLayer2()
,这个函数里面简单调用了RS_GraphicView::drawEntity()
,
void RS_GraphicView::drawLayer2(RS_Painter *painter)
{
drawEntity(painter, container); // Draw all entities.
...
}
然后在drawEntity()
中又调用了RS_GraphicView::drawEntityPlain()
,这个函数的重点是调用draw()
函数。drawEntityPlain()
函数中的第二个参数RS_Entity* e
实际上就是drawEntity(painter, container);
中的container
,也就是说此时的参数e
表示一个实体容器,而并非一个单纯的实体对象。从而表明上面标注*的行最终会调用RS_EntityContainer::draw()
;
void RS_GraphicView::drawEntityPlain(RS_Painter *painter, RS_Entity* e, double& patternOffset) {
if (!e) { return; }
if (!e->isContainer() && (e->isSelected()!=painter->shouldDrawSelected()))
{ return; }
e->draw(painter, this, patternOffset); // *
}
在RS_EntityContainer::draw()
函数中,遍历容器中的所有实体,执行各个实体的绘制工作。这里还是调用了RS_GraphicView::drawEntity()
,也就是说,还是会调用RS_GraphicView::drawEntityPlain()
,但是此时这个e
的实际类型已经不是一个容器,而是一个实体,例如直线、圆。
void RS_EntityContainer::draw(RS_Painter* painter, RS_GraphicView* view, double& /*patternOffset*/)
{
if (painter == nullptr || view == nullptr)
return;
foreach (auto* e, entities)
view->drawEntity(painter, e);
}
实体绘制
在第二次调用void RS_GraphicView::drawEntityPlain()
函数时,此时该函数的第二个参数e就是表示待绘制的实体,在这里是直线实体。所以就会调用RS_Line::draw()
函数,绘制直线。这里调用了RS_PainterQt
类进行绘制。RS_PainterQt
类是一个使用Qt
的QPainter
实现了各种实体(直线、圆弧、矩形)的绘图类。
void RS_Line::draw(RS_Painter* painter, RS_GraphicView* view, double& patternOffset) {
...
painter->drawLine(view->toGui(getStartpoint()), view->toGui(getEndpoint()));
...
}
void RS_PainterQt::drawLine(const RS_Vector& p1, const RS_Vector& p2)
{
PainterGuard painterGuard{*this};
QPainter::drawLine(toScreenX(p1.x), toScreenY(p1.y),
toScreenX(p2.x), toScreenY(p2.y));
}
总结
在LibreCAD
中,绘图的画布是QWidget
的派生类,绘图设备是QPainter
的派生类,利用Qt
提供的事件机制进行重绘事件通知。
有一点需要吐槽一下,容器类RS_EntityContainer
继承于实体类RS_Entity
,而直线类RS_Line
、圆类RS_Circle
等绘图实体类型也是继承于RS_Entity
,然后RS_EntityContainer
可以包含若干RS_Entity
对象。这种奇怪的设计不符合一般认知,造成了源码阅读困难,应予以修正。