QGis二次开发基础 — 添加矢量要素功能

矢量编辑的功能,是让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* currentVectorLayer();

    /**给矢量要素添加节点,并保证拓扑关系正确。
       @param geom list of points (in layer coordinate system)
       @return 0 in case of success*/
    int addTopologicalPoints( const QList<QgsPoint>& geom );

    /**通过信息栏提示当前图层不是矢量图层 */
    void notifyNotVectorLayer();
    /**通过信息栏提示当前图层为不可编辑状态 */
    void notifyNotEditableLayer();
};

QgsMapToolCapture

class APP_EXPORT QgsMapToolCapture : public QgsMapToolEdit
{
    Q_OBJECT

  public:
    //! 构造函数,需要 QgsMapCanvas 指针,可以配置矢量化类型
    QgsMapToolCapture( QgsMapCanvas* canvas, CaptureMode mode = CaptureNone );

    virtual ~QgsMapToolCapture();

    //! 重写鼠标移动事件(注意这里的事件类别变成了 QgsMapMouseEvent )
    virtual void canvasMapMoveEvent( QgsMapMouseEvent* e ) override;

    //! 重写鼠标按下事件(注意这里的事件类别变成了 QgsMapMouseEvent )
    virtual void canvasMapPressEvent( QgsMapMouseEvent * e ) override;

    //! 重写键盘按下事件
    virtual void canvasKeyPressEvent( QKeyEvent* e ) override;

    //! 使工具变为非活动状态
    virtual void deactivate() override;

  public slots:
    //! 当前图层改变时触发这个函数
    void currentLayerChanged( QgsMapLayer *layer );
    //! 报错
    void addError( QgsGeometry::Error );
    //! 验证完毕时触发这个函数
    void validationFinished();

  protected:
    //! 返回下一个点的索引
    int nextPoint( const QgsPoint& mapPoint, QgsPoint& layerPoint );

    /** 添加一个地图坐标点到临时图层和矢量化列表,
    成功则返回0,当前图层不是矢量图层则返回1,坐标转换失败则返回2*/
    int addVertex( const QgsPoint& point );

    /**撤销上次添加点*/
    void undo();

    void startCapturing(); // 开始捕获
    bool isCapturing() const; // 是否正在捕获
    void stopCapturing(); // 停止捕获
    void deleteTempRubberBand(); // 删除临时图层

    //! 返回当前捕获列表大小
    int size() { return mCaptureList.size(); }
    //! 返回当前捕获列表头
    QList<QgsPoint>::iterator begin() { return mCaptureList.begin(); }
    //! 返回当前捕获列表尾
    QList<QgsPoint>::iterator end() { return mCaptureList.end(); }
    //! 返回当前捕获列表
    const QList<QgsPoint> &points() { return mCaptureList; }
    //! 返回捕获列表
    void setPoints( const QList<QgsPoint>& pointList ) { mCaptureList = pointList; }
    //! 封闭多边形
    void closePolygon();

  private:
    bool mCapturing; // 表明当前状态是在捕获

    /** 为线和多边形要素提供的临时图层*/
    QgsRubberBand* mRubberBand;

    /** 为线和多边形要素提供的,添加了最后一个鼠标点位置的临时矢量图层 */
    QgsRubberBand* mTempRubberBand;

    /** 用于存放线和多边形要素的捕获节点列表*/
    QList<QgsPoint> mCaptureList;

    //! 验证几何有效性
    void validateGeometry(); 
    QString mTip;
    QgsGeometryValidator *mValidator; // 验证器
    QList< QgsGeometry::Error > mGeomErrors; // 错误列表
    QList< QgsVertexMarker * > mGeomErrorMarkers; // 错误标记列表

    // 是否根据图层类型来判断矢量化类型。
    // 比如当前图层是线图层,那矢量化类型就应该是 CaptureLine 类型。
    bool mCaptureModeFromLayer; 
    // 捕捉标记
    QgsVertexMarker* mSnappingMarker;
};

QgsMapToolAddFeature

好了,接下来终于到正式添加要素的工具类了。QgsMapToolAddFeature 类继承自 QgsMapToolCapture 类,作用是添加一个新的点/线/多边形要素到一个已有矢量图层中。

来看定义代码:

