記事の概要
PySimpleGUIを使うプログラムで、画面部位に応じてモジュール設ける方式を試しました。この方式ではコールバックを使って依存を避けます。
上部に「一覧表」という画面部位があり、本の一覧が表の形で表示されます。
下部に「書誌枠」という画面部位があり、本の書誌(ISBN、著者、標題)が表示されます。
「一覧表」で行を選択すると、その本の書誌が「書誌枠」に表示されます。
「書誌枠」で入力を行い、適用ボタンをクリックすると、変更が「一覧表」に反映されます。
例題プログラムのモジュール構造を図示します。矢印は依存の向きです。
4モジュールのうち、ListPartとBiblioPartが画面部位モジュールです。
※各モジュールのコードは後方にあります。
※参考情報を末尾に書きました。
方式の説明
この方式では「メイン」「画面部位モジュール」「その他」という3種のモジュールを設け、「メイン」と各「画面部位モジュール」は一定の様式で記述します。
メイン(Main)
メインには「イベント波及マップ」「波及処理関数」があり、レイアウトの作成とメインループ内のディスパッチが独特の形です。
イベント波及マップ
「イベント波及マップ」は「イベントが生じた画面部位の名称」から「画面の更新が必要な画面部位のredraw()関数のリスト」を引く辞書です。
波及処理関数
「波及処理関数」は各画面部位のイベント処理関数からコールバックされ、「イベント波及マップ」を参照して、他の画面部位モジュールのredraw()関数を呼びます。その結果、他の画面部位の再描画が生じます。
レイアウトの作成
レイアウトの作成に当たっては各画面部位モジュールのmake_layout()関数を呼び、その結果を使って全体のレイアウトを組み立てます。
ディスパッチ
イベントループ内のディスパッチは次の3ケースから成ります。
- プログラム終了ケース
Windowsの画面の×ボタンクリック時のケースであり、メインループを抜けます。 - タプル型イベントのケース
一覧表の行選択イベントで生じるケースであり、event[0]が関数であり、それを呼びます。その際に「波及処理関数」を引数として渡します。 - 関数イベントのケース
eventが関数であり、それを呼びます。その際に「波及処理関数」を引数として渡します。
画面部位モジュール(ListPart.、BiblioPart)
画面部位モジュールには、再描画関数redrow()、イベント処理関数on_foo()、レイアウトの該当部分を作る関数make_layoyut()があります。
エレメントの作成時には、キーにイベント処理関数on_foo()そのものを指定します。この結果、メインのディスパッチでは名前を使わずにイベント処理関数を呼び出すことが出来ます。
イベント処理関数はその画面部位に関わる処理を行った後、引数で与えられた「波及処理関数」をコールバックします。この結果、メインの中で他の画面部位の再描画が生じます。
方式の評価
この方式の良い点を挙げます。
- ある画面部位の画面か機能に変更が生じた場合、コードが変わるのはその画面部位モジュールだけです。メインまたは他の画面部位は変わりません。
- 画面部位の追加時に、既存の画面部位モジュールは変わりません。変わるのは、メインの「イベント波及マップ」と「レエイアウトの作成」の箇所のみです。
- 画面部位構成が変わっても、ディスパッチは変わりません。
この方式の悪い点を挙げます。
- イベント処理の引数が様式として決まっているので、引数が冗長な場合があります。
- イベント処理が戻り値でディスパッチの動きを指図しています。
この方式で解決しなかった点を挙げます。
- 入出力項目(Input、Multiline)のキーを一意にするために命名規則を使いました。
- 画面部位モジュールの内部のみで使う名前(on_foo)を隠していません。
例題コード
4つのファイルを同一のフォルダに夫々のファイル名で保存し、Main.pywを走らせると動きます。
この例題は、Windows11(22H2)上のVisual Studio 2022 Communityで作りました。
Main.pyw
import PySimpleGUI as sg
import BookData
import ListPart
import BiblioPart
# イベント波及マップ
event_distribution = {
'ListPart':[BiblioPart.redraw],
'BiblioPart':[ListPart.redraw],
}
# イベント波及コールバック
def xfer_event(part, window):
funcs = event_distribution[part]
for func in funcs:
func(window)
pass
# メイン処理
sg.theme('Default1')
layout = [
[ListPart.make_layout()],
[BiblioPart.make_layout()],
[sg.Text('国立国会図書館サーチのAPIで書誌情報を取得しています')],
]
window = sg.Window('Bookshelf', layout)
while True: # Event Loop
event, values = window.Read()
if event is None or event == 'Exit': #Windowsの×ボタン
break
elif isinstance(event, tuple): #タプル型イベント(sg.Tableのクリック)
if callable(event[0]):
more = event[0](event, values, xfer_event, window)
if more == True: continue
elif more == False: break
elif callable(event): #関数イベント(sg.Buttonのクリック)
more = event(values, xfer_event, window)
if more == True: continue
elif more == False: break
window.Close()
ListPart.py
import PySimpleGUI as sg
import BookData
# 表示更新関数
def redraw(window, keep_current=False):
show = []
for data in BookData.bookshelf_data:
line = []
line.append(data[BookData.I_ISBN])
line.append(data[BookData.I_AUTHOR])
line.append(data[BookData.I_TITLE])
show.append(line)
window[on_table].update(show)
if keep_current:
window[on_table].update(select_rows=[BookData.selected_book])
# テーブルの行選択時イベント処理関数
def on_table(event, values, xfer_event, window):
row, col = event[2]
if row is not None:
BookData.selected_book = row
xfer_event('ListPart', window)
return True
return None
# レイアウト作成関数
def make_layout():
table = sg.Table(
key=on_table,
values=BookData.bookshelf_data,
enable_click_events=True, #セルのクリックを拾う。eventはtuple。
headings=BookData.HEADING,
num_rows=10,
auto_size_columns=True,
justification='left',
selected_row_colors=("#000060", "#ccddff"),
expand_x=True,
expand_y=True,
)
return table
BiblioPart.py
import PySimpleGUI as sg
import BookData
# 表示更新関数
def redraw(window):
items = ['','',''] if BookData.selected_book is None else BookData.bookshelf_data[BookData.selected_book]
window['-BiblioIsbn-'].update(items[BookData.I_ISBN])
window['-BiblioAuthor-'].update(items[BookData.I_AUTHOR])
window['-BiblioTitle-'].update(items[BookData.I_TITLE])
# 適用ボタンク処理関数
def on_apply(values, xfer_event, window):
if(BookData.selected_book is None): return
list = BookData.bookshelf_data[BookData.selected_book]
list[BookData.I_TITLE] = values['-BiblioTitle-']
list[BookData.I_AUTHOR] = values['-BiblioAuthor-']
BookData.selected_book = None
redraw(window)
xfer_event('BiblioPart', window)
return True
# レイアウト作成関数
def make_layout():
contentIsbnPart = [[
sg.Text('ISBN', size=(6,1)),
sg.Input(key='-BiblioIsbn-', size=(13,1), readonly=True),
]]
contentAuthorPart = [[
sg.Text('著者', size=(6,1)),
sg.Multiline(key='-BiblioAuthor-', size=(100,2)),
]]
contentTitlePart = [[
sg.Text('標題', size=(6,1)),
sg.Multiline(key='-BiblioTitle-', size=(100,2)),
]]
elmContentApplyButton = sg.Button('適用', key=on_apply)
frame = sg.Frame('書誌', [
[sg.Column(contentIsbnPart)],
[sg.Column(contentAuthorPart)],
[sg.Column(contentTitlePart)],
[elmContentApplyButton],
])
return frame
BookData.py
# データ項目名
HEADING = ['ISBN', '著者', '標題']
# データ項目番号
I_ISBN = 0
I_AUTHOR = 1
I_TITLE = 2
# 蔵書一覧データ
bookshelf_data = [
['9784320026926', 'Brian W. Kernighan、Dennis M. Ritchie、石田 晴久', 'プログラミング言語C : ANSI規格準拠 第2版'],
['9784810180473', 'Bjarne Stroustrup、斎藤 信男', 'プログラミング言語C++ 第2版'],
]
# 選択されている行
selected_book = None
参考情報
Buttonのキーに関数を指定するアイデアはDemo_Dispatchers.pyで知りました。
PySimpleGUIの使い方については、主にPySimpleaGUI公式サイトのcall_referenceタブを参照しました。
モジュール分割にあたっては、凝集度・結合度・依存等を考慮しました。参考書を挙げておきます。
- 高信頼性ソフトウェア-複合設計…ソフトウェア工学書(1975年著)
- 継続的デリバリーのソフトウェア工学…ソフトウェア工学書(2022年著)