[Python] matplotlib(NavigationToolbar2TK)の継承を使った拡張


個人の備忘録。

課題設定

  • 次の製品では、matplotlibのプロットを使ってデータ解析が出来ることを目指しているが、若干柔軟性に欠けるので、Tkinterで最低限グラフの自由な操作が出来るようにGUIを追加したい。

  • そこで、デフォルトのナビゲーションツールバーであるNavigationToolbar2TKの拡張を行なう。

実例集

NavigationToolbarに表示されている一部のボタンを表示しないようにする

ソースコード

disable_somebuttons.py

import matplotlib.pyplot as plt
import tkinter as tk

from matplotlib.backends.backend_tkagg import (FigureCanvasTkAgg, NavigationToolbar2Tk)

class NavigationToolbarExt(NavigationToolbar2Tk):
    toolitems = [t for t in NavigationToolbar2Tk.toolitems if t[0] in ('Pan', 'Save')]

## --- 省略 --- ##
## 以下メインプログラム ###

root = tk.Tk()
fig = plt.figure(figsize=(8, 5))

canvas = FigureCanvasTkAgg(fig, master=root)
canvas.draw()

toolbar = NavigationToolbarExt(canvas, root)

canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
toolbar.pack(side=tk.BOTTOM, fill=tk.BOTH)

root.protocol("WM_DELETE_WINDOW", toolbar.quit)
root.mainloop()

実行結果

Pan, Saveのみが表示されるようになる。

ユーザ設定のアイコンを使って、新たなボタンをNavigationToolbarに追加する

  • コンストラクタに、self._buttonsに関連する宣言文を追加します。

  • また、これだけだとToolTipが機能しませんので、ToolTipに関する文章も追加します。

  • 現在は、toggle=Falseとなっていますが、別のメソッドをオーバーライドすることで、toggle=Trueにも対応できる可能性はあります。(要調査)

※ただ、この方法暫定的には出来るのですが、"_"が先頭についているメソッドについてこのような記載をするのは本来良くないので、別の手段を検討する必要があります・・・要考察

originalbutton.py

import matplotlib.pyplot as plt
import tkinter as tk

from matplotlib.backends.backend_tkagg import (FigureCanvasTkAgg, NavigationToolbar2Tk)
from matplotlib.backends._backend_tk import (ToolTip)

class NavigationToolbarExt(NavigationToolbar2Tk):
    toolitems = [t for t in NavigationToolbar2Tk.toolitems if t[0] in ('Pan', 'Save')]

    def __init__(self, canvas=None, master=None):
        super().__init__(canvas, master)

        # move2.pngをアイコンとして、そのボタンがクリックされると、HelloWorldというメンバメソッドを、実行する。
        # なお、ToolTipとしてFoo?が表示される。
        self._buttons["Foo"] = (self._Button('Foo', 'move2.png', toggle=False , command=getattr(self, 'HelloWorld'))) 
        ToolTip.createToolTip(self._buttons["Foo"], 'Foo?')
        self.canvas = canvas
        self.master = master

    def HelloWorld(self):
        print("HelloWorld")

### (以下省略) ###

subplotにおいて、一部のグラフ(ここではsubplot1番目)のみpan出来るように処理を変更する

  • press_panをオーバーライド
pan_somegraph.py

import matplotlib.pyplot as plt
import tkinter as tk

from matplotlib.backends.backend_tkagg import (FigureCanvasTkAgg, NavigationToolbar2Tk)

class NavigationToolbarExt(NavigationToolbar2Tk):
    def press_pan(self, event):
        if( event.inaxes != None):  # 枠外をクリックされた場合(event.inaxes==None)は無視したい
            if event.inaxes.get_subplotspec().colspan.start == 1:   # 1番に指定されているものだけを対象とする
                super().press_pan(event)

### (以下省略) ###

panをしている最中、クリックイベントを受け付けないようにする

  • panをオーバーライド

  • javascript, C#などでは、よくデリゲート演算子(+=, -=)を使ってイベント受付可否の制御を行ないますが、これと同様の原理を使って実装しました。これに対応する関数が、mpl_connect, mpl_disconnectとなります。

 注意として、コンストラクタにもボタンイベントを入れないと、最初の処理のクリックが無視されてしまいます。コンストラクタも、軽くですが実装が必要です。

pan_click_available_onlyidle.py

import matplotlib.pyplot as plt
import tkinter as tk

from matplotlib.backends.backend_tkagg import (FigureCanvasTkAgg, NavigationToolbar2Tk)
from matplotlib.backend_bases import (_Mode)

class NavigationToolbarExt(NavigationToolbar2Tk):
    toolitems = [t for t in NavigationToolbar2Tk.toolitems if t[0] in ('Pan', 'Save')]

    def __init__(self, canvas=None, master=None):
        super().__init__(canvas, master)
        self.canvas = canvas
        self.master = master

        self.pressevent_cid = fig.canvas.mpl_connect('button_press_event', self.onclick)  # ここが必要

    def pan(self):
        super().pan()
        if self.mode == _Mode.PAN:  # PANモードになった場合
            fig.canvas.mpl_disconnect(self.pressevent_cid)    # イベントを削除する
        else:  # PANモードではない場合
            self.pressevent_cid = fig.canvas.mpl_connect('button_press_event', self.onclick)  # イベントを追加する

    def onclick(self, event):
        print("Clicked")

## (以下省略) ###

NavigationToolbarの背景色を変える

Pull Requestしたときに、別の提案をしていただきました。
これが一番すっきりしている形かもしれません。ただ、背景色以外を設定する場合には懸念はちょっとあるのですけどね。


import matplotlib.pyplot as plt
import tkinter as tk

from matplotlib.backends.backend_tkagg import (FigureCanvasTkAgg, NavigationToolbar2Tk)

class NavigationToolbarExt(NavigationToolbar2Tk):
    def __init__(self, canvas=None, master=None):
        super().__init__(canvas, master)   
        self.canvas = canvas
        self.master = master

        self['bg'] = 'white'
        for item in self.winfo_children():
           item['bg'] = 'white'

終了イベントを受け入れる

tkinterを使っているので、mainルーチンに以下の構文を追加しないとpython.exeが終了しない場合があるので注意する。

root.protocol("WM_DELETE_WINDOW", toolbar.quit)