对矢量要素的编辑是 GIS 软件很重要的功能点之一,也是最难实现的功能点之一。编辑矢量要素涉及到很多方面的考虑,包括且不限于矢量要素的几何类型,拓扑关系,构成要素的节点的增删改,编辑会话 (session) 的启动、回溯和提交,要素属性的增删改等。本文不会也不可能涉及到属性编辑的方方面面,仅仅实现了一个添加面要素的地图工具,作抛砖引玉的作用。
我们预计实现如下需求:
- 参照 QGIS 和 ArcGIS,用一个按钮控制编辑会话的开始和结束,即控制图层处于编辑状态与否。按下表示处于编辑状态,弹起处于非编辑状态;
- 编辑状态下,激活“绘制多边形”按钮,点击激活添加多边形地图工具,弹起取消激活;
- 添加多边形地图工具激活时,用户可以在画布上点击绘制多边形:左键添加节点,右键结束当前多边形绘制。
运行效果
程序刚运行起来的效果如下,此时的绘制多边形按钮是点不了的,只有点击开始编辑之后,绘制多边形按钮才可见,再点击绘制多边形按钮,即可开始绘制。
绘制效果如下
代码解释
其实QGIS 提供了一个 QgsMapToolCapture类,可以实现上述上面绘制多边形的功能(这是 QGIS 软件自己所使用的地图工具),但不幸的是,这个工具的实例化需要引入 QgsAdvancedDigitizingDockWidget 类。
QgsMapToolCapture::QgsMapToolCapture(
QgsMapCanvas* canvas,
QgsAdvancedDigitizingDockWidget* cadDockWidget,
CaptureMode mode
)
从 QGIS 开发文档进入 qgsadvanceddigitizingdockwidget.h 源代码,可以发现一行 include
#include "ui_qgsadvanceddigitizingdockwidgetbase.h"
这个类是一个停靠窗口 QDockWidget,在编辑要素的过程中,会弹出这个界面显示一些信息。因此如果我们强行 include 这个类,编译器会提示找不到 ui_qgsadvanceddigitizingdockwidgetbase.h 导致编译错误。原因是,这个类是一个组件,它是自带 UI 的,QGIS 的源代码中提供了这个组件的 .ui 文件(如同我们自己用 Qt Designer 创建的 .ui 文件一样)。所以,要使用QgsMapToolCapture,我们必须从 GitHub 上下载 QgsAdvancedDigitizingDockWidget 的 .ui 文件 qgsadvanceddigitizingdockwidget.ui,用 uic 编译成 ui_qgsadvanceddigitizingdockwidgetbase.h,放到我们的源代码中,才可以通过编译。
然而事情并没有那么简单。如果用 Qt Designer 打开下载回来的 qgsadvanceddigitizingdockwidget.ui,会发现缺失一大堆资源文件,你用 uic 编译也会报同样的错(虽然也可以编译)。因为 QGIS 做的 UI 有不少共享的资源文件,如图标等,都存在 Qt Designer 的资源描述文件(.qrc)文件中。如果要完整编译你还得去把 QGIS 所有的资源文件下载回来。做这么多麻烦事的目的仅仅是让 QgsMapToolCapture 通过编译,有一点本末倒置的感觉。因此,作者决定放弃使用 QgsMapToolCapture 转而手动实现我们所需要的地图工具。
QgsMapToolCapture 的继承链为
逐级往上翻看源代码,发现 QgsAdvancedDigitizingDockWidget 是在 QgsMapToolAdvancedDigitizing 这一级引入的。因此我们可以直接继承 QgsMapToolEdit,QgsMapToolEdit又继承自QgsMapTool
QgsMapToolEdit 相对于基本的 QgsMapTool,额外实现了如下重要功能:
- currentVectorLayer(): 获取当前正在编辑的图层(即工具所属 QMapCanvas 的当前激活图层
- createRubberBand(): 可以直接从工具创建 QgsRubberBand,创建后自动附着于工具所属的 QMapCanvas上
这让我们可以比较方便的操作工具所属的图层和画布。我们创建一个 QgsMapToolEdit 的派生类AddPolygonTool,作为我们绘制多边形的地图工具。
AddPolygonTool.h
#pragma once
#include <qgsmaptooledit.h> // 用于编辑矢量几何图形的地图工具的基类
#include <qgsmapcanvas.h> // 画布
#include <qgsrubberband.h> // 用于在绘制折线或多边形时跟踪鼠标,记录绘制图形过程中的临时要素
#include <qgsmapmouseevent.h> // QGIS中的鼠标事件
class AddPolygonTool :
public QgsMapToolEdit
{
public:
AddPolygonTool(QgsMapCanvas* pMapCanvas);
// 清除当前的 RubberBand
void clearRubberBand();
protected:
// 重写 QgsMapTool 的鼠标移动事件
virtual void canvasMoveEvent(QgsMapMouseEvent *e);
// 重写 QgsMapTool 的鼠标点击事件
virtual void canvasPressEvent(QgsMapMouseEvent *e);
private:
// 当前正在工作的 RubberBand
QgsRubberBand* mpRubberBand = nullptr;
// 记录是否正在绘制中,构造函数中初始化为 false
bool mIsDrawing;
};
接下来重写鼠标点击事件,大体思路是:如当前无工作中的 RubberBand,则创建并存入 mpRubberBand 并点下第一个点。之后用户连续点击鼠标左键往 mpRubberBand 加入点,直到点击鼠标右键。点击鼠标右键表示停止绘制,如此时有效点数小于 3,不足以构成多边形,则丢弃,否则将 mpRubberBand 输出为新的 QgsFeature,加入受编辑的 QgsVectorLayer 之中。
// 重写QgsMapTool的鼠标点击事件
void AddPolygonTool::canvasPressEvent(QgsMapMouseEvent * e)
{
// 如果当前“橡皮筋”没有被创建
if (!mpRubberBand)
{
// 使用QGIS设置中的颜色/线宽创建一个“橡皮筋”,方法来自QgsMapToolEdit
mpRubberBand = createRubberBand(QgsWkbTypes::GeometryType::PolygonGeometry);
}
// 左键按下
if (e->button() == Qt::MouseButton::LeftButton)
{
mIsDrawing = true; // 开始绘制
mpRubberBand->addPoint(e->mapPoint()); // 向“橡皮筋”和更新画布添加一个顶点
}
// 右键按下
else if (e->button() == Qt::MouseButton::RightButton)
{
// 停止绘制
mIsDrawing = false;
// 如果“橡皮筋”中的顶点数大于3
if (mpRubberBand->numberOfVertices() >= 3)
{
// 创建一个QgsFeature来给当前矢量图层添加特征
QgsFeature f;
// QgsGeometry QgsRubberBand::asGeometry() const 返回“橡皮筋”当前对应的几何对象
f.setGeometry(mpRubberBand->asGeometry());
// QgsMapToolEdit的currentVectorLayer()返回值类型为QgsVectorLayer
// DefMainWindow.cpp中必须设置了当前图层才能用这个方法
currentVectorLayer()->addFeature(f);
// QgsMapCanvas * QgsMapTool::canvas() const,返回一个指向画布的指针
// void QgsMapCanvas::refresh() 重新绘制画布地图
canvas()->refresh();
}
// 绘制完毕清楚“橡皮筋”
clearRubberBand();
}
}
上述代码中,如果当前 RubberBand 内顶点数不小于 3,则符合多边形生成的条件。此时创建一个新的要素 (QgsFeature),将当前 RubberBand 绘制好的几何图形 (QgsGeometry 类型,通过 asGeometry() 方法获取) 赋予新建立的要素,并将此要素通过调用 QgsVectorLayer 的 addFeature() 方法,加入到图层之中。当前图层通过QgsMapToolEdit 的 currentVectorLayer() 获取。最后刷新画布。
最后,无论新要素是否生成,删除当前 RubberBand,准备下一个多边形的绘制。
删除当前RubberBand的代码如下
void AddPolygonTool::clearRubberBand()
{
// 若当前 RubberBand 为空则直接退出
if (!mpRubberBand)
{
return;
}
// 清除其内存并将指针置空
delete mpRubberBand;
mpRubberBand = nullptr;
}
然后,为了实现绘制的过程中,RubberBand 的最后一个点“跟着鼠标走”的效果,我们重写工具的鼠标移动事件,这样就可以实现绘制时的“动态”效果。:
// 重写QgsMapTool的鼠标移动事件
void AddPolygonTool::canvasMoveEvent(QgsMapMouseEvent * e)
{
// 如果mpRubberBand未被创建,或者当前未绘画
if (!mpRubberBand || !mIsDrawing)
{
return;
}
// “橡皮筋”最后一个点“跟着鼠标走”的效果,实现动态绘制
// e->mapPoint()是鼠标的最后位置
mpRubberBand->movePoint(e->mapPoint());
}
以上代码我们完成了自定义地图工具 AddPolygonTool 的编写。
接下来我们回到主程序窗体。为方便起见,这里我们创建一个“内存图层”用于编辑。内存图层是指不来源于任何外部数据。直接创建于内存之中的图层。在 QGIS 中通过 New Scratch Layer (草稿图层) 创建的图层就是内存图层。
内存图层的创建非常简单,在 URL 中通过正确的语法描述几何数据类型、坐标系、字段信息即可。具体可参考 QgsVectorLayer 的开发文档,写得十分详细。
主窗体代码头文件DefMainWindow.h内容如下
#pragma once
#include <qmainwindow.h>
#include "mainWindow.h"
#include "AddPolygonTool.h"
#include <qgsvectorlayer.h>
class DefMainWindow :
public QMainWindow
{
public:
DefMainWindow(QWidget * parent = nullptr);
private:
Ui::MainWindow ui;
QgsMapCanvas mCanvas; // 画布
QgsVectorLayer* mpStratchLayer = nullptr; // 内存图层
AddPolygonTool* mpToolAddPolygon = nullptr; // “添加多边形”地图工具
void onStartEditingButtonToggled(bool isChecked); // 开始编辑按钮的槽函数
void onDrawPolygonButtonToggled(bool isChecked); // 绘制多边形按钮的槽函数
};
接下来写主窗体的构造函数:
#include "DefMainWindow.h"
DefMainWindow::DefMainWindow(QWidget *parent) :
QMainWindow(parent),
mCanvas(this)
{
ui.setupUi(this);
ui.verticalLayout->addWidget(&mCanvas);
// 在内存中创建一个多边形图层,使用坐标系EPSG : 4326 (WGS 84), "memory" 表示内存图层
mpStratchLayer = new QgsVectorLayer("polygon?crs=epsg:4326", u8"临时面图层", "memory");
mCanvas.setLayers(QList<QgsMapLayer*>() << mpStratchLayer);
// 设置当前图层,因为AddPolygonTool.cpp中添加的特征是添加到当前图层,因此必须设置
mCanvas.setCurrentLayer(mpStratchLayer);
// 将画布缩放到 WGS 84 坐标系的边界范围,否则画布的初始范围与坐标系范围不符,会导致绘制出现问题
mCanvas.setExtent(QgsCoordinateReferenceSystem("EPSG:4326").bounds());
// 创建一个画多边形的自定义地图工具AddPolygonTool
mpToolAddPolygon = new AddPolygonTool(&mCanvas);
// 绑定“开始编辑”按钮和“绘制多边形”按钮点击事件
QObject::connect(ui.btnStartEditing, &QPushButton::toggled, this, &DefMainWindow::onStartEditingButtonToggled);
QObject::connect(ui.btnDrawPolygon, &QPushButton::toggled, this, &DefMainWindow::onDrawPolygonButtonToggled);
}
// 点击开始编辑按钮
void DefMainWindow::onStartEditingButtonToggled(bool checked)
{
// 如果按钮被按下
if (checked)
{
// 使图层可编辑
mpStratchLayer->startEditing();
// 使“绘制多边形”按钮可以点击
ui.btnDrawPolygon->setEnabled(true);
}
// 如果按钮被释放
else
{
// 向底层数据提供程序提交自上次调用startEditing()以来所做的任何缓冲更改
// 即保留当前已经编辑完毕的数据
mpStratchLayer->commitChanges();
// 设置“绘制多边形”按钮为释放状态
ui.btnDrawPolygon->setChecked(false);
// 设置“绘制多边形”按钮不可点击
ui.btnDrawPolygon->setEnabled(false);
}
}
// 点击绘制多边形按钮
void DefMainWindow::onDrawPolygonButtonToggled(bool checked)
{
// 如果按钮被按下
if (checked)
{
// 设置当前在画布上使用的地图工具
mCanvas.setMapTool(mpToolAddPolygon);
}
// 如果按钮被释放
else
{
// 清除当前的“橡皮筋”
mpToolAddPolygon->clearRubberBand();
// 取消设置当前地图工具或最后一个非缩放工具
mCanvas.unsetMapTool(mpToolAddPolygon);
}
}
运行程序,先点击“开始编辑”,再点击“绘制多边形”,然后在画布上点击就能绘制多边形了。点击右键结束当前多边形的绘制,绘制完成的多边形会自动变成临时图层的要素。注意我们并没有实现图层保存的功能,因此你退出程序之后图层就从内存里释放了。
参考文章 mriiiron’s blog