2012년 12월 18일 화요일

PyQt GUI에 matplotlib 플랏 삽입하기 (번역)


(원글 출처 : http://www.technicaljar.com/?p=688 )

PyQt GUI에 matplotlib 플랏 삽입하기

admin 씀



Matplotlib는 광범위한 플라팅 툴과 기능성을 제공해 줍니다.
파이썬에서 Matplotlib를 사용하여 플라팅을 하는 것은 전혀 어렵지 않아요~
하지만 간혹 푸쉬버튼이나 텍스트 필드 같은 다른 GUI 요소들에 삽입하고 싶을 때가 있죠.
이 글에서는 PyQt로 GUI를 만들고, 여기에 Matplotlib를 삽입하는 방법을 설명드리겠어요.
일단 QtDesigner를 실행시키고...
File->New 를 해 줘야겠죠.  버튼이 없는 새로운 Dialog를 생성해 줍시다.
(버튼은 곧 직접 넣을 테니깐)
본 예제에서는 푸쉬버튼을 누를 때 새로이 생성되는 랜덤 넘버를 간단한 GUI에 띄워진 Matplotlib 화면에 플랏해 주려고 합니다.
일단 푸쉬버튼 하나를 찝어서 넣어줍시다.
Matplotlib는 PyQt의 일부가 아니기 때문에, Matplotlib를 PyQt상의 위젯이 되도록 만들어줘야 합니다.  일단 컨테이너(Containers)로부터 위젯(Widget)을 선택하고, GUI 위에 올려놔 줍시다.
그럼 이렇게 보일 겁니다.




오른쪽 위에 보이는 객체 탐색기(Object Inspector)를 보면, QWidget의 클래스로 위젯이 설정되어 있는 것을 주목해 볼 수 있어요.
하지만 우리는 Matplotlib를 그리려는 목적을 가지고 있으니깐 변경해 줄 필요가 있습니다.
변경해 줘야 하는 부분은, 구체적으로 위젯의 행동(Behavior)을 변화시켜주는 것입니다.
위젯의 행동을 변화시켜주려면, QWidget이라는 클래스 타입 대신에 Matplotlib를 그릴 수 있도록 타입을 바꿔주면 됩니다.
이런 작업을 승격(Promoting)이라고 합니다.
즉 위젯을 우리가 필요한 타입으로 변경해 주는 승격을 해 준다는 거죠.
승격 방법은 열라 쉬워요.
객체 탐색기(Object Inspector)에서 위젯(widget)을 오른쪽 클릭 해 주고, '다음으로 승격(Promote to)'을 선택해 주면 됩니다.  그리고 다음 그림과 같이 2개의 필드에 똑같이 써 줍시다.




이제 클래스 네임과 헤더파일 네임을 마음대로 쓸 수 있도록 허용된 것입니다.
이렇게 해 줌으로써, matplotlibwidgetFile.py 파일(.py는 써 줄 필요 없음)을 가진 matplotlibWidget이라는 이름의 클래스가, 이 위젯의 행동을 정의(define)해 줄 것이라고 PyQt에게 알려주는 의미가 됩니다.
본 예제에서 사용된 것과 클래스 네임과 헤더파일 네임이 반드시 똑같을 필요는 전혀 없습니다.
뭐 어쨌든 Add 버튼 눌러 주고, 그 다음 Promote 버튼 눌러 줍시다.
객체 탐색기(Object Inspector)에서 위젯의 타입이 변경되었는지 확인해 보시고요.





위젯의 클래스 타입이 matplotlibWidget으로 변경이 어떻게 되었는지 확인해 보고, 승격이 제대로 되었는지도 확인했습니다.
이제 matplotlibwidgetFile.py을 생성해서 위젯의 행동을 정의해 주면 됩니다.
다만 그 전에 먼저, GUI 코드를 파이썬 코드로 변환해 줄 필요가 있습니다.
일단 만들어진 GUI를 PlotGUI.ui 이름으로 저장하면, 아래와 같은 내용의 XML파일로 저장됩니다.



<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>Dialog</class>
 <widget class="QDialog" name="Dialog">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>570</width>
    <height>449</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>Dialog</string>
  </property>
  <widget class="QPushButton" name="pushButton">
   <property name="geometry">
    <rect>
     <x>30</x>
     <y>20</y>
     <width>501</width>
     <height>41</height>
    </rect>
   </property>
   <property name="text">
    <string>Push To Plot</string>
   </property>
  </widget>
  <widget class="matplotlibWidget" name="widget" native="true">
   <property name="geometry">
    <rect>
     <x>40</x>
     <y>90</y>
     <width>501</width>
     <height>321</height>
    </rect>
   </property>
  </widget>
 </widget>
 <customwidgets>
  <customwidget>
   <class>matplotlibWidget</class>
   <extends>QWidget</extends>
   <header>matplotlibwidgetFile</header>
   <container>1</container>
  </customwidget>
 </customwidgets>
 <resources/>
 <connections/>
</ui>



.py 파일로 변환하려면 아래의 명령어를 터미널에서 쳐 주면 됩니다.
$ pyuic4 PlotGUI.ui > PlotGUI.py
이렇게 변환된 파이썬 파일을 열어보면 아래와 같은 내용이 들어가 있을 것입니다.



# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'PlotGUI.ui'
#
# Created: Thu Jul 26 23:56:29 2012
#      by: PyQt4 UI code generator 4.8.3
#
# WARNING! All changes made in this file will be lost!

from PyQt4 import QtCore, QtGui

try:
    _fromUtf8 = QtCore.QString.fromUtf8
except AttributeError:
    _fromUtf8 = lambda s: s

class Ui_Dialog(object):
    def setupUi(self, Dialog):
        Dialog.setObjectName(_fromUtf8("Dialog"))
        Dialog.resize(570, 449)
        self.pushButton = QtGui.QPushButton(Dialog)
        self.pushButton.setGeometry(QtCore.QRect(30, 20, 501, 41))
        self.pushButton.setObjectName(_fromUtf8("pushButton"))
        self.widget = matplotlibWidget(Dialog)
        self.widget.setGeometry(QtCore.QRect(40, 90, 501, 321))
        self.widget.setObjectName(_fromUtf8("widget"))

        self.retranslateUi(Dialog)
        QtCore.QMetaObject.connectSlotsByName(Dialog)

    def retranslateUi(self, Dialog):
        Dialog.setWindowTitle(QtGui.QApplication.translate("Dialog", "Dialog", None, QtGui.QApplication.UnicodeUTF8))
        self.pushButton.setText(QtGui.QApplication.translate("Dialog", "Push To Plot", None, QtGui.QApplication.UnicodeUTF8))

from matplotlibwidgetFile import matplotlibWidget



파일의 마지막 라인을 주목해 봅시다.
이 라인의 내용은, matplotlibwidgetFile을 인클루드 해 주고, matplotlibWidget를 임포트 해 주는 것입니다.
임포트되는 matplotlibWidget은 위젯의 행동을 정의해 주는 클래스가 될 것이구요.
아래 두 라인을 봅시다.

self.pushButton = QtGui.QPushButton(Dialog)
self.widget = matplotlibWidget(Dialog)


이 두 줄의 내용은, 우리가 만든 GUI에 있는 2개의 객체(Objects)를 정의하는 내용입니다.
pushButton과 Widget이 각각 자신의 정의함수(identifiers)를 가지게 됨을 알 수 있습니다.
우리는 이 2개의 이름을 가지고 이들에게 억세스 할 수 있게 됩니다.
PyQt GUI를 이제 만들었고, 여기에 푸쉬버튼과 위젯도 추가해 줬습니다.
그럼 이제 위젯의 행동을, matplotlibWidget 클래스를 핸들링해 줘서 설정해 줄 것입니다.
이걸 하려면 이 클래스를 정의(define)해 주기만 하면 됩니다.
방법은 아래와 같이 matplotlibwidgetFile.py 파일을 써서 만들어 주면 되고, 쉬워요.



from PyQt4 import QtGui
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas

from matplotlib.figure import Figure

class MplCanvas(FigureCanvas):

    def __init__(self):
        self.fig = Figure()
        self.ax = self.fig.add_subplot(111)

        FigureCanvas.__init__(self, self.fig)
        FigureCanvas.setSizePolicy(self, QtGui.QSizePolicy.Expanding,QtGui.QSizePolicy.Expanding)
        FigureCanvas.updateGeometry(self)


class matplotlibWidget(QtGui.QWidget):

    def __init__(self, parent = None):
        QtGui.QWidget.__init__(self, parent)
        self.canvas = MplCanvas()
        self.vbl = QtGui.QVBoxLayout()
        self.vbl.addWidget(self.canvas)
        self.setLayout(self.vbl)



이 파일은 핵심적으로 이런 일을 합니다.
그림을 그릴 수 있는 캔버스(canvas)를 만들어주고,
우리가 만든 matplotlibWidget으로 넘겨주는 것입니다.
우리의 플라팅 위젯이 어떻게 핸들링 될 것인지를 정의해 준 것입니다.
이 모든게 그 위에 뭔가를 실제로 그리도록 해 주는 거죠.
이제, 아래에 우리의 메인 프로그램(main.py)이 있습니다.
이것은 푸쉬버튼이 눌러지면 matplotlib에서 랜덤 넘버를 그려주게 될 것입니다.



import sys
from PlotGUI import *
import random

class GUIForm(QtGui.QDialog):

    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self,parent)
        self.ui = Ui_Dialog()
        self.ui.setupUi(self)
        QtCore.QObject.connect(self.ui.pushButton, QtCore.SIGNAL('clicked()'), self.PlotFunc)

    def PlotFunc(self):
        randomNumbers = random.sample(range(0, 10), 10)
        self.ui.widget.canvas.ax.clear()
        self.ui.widget.canvas.ax.plot(randomNumbers)
        self.ui.widget.canvas.draw()


