以 Python 制作附加元件

增进 QGIS 功能的最佳方法,就是使用附加元件。你也可以使用 Python 来写一个,从只有一个按钮到复杂的功能面板,都可任君挑选。本教学会介绍设计附加元件的大致流程,包括设置开发环境、打造使用者介面,以及撰写程序代码与 QGIS 互动。 有关较为基础的部分,请参阅 Python 程序设计的初步上手。getting_started_with_pyqgis

内容说明

我们要开发一个简单的附加元件,称为 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 附加元件,它可以创造附加元件所需档案和样板设计的代码。 请寻找并安装 Plugin Builder 附加元件.。安装细节请参考 使用附加元件

附加元件「Plugin Reloader」

还有一个实用的附加元件,可以让我们反复测试不断更新的附加元件。使用此元件,可以在改变附加元件的程序代码之后,不用重新启动 QGIS 就能读取程序代码修改的部分。请寻找并安装 Plugin Reloader 附加元件,安装细节请参考:doc:using_plugins

注解

Plugin Reloader 属于实验性的附加元件,所以如果你找不到它,请确认你已在 附加元件的设定分页 中勾选了 显示实验性质的附加元件。

操作流程

  1. 开启 QGIS. 选择:menuselection:Plugins –> Plugin Builder –> Plugin Builder
../_images/116.png
  1. guilabel:QGIS Plugin Builder`视窗会与资料表格一起出现,你可以在此处把我们要制作的附加元件的相关资讯填在表中。guilabel:`Class name 是本附加元件使用的主要 Python 类别,同时也是储存所有附加元件档案的资料夹名称,请在此输入 SaveAttributes。guilabel:Plugin name 是你的附加元件会在:guilabel:Plugin Manager`中显示的名称,请输入``Save Attributes`。guilabel:Description 栏位中可添加一些相关描述, 而:guilabel:Module name 则是附加元件主要存取的 Python 档案名称,请输入 save_attributes。版本号码可先维持预设,guilabel:Text for menu item v则与附加元件在 QGIS 选单中显示的文字有关。guilabel:Menu`栏位则是让你决定外挂元件会放在选单列中的哪个分类(译按:新版已移除此栏位)。由于我们的附加元件是针对向量资料,这里请选``Vector`。 勾选底部的:guilabel:Flag the plugin as experimental 然后按下:guilabel:`OK`或 NEXT(译按:新版的 QGIS 把此表格分成几个页面,所以你可能要按下几次 NEXT 才可见到所有选项)。
../_images/213.png
  1. 接下来你要为附加元件指定储存的路径。 你需要前往你电脑内的 QGIS python 附加元件路径,然后按下 Select Folder。 在一般的状况下,.qgis2/``资料夹会在你的家目录底下。 然后 ``plugin 资料夹的路径则会根据作业系统的不同而不同,如下所示: (请把``username``换成你的使用者名称)

Windows

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

Mac

/Users/username/.qgis2/python/plugins

Linux

/home/username/.qgis2/python/plugins
../_images/37.png
  1. 附加元件的模板建立后,会有个确认视窗出现。请注意存放附加元件的路径。
../_images/44.png
  1. 在我们可以使用新创造的附加元件之前,必须要先编译由 Plugin Builder 产生的``resources.qrc`` 档案。 请在 windows 上开启:guilabel:OSGeo4W Shell,或在 Mac 或 Linux 上开启终端机。
../_images/54.png
  1. 你可以透过``cd``指令,接续资料夹的路径名称,前往``Plugin Builder``输出的附加元件档案存放的资料夹。
cd c:\Users\username\.qgis2\python\plugins\SaveAttributes
../_images/64.png
  1. 在此目录之下输入 make。之前安装的 Python 的 Qt bindings 中的 ``pyrcc4``指令就会执行。
