以 Python 製作附加元件

警告

本教學已有新的版本,請前往 以 Python 製作附加元件 (QGIS3)

增進 QGIS 功能的最佳方法,就是使用附加元件。你也可以使用 Python 來寫一個,從只有一個按鈕到複雜的功能面板,都可任君挑選。本教學會介紹設計附加元件的大致流程,包括設置開發環境、打造使用者介面,以及撰寫程式碼與 QGIS 互動。有關較為基礎的部分,請參閱 Python 程式設計的初步上手

內容說明

我們要開發一個簡單的附加元件,稱為 Save Attributes,它可以讓使用者任意挑選一個向量圖層,把它的屬性另存為 CSV 檔。

取得工具

Qt Creator

Qt 是一套軟體開發框架,用於設計在 Windows、Mac、Linux 或是其他行動作業系統上執行的軟體。QGIS 本身就是用 Qt 框架打造的,所以我們在這裡要使用一個稱為 Qt Creator 的程式來設計我們附加元件的介面。

SourgeForge 上下載並安裝 Qt Creator。

Qt 的 Python Bindings

由於我們要使用 Python 來設計附加元件,因此得安裝 Qt 的 Python binding ,以便在 Python 中可以輕鬆使用 Qt 的功能。此步驟會因作業系統的不同而不同,像是 Windows 命令列下,要裝的程式叫做 pyrcc4

Windows

下載 OSGeo4W network installer 然後選擇 Express Desktop Install,選擇安裝 QGIS 的套件。安裝完畢後,你就可以經由 OSGeo4W Shell 存取 pyrcc4 工具。

Mac

安裝 Homebrew 套件管理員,然後使用以下指令安裝 PyQt 套件:

brew install pyqt

Linux

在你的發行版中尋找並安裝 python-qt4 套件。在 Ubuntu 和其他基於 Debian 的發行版中,可以使用如下指令:

sudo apt-get install python-qt4

編輯器或 Python IDE

要進行任何種類的軟體開發,優良的文字編輯器是必不可少的。在本教學中,你可以使用你喜歡的文字編輯器或 IDE (整合開發環境);如果沒有的話,每個作業系統都有很多免費或付費的文字編輯軟體可使用,挑一個符合你需求的即可。

本教學中使用的是 Windows 版本的 Notepad++ 編輯器。

Windows

Notepad++ 是一款好用且免費的編輯器,可安裝於 Windows 下。下載並安裝 Notepad++ editor

備註

如果你使用的是 Notepad++,請確認你有在 Settings ‣ Preferences ‣ Tab Settings 的地方勾選 Replace by space。Python 對於空白縮排設定非常敏感,此選項可以確保你使用 tab 和 space 鍵製造的空白可以被適當的設定。

附加元件「Plugin Builder」

QGIS 有個實用的 Plugin Builder 附加元件,它可以創造附加元件所需檔案和樣板設計的代碼。請尋找並安裝 Point Sampling Tool 附加元件,安裝細節請參考 使用附加元件

附加元件「Plugin Reloader」

還有一個實用的附加元件,可以讓我們反覆測試不斷更新的附加元件。使用此元件,可以在改變附加元件的程式碼之後,不用重新啟動 QGIS 就能讀取程式碼修改的部分。請尋找並安裝 Plugin Reloader 附加元件,安裝細節請參考 使用附加元件

備註

Plugin Reloader 屬於實驗性的附加元件,所以如果你找不到它,請確認你已在 附加元件的設定分頁 中勾選了 顯示實驗性質的附加元件

操作流程

  1. 開啟 QGIS,選擇 附加元件 ‣ Plugin Builder ‣ Plugin Builder

../_images/1168.png
  1. QGIS Plugin Builder 視窗會與資料表格一起出現,你可以在此處把我們要製作的附加元件的相關資訊填在表中。Class name 是本附加元件使用的主要 Python 類別,同時也是儲存所有附加元件檔案的資料夾名稱,請在此輸入 SaveAttributesPlugin name 是你的附加元件會在 Plugin Manager 中顯示的名稱,請輸入 Save AttributesDescription 欄位中可添加一些相關描述,而 Module name 則是附加元件主要存取的 Python 檔案名稱,請輸入 save_attributes。版本號碼可先維持預設,Text for menu item 則與附加元件在 QGIS 選單中顯示的文字有關。Menu 欄位則是讓你決定外掛元件會放在選單列中的哪個分類(譯按:新版已移除此欄位)。由於我們的附加元件是針對向量資料,這裡請選 Vector。勾選底部的 Flag the plugin as experimental,然後按下 OKNEXT(譯按:新版的 QGIS 把此表格分成幾個頁面,所以你可能要按下幾次 NEXT 才可見到所有選項)。

