SWELL公式サイトへ 詳しくはこちら

【第8回】状態の境界を引く|tkinterでUIが持つもの・持たないもの

  • URLをコピーしました!
目次

はじめに

前回は、状態を“クラス”として扱うことで、tkinter アプリを少しずつ構造化していける、という話を書きました。

ウィジェットのあちこちに値や処理を分散させるのではなく、状態をひとつのまとまりとして扱う。
それだけでも、コードの見通しはかなり変わってきます。

ただ、実際に状態クラスを作り始めると、次の疑問が出てきます。

「この状態クラスには、何を入れるべきなのか」
「逆に、何を入れないほうがよいのか」

ここが曖昧なままだと、せっかく状態をクラスにしても、結局は“なんでも入れ箱”になってしまいます。
それでは、構造化したつもりでも、別の形で複雑さを抱え込むことになります。

tkinter のコードが難しくなりやすい理由のひとつは、UIの都合で必要な情報と、アプリの本質として持つべき情報が、同じ場所に混ざりやすいことです。

たとえば、次のようなものはすべて「状態」と呼べそうです。

  • 入力欄にいま入っている文字
  • 選択中のタブ
  • Todo の一覧データ
  • 保存すべき設定値
  • 画面に表示する件数
  • 入力エラーの有無

ですが、これらは全部同じ種類の状態ではありません。

今回は、tkinter で状態管理を考えるときにとても重要になる、
「UIが持つべき状態」と「UIが持つべきでない状態」
の境界について整理してみます。

1. なぜ「状態の境界」を考える必要があるのか

tkinter は、ボタン、ラベル、入力欄、コンボボックスなどを直接組み合わせながら、画面を作っていくスタイルです。
このやり方は直感的でわかりやすい反面、少しずつ機能が増えてくると、UIとデータと処理が密着しやすくなります。

たとえば、最初はこんな感覚で書き始めることが多いと思います。

  • ボタンが押されたら Entry.get() で値を読む
  • 読み取った値をそのままリストに追加する
  • ラベルやリストボックスをその場で更新する
  • 必要ならファイルにも保存する

小さなアプリなら、これでもちゃんと動きます。
むしろ、最初はこのくらいのほうが作りやすいです。

しかし、機能が増えてくると、次のようなことが起こり始めます。

  • どの値が「一時的な入力中の値」なのか分からなくなる
  • どの値が「正式なアプリのデータ」なのか曖昧になる
  • 保存処理と表示処理が密着してしまう
  • 別画面や別機能から再利用しにくくなる
  • 修正した場所とは別の画面で不具合が出る

つまり、状態の種類が区別されないまま増えていくのです。

このとき必要になるのが、状態に境界を引くことです。

状態をひとまとめに扱うのではなく、

  • UIのためだけに存在する状態
  • アプリの中心として持つ状態
  • 外部に保存される状態
  • 計算して導き出せる状態

を分けて考える。
この整理ができると、コードの責務が見えやすくなり、変更にも強くなります。

2. 状態にはいくつかの種類がある

「状態」という言葉は便利ですが、便利すぎるぶん、何でも同じように見えてしまうことがあります。
ここでは、tkinter アプリでよく出てくる状態を、4つの種類に分けて考えてみます。

2-1. UI状態

これは、画面の見え方や操作中の一時情報です。

たとえば、次のようなものです。

  • どのタブが開かれているか
  • チェックボックスがオンかオフか
  • 入力欄に現在何が入っているか
  • ダイアログが開いているか
  • ボタンを一時的に無効化しているか

これらは、画面を操作するために必要な情報です。
ただし、アプリの本質そのものではありません。

たとえば、Todoアプリで入力欄に「牛乳を買う」と入力している最中の文字列は、たしかに状態です。
でも、それはまだ「正式なTodoデータ」ではありません。
あくまでUI上で編集している途中の値です。

2-2. アプリケーション状態

これは、アプリの中心にある、本質的なデータです。

たとえば、次のようなものです。

  • Todo一覧
  • 現在選択しているタスクID
  • 検索条件
  • フィルタ条件
  • 業務データや一覧データそのもの

この状態は、画面の見た目が多少変わっても意味を持ちます。
入力欄が Entry であっても、別画面で編集していても、本質は変わりません。

UIが変わっても残るべき情報は、UIの中ではなく、アプリ側の状態として持つほうが自然です。

2-3. 永続化される状態

