📜

【TkEasyGUI】ラジオボタンの選択制御を実装する方法

2024/09/10に公開

PySimpleGUIのようにグループ化したラジオボタンをボタンなどで切り替えするものをTkEasyGUIで実装する方法。

PySimpleGUI

ボタンを押すとラジオボタンの選択が変更されています。

TkEasyGUI
コードを変更する前

ボタンを押しても、ラジオボタンの選択が変更されずテキストが数字に更新されています。

コードを変更する後

PySimpleGUIのように、ボタンを押すとラジオボタンの選択が変更されています。

実験コードに同じものが試せるものを置いています。


実行環境

  • os Windows
  • TkEasyGUI 0.2.73
  • Python 3.10.11

https://github.com/kujirahand/tkeasygui-python/blob/main/docs/TkEasyGUI/utils-py.md#set_theme

TkEasyGUIバージョン0.2.73のRadioクラスの update メソッドにはラジオボタンのテキスト変更しか備わっておらず、PySimpleGUIのようにボタンや設定ファイルの情報を読み取り、切り替えするにはコードの書き換えが必要です。

コードだけほしい方は目次からRadioクラス全体のコードに飛んでください。

コードの修正、追加の解説

TkEasyGUIのフォルダの widgets.py ファイルを開きます。
Radioクラスを探してください、1724行あたりにあると思います。

  • 変更するメソッド

    • update
    • select
    • create
  • 新たに加えるメソッド

    • deselect

update メソッドの修正

def update(self, value=None, text: Union[str, None] = None, **kw) -> None:
    """Update the widget."""
    if text is not None:
        self.set_text(text)
    if value is not None:
        self.select()

update メソッドを使ってラジオボタンの状態(選択されているかどうか)を変更した場合にも、グループ内で他のボタンが自動的に非選択状態になるようにします。

  • 変更点
    • value が指定された場合、self.select() メソッドを呼び出しています。これにより、選択されたラジオボタンだけが有効になり、他のグループ内のボタンは解除されます。
    • もともとは value の更新のみが行われていましたが、select() を通してグループ内の一貫性を保つ処理を行うようにしました。

select メソッドの修正

def select(self) -> None:
    """Select the radio button and unselect others in the group."""
    for radio in self.window.radio_group_dict[self.group_id][1]:
        if radio != self:
            radio.deselect()  # Unselect other radio buttons in the same group
    self.window.radio_group_dict[self.group_id][0].set(self.value)

ラジオボタンのグループ内で一度に1つのボタンだけが選択されるようにするための処理です。

  • 変更点
    • ラジオボタンのグループ(self.group_id)に属している他のラジオボタンをすべて非選択deselect())状態にします。
    • 具体的には、グループ内のラジオボタンオブジェクトをループで回し、self 以外のものにはdeselect() を呼び出すことで、グループ内で他のボタンを非選択にします。
    • 最終的に、現在のラジオボタンの値を set(self.value) で選択状態にします。

create メソッドの修正

def create(self, win: Window, parent: tk.Widget) -> tk.Widget:
    # create radio group
    if self.group_id not in win.radio_group_dict:
        win.radio_group_dict[self.group_id] = [tk.IntVar(value=0), []]  # Second element will store radio button objects
        win.radio_group_dict[self.group_id][0].trace_add("write", lambda *args: self.disptach_event({"event_type": "change", "event": args}))
    win.radio_group_dict[self.group_id][1].append(self)  # Store the radio button object itself
    self.value = len(win.radio_group_dict[self.group_id][1])  # Value is the number of radio buttons in the group
    
    # create radiobutton
    self.widget = ttk.Radiobutton(
        parent,
        value=self.value,
        variable=win.radio_group_dict[self.group_id][0],
        style=self.style_name,
        **self.props)
    if self.default_value:
        self.select()
    return self.widget
  1. ラジオボタングループの作成・管理部分
if self.group_id not in win.radio_group_dict:
    win.radio_group_dict[self.group_id] = [tk.IntVar(value=0), []]

すべてのラジオボタンをグループで管理するため、win.radio_group_dict という辞書にグループIDをキーとして格納します。

  • 変更点
  • win.radio_group_dict[self.group_id] に、2つの要素を持つリストを作成しています。
    • 1つ目の要素は tk.IntVar(value=0) です。これは、グループ内のラジオボタンの選択状態を管理する変数です。0 はデフォルトの未選択状態を意味します。
    • 2つ目の要素は空のリスト [] で、これは後ほど各ラジオボタンのオブジェクトを格納するためのリストです。
  1. trace_add で変更検知を追加
win.radio_group_dict[self.group_id][0].trace_add("write", lambda *args:self.disptach_event({"event_type": "change", "event": args}))

ラジオボタンの選択状態が変更された際にイベントを検知し、反応できるようにするためのコードです。

  • 変更点:
    • trace_add("write") は、IntVar が書き換えられた(つまり選択が変更された)ときに自動でコールバックを呼び出します。
    • ここでは self.disptach_event({"event_type": "change", "event": args}) を使ってイベントを発火させています。これにより、ラジオボタンの選択が変更されると、外部からその変更に応じた処理が実行できるようになります。
  1. ラジオボタンオブジェクトをグループに追加
win.radio_group_dict[self.group_id][1].append(self)