class APP_EXPORT QgsMapToolAddFeature : public QgsMapToolCapture
{
    Q_OBJECT
  public:
    //! 构造函数,接收 QgsMapCanvas 指针作为输入
    QgsMapToolAddFeature( QgsMapCanvas* canvas );
    virtual ~QgsMapToolAddFeature();
    //! 重写鼠标指针释放事件
    void canvasMapReleaseEvent( QgsMapMouseEvent * e ) override;

    /** 添加要素函数
    传入 QgsVectorLayer 指针、当前要素,以及是否实时显示 */
    bool addFeature( QgsVectorLayer *vlayer, QgsFeature *f, bool showModal = true );

    //! 激活工具
    void activate() override;
};

小结

OK,我们来捋一捋。QgsMapTool 类定义了所有地图工具与用户交互的鼠标事件、键盘事件等,并包括了一些基本的通用方法。QgsMapToolAdvancedDigitizing 类继承自 QgsMapTool 类,并提供用于进行矢量化所需要的方法。QgsMapToolEdit 类又继承自 QgsMapToolAdvancedDigitizing 类,进一步定义了用于编辑图层所需的方法。最后 QgsMapToolAddFeature 类继承自 QgsMapToolEdit,并实现本文所关注的添加要素的方法,主要是在重写鼠标释放事件的函数中实现添加要素功能。

然后,我们通过一个时序图,来看一下添加一个矢量要素时,究竟发生了什么。
这里写图片描述

矢量图层添加要素功能实现

通过上文中对源码的剖析,现在要实现矢量图层添加要素的功能就有思路了。主要有两种办法:

  1. 自己定义一个类,继承自 QgsMapTool 并重写鼠标事件,在重写函数中,加入添加要素功能。
  2. 仿造 QGis 的模式,分别拷贝以上几个类,以及它们调用的其他类,来实现添加要素功能。

在以前的博客中,上面所讲的第二种方式都较为简单,但是本文的功能,要将所有相关类都拷贝过来,并理清它们之间的各种调用关系会稍微繁琐一点。如果你的关注点只是添加要素功能,其他编辑功能暂时不会使用,那么采用第一种方式是最直接,也是最简单的。但是如果你今后还需要添加其他的编辑功能,那我建议还是做第二种方法,毕竟以后的功能实现会更有章法,更轻松一点。

下面将会分别讲解第一种和第二种方法的具体实现方式,并提供示例代码,供大家使用。

第一种方法

在上文的源码剖析中,我们注意到,添加要素功能需要重写 QgsMapTool 的鼠标释放事件。那么我们需要做的就是定义一个类,直接继承自 QgsMapTool 类,并重写它的鼠标释放事件,来获取用户点击在地图画布上的位置。

class qgis_dev_addFeatureTool : public QgsMapTool
{
    Q_OBJECT

public:
    qgis_dev_addFeatureTool( QgsMapCanvas* mapCanvas );
    ~qgis_dev_addFeatureTool();

    //! 重写鼠标指针释放事件
    void canvasReleaseEvent( QMouseEvent* e ) override;
};

当然,我们需要在主界面触发这个工具类,我们在菜单条上新建了一个工具,叫 Add Feature,如下图所示

这里写图片描述

然后我们还需要在代码中绑定上这个工具的触发事件

void qgis_dev::on_actionAdd_Feature_triggered()
{
    QgsMapTool* addFeatureTool = new qgis_dev_addFeatureTool( m_mapCanvas );
    m_mapCanvas->setMapTool( addFeatureTool );
}

好了,现在开始实现工具的功能。我们知道,添加矢量要素,实际上调用的是 QgsVectorLayer 类的 addFeature() 方法。我们现在不考虑任何可能的bug与设计模式,仅仅考虑添加一个自定义点到这个矢量图层上,那么我们的代码就写成:

