以 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/1143.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/2100.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/352.png
  1. 附加元件的模板建立后,会有个确认视窗出现。请注意存放附加元件的路径。

../_images/434.png
  1. 在我们可以使用新创造的附加元件之前,必须要先编译由 Plugin Builder 产生的 resources.qrc 档案。请在 windows 上开启 OSGeo4W Shell,或在 Mac 或 Linux 上开启终端机。

../_images/534.png
  1. 你可以透过 cd 指令,接续资料夹的路径名称,前往 Plugin Builder 输出的附加元件档案存放的资料夹。

cd c:\Users\username\.qgis2\python\plugins\SaveAttributes
../_images/633.png
  1. 在此目录之下输入 make,之前安装的 Python 的 Qt bindings 中的 pyrcc4 指令就会执行。

make
../_images/733.png
  1. 现在我们已经准备完毕,来看看我们刚才创造的附加元件吧。关闭 QGIS 后重新启动,然后前往 附加元件 ‣ 管理与安装附加元件,在 已安装 分页中启用 Save Attributes。然后你会发现在工具列的以下路径出现了,而且还有新图示: 向量 ‣ Save Attributes ‣ Save Attributes as CSV。点选后可开启附加元件的视窗。

../_images/832.png
  1. 你会看到一个叫做 Save Attributes 的视窗出现。可以关掉了。

../_images/933.png
  1. 我们现在要开始设计我们的视窗,并在上面添加一些新元素。开启 Qt Creator 程式,选择 档案 –> 开启档案或专案…

../_images/1031.png
  1. 前往附加元件的资料夹,选择档案 save_attributes_dialog_base.ui,然后按 开启

../_images/1144.png
  1. 附加元件的空白视窗就会在这里出现。你可以从左边的面板中拖曳加入视窗中的一些元件,这里我们要加上 Input Widget 中的 Combo Box(组合框),把它拖曳到附加元件的视窗中。

../_images/1233.png
  1. 调整组合框的大小,然后再从 Display Widget 中拖曳一个 Label(标籤)到视窗上。

../_images/1332.png
  1. 点选标籤的文字然后输入 Select a layer

../_images/1431.png
  1. 选择 档案 ‣ Save save_attributes_dialog_base.ui,以储存档案。注意组合框目前的物件名称为 comboBox,如果要使用 Python 操作物件,我们需要记住物件的名称才行。

../_images/1530.png
  1. QGIS 重新载入附加元件之后,我们就能在视窗中看到刚才做的改变。前往 附加元件 ‣ Plugin Reloader ‣ Choose a plugin to be reloaded

../_images/1627.png
  1. Configure Plugin reloader 视窗中选择 SaveAttributes

../_images/1725.png
  1. 现在点选 Save Attributes as CSV 按钮后,你就会看到新设计的视窗。

../_images/1825.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/1919.png
  1. 回到 QGIS 主视窗,然后选择 Plugins ‣ Plugin Reloader ‣ Reload plugin: SaveAttributes 以再次重新启动附加元件;或是按下 F5 也可以达到相同目的。为了测试刚才新加的功能,我们要在 QGIS 中载入一些图层。载入之后,选择 Vector ‣ Save Attributes ‣ Save Attributes as CSV 以开启此附加元件。

../_images/2014.png
  1. 现在你可以看到我们的组合框出现了 QGIS 中载入图层的名字了。

../_images/2119.png
  1. 让我们把剩下的使用者介面元素也添加进来。回到 Qt Creator 然后载入 save_attributes_dialog_base.ui,再从 Display Widget 加入一个 Label,然后文字改为 Select output file,接着从 Input Widget 加入 LineEdit,他会秀出使用者选择的输出档档名;再来从 Button 加入一个 Push Button (按钮),然后把按钮的标籤改为 ...。记住这些物件的名字,我们要使用它们与物件本体互动。最后请存档。

../_images/2217.png
  1. 现在要做的是加上一段 Python 程式码,让使用者在按下 ... 钮的时候,会开启一个新视窗选择档案路径,并且把此路径显示在 Line Edit 框内。以文字编辑器打开 save_attributes.py,在档案开头部分、汇入模组的清单中加上 QFileDialog

../_images/2314.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/2413.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/2513.png
  1. 回到 QGIS 中,重新载入附加元件,然后开启 Save Attributes` 使窗。如果一切正常,你就可以按下 ... 钮,然后从磁碟中选择输出档档案。

../_images/2611.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/2711.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()