08_結果表示__resultviewer__

Chapter 8: 結果表示 (ResultViewer)

こんにちは!前の第7章: ファイル形式テンプレート (File Format Templates)では、様々な形式やレイアウトを持つ客先の勤務表ファイルを、まるで「読み方説明書」のように解析する「テンプレート」の仕組みについて学びましたね。テンプレートのおかげで、PDFでもExcelでも、どんなレイアウトでも、プログラムは必要な情報を正確に抜き出すことができるようになりました。

さて、社内勤務表と客先勤務表、両方のファイルを読み込み、第1章: 勤務表比較 (TimesheetComparator) がそれらを比較しました。その結果、どこが一致していて、どこが一致していないかの詳細な情報が ComparisonResult という形で得られました。

でも、この ComparisonResult はプログラムが扱いやすいデータ形式です。私たち人間が直接見て、「なるほど、この日の時間が違うのか」とすぐに理解するのは少し難しいかもしれません。

そこで登場するのが、この最終章で学ぶ結果表示 (ResultViewer) です!

結果表示 (ResultViewer) とは? - 比較結果を見やすく示す「診断レポート」

ResultViewer は、勤務表比較 (TimesheetComparator) が生成した比較結果(一致・不一致の詳細)を、ユーザーである皆さんに分かりやすく表示するための画面部品です。

ちょうど、健康診断を受けた後に、お医者さんが検査結果(数値データ)を整理して、「ここに注意が必要です」と分かりやすく説明してくれる診断レポートのようなものです。ResultViewer は、プログラムが出した比較結果(診断)を受け取り、問題点(不一致箇所)が一目でわかるように整理して提示します。

主な役割は以下の通りです:

  1. 比較結果データを受け取る: TimesheetComparator が作成した ComparisonResult オブジェクト(またはそのリスト)を受け取ります。このオブジェクトには、誰の、いつの期間の比較結果か、そして日々の詳細な比較データが含まれています (第2章: 勤務表データモデル で定義された ComparisonResultComparisonDataInfo)。
  2. 情報を整理して表示する: 受け取ったデータを基に、専用のウィンドウ(ダイアログ)を開き、以下の情報を表示します。
  3. 不一致箇所を強調する: テーブルの中で、値が一致しなかった項目(特に社内側の値)の背景色を変えるなどして、問題点を視覚的に分かりやすくします。
  4. 追加情報を提供する: テーブルのセル(マス)にマウスカーソルを合わせたりクリックしたりすると、より詳細な情報(例えば、稼働時間のセルなら、その日の出勤時刻・退勤時刻・休憩時間)をツールチップ(小さな吹き出し)で表示します。

これにより、ユーザーは比較結果を直感的に理解し、どこを確認・修正する必要があるかを素早く把握できます。

ResultViewer の見た目と主な機能

では、実際に ResultViewer が表示する画面はどのようなものでしょうか?(実際の見た目はバージョンによって多少異なる場合があります)

+-----------------------------------------------------------------------------+
| 比較結果 (ResultViewer ウィンドウ)                                         |
+-----------------------------------------------------------------------------+
| [社員Aさんのタブ] [社員Bさんのタブ] ...                                    |
+-----------------------------------------------------------------------------+
| | [比較概要]                                                              | |
| |   対象期間: 2024年7月1日~2024年7月31日                                    | |
| |   対象日数: 20日                                                          | |
| |   一致日数: 18日                                                          | |
| +-------------------------------------------------------------------------+ |
| | [不一致詳細テーブル]                                                      | |
| | +------+--------+--------+--------+------------------------------------+ |
| | | 日付   | 項目   | 客先   | 社内   | コメント                           | |
| | +======+========+========+========+====================================+ |
| | |2024-07-05|稼働時間| 08:00  | 07:45* |                                    | | <- 不一致箇所は色が変わる
| | |2024-07-10|稼働時間| 07:30  | 07:30  | 午後早退                           | | <- 一致
| | | ...  | ...    | ...    | ...    | ...                                | |
| | +------+--------+--------+--------+------------------------------------+ |
| +-----------------------------------------------------------------------------+

(これは画面の構成を簡易的に表現したものです。 * は背景色が変わることを示します)

