Excelレポート生成
データを入力するとopenpyxlで書式付きExcelを自動生成。グラフ埋め込みと条件付き書式対応。
1. アプリ概要
データを入力するとopenpyxlで書式付きExcelを自動生成。グラフ埋め込みと条件付き書式対応。
このアプリはfileカテゴリの実践的なPythonアプリです。使用ライブラリは tkinter(標準ライブラリ)・openpyxl、難易度は ★★☆ です。
このアプリは「ファイル操作」カテゴリです。ファイル操作は業務スクリプトの心臓部で、I/O のパターンは他のあらゆる Python プログラムで再利用できます。tkinter(標準ライブラリ)・openpyxl を活かして実装するこの構造は、他のアプリにも応用が効きます。
出力する Excel には、表の下に商品別の合計を並べた棒グラフ(BarChart)と、合計が 100,000 を超えたセルを赤くする条件付き書式(CellIsRule)が入ります。条件付き書式は Excel 側のルールとして保存されるため、出力後に Excel 上で合計セルの値を書き換えると、色も自動で切り替わります。
画面に表示される合計と Excel に書き込まれる合計は、_filled_rows() という 1 つのメソッドから作っています。表で 490,000 と見えた行は、Excel でも 490,000 になります(後述の実測で確認済み)。
コピー&実行で動作確認 → コード解説で構造理解 → カスタマイズで応用、の 3 ステップで進めると、短時間で本質を押さえられます。
アプリを完成させた後は、自分の使い方に合わせて改造するのが学びを定着させる近道です。カスタマイズ章のアイデアを足がかりに、独自機能を一つ追加してみてください。
2. 機能一覧
- 「📂 CSV読込」ボタン(
_load_csv())・「+ 行追加」ボタン(_add_row())・「✕ 行削除」ボタン(_del_row())・「📥 Excel出力」ボタン(_export())・「追加」ボタン(_add_row())で操作 - マウス操作
<Double-1>から_on_double_click()を実行 - キー
<Return>から 処理 を実行 <FocusOut>イベント(ウィンドウ側からの通知)で 処理 を自動実行ttk.Treeviewによる表形式の一覧表示- 合計列(単価×数量)を
_filled_rows()で計算し、画面表示と Excel 出力で同じ値を使う openpyxl.chart.BarChartで商品別の合計を棒グラフにして Excel へ埋め込むopenpyxl.formatting.rule.CellIsRuleで、合計がHIGHLIGHT_TOTAL(既定 100,000)を超えたセルを赤く塗る条件付き書式を設定filedialog.askopenfilename()によるファイル選択ダイアログfiledialog.asksaveasfilename()によるファイル選択ダイアログmessagebox.showerror()・messagebox.showinfo()によるダイアログ通知- ウィンドウはタイトル「Excelレポート生成」・サイズ 900x600 で起動
3. 事前準備・環境
Python 3.10 以上 / Windows 11(実機)で起動・基本操作を確認 / Linux Mint 22.3(仮想マシン)で起動・画面表示を確認(macOS は未検証)
Windows 11(実機)で起動と基本操作を確認しています(全機能の網羅テストではありません)。Linux Mint 22.3(仮想マシン)では起動と画面表示を確認しました。macOS は必要なライブラリ(pip install)を入れれば動作する想定ですが、未検証です。
- Python 3.10 以上
- OS: Windows 11(実機で起動・基本操作を確認)・Linux Mint 22.3(仮想マシンで起動・画面表示を確認)
インストールが必要なライブラリ
pip install openpyxl
4. 完全なソースコード
右上の「コピー」ボタンをクリックするとコードをクリップボードにコピーできます。
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import os
import csv
import threading
from datetime import datetime
try:
import openpyxl
from openpyxl.styles import (Font, PatternFill, Alignment,
Border, Side)
from openpyxl.utils import get_column_letter
from openpyxl.chart import BarChart, Reference
from openpyxl.formatting.rule import CellIsRule
OPENPYXL_AVAILABLE = True
except ImportError:
OPENPYXL_AVAILABLE = False
class App055:
"""Excelレポート生成"""
SAMPLE_DATA = [
["商品名", "カテゴリ", "単価", "数量", "合計"],
["ノートPC", "電子機器", 98000, 5, None],
["マウス", "電子機器", 2500, 20, None],
["キーボード", "電子機器", 5800, 15, None],
["デスク", "家具", 45000, 3, None],
["チェア", "家具", 32000, 8, None],
["モニター", "電子機器", 38000, 10, None],
["USB-Cケーブル", "アクセサリ", 1200, 50, None],
["Webカメラ", "電子機器", 8500, 7, None],
]
# 条件付き書式で強調する合計のしきい値(この値を超えたセルを赤くする)
HIGHLIGHT_TOTAL = 100000
def __init__(self, root):
self.root = root
self.root.title("Excelレポート生成")
self.root.geometry("900x600")
self.root.configure(bg="#1e1e1e")
self._rows = [list(r) for r in self.SAMPLE_DATA]
self._build_ui()
self._refresh_table()
def _build_ui(self):
header = tk.Frame(self.root, bg="#252526", pady=6)
header.pack(fill=tk.X)
tk.Label(header, text="📊 Excelレポート生成",
font=("Noto Sans JP", 12, "bold"),
bg="#252526", fg="#4fc3f7").pack(side=tk.LEFT, padx=12)
if not OPENPYXL_AVAILABLE:
tk.Label(self.root,
text="⚠ openpyxl が未インストールです (pip install openpyxl)",
bg="#fff3cd", fg="#856404", font=("Arial", 9),
anchor="w", padx=8).pack(fill=tk.X)
# ツールバー
tb = tk.Frame(self.root, bg="#2d2d2d", pady=4)
tb.pack(fill=tk.X)
ttk.Button(tb, text="📂 CSV読込", command=self._load_csv).pack(side=tk.LEFT, padx=4)
ttk.Button(tb, text="+ 行追加", command=self._add_row).pack(side=tk.LEFT, padx=2)
ttk.Button(tb, text="✕ 行削除", command=self._del_row).pack(side=tk.LEFT, padx=2)
ttk.Button(tb, text="📥 Excel出力", command=self._export).pack(side=tk.LEFT, padx=8)
tk.Label(tb, text="タイトル:", bg="#2d2d2d", fg="#ccc",
font=("Arial", 9)).pack(side=tk.LEFT, padx=(12, 4))
self.title_var = tk.StringVar(value="売上レポート")
ttk.Entry(tb, textvariable=self.title_var, width=20).pack(side=tk.LEFT)
# スタイル
tk.Label(tb, text="スタイル:", bg="#2d2d2d", fg="#ccc",
font=("Arial", 9)).pack(side=tk.LEFT, padx=(12, 4))
self.style_var = tk.StringVar(value="ブルー")
ttk.Combobox(tb, textvariable=self.style_var,
values=["ブルー", "グリーン", "オレンジ"],
state="readonly", width=10).pack(side=tk.LEFT)
# テーブル(Treeview)
table_f = tk.Frame(self.root, bg="#1e1e1e")
table_f.pack(fill=tk.BOTH, expand=True, padx=8, pady=4)
# 列数に応じて動的にTreeviewを構築
cols = [f"c{i}" for i in range(len(self._rows[0]))]
self.tree = ttk.Treeview(table_f, columns=cols, show="headings",
selectmode="browse")
sb_x = ttk.Scrollbar(table_f, orient=tk.HORIZONTAL,
command=self.tree.xview)
sb_y = ttk.Scrollbar(table_f, command=self.tree.yview)
self.tree.configure(xscrollcommand=sb_x.set, yscrollcommand=sb_y.set)
sb_y.pack(side=tk.RIGHT, fill=tk.Y)
self.tree.pack(fill=tk.BOTH, expand=True)
sb_x.pack(fill=tk.X)
self.tree.bind("<Double-1>", self._on_double_click)
# 行追加フォーム
add_f = tk.Frame(self.root, bg="#252526", pady=4)
add_f.pack(fill=tk.X, padx=8)
tk.Label(add_f, text="新規行:", bg="#252526", fg="#ccc",
font=("Arial", 9)).pack(side=tk.LEFT)
self.row_entries = []
for _ in range(len(self._rows[0])):
e = ttk.Entry(add_f, width=12)
e.pack(side=tk.LEFT, padx=2)
self.row_entries.append(e)
ttk.Button(add_f, text="追加",
command=self._add_row).pack(side=tk.LEFT, padx=4)
self.status_var = tk.StringVar(value="データを編集してExcelを出力できます")
tk.Label(self.root, textvariable=self.status_var,
bg="#252526", fg="#858585", font=("Arial", 9),
anchor="w", padx=8).pack(fill=tk.X, side=tk.BOTTOM)
def _calc_total(self, row):
"""1行分の合計(単価×数量)を計算する。計算できなければ None。"""
try:
return int(row[2]) * int(row[3])
except Exception:
return None
def _filled_rows(self):
"""合計列を計算で埋めた表データを返す。
画面表示(_refresh_table)とExcel出力(_do_export)が
この1つのメソッドを共有するので、両者の数値は必ず一致する。
"""
if not self._rows:
return []
headers = list(self._rows[0])
# 最終列が「合計」のときだけ計算で埋める(任意のCSVを壊さないため)
has_total = "合計" in str(headers[-1]) if headers else False
filled = [headers]
for row in self._rows[1:]:
r = list(row)
if has_total and len(r) == len(headers) and r[-1] in (None, ""):
total = self._calc_total(r)
if total is not None:
r[-1] = total
filled.append(r)
return filled
def _refresh_table(self):
# ヘッダー設定
rows = self._filled_rows()
if not rows:
return
headers = rows[0]
n = len(headers)
cols = [f"c{i}" for i in range(n)]
# Treeviewを再構成
self.tree.configure(columns=cols)
for i, h in enumerate(headers):
self.tree.heading(f"c{i}", text=str(h))
self.tree.column(f"c{i}", width=120, anchor="center")
self.tree.delete(*self.tree.get_children())
for row in rows[1:]:
values = [("" if v is None else v) for v in row]
self.tree.insert("", tk.END, values=values)
self.status_var.set(f"{len(rows)-1} 行のデータ")
def _load_csv(self):
path = filedialog.askopenfilename(
filetypes=[("CSV", "*.csv"), ("すべて", "*.*")])
if not path:
return
try:
with open(path, encoding="utf-8-sig") as f:
reader = csv.reader(f)
rows = list(reader)
if rows:
self._rows = rows
self._refresh_table()
except Exception as e:
messagebox.showerror("エラー", str(e))
def _add_row(self):
vals = [e.get().strip() for e in self.row_entries]
if any(vals):
self._rows.append(vals)
self._refresh_table()
for e in self.row_entries:
e.delete(0, tk.END)
def _del_row(self):
sel = self.tree.selection()
if not sel:
return
idx = self.tree.index(sel[0]) + 1
if 1 <= idx < len(self._rows):
del self._rows[idx]
self._refresh_table()
def _on_double_click(self, event):
"""セルをダブルクリックして編集"""
region = self.tree.identify("region", event.x, event.y)
if region != "cell":
return
col = self.tree.identify_column(event.x)
row_iid = self.tree.identify_row(event.y)
if not row_iid:
return
col_idx = int(col.lstrip("#")) - 1
row_idx = self.tree.index(row_iid) + 1
current_val = self.tree.set(row_iid, f"c{col_idx}")
# ポップアップ編集
bbox = self.tree.bbox(row_iid, col)
if not bbox:
return
entry = tk.Entry(self.tree, font=("Arial", 10))
entry.place(x=bbox[0], y=bbox[1], width=bbox[2], height=bbox[3])
entry.insert(0, current_val)
entry.focus_set()
def commit(e=None):
new_val = entry.get()
self._rows[row_idx][col_idx] = new_val
self._refresh_table()
entry.destroy()
entry.bind("<Return>", commit)
entry.bind("<FocusOut>", commit)
def _export(self):
if not OPENPYXL_AVAILABLE:
messagebox.showerror("エラー",
"openpyxl をインストールしてください:\npip install openpyxl")
return
path = filedialog.asksaveasfilename(
defaultextension=".xlsx",
filetypes=[("Excel", "*.xlsx")])
if not path:
return
threading.Thread(target=self._do_export, args=(path,),
daemon=True).start()
def _do_export(self, path):
try:
# 画面と同じ計算結果を使う(合計が空欄になるのを防ぐ)
rows = self._filled_rows()
if not rows:
return
wb = openpyxl.Workbook()
ws = wb.active
ws.title = self.title_var.get() or "レポート"
# スタイル定義
style = self.style_var.get()
if style == "グリーン":
hdr_fill_color = "1E7E34"
row_fill_color = "D4EDDA"
elif style == "オレンジ":
hdr_fill_color = "E65100"
row_fill_color = "FFF3E0"
else: # ブルー
hdr_fill_color = "1565C0"
row_fill_color = "E3F2FD"
hdr_fill = PatternFill("solid", fgColor=hdr_fill_color)
row_fill = PatternFill("solid", fgColor=row_fill_color)
thin = Side(style="thin", color="AAAAAA")
border = Border(left=thin, right=thin, top=thin, bottom=thin)
center = Alignment(horizontal="center", vertical="center")
# タイトル行
title_cell = ws.cell(row=1, column=1,
value=self.title_var.get())
title_cell.font = Font(bold=True, size=14, color="222222")
title_cell.alignment = center
ws.merge_cells(start_row=1, start_column=1,
end_row=1, end_column=len(rows[0]))
# 生成日時
date_cell = ws.cell(row=2, column=1,
value=f"生成日時: {datetime.now().strftime('%Y-%m-%d %H:%M')}")
date_cell.font = Font(size=9, color="888888")
ws.row_dimensions[1].height = 30
ws.row_dimensions[2].height = 16
# ヘッダー
headers = rows[0]
for j, h in enumerate(headers, 1):
cell = ws.cell(row=3, column=j, value=h)
cell.fill = hdr_fill
cell.font = Font(bold=True, color="FFFFFF")
cell.alignment = center
cell.border = border
ws.row_dimensions[3].height = 22
# データ行
for i, row in enumerate(rows[1:], 4):
for j, val in enumerate(row, 1):
v = val
if v is None:
v = ""
# 数値変換
if isinstance(v, str):
try:
v = int(v)
except Exception:
try:
v = float(v)
except Exception:
pass
cell = ws.cell(row=i, column=j, value=v)
cell.border = border
cell.alignment = center
if i % 2 == 0:
cell.fill = row_fill
# 列幅調整
for col_cells in ws.columns:
max_len = 0
col_letter = get_column_letter(col_cells[0].column)
for cell in col_cells:
if cell.value:
max_len = max(max_len, len(str(cell.value)))
ws.column_dimensions[col_letter].width = min(max_len + 4, 30)
# 合計行
last_row = len(rows) + 3
try:
num_cols = len(rows[0])
for j in range(1, num_cols + 1):
cell = ws.cell(row=last_row, column=j)
col_letter = get_column_letter(j)
# 数値列に合計を入れる
try:
sample = float(rows[1][j-1])
cell.value = f"=SUM({col_letter}4:{col_letter}{last_row-1})"
cell.font = Font(bold=True)
except Exception:
if j == 1:
cell.value = "合計"
cell.font = Font(bold=True)
cell.fill = PatternFill("solid", fgColor="EEEEEE")
cell.border = border
cell.alignment = center
except Exception:
pass
# 合計列がある表だけ、グラフと条件付き書式を追加する
total_col = len(headers)
if "合計" in str(headers[-1]) and len(rows) > 1:
col_letter = get_column_letter(total_col)
# 棒グラフを埋め込む(商品名を横軸・合計を棒の高さにする)
chart = BarChart()
chart.title = f"{self.title_var.get()} 商品別の合計"
chart.y_axis.title = str(headers[-1])
data = Reference(ws, min_col=total_col,
min_row=3, max_row=last_row - 1)
cats = Reference(ws, min_col=1,
min_row=4, max_row=last_row - 1)
chart.add_data(data, titles_from_data=True)
chart.set_categories(cats)
chart.height, chart.width = 8, 16
ws.add_chart(chart, f"A{last_row + 2}")
# 条件付き書式(しきい値を超えた合計セルをExcel側で赤くする)
ws.conditional_formatting.add(
f"{col_letter}4:{col_letter}{last_row - 1}",
CellIsRule(operator="greaterThan",
formula=[str(self.HIGHLIGHT_TOTAL)],
fill=PatternFill("solid", fgColor="FFC7CE"),
font=Font(bold=True, color="9C0006")))
wb.save(path)
self.root.after(0, self._export_done, path)
except Exception as e:
self.root.after(0, lambda: messagebox.showerror("エラー", str(e)))
def _export_done(self, path):
self.status_var.set(f"保存完了: {os.path.basename(path)}")
messagebox.showinfo("完了", f"Excelファイルを保存しました:\n{path}")
if __name__ == "__main__":
root = tk.Tk()
app = App055(root)
root.mainloop()
5. コード解説
Excelレポート生成のコードを、実際に書かれている実装に沿って解説します。
クラス設計とコンストラクタ
App055 クラスにアプリの全機能をまとめています(メソッド12個・全384行)。__init__ ではタイトル「Excelレポート生成」とウィンドウサイズ 900x600 を設定します。そのあと _build_ui() → _refresh_table() の順に呼び出して初期状態を作ります。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
ウィジェット構成
画面は tk.Frame×4・tk.Label×6・ttk.Button×5・ttk.Entry(ループで生成)・ttk.Combobox・ttk.Treeview・ttk.Scrollbar×2・tk.Entry で構成しています。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
イベント処理とボタンの接続
「📂 CSV読込」ボタン(_load_csv())・「+ 行追加」ボタン(_add_row())・「✕ 行削除」ボタン(_del_row())・「📥 Excel出力」ボタン(_export())・「追加」ボタン(_add_row())を command= で接続しています。また bind("<Double-1>", ...) から _on_double_click()、bind("<Return>", ...) から 処理、bind("<FocusOut>", ...) から 処理 を呼べるようにイベントも登録しています。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
合計計算を1か所にまとめる(画面と Excel をずらさない)
合計列の計算は _calc_total()(単価×数量)と _filled_rows() の2つだけが担当します。_refresh_table()(画面)も _do_export()(Excel)も、自前で掛け算せず _filled_rows() の戻り値を使います。計算を1か所に置くと、片方だけ直して数値がずれる事故が起きません。
_filled_rows() が合計を埋めるのは「最終列の見出しに合計が含まれ、かつその欄が空のとき」だけです。読み込んだ CSV の最終列が合計とは限らないため、この条件で他人のデータを勝手に上書きしないようにしています。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
グラフの埋め込みと条件付き書式
グラフは BarChart() を作り、Reference() で「棒の高さにする範囲(合計列)」と「横軸のラベル(商品名列)」を指定して ws.add_chart(chart, f"A{last_row + 2}") で貼り付けます。合計行の2行下に置くので、表とグラフが重なりません。データ範囲の min_row を見出し行(3行目)にして titles_from_data=True を渡すと、凡例に「合計」がそのまま入ります。
条件付き書式は ws.conditional_formatting.add(範囲, CellIsRule(...)) で登録します。セルを直接塗る PatternFill と違い、これはルールとして Excel ファイルに保存されるため、出力後に Excel 上で合計セルの値を書き換えると、しきい値 HIGHLIGHT_TOTAL を超えた時点で自動的に赤くなります。なお合計列には計算式ではなく計算済みの値が入るため、数量セルだけを変えても合計は再計算されません。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
Windows 11 / Python 3.12 / openpyxl 3.1.5(検証時の環境・2026-09-08 に確認)で、サンプルデータのまま Excel を出力し、次を確かめました。画面表示に使う _filled_rows() の合計8行(ノートPC 490,000 〜 Webカメラ 59,500)が、出力した .xlsx を openpyxl で読み戻した合計欄と全行一致すること。BarChart が1つ、条件付き書式が E4:E11 に1件入っていること。
自分でも確かめられます。出力した .xlsx を開き、ノートPC の行が 98,000×5=490,000 になっているか、その合計セルが赤いか(100,000 超)、表の下に棒グラフがあるかを見てください。Excel 上でノートPC の合計セルを 98,000 に書き換えると赤が消えます。合計セルには計算式ではなく計算済みの値を書き込むので、数量セルだけを変えても合計は再計算されません。
例外処理
try-except で Exception・ImportError を捕捉しています。あわせて messagebox.showerror()・messagebox.showinfo() のダイアログで、ユーザーへの通知・確認を行います。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
6. ステップバイステップガイド
このアプリをゼロから自分で作る手順を解説します。コードをコピーするだけでなく、実際に手順を追って自分で書いてみましょう。
-
1ファイルを作成する
新しいファイルを作成して app055.py と保存します。外部ライブラリ
openpyxlを使います。先にpip install openpyxlを実行してください。 -
2クラスの骨格を作る
App055クラスを定義し、__init__と、末尾のroot = tk.Tk()~mainloop()の最小構成を書いて、まず空のウィンドウが出ることを確認します。 -
3ウィンドウを設定する
title("Excelレポート生成")・geometry("900x600")・configure(bg="#1e1e1e")をコンストラクタで設定します。 -
4画面部品を並べる
_build_ui()の中で、tk.Frame×4・tk.Label×6・ttk.Button×5・ttk.Entry(ループで生成)・ttk.Combobox・ttk.Treeview・ttk.Scrollbar×2・tk.Entryを作って配置します(掲載コードと同じ並び順で書くとレイアウトが一致します)。まず表示だけ確認しましょう。 -
5イベントを接続する
「2. 機能一覧」で挙げた各ボタンを
command=で対応するメソッド(_load_csv()・_add_row()・_del_row()・_export()・_add_row())につなぎます。bind("<Double-1>", ...)・bind("<Return>", ...)・bind("<FocusOut>", ...)の登録も忘れずに。 -
6中心になるメソッドを実装する
アプリの本体である
_export_done()(3行)・_on_double_click()(30行)・_load_csv()(14行)など を実装します。合計計算は_calc_total()・_filled_rows()に切り出し、_refresh_table()と_do_export()の両方から呼ぶようにしてください。ここを分けずに画面側だけで掛け算すると、Excel の合計欄が空のまま出力されます。 -
7動作確認する
python app055.pyで起動し、各ボタンが反応すること・キー操作(<Return>)が効くこと・マウス操作(クリック・ホイールなど)が効くことを確認します。
7. カスタマイズアイデア
基本機能を習得したら、以下のカスタマイズに挑戦してみましょう。
💡 ダークモードを追加する
bg色・fg色を辞書で管理し、ボタン1つでダークモード・ライトモードを切り替えられるようにしましょう。
💡 クラス定数を書き換えて動きを変える
データはクラス定数(SAMPLE_DATA・HIGHLIGHT_TOTAL)にまとまっています。中身を書き換えて動きが変わることを確かめましょう。複数の定数が対応関係にある場合は、セットで整合させる必要がある点に注意してください。
💡 条件付き書式のしきい値を変える・増やす
クラス定数 HIGHLIGHT_TOTAL(既定 100,000)を書き換えると、赤くなる合計の基準が変わります。ws.conditional_formatting.add() をもう1回呼び、operator="lessThan" のルールを足せば「小さすぎる合計は青」といった2色運用もできます。
💡 設定ダイアログ
フォントサイズや色などの設定をユーザーが変更できるオプションダイアログを追加しましょう。
8. よくある問題と解決法
❌ 文字のフォントが崩れる・見た目が違う
原因:このコードは font=("Noto Sans JP", ...) のようにフォント名を直接指定しています。環境にこのフォントが入っていないと、OS が代わりのフォントで表示するため、見本と見た目が変わることがあります。
解決法:動作には支障ありません。気になる場合は font 引数の名前を自分の OS に入っているフォントへ書き換えるか、font 引数を省略してください。
❌ <Return> キーを押しても反応しない
原因:bind("<Return>", ...) はウィジェット単位で登録されるため、ウィンドウ(またはバインド先)にキーボードフォーカスが無いとイベントが届きません。
解決法:一度ウィンドウ内をクリックしてフォーカスを与えてから、キーを押してください。
❌ ウィンドウの大きさが画面に合わない
原因:geometry("900x600") の固定サイズで起動するためです。
解決法:この数値を書き換えて起動サイズを調整してください。なお、このコードにはウィンドウサイズを固定する設定(resizable の指定)が無いため、ウィンドウの端をドラッグしたサイズ変更は既定どおり可能です。
9. 練習問題
アプリの理解を深めるための練習問題です。気になるものから挑戦してみてください(課題3は定型の発展課題です)。
-
課題1:赤くするしきい値を画面から指定する
クラス定数
HIGHLIGHT_TOTAL(100,000)に固定されている「合計を赤くするしきい値」を、ツールバーの入力欄から変えられるようにします。期待結果:サンプルデータのまま出力すると、Excel の「合計」列で赤くなるのは 490,000 / 135,000 / 256,000 / 380,000 の4セル。しきい値を 50000 にして出力し直すと 87,000・60,000・59,500 が加わって7セルになる(マウスの 50,000 はちょうど同値なので赤くならない)。
合格条件
- 入力欄が空・数字以外のときは既定の 100,000 で出力される(落ちない)
- しきい値を変えても合計行の
=SUM(...)数式は変わらない - 画面の表(Treeview)の見た目は変わらない(色が付くのは出力した .xlsx の中だけ)
ヒント①(どこを触るか): しきい値を使っているのは
_do_export()のCellIsRule(operator="greaterThan", formula=[str(self.HIGHLIGHT_TOTAL)], …)の1か所です。入力欄は_build_ui()のツールバー(スタイル選択の隣)に足します。
ヒント②(使うもの): 入力はtk.StringVarで受け取り、int()をtry / except ValueErrorで囲みます。CellIsRuleのformulaは文字列のリストなので[str(値)]の形にします。
つまずきやすい点:_do_export()はthreading.Threadで別スレッドとして動きます。入力欄の読み取りと数値変換は、スレッドを起動する_export()の側(メインスレッド)で済ませ、引数で渡す形にしてください。 -
課題2:カテゴリ別の集計シートを足す
出力する .xlsx に「カテゴリ別」シートを追加し、カテゴリごとの合計金額を書き出します。
期待結果:サンプルデータのまま出力すると、「売上レポート」シートのほかに「カテゴリ別」シートができ、電子機器 1,066,500 / 家具 391,000 / アクセサリ 60,000 の3行が入る。
合格条件
- 3行の合計が元の表の合計行(
=SUM(E4:E11)の結果 1,517,500)と一致する - 「カテゴリ」列を持たない CSV を読み込んだときはシートを作らない(落ちない)
- 元の「売上レポート」シートの中身は変わらない
ヒント①(どこを触るか): 集計元は
_filled_rows()が返す表(合計列が計算済み)です。シートを足すのは_do_export()のwb.save(path)の前。
ヒント②(使うもの): 新しいシートはwb.create_sheet("カテゴリ別")、集計はcollections.defaultdict(int)(from collections import defaultdictを追加)か、追加の import が要らない辞書のget()で足し込みます。カテゴリ列の位置はheaders.index("カテゴリ")で調べられます。
つまずきやすい点: CSV を読み込んだ直後の値はすべて文字列なので、そのまま足すとTypeError: unsupported operand type(s) for +=: 'int' and 'str'になります。足す前にint()へ変換してください(_calc_total()が同じことをしています)。 - 3行の合計が元の表の合計行(
-
課題3:新しいボタンを追加する
既存のボタンと同じ書き方で新しいボタンを1つ足し、押されたときに動くメソッドを自作してみましょう。ボタンの作成と
command=の接続がセットで身に付きます。
写経中に赤いエラー文が出たら、Pythonエラー一覧&解決法(英語メッセージ逆引き)で原因と直し方をすぐ確認できます。
このレベルのアプリが作れたら、入門書の次の「作るための本」へ進む時期です。実践におすすめのPython本(当サイトの参考書ランキング総合3〜4位)で自動化とコードの書き方の2冊を、その直後の用途別専門書の節でデータ分析・Web・AIの本を比較しています。