home about terms twitter

【Python】PySimpleGUIとmatplotlibでグラフとメッセージを表示

date: 2021-07-23 | mod: 2021-08-05

main_window

前書き

PythonはJupyter NotebookやGoogle Colaboratory など、ブラウザ上でグラフを扱うアプリケーションの機能が充実しており、デスクトップ上のGUIを使わずとも比較的容易にデータの可視化が行えるようになっています。

しかしながら、いまだにGUIを用いる事への需要があることは事実です。 GUIによるグラフの描画は、

  • オフライン環境においてGUIを用いる時(オンラインでしか使えないツールの代替として)
  • ボタンを押したりスライダーを動かすなどにより直感的な操作をしたい時
  • 職場等において他のメンバーがCUIやJupyter Notebookなどに慣れていない
  • ツールのデザインをオリジナルなものにしたい

などに役に立ちます。 また、クラウドベースな開発環境や作業環境が充実してくる中で、ネイティブ環境において動作するアプリケーションを作成することはソフトウェア開発の学習においても有意義であると言えます。

今回は以下の二つのライブラリを組み合わせることにより、Pythonで作成したGUI上でグラフを表示させ、同時にメッセージボックスを表示させるまでの環境構築を目的とします。

  • PySimpleGUI: シンプルな記述によってGUIを構築可能なライブラリです。(公式ドキュメント)
  • matplotlib: Pythonにおいて広く用いられているグラフ描画ライブラリです。(公式ドキュメント)

方法

開発環境の構築

このチュートリアルで使用している開発環境は以下の通りです。

  • Windows10
  • Python 3.7.4

pipを用いており、matplotlibもPySimpleGUIもインストールされていない場合は、以下のコマンドでインストールを行います。 condaを用いる際は別途conda用のコマンドをご参照ください。

  • pip install matplotlib
  • pip install pysimplbegui

インストールが終了すると、これらのライブラリが使用できます。

コード

以下のコードを.pyファイルにペーストします。

一部のコードは以下のページ(PySimpleGUI公式のGitHub)を参考にしています。

ファイル名はtest.pyとしています。

from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
import matplotlib.patches as patch
import matplotlib.pyplot as plt
import numpy as np
import PySimpleGUI as sg

# 図の描画関数
def draw_figure(canvas, figure):
    figure_canvas_agg = FigureCanvasTkAgg(figure, canvas)
    figure_canvas_agg.draw()
    figure_canvas_agg.get_tk_widget().pack(side='top', fill='both', expand=1)
    return figure_canvas_agg

