PythonでmatplotlibとPySideを連携させる


# はじめに

Pythonは固定のGUIパッケージがなく、いくつかのパッケージの中から、自分の目的に合ったものを使う。単体のパッケージだけ使っている分には構わないが、複数のパッケージを使うとなると必ずと言ってよいほど競合が起きる。このページはその中でも、PySideとmatplotlibの連携についてのリマインダである。

# インストール

PySideはQtのPythonバインディングである。Pythonに付属しているTkinter(GUIライブラリ)の代わりとして用いられる。PyQt4と悩むところだが、MacにおいてはPySiteの方が遥かにインストールが楽である。具体的にはbrewでインストールすれば良い

```
$ brew intall pyside
```

# PySideとmatplotlibの連携

matplotlibはGUIライブラリというには少し語弊があるが、キーイベントなどのイベント駆動でグラフの遷移を書くことが出来る柔軟で、便利なライブラリである。例えば、棒グラフをドラッグアンドドロップで移動させる実装は以下のように書くことが出来る。

```draggable_rectangle.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import numpy as np
import matplotlib.pyplot as plt


class DraggableRectangle:
    def __init__(self, rect):
        self.rect = rect
        self.press = None

    def connect(self):
        'connect to all the events we need'
        self.cidpress = self.rect.figure.canvas.mpl_connect(
            'button_press_event', self.on_press)
        self.cidrelease = self.rect.figure.canvas.mpl_connect(
            'button_release_event', self.on_release)
        self.cidmotion = self.rect.figure.canvas.mpl_connect(
            'motion_notify_event', self.on_motion)

    def on_press(self, event):
        '''
        on button press we will see if the mouse is over us and store some data
        '''
        if event.inaxes != self.rect.axes:
            return

        contains, attrd = self.rect.contains(event)
        if not contains:
            return
        print 'event contains', self.rect.xy
        x0, y0 = self.rect.xy
        self.press = x0, y0, event.xdata, event.ydata

    def on_motion(self, event):
        'on motion we will move the rect if the mouse is over us'
        if self.press is None:
            return
        if event.inaxes != self.rect.axes:
            return
        x0, y0, xpress, ypress = self.press
        dx = event.xdata - xpress
        dy = event.ydata - ypress

        self.rect.set_x(x0 + dx)
        self.rect.set_y(y0 + dy)

        self.rect.figure.canvas.draw()

    def on_release(self, event):
        'on release we reset the press data'
        self.press = None
        self.rect.figure.canvas.draw()

    def disconnect(self):
        'disconnect all the stored connection ids'
        self.rect.figure.canvas.mpl_disconnect(self.cidpress)
        self.rect.figure.canvas.mpl_disconnect(self.cidrelease)
        self.rect.figure.canvas.mpl_disconnect(self.cidmotion)


if __name__ == '__main__':
    fig = plt.figure()
    ax = fig.add_subplot(111)
    rects = ax.bar(range(10), 20 * np.random.rand(10))
    drs = []
    for rect in rects:
        dr = DraggableRectangle(rect)
        dr.connect()
        drs.append(dr)

    plt.show()
```

GUIのコードとしては破格の行数の少なさで実装ができることが分かると思う。このようなGUIにテキストボックスやボタンといったGUIコンポーネントを配置したいと思うと、Qtを使ったUIコンポーネントを作りたくなる。matplotlibも内部でQtのライブラリを叩いているはずなので、この連携は楽に思えるが、組み合わせるとなると、色々と障害が出てくる。そこで、PySideの仕組みを使って同じウィンドウを表示することを試みる。

その結果が以下のコードだ。

```draggable_rectangle_with_pyside.py
#!/usr/bin/env python
# coding: utf-8

import matplotlib
matplotlib.rcParams['backend.qt4'] = 'PySide'
matplotlib.use('Qt4Agg')

import numpy as np
import matplotlib.pyplot as plt

from matplotlib.backends.backend_qt4agg \
    import FigureCanvasQTAgg as FigureCanvas
from PySide.QtGui import QApplication
from PySide.QtGui import QMainWindow
from PySide.QtCore import Qt


class DraggableRectangle(object):
    def __init__(self, rect):
        self.rect = rect
        self.press = None

    def connect(self):
        'connect to all the events we need'
        self.cidpress = self.rect.figure.canvas.mpl_connect(
            'button_press_event', self.on_press)
        self.cidrelease = self.rect.figure.canvas.mpl_connect(
            'button_release_event', self.on_release)
        self.cidmotion = self.rect.figure.canvas.mpl_connect(
            'motion_notify_event', self.on_motion)

    def on_press(self, event):
        '''
        on button press we will see if the mouse is over us and store some data
        '''
        if event.inaxes != self.rect.axes:
            return

        contains, attrd = self.rect.contains(event)
        if not contains:
            return
        print 'event contains', self.rect.xy
        x0, y0 = self.rect.xy
        self.press = x0, y0, event.xdata, event.ydata

    def on_motion(self, event):
        'on motion we will move the rect if the mouse is over us'
        if self.press is None:
            return
        if event.inaxes != self.rect.axes:
            return
        x0, y0, xpress, ypress = self.press
        dx = event.xdata - xpress
        dy = event.ydata - ypress

        self.rect.set_x(x0 + dx)
        self.rect.set_y(y0 + dy)

        self.rect.figure.canvas.draw()

    def on_release(self, event):
        'on release we reset the press data'
        self.press = None
        self.rect.figure.canvas.draw()

    def disconnect(self):
        'disconnect all the stored connection ids'
        self.rect.figure.canvas.mpl_disconnect(self.cidpress)
        self.rect.figure.canvas.mpl_disconnect(self.cidrelease)
        self.rect.figure.canvas.mpl_disconnect(self.cidmotion)


if __name__ == '__main__':
    import sys

    app = QApplication.instance()
    if app is None:
        app = QApplication(sys.argv)

    window = QMainWindow()
    fig = plt.figure()
    canvas = FigureCanvas(fig)
    ax = fig.add_subplot(111)
    rects = ax.bar(range(10), 20 * np.random.rand(10))
    drs = []
    for rect in rects:
        dr = DraggableRectangle(rect)
        dr.connect()
        drs.append(dr)

    #plt.show()
    canvas.setFocusPolicy(Qt.StrongFocus)
    canvas.setFocus()
    window.setCentralWidget(canvas)

    window.show()

    app.exec_()
```

ポイントは

```
import matplotlib
matplotlib.rcParams['backend.qt4'] = 'PySide'
matplotlib.use('Qt4Agg')
```

の部分で、Qt, PySideを使うこと明示することと、

```
if __name__ == '__main__':
    import sys

    app = QApplication.instance()
    if app is None:
        app = QApplication(sys.argv)

    window = QMainWindow()
    fig = plt.figure()
    canvas = FigureCanvas(fig)
    ax = fig.add_subplot(111)
    rects = ax.bar(range(10), 20 * np.random.rand(10))
    drs = []
    for rect in rects:
        dr = DraggableRectangle(rect)
        dr.connect()
        drs.append(dr)

    #plt.show()
    canvas.setFocusPolicy(Qt.StrongFocus)
    canvas.setFocus()
    window.setCentralWidget(canvas)

    window.show()

    app.exec_()
```

の部分で、FigureオブジェクトをFigureCanvasオブジェクトに取り込むことである。