これは、アプリを終了したあとも残したい情報です。

たとえば、次のようなものです。

  • 設定ファイルの内容
  • 前回使った保存先フォルダ
  • ウィンドウサイズ
  • ユーザーごとの表示設定
  • 最後に選んだタブやフィルタ条件

これらは、実行中の一時状態とは少し性質が違います。
アプリの内部で保持するだけでなく、ファイルやDBなど外部に保存される可能性があるからです。

ここを意識せずに混ぜてしまうと、「今だけ必要な値」と「長く残すべき値」が同じ扱いになり、設計がぼやけます。

2-4. 導出される状態

これは、元の状態から計算して求められるものです。

たとえば、次のようなものです。

  • Todoの件数
  • フィルタ後の表示リスト
  • 入力値が有効かどうかの判定結果
  • 合計金額
  • ラベルに表示する要約文字列

これらは便利なので、つい変数として保存したくなります。
ですが、元のデータから毎回求められるなら、むやみに保持しないほうが安全なことも多いです。

同じ意味の情報を複数箇所に持つと、今度は整合性が問題になります。
片方だけ更新されて、もう片方が古いまま、ということが起きやすくなるからです。

3. UIが持つべき状態、持たないほうがよい状態

ここで大事なのは、「UIは何でも持ってよいわけではない」ということです。

UIが持ってよい状態

UIが持ってよいのは、その画面を操作・表示するために必要な一時的な情報です。

たとえば、次のようなものです。

  • 現在の入力値
  • フォーカスの位置
  • 選択中のタブ
  • ダイアログの開閉状態
  • 一時的なエラーメッセージ表示フラグ

これらは、UIの近くにあるほうが扱いやすいです。
画面を閉じたり切り替えたりすれば、消えてもよいものも多いでしょう。

UIが持たないほうがよい状態

一方で、UIが持たないほうがよいのは、アプリの本体として意味を持つ情報です。

たとえば、次のようなものです。

  • 業務上の中心データ
  • 保存対象となる正式な設定値
  • 他画面でも利用する共有データ
  • 計算ルールそのもの
  • ビジネスロジックの判定条件

ここをUIに寄せすぎると、EntryListboxStringVar が、ただの表示部品ではなく、データの本体そのものになってしまいます。

そうなると、次のような問題が出ます。

  • この値は Entry.get() で読むのか、状態クラスから読むのか分からない
  • 別画面から同じ情報を扱いたいときに困る
  • テスト時にUI部品が必要になる
  • データ処理の再利用が難しい

つまり、部品がデータの所有者になってしまうのです。

UIは入力や表示の窓口ではありますが、必ずしも本体の持ち主ではありません。
この感覚を持てるかどうかで、あとからの保守性がかなり変わってきます。

4. なんでもUIから読むと何が起きるのか

小さなサンプルでは、よくこんなコードになります。

def save_task():
    title = title_entry.get()
    category = category_combo.get()
    deadline = deadline_entry.get()

    tasks.append({
        "title": title,
        "category": category,
        "deadline": deadline
    })

    task_listbox.insert("end", title)

一見すると、分かりやすいコードです。
実際、簡単なツールならこれでも十分動きます。

ただ、この書き方には問題の芽があります。

まず、保存処理がUI部品に強く依存しています。
title_entrycategory_combo が存在しないと、この関数は動きません。

次に、「入力中の値」と「正式に保存された値」の境界が曖昧です。
関数の中で直接 get() して、そのまま保存しているため、UIの状態とデータの状態がほぼ一体化しています。

さらに、画面が増えたときに困ります。
たとえば、別の画面や別の入力方法から同じ保存処理を呼びたいと思っても、この関数はUI部品前提なので流用しにくいです。

つまり、見た目の簡潔さと引き換えに、構造上の自由度を失っているのです。

5. UIは“入力の入口”、状態は“データの持ち主”

では、どう考えると整理しやすいのでしょうか。

ここでひとつの見方として、
UIは入力や表示のための入口であり、状態はデータの持ち主である
と捉えると分かりやすくなります。

たとえば、状態クラスを次のように持つことができます

class AppState:
    def __init__(self):
        self.tasks = []
        self.current_input = {
            "title": "",
            "category": "",
            "deadline": ""
        }

    def add_task(self):
        task = self.current_input.copy()
        self.tasks.append(task)
        self.current_input = {
            "title": "",
            "category": "",
            "deadline": ""
        }

