파이썬 플러그인 만들기

경고

A new version of this tutorial is available at Building a Python Plugin (QGIS3)

플러그인은 QGIS의 기능을 확장시키는데 있어 훌륭한 방법입니다. 단순히 명령어버튼을 추가하는 것에서 부터 정교한 툴킷까지 폭넓은 범위를 파이썬을 이용하여 플러그인을 작성할 수 있습니다. 이 예제에서는 개발환경을 포함하는 과정, 플러그인에 대한 사용자 인터페이스 설계 그리고 QGIS와 상호작용하는 코드 작성의 전체적인 윤곽을 보여줄 것입니다. 기본적인 내용은 파이썬 프로그램밍 시작 예제를 살펴보시기 바랍니다.

작업 개요

이 예제에서는 사용자가 벡터레이어를 선택할 수 있고, CSV 포맷의 파일로 속성을 쓸 수 있는 속성 저장 ``Save Attributes``이라고 하는 간단한 플러그인을 개발할 것입니다.

도구 획득

Qt Creator

`Qt <http://www.qt.io/>`_는 윈도우, 맥, 리눅스에서 구동되는 어플리케이션뿐 아니라 다양한 모바일 구동 시스템을 개발하는데 사용되는 소프트웨어 개발 프레임웤입니다. QGIS 그 자체는 Qt 프레임웤을 사용해서 만들어졌습니다. 플러그인 개발을 위해서는 플러그인 인터페이스을 설계하기 위해 `Qt Creator <http://doc.qt.io/qtcreator/index.html>`_라고 하는 어플리케이션을 사용할 것입니다.

Qt Creator 소프트웨어를 `SourgeForge <http://sourceforge.net/projects/qtcreator.mirror/files/latest/download>`_에서 다운로드하고 설치하십시오.

Qt를 위한 파이썬 만들기

파이썬에서 플러그인을 개발해야 하므로 Qt를 위한 파이썬 바인딩을 설치할 필요가 있습니다. 이것들을 설치하는 방법은 사용하는 플랫폼에 달려 있습니다. 플러그인을 만들기위해서는 pyrcc4 명령어-라인 툴이 필요합니다.

윈도우

Download the OSGeo4W network installer and choose Express Desktop Install. Install the package QGIS. After installation, you will be able to access the pyrcc4 tool via the OSGeo4W Shell.

Install the Homebrew package manager. Install PyQt package by running the following command:

brew install pyqt

리눅스

배포판에 따라 ``python-qt4``패키지를 찾아 설치하십시오. 우분투와 데비안 배포판에 대해서는 다음의 명령어를 따라 구동시킬 수 있습니다.

sudo apt-get install python-qt4

텍스트 편집기 또는 파이썬 IDE

Any kind of software development requires a good text editor. If you already have a favorite text editor or an IDE (Integrated Development Environment), you may use it for this tutorial. Otherwise, each platform offers a wide variety of free or paid options for text editors. Choose the one that fits your needs.

이 예제에서는 위도우상에서 노트패드 ++ 편집기를 사용합니다.

윈도우

`노트패드++ <http://notepad-plus-plus.org/>`_는 윈도우에서 사용하기 좋은 무료 편집기 입니다. 노트패드++ `Notepad++ editor <http://notepad-plus-plus.org/repository/6.x/6.7.5/npp.6.7.5.Installer.exe>`_를 다운로드하여 설치하십시오.

참고

노트패드++를 사용하고 있다면 :menuselection:`Settings –> Preferences –> Tab Settings`에서 :guilabel:`Replace by space`를 체크했는지 확인하십시오. 파이썬은 여백에 매우 민감하고 이러한 세팅이 탭과 스페이스가 적절하게 처리되는 것을 확인시켜줄 것입니다.

플러그인 빌더 플러그인