../_images/2138.png
  1. 接下來你要為附加元件指定儲存的路徑。你需要前往你電腦內的 QGIS python 附加元件路徑,然後按下 選擇資料夾。在一般的狀況下,.qgis2/ 資料夾會在你的家目錄底下,然後 plugin 資料夾的路徑則會根據作業系統的不同而不同,如下所示:(請把 username 換成你的使用者名稱)

Windows

c:\Users\username\.qgis2\python\plugins

Mac

/Users/username/.qgis2/python/plugins

Linux

/home/username/.qgis2/python/plugins
../_images/379.png
  1. 附加元件的模板建立後,會有個確認視窗出現。請注意存放附加元件的路徑。

../_images/447.png
  1. 在我們可以使用新創造的附加元件之前,必須要先編譯由 Plugin Builder 產生的 resources.qrc 檔案。請在 windows 上開啟 OSGeo4W Shell,或在 Mac 或 Linux 上開啟終端機。

../_images/541.png
  1. 你可以透過 cd 指令,接續資料夾的路徑名稱,前往 Plugin Builder 輸出的附加元件檔案存放的資料夾。

cd c:\Users\username\.qgis2\python\plugins\SaveAttributes
../_images/640.png
  1. 在此目錄之下輸入 make,之前安裝的 Python 的 Qt bindings 中的 pyrcc4 指令就會執行。

make
../_images/740.png
  1. 現在我們已經準備完畢,來看看我們剛才創造的附加元件吧。關閉 QGIS 後重新啟動,然後前往 附加元件 ‣ 管理與安裝附加元件,在 已安裝 分頁中啟用 Save Attributes。然後你會發現在工具列的以下路徑出現了,而且還有新圖示: 向量 ‣ Save Attributes ‣ Save Attributes as CSV。點選後可開啟附加元件的視窗。

../_images/839.png
  1. 你會看到一個叫做 Save Attributes 的視窗出現。可以關掉了。

../_images/940.png
  1. 我們現在要開始設計我們的視窗,並在上面添加一些新元素。開啟 Qt Creator 程式,選擇 檔案 –> 開啟檔案或專案…

../_images/1047.png
  1. 前往附加元件的資料夾,選擇檔案 save_attributes_dialog_base.ui,然後按 開啟

../_images/1169.png
  1. 附加元件的空白視窗就會在這裡出現。你可以從左邊的面板中拖曳加入視窗中的一些元件,這裡我們要加上 Input Widget 中的 Combo Box(組合框),把它拖曳到附加元件的視窗中。

../_images/1250.png
  1. 調整組合框的大小,然後再從 Display Widget 中拖曳一個 Label(標籤)到視窗上。

../_images/1348.png
  1. 點選標籤的文字然後輸入 Select a layer

../_images/1445.png
  1. 選擇 檔案 ‣ Save save_attributes_dialog_base.ui,以儲存檔案。注意組合框目前的物件名稱為 comboBox,如果要使用 Python 操作物件,我們需要記住物件的名稱才行。

../_images/1541.png
  1. QGIS 重新載入附加元件之後,我們就能在視窗中看到剛才做的改變。前往 附加元件 ‣ Plugin Reloader ‣ Choose a plugin to be reloaded

../_images/1639.png
  1. Configure Plugin reloader 視窗中選擇 SaveAttributes

../_images/1737.png
  1. 現在點選 Save Attributes as CSV 按鈕後,你就會看到新設計的視窗。

../_images/1834.png
  1. 讓我們來增加一點東西到組合框中,使 QGIS 載入的圖層能列出來。前往附加元件資料夾,使用文字編輯器開啟 save_attributes.py,下拉至 run(self) 的方法,這個方法會在從工具列按鈕或選單點選附加元件時。在此方法的開頭添加以下的程式碼,它會取得 QGIS 中載入的圖層然後把它們加到附加元件視窗中的 comboBox 物件中。

layers = self.iface.legendInterface().layers()
layer_list = []
for layer in layers:
     layer_list.append(layer.name())
     self.dlg.comboBox.addItems(layer_list)
../_images/1925.png
  1. 回到 QGIS 主視窗,然後選擇 Plugins ‣ Plugin Reloader ‣ Reload plugin: SaveAttributes 以再次重新啟動附加元件;或是按下 F5 也可以達到相同目的。為了測試剛才新加的功能,我們要在 QGIS 中載入一些圖層。載入之後,選擇 Vector ‣ Save Attributes ‣ Save Attributes as CSV 以開啟此附加元件。

../_images/2022.png
  1. 現在你可以看到我們的組合框出現了 QGIS 中載入圖層的名字了。

