macOSチャット送信ツール開発記録 – UI改良と機能拡張で作業効率を大幅向上(ChatSpooler開発記)

開発のきっかけと2列レイアウトへの挑戦

前回の記事でmacOS向けのAIチャット自動送信ツール「ChatSpooler」の基本機能を完成させました。しかし、実際に使ってみると改善点が見えてきました。

特にUIの配置が作業効率を妨げていて、従来のレイアウトでは送信キューや送信履歴の高さが狭くて一覧できません。

そこで、画面上部を左右2列に分割する新しいレイアウトを検討しました。左列に送信先設定、右列に文章入力を配置することで、両方の操作を同時に見ながら行えるようになります。

レイアウト変更の設計と実装

新しいレイアウトの設計では、効率的な画面使用を最優先に考えました。上部を2列に分割し、送信キューと送信履歴を左右に並列配置することで、より多くの情報を同時に表示できます。

tkinter(Pythonの標準GUI ライブラリ)のgridレイアウトマネージャー1を使用して、柔軟な画面配置を実現しました。gridは表のようにウィジェットを配置できる仕組みで、列と行の重み設定により画面サイズの変更に対応できます。

def create_top_area(self, parent):
    """上部2列エリアを作成"""
    top_frame = ttk.Frame(parent)
    top_frame.grid(row=0, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 10))
    
    # 左列:送信先設定
    self.create_left_column(top_frame)
    
    # 右列:文章入力
    self.create_right_column(top_frame)
    
    # 列重み設定(1:1の比率)
    top_frame.columnconfigure(0, weight=1)
    top_frame.columnconfigure(1, weight=1)
    top_frame.rowconfigure(0, weight=1)
Code language: PHP (php)

この設定により、左右の列が常に同じ幅を保ちながら画面サイズに追従します。

送信方法選択UIの改良

従来はラジオボタンを縦に並べて送信方法を選択していましたが、これが縦方向のスペースを圧迫していました。コンボボックス(ドロップダウンリスト)2に変更することで、省スペース化を実現しました。

method_combo = ttk.Combobox(method_frame, textvariable=self.method_var, 
                           values=["applescript", "clipboard", "pyautogui"], 
                           state="readonly", width=15)
Code language: PHP (php)

この変更により、機能性を保ちながら画面の有効活用ができるようになりました。

大規模コードのモジュール分割(ui.py)

開発が進むにつれ、ui.pyファイルが500行を超える大きなファイルになりました。コードが長くなると、機能追加時の影響範囲がわかりにくくなり、デバッグも困難になります。

モジュール分割による保守性向上 分割前:ui.py(500行超) 全機能混在 影響範囲不明 デバッグ困難 分割後:責任別3ファイル ui_main 設定管理 components UI部品 handlers イベント 分割による効果 保守性向上 適切なファイル選択 影響範囲限定 機能追加安全 再利用性 部品として活用 開発効率 大幅向上 複数人開発対応 分割指針:初期から責任分離設計が効率的

そこで、責任別に3つのファイルに分割しました。

分割後のファイル構成

# ui_main.py - メインクラスと設定管理
class AutoChatSenderUI(UIComponents, UIHandlers):
    def __init__(self, root: tk.Tk):
        self.root = root
        self.setup_ui()
        self.setup_callbacks()

# ui_components.py - UI部品の作成
class UIComponents:
    def create_app_selection_area(self, parent):
        """アプリケーション選択エリアを作成"""
        app_frame = ttk.LabelFrame(parent, text="送信先設定")
        # UI要素の配置処理

# ui_handlers.py - イベント処理
class UIHandlers:
    def add_to_queue(self):
        """テキストをキューに追加"""
        text = self.text_area.get("1.0", tk.END).strip()
        if self.core_manager.queue_manager.add_item(text, wait_time):
            self.check_auto_send()

この分割により、新機能を追加する際に適切なファイルを選択でき、コードの保守性が大幅に向上しました。例えば、新しいボタンを追加したい場合はui_components.py、そのボタンの動作を定義したい場合はui_handlers.pyを編集すれば済みます。

送信制御機能の大幅強化(_sending_loop)

実際の使用場面を想定すると、送信タイミングの細かい制御が必要になることがわかりました。特に、送信開始の遅延時間を調整したい場面や、全送信完了後も待機を継続したい場面が想定されます。

送信制御機能の強化 送信遅延機能 0〜60秒設定 送信前の準備時間 完了後自動リセット 送信完了後待機 最後の待機時間 連続追加対応 チェックボックス制御 待機スキップ 即座に次処理 待機中のみ有効 フラグ制御方式 設定永続化 JSON保存 再起動時復元 自動保存 送信制御フロー 送信遅延 カウント 0-60秒 送信実行 テキスト 送信 次回待機 設定時間 スキップ可 完了判定 キュー 確認 最終待機 (オプション) 継続設定時 技術実装ポイント 制御フラグ: skip_waiting_flag | 状態管理: UI連動 | 設定保存: 即座反映

送信遅延機能の実装

