本ブログで紹介しているツールも含め、自作ツールはちょっとした作業を自動化することが目的なので、簡易的な画面で事足りることが大半です。
とはいうものの、CustomTkinterでGUIの画面を作ろうとした場合、数個のウィジェットを表示するだけでも、それなりのコードを書かなければなりません。
手間を省くために昔作った類似画面を修正することで、ある程度の生産性は向上しますが、不要なウィジェットの削除や文言の変更などでソースコードの中身を理解しなければならず、結構手間が掛かります。
そこで、今回はテキストに記述した内容に従って画面を表示するライブラリを紹介します。
CustomTkinterでよく使うウィジェットに限定し、必要最小限の機能に限定していますので、凝った画面は作成できませんが、自作ツールで必要な簡易画面程度であれば十分使えると思います。
create_ui の概要
create_uiとは、UI定義ファイル(テキストファイル)の内容に従って画面を作成するための簡易ライブラリです。
customtkinter の一部機能に絞っているため、画面デザインの自由度は無く、複雑なUI操作は実現できませんが、
自作ツールで使う簡易的なUI画面ならサクッと作ることができます。
例えば上記の画面であれば、UI定義ファイルに以下の内容を記述するだけで完成します。
title:PDF結合ツール
mode:dark
size:600,300
input_folder:入力フォルダ,選択
output_file:出力ファイル,選択
{
label:PDFの回転
rotate_combo:90,180,270
}
exec_button:PDF結合
UI画面の表示は、CreateUIクラスのインスタンスを生成し、showメソッドを呼び出すだけです。
ui = CreateUI(ボタンが押された時に呼び出される関数名)
ui.show()
画面に表示されたボタンが押されると、CreateUIクラスの引数で指定した関数が呼び出されます。この時、画面に入力されている情報が辞書形式で渡されます。
from create_ui import CreateUI
# ボタンが押された時に呼び出される関数
def exec(param):
print(param)
# 画面作成クラスのインスタンス生成
ui = CreateUI(exec)
# UI定義ファイルに従ってウィジェットを表示
ui.show()
上記のプログラムを実行し、UI画面の「PDF結合」ボタンを押すと、下記の内容がコンソール出力されます。
後ほど詳しく説明しますが、UI定義ファイルに記述したウィジェット名をキーにすることで、入力済みの値を取得したり、クリックしたボタンを特定できるようになっています。
{'input_folder': 'P:/data', 'output_file': 'P:/result', 'label': <customtkinter.windows.widgets.ctk_label.CTkLabel object .!ctkframe.!ctklabel>, 'rotate_combo': '180', 'exec_button': <customtkinter.windows.widgets.ctk_button.CTkButton object .!ctkbutton>,, '_clicked_widget': 'exec_button'}
ソースのダウンロード
以降に説明するサンプルソース一式は下記からダウンロード可能です。
簡易UI作成ライブラリ
ファイル名 | 内容 |
---|---|
create_ui.py | 今回紹介する簡易UI作成ライブラリ |
appconfig.py | 画面の入力内容の保存と復帰 |
customtkintercontrols.py | CustomTkinterの拡張機能 |
design.txt | UI定義ファイルのサンプル |
main.py | メインプログラム |
myfunc.py | ボタンクリック時に処理される関数のサンプルソース |
00365-4029209453.png | サンプル画像1 |
00352-1952246855.png | サンプル画像2 |
任意のフォルダに解凍後、コマンドプロンプロから次のコマンドを実行してください。
python main.py
UI定義ファイルの例と画面デザイン
下記は、UI定義ファイルと、それに基づいて表示される画面のサンプルです。
title:選択画面
mode:light
max:1024,900
size:600,900
min:600,900
select_file:ファイル選択,選択
_select_folder:フォルダ選択,選択
{
label:ON/OFFスイッチ3連
_main1_switch:スイッチ1
_main2_switch:スイッチ2
_main3_switch:スイッチ3
}
{
label:ON/OFFスイッチ単独
test1_switch:スイッチ Single
}
{<>
_my1_image:250,150,./00365-4029209453.png
_my2_image:250,150,./00352-1952246855.png
}
edit_text:100
input_check:チェックボックス
{
label:コンボボックス
baset_combo:選択1,選択2,選択3
line_entry:60,12345ABCDE
}
target_listbox:True,Select1,Select2,Select3
exec_button:実行,myfunc,func
input_fig:1,1
定義ファイルの記述は、画面全体に関するものと、ウィジェットに関するものに分かれます。
画面全体に関するものとして、 title,mode,max,min,size が用意されています。
一方、ウィジェットに関するものとして、file,folder,label,image,switch,checkbox,
label,message,listbox,image,entry,text,combobox,button,fig があります。
また、ウィジェットは { } を使って束ねることが可能です。
ウィンドウ全体の体裁を整えるには
ウィンドウに対して、ウィンドウタイトルやウィンドウサイズ、テーマ(ダーク、ライト)を指定することが可能です。
意味 | ウィジェット名 | パラメータ | 参考例 |
---|---|---|---|
画面タイトル | title | 任意の文字列 | title:選択画面 |
画面テーマ | mode | dark 又は ligh | mode:light |
画面サイズ上限 | max | 縦サイズ,横サイズ | max:1024,900 |
画面サイズ下限 | min | 縦サイズ,横サイズ | min:600,900 |
画面サイズ | size | 縦サイズ,横サイズ | size:600,900 |
title:選択画面
mode:light
max:1024,900
size:600,900
min:600,900
画面にウィジェットを表示するには
ウィンドウにウィジェットを表示したい場合は、ウイジェット名とパラメータを半角のコロン(:)で区切って表記します。
ウイジェット名 : パラメータ1,パラメータ2,パラメータ3,・・・
select_file:ファイル選択,選択
baset_combo:選択1,選択2,選択3
line_entry:60,12345ABCDE
現時点で下記のウィジェットをサポートしています。尚、ファイル選択ダイアログ、ファイル保存ダイアログ、フォルダ選択ダイアログ、複数行テキスト入力、画像のウィジェットに関しては、フォルダやファイルをドラッグ&ドロップすることが可能です。
意味 | ウィジェット名 | パラメータ | 参考例 |
---|---|---|---|
ファイル選択ダイアログ | ~ file | 項目名,ボタン表記 | select_file:ファイル選択,選択 |
ファイル保存ダイアログ | ~ save | 項目名,ボタン表記 | select_save:ファイル選択,選択 |
フォルダ選択ダイアログ | ~ folder | 項目名,ボタン表記 | select_folder:ファイル選択,選択 |
文字列 | ~ label | 任意の文字列(プログラムから変更不可) | label:ON/OFFスイッチ3連 |
メッセージ文字列 | ~message | 任意の文字列(プログラムから変更可能) | err_message:エラー |
リストボックス | ~listbox | フラグ,選択項目1,選択項目2,・・・ ※フラグ=true⇒複数選択を許可 | my_listbox:true,japan,usa,canada |
画像 | ~ image | 縦サイズ,横サイズ,画像ファイルのパス | my_image:250,150,P:/data/images2/00365-4029209453.png |
スイッチ | ~ switch | スイッチの右に表示したい文字列 | temperature_switch:温度センサー |
チェックボックス | ~ check | スイッチの右に表示したい文字列 | spel_check:スペルチェック |
1行テキスト入力 | ~ entry | 横サイズ,初期値の文字列 | line_entry:60,12345ABCDE |
複数行テキスト入力 | ~ text | 縦サイズ,初期値の文字列 | edit_text:100,ABCDE |
ボタン | ~ button | ボタン表記の文言,モジュール名,関数名 | exect_button:実行,myfunc,func |
コンボボックス | ~ combo | 選択項目1,選択項目2,・・・ | encode_combo:shift-jis,utf-8 |
ラジオボタングループ | ~group | なし | mode_group: |
ラジオボタン | ~radio | 項目名,グループ名,値 | readonly_radio:上書禁止,mode_group,1 |
グラフ | ~ fig | 横サイズ,縦サイズ | input_fig:1,1 |
Frameの開始 | { 又は {<> | { は pack(fill="x")、{<> は pack(expand=True) | {<> |
Frameの終了 | } | 1つの { 又は {<> に対して 1つの } を記述 | } |
ラジオボタンをグルーピングするには
複数のラジオボタンをグルーピングすることで、その中の1つを選択すると、自動的に他の選択が解除されます。
これは、グルーピングされた複数のラジオボタンが1つの変数を共有しており、ラジオボタンを選択したタイミングで、ラジオボタンごとに割り当てられた識別値(整数値)が変数に書き込むことで実現されています。
この共通変数は仮想的なウィジェット( ~group)を定義することで作成されます。下記の例では select_group: で変数を作成しています。
そして、ラジオボタン(~radio)の第2パラメータに~group を指定することで変数の値を共有します。
第3パラメータには、選択時に変数に書き込みたい識別値を整数で指定します。
update_group:
update1_radio:更新モード1,update_group,1
update2_radio:更新モード2,update_group,2
複数のグループを作成したい場合は、複数の共通変数を作成し、ラジオボタンの第2パラメータに指定してください。
update_group:
update1_radio:更新モード1,update_group,1
update2_radio:更新モード2,update_group,2
delete_group:
mode1_radio:削除モード1,delete_group,1
mode2_radio:削除モード2,delete_group,2
複数のウィジェットを束ねるには
複数のウィジェットを1つのFrameに束ねることが可能です。Frameの開始は "{" を記述し、Frameの終了は "}" を記述します。またFrameは入れ子にすることが可能です。
label:入力文字コード選択
input_encoder_combo:shift-jis,utf-8
label:出力文字コード選択
output_encoder_combo:shift-jis,utf-8
{
label:入力文字コード選択
input_encoder_combo:shift-jis,utf-8
label:出力文字コード選択
output_encoder_combo:shift-jis,utf-8
}
"{" の代わりに "{<>" を記述すると、ウィンドウサイズを左右に広げた際、Frameで束ねたウィジェットをウィンドウ中央に寄せることが可能です。
{
label:入力文字コード選択
input_encoder_combo:shift-jis,utf-8
label:出力文字コード選択
output_encoder_combo:shift-jis,utf-8
}
{<>
label:入力文字コード選択
input_encoder_combo:shift-jis,utf-8
label:出力文字コード選択
output_encoder_combo:shift-jis,utf-8
}
前回終了時の入力値を、次回起動時に復元するには
ウィジェット名は、画面終了時に直前の値を保存するか否かと、ウィジェットの種類を含める必要があります。
先頭がアンダースコアで始まる場合、そのウィジェットの値は次回起動時に復帰されません。
また、末尾にはウィジェットの種類を指定します。例えばボタンを表示したい場合は、ウィジェット名の末尾に button(exec_button や startbuttonなど) となります。
_select_file ⇒ ファイル選択ダイアログが表示され、画面終了時の選択状態が次回起動時に保存されない。input_entry ⇒ 1行入力のウィジェットが表示され、画面終了時の入力値が次回起動時に復元される。
ボタンが押された時の処理を書くには
CreateUIクラスの引数に関数を1つだけ指定することができます。この関数はすべてのボタンに共通するコールバック関数であり、画面上のどのボタンをクリックしても、この関数が呼び出されます。
param["_clicked_widget"] でクリック元のウィジェット名を取得することができるので、これを使うとボタンごとの処理が記述できます。
from create_ui import CreateUI
# ボタンが押された時に呼び出される関数
def exec(param):
if param["_clicked_widget"] == "exec_button":
# ここに、exec_buttonがクリックされた時の処理を記述
# 画面作成クラスのインスタンス生成
ui = CreateUI(exec)
# UI定義ファイルに従ってウィジェットを表示
ui.show()
また、UI定義ファイルにおいても、各 ~button ごとにボタンがクリックされた時に実行したいモジュール名と関数名を指定できるようになっています。
~button : ボタン表記 , モジュール名 , 関数名
例えば、 exec_button が押された時、myfunc.py に定義されている func 関数を呼び出したい場合、次のように記述します。
exec_button:実行,myfunc,func
UI定義ファイルのボタンウィジェットにモジュール名と関数名が指定されている状態で、CreateUIの引数に関数が渡された場合、CreateUI引数の関数が優先されます。
ウィジェットにプログラムから値を設定するには
CreateUI クラスのインスタンス変数に widgets があり、ここにUI定義ファイルに記述した全てのウィジェットが、辞書形式で格納されています。例えば、 ui = CreateUI() でインスタンスを生成したと仮定すると、次の記述になります。
ui.widgets[ウィジェット名].set(値)
スイッチや チェックボックスは select もしくは deselect メソッドを呼び出します。
ui.widgets[ウィジェット名].select() #選択状態にする
ui.widgets[ウィジェット名].deselect() #選択状態を解除する
ui.widgets["_main1_switch"].select()
ui.widgets["_main2_switch"].deselect()
メッセージボックスは、内部で CTkLabel() を呼び出しているだけなので、CusomTkinter のウィジェットに標準実装されている configure() を使って文字列を表示します。
ui.widgets[ウィジェット名].configure(text="任意の文字列")
ui.widgets["err_message"].configure(text="ファイルを更新できません。")
リストボックスは、セットしたい値をリスト形式としてset_items() の引数に渡します。
ui.widgets[ウィジェット名].set_items([値1,値2,値3])
ui.widgets["my_listbox"].set_items(["1","2","3"])
グラフを表示するには
UI定義ファイルに記述したグラフ表示用のウィジェット名(末尾が fig )を使って、matplotlib の fig を取り出すことが可能です。例えば input_fig というウィジェット名なら、次のようになります。
fig = x[ウィジェット名].fig
取り出した fig を使って matplitlibでグラフを描画し、最後に draw() メソッドを呼び出します。
x[ウィジェット名].draw()
# グラフ描画の関数
def func(x):
fig = x["input_fig"].fig
ax = fig.add_subplot(111)
ax.plot([1, 2, 3, 4, 3, 2, 1], [1, 4, 2, 3, 3, 2, 1])
fig.tight_layout()
x["input_fig"].draw()
ui = CreateUI(func)
ui.show()
任意の画像を表示するには
末尾が image で終わるウィジェット名を記述し、横サイズ、縦サイズ、画像ファイル名を指定します。
~image : 横サイズ , 縦サイズ , 画像ファイル名
my_image:250,150,P:/data/images2/00365-4029209453.png
プログラムから画像をセットする場合は、 CreateUI クラスのインスタンス変数 widgets を使います。
ui.widgets[ウィジェット名].set(画像ファイルのパス)
# 画像の表示関数
def func(x):
ui.widgets["_my1_image"].set("./00352-1952246855.png")
ui = CreateUI(func)
ui.show()
ソースコード
CreateUIクラスのソースコードは次の通りです。
ファイル名 | create_ui.py |
---|
import os
from datetime import datetime
import customtkinter as ctk
from PIL import Image, ImageTk
from customtkintercontrols import App
from customtkintercontrols import FileSelectionControl
from customtkintercontrols import FolderSelectionControl
from customtkintercontrols import SaveFileDialogControl
from customtkintercontrols import EntryControl
from customtkintercontrols import TextBoxControl
from customtkintercontrols import ComboBoxControl
from customtkintercontrols import FigureCanvas
from customtkintercontrols import ImageControl
from customtkintercontrols import ListBoxControl
from appconfig import AppConfig
class CreateUI:
"""
テキストで定義された内容に従って画面にウィジェットを配置する
"""
def __init__(self,command=None,design_path="./design.txt",config_path="./config.json"):
self.design_path = design_path
self.command = command
self.widgets = {}
self.stack = []
self.group = {}
# 前回入力値の保存・復元用インスタンスの生成
self.conf = AppConfig(config_path)
# メインウィンドウの作成
self.app = App()
# ウィンドウを閉じる時に呼ばれる関数
def close_window(self):
# ラジオボタン用グループ変数の復元
for key,val in self.group.items():
self.conf.set(key,val.get())
# 画面入力値を保存
for key,widget in self.widgets.items():
if hasattr(widget, 'get') :
self.conf.set(key,widget.get())
self.conf.write()
# プログラムの終了
self.app.quit()
# ボタンが押された時のコールバック関数
def call_back(self,func,widget_name):
param = {k:v.get() if hasattr(v, 'get') else v for k,v in self.widgets.items() }
param.update({k: v.get() for k, v in self.group.items()})
param['_clicked_widget'] = widget_name
func(param)
def show(self):
# -----------------------------------------------
# メインウインドウの定義
# -----------------------------------------------
# ウィンドウを閉じる時に呼びたい関数を登録
self.app.protocol("WM_DELETE_WINDOW", self.close_window)
# -----------------------------------------------
# 画面レイアウト(ウィジェット)の定義
# -----------------------------------------------
# 定義ファイルの参照と解析
with open(self.design_path,"r",encoding="utf-8") as f:
# ファイルの内容に従ってウィジェットを生成する
for line in f.readlines() :
# 改行を削除し、コロンで分割
item = line.strip().split(":")
# 空行またはコメント行(#)なら読み飛ばす
if item[0] == "" or item[0].startswith("#"):
continue
# 0番目(キー)を取得
name = item[0].strip()
# グループの開始なら、そのグループに以降のウィジェットを配置
if name.startswith("{") :
self.stack.append(self.app)
self.app = ctk.CTkFrame(self.app)
if "<>" in name :
self.app.pack(padx=10,pady=10,expand=True)
else:
self.app.pack(padx=10,pady=10,fill="x")
continue
# グループの終了なら、ウィジェットの配置先をルートに戻す
if name == "}":
self.app = self.stack.pop()
continue
# 1番目(バリュー)を取得し、カンマで分割
values = item[1].split(",")
# ファイルの内容に従って生成したウィジェットを格納するための変数を初期化
widget = None
# キーに従って処理を分岐
if name == "title":
self.app.title(values[0])
continue
# 画面サイズ
elif name == "size":
self.app.geometry(f"{values[0]}x{values[1]}")
continue
# 画面の最小サイズ
elif name == "min":
self.app.minsize(width=int(values[0]), height=int(values[1]))
continue
# 画面の最大サイズ
elif name == "max":
self.app.maxsize(width=int(values[0]), height=int(values[1]))
continue
# dark又はlight モーdの指定
elif name == "mode":
ctk.set_appearance_mode(values[0])
continue
# グラフ描画
elif name.endswith("fig"):
self.widgets[name] = FigureCanvas(figsize=(int(values[0]),int(values[1])))
continue
# 画像表示
elif name.endswith("image"):
width = int(values[0])
height = int(values[1])
img = line.strip().split(',')
widget = ImageControl(self.app,width,height)
if len(img) > 2:
widget.set(img[2])
# ラベル
elif name == "label":
widget = ctk.CTkLabel(self.app,text=values[0])
# メッセージ表示ラベル
elif name.endswith("message"):
widget = ctk.CTkLabel(self.app,text=values[0])
if len(values) > 0:
widget.configure(text=values[0])
# リストボックス
elif name.endswith("listbox"):
multi = True if values[0].lower() == "true" else False
widget = ListBoxControl(self.app,multiple_selection=multi)
if len(values) > 1:
widget.set_items(x for x in values[1:])
# ファイル選択ダイアログ
elif name.endswith("file"):
widget = FileSelectionControl(self.app)
widget.label.configure(text=values[0])
widget.button.configure(text=values[1])
# ファイル選択ダイアログ(保存用)
elif name.endswith("save"):
widget = SaveFileDialogControl(self.app)
widget.label.configure(text=values[0])
widget.button.configure(text=values[1])
# フォルダ選択ダイアログ
elif name.endswith("folder") :
widget = FolderSelectionControl(self.app)
widget.label.configure(text=values[0])
widget.button.configure(text=values[1])
# スイッチ
elif name.endswith("switch") :
widget = ctk.CTkSwitch(self.app, text=values[0])
# チェックボックス
elif name.endswith("check") :
widget = ctk.CTkCheckBox(self.app, text=values[0])
# チェックボックス用グループ
elif name.endswith("group") :
self.group[name] = ctk.IntVar()
self.group[name].set(self.conf.get(name,0))
continue
# ラジオボタン
elif name.endswith("radio") :
widget = ctk.CTkRadioButton(self.app, text=values[0],variable=self.group[values[1]],value=int(values[2]))
# 1行入力
elif name.endswith("entry") :
widget = EntryControl(self.app)
widget.entry.configure(width=int(values[0]))
if len(values) > 1:
widget.set(values[1])
# エリア入力
elif name.endswith("text") :
widget = TextBoxControl(self.app)
val = int(values[0])
if val >= 0:
widget.textbox.configure(height=val)
if len(values) > 1:
widget.set(values[1])
# コンボボックス
elif name.endswith("combo") :
widget = ComboBoxControl(self.app)
if len(values) > 0:
widget.set_items([x for x in values])
# ボタン
elif name.endswith("button"):
if self.command is None:
if len(values) > 2 and os.path.isfile(values[1]):
module = __import__(values[1])
func = getattr(module, values[2])
else:
func = self.command
widget = ctk.CTkButton(self.app,text=values[0],height=40,command=lambda n=name:self.call_back(func,n))
# ウィジェットを画面に配置
if widget is not None:
self.widgets[name] = widget
if len(self.stack) > 0:
widget.pack(padx=10,pady=10,side="left")
else:
widget.pack(padx=10,pady=10,fill="x")
# -----------------------------------------------
# 前回実行時の画面入力値を復元
# -----------------------------------------------
# 各種ウイジェットの復元
for key,widget in self.widgets.items():
if key.startswith("_"):
continue
if isinstance(widget, ctk.CTkCheckBox) or isinstance(widget, ctk.CTkSwitch):
if self.conf.get(key):
widget.select()
else :
widget.deselect()
elif hasattr(widget, 'set') :
val = self.conf.get(key)
widget.set("" if val is None else val)
# -----------------------------------------------
# CustomTkinterの起動
# -----------------------------------------------
# メインループを開始
self.app.mainloop()
まとめ
今回は、自作ツールで簡単なUI画面を作りたい時に重宝する create_ui ライブラリ について、その使い方とUI定義ファイルの記述方法について詳しく解説しました。
custumtkinter の一部機能に絞ったことで画面デザインの自由度は犠牲になりましたが、その代わりにUIの定義が非常にシンプルになり、簡単な画面がサクッと作れるようになっています。
とにかく簡単なUIで良いから時間を掛けずに作りたい場合は、是非このライブラリを活用してみてください。
コメント