void qgis_dev_addFeatureTool::canvasReleaseEvent( QMouseEvent* e )
{
    // 获取当前图层
    QgsVectorLayer* layer = qobject_cast<QgsVectorLayer*>( mCanvas->currentLayer() );
    // 判断当前图层是否为矢量图层
    if( !layer ) {emit messageEmitted( tr( "not a valid vector layer." ) ); return;}
    // 判断当前图层是否可编辑
    if( !layer->isEditable() ) {emit messageEmitted( tr( "can't edit this layer." ) ); return;}

    // 得到点坐标,转换为地图坐标
    QgsPoint savePoint = toLayerCoordinates( layer, mCanvas->mapSettings().mapToPixel().toMapCoordinates( e->pos() ) );

    switch( layer->geometryType() )
    {
    case QGis::Point:
        m_captureMode = CapturePoint;
        break;
    case QGis::Line:
        m_captureMode = CaptureLine;
        break;
    case QGis::Polygon:
        m_captureMode = CapturePolygon;
        break;
    default:
        break;
    }

    // 转换为geometry
    QgsGeometry* g = 0;
    if ( m_captureMode == CapturePoint ) // 先考虑点的情况
    {
        if ( layer->wkbType() == QGis::WKBPoint || layer->wkbType() == QGis::WKBPoint25D )
        {
            g = QgsGeometry::fromPoint( savePoint );
        }
        else if( layer->wkbType() == QGis::WKBMultiPoint || layer->wkbType() == QGis::WKBMultiPoint25D )
        {
            g = QgsGeometry::fromMultiPoint( QgsMultiPoint() << savePoint );
        }
    }
    else if ( m_captureMode == CaptureLine )
    {

    }
    else if ( m_captureMode == CapturePolygon )
    {

    }

    // 转换为feature
    QgsFeature feature( layer->pendingFields(), 0 );
    feature.setGeometry( g );

    layer->addFeature( feature, true );
    mCanvas->setExtent( layer->extent() );
    mCanvas->refresh();

直接编译运行,并运用我们上一次讲到的矢量图层的建立功能,新建一个空的点矢量图层。然和我们就可以开始在这个图层上画点了。
这里写图片描述

留意到,上面的方法中,并没有实现线和多边形的添加。现在来讲解这两种类型。首先是线要素,添加一个线要素的步骤如下:

  1. 鼠标左键单击第一个点
  2. 鼠标左键单击第二个点
  3. 鼠标左键单击第三个点
  4. ……
  5. 鼠标右键完成

因此,想到要加入对鼠标按键的判断,并且还需要有一个数据结构来存储添加进来的这些点。在头文件里面定义这个存储结构为

QList<QgsPoint> mCaptureList;
  •  

现在来完成上面定义的那几个步骤

// 接鼠标释放事件重写函数
else if ( m_captureMode == CaptureLine )
{
    if ( e->button() == Qt::LeftButton ) // 鼠标左键
        {
            m_captureList.append( mCanvas->mapSettings().mapToPixel().toMapCoordinates( e->pos() ) );
        }
        else if ( e->button() == Qt::RightButton ) // 鼠标右键
        {
            if ( m_captureList.size() < 2 ) { return; }
            if ( layer->wkbType() == QGis::WKBLineString || layer->wkbType() == QGis::WKBLineString25D )
            {
                g = QgsGeometry::fromPolyline( m_captureList.toVector() );
            }
            else if ( layer->wkbType() == QGis::WKBMultiLineString || layer->wkbType() == QGis::WKBMultiLineString25D )
            {
                g = QgsGeometry::fromMultiPolyline( QgsMultiPolyline() << m_captureList.toVector() );
            }
        }
}
  •  

这样,新建一个线图层,然后打开添加要素工具,并随便用左键点三个点,最后右键点一下(因为没有rubber band,点击的时候看不到点的实时刷新)。最后,会看到如下图的效果。

这里写图片描述

最后一个是多边形的添加了,跟线图层其实挺像的。

// 接鼠标释放事件重写函数
if ( e->button() == Qt::LeftButton ) // 鼠标左键
        {
            m_captureList.append( mCanvas->mapSettings().mapToPixel().toMapCoordinates( e->pos() ) );
        }
        else if ( e->button() == Qt::RightButton ) // 鼠标右键
        {
            if ( m_captureList.size() < 3 ) { return; }

            if ( layer->wkbType() == QGis::WKBPolygon ||  layer->wkbType() == QGis::WKBPolygon25D )
            {
                g = QgsGeometry::fromPolygon( QgsPolygon() << m_captureList.toVector() );
            }
            else if ( layer->wkbType() == QGis::WKBMultiPolygon ||  layer->wkbType() == QGis::WKBMultiPolygon25D )
            {
                g = QgsGeometry::fromMultiPolygon( QgsMultiPolygon() << ( QgsPolygon() << m_captureList.toVector() ) );
            }
        }

同样,新建一个多边形图层,然后随便点三个点,并以右键结束,会得到如下效果

这里写图片描述

至此,核心的功能就讲解完了。但是如果代码里面光是这样写的话bug就会很多,体验也不会好。因此我下面给出比较完整的示例代码供大家测试使用,虽然会感觉比上面的代码多了好多东西,但是核心是不变的,增加的代码只不过是为了完善而已。

首先是完整的 .h 文件

#ifndef QGIS_DEV_ADDFEATURETOOL_H
#define QGIS_DEV_ADDFEATURETOOL_H

#include <QObject>
#include <QMouseEvent>
#include <QList>

#include <qgsmaptool.h>
#include <qgsmapcanvas.h>
#include "qgsmapmouseevent.h"
#include <qgsvectorlayer.h>
#include "qgsfeature.h"
#include <qgsgeometryvalidator.h>
#include <qgsvertexmarker.h>
#include "qgsrubberband.h"
#include "qgis.h"

class qgis_dev_addFeatureTool : public QgsMapTool
{
    Q_OBJECT

public:
    qgis_dev_addFeatureTool( QgsMapCanvas* mapCanvas );
    ~qgis_dev_addFeatureTool();

    enum CaptureMode // 矢量化类型
    {
        CaptureNone,       // 无
        CapturePoint,      // 点
        CaptureLine,       // 线
        CapturePolygon     // 面
    };

    //! 重写鼠标指针释放事件
    void canvasReleaseEvent( QMouseEvent* e ) override;

    //! 添加要素函数,传入 QgsVectorLayer 指针、当前要素,以及是否实时显示
    bool addFeature( QgsVectorLayer *vlayer, QgsFeature *f, bool showModal = true );

    //! 激活工具
    void activate() override;

    //! 获取捕获状态
    CaptureMode mode();

    //! 返回当前捕获列表大小
    int size() { return m_captureList.size(); }
    const QList<QgsPoint> &points() { return m_captureList; }

private:

    void notifyNotVectorLayer();
    void notifyNotEditableLayer();
    int addVertex( const QgsPoint& point );
    int nextPoint( const QgsPoint& mapPoint, QgsPoint& layerPoint );
    QgsRubberBand* createRubberBand( QGis::GeometryType geometryType = QGis::Line, bool alternativeBand = false );

    void startCapturing();
    void stopCapturing();
    void deleteTempRubberBand();

    /** 需要自己维护 captureList*/
    QList<QgsPoint> m_captureList;
    CaptureMode m_captureMode;
    bool mCapturing;
    /** rubber band for polylines and polygons */
    QgsRubberBand* mRubberBand;

    /** temporary rubber band for polylines and polygons. this connects the last added point to the mouse cursor position */
    QgsRubberBand* mTempRubberBand;

    QString mTip;
    QgsGeometryValidator *mValidator;
    QList< QgsGeometry::Error > mGeomErrors;
    QList< QgsVertexMarker * > mGeomErrorMarkers;

    bool mCaptureModeFromLayer;

    void validateGeometry();

    QgsVertexMarker* mSnappingMarker;
};

#endif // QGIS_DEV_ADDFEATURETOOL_H

然后是 .cpp 文件

#include "qgis_dev_addfeaturetool.h"
#include "qgis_dev.h"
#include <QStringList>

#include <qgsvectorlayer.h>
#include <qgslogger.h>
#include <qgsvectordataprovider.h>
#include "qgscsexception.h"
#include "qgsproject.h"
#include "qgsmaplayerregistry.h"
#include "qgsmaptopixel.h"
#include "qgsgeometry.h"
#include "qgsfeature.h"


qgis_dev_addFeatureTool::qgis_dev_addFeatureTool( QgsMapCanvas* mapCanvas )
    : QgsMapTool( mapCanvas )
{
    mToolName = tr( "Add Feature" );
    mRubberBand = 0;
    mTempRubberBand = 0;
    mValidator = 0;
}

qgis_dev_addFeatureTool::~qgis_dev_addFeatureTool()
{

}


bool qgis_dev_addFeatureTool::addFeature( QgsVectorLayer *layer, QgsFeature *feature, bool showModal /*= true */ )
{
    if ( !layer || !layer->isEditable() ) {return false;}

    QgsAttributeMap defaultAttributes;

    QgsVectorDataProvider *provider = layer->dataProvider();

    QSettings settings;
    bool reuseLastValues = settings.value( "/qgis/digitizing/reuseLastValues", false ).toBool();
    QgsDebugMsg( QString( "reuseLastValues: %1" ).arg( reuseLastValues ) );

    // add the fields to the QgsFeature
    const QgsFields& fields = layer->pendingFields();
    feature->initAttributes( fields.count() );
    for ( int idx = 0; idx < fields.count(); ++idx )
    {
        QVariant v = provider->defaultValue( idx );
        feature->setAttribute( idx, v );
    }

    //show the dialog to enter attribute values
    //only show if enabled in settings and layer has fields
    bool isDisabledAttributeValuesDlg = ( fields.count() == 0 ) || settings.value( "/qgis/digitizing/disable_enter_attribute_values_dialog", false ).toBool();

    // override application-wide setting with any layer setting
    switch ( layer->featureFormSuppress() )
    {
    case QgsVectorLayer::SuppressOn:
        isDisabledAttributeValuesDlg = true;
        break;
    case QgsVectorLayer::SuppressOff:
        isDisabledAttributeValuesDlg = false;
        break;
    case QgsVectorLayer::SuppressDefault:
        break;
    }
    if ( isDisabledAttributeValuesDlg )
    {
        layer->beginEditCommand( "" );
        bool mFeatureSaved = layer->addFeature( *feature );

        if ( mFeatureSaved )
        {
            layer->endEditCommand();
        }
        else
        {
            layer->destroyEditCommand();
        }
    }
    else
    {
        // 这里添加代码,做增加要素时填写属性

    }
}

void qgis_dev_addFeatureTool::canvasReleaseEvent( QMouseEvent* e )
{
    // 获取当前图层
    QgsVectorLayer* layer = qobject_cast<QgsVectorLayer*>( mCanvas->currentLayer() );

    // 判断当前图层是否为矢量图层
    if( !layer ) {emit messageEmitted( tr( "not a valid vector layer." ) ); return;}

    // 判断数据驱动状态
    QgsVectorDataProvider* provider = layer->dataProvider();
    if ( !( provider->capabilities() & QgsVectorDataProvider::AddFeatures ) )
    {
        emit messageEmitted(
            tr( "The data provider for this layer does not support the addition of features." ),
            QgsMessageBar::WARNING );
        return;
    }

    // 判断当前图层是否可编辑
    if( !layer->isEditable() ) {emit messageEmitted( tr( "can't edit this layer." ) ); return;}

    // 得到点坐标,转换为地图坐标
    QgsPoint savePoint;
    try
    {
        savePoint = toLayerCoordinates( layer, mCanvas->mapSettings().mapToPixel().toMapCoordinates( e->pos() ) );
        QgsDebugMsg( "savePoint = " + savePoint.toString() );
    }
    catch ( QgsCsException &cse )
    {
        Q_UNUSED( cse );
        emit messageEmitted( tr( "Cannot transform the point to the layers coordinate system" ), QgsMessageBar::WARNING );
        return;
    }


    switch( layer->geometryType() )
    {
    case QGis::Point:
        m_captureMode = CapturePoint;
        break;
    case QGis::Line:
        m_captureMode = CaptureLine;
        break;
    case QGis::Polygon:
        m_captureMode = CapturePolygon;
        break;
    default:
        break;
    }

    QgsGeometry* g = 0; // 新建一个geometry
    if ( m_captureMode == CapturePoint )
    {
        // 转换为geometry
        if ( layer->wkbType() == QGis::WKBPoint || layer->wkbType() == QGis::WKBPoint25D )
        {
            g = QgsGeometry::fromPoint( savePoint );
        }
        else if( layer->wkbType() == QGis::WKBMultiPoint || layer->wkbType() == QGis::WKBMultiPoint25D )
        {
            g = QgsGeometry::fromMultiPoint( QgsMultiPoint() << savePoint );
        }

        // 转换为feature
        QgsFeature feature( layer->pendingFields(), 0 );
        feature.setGeometry( g );
        addFeature( layer, &feature, false );
        //layer->addFeature( feature, true );
        mCanvas->setExtent( layer->extent() );
        mCanvas->refresh();
    }
    else if ( m_captureMode == CaptureLine || m_captureMode == CapturePolygon )
    {
        if ( e->button() == Qt::LeftButton ) // 鼠标左键
        {
            int error = addVertex( mCanvas->mapSettings().mapToPixel().toMapCoordinates( e->pos() ) );
            if ( error == 1 ) {return;} // current layer is not a vector layer
            else if ( error == 2 ) // problem with coordinate transformation
            {
                emit messageEmitted( tr( "Cannot transform the point to the layers coordinate system" ), QgsMessageBar::WARNING );
                return;
            }
            startCapturing();
        }
        else if ( e->button() == Qt::RightButton ) // 鼠标右键
        {
            deleteTempRubberBand();
            if ( m_captureMode == CaptureLine && m_captureList.size() < 2 ) { return; }
            if ( m_captureMode == CapturePolygon && m_captureList.size() < 3 ) { return; }

            QgsFeature* feature = new QgsFeature( layer->pendingFields(), 0 );
            QgsGeometry* g = 0; // 新建一个geometry
            if ( m_captureMode == CaptureLine )
            {
                if ( layer->wkbType() == QGis::WKBLineString || layer->wkbType() == QGis::WKBLineString25D )
                {
                    g = QgsGeometry::fromPolyline( m_captureList.toVector() );
                }
                else if ( layer->wkbType() == QGis::WKBMultiLineString || layer->wkbType() == QGis::WKBMultiLineString25D )
                {
                    g = QgsGeometry::fromMultiPolyline( QgsMultiPolyline() << m_captureList.toVector() );
                }
                else
                {
                    emit messageEmitted( tr( "Cannot add feature. Unknown WKB type" ), QgsMessageBar::CRITICAL );
                    stopCapturing();
                    delete feature;
                    return;
                }
                feature->setGeometry( g );
            }
            else if ( m_captureMode == CapturePolygon )
            {
                if ( layer->wkbType() == QGis::WKBPolygon ||  layer->wkbType() == QGis::WKBPolygon25D )
                {
                    g = QgsGeometry::fromPolygon( QgsPolygon() << m_captureList.toVector() );
                }
                else if ( layer->wkbType() == QGis::WKBMultiPolygon ||  layer->wkbType() == QGis::WKBMultiPolygon25D )
                {
                    g = QgsGeometry::fromMultiPolygon( QgsMultiPolygon() << ( QgsPolygon() << m_captureList.toVector() ) );
                }
                else
                {
                    emit messageEmitted( tr( "Cannot add feature. Unknown WKB type" ), QgsMessageBar::CRITICAL );
                    stopCapturing();
                    delete feature;
                    return;
                }

                if ( !g )
                {
                    stopCapturing();
                    delete feature;
                    return; // invalid geometry; one possibility is from duplicate points
                }
                feature->setGeometry( g );

                int avoidIntersectionsReturn = feature->geometry()->avoidIntersections();
                if ( avoidIntersectionsReturn == 1 )
                {
                    //not a polygon type. Impossible to get there
                }
#if 0
                else if ( avoidIntersectionsReturn == 2 ) //MH120131: disable this error message until there is a better way to cope with the single type / multi type problem
                {
                    //bail out...
                    emit messageEmitted( tr( "The feature could not be added because removing the polygon intersections would change the geometry type" ), QgsMessageBar::CRITICAL );
                    delete feature;
                    stopCapturing();
                    return;
                }
#endif
                else if ( avoidIntersectionsReturn == 3 )
                {
                    emit messageEmitted( tr( "An error was reported during intersection removal" ), QgsMessageBar::CRITICAL );
                }

                if ( !feature->geometry()->asWkb() ) //avoid intersection might have removed the whole geometry
                {
                    QString reason;
                    if ( avoidIntersectionsReturn != 2 )
                    {
                        reason = tr( "The feature cannot be added because it's geometry is empty" );
                    }
                    else
                    {
                        reason = tr( "The feature cannot be added because it's geometry collapsed due to intersection avoidance" );
                    }
                    emit messageEmitted( reason, QgsMessageBar::CRITICAL );
                    delete feature;
                    stopCapturing();
                    return;
                }
            }

            if ( addFeature( layer, feature, false ) )
            {
                //add points to other features to keep topology up-to-date
                int topologicalEditing = QgsProject::instance()->readNumEntry( "Digitizing", "/TopologicalEditing", 0 );

                //use always topological editing for avoidIntersection.
                //Otherwise, no way to guarantee the geometries don't have a small gap in between.
                QStringList intersectionLayers = QgsProject::instance()->readListEntry( "Digitizing", "/AvoidIntersectionsList" );
                bool avoidIntersection = !intersectionLayers.isEmpty();
                if ( avoidIntersection ) //try to add topological points also to background layers
                {
                    QStringList::const_iterator lIt = intersectionLayers.constBegin();
                    for ( ; lIt != intersectionLayers.constEnd(); ++lIt )
                    {
                        QgsMapLayer* ml = QgsMapLayerRegistry::instance()->mapLayer( *lIt );
                        QgsVectorLayer* vl = qobject_cast<QgsVectorLayer*>( ml );
                        //can only add topological points if background layer is editable...
                        if ( vl && vl->geometryType() == QGis::Polygon && vl->isEditable() )
                        {
                            vl->addTopologicalPoints( feature->geometry() );
                        }
                    }
                }
                else if ( topologicalEditing )
                {
                    layer->addTopologicalPoints( feature->geometry() );
                }
            }

            stopCapturing();
        }
    }
}

void qgis_dev_addFeatureTool::activate()
{
    QgsVectorLayer *layer = qobject_cast<QgsVectorLayer *>( mCanvas->currentLayer() );
    if ( layer && layer->geometryType() == QGis::NoGeometry )
    {
        QgsFeature f;
        addFeature( layer, &f, false );
        return;
    }

    QgsMapTool::activate();
}

void qgis_dev_addFeatureTool::notifyNotVectorLayer()
{
    emit messageEmitted( tr( "No active vector layer" ) );
}

void qgis_dev_addFeatureTool::notifyNotEditableLayer()
{
    emit messageEmitted( tr( "Layer not editable" ) );
}

qgis_dev_addFeatureTool::CaptureMode qgis_dev_addFeatureTool::mode()
{
    return m_captureMode;
}

int qgis_dev_addFeatureTool::addVertex( const QgsPoint& point )
{
    if ( mode() == CaptureNone ) { QgsDebugMsg( "invalid capture mode" ); return 2;}

    QgsPoint layerPoint;
    int res = nextPoint( point, layerPoint );
    if ( res != 0 ) {return res;} // 当前点必须是最后一个点

    if ( !mRubberBand ) // 没有rubber band,就创建一个
    {
        mRubberBand = createRubberBand( m_captureMode == CapturePolygon ? QGis::Polygon : QGis::Line );
    }
    mRubberBand->addPoint( point );
    m_captureList.append( layerPoint );

    if ( !mTempRubberBand )
    {
        mTempRubberBand = createRubberBand( m_captureMode == CapturePolygon ? QGis::Polygon : QGis::Line, true );
    }
    else
    {
        mTempRubberBand->reset( m_captureMode == CapturePolygon ? true : false );
    }

    if ( m_captureMode == CaptureLine )
    {
        mTempRubberBand->addPoint( point );
    }
    else if ( m_captureMode == CapturePolygon )
    {
        const QgsPoint *firstPoint = mRubberBand->getPoint( 0, 0 );
        mTempRubberBand->addPoint( *firstPoint );
        mTempRubberBand->movePoint( point );
        mTempRubberBand->addPoint( point );
    }

    validateGeometry(); // 验证几何有效性

    return 0;
}

void qgis_dev_addFeatureTool::startCapturing()
{
    mCapturing = true;
}

void qgis_dev_addFeatureTool::deleteTempRubberBand()
{
    if ( mTempRubberBand )
    {
        delete mTempRubberBand;
        mTempRubberBand = 0;
    }
}

void qgis_dev_addFeatureTool::stopCapturing()
{
    if ( mRubberBand )
    {
        delete mRubberBand;
        mRubberBand = 0;
    }

    if ( mTempRubberBand )
    {
        delete mTempRubberBand;
        mTempRubberBand = 0;
    }

    while ( !mGeomErrorMarkers.isEmpty() )
    {
        delete mGeomErrorMarkers.takeFirst();
    }

    mGeomErrors.clear();

    mCapturing = false;
    m_captureList.clear();
    mCanvas->refresh();
}

int qgis_dev_addFeatureTool::nextPoint( const QgsPoint& mapPoint, QgsPoint& layerPoint )
{
    QgsVectorLayer *vlayer = qobject_cast<QgsVectorLayer *>( mCanvas->currentLayer() );
    if ( !vlayer ) { QgsDebugMsg( "no vector layer" ); return 1;}
    try
    {
        layerPoint = toLayerCoordinates( vlayer, mapPoint ); //transform snapped point back to layer crs
    }
    catch ( QgsCsException &cse )
    {
        Q_UNUSED( cse );
        QgsDebugMsg( "transformation to layer coordinate failed" );
        return 2;
    }

    return 0;
}

QgsRubberBand* qgis_dev_addFeatureTool::createRubberBand( QGis::GeometryType geometryType /*= QGis::Line*/, bool alternativeBand /*= false */ )
{
    QSettings settings;
    QgsRubberBand* rb = new QgsRubberBand( mCanvas, geometryType );
    rb->setWidth( settings.value( "/qgis/digitizing/line_width", 1 ).toInt() );
    QColor color( settings.value( "/qgis/digitizing/line_color_red", 255 ).toInt(),
                  settings.value( "/qgis/digitizing/line_color_green", 0 ).toInt(),
                  settings.value( "/qgis/digitizing/line_color_blue", 0 ).toInt() );
    double myAlpha = settings.value( "/qgis/digitizing/line_color_alpha", 200 ).toInt() / 255.0;
    if ( alternativeBand )
    {
        myAlpha = myAlpha * settings.value( "/qgis/digitizing/line_color_alpha_scale", 0.75 ).toDouble();
        rb->setLineStyle( Qt::DotLine );
    }
    if ( geometryType == QGis::Polygon )
    {
        color.setAlphaF( myAlpha );
    }
    color.setAlphaF( myAlpha );
    rb->setColor( color );
    rb->show();
    return rb;
}

void qgis_dev_addFeatureTool::validateGeometry()
{
    QSettings settings;
    if ( settings.value( "/qgis/digitizing/validate_geometries", 1 ).toInt() == 0 ) {return;}

    if ( mValidator )
    {
        mValidator->deleteLater();
        mValidator = 0;
    }

    mTip = "";
    mGeomErrors.clear();
    while ( !mGeomErrorMarkers.isEmpty() )
    {
        delete mGeomErrorMarkers.takeFirst();
    }

    QgsGeometry *g = 0;
    switch ( m_captureMode )
    {
    case CaptureNone:
    case CapturePoint:
        return;

    case CaptureLine:
        if ( m_captureList.size() < 2 ) {return;}
        g = QgsGeometry::fromPolyline( m_captureList.toVector() );
        break;

    case CapturePolygon:
        if ( m_captureList.size() < 3 ) {return;}
        g = QgsGeometry::fromPolygon( QgsPolygon() << ( QgsPolyline() << m_captureList.toVector() << m_captureList[0] ) );
        break;
    }

    if ( !g ) {return;}

    mValidator = new QgsGeometryValidator( g );
    connect( mValidator, SIGNAL( errorFound( QgsGeometry::Error ) ), this, SLOT( addError( QgsGeometry::Error ) ) );
    connect( mValidator, SIGNAL( finished() ), this, SLOT( validationFinished() ) );
    mValidator->start();

    QStatusBar *sb = qgis_dev::instance()->statusBar();
    sb->showMessage( tr( "Validation started." ) );
    delete g;
}

把上面的类文件复制,并根据实际情况做适当的依赖项修改,添加到主界面事件后,会得到下图的运行效果:

这里写图片描述

注意:这里还没有讲保存操作,因此目前的图层是没法保存的。

第二种方法

第二种方法,自然就是依葫芦画瓢,直接使用QGis现有的代码。先来看一个流程图:

这里写图片描述

未完待更~

© 版权声明
THE END
喜欢就支持一下吧
点赞625 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容