矢量编辑的功能,是让GIS软件成为生产力工具所必备的基础功能。本文想跟大家探讨一下QGis二次开发中的添加矢量要素功能。
文章的示例工程地址在 https://github.com/Jacory/qgis_dev, 可fork自己的版本,并留意我不定时的更新
注意:本文开头部分代码比较多,篇幅比较长。虽然并非所有东西都与本文直接相关,但是我想通过前面的介绍,让大家对工具的功能实现有个基本的了解,这样自己扩展功能的时候才有明确的思路。
注意2: 本文目前还并没有完全写完,后文的UML图显示也不完整,按理说不应该直接发布出来。一来是最近实在比较忙,没有完整的时间来整理,二来我希望读到的朋友能够对我的文章结构以及叙述方式上给一些意见与建议,也便于我边写边修改,我认为这样对一篇长文的最终形成是非常有益的。
QgsMapTool
QgsMapTool 这个类定义为所有地图工具的抽象父类,因此,所有与地图交互操作的工具都应该继承自这个类。来看一下 QgsMapTool 类的定义,如下
class GUI_EXPORT QgsMapTool : public QObject
{
Q_OBJECT
public:
//! 虚析构函数
virtual ~QgsMapTool();
//! 鼠标移动事件. 默认不做任何响应.
virtual void canvasMoveEvent( QMouseEvent * e );
//! 鼠标双击事件. 默认不做任何响应.
virtual void canvasDoubleClickEvent( QMouseEvent * e );
//! 鼠标键按下事件. 默认不做任何响应.
virtual void canvasPressEvent( QMouseEvent * e );
//! 鼠标键释放事件. 默认不做任何响应.
virtual void canvasReleaseEvent( QMouseEvent * e );
//! 鼠标滚轮事件. 默认不做任何响应.
virtual void wheelEvent( QWheelEvent* e );
//! 键盘按下事件. 默认不做任何响应.
virtual void keyPressEvent( QKeyEvent* e );
//! 鼠标释放事件. 默认不做任何响应..
virtual void keyReleaseEvent( QKeyEvent* e );
#ifdef HAVE_TOUCH
//! 为触摸设备准备的手势事件. 默认不做任何响应.
virtual bool gestureEvent( QGestureEvent* e );
#endif
//! 渲染结束时调用. 默认不做任何响应.
//! 2.4以后的版本不再使用这个函数 -- 地图工具不应该直接跟地图渲染关联。
Q_DECL_DEPRECATED virtual void renderComplete();
/** 这个方法用来给地图工具挂上一个action,通过action来指定这个地图工具应该完成的操作。
void setAction( QAction* action );
/** 返回指定的action,没有则返回null */
QAction* action();
/** 给这个地图工具挂接上一个按钮对象*/
void setButton( QAbstractButton* button );
/** 返回指定的button,没有则返回null */
QAbstractButton* button();
/** 设置用户指定的鼠标形态 */
virtual void setCursor( QCursor cursor );
/** 判断这个地图工具是否是完成缩放或漫游的操作。如果是,就完成相应的缩放或漫游,并把工具切换到上一个工具状态。 */
virtual bool isTransient();
/** 判断这个工具是否具备编辑功能。如果是,当地图不处于编辑状态时,它将不可访问。*/
virtual bool isEditTool();
//! 当切换到当前工具时,就是activate状态,调用这个函数
virtual void activate();
//! 当从当前工具切换成别的工具时,调用这个函数
virtual void deactivate();
//! 返回地图画布指针
QgsMapCanvas* canvas();
//! 2.3版本之后添加,返回工具名称,并发送工具改变的信号(以上一个工具名称作为参数)。
QString toolName() { return mToolName; }
/** 这个是2.3版本后添加的,只有identify tool等工具才使用,其他暂时不需要关注。*/
static double searchRadiusMM();
/** 这个也是2.3版本后添加的,只有identify tool等工具才使用,其他暂时不需要关注。 */
static double searchRadiusMU( const QgsRenderContext& context );
/** 同样是2.3版本后添加的,只有identify tool等工具才使用,其他暂时不需要关注。 */
static double searchRadiusMU( QgsMapCanvas * canvas );
signals:
//! 发送一个消息
void messageEmitted( QString message, QgsMessageBar::MessageLevel = QgsMessageBar::INFO );
//! 发送清除消息信号
void messageDiscarded();
//! 当切换到该工具时会发送这个信号
void activated();
//! 当从该工具切换到别的工具时会发送这个信号
void deactivated();
private slots:
//! 用于当action被销毁时清除指针
void actionDestroyed();
protected:
//! 构造函数,传入地图画布指针作为参数
QgsMapTool( QgsMapCanvas* canvas );
//! 将点从屏幕坐标转换到地图坐标
QgsPoint toMapCoordinates( const QPoint& point );
//! 将点从屏幕坐标转换到图层坐标
QgsPoint toLayerCoordinates( QgsMapLayer* layer, const QPoint& point );
//! 将点从地图坐标转换到图层坐标(如果用了重投影,这两个坐标是不一样的)
QgsPoint toLayerCoordinates( QgsMapLayer* layer, const QgsPoint& point );
//! 将点从图层坐标转换到地图坐标(如果用了重投影,这两个坐标是不一样的)
QgsPoint toMapCoordinates( QgsMapLayer* layer, const QgsPoint& point );
//! 将矩形从地图坐标转到图层的坐标
QgsRectangle toLayerCoordinates( QgsMapLayer* layer, const QgsRectangle& rect );
//! 从地图坐标转换到屏幕坐标
QPoint toCanvasCoordinates( const QgsPoint& point );
//! 地图画布指针
QgsMapCanvas* mCanvas;
//! 指针形态
QCursor mCursor;
//! 工具关联的action
QAction* mAction;
//! 工具关联的button
QAbstractButton* mButton;
//! 工具名称
QString mToolName;
};
可以看到,在 QgsMapTool 类中主要定义的是鼠标\键盘操作的事件函数、地图工具的功能属性等。回想任意一个常用的地图工具,与地图做交互的主要是鼠标\键盘事件,然后这个工具可能还会有自己的鼠标指针图案、具有相应的名称、会有不用的切换状态,当然,与地图做交互自然要有各种地图坐标、屏幕坐标等等的转换功能。
同样,矢量图层的编辑工具也是一个地图工具,因此,以上这些属性它都具有。
QgsMapToolAdvancedDigitizing
再来看看 QgsMapToolAdvancedDigitizing 这个类。这个类继承自 QgsMapTool 类,并直接实现了它定义的事件响应。首先一个 QgsMapTool 的事件消息被捕获后,它的类型 QMouseEvent 会被转换为 QgsMapMouseEvent,这个事件消息会带上地图坐标信息,并且传递到 QgsMapTool 类的子类实例中。对应的子类通过实现 QgsMapTool 类的虚方法,来实现自己对事件处理的对应功能。而 QgsMapToolAdvancedDigitizing 其实是扮演一个地图数字化工具事件的响应者父类,为什么这么说?因为它也接收到消息以后也并没有直接定义响应方法,而是通过一个叫 QgsMapToolMapEventFilter 的类将地图事件过滤并重新封装,之后再传递给 QgsMapToolAdvancedDigitizing 的子类来实现。
来看看他的定义代码:
class APP_EXPORT QgsMapToolAdvancedDigitizing : public QgsMapTool
{
Q_OBJECT
public:
enum CaptureMode // 矢量化类型
{
CaptureNone, // 无
CapturePoint, // 点
CaptureLine, // 线
CapturePolygon // 面
};
//! 构造函数,接收 QgsMapCanvas 指针作为输入
explicit QgsMapToolAdvancedDigitizing( QgsMapCanvas* canvas );
~QgsMapToolAdvancedDigitizing();
//! 捕获鼠标按下事件,并转换为地图坐标后,传给虚方法响应
void canvasPressEvent( QMouseEvent* e ) override;
//! 捕获鼠标s事释放件,并转换为地图坐标后,传给虚方法响应
void canvasReleaseEvent( QMouseEvent* e ) override;
//! 捕获鼠标y事移动件,并转换为地图坐标后,传给虚方法响应
void canvasMoveEvent( QMouseEvent* e ) override;
//! 捕获鼠标s事双击件,并转换为地图坐标后,传给虚方法响应
void canvasDoubleClickEvent( QMouseEvent* e ) override;
//! 捕获键盘按下事件,传给虚方法响应
void keyPressEvent( QKeyEvent* event ) override;
//! 捕获键盘释放事件,传给虚方法响应
void keyReleaseEvent( QKeyEvent* event ) override;
//! 鼠标按下事件的响应虚方法,带地图坐标,可供子类实现并重写
virtual void canvasMapPressEvent( QgsMapMouseEvent* e );
//! 鼠标释放事件的响应虚方法,带地图坐标,可供子类实现并重写
virtual void canvasMapReleaseEvent( QgsMapMouseEvent* e );
//! 鼠标移动事件的响应虚方法,带地图坐标,可供子类实现并重写
virtual void canvasMapMoveEvent( QgsMapMouseEvent* e );
//! 鼠标双击事件的响应虚方法,带地图坐标,可供子类实现并重写
virtual void canvasMapDoubleClickEvent( QgsMapMouseEvent* e );
//! 键盘按下事件的响应虚方法,可供子类实现并重写
virtual void canvasKeyPressEvent( QKeyEvent* e );
//! 键盘释放事件的响应虚方法,可供子类实现并重写
virtual void canvasKeyReleaseEvent( QKeyEvent* e );
//! 如果允许使用CAD,返回true。(这个暂时可以不用关心)
bool cadAllowed() { return mCadAllowed; }
//! 返回矢量化的类型枚举
CaptureMode mode() { return mCaptureMode; }
protected:
//! 这个 dock widget 是用来装高级矢量化工具命令的
QgsAdvancedDigitizingDockWidget* mCadDockWidget;
bool mCadAllowed; // 是否允许使用CAD
CaptureMode mCaptureMode; // 矢量化类型枚举
// 以下四个定义矢量化时的捕捉触发方式
bool mSnapOnPress;
bool mSnapOnRelease;
bool mSnapOnMove;
bool mSnapOnDoubleClick;
};
看了代码,我们可以看到,这个类也是一个抽象类,那么到底实现矢量图层编辑的功能代码在哪里呢?我们往下看。
QgsMapToolEdit
这个类继承自 QgsMapToolAdvancedDigitizing ,是编辑矢量图层地图工具的父类。这次直接看代码:
class APP_EXPORT QgsMapToolEdit: public QgsMapToolAdvancedDigitizing
{
public:
//! 构造函数,接收 QgsMapCanvas 指针作为输入
QgsMapToolEdit( QgsMapCanvas* canvas );
virtual ~QgsMapToolEdit();
//! 由子类重写这个方法,可识别自己是不是一个编辑工具
virtual bool isEditTool() override { return true; }
protected:
// 新建一个 QgsRubberBand 图层,并制定它的颜色、线宽等属性
// alternativeBand 如果设为true,会显示更多的样式,
// 如透明度、线样式等,默认为false。
QgsRubberBand* createRubberBand( QGis::GeometryType geometryType = QGis::Line, bool alternativeBand = false );
/**返回地图控件中的当前图层,没有则返回0*/
QgsVectorLayer* currentVect