04_ファイル選択__fileselector__

Chapter 4: ファイル選択 (FileSelector)

こんにちは!前の第3章: メインウィンドウ (MainWindow)では、アプリケーション全体の「顔」であり、ユーザーが操作を行う「司令塔」であるメインウィンドウについて学びましたね。メインウィンドウは、比較を実行するための指示を出す場所でしたが、そもそも比較するには、どのファイルを比較するのかをアプリケーションに伝える必要があります。

そこで登場するのが、今回学ぶファイル選択 (FileSelector) です!

ファイル選択 (FileSelector) とは? - 必要なファイルを受け取る「受付係」

FileSelector は、ユーザーが比較したい「客先勤務表」「社内勤務表」の2つのファイルをアプリケーションに指定するための部品(コンポーネント)です。

ちょうどホテルのフロントにいる受付係が、チェックインに必要な書類(予約確認書など)を宿泊客から受け取るように、FileSelector は比較処理に必要な「書類」、つまり勤務表ファイルを受け取る役割を担います。

このツールを使う上で、まず最初に行う操作の一つが、この FileSelector を使ったファイル選びなのです。

FileSelector の主な機能

FileSelector は、メインウィンドウの中に組み込まれていて、ユーザーが直感的にファイルを選べるように、いくつかの方法を提供しています。

  1. ファイル選択ダイアログ:
  2. ドラッグ&ドロップ:

どちらの方法でも、「客先勤務表」と「社内勤務表」のファイルをそれぞれ指定します。ファイルが正しく選択されると、そのファイル名が画面に表示され、どのファイルが選ばれているか一目でわかるようになっています。

メインウィンドウでの見た目は、第3章で見たように、以下のようになっています。

+--------------------------------------------------------------+
| ... (ウィンドウ上部) ...                                     |
+--------------------------------------------------------------+
| [社内勤務表]                                                 |
| +----------------------------------------------------------+ |
| | [📂] [ ここにファイルをドロップまたはファイル選択 ]        | |  <- FileSelector (社内用)
| +----------------------------------------------------------+ |
|                                                              |
| [客先勤務表]                                                 |
| +----------------------------------------------------------+ |
| | [📂] [ ここにファイルをドロップまたはファイル選択 ]        | |  <- FileSelector (客先用)
| +----------------------------------------------------------+ |
| ... (ウィンドウ下部) ...                                     |
+--------------------------------------------------------------+

(これは画面の構成を簡易的に表現したものです。[📂] はファイル選択ボタンを表します)

FileSelector の使い方 (ユーザーの視点)

実際に FileSelector を使ってファイルを指定する手順はとても簡単です。

  1. 社内勤務表を選ぶ:
  2. 客先勤務表を選ぶ:
  3. 選択をやり直す (キャンセル):

これで、比較したい2つのファイルがアプリケーションに伝えられました!

内部の仕組み (少し詳しく)

では、ボタンをクリックしたり、ファイルをドロップしたりしたとき、FileSelector の裏側では何が起きているのでしょうか?

FileSelector は、ユーザーが選んだファイルの「パス (Path)」を内部に記憶します。「パス」とは、コンピューター上でのファイルの住所のようなものです(例: C:\Users\あなたの名前\Documents\勤務表\社内勤務表_202407.csv)。

そして、ファイル名を表示するために、保存したパスからファイル名部分だけを取り出して、画面上のラベル(「ここにファイルをドロップ…」と書かれていた場所)の表示を更新します。

この「記憶されたパス」が、後で実際にファイルを読み込む際に非常に重要になります。

ユーザーがファイルを選択する流れを簡単な図で見てみましょう。

sequenceDiagram
    participant User as ユーザー
    participant FS as ファイル選択 (FileSelector)
    participant OS as OS (ファイルシステム)

    User->>FS: フォルダ[📂]ボタンをクリック (例: 社内用)
    FS->>OS: ファイル選択ダイアログを開くよう要求
    OS-->>User: ファイル選択ダイアログを表示
    User->>OS: ファイルを選択し「開く」
    OS-->>FS: 選択されたファイルのパスを通知 (例: path/to/internal.csv)
    FS->>FS: 内部変数 (_internal_file_path) にパスを記憶
    FS->>FS: ラベルの表示をファイル名に更新

    User->>FS: ファイルをドラッグ&ドロップ (例: 客先用)
    FS->>OS: ドロップされたファイル情報を取得
    OS-->>FS: ファイルのパスを通知 (例: path/to/client.pdf)
    FS->>FS: 内部変数 (_client_file_path) にパスを記憶
    FS->>FS: ラベルの表示をファイル名に更新