There is a helpful QGIS plugin named Plugin Builder which creates all the necessary files and the boilerplate code for a plugin. Find and install the Plugin Builder plugin. See 플러그인 사용하기 for more details on how to install plugins.

플러그인 리로더 플러그인

이것은 또다른 유용한 플러그인으로 플러그인의 반복개발을 가능하게 해줍니다. 이 플러그인을 사용하여 플러그인 코드를 변경할 수 있고, 매번 QGIS를 재시작하지 않고 QGIS에 반영할 수 있게 해줍니다. Plugin Reloader 플러그인을 찾아 설치하십시오. 플러그인을 어떻게 설치하는 지에 대한 상세한 내용은 :doc:`using_plugins`를 보십시오.

참고

플러그인 리로더는 실험적인 플러그인입니다. 이 플러그인을 찾을 수 없다면 Plugin Manager 세팅에서 Show also experimental plugins 가 체크되어 있는지 확인하십시오.

과정

  1. Open QGIS. Go to Plugins ‣ Plugin Builder ‣ Plugin Builder.

../_images/1168.png
  1. You will see the QGIS Plugin Builder dialog with a form. You can fill the form with details relating to our plugin. The Class name will be the name of the Python Class containing the logic of the plugin. This will also be the name of the folder containing all the plugin files. Enter SaveAttributes as the class name. The Plugin name is the name under which your plugin will appear in the Plugin Manager. Enter the name as Save Attributes. Add a description in the Description field. The Module name will be the name of the main python file for the plugin. Enter it as save_attributes. Leave the version numbers as they are. The Text for menu item value will be how the users will find your plugin in QGIS menu. Enter it as Save Attributes as CSV. Enter your name and email address in the appropriate fields. The Menu field will decide where your plugin item is added in QGIS. Since our plugin is for vector data, select Vector. Check the Flag the plugin as experimental box at the bottom. Click OK.

../_images/2138.png
  1. Next, you will be prompted to choose a directory for your plugin. You need to browse to the QGIS python plugin directory on your computer and select Select Folder. Typically, a .qgis2/ directory is located in your home directory. The plugin folder location will depend on your platform as follows: (Replace username with your login name)

윈도우

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

/Users/username/.qgis2/python/plugins

리눅스

/home/username/.qgis2/python/plugins
../_images/379.png
  1. You will see a confirmation dialog once your plugin template is created. Note the path to the plugin folder.

../_images/447.png
  1. Before we can use the newly created plugin, we need to compile the resources.qrc file that was created by Plugin Builder. Launch the OSGeo4W Shell on windows or a terminal on Mac or Linux.

../_images/541.png
  1. Browse to the plugin directory where the output of Plugin Builder was created. You can use the cd command followed by the path to the directory.

cd c:\Users\username\.qgis2\python\plugins\SaveAttributes
../_images/640.png
  1. Once you are in the directory, type make. This will run the pyrcc4 command that we had installed as part of Qt bindings for Python.

