wxPython/wxFormBuilderを使ってGUIプログラミングを始めました。ここではウィンドウ処理やファイル処理をはじめとしたGUIプログラミングに必要となりそうな動作を一通り実装する方法を紹介します。目次から必要な項目が参照できます。
こんにちは。wat(@watlablog)です。ここではwxPythonによるGUIプログラミングの基礎を辞書的にまとめます!
この記事の前提
wxFormBuilderを使っている
GUIプログラミングには色々な書き方があると思いますが、この記事ではwxFormBuilderを使って自動生成したクラスを編集するスタイルをとっています。
wxFormBuilderの詳細は「wxFormBuilderでwxPythonのGUIコード自動生成」に記載しています。
基本のPythonファイル
wxFormBuilderで自動生成した以下のファイルを前提として解説しますが、それぞれの動作実装コードはwxPythonを1から書いた場合でも参考できるものと思います。
MainClass.py
wxFormBuilderでfileを「MainClass」と設定して生成されるPythonファイル。基本このファイルはいじりません。
ProjectMainFrame.py
MainClassを継承し、FrameクラスをまとめたPythonファイルです。このファイルにイベントを設定します。
main.py
GUIクラスコードを呼びだすメインのPythonファイルです。exe化する時の対象でもあります。
それでは以下に各要素毎の動作実装方法をまとめます。
Frame全体に関するイベント
ウィンドウを閉じる
ウィンドウはそのままでも✖️印により閉じることができますが、うっかり押してしまった時のために一度メッセージダイアログを表示させた方が親切でしょう。
FrameのOnCloseにイベントを設定します。
wxFrame : https://wxpython.org/Phoenix/docs/html/wx.Frame.html
イベント部に以下のコードを追記することで、Yes/Noボタンによるウィンドウクローズを実装します。
1 2 3 4 5 6 7 8 9 10 |
def MainFrameOnClose( self, event ): '''ダイアログボックス選択式のウィンドウクローズ''' # Yes/Noダイアログを使ってユーザに選択させる。 dialog = wx.MessageDialog(None, 'Do you really want to close window?', 'Window close dialog', wx.YES_NO) ans = dialog.ShowModal() # YES=5103, NO=5104 if ans == 5103: print('User selected YES button to close window.') self.Destroy() |
参考動作↓
タブを作る
タブはまずwxFormBuilderのContainers→wxNotebookで入れ物を作り、その後にContainers→wxPanelで任意数のパネルを追加して作ります。
wxNotebook : https://docs.wxpython.org/wx.Notebook.html
wxNotebookだけではページ追加できないという仕様は以下の公式ディスカッションページが参考になりました。
i noticed that i can add a wxnotebook, but how can I add a page to this notebook.
→You have to add a new wxPanel, each time you add a new wxPanel you will have a new tab in the wxNoteBook.
https://forums.wxwidgets.org/viewtopic.php?t=6822
Macだとこんな見栄えです↓
タブ見出しの位置はstyleで変更可能です(左とか右とか)。
ファイル操作に関するイベント
ファイルパスを取得する
ファイルパスはwxFilePickerを使います。同じような動作をするウィジェットは他にもwxdirPicker等があります。
wxfilePickerCtrl : https://wxpython.org/Phoenix/docs/html/wx.FilePickerCtrl.html
wxFilePickerはAdditionalにあります。
イベントはファイルパスが変化した時に設定します。
コード例はこちら。pathはオブジェクトの名前を指定して.GetPath()で取得できます。オブジェクト名はwxFormBuilderにも書いてありますが、MainClass.pyを参照しても良いでしょう。
1 2 3 4 |
def m_filePicker1OnFileChanged( self, event ): '''Browseボタンをクリックしてpathを得る''' path = self.m_filePicker1.GetPath() print('User picked a file path:', path) |
動作例↓
ボタンからも同様のメソッドでパスを取得することができます。
以下の動画は直接wxFilePickerではなく、ボタンからパスを取得した例です。
取得したパスを静的テキストに書いたりしています。
パスを読めるようになったということで、csvやwavファイルといった外部ファイルをGUIから読みだすことができるようになったということですね。
フォルダ内の全ファイルをコンボボックスに設定する
次はコンボボックス(項目をリストから選択して値を取得するウィジェット)を使ってみます。コンボボックスは大変便利で、GUIプログラミングでよく使われます。
最も頻繁に使われる例としては、「フォルダ内のファイルを一括で設定し、後で選択できるようにする」といった操作ではないでしょうか。ここではフォルダを扱うwxdirPickerとコンボボックスwxComboBoxの2つを使って実装してみましょう。
wxComboBox : https://docs.wxpython.org/wx.ComboBox.html
wxdirPicker : https://docs.wxwidgets.org/3.0/classwx_dir_picker_ctrl.html
wxdirPickerはOnDirChangedにイベントを設定します。
wxComboBoxはOnComboBoxにイベント設定をすることで、プルダウンから項目を選択した時にイベントトリガーがかかります。
今回はパスリストを保持するために、ProjectMainFrame.pyのコンストラクタも編集しています。この書き方が良いかはわかりませんが、複数メソッドの中で共通の変数としたかったので…。
この辺は「Pythonのクラスの使い方とオブジェクト指向の考え方を理解する」で扱った内容なので、コンストラクタやメソッドについてはこちらの記事をご覧ください。
1 2 3 4 5 6 7 |
# Implementing MainFrame class ProjectMainFrame( MainClass.MainFrame ): def __init__( self, parent ): MainClass.MainFrame.__init__( self, parent ) # コンストラクタに変数を初期化して追加 self.path_list = [] |
イベント部分のメソッドはこんな感じにしてみました。コンボボックスに最初に値を入れた時に、初回の処理を書いておくのが良いかも知れませんね(TODO部分)。好みだと思いますが。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
def m_dirPicker1OnDirChanged( self, event ): '''フォルダ内の全ファイルパスをコンボボックスに設定する''' # フォルダ内のファイルパスをソートして取得 dir = self.m_dirPicker1.GetPath() self.path_list = sorted(glob.glob(os.path.join(*[dir, '*']))) # ファイルのフルパスから拡張子有りファイル名を抽出し、 # コンボボックスにファイル名とパスのセットを追加 for i in range(len(self.path_list)): file = os.path.basename(self.path_list[i]) self.m_comboBox1.Append(file, i) # コンボボックスの初期選択を先頭にする self.m_comboBox1.SetSelection(0) # TODO: 本来はここに最初の選択項目に対する処理を書く def m_comboBox1OnCombobox( self, event ): '''コンボボックスから項目を選択した時に、選択したファイル名とパスを取得する''' current_selection_name = self.m_comboBox1.GetStringSelection() current_selection_value = self.path_list[self.m_comboBox1.GetSelection()] print('Selection=', current_selection_name) print('path=', current_selection_value) |
以下動作例↓
matplotlibプロットに関するイベント
単純プロット
当ブログは数多くの記事でmatplotlibによるプロットを行っています。やはりGUIプログラミングでもmatplotlibを扱いたいと思います。
Tkinterでも「Tkinterでmatplotlibを埋め込んでグラフ表示する方法」で苦労しましたが、wxPython, さらにクラス表記の場合もひとクセありました。
今回は以下のページに記載のコードを参考にしました。https://gist.github.com/ikapper/765932799dd5dd36230b0d5205735bd3
ここでは少々やっかいなコードになりましたので、全コードをメモしておこうと思います。
MainClass.py
wxFormBuilderで生成したMainClass.pyは通常編集しませんが、wxFormBuilderでmatplotlibの設定がそのままあるわけではないので、今回は編集しました。
あらかじめwx.PanelをwxFormBuilderで配置させておき、bSizer1.Add()の部分でPanelの代わりに新しく作成したcanvasをSizerに追加しています。
まだこちらも仕組みがよく把握できていないので、この書き方で良いかは疑問…。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
# -*- coding: utf-8 -*- ########################################################################### ## Python code generated with wxFormBuilder (version 3.10.1-0-g8feb16b) ## http://www.wxformbuilder.org/ ## ## PLEASE DO *NOT* EDIT THIS FILE! ########################################################################### import wx import wx.xrc import matplotlib from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg from matplotlib.figure import Figure matplotlib.interactive(True) matplotlib.use('WXAgg') ########################################################################### ## Class MainFrame ########################################################################### class MainFrame ( wx.Frame ): def __init__( self, parent ): wx.Frame.__init__ ( self, parent, id = wx.ID_ANY, title = wx.EmptyString, pos = wx.DefaultPosition, size = wx.Size( 1000,600 ), style = wx.DEFAULT_FRAME_STYLE|wx.TAB_TRAVERSAL ) self.SetSizeHints( wx.DefaultSize, wx.DefaultSize ) bSizer1 = wx.BoxSizer( wx.VERTICAL ) self.m_panel1 = wx.Panel( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL ) # matplotlib matplotlib.rcParams['xtick.direction'] = 'in' matplotlib.rcParams['ytick.direction'] = 'in' self.fig = Figure() self.ax1 = self.fig.add_subplot(111) self.ax1.xaxis.set_ticks_position('both') self.ax1.yaxis.set_ticks_position('both') self.ax1.set_xlabel('x') self.ax1.set_ylabel('y') # canvas self.canvas = FigureCanvasWxAgg(self, -1, self.fig) bSizer1.Add(self.canvas, 1, wx.EXPAND | wx.ALL, 5) #bSizer1.Add(self.m_panel1, 1, wx.EXPAND | wx.ALL, 5) self.m_button1 = wx.Button( self, wx.ID_ANY, u"Draw", wx.DefaultPosition, wx.DefaultSize, 0 ) bSizer1.Add( self.m_button1, 0, wx.ALL, 5 ) self.SetSizer( bSizer1 ) self.Layout() self.Centre( wx.BOTH ) # Connect Events self.m_button1.Bind( wx.EVT_BUTTON, self.m_button1OnButtonClick ) def __del__( self ): pass # Virtual event handlers, override them in your derived class def m_button1OnButtonClick( self, event ): event.Skip() |
ProjectMainFrame.py
子フレームクラスをまとめたProjectMainFrame.pyにはイベントを設定しています。自分でdef plot()を作成し、ボタンイベントから呼び出して使うようにしてみました。
pop()とremove()を使って毎回データを消去しているのがポイントです。これをやらないとボタンをクリックする度にプロットが増えます。色も毎回指定していますが…もっと良い書き方があるのかな?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
"""Subclass of MainFrame, which is generated by wxFormBuilder.""" import wx import MainClass import matplotlib matplotlib.interactive(True) matplotlib.use('WXAgg') import numpy as np # Implementing MainFrame class ProjectMainFrame( MainClass.MainFrame ): def __init__( self, parent ): MainClass.MainFrame.__init__( self, parent ) def plot(self, event=None): '''サンプル波形をプロットするメソッド''' print('Draw button clicked.') x = np.arange(0, 10, 0.01) y = np.sin(2 * np.pi * 1 * x) + 0.5 * np.random.normal(loc=0, scale=1, size=len(x)) data = self.ax1.plot(x, y, color='red') self.canvas.draw() # 毎回ax1からデータを消去して常にボタンを押されたら1波形のみプロットする。 line = data.pop(0) line.remove() # Handlers for MainFrame events. def m_button1OnButtonClick( self, event ): self.plot() |
main.py
main.pyはこれまでと変わりませんが、念のためメモ。
1 2 3 4 5 6 7 8 |
import wx from ProjectMainFrame import ProjectMainFrame if __name__ == '__main__': app = wx.App() frame = ProjectMainFrame(None) frame.Show() app.MainLoop() |
以上3つのファイルを使ってmain.pyを実行すると、以下の動画に示す動作が確認できます。
まとめ
今後も追記予定
今回はここまでですが、このページを見ればいつでもwxPythonとwxFormBuilderの使い方がわかるように今後も新しい機能を使う時があれば追記していこうと思います。
wxFormBuilderで大まかなフレームを作成し、その後にカスタマイズしていくというやり方は僕のようなGUIプログラミングライト層のスタイルに合っているようです。
1から組むのは面倒ですが、半分自動生成してくれるというのはかなり楽でした。
GUIプログラミングへの抵抗感が減ってきました!
Twitterでも関連情報をつぶやいているので、wat(@watlablog)のフォローお待ちしています!