この画面には、主に以下の要素があります。

  1. タブ: 比較対象の社員が複数いる場合、社員ごとにタブが作成され、切り替えて結果を見ることができます。
  2. 比較概要エリア: 選択されているタブの社員について、対象期間、比較した日数、その中で一致した日数が表示されます。これにより、全体的な一致度を把握できます。
  3. 不一致詳細テーブル:

ResultViewer はいつ、どう使われる?

ResultViewer は、ユーザーがメインウィンドウ (MainWindow) で「実行(比較)」ボタンを押した後、比較処理が完了したタイミングで自動的に呼び出され、画面に表示されます。

処理の流れを思い出してみましょう。

sequenceDiagram
    participant User as ユーザー
    participant MW as メインウィンドウ (MainWindow)
    participant TC as 勤務表比較 (TimesheetComparator)
    participant RV as 結果表示 (ResultViewer)

    User->>MW: 「実行(比較)」ボタンをクリック
    MW->>TC: compare(...) を呼び出し、比較を依頼
    Note right of TC: 比較処理中...
    TC-->>MW: 比較結果 (ComparisonResultのリスト) を返す
    MW->>RV: show_result(比較結果リスト) を呼び出し、表示を依頼
    RV-->>User: 比較結果を整形して画面に表示
  1. ユーザーが比較を指示します。
  2. MainWindowTimesheetComparator に比較を依頼します。
  3. TimesheetComparator が比較を行い、結果 (ComparisonResult のリスト) を返します。
  4. MainWindow は、受け取った比較結果を ResultViewershow_result メソッドに渡します。
  5. ResultViewer がその結果を解釈し、上で説明したような分かりやすい形式で画面(新しいウィンドウ)に表示します。

ユーザーが ResultViewer に対して直接行う操作は主に、タブを切り替えたり、テーブルをスクロールしたり、ツールチップで詳細を確認したりすることです。

内部の仕組み:どうやって表示しているの?

ResultViewershow_result メソッドが呼び出されると、内部では以下のようなステップで表示内容を組み立てています。

  1. ウィンドウ準備: まず、結果表示用のウィンドウ(QDialog)を表示する準備をします。もし前に表示した結果が残っていれば、それをクリアします。
  2. タブ作成ループ: 受け取った ComparisonResult のリストを一つずつ処理します。リストの各要素は一人の社員の比較結果に対応します。
  3. 概要表示: 各タブ内に、概要情報(期間、総日数、一致日数)を表示するためのラベル (QLabel) を配置し、ComparisonResult から取得したテキストを設定します (_setup_summary_section が担当)。
  4. テーブル作成: 各タブ内に、詳細比較結果を表示するためのテーブル (QTableWidget) を作成します。列のヘッダー(「日付」「項目」など)や幅を設定します (_setup_discrepancy_table が担当)。
  5. テーブル内容作成ループ: 各社員の ComparisonResult に含まれる日次データのリスト (data: List[ComparisonDataInfo]) を一つずつ処理します。
  6. ツールチップ連携: テーブルのセルがクリックされたときに _show_details メソッドが呼ばれるように設定します。
  7. ウィンドウ表示: 全てのタブとテーブルの準備ができたら、結果表示ウィンドウを画面に表示 (self.show()) します。

シーケンス図で流れを確認しましょう。

sequenceDiagram
    participant MW as メインウィンドウ (MainWindow)
    participant RV as 結果表示 (ResultViewer)
    participant CR as 比較結果 (ComparisonResult)
    participant UI as 画面 (UI)

    MW->>RV: show_result(比較結果リスト) を呼び出す
    RV->>RV: 既存タブをクリア
    loop 各比較結果 (社員ごと result)
        RV->>UI: 新しいタブを作成 (社員名)
        RV->>CR: resultから概要情報 (期間, 日数) を取得
        RV->>UI: 概要情報を表示
        RV->>UI: 詳細テーブルを作成
        loop 各日次比較データ (d in result.data)
            RV->>CR: d から日次データを取得 (日付, 稼働時間, コメント等)
            RV->>UI: テーブルに行を追加・表示 (QTableWidgetItem)
            alt d.match が False (不一致)
                RV->>UI: 社内値セルの背景色を変更
            end
            RV->>UI: 稼働時間セルに詳細(出退勤/休憩)を埋め込み (setData)
            RV->>UI: コメントセルに全文を埋め込み (setData)
        end
    end
    RV->>UI: 結果ウィンドウを表示 (show)

コードの中身を見てみよう

