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現有的代碼。先來看一個流程圖:

這裏寫圖片描述

未完待更~

發佈了33 篇原創文章 · 獲贊 41 · 訪問量 25萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章