コンテンツにスキップ

サンプル集

PubSubTkを使った現実的なGUIアプリケーション例を紹介します。


1. Todoアプリ(状態管理・リスト操作・画面遷移)

状態定義・ビジネスロジック・UIコンポーネントの3層分離を活用した典型的な例です。

from pydantic import BaseModel
from typing import List
from pubsubtk import TkApplication, ContainerComponentTk, ProcessorBase, AutoNamedTopic
from enum import auto

class TodoItem(BaseModel):
    id: int
    text: str
    done: bool = False

class AppState(BaseModel):
    todos: List[TodoItem] = []
    next_id: int = 1

class Events(AutoNamedTopic):
    ADD = auto()
    TOGGLE = auto()
    DELETE = auto()

class MainProcessor(ProcessorBase[AppState]):
    def setup_subscriptions(self):
        self.subscribe(Events.ADD, self.add)
        self.subscribe(Events.TOGGLE, self.toggle)
        self.subscribe(Events.DELETE, self.delete)
    def add(self, text: str):
        state = self.store.get_current_state()
        todo = TodoItem(id=state.next_id, text=text)
        self.pub_add_to_list(self.store.state.todos, todo)
        self.pub_update_state(self.store.state.next_id, state.next_id + 1)
    def toggle(self, todo_id: int):
        state = self.store.get_current_state()
        new_todos = [
            TodoItem(**dict(t), done=not t.done) if t.id == todo_id else t
            for t in state.todos
        ]
        self.pub_update_state(self.store.state.todos, new_todos)
    def delete(self, todo_id: int):
        state = self.store.get_current_state()
        new_todos = [t for t in state.todos if t.id != todo_id]
        self.pub_update_state(self.store.state.todos, new_todos)

class TodoContainer(ContainerComponentTk[AppState]):
    def setup_ui(self):
        import tkinter as tk
        self.entry = tk.Entry(self)
        self.entry.pack(fill=tk.X, padx=5, pady=5)
        self.entry.bind("<Return>", lambda e: self.on_add())
        tk.Button(self, text="追加", command=self.on_add).pack()
        self.list_frame = tk.Frame(self)
        self.list_frame.pack(fill=tk.BOTH, expand=True)
        self.todo_widgets = {}
    def setup_subscriptions(self):
        self.sub_for_refresh(self.store.state.todos, self.refresh_list)
    def refresh_from_state(self):
        self.refresh_list()
    def refresh_list(self):
        for w in self.todo_widgets.values():
            w.destroy()
        self.todo_widgets.clear()
        state = self.store.get_current_state()
        for todo in state.todos:
            self.add_todo_widget(todo)
    def add_todo_widget(self, todo):
        import tkinter as tk
        f = tk.Frame(self.list_frame)
        f.pack(fill=tk.X, pady=2)
        var = tk.BooleanVar(value=todo.done)
        chk = tk.Checkbutton(f, variable=var, command=lambda: self.on_toggle(todo.id))
        chk.pack(side=tk.LEFT)
        lbl = tk.Label(f, text=todo.text)
        lbl.pack(side=tk.LEFT, padx=5)
        btn = tk.Button(f, text="削除", command=lambda: self.on_delete(todo.id))
        btn.pack(side=tk.RIGHT)
        self.todo_widgets[todo.id] = f
    def on_add(self):
        text = self.entry.get().strip()
        if text:
            self.publish(Events.ADD, text=text)
            self.entry.delete(0, 'end')
    def on_toggle(self, todo_id: int):
        self.publish(Events.TOGGLE, todo_id=todo_id)
    def on_delete(self, todo_id: int):
        self.publish(Events.DELETE, todo_id=todo_id)

if __name__ == "__main__":
    app = TkApplication(AppState, title="Todo App")
    app.pub_register_processor(MainProcessor)
    app.switch_container(TodoContainer)
    app.run()

2. データビューア(CSV/JSON表示)

CSVやJSONを読み込んでテーブル表示・編集するアプリ例(詳細はCookbookやGitHub参照)。


3. PubSubDefaultTopicBaseの全メソッドデモ

PubSubDefaultTopicBaseの全メソッドを使ったコンパクトなデモアプリケーション 各種画面遷移・状態変更・サブウィンドウ・Processor動的登録など、フレームワークの使い方を総合的に確認したい方はこちらのデモコードも参照してください。


4. Storybookを使ったコンポーネントカタログ

UIコンポーネントライブラリをStorybookで開発・確認する完全な例です。

# components/buttons.py
from pubsubtk import PresentationalComponentTk
from pubsubtk.storybook import story
import tkinter as tk

class PrimaryButton(PresentationalComponentTk):
    def setup_ui(self):
        self.button = tk.Button(
            self,
            bg="#007bff",
            fg="white",
            font=("Arial", 10, "bold"),
            relief=tk.FLAT,
            padx=20,
            pady=10
        )
        self.button.pack()

    def set_text(self, text: str):
        self.button.config(text=text)

    def set_command(self, command):
        self.button.config(command=command)