if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    myapp = GUIForm()
    myapp.show()
    sys.exit(app.exec_())


푸쉬버튼을 누를 때 마다, PlotFunc()이 호출되고, 정수의 랜덤 넘버셋이 그래프 위에 그려질 것입니다.  이 코드의 첫 실행 부분은 아래 부분입니다.


if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    myapp = GUIForm()
    myapp.show()
    sys.exit(app.exec_())



이 부분은 기본적으로 우리의 GUI 인스턴스를 실행해 주는 QApplication 인스턴스를 생성해 줍니다.  두 번째로, QUIForm 인스턴스를 생성해 줍니다.
이것이 바로 우리의 프로세싱을 수행하고, 데이타를 그림그리는 위젯에 넘겨주도록 하는 우리의 클래스입니다.

우리가 인스턴스를 생성할 때, GUIForm은 초기화 부분(initializer)을 호출하게 됩니다.



    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self,parent)
        self.ui = Ui_Dialog()
        self.ui.setupUi(self)
        QtCore.QObject.connect(self.ui.pushButton, QtCore.SIGNAL('clicked()'), self.PlotFunc)


여기서 먼저 Ui_Dialog 인스턴스를 생성시킵니다.
Ui_Dialog가 PlotGUI.py 안에 있는 클래스의 이름이라는 점을 주목합시다.
(PlotGUI.py는 우리가 미리 만들어둔 GUI파일을 파이썬 포멧으로 변환해 둔 것이죠)
때문에 이 부분은 기본적으로 우리의 GUI 인스턴스를 만들고, 그 다음 setupUi(self)를 호출해서 GUI를 셋업합니다.  마지막 라인은, 우리가 누른 푸쉬버튼 신호를 PlotFunc로 연결해 줍니다.
그 다음에는, GUI가 myapp.show()에 의해 만들어지고 스크린에 뜨게 됩니다.
app.exec_()는, GUI상의 어떤 액션(클릭 따위)이 발생하면 그걸 잡아서 sys.exit()로 연결해 주는 루프를 시동시켜줍니다.  sys.exit()는 GUI를 확실하게 종료시켜 주는 것이구요.
PlotFunc는 명백하게 스스로 설명됩니다.
즉 그림을 만드는 위젯 객체에 억세스하는 것이죠. (이 이름은 PlotGUI.py에서 설정되어 있음을 기억합시다)
결국 푸쉬버튼을 누르면 아래와 같은 장면을 볼 수 있습니다.




이상 PyQt GUI에 matplotlib 그래프를 삽입하는 방법이었습니다.


레퍼런스:
[1] Matplotlib for Python Developers
[2] Introduction to Python Programming and Developing GUI Applications With PyQT

기타 참고 예제는 여기서 : http://eli.thegreenplace.net/2009/01/20/matplotlib-with-pyqt-guis/

댓글 3개:

  1. 좋은 예제에 친절한 설명 덕분에 많이 배우고 갑니다 감사합니다!

    답글삭제
  2. 자세한 설명 감사드립니다. 아직 제가 이해를 못했는데...그럼 python 파일은 plotGUI.py, matplotlibwidgetFile.py, main.py 이렇게 3개를 만들어야하는건가요?

    답글삭제
  3. 위젯을 굳이 승격해서 써야 하는 이유가 있나요? 그냥 사용하는 것이 더 간단한 듯합니다만...

    답글삭제