こんにちは!前の第7章: ファイル形式テンプレート (File Format Templates)では、様々な形式やレイアウトを持つ客先の勤務表ファイルを、まるで「読み方説明書」のように解析する「テンプレート」の仕組みについて学びましたね。テンプレートのおかげで、PDFでもExcelでも、どんなレイアウトでも、プログラムは必要な情報を正確に抜き出すことができるようになりました。
さて、社内勤務表と客先勤務表、両方のファイルを読み込み、第1章: 勤務表比較
(TimesheetComparator)
がそれらを比較しました。その結果、どこが一致していて、どこが一致していないかの詳細な情報が
ComparisonResult という形で得られました。
でも、この ComparisonResult
はプログラムが扱いやすいデータ形式です。私たち人間が直接見て、「なるほど、この日の時間が違うのか」とすぐに理解するのは少し難しいかもしれません。
そこで登場するのが、この最終章で学ぶ結果表示 (ResultViewer) です!
ResultViewer は、勤務表比較
(TimesheetComparator)
が生成した比較結果(一致・不一致の詳細)を、ユーザーである皆さんに分かりやすく表示するための画面部品です。
ちょうど、健康診断を受けた後に、お医者さんが検査結果(数値データ)を整理して、「ここに注意が必要です」と分かりやすく説明してくれる診断レポートのようなものです。ResultViewer
は、プログラムが出した比較結果(診断)を受け取り、問題点(不一致箇所)が一目でわかるように整理して提示します。
主な役割は以下の通りです:
TimesheetComparator が作成した
ComparisonResult
オブジェクト(またはそのリスト)を受け取ります。このオブジェクトには、誰の、いつの期間の比較結果か、そして日々の詳細な比較データが含まれています
(第2章:
勤務表データモデル で定義された ComparisonResult と
ComparisonDataInfo)。これにより、ユーザーは比較結果を直感的に理解し、どこを確認・修正する必要があるかを素早く把握できます。
では、実際に 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 | 午後早退 | | <- 一致
| | | ... | ... | ... | ... | ... | |
| | +------+--------+--------+--------+------------------------------------+ |
| +-----------------------------------------------------------------------------+
(これは画面の構成を簡易的に表現したものです。 *
は背景色が変わることを示します)
この画面には、主に以下の要素があります。
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: 比較結果を整形して画面に表示
MainWindow が TimesheetComparator
に比較を依頼します。TimesheetComparator が比較を行い、結果
(ComparisonResult のリスト) を返します。MainWindow は、受け取った比較結果を
ResultViewer の show_result
メソッドに渡します。ResultViewer
がその結果を解釈し、上で説明したような分かりやすい形式で画面(新しいウィンドウ)に表示します。ユーザーが ResultViewer
に対して直接行う操作は主に、タブを切り替えたり、テーブルをスクロールしたり、ツールチップで詳細を確認したりすることです。
ResultViewer の show_result
メソッドが呼び出されると、内部では以下のようなステップで表示内容を組み立てています。
QDialog)を表示する準備をします。もし前に表示した結果が残っていれば、それをクリアします。ComparisonResult
のリストを一つずつ処理します。リストの各要素は一人の社員の比較結果に対応します。
QWidget)
を作成し、タブウィジェット (QTabWidget)
に追加します。タブの名前には社員名を表示します。QLabel) を配置し、ComparisonResult
から取得したテキストを設定します (_setup_summary_section
が担当)。QTableWidget)
を作成します。列のヘッダー(「日付」「項目」など)や幅を設定します
(_setup_discrepancy_table が担当)。ComparisonResult に含まれる日次データのリスト
(data: List[ComparisonDataInfo]) を一つずつ処理します。
ComparisonDataInfo
から日付、稼働時間の客先値・社内値、コメントなどを取得します。QTableWidgetItem
という部品に変換し、テーブルの適切なセルに配置します
(_update_table が担当)。ComparisonDataInfo の
match フラグが False (不一致)
の場合、社内値のセルの背景色を赤系統の色に変更します。QTableWidgetItem)
には、出退勤時刻や休憩時間の情報も一緒に保存しておきます
(setData(Qt.UserRole, ...)
を使用)。コメントセルにも同様にコメント全文を保存します。_show_details
メソッドが呼ばれるように設定します。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
にあります。主要な部分を簡略化して見ていきましょう。
_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
にしてテーブルにセットしています。特に重要なのは以下の点です。
if not d.match:
で不一致を判定し、背景色を変えています。item.setData(Qt.UserRole, ...)
を使って、表示されている値(稼働時間や短いコメント)だけでなく、ツールチップで表示したい詳細情報(出退勤時刻やコメント全文)を目に見えない形でセルに埋め込んでいます。table.cellClicked.connect(self._show_details)
で、セルがクリックされたら _show_details
メソッドを呼び出すように設定しています。_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) について学びました。
ResultViewer は、勤務表比較
(TimesheetComparator) が生成したプログラム向けの比較結果データ
(ComparisonResult) を受け取ります。QDialog,
QTabWidget, QTableWidget,
QTableWidgetItem, QToolTip など)
を組み合わせて画面を構築し、データを表示しています。これで、timesheet_compare
プロジェクトの主要な部品とその連携について、一通りの解説が終わりました。ファイルを選択し、様々な形式のファイルを読み込み、それらを比較し、最後に結果を分かりやすく表示する、という一連の流れを理解いただけたでしょうか。
このチュートリアルを通じて、timesheet_compare
がどのように動作しているのか、その基本的な仕組みを掴む一助となれば幸いです。
| (この章でチュートリアルは終了です) |
Generated by AI Codebase Knowledge Builder