ResultViewer の実装は src/ui/result_viewer.py にあります。主要な部分を簡略化して見ていきましょう。

UI要素のセットアップ (_setup_... メソッド群)

ウィンドウが表示される際に、タブや概要欄、テーブルの骨組みを作ります。

# src/ui/result_viewer.py より (簡略化)
from PySide6.QtWidgets import QDialog, QTabWidget, QGroupBox, QLabel, QTableWidget, QHeaderView
from PySide6.QtCore import Qt

class ResultViewer(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("比較結果")
        self._setup_ui() # UIの骨組みを作る

    def _setup_ui(self):
        # 全体のタブ管理部品を作成
        self.tabs = QTabWidget(self)
        self.tabs.setGeometry(10, 10, 770, 530) # 位置とサイズ

    def _setup_summary_section(self, tab: QWidget, target_period, total_days, match_days):
        # 概要表示用のグループとラベルを作成
        groupBox = QGroupBox(tab)
        groupBox.setGeometry(20, 45, 550, 80)
        target_period_label = QLabel(f"対象期間 :  {target_period}", groupBox)
        total_days_label = QLabel(f"対象日数 :  {total_days}日", groupBox)
        matched_days_label = QLabel(f"一致日数 :  {match_days}日", groupBox)
        # ... ラベルの位置調整 ...

    def _setup_discrepancy_table(self, tab: QWidget):
        # 詳細表示用のテーブルを作成
        table = QTableWidget(tab)
        table.setGeometry(40, 110, 690, 350)
        table.setColumnCount(5) # 列数を設定
        table.setHorizontalHeaderLabels([ # 列のタイトルを設定
            "日付", "項目", "客先", "社内", "コメント",
        ])
        # ... 列幅やヘッダーの設定 ...
        table.setEditTriggers(QTableWidget.NoEditTriggers) # 編集不可にする
        return table

これらのメソッドで、画面の見た目の骨組み(タブ、概要欄のラベル、テーブルの列)を準備しています。

結果の表示 (show_result メソッド)

MainWindow から比較結果を受け取り、表示処理を開始するメインのメソッドです。

# src/ui/result_viewer.py より (簡略化)
from PySide6.QtWidgets import QWidget
from typing import List
from ..core.timesheet_comparator import ComparisonResult

class ResultViewer(QDialog):
    # ... (__init__, _setup_ui など) ...

    def show_result(self, results: List[ComparisonResult]):
        """比較結果を表示します。"""
        self.tabs.clear() # 前の結果が残っていればクリア

        # 各社員の結果ごとに処理
        for result in results:
            # 1. 新しいタブを作成
            tab = QWidget()
            self.tabs.addTab(tab, f"{result.employee_name}") # 社員名をタブ名に

            # 2. 概要を表示
            self._setup_summary_section(tab,
                                        result.target_period,
                                        result.total_days,
                                        result.matched_days)
            # 3. 詳細テーブルを作成
            table = self._setup_discrepancy_table(tab)

            # 4. テーブルにデータを設定
            self._update_table(result.data, table)

        # 5. ウィンドウを表示
        self.show()
        # ... (親ウィンドウの有効/無効制御) ...
        self.results = results # 結果を保持 (将来の機能用)

このメソッドが、受け取った結果リスト (results) を元に、社員ごとのタブを作成し、概要表示とテーブル作成のメソッドを呼び出し、最後に _update_table でテーブルの中身を埋めています。

テーブル内容の更新 (_update_table メソッド)

日々の比較データ (ComparisonDataInfo) をテーブルに一行ずつ書き込んでいく処理です。

# src/ui/result_viewer.py より (簡略化)
from PySide6.QtWidgets import QTableWidgetItem
from PySide6.QtGui import QBrush, QColor
from PySide6.QtCore import Qt
from typing import List
from ..core.timesheet_comparator import ComparisonDataInfo

class ResultViewer(QDialog):
    # ... (他のメソッド) ...

    def _update_table(self, data: List[ComparisonDataInfo], table):
        table.setRowCount(len(data)) # 必要な行数を用意

        row = 0
        for d in data: # 日次データ(d)を一つずつ処理
            # --- 各セルのアイテムを作成し、テーブルに設定 ---
            # 日付
            date_item = QTableWidgetItem(d.date.strftime("%Y-%m-%d"))
            date_item.setTextAlignment(Qt.AlignCenter) # 中央揃え
            table.setItem(row, 0, date_item)

            # 項目 (ここでは固定で"稼働時間")
            field_item = QTableWidgetItem("稼働時間")
            field_item.setTextAlignment(Qt.AlignCenter)
            table.setItem(row, 1, field_item)

            # 客先値 (稼働時間)
            client_item = QTableWidgetItem(d.work.client_value)
            client_item.setTextAlignment(Qt.AlignCenter)
            # ★ツールチップ用: 出退勤・休憩時間をUserRoleに保存
            client_item.setData(Qt.UserRole, (d.start.client_value, d.end.client_value, d.breaktime.client_value))
            table.setItem(row, 2, client_item)

            # 社内値 (稼働時間)
            internal_item = QTableWidgetItem(d.work.internal_value)
            internal_item.setTextAlignment(Qt.AlignCenter)
            # ★ツールチップ用: 出退勤・休憩時間をUserRoleに保存
            internal_item.setData(Qt.UserRole, (d.start.internal_value, d.end.internal_value, d.breaktime.internal_value))
            table.setItem(row, 3, internal_item)

            # ★不一致なら色を変える
            if not d.match:
                internal_item.setBackground(QBrush(QColor(255, 75, 75))) # 赤っぽい色

            # コメント
            comment_item = QTableWidgetItem(d.comment)
            comment_item.setTextAlignment(Qt.AlignLeft | Qt.AlignVCenter) # 左揃え
            # ★ツールチップ用: コメント全文をUserRoleに保存
            comment_item.setData(Qt.UserRole, d.comment)
            table.setItem(row, 4, comment_item)

            table.setRowHeight(row, 20) # 行の高さを設定
            row += 1

        # ★テーブルセルクリック時の処理を接続
        table.cellClicked.connect(self._show_details)

ここでは、ComparisonDataInfo から取得した値を QTableWidgetItem にしてテーブルにセットしています。特に重要なのは以下の点です。

詳細情報のツールチップ表示 (_show_details メソッド)

テーブルのセルがクリックされたときに、埋め込まれた詳細情報をツールチップで表示します。

# src/ui/result_viewer.py より (簡略化)
from PySide6.QtWidgets import QToolTip
from PySide6.QtCore import Qt, QPoint

class ResultViewer(QDialog):
    # ... (他のメソッド) ...

    def _show_details(self, row, col):
        # 現在表示中のタブからテーブルを取得
        table = self.tabs.currentWidget().findChild(QTableWidget)
        item = table.item(row, col) # クリックされたセルを取得

        if item and (col == 2 or col == 3): # 客先 or 社内 の稼働時間セルなら
            # ★ UserRole から詳細情報を取り出す
            start_time, end_time, break_time = item.data(Qt.UserRole)
            message = f"出勤: {start_time}\n退勤: {end_time}\n休憩: {break_time}"
            # ツールチップを表示する位置を計算
            cell_pos = table.visualItemRect(item).topRight() # セルの右上
            global_pos = table.viewport().mapToGlobal(cell_pos) # 画面全体の座標に
            # ツールチップを表示
            QToolTip.showText(global_pos, message, table)

        elif item and col == 4: # コメントセルなら
            # ★ UserRole からコメント全文を取り出す
            message = item.data(Qt.UserRole)
            # ... 位置を計算してツールチップを表示 ...
            QToolTip.showText(..., message, table)

セルがクリックされるとこのメソッドが呼ばれ、クリックされたセル (item) から item.data(Qt.UserRole) を使って埋め込まれていた詳細情報を取り出し、QToolTip.showText でマウスカーソルの近くに吹き出し表示します。これで、テーブルには主要な情報だけを表示しつつ、必要に応じて詳細を確認できる、という便利な機能を実現しています。

まとめ

この最終章では、勤務表の比較結果をユーザーに分かりやすく提示する「診断レポート」役の結果表示 (ResultViewer) について学びました。

これで、timesheet_compare プロジェクトの主要な部品とその連携について、一通りの解説が終わりました。ファイルを選択し、様々な形式のファイルを読み込み、それらを比較し、最後に結果を分かりやすく表示する、という一連の流れを理解いただけたでしょうか。

このチュートリアルを通じて、timesheet_compare がどのように動作しているのか、その基本的な仕組みを掴む一助となれば幸いです。

(この章でチュートリアルは終了です)

Generated by AI Codebase Knowledge Builder