def main():

    # ------------------------------------------------------
    # カラム設定
    # ------------------------------------------------------

    # カラム1
    col_1 = [
        [sg.Canvas(size=(640, 480), key='-CANVAS_2-')],
        ]
    # カラム2
    col_2 = [
        [sg.Canvas(size=(640, 480), key='-CANVAS_1-')],
        [sg.Slider(range=(10, 500), default_value=40, size=(20, 10), orientation='h', key='-SLIDER-DATAPOINTS-')],
        [sg.Button('ボタン1')],
        [sg.Button('ボタン2')],
        [sg.Button('ボタン3')]
        ]
    layout = [
                # 上部カラム
                [
                    sg.Column(col_1, vertical_alignment='True'),
                    sg.Column(col_2, vertical_alignment='True', justification='True')
                ],
            
                # 下部カラム
                [
                    sg.T(size=(45,5), key='-M_BOX_1-', background_color='black'),
                    sg.Multiline(size=(45,5), key='-M_BOX_2-'),
                    sg.Canvas(size=(640, 480), key='-CANVAS_3-')
                ]
            ]

    # ------------------------------------------------------
    # Window設定
    # ------------------------------------------------------
    sg.theme('Topanga')   # GUIテーマの変更
    window = sg.Window(
                'テスト', # タイトルバーのタイトル
                layout, # 採用するレイアウトの変数
                finalize=True,
                auto_size_text=True,
                location=(0, 0),
                # no_titlebar=True, # タイトルバー無しにしたい時はコメントアウトを解除
                )
    # window.Maximize()   # フルスクリーン化したい時にはコメントアウトを解除

    canvas_elem_1 = window['-CANVAS_1-'] # CANVAS_1 = 折れ線アニメーショングラフ
    canvas_elem_2 = window['-CANVAS_2-'] # CANVAS_2 = 赤点の点滅グラフ
    canvas_elem_3 = window['-CANVAS_3-'] # CANVAS_3 = 円の回転アニメーショングラフ
    
    m_box_1 = window['-M_BOX_1-']   # 左下左メッセージボックス
    m_box_2 = window['-M_BOX_2-']   # 左下右メッセージボックス

    canvas_1 = canvas_elem_1.TKCanvas
    canvas_2 = canvas_elem_2.TKCanvas
    canvas_3 = canvas_elem_3.TKCanvas
    
    
    # ------------------------------------------------------
    # プロット設定
    #-------------------------------------------------------

    # matplotlibスタイル('dark_background')
    plt.style.use('dark_background') 

    # グラフサイズ変更(figsize=(横インチ ,縦インチ)
    fig_1 = Figure(figsize=(2, 2))    
    fig_2 = Figure(figsize=(6, 4)) 
    fig_3 = Figure(figsize=(1, 1)) 

    # axesオブジェクト設定(1行目・1列・1番目)
    ax_1 = fig_1.add_subplot(111)
    ax_2 = fig_2.add_subplot(111)
    ax_3 = fig_3.add_subplot(111)

    ax_1.xaxis.set_visible(False) # 軸消去
    ax_1.yaxis.set_visible(False)
    ax_2.xaxis.set_visible(False)
    ax_2.yaxis.set_visible(False)
    ax_3.xaxis.set_visible(False)
    ax_3.yaxis.set_visible(False)
    
    # x軸, y軸のラベル
    ax_1.set_xlabel("X axis")
    ax_1.set_ylabel("Y axis")

    # グリッドの描画
    ax_1.grid()

    # グラフの描画
    fig_agg_1 = draw_figure(canvas_1, fig_1)
    fig_agg_2 = draw_figure(canvas_2, fig_2)
    fig_agg_3 = draw_figure(canvas_3, fig_3)

    # ランダム数値データの用意
    NUM_DATAPOINTS = 10000 # ランダムデータ用の数値ポイント最大値
    dpts = [np.sqrt(1-np.sin(x)) for x in range(NUM_DATAPOINTS)] # ランダム数値リスト

    # ------------------------------------------------------
    # 描画設定
    #-------------------------------------------------------

    # 描画ループ
    while True:
        for i in range(len(dpts)):

            event, values = window.read(timeout=10)
            if event in ('Exit', None):
                exit(69)

            # グラフ描画のクリア
            ax_1.cla()
            ax_2.cla()
            ax_3.cla()

            ax_1.grid() # ax_1のグリッド描画

            # グラフ1の描画
            # 黄色折れ線グラフのアニメーション
            data_points = int(values['-SLIDER-DATAPOINTS-']) # スライダーによるデータ数の変更(グラフ1用)
            ax_1.plot(range(data_points), dpts[i:i+data_points],  color='yellow')

            # グラフ2の描画
            # マップ上の赤丸の点滅: iが偶数であるときに点滅
            if i % 2 == 0:
                ax_2.plot([0, 1], [2, 3], 'ro') # x座標リスト、y座標リスト、ro = 赤丸

            # グラフ3の描画
            # 円-楕円 描画
            ellipse = patch.Ellipse(xy=(0.5, 0.5), width=np.sin(i/3), height=1, fill=False, ec='yellow')
            ax_3.add_patch(ellipse)


            # グラフの描画
            fig_agg_1.draw()
            fig_agg_2.draw()
            fig_agg_3.draw()


            # ------------------------------------------------------
            # メッセージ内容設定
            # ------------------------------------------------------

            # iが各数字の倍数であるときに色を変更しながらメッセージとして表示する
            if i % 5 == 0:
                window['-M_BOX_2-'].print(str(i)+' は 5 の倍数です。', text_color='green')
            if i % 7 == 0:
                window['-M_BOX_2-'].print(str(i)+' は 7 の倍数です。', text_color='black')
            if i % 13 == 0:
                window['-M_BOX_2-'].print(str(i)+' は 13 の倍数です。', text_color='red')

            # message.txtに書かれている内容を表示する
            with open('message.txt',encoding="utf-8") as message_file: # メッセージボックス入力
                window['-M_BOX_1-'].Update(message_file.read())

    window.close()

if __name__ == '__main__':
    main()

実行前には以下のファイルが同一のディレクトリに入っていることを確認します。

  • test.py
  • message.txt

message.txtは、後述するメッセージボックスにおける文章の表示の際に必要になります。 今回の場合は、内容にtestとのみ記入したファイルを用意しています。

以上を確認したうえで、以下のコマンドでファイルを実行します。

python test.py

結果

次のようなウィンドウが表示されます。 キャプチャソフトの都合上タイトルバーは表示されていませんが、PySimpleGUIのアイコンとともに「テスト」というタイトルが表示されます。

main_window

一番大きいグラフに赤点、右上に黄色の折れ線グラフ、右下に回転する黄色い円のグラフが表示されています。

PySimpleGUIでは、カラム区切りで要素を配置することができます。

今回のコードでは、以下のように区切ることで要素を配置しています。

  • 上部カラム: グラフ1,2
    • カラム1: グラフ2
    • カラム2: グラフ1
  • 下部カラム: メッセージボックス、グラフ3 main_window
# カラム1
col_1 = [
	[sg.Canvas(size=(640, 480), key='-CANVAS_2-')],
	]