AIが処理に時間がかかっているときに、送信開始前に任意の秒数待機できる機能を追加しました。デフォルトは0秒ですが、1秒から60秒まで設定可能です。この機能により、送信先アプリの準備時間を確保できます。

def _sending_loop(self, method: str, target_app: Optional[str]):
    """送信ループ(別スレッドで実行)"""
    try:
        # 送信遅延のカウントダウン
        if self.send_delay > 0:
            for i in range(self.send_delay, 0, -1):
                if not self.is_running or self.skip_waiting_flag:
                    if self.skip_waiting_flag:
                        self.skip_waiting_flag = False
                        self.logger.log("送信遅延をスキップしました")
                    return
                self._update_status(f"送信開始まで {i} 秒...", "warning")
                time.sleep(1)
Code language: PHP (php)

重要な仕様として、送信完了後は自動的にデフォルト値(0秒)にリセットされます。これにより、一時的な調整が次回の送信に影響しません。

送信完了後の待機継続

従来は全アイテムの送信が完了すると即座に待機状態になっていました。しかし、連続してアイテムを追加する場合、間隔を空けたい場面があります。

新機能では、最後のアイテムの待機時間を使って送信完了後も待機を継続できます。この設定はチェックボックスで制御し、設定は永続化されます。

def _handle_final_waiting(self, last_wait_time: int):
    """送信完了後の最終待機処理"""
    from utils import settings_manager
    
    if settings_manager.get_continue_waiting():
        self.is_in_final_waiting = True
        self.logger.log(f"送信完了後の待機を開始します({last_wait_time}秒)")
        
        for i in range(last_wait_time):
            if not self.is_running:
                return
            if self.skip_waiting_flag:
                self.skip_waiting_flag = False
                self.logger.log("最終待機をスキップしました")
                break
            remaining = last_wait_time - i
            self._update_status(f"送信完了後の待機 {remaining} 秒...", "normal")
            time.sleep(1)
Code language: PHP (php)

待機スキップ機能

待機中や遅延中に即座に次の処理に進める「待機のスキップ」ボタンを追加しました。このボタンは待機中のみ有効になり、送信中は無効化されます。

実装では、skip_waiting_flagという制御フラグを使用しています。待機ループ内でこのフラグをチェックし、設定されていれば待機を中断します。

def skip_current_waiting(self):
    """現在の待機をスキップ"""
    self.skip_waiting_flag = True
    self.logger.log("待機スキップが要求されました")

# 待機ループ内での処理
if self.skip_waiting_flag:
    self.skip_waiting_flag = False
    self.logger.log("待機をスキップしました")
    return
Code language: PHP (php)

ファイル一括読み込み機能の実装(load_folder_to_queue)

実用性を高めるため、フォルダ内のテキストファイルを一括でキューに追加する機能を実装しました。この機能には複数の技術的課題がありました。

ファイル一括読み込み機能 フォルダ 選択 .txt検出 直下のみ サブフォルダ除外 自然ソート 1,2,10順 エンコード 自動判定 UTF-8優先 Shift_JIS対応 キュー 追加 技術仕様 自然ソート実装 正規表現で数値分離 例: 1.txt → 2.txt → 10.txt エンコーディング UTF-8 → Shift_JIS → CP932 失敗時は自動スキップ エラー処理 ログ出力 処理継続 使用例 フォルダ内: question1.txt, question10.txt, question2.txt 読み込み順: 1 → 2 → 10

自然ソートの実装

ファイル名の並び順では、「1.txt, 10.txt, 2.txt」という文字列ソートではなく、「1.txt, 2.txt, 10.txt」という自然ソート3が求められます。

正規表現を使用して数値部分を分離し、数値として比較する仕組みを実装しました。

@staticmethod
def natural_sort_key(text: str) -> List:
    """自然ソート用のキーを生成"""
    def tryint(s):
        try:
            return int(s)
        except ValueError:
            return s
    
    return [tryint(c) for c in re.split(r'(\d+)', text)]

# 使用例
txt_files.sort(key=TextFormatter.natural_sort_key)
Code language: PHP (php)

この関数により、ファイル名に含まれる数値が正しい順序で並びます。

エンコーディング対応

日本語環境では複数の文字エンコーディングが混在する可能性があります。UTF-8、Shift_JIS、CP932の順で読み込みを試行し4、最初に成功したエンコーディングを使用します。

def _read_file_with_encoding(self, file_path: str) -> Optional[str]:
    """エンコーディングを試行してファイルを読み込み"""
    encodings = ['utf-8', 'shift_jis', 'cp932']
    
    for encoding in encodings:
        try:
            with open(file_path, 'r', encoding=encoding) as f:
                content = f.read()
                if content.strip():  # 空でない場合のみ返す
                    return content
                else:
                    log_message(f"警告: {os.path.basename(file_path)} は空のファイルです")
                    return None
        except UnicodeDecodeError:
            continue
        except Exception as e:
            log_message(f"ファイル読み込みエラー {os.path.basename(file_path)}: {str(e)}")
            return None
    
    log_message(f"エンコーディングエラー: {os.path.basename(file_path)} を読み込めませんでした")
    return None