この図のように、FileSelector はユーザーの操作に応じてOS(オペレーティングシステム)と連携し、最終的にファイルの「住所」であるパスを記憶する役割を果たしています。

コードの中身 (さらに少しだけ)

FileSelector の実際のコードは src/ui/file_selector.py に書かれています。いくつか重要な部分を覗いてみましょう。

ファイルパスの記憶

FileSelector クラスの中には、選択されたファイルのパスを覚えておくための変数が用意されています。

# src/ui/file_selector.py より (簡略化)
from pathlib import Path
from typing import Optional

class FileSelector:
    def __init__(self):
        # 選択されたファイルパスを保持する変数
        self._client_file_path: Optional[Path] = None  # 客先ファイルのパス (最初は空)
        self._internal_file_path: Optional[Path] = None # 社内ファイルのパス (最初は空)
        # ... 他の初期化処理 ...

_client_file_path_internal_file_path という変数に、それぞれ選択されたファイルのパスが Path オブジェクト(ファイルの場所を扱うための便利な型)として保存されます。Optional[Path] = None は、最初は何も選択されていない状態(None)であることを示しています。

ファイル選択ダイアログを開く処理

フォルダアイコンのボタンがクリックされたときに呼び出される処理の一部です。

# src/ui/file_selector.py より (簡略化)
from PySide6.QtWidgets import QFileDialog

class FileSelector:
    # ... ( __init__ などは省略 ) ...

    def _select_internal_file(self):
        """社内勤務表ファイルを選択します。"""
        # ファイル選択ダイアログを表示
        file_path_str, _ = QFileDialog.getOpenFileName(
            self, # 親ウィンドウ
            "社内勤務表を選択", # ダイアログのタイトル
            self._last_directory, # 前回開いた場所から開始
            "CSVファイル (*.csv);;すべてのファイル (*.*)" # 選択できるファイルの種類
        )

        if file_path_str: # ファイルが選択された場合
            self._internal_file_path = Path(file_path_str) # パスを記憶
            self._last_directory = str(self._internal_file_path.parent) # 次回のために場所を覚えておく
            self.update_label_text(self.internal_file_label, file_path_str) # ラベル表示を更新

    def _select_client_file(self):
        """客先勤務表ファイルを選択します。(社内用とほぼ同様)"""
        # ... (QFileDialog.getOpenFileName を使い、客先用ファイルを選択) ...
        # ... (パスを _client_file_path に記憶し、ラベルを更新) ...

QFileDialog.getOpenFileName という命令を使って、おなじみのファイル選択ダイアログを開いています。ユーザーがファイルを選ぶと、そのファイルのパスが file_path_str に文字列として返ってくるので、それを Path オブジェクトに変換して _internal_file_path (または _client_file_path) に保存し、update_label_text で画面表示を更新しています。

ドラッグ&ドロップの処理

ファイルをドラッグしてエリアに持ってきたとき (dragEnterEvent) と、ドロップしたとき (dropEvent) に対応する処理も定義されています。

# src/ui/file_selector.py より (簡略化)
from PySide6.QtGui import QDragEnterEvent, QDropEvent

class DraggableLabel(QLabel): # FileSelector内で使う特別なラベル
    # ... ( __init__ など ) ...

    def dragEnterEvent(self, event: QDragEnterEvent):
        """ファイルがエリア内にドラッグされてきたときの処理"""
        if event.mimeData().hasUrls(): # ファイルのURLが含まれているか?
            event.acceptProposedAction() # ドロップを受け入れる準備OKの合図

    def dropEvent(self, event: QDropEvent):
        """ファイルがエリア内でドロップされたときの処理"""
        # ドロップされたファイルのURLリストを取得し、最初のファイルのパスを取得
        files = [url.toLocalFile() for url in event.mimeData().urls()]
        if not files:
            return # ファイルがなければ何もしない
        file_path = Path(files[0]) # 最初のファイルのパスを取得

        # どのラベルにドロップされたかに応じて、対応するパスを更新
        if self.label_type == 'client': # 客先用ラベルなら
            self.file_selector._client_file_path = file_path # 客先パスを記憶
            self.file_selector.update_label_text(...) # ラベル更新
        else: # 社内用ラベルなら
            self.file_selector._internal_file_path = file_path # 社内パスを記憶
            self.file_selector.update_label_text(...) # ラベル更新

