以 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++,請確認你有在 Replace by space。Python 對於空白縮排設定非常敏感,此選項可以確保你使用 tab 和 space 鍵製造的空白可以被適當的設定。
的地方勾選附加元件「Plugin Builder」¶
QGIS 有個實用的 Plugin Builder
附加元件,它可以創造附加元件所需檔案和樣板設計的代碼。請尋找並安裝 Point Sampling Tool
附加元件,安裝細節請參考 使用附加元件。
附加元件「Plugin Reloader」¶
還有一個實用的附加元件,可以讓我們反覆測試不斷更新的附加元件。使用此元件,可以在改變附加元件的程式碼之後,不用重新啟動 QGIS 就能讀取程式碼修改的部分。請尋找並安裝 Plugin Reloader
附加元件,安裝細節請參考 使用附加元件。
備註
Plugin Reloader 屬於實驗性的附加元件,所以如果你找不到它,請確認你已在 附加元件的設定分頁 中勾選了 顯示實驗性質的附加元件。
操作流程¶
開啟 QGIS,選擇
。
QGIS Plugin Builder 視窗會與資料表格一起出現,你可以在此處把我們要製作的附加元件的相關資訊填在表中。Class name 是本附加元件使用的主要 Python 類別,同時也是儲存所有附加元件檔案的資料夾名稱,請在此輸入
SaveAttributes
。 Plugin name 是你的附加元件會在 Plugin Manager 中顯示的名稱,請輸入Save Attributes
。Description 欄位中可添加一些相關描述,而 Module name 則是附加元件主要存取的 Python 檔案名稱,請輸入save_attributes
。版本號碼可先維持預設,Text for menu item 則與附加元件在 QGIS 選單中顯示的文字有關。Menu 欄位則是讓你決定外掛元件會放在選單列中的哪個分類(譯按:新版已移除此欄位)。由於我們的附加元件是針對向量資料,這裡請選Vector
。勾選底部的 Flag the plugin as experimental,然後按下 OK 或 NEXT(譯按:新版的 QGIS 把此表格分成幾個頁面,所以你可能要按下幾次 NEXT 才可見到所有選項)。
接下來你要為附加元件指定儲存的路徑。你需要前往你電腦內的 QGIS python 附加元件路徑,然後按下 選擇資料夾。在一般的狀況下,
.qgis2/
資料夾會在你的家目錄底下,然後plugin
資料夾的路徑則會根據作業系統的不同而不同,如下所示:(請把username
換成你的使用者名稱)
Windows
c:\Users\username\.qgis2\python\plugins
Mac
/Users/username/.qgis2/python/plugins
Linux
/home/username/.qgis2/python/plugins
附加元件的模板建立後,會有個確認視窗出現。請注意存放附加元件的路徑。
在我們可以使用新創造的附加元件之前,必須要先編譯由 Plugin Builder 產生的
resources.qrc
檔案。請在 windows 上開啟 OSGeo4W Shell,或在 Mac 或 Linux 上開啟終端機。
你可以透過
cd
指令,接續資料夾的路徑名稱,前往Plugin Builder
輸出的附加元件檔案存放的資料夾。
cd c:\Users\username\.qgis2\python\plugins\SaveAttributes
在此目錄之下輸入
make
,之前安裝的 Python 的 Qt bindings 中的pyrcc4
指令就會執行。
make
現在我們已經準備完畢,來看看我們剛才創造的附加元件吧。關閉 QGIS 後重新啟動,然後前往 已安裝 分頁中啟用
,在Save Attributes
。然後你會發現在工具列的以下路徑出現了,而且還有新圖示: 。點選後可開啟附加元件的視窗。
你會看到一個叫做 Save Attributes 的視窗出現。可以關掉了。
我們現在要開始設計我們的視窗,並在上面添加一些新元素。開啟
Qt Creator
程式,選擇 檔案 –> 開啟檔案或專案…
前往附加元件的資料夾,選擇檔案
save_attributes_dialog_base.ui
,然後按 開啟。
附加元件的空白視窗就會在這裡出現。你可以從左邊的面板中拖曳加入視窗中的一些元件,這裡我們要加上 Input Widget 中的 Combo Box(組合框),把它拖曳到附加元件的視窗中。
調整組合框的大小,然後再從 Display Widget 中拖曳一個 Label(標籤)到視窗上。
點選標籤的文字然後輸入
Select a layer
。
選擇
,以儲存檔案。注意組合框目前的物件名稱為comboBox
,如果要使用 Python 操作物件,我們需要記住物件的名稱才行。
QGIS 重新載入附加元件之後,我們就能在視窗中看到剛才做的改變。前往
在 Configure Plugin reloader 視窗中選擇
SaveAttributes
。
現在點選 Save Attributes as CSV 按鈕後,你就會看到新設計的視窗。
讓我們來增加一點東西到組合框中,使 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)
回到 QGIS 主視窗,然後選擇
以再次重新啟動附加元件;或是按下 F5 也可以達到相同目的。為了測試剛才新加的功能,我們要在 QGIS 中載入一些圖層。載入之後,選擇 以開啟此附加元件。
現在你可以看到我們的組合框出現了 QGIS 中載入圖層的名字了。
讓我們把剩下的使用者介面元素也添加進來。回到
Qt Creator
然後載入save_attributes_dialog_base.ui
,再從 Display Widget 加入一個Label
,然後文字改為Select output file
,接著從 Input Widget 加入LineEdit
,他會秀出使用者選擇的輸出檔檔名;再來從 Button 加入一個Push Button
(按鈕),然後把按鈕的標籤改為...
。記住這些物件的名字,我們要使用它們與物件本體互動。最後請存檔。
現在要做的是加上一段 Python 程式碼,讓使用者在按下
...
鈕的時候,會開啟一個新視窗選擇檔案路徑,並且把此路徑顯示在 Line Edit 框內。以文字編輯器打開save_attributes.py
,在檔案開頭部分、匯入模組的清單中加上QFileDialog
。
加入名為
select_output_file
的新方法,內容如下程式碼所示。此程式碼會開啟檔案瀏覽器,並且在 Line Edit 框位中貼上使用者選擇的檔案路徑。
def select_output_file(self):
filename = QFileDialog.getSaveFileName(self.dlg, "Select output file ","", '*.txt')
self.dlg.lineEdit.setText(filename)
現在我們要加上「當按下 … 鈕時,就啟動
select_output_file
方法」的程式碼。上移至__init__
方法,然後在底部加上如下幾行,它們的作用是清除之前在 Line Edit 框中遺留下來的任何文字 (如果有的話),然後把按鈕的點選
訊號與select_output_file
方法連結起來。
self.dlg.lineEdit.clear()
self.dlg.pushButton.clicked.connect(self.select_output_file)
回到 QGIS 中,重新載入附加元件,然後開啟 Save Attributes` 使窗。如果一切正常,你就可以按下
...
鈕,然後從磁碟中選擇輸出檔檔案。
當按下 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()
現在附加元件已完成,重新載入後就來試試看吧,你會發現輸出的文字檔會含有向量圖層中的屬性資訊。附加元件的資料夾可以壓縮後與其他使用者分享,只要重新解壓縮到他們的附加元件資料夾,就可以開始使用。你也可以上傳到官方的 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)