../_images/2139.png
  1. 讓我們把剩下的使用者介面元素也添加進來。回到 Qt Creator 然後載入 save_attributes_dialog_base.ui,再從 Display Widget 加入一個 Label,然後文字改為 Select output file,接著從 Input Widget 加入 LineEdit,他會秀出使用者選擇的輸出檔檔名;再來從 Button 加入一個 Push Button (按鈕),然後把按鈕的標籤改為 ...。記住這些物件的名字,我們要使用它們與物件本體互動。最後請存檔。

../_images/2224.png
  1. 現在要做的是加上一段 Python 程式碼,讓使用者在按下 ... 鈕的時候,會開啟一個新視窗選擇檔案路徑,並且把此路徑顯示在 Line Edit 框內。以文字編輯器打開 save_attributes.py,在檔案開頭部分、匯入模組的清單中加上 QFileDialog

../_images/2321.png
  1. 加入名為 select_output_file 的新方法,內容如下程式碼所示。此程式碼會開啟檔案瀏覽器,並且在 Line Edit 框位中貼上使用者選擇的檔案路徑。

def select_output_file(self):
    filename = QFileDialog.getSaveFileName(self.dlg, "Select output file ","", '*.txt')
    self.dlg.lineEdit.setText(filename)
../_images/2421.png
  1. 現在我們要加上「當按下 鈕時,就啟動 select_output_file 方法」的程式碼。上移至 __init__ 方法,然後在底部加上如下幾行,它們的作用是清除之前在 Line Edit 框中遺留下來的任何文字 (如果有的話),然後把按鈕的 點選 訊號與 select_output_file 方法連結起來。