この例では、

  • tasks がアプリの本体データ
  • current_input が入力中の一時状態

として分けられています。

そしてUI側では、入力値を受け取って状態に渡します。

def on_save():
    state.current_input["title"] = title_entry.get()
    state.current_input["category"] = category_combo.get()
    state.current_input["deadline"] = deadline_entry.get()

    state.add_task()
    refresh_task_list()
    clear_form()

この形にすると、責務が少し整理されます。

  • Entry は入力値を受け取る場所
  • state はデータを保持する場所
  • add_task() は保存のルールを持つ場所
  • refresh_task_list() は表示を更新する場所

もちろん、まだ改善の余地はあります。
ですが、少なくとも「ウィジェットの中身がそのまま正式データになる」状態からは一歩進めています。

ここで大事なのは、UI部品そのものを中心に考えるのではなく、
状態の流れを中心に考えることです。

6. 導出できるものまで全部持たない

状態設計で意外と大事なのが、持ちすぎないことです。

たとえば、Todo一覧があるときに、

  • tasks
  • task_count
  • filtered_tasks
  • visible_count

のように、何でもかんでも状態として持ちたくなることがあります。

しかし、その中には、元のデータから計算できるものもあります。

たとえば件数なら、

len(state.tasks)

で求められます。

フィルタ済み一覧も、条件と元データがあれば導けます。
それなら、必要なときに計算するほうが、整合性の面では安全です。

もちろん、毎回計算すると重い処理もあります。
その場合はキャッシュや更新戦略を考える必要があります。
ただ、小規模な tkinter アプリでは、まずは最小の元データだけを持つ意識のほうが役に立つことが多いです。

状態が増えるとき、問題なのは「量」だけではありません。
意味が重複した状態が増えることが、混乱を生みやすいのです。

7. 状態の境界を引くと何が楽になるのか

ここまで見てきたことを整理すると、状態の境界を引くことには、かなり実務的な意味があります。

まず、修正箇所が予測しやすくなります。
UIの表示を直したいのか、データ構造を直したいのか、保存形式を直したいのかで、触る場所が分かれてくるからです。

次に、機能追加がしやすくなります。
たとえば、今は Entry から入力していても、将来的にCSV読込や別画面編集を追加したくなるかもしれません。
そのとき、データの持ち主がUIではなく状態側にあれば、拡張しやすくなります。

さらに、テストや検証もしやすくなります。
UIを起動しなくても、状態クラスやロジック部分だけで確認できる範囲が増えていきます。

そして何より、コードを読んだときに「これは何のための情報なのか」が分かりやすくなります。

  • これは表示だけのための値
  • これは正式なアプリデータ
  • これは保存される設定
  • これは計算結果

こうした区別がつくだけでも、コードの圧迫感はかなり減ります。

境界を引くことは、見た目をきれいにするための作業ではありません。
変更に耐えられる構造を作るための整理です。

8. まとめ

前回は、状態を“クラス”として持つことで、tkinter アプリを構造化する第一歩について書きました。

そして今回は、その次の段階として、
状態には種類があり、同じ場所に全部を押し込めないほうがよい
という話をしてきました。

ポイントを整理すると、次のようになります。

  • UI状態とアプリケーション状態は同じではない
  • 保存される状態と一時的な状態も分けて考えたほうがよい
  • 計算で導けるものは、むやみに保持しないほうが安全なことがある
  • UIは入力や表示の入口であり、必ずしもデータの持ち主ではない
  • 状態の境界を引くと、変更・拡張・保守がしやすくなる

tkinter は小さく始めやすいぶん、境界が曖昧なまま育ってしまうことがあります。
だからこそ、状態の置き場所を意識することが、あとから効いてきます。

状態クラスを作ること自体がゴールではありません。
本当に大事なのは、どの情報をどこで持つのかを言語化できることなのだと思います。

次回予告

状態の種類と境界が見えてくると、次に気になってくるのは、
「では、その状態はどんな流れで更新されるべきか」
という点です。

tkinter では、ボタンが押される、入力が変わる、選択が変わる、といったイベントが起点になって状態が変化します。
この流れが整理されていないと、どこで何が更新されたのか分かりにくくなり、コードが崩れやすくなります。

次回は、
イベント → 状態更新 → UI反映
という流れをどう整理するか、単方向データフローという考え方をもとに掘り下げてみます。

よかったらシェアしてね!
  • URLをコピーしました!
目次