dragEnterEvent では、ドラッグされてきたものがファイルであることを確認して、ドロップを受け入れる姿勢を示します。dropEvent では、実際にドロップされたファイルの情報からパスを取得し、_client_file_path_internal_file_path に保存して、ラベル表示を更新します。

ラベル表示の更新

ファイルが選択された後、ファイル名を表示するための処理です。

# src/ui/file_selector.py より (簡略化)
import os
from PySide6.QtCore import Qt

class FileSelector:
    # ... ( 他のメソッドは省略 ) ...

    def update_label_text(self, label, file_path_str):
        """ラベルのテキストを選択されたファイル名に更新します。"""
        file_name = os.path.basename(file_path_str) # パスからファイル名だけを取得
        label.setText(f"{file_name}") # ラベルの表示をファイル名に設定
        label.setAlignment(Qt.AlignLeft) # 左揃えにする (見やすくするため)
        label.setToolTip(file_path_str) # マウスを重ねた時にフルパスを表示

os.path.basename を使って、長いファイルパス(住所)からファイル名(建物名)だけを取り出し、label.setText で画面のラベルに表示しています。

ファイル読み込みへの橋渡し

FileSelector の重要な役割の一つは、記憶したファイルパスを、実際にファイルを読み込む担当者(リーダー)に渡すことです。第3章: メインウィンドウ (MainWindow) が比較を実行しようとするとき、FileSelector に対して「選択されたファイルのデータ(中身)をちょうだい」と依頼します。その依頼に応えるのが get_client_timesheetget_internal_timesheet といったメソッドです。

# src/ui/file_selector.py より (簡略化)
from ..pdf_handler.pdf_reader import PDFReader
from ..excel_handler.excel_reader import ExcelReader
from ..core.internal_timesheet_reader import InternalTimesheetReader

class FileSelector:
    # ... ( __init__ などでリーダーを準備 ) ...
    # self._pdf_reader = PDFReader()
    # self._excel_reader = ExcelReader()
    # self._internal_reader = InternalTimesheetReader()

    def get_client_timesheet(self):
        """選択された客先勤務表を読み込んでデータを返す"""
        if not self._client_file_path:
            raise ValueError("客先勤務表が選択されていません。") # エラー処理

        # ファイルの拡張子を見て、適切なリーダーを選択
        suffix = self._client_file_path.suffix.lower()
        if suffix == ".pdf":
            # PDFリーダーにファイルパスを渡して読み込みを依頼
            return self._pdf_reader.read(self._client_file_path)
        elif suffix in [".xlsx", ".xls", ".xlsm"]:
            # Excelリーダーにファイルパスを渡して読み込みを依頼
            return self._excel_reader.read(self._client_file_path)
        else:
            raise ValueError(f"未対応のファイル形式です: {suffix}") # エラー処理

    def get_internal_timesheet(self):
        """選択された社内勤務表を読み込んでデータを返す"""
        if not self._internal_file_path:
            raise ValueError("社内勤務表が選択されていません。") # エラー処理

        # 社内リーダーにファイルパスを渡して読み込みを依頼
        return self._internal_reader.read(self._internal_file_path)

これらのメソッドは、まずファイルが選択されているかを確認します。そして、記憶しているファイルパス (_client_file_path_internal_file_path) を使って、適切な「ファイル読み込み担当」(第5章: 社内勤務表リーダー第6章: 客先勤務表リーダー - これらは次の章以降で学びます!) に実際の読み込み処理を依頼 (read メソッドを呼び出す) します。

つまり、FileSelector はファイルを選ぶ「受付係」であると同時に、選ばれたファイルを読み込む専門家への「橋渡し役」もしているのです。

まとめ

この章では、勤務表比較ツールで比較対象のファイルを指定するための部品、ファイル選択 (FileSelector) について学びました。

これで、アプリケーションがどのようにしてユーザーから比較したいファイルの情報を受け取っているのかが分かりましたね。ファイルを選ぶという、ツールを使う上での最初のステップを担う重要な部品でした。

さて、ファイルが選ばれたら、次はそのファイルの中身をコンピューターが理解できる形に読み込む必要があります。次の章では、まず「社内勤務表」のファイル(CSV形式)を読み込むための専門家について見ていきましょう。

次の章: 第5章: 社内勤務表リーダー (InternalTimesheetReader)


Generated by AI Codebase Knowledge Builder