self.dlg.lineEdit.clear()
self.dlg.pushButton.clicked.connect(self.select_output_file)
../_images/2520.png
  1. 回到 QGIS 中,重新載入附加元件,然後開啟 Save Attributes` 使窗。如果一切正常,你就可以按下 ... 鈕,然後從磁碟中選擇輸出檔檔案。

../_images/2618.png
  1. 當按下 OK 時,什麼事都不會發生。這是因為我們還沒有加上把屬性的資訊轉存到文字檔內的城市部分。我們現在已經有所需的所有元素來做到這件事了,請前往 run 的方法,其中會看到一個 pass,再把它以如下的程式碼取代。這段程式碼的解釋可在 Python 程式設計的初步上手 中找到。

filename = self.dlg.lineEdit.text()
output_file = open(filename, 'w')

selectedLayerIndex = self.dlg.comboBox.currentIndex()
selectedLayer = layers[selectedLayerIndex]
fields = selectedLayer.pendingFields()
fieldnames = [field.name() for field in fields]

for f in selectedLayer.getFeatures():
    line = ','.join(unicode(f[x]) for x in fieldnames) + '\n'
    unicode_line = line.encode('utf-8')
    output_file.write(unicode_line)
output_file.close()
../_images/2717.png
  1. 現在附加元件已完成,重新載入後就來試試看吧,你會發現輸出的文字檔會含有向量圖層中的屬性資訊。附加元件的資料夾可以壓縮後與其他使用者分享,只要重新解壓縮到他們的附加元件資料夾,就可以開始使用。你也可以上傳到官方的 QGIS 附加元件儲存庫,這樣所有的 QGIS 使用者都能找到並下載你的附加元件。

備註

本附加元件僅供示範使用,請勿任意出版或上傳至 QGIS 附加元件儲存庫。

以下放上完整的 save_attributes.py 檔做為參考。

# -*- coding: utf-8 -*-
"""
/***************************************************************************
 SaveAttributes
                                 A QGIS plugin
 This plugin saves the attribute of the selected vector layer as a CSV file.
                              -------------------
        begin                : 2015-04-20
        git sha              : $Format:%H$
        copyright            : (C) 2015 by Ujaval Gandhi
        email                : ujaval@spatialthoughts.com
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/
"""
from PyQt4.QtCore import QSettings, QTranslator, qVersion, QCoreApplication
from PyQt4.QtGui import QAction, QIcon, QFileDialog
# Initialize Qt resources from file resources.py
import resources_rc
# Import the code for the dialog
from save_attributes_dialog import SaveAttributesDialog
import os.path


class SaveAttributes:
    """QGIS Plugin Implementation."""

    def __init__(self, iface):
        """Constructor.

        :param iface: An interface instance that will be passed to this class
            which provides the hook by which you can manipulate the QGIS
            application at run time.
        :type iface: QgsInterface
        """
        # Save reference to the QGIS interface
        self.iface = iface
        # initialize plugin directory
        self.plugin_dir = os.path.dirname(__file__)
        # initialize locale
        locale = QSettings().value('locale/userLocale')[0:2]
        locale_path = os.path.join(
            self.plugin_dir,
            'i18n',
            'SaveAttributes_{}.qm'.format(locale))

        if os.path.exists(locale_path):
            self.translator = QTranslator()
            self.translator.load(locale_path)

            if qVersion() > '4.3.3':
                QCoreApplication.installTranslator(self.translator)

        # Create the dialog (after translation) and keep reference
        self.dlg = SaveAttributesDialog()

        # Declare instance attributes
        self.actions = []
        self.menu = self.tr(u'&Save Attributes')
        # TODO: We are going to let the user set this up in a future iteration
        self.toolbar = self.iface.addToolBar(u'SaveAttributes')
        self.toolbar.setObjectName(u'SaveAttributes')
        
        self.dlg.lineEdit.clear()
        self.dlg.pushButton.clicked.connect(self.select_output_file)
        

    # noinspection PyMethodMayBeStatic
    def tr(self, message):
        """Get the translation for a string using Qt translation API.

        We implement this ourselves since we do not inherit QObject.

        :param message: String for translation.
        :type message: str, QString

        :returns: Translated version of message.
        :rtype: QString
        """
        # noinspection PyTypeChecker,PyArgumentList,PyCallByClass
        return QCoreApplication.translate('SaveAttributes', message)


    def add_action(
        self,
        icon_path,
        text,
        callback,
        enabled_flag=True,
        add_to_menu=True,
        add_to_toolbar=True,
        status_tip=None,
        whats_this=None,
        parent=None):
        """Add a toolbar icon to the toolbar.

        :param icon_path: Path to the icon for this action. Can be a resource
            path (e.g. ':/plugins/foo/bar.png') or a normal file system path.
        :type icon_path: str

        :param text: Text that should be shown in menu items for this action.
        :type text: str

        :param callback: Function to be called when the action is triggered.
        :type callback: function

        :param enabled_flag: A flag indicating if the action should be enabled
            by default. Defaults to True.
        :type enabled_flag: bool

        :param add_to_menu: Flag indicating whether the action should also
            be added to the menu. Defaults to True.
        :type add_to_menu: bool

        :param add_to_toolbar: Flag indicating whether the action should also
            be added to the toolbar. Defaults to True.
        :type add_to_toolbar: bool

        :param status_tip: Optional text to show in a popup when mouse pointer
            hovers over the action.
        :type status_tip: str

        :param parent: Parent widget for the new action. Defaults None.
        :type parent: QWidget

        :param whats_this: Optional text to show in the status bar when the
            mouse pointer hovers over the action.

        :returns: The action that was created. Note that the action is also
            added to self.actions list.
        :rtype: QAction
        """

        icon = QIcon(icon_path)
        action = QAction(icon, text, parent)
        action.triggered.connect(callback)
        action.setEnabled(enabled_flag)

        if status_tip is not None:
            action.setStatusTip(status_tip)

        if whats_this is not None:
            action.setWhatsThis(whats_this)

        if add_to_toolbar:
            self.toolbar.addAction(action)

        if add_to_menu:
            self.iface.addPluginToVectorMenu(
                self.menu,
                action)

        self.actions.append(action)

        return action

    def initGui(self):
        """Create the menu entries and toolbar icons inside the QGIS GUI."""

        icon_path = ':/plugins/SaveAttributes/icon.png'
        self.add_action(
            icon_path,
            text=self.tr(u'Save Attributes as CSV'),
            callback=self.run,
            parent=self.iface.mainWindow())


    def unload(self):
        """Removes the plugin menu item and icon from QGIS GUI."""
        for action in self.actions:
            self.iface.removePluginVectorMenu(
                self.tr(u'&Save Attributes'),
                action)
            self.iface.removeToolBarIcon(action)
        # remove the toolbar
        del self.toolbar

    def select_output_file(self):
        filename = QFileDialog.getSaveFileName(self.dlg, "Select output file ","", '*.txt')
        self.dlg.lineEdit.setText(filename)
        
    def run(self):
        """Run method that performs all the real work"""
        layers = self.iface.legendInterface().layers()
        layer_list = []
        for layer in layers:
                layer_list.append(layer.name())
            
        self.dlg.comboBox.clear()
        self.dlg.comboBox.addItems(layer_list)
        # show the dialog
        self.dlg.show()
        # Run the dialog event loop
        result = self.dlg.exec_()
        # See if OK was pressed
        if result:
            # Do something useful here - delete the line containing pass and
            # substitute with your code.
            filename = self.dlg.lineEdit.text()
            output_file = open(filename, 'w')
           
            selectedLayerIndex = self.dlg.comboBox.currentIndex()
            selectedLayer = layers[selectedLayerIndex]
            fields = selectedLayer.pendingFields()
            fieldnames = [field.name() for field in fields]
            
            for f in selectedLayer.getFeatures():
                line = ','.join(unicode(f[x]) for x in fieldnames) + '\n'
                unicode_line = line.encode('utf-8')
                output_file.write(unicode_line)
            output_file.close()

If you want to give feedback or share your experience with this tutorial, please comment below. (requires GitHub account)