make
../_images/74.png
  1. 现在我们已经准备完毕,来看看我们刚才创造的附加元件吧。关闭 QGIS 后重新启动,然后前往:menuselection:Plugins –> Manage and Install plugins`在:guilabel:`Installed`分页中启用 ``Save Attributes`。 然后你会发现在工具列的以下路径出现了,而且还有新图示:menuselection:Vector –> Save Attributes –> Save Attributes as CSV`。 点选后可开启附加元件的视窗。
../_images/84.png
  1. 你会看到一个叫做 :guilabel:`Save Attributes`的视窗出现。可以关掉了。
../_images/94.png
  1. 我们现在要开始设计我们的视窗,并在上面添加一些新元素。开启 Qt Creator 程序,选择 File –> Open File or Project...
../_images/104.png
  1. 前往附加元件的资料夹,选择档案``save_attributes_dialog_base.ui`` 然后按 Open
../_images/117.png
  1. 附加元件的空白视窗就会在这里出现。 你可以从左边的面板中拖曳加入视窗中的一些元件,这里我们要加上:guilabel:Input Widget`中的:guilabel:`Combo Box,把它拖曳到附加元件的视窗中。
../_images/124.png
  1. 调整组合框的大小,然后再从:guilabel:`Display Widget`中拖曳一个 `Label`(标签)到视窗上。
../_images/134.png
  1. 点选标签的文字然后输入``Select a layer``。
../_images/144.png
  1. 选择:menuselection: File –> Save save_attributes_dialog_base.ui`以储存档案。 注意组合框目前的物件名称为 ``comboBox`。如果要使用 Python 操作物件,我们需要记住物件的名称才行。
../_images/154.png
  1. QGIS 重新载入附加元件之后,我们就能在视窗中看到刚才做的改变。 前往:menuselection:Plugin –> Plugin Reloader –> Choose a plugin to be reloaded
../_images/164.png
  1. 在:guilabel:Configure Plugin reloader`视窗中选择``SaveAttributes`
../_images/173.png
  1. 现在点选:guilabel:`Save Attributes as CSV`按钮后,你就会看到新设计的视窗。
../_images/184.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/192.png
  1. 回到QGIS 主视窗,然后选择:menuselection:`Plugins –> Plugin Reloader –> Reload plugin:SaveAttributes`以再次重新启动附加元件;或是按下:kbd:`F5`也可以达到相同目的。为了测试刚才新加的功能,我们要在 QGIS 中载入一些图层。载入之后,选择:menuselection:`Vector –> Save Attributes –> Save Attributes as CSV`以开启此附加元件。
../_images/201.png
  1. 现在你可以看到我们的组合框出现了 QGIS 中载入图层的名字了。
../_images/214.png
  1. 让我们把剩下的使用者介面元素也添加进来。 回到 Qt Creator``然后载入 ``save_attributes_dialog_base.ui。再从:guilabel:Display Widget 加入一个``Label``,然后文字改为``Select output file``。接着从:guilabel:Input Widget`加入一个``LineEdit`,他会秀出使用者选择的输出档档名; 再来从:guilabel:Button 加入一个 Push Button``(按钮),然后把按钮的标签改为 ``...。记住这些物件的名字,我们要使用它们与物件本体互动。最后请存档。
../_images/221.png

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

../_images/231.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/241.png
  1. 现在我们要加上「当按下 ... 钮时,就启动``select_output_file`` 方法」的程序代码。 上移至 __init__``方法,然后在底部加上如下几行,它们的作用是清除之前在 Line Edit 框中遗留下来的任何文字 (如果有的话),然后把按钮的 ``clicked 讯号与``select_output_file`` 方法连结起来。
self.dlg.lineEdit.clear()
self.dlg.pushButton.clicked.connect(self.select_output_file)
../_images/251.png

26.回到 QGIS 中,重新载入附加元件,然后开启:guilabel:Save Attributes`视窗。如果一切正常,你就可以按下`...``钮,然后从磁碟中选择输出档档案。

../_images/261.png
  1. 当按下:guilabel:OK`时, 什么事都不会发生。 这是因为我们还没有加上把属性的资讯转存到文字档内的城市部分。 我们现在已经有所需的所有元素来做到这件事了,请前往``run` 的方法,其中会看到一个 pass。再把它以如下的程序代码取代。这段程序代码的解释可在:doc:`getting_started_with_pyqgis`中找到。
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/271.png
  1. 现在附加元件已完成,重新载入后就来试试看吧,你会发现输出的文字档会含有向量图层中的属性资讯。附加元件的资料夹可以压缩后与其他使用者分享,只要重新解压缩到他们的附加元件资料夹,就可以开始使用。你也可以上传到官方的`QGIS Plugin Repository <https://plugins.qgis.org/>`_ ,这样所有的 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.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()