QGIS3.28的二次开发九:添加矢量要素

对矢量要素的编辑是 GIS 软件很重要的功能点之一,也是最难实现的功能点之一。编辑矢量要素涉及到很多方面的考虑,包括且不限于矢量要素的几何类型,拓扑关系,构成要素的节点的增删改,编辑会话 (session) 的启动、回溯和提交,要素属性的增删改等。本文不会也不可能涉及到属性编辑的方方面面,仅仅实现了一个添加面要素的地图工具,作抛砖引玉的作用。

我们预计实现如下需求:

  • 参照 QGIS 和 ArcGIS,用一个按钮控制编辑会话的开始和结束,即控制图层处于编辑状态与否。按下表示处于编辑状态,弹起处于非编辑状态;
  • 编辑状态下,激活“绘制多边形”按钮,点击激活添加多边形地图工具,弹起取消激活;
  • 添加多边形地图工具激活时,用户可以在画布上点击绘制多边形:左键添加节点,右键结束当前多边形绘制。

运行效果

程序刚运行起来的效果如下,此时的绘制多边形按钮是点不了的,只有点击开始编辑之后,绘制多边形按钮才可见,再点击绘制多边形按钮,即可开始绘制。
QGIS3.28的二次开发九:添加矢量要素_第1张图片
绘制效果如下
QGIS3.28的二次开发九:添加矢量要素_第2张图片

代码解释

其实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 的继承链为
QGIS3.28的二次开发九:添加矢量要素_第3张图片
逐级往上翻看源代码,发现 QgsAdvancedDigitizingDockWidget 是在 QgsMapToolAdvancedDigitizing 这一级引入的。因此我们可以直接继承 QgsMapToolEdit,QgsMapToolEdit又继承自QgsMapTool
QGIS3.28的二次开发九:添加矢量要素_第4张图片
QgsMapToolEdit 相对于基本的 QgsMapTool,额外实现了如下重要功能:

  • currentVectorLayer(): 获取当前正在编辑的图层(即工具所属 QMapCanvas 的当前激活图层
  • createRubberBand(): 可以直接从工具创建 QgsRubberBand,创建后自动附着于工具所属的 QMapCanvas上

这让我们可以比较方便的操作工具所属的图层和画布。我们创建一个 QgsMapToolEdit 的派生类AddPolygonTool,作为我们绘制多边形的地图工具。

AddPolygonTool.h

#pragma once
#include 	// 用于编辑矢量几何图形的地图工具的基类
#include 	// 画布
#include 	// 用于在绘制折线或多边形时跟踪鼠标,记录绘制图形过程中的临时要素
#include 	// 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 
#include "mainWindow.h"
#include "AddPolygonTool.h"
#include 

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

你可能感兴趣的:(QGIS,qgis)