# カラム2
col_2 = [
	[sg.Canvas(size=(640, 480), key='-CANVAS_1-')],
	[sg.Slider(range=(10, 500), default_value=40, size=(20, 10), orientation='h', key='-SLIDER-DATAPOINTS-')],
	[sg.Button('ボタン1')],
	[sg.Button('ボタン2')],
	[sg.Button('ボタン3')]
	]
layout = [
			# 上部カラム
			[
				sg.Column(col_1, vertical_alignment='True'),
				sg.Column(col_2, vertical_alignment='True', justification='True')
			],

			# 下部カラム
			[
				sg.T(size=(45,5), key='-M_BOX_1-', background_color='black'),
				sg.Multiline(size=(45,5), key='-M_BOX_2-'),
				sg.Canvas(size=(640, 480), key='-CANVAS_3-')
			]
		]

これらをlayout変数に格納し、読み込ませることで要素を任意の配置にすることができます。

グラフ1:折れ線グラフ

右上の折れ線グラフは先ほど参考にしたPySimpleGUIの公式GitHubに例としてあげられているものです(PySimpleGUI/DemoPrograms/Demo_Matplotlib_Animated.py)。 この例では、スライダーを変化させることで折れ線グラフの密度を変更することができるようになっています。

graph_slider

これは、

# ランダム数値データの用意
NUM_DATAPOINTS = 10000 # ランダムデータ用の数値ポイント最大値
dpts = [np.sqrt(1-np.sin(x)) for x in range(NUM_DATAPOINTS)] # ランダム数値リスト

で数値を取得したうえで、

# グラフ1の描画
# 黄色折れ線グラフのアニメーション
data_points = int(values['-SLIDER-DATAPOINTS-']) # スライダーによるデータ数の変更(グラフ1用)
ax_1.plot(range(data_points), dpts[i:i+data_points],  color='yellow')

によって実現されています。

PySimpleGUIによるスライダーの表示は、

[sg.Slider(range=(10, 500), default_value=40, size=(20, 10), orientation='h', key='-SLIDER-DATAPOINTS-')]

によって行います。

なお、画像中にボタンが表示されていますが、仮につけたものなので上記のコード中では役割を持っていません。

グラフ2:赤点点滅プロット

red_dot_plot

赤色の点を点滅させるプロットは、ループしているときのループ番号が偶数の時には点をプロットさせることで実現しています。

Pythonにおいて、ある数が偶数であるときに任意のコードを実行したい時には、

if i % 2 == 0:

とします。

それを踏まえて、以下のコードによってプロットを行います。

# グラフ2の描画
# マップ上の赤丸の点滅: iが偶数であるときに点滅
if i % 2 == 0:
	ax_2.plot([0, 1], [2, 3], 'ro') # x座標リスト、y座標リスト、ro = 赤丸

グラフ3:回転する円グラフ

rotate_circle

回転する円グラフは、楕円を描画する関数である Ellipse()を用いることで実現しています(公式ドキュメント - matplotlib.patches.Ellipse)。

  • width: 縦方向の長さ
  • height: 横方向の長さ

今回の場合は、横方向の長さが周期的に変化してほしいです。そのため、正弦関数($sin$)を用いることで周期的な横方向の変化を実現しています。

縦方向の長さは一定であってほしいので1としています。

# グラフ3の描画
# 円-楕円 描画
ellipse = patch.Ellipse(xy=(0.5, 0.5), width=np.sin(i/3), height=1, fill=False, ec='yellow')
ax_3.add_patch(ellipse)

メッセージボックス

メッセージボックスは2種類用意しました。

一つ目はプログラム内で処理したテキストを表示させるものです。

# iが各数字の倍数であるときに色を変更しながらメッセージとして表示する
if i % 5 == 0:
	window['-M_BOX_2-'].print(str(i)+' は 5 の倍数です。', text_color='green')
if i % 7 == 0:
	window['-M_BOX_2-'].print(str(i)+' は 7 の倍数です。', text_color='black')
if i % 13 == 0:
	window['-M_BOX_2-'].print(str(i)+' は 13 の倍数です。', text_color='red')

colored_message_box

二つ目はテキストファイルを読み取るものです。test.pyと同じディレクトリにmessage.txtを読み取り、その内容を表示します。

# message.txtに書かれている内容を表示する
with open('message.txt',encoding="utf-8") as message_file: # メッセージボックス入力
	window['-M_BOX_1-'].Update(message_file.read())

message_txt 内容は更新されるたびに、ウィンドウ中に更新された文章が表示されます。


以上の方法で、PySimpleGUIとmatplotlibを用いてGUI上でグラフとメッセージを表示させることができました。

リアルタイムでグラフの更新を行いたい場合や、それによるログを表示させたい時にこのコードは役に立つと考えられます。

以下の関連記事と組み合わせることで、拡張したアプリケーションが作成可能になると期待されます。