Code language: PHP (php)

読み込みに失敗したファイルは自動的にスキップされ、エラーログが出力されます。これにより、一部のファイルに問題があっても処理を継続できます。

フォルダ読み込みの実装

def load_folder_to_queue(self):
    """フォルダからtxtファイルを読み込んでキューに追加"""
    folder_path = filedialog.askdirectory(title="txtファイルが含まれるフォルダを選択してください")
    if not folder_path:
        return
    
    try:
        # フォルダ内の.txtファイルを取得
        txt_files = []
        for filename in os.listdir(folder_path):
            file_path = os.path.join(folder_path, filename)
            # 直下のファイルのみ、かつ.txtファイルのみ
            if os.path.isfile(file_path) and filename.lower().endswith('.txt'):
                txt_files.append(filename)
        
        if not txt_files:
            messagebox.showinfo("情報", "選択されたフォルダにtxtファイルがありませんでした")
            return
        
        # 自然ソートで並び替え
        txt_files.sort(key=TextFormatter.natural_sort_key)
        
        # デフォルト待機時間を取得
        default_wait_time = settings_manager.get_default_wait_time()
        
        added_count = 0
        for filename in txt_files:
            file_path = os.path.join(folder_path, filename)
            content = self._read_file_with_encoding(file_path)
            if content:
                if self.core_manager.queue_manager.add_item(content.strip(), default_wait_time):
                    added_count += 1
        
        log_message(f"フォルダ読み込み完了: {added_count}件追加")
Code language: PHP (php)

設定の永続化とUI状態管理(SettingsManager)

アプリケーションの使いやすさを向上させるため、設定の自動保存機能を強化しました。新しい設定項目(送信完了後の待機継続)も含め、JSONファイルに保存されます。

class SettingsManager:
    def get_continue_waiting(self) -> bool:
        """送信完了後の待機継続を取得"""
        return self.get("continue_waiting", False)
    
    def set_continue_waiting(self, enabled: bool):
        """送信完了後の待機継続を設定"""
        self.set("continue_waiting", enabled)
        self.save_settings()
Code language: CSS (css)

UI状態管理では、送信状況に応じてボタンの有効・無効を自動切り替えします。待機中はスキップボタンが有効、送信中は無効といった制御により、操作ミスを防げます。

def update_status(self, status: str, color_type: str = "normal"):
    """ステータスを更新"""
    self.status_var.set(status)
    
    # ボタン状態の管理
    is_waiting = any(keyword in status for keyword in ["まで", "秒", "カウント"])
    is_sending = "送信中" in status
    
    if status == "待機中":
        self.start_button.config(state=tk.NORMAL)
        self.stop_button.config(state=tk.DISABLED)
        self.skip_button.config(state=tk.DISABLED)
    elif is_waiting:
        self.start_button.config(state=tk.DISABLED)
        self.stop_button.config(state=tk.NORMAL)
        self.skip_button.config(state=tk.NORMAL)
    elif is_sending:
        self.start_button.config(state=tk.DISABLED)
        self.stop_button.config(state=tk.NORMAL)
        self.skip_button.config(state=tk.DISABLED)
Code language: PHP (php)

まとめ

macOSチャット送信ツールの開発を通じて、UI設計、コード構造、機能実装の各面で実践的な経験を積むことができました。2列レイアウトによる作業効率向上、モジュール分割による保守性改善、送信制御機能の強化、ファイル一括処理機能の追加により、実用的なツールに仕上がりました。

今回の機能拡張では、ユーザビリティの重要性を改めて実感しました。日常的な作業効率を改善する小さな改良の方は大事です。コードの分割でも適切な構造に分けると、AIによるコード生成がスムーズになり、管理がしやすくなります。

  1. tkinterのgridは表形式でウィジェットを配置する仕組みで、HTMLのテーブルレイアウトに似ています。packやplaceと比べて柔軟性が高く、現代的なGUIアプリケーション開発で推奨されています。 – Tkinter Grid Geometry Manager – Python Tutorial
  2. コンボボックスは入力フィールドとドロップダウンリストを組み合わせたUIコンポーネントです。ユーザーは予め定義された選択肢から選ぶか、直接入力することができます。tkinterではttk.Comboboxとして提供されています。 – Using the Grid Geometry Manager in Tkinter
  3. 自然ソート(Natural Sort Order)とは、文字列に含まれる数値部分を数値として認識して並び替える手法です。通常の文字列ソートでは「1.txt, 10.txt, 2.txt」となりますが、自然ソートでは「1.txt, 2.txt, 10.txt」と人間にとって自然な順序になります。 – Natural sort order – Wikipedia
  4. 日本語環境では複数の文字エンコーディングが混在することがあります。UTF-8は現在の標準、Shift_JISは古いWindowsシステム、CP932はWindowsのコードページ932(Shift_JISの拡張版)です。エンコーディングの自動判定は完璧ではないため、優先順位を決めて順次試行する方法が実用的です。 – [解決!Python]エンコーディングを指定して、シフトJISなどのファイルを読み書きするには