グループ内のラジオボタンオブジェクトそのものを保存し、他のボタンを非選択にするために参照できるようにします。

  • 変更点
    • self(現在のラジオボタンオブジェクト)を、グループに対応するリスト(2番目の要素)に追加しています。
    • これにより、グループ内のすべてのラジオボタンオブジェクトがリスト内で管理され、後から他のボタンの選択状態を変更する際に利用できます。
  1. value の設定
self.value = len(win.radio_group_dict[self.group_id][1])

ラジオボタンの value 属性を設定します。value は、このラジオボタンがグループ内で何番目のボタンかを示すために使われます。

  • 変更点
    • len(win.radio_group_dict[self.group_id][1]) により、現在のグループ内のボタン数を取得し、それを self.value に割り当てています。
    • つまり、グループ内で新しく追加されたラジオボタンは、ボタン数に応じた値を持ちます。これにより、選択状態を管理しやすくなります。

deselect メソッドの追加

def deselect(self) -> None:
    """Deselect the radio button."""
    self.window.radio_group_dict[self.group_id][0].set(0)

ラジオボタンを非選択状態にするためのメソッドです。

  • self.window.radio_group_dict[self.group_id][0].set(0) を使って、現在のラジオボタンの変数に 0 を設定し、非選択状態にします。

Radioクラス全体のコード

class Radio(Element):
    """Checkbox element."""
    def __init__(
                self, text: str="",
                group_id: Union[int, str] = "group",
                default: bool = False,
                key: Union[str, None] = None,
                enable_events: bool = False,
                # other
                metadata: Union[dict[str, Any], None] = None,
                **kw) -> None:
        if key is None or key == "":
            key = text
        super().__init__("Radio", "TRadiobutton", key, True, metadata, **kw)
        self.use_ttk = True
        self.default_value = default
        self.value: int = 0
        self.props["text"] = text
        self.group_id: str = str(group_id)
        if enable_events:
            self.bind_events({
                "<Button-3>": "right_click"
            }, "system")

    def create(self, win: Window, parent: tk.Widget) -> tk.Widget:
        # create radio group
        if self.group_id not in win.radio_group_dict:
            win.radio_group_dict[self.group_id] = [tk.IntVar(value=0), []]  # Second element will store radio button objects
            win.radio_group_dict[self.group_id][0].trace_add("write", lambda *args: self.disptach_event({"event_type": "change", "event": args}))
        win.radio_group_dict[self.group_id][1].append(self)  # Store the radio button object itself
        self.value = len(win.radio_group_dict[self.group_id][1])  # Value is the number of radio buttons in the group
        
        # create radiobutton
        self.widget = ttk.Radiobutton(
            parent,
            value=self.value,
            variable=win.radio_group_dict[self.group_id][0],
            style=self.style_name,
            **self.props)
        if self.default_value:
            self.select()
        return self.widget
    
    def deselect(self) -> None:
        """Deselect the radio button."""
        self.window.radio_group_dict[self.group_id][0].set(0)
        
    def select(self) -> None:
        """Select the radio button and unselect others in the group."""
        for radio in self.window.radio_group_dict[self.group_id][1]:
            if radio != self:
                radio.deselect()  # Unselect other radio buttons in the same group
        self.window.radio_group_dict[self.group_id][0].set(self.value)
        
    def is_selected(self) -> bool:
        """Check if the radio button is selected."""
        return self.window.radio_group_dict[self.group_id][0].get() == self.value

    def get_value(self) -> bool:
        """Get the value of the widget."""
        return self.value

    def get(self) -> Any:
        """Get the value of the widget."""
        return self.is_selected()

    def set_text(self, text: str) -> None:
        """Set the text of the widget."""
        self.props["text"] = text
        self._widget_update(text=text)
        
    def set_value(self, b: bool) -> None:
        """Set the value of the widget."""
        self.widget.set(b)
        
    def update(self, value=None, text: Union[str, None] = None, **kw) -> None:
        """Update the widget."""
        if text is not None:
            self.set_text(text)
        if value is not None:
            self.select()
            
        self._widget_update(**kw)

実験コード

import TkEasyGUI as eg
# import PySimpleGUI as eg

# レイアウトの作成
layout = [
    [eg.Radio('選択肢1', group_id=0, key="RADIO1", default=True), eg.Radio('選択肢2', group_id=0, key="RADIO2")],
    [eg.Button('選択肢2に変える', key='-CHANGE1-'), eg.Button('選択肢1に変える', key='-CHANGE2-')],
    [eg.Button('交互に変える', key='-CHANGE3-')]
]

# ウィンドウの作成
window = eg.Window('ラジオボタン切り替え', layout)

counter = 0
# イベントループ
while True:
    event, values = window.read()
    counter += 1
    
    if event == eg.WINDOW_CLOSED:
        break
    if event == '-CHANGE1-':
        # ラジオボタンの値を切り替える
        window['RADIO2'].update(True)
    if event == '-CHANGE2-':
        # ラジオボタンの値を切り替える
        window['RADIO1'].update(True)
    if event == '-CHANGE3-':
        if counter % 2 == 0:
            window['RADIO1'].update(True)
        else:
            window['RADIO2'].update(True)

# ウィンドウを閉じる
window.close()

Discussion