make
../_images/740.png
  1. Now we are ready to have a first look at the brand new plugin we created. Close QGIS and launch it again. Go to Plugins ‣ Manage and Install plugins and enable the Save Attributes plugin in the Installed tab. You will notice that there is a new icon in the toolbar and a new menu entry under Vector ‣ Save Attributes ‣ Save Attributes as CSV`. Select it to launch the plugin dialog.

../_images/839.png
  1. You will notice a new blank dialog named Save Attributes. Close this dialog.

../_images/940.png
  1. We will now design our dialog box and add some user interface elements to it. Open the Qt Creator program and to to File –> Open File or Project….

../_images/1047.png
  1. Browse to the plugin directory and select the save_attributes_dialog_base.ui file. Click Open.

../_images/1169.png
  1. You will see the blank dialog from the plugin. You can drag-and-drop elements from the left-hand panel on the dialog. We will add a Combo Box type of Input Widget. Drag it to the plugin dialog.

../_images/1250.png
  1. Resize the combo box and adjust its size. Now drag a Label type Display Widget on the dialog.

../_images/1348.png
  1. Click on the label text and enter Select a layer.

../_images/1445.png
  1. Save this file by going to File ‣ Save save_attributes_dialog_base.ui. Note the name of the combo box object is comboBox. To interact with this object using python code, we will have to refer to it by this name.

../_images/1541.png
  1. Let’s reload our plugin so we can see the changes in the dialog window. Go to Plugin ‣ Plugin Reloader ‣ Choose a plugin to be reloaded.

../_images/1639.png
  1. Select SaveAttributes in the Configure Plugin reloader dialog.

../_images/1737.png
  1. Now click the Save Attributes as CSV button. You will see the newly designed dialog box.

../_images/1834.png
  1. Let’s add some logic to the plugin that will populate the combo box with the layers loaded in QGIS. Go to the plugin directory and load the file save_attributes.py in a text editor. Scroll down and find the run(self) method. This method will be called when you click the toolbar button or select the plugin menu item. Add the following code at the beginning of the method. This code gets the layers loaded in QGIS and adds it to the comboBox object from the plugin dialog.

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. Back in the main QGIS window, reload the plugin by going to Plugins ‣ Plugin Reloader ‣ Reload plugin: SaveAttributes. Alternatively, you can just press F5. To test this new functionality, we must load some layers in QGIS. After you load some data, launch the plugin by going to Vector ‣ Save Attributes ‣ Save Attributes as CSV.

../_images/2022.png
  1. You will see that our combo box is now populated with the layer names that are loaded in QGIS.

../_images/2139.png
  1. Let’s add remaining user interface elements. Switch back to Qt Creator and load the save_attributes_dialog_base.ui file. Add a Label Display Widget and change the text to Select output file. Add a LineEdit type Input Widget that will show the output file path that the user has chosen. Next, add a Push Button type Button and change the button label to .... Note the object names of the widgets that we will have to use to interact with them. Save the file.

../_images/2224.png
  1. We will now add python code to open a file browser when the user clicks the ... push button and show the select path in the line edit widget. Open the save_attributes.py file in a text editor. Add QFileDialog to our list of imports at the top of the file.

../_images/2321.png
  1. Add a new method called select_output_file with the following code. This code will open a file browser and populate the line edit widget with the path of the file that the user chose.

def select_output_file(self):
    filename = QFileDialog.getSaveFileName(self.dlg, "Select output file ","", '*.txt')
    self.dlg.lineEdit.setText(filename)
../_images/2421.png
  1. Now we need to add code so that when the button is clicked, select_output_file method is called. Scroll up to the __init__ method and add the following lines at the bottom. This code will clear the previously loaded text (if any) in the line edit widget and also connect the select_output_file method to the clicked signal of the push button widget.

self.dlg.lineEdit.clear()
self.dlg.pushButton.clicked.connect(self.select_output_file)
../_images/2520.png
  1. Back in QGIS, reload the plugin and open the Save Attributes` dialog. If all went fine, you will be able to click the ... button and select an output text file from your disk.

../_images/2618.png
  1. When you click OK on the plugin dialog, nothing happens. That is because we have not added the logic to pull attribute information from the layer and write it to the text file. We now have all the pieces in place to do just that. Find the place in the run method where it says pass. Replace it with the code below. The explanation for this code can be found in 파이썬 프로그램밍 시작.

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. Now our plugin is ready. Reload the plugin and try it out. You will find that the output text file you chose will have the attributes from the vector layer. You can zip the plugin directory and share it with your users. They can unzip the contents to their plugin directory and try out your plugin. If this was a real plugin, you would upload it to the QGIS Plugin Repository so that all QGIS users will be able to find and download your plugin.

참고

This plugin is for demonstration purpose only. Do not publish this plugin or upload it to the QGIS plugin repository.

Below is the full save_attributes.py file as a reference.

# -*- 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)