@story("Buttons.Primary")
def primary_button_story(ctx):
    text = ctx.knob("text", str, "Primary Button")
    enabled = ctx.knob("enabled", bool, True)

    btn = PrimaryButton(ctx.parent)
    btn.set_text(text.value)
    btn.button.config(state="normal" if enabled.value else "disabled")

    # Knob変更時の更新
    text.add_change_callback(lambda v: btn.set_text(v))
    enabled.add_change_callback(
        lambda v: btn.button.config(state="normal" if v else "disabled")
    )

    btn.pack(padx=20, pady=20)
    return btn

# components/forms.py
from tkinter import ttk

class FormField(PresentationalComponentTk):
    def setup_ui(self):
        self.label = ttk.Label(self)
        self.label.pack(anchor="w")

        self.entry = ttk.Entry(self, width=40)
        self.entry.pack(fill="x", pady=(5, 0))

        self.error_label = ttk.Label(self, foreground="red", font=("Arial", 8))
        self.error_label.pack(anchor="w")

    def set_label(self, text: str):
        self.label.config(text=text)

    def set_error(self, error: str):
        self.error_label.config(text=error)

    def get_value(self):
        return self.entry.get()

@story("Forms.TextField")
def text_field_story(ctx):
    label = ctx.knob("label", str, "Email Address")
    placeholder = ctx.knob("placeholder", str, "user@example.com")
    required = ctx.knob("required", bool, True)
    error = ctx.knob("error", str, "", desc="Error message to display")

    field = FormField(ctx.parent)
    field.set_label(label.value + (" *" if required.value else ""))
    field.entry.insert(0, placeholder.value)
    field.set_error(error.value)

    # 動的更新
    label.add_change_callback(
        lambda v: field.set_label(v + (" *" if required.value else ""))
    )
    error.add_change_callback(lambda v: field.set_error(v))

    field.pack(padx=20, pady=20, fill="x")
    return field

# run_storybook.py
from pubsubtk.storybook import StorybookApplication
from pubsubtk.storybook.core.auto_discover import discover_stories

if __name__ == "__main__":
    # components/以下の全ストーリーを自動検出
    discover_stories("components")

    app = StorybookApplication(
        theme="arc",
        title="My Component Library",
        geometry="1400x900"
    )
    app.run()

5. 複合的なStorybookサンプル(状態付きコンポーネント)

# components/counter_widget.py
from pubsubtk import ContainerComponentTk
from pubsubtk.storybook import story, StoryContext
from pydantic import BaseModel
import tkinter as tk

class CounterState(BaseModel):
    count: int = 0

class CounterWidget(ContainerComponentTk[CounterState]):
    def setup_ui(self):
        self.label = tk.Label(self, font=("Arial", 24))
        self.label.pack(pady=10)

        button_frame = tk.Frame(self)
        button_frame.pack()

        tk.Button(button_frame, text="-", command=self.decrement).pack(side="left", padx=5)
        tk.Button(button_frame, text="+", command=self.increment).pack(side="left", padx=5)
        tk.Button(button_frame, text="Reset", command=self.reset).pack(side="left", padx=5)

    def setup_subscriptions(self):
        self.sub_for_refresh(self.store.state.count, self.update_display)

    def refresh_from_state(self):
        self.update_display()

    def update_display(self):
        count = self.store.get_current_state().count
        self.label.config(text=str(count))

    def increment(self):
        state = self.store.get_current_state()
        self.pub_update_state(self.store.state.count, state.count + 1)

    def decrement(self):
        state = self.store.get_current_state()
        self.pub_update_state(self.store.state.count, state.count - 1)

    def reset(self):
        self.pub_update_state(self.store.state.count, 0)

@story("Widgets.Counter")
def counter_story(ctx: StoryContext):
    # Knobでカスタマイズ
    initial_value = ctx.knob("initialValue", int, 0, range_=(-100, 100))
    step = ctx.knob("step", int, 1, range_=(1, 10))

    # ローカルストアを作成
    from pubsubtk import get_store
    store = get_store(CounterState)

    # 初期値を設定
    store.update_state("count", initial_value.value)

    # ウィジェットを作成
    counter = CounterWidget(ctx.parent, store=store)

    # stepに応じてincrement/decrementを調整
    original_increment = counter.increment
    original_decrement = counter.decrement

    def custom_increment():
        state = store.get_current_state()
        store.update_state("count", state.count + step.value)

    def custom_decrement():
        state = store.get_current_state()
        store.update_state("count", state.count - step.value)

    counter.increment = custom_increment
    counter.decrement = custom_decrement

    counter.pack(padx=40, pady=40)
    return counter

6. 他にも…

  • 設定ダイアログ付きツール
  • マルチ画面ウィザード
  • ダッシュボード系アプリ

詳細や小技は レシピ集 もご参照ください。