ファイル圧縮・解凍ツール
ZIP・TAR・GZ形式でファイルを圧縮・解凍するGUIツール。zipfileとtarfileモジュールの活用を学びます。
1. アプリ概要
ZIP・TAR・GZ形式でファイルを圧縮・解凍するGUIツール。zipfileとtarfileモジュールの活用を学びます。
このアプリは中級カテゴリに分類される実践的なGUIアプリです。使用ライブラリは tkinter(標準ライブラリ) で、難易度は ★★☆ です。
このアプリは「ファイル操作」カテゴリです。ファイル操作は業務スクリプトの心臓部で、I/O のパターンは他のあらゆる Python プログラムで再利用できます。tkinter(標準ライブラリ) を活かして実装するこの構造は、他のアプリにも応用が効きます。
コピー&実行で動作確認 → コード解説で構造理解 → カスタマイズで応用、の 3 ステップで進めると、短時間で本質を押さえられます。
アプリを完成させた後は、自分の使い方に合わせて改造するのが学びを定着させる近道です。カスタマイズ章のアイデアを足がかりに、独自機能を一つ追加してみてください。
2. 機能一覧
- 「➕ ファイルを追加」ボタン(
_add_files_compress())・「📂 フォルダを追加」ボタン(_add_folder_compress())・「🗑 リストをクリア」ボタン(_clear_compress_list())・「📂」ボタン(_browse_compress_out())・「▶ 圧縮実行」ボタン(_compress())・「📂 参照」ボタン(_browse_archive())・「📂 参照」ボタン(lambda: self.extract_out_var.set(filedialog.askdirectory() or self.extract_out_var.get()))・「▶ 解凍実行」ボタン(_extract())・「📂 参照」ボタン(_browse_view_archive())・「🔍 内容確認」ボタン(_view_archive())で操作 tk.Listboxによる一覧表示(スクロールバー付き)ttk.Treeviewによる表形式の一覧表示filedialog.askdirectory()によるファイル選択ダイアログfiledialog.askopenfilename()によるファイル選択ダイアログfiledialog.askopenfilenames()によるファイル選択ダイアログfiledialog.asksaveasfilename()によるファイル選択ダイアログmessagebox.showerror()・messagebox.showwarning()によるダイアログ通知- ウィンドウはタイトル「ファイル圧縮・解凍ツール」・サイズ 860x600 で起動
3. 事前準備・環境
Python 3.10 以上 / Windows 11(実機)で起動・基本操作を確認 / Linux Mint 22.3(仮想マシン)で起動・画面表示を確認(macOS は未検証)
Windows 11(実機)で起動と基本操作を確認しています(全機能の網羅テストではありません)。Linux Mint 22.3(仮想マシン)では起動と画面表示を確認しました。macOS は標準ライブラリの範囲で動作する想定ですが、未検証です。
- Python 3.10 以上
- OS: Windows 11(実機で起動・基本操作を確認)・Linux Mint 22.3(仮想マシンで起動・画面表示を確認)
4. 完全なソースコード
右上の「コピー」ボタンをクリックするとコードをクリップボードにコピーできます。
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import zipfile
import tarfile
import os
import threading
from datetime import datetime
class App35:
"""ファイル圧縮・解凍ツール"""
def __init__(self, root):
self.root = root
self.root.title("ファイル圧縮・解凍ツール")
self.root.geometry("860x600")
self.root.configure(bg="#f8f9fc")
self._files_to_compress = []
self._build_ui()
def _build_ui(self):
header = tk.Frame(self.root, bg="#01579b", pady=8)
header.pack(fill=tk.X)
tk.Label(header, text="🗜️ ファイル圧縮・解凍ツール",
font=("Noto Sans JP", 13, "bold"),
bg="#01579b", fg="white").pack(side=tk.LEFT, padx=12)
notebook = ttk.Notebook(self.root)
notebook.pack(fill=tk.BOTH, expand=True, padx=6, pady=6)
# 圧縮タブ
compress_tab = tk.Frame(notebook, bg="#f8f9fc")
notebook.add(compress_tab, text="📦 圧縮")
self._build_compress_tab(compress_tab)
# 解凍タブ
extract_tab = tk.Frame(notebook, bg="#f8f9fc")
notebook.add(extract_tab, text="📂 解凍")
self._build_extract_tab(extract_tab)
# アーカイブ閲覧タブ
view_tab = tk.Frame(notebook, bg="#f8f9fc")
notebook.add(view_tab, text="🔍 内容確認")
self._build_view_tab(view_tab)
self.status_var = tk.StringVar(value="ファイルを選択してください")
tk.Label(self.root, textvariable=self.status_var,
bg="#dde", font=("Arial", 9), anchor="w", padx=8
).pack(fill=tk.X, side=tk.BOTTOM)
# ── 圧縮タブ ──────────────────────────────────────────────────
def _build_compress_tab(self, parent):
lbl_s = {"bg": "#f8f9fc", "font": ("Arial", 10)}
# ファイル/フォルダ追加
top_f = tk.Frame(parent, bg="#f8f9fc")
top_f.pack(fill=tk.X, padx=8, pady=6)
ttk.Button(top_f, text="➕ ファイルを追加",
command=self._add_files_compress).pack(side=tk.LEFT, padx=4)
ttk.Button(top_f, text="📂 フォルダを追加",
command=self._add_folder_compress).pack(side=tk.LEFT, padx=4)
ttk.Button(top_f, text="🗑 リストをクリア",
command=self._clear_compress_list).pack(side=tk.LEFT, padx=4)
# ファイルリスト
list_f = ttk.LabelFrame(parent, text="圧縮するファイル/フォルダ", padding=4)
list_f.pack(fill=tk.BOTH, expand=True, padx=8, pady=4)
self.compress_listbox = tk.Listbox(list_f, font=("Arial", 10), height=10)
sb = ttk.Scrollbar(list_f, command=self.compress_listbox.yview)
self.compress_listbox.configure(yscrollcommand=sb.set)
sb.pack(side=tk.RIGHT, fill=tk.Y)
self.compress_listbox.pack(fill=tk.BOTH, expand=True)
# 設定
setting_f = ttk.LabelFrame(parent, text="圧縮設定", padding=8)
setting_f.pack(fill=tk.X, padx=8, pady=4)
row1 = tk.Frame(setting_f, bg=ttk.Style().lookup("TLabelframe", "background"))
row1.pack(fill=tk.X, pady=2)
tk.Label(row1, text="形式:", **lbl_s).pack(side=tk.LEFT)
self.compress_fmt_var = tk.StringVar(value="ZIP")
ttk.Combobox(row1, textvariable=self.compress_fmt_var,
values=["ZIP", "TAR", "TAR.GZ", "TAR.BZ2"],
state="readonly", width=10).pack(side=tk.LEFT, padx=6)
tk.Label(row1, text="圧縮レベル:", **lbl_s).pack(side=tk.LEFT, padx=(12, 0))
self.compress_level_var = tk.IntVar(value=6)
ttk.Spinbox(row1, from_=1, to=9,
textvariable=self.compress_level_var, width=4).pack(side=tk.LEFT, padx=4)
row2 = tk.Frame(setting_f, bg=ttk.Style().lookup("TLabelframe", "background"))
row2.pack(fill=tk.X, pady=4)
tk.Label(row2, text="出力先:", **lbl_s).pack(side=tk.LEFT)
self.compress_out_var = tk.StringVar()
ttk.Entry(row2, textvariable=self.compress_out_var,
width=40).pack(side=tk.LEFT, padx=4, fill=tk.X, expand=True)
ttk.Button(row2, text="📂",
command=self._browse_compress_out).pack(side=tk.LEFT)
ttk.Button(parent, text="▶ 圧縮実行",
command=self._compress).pack(padx=8, pady=6)
self.compress_progress = ttk.Progressbar(parent, mode="indeterminate")
self.compress_progress.pack(fill=tk.X, padx=8, pady=2)
def _add_files_compress(self):
paths = filedialog.askopenfilenames()
for p in paths:
if p not in self._files_to_compress:
self._files_to_compress.append(p)
self.compress_listbox.insert(tk.END, p)
self._suggest_output()
def _add_folder_compress(self):
path = filedialog.askdirectory()
if path and path not in self._files_to_compress:
self._files_to_compress.append(path)
self.compress_listbox.insert(tk.END, path)
self._suggest_output()
def _clear_compress_list(self):
self._files_to_compress.clear()
self.compress_listbox.delete(0, tk.END)
def _suggest_output(self):
if self._files_to_compress and not self.compress_out_var.get():
first = self._files_to_compress[0]
base = os.path.splitext(os.path.basename(first))[0]
folder = os.path.dirname(first)
fmt = self.compress_fmt_var.get().lower().replace(".", "")
ext = ".zip" if fmt == "zip" else ".tar" if fmt == "tar" else \
".tar.gz" if fmt == "targz" else ".tar.bz2"
self.compress_out_var.set(os.path.join(folder, base + ext))
def _browse_compress_out(self):
fmt = self.compress_fmt_var.get()
ft = [("ZIP", "*.zip")] if fmt == "ZIP" else \
[("TAR", "*.tar"), ("TAR.GZ", "*.tar.gz"), ("TAR.BZ2", "*.tar.bz2")]
path = filedialog.asksaveasfilename(filetypes=ft + [("すべて", "*.*")])
if path:
self.compress_out_var.set(path)
def _compress(self):
if not self._files_to_compress:
messagebox.showwarning("警告", "ファイルを追加してください")
return
out = self.compress_out_var.get().strip()
if not out:
messagebox.showwarning("警告", "出力先を指定してください")
return
self.compress_progress.start(10)
threading.Thread(target=self._do_compress,
args=(list(self._files_to_compress), out),
daemon=True).start()
def _do_compress(self, files, out):
fmt = self.compress_fmt_var.get()
level = self.compress_level_var.get()
try:
if fmt == "ZIP":
with zipfile.ZipFile(out, "w",
compression=zipfile.ZIP_DEFLATED,
compresslevel=level) as zf:
for path in files:
if os.path.isdir(path):
for dirpath, _, filenames in os.walk(path):
for fn in filenames:
fp = os.path.join(dirpath, fn)
arcname = os.path.relpath(fp, os.path.dirname(path))
zf.write(fp, arcname)
else:
zf.write(path, os.path.basename(path))
else:
mode_map = {"TAR": "w", "TAR.GZ": "w:gz", "TAR.BZ2": "w:bz2"}
mode = mode_map[fmt]
with tarfile.open(out, mode) as tf:
for path in files:
tf.add(path, arcname=os.path.basename(path))
size_kb = os.path.getsize(out) // 1024
self.root.after(0, self.compress_progress.stop)
self.root.after(0, self.status_var.set,
f"圧縮完了: {out} ({size_kb}KB)")
self.root.after(0, messagebox.showinfo, "完了",
f"圧縮完了!\n{out}\n({size_kb}KB)")
except Exception as e:
self.root.after(0, self.compress_progress.stop)
self.root.after(0, messagebox.showerror, "エラー", str(e))
# ── 解凍タブ ──────────────────────────────────────────────────
def _build_extract_tab(self, parent):
lbl_s = {"bg": "#f8f9fc", "font": ("Arial", 10)}
# ファイル選択
row1 = tk.Frame(parent, bg="#f8f9fc")
row1.pack(fill=tk.X, padx=8, pady=8)
tk.Label(row1, text="アーカイブ:", **lbl_s).pack(side=tk.LEFT)
self.extract_file_var = tk.StringVar()
ttk.Entry(row1, textvariable=self.extract_file_var,
width=45).pack(side=tk.LEFT, padx=6, fill=tk.X, expand=True)
ttk.Button(row1, text="📂 参照",
command=self._browse_archive).pack(side=tk.LEFT)
# 出力先
row2 = tk.Frame(parent, bg="#f8f9fc")
row2.pack(fill=tk.X, padx=8, pady=4)
tk.Label(row2, text="解凍先:", **lbl_s).pack(side=tk.LEFT)
self.extract_out_var = tk.StringVar()
ttk.Entry(row2, textvariable=self.extract_out_var,
width=45).pack(side=tk.LEFT, padx=6, fill=tk.X, expand=True)
ttk.Button(row2, text="📂 参照",
command=lambda: self.extract_out_var.set(
filedialog.askdirectory() or self.extract_out_var.get())
).pack(side=tk.LEFT)
# オプション
opt_f = ttk.LabelFrame(parent, text="オプション", padding=6)
opt_f.pack(fill=tk.X, padx=8, pady=4)
self.extract_subdir_var = tk.BooleanVar(value=True)
ttk.Checkbutton(opt_f, text="アーカイブ名のサブフォルダに解凍",
variable=self.extract_subdir_var).pack(anchor="w")
self.extract_overwrite_var = tk.BooleanVar(value=False)
ttk.Checkbutton(opt_f, text="既存ファイルを上書き",
variable=self.extract_overwrite_var).pack(anchor="w")
ttk.Button(parent, text="▶ 解凍実行",
command=self._extract).pack(padx=8, pady=6)
self.extract_progress = ttk.Progressbar(parent, mode="indeterminate")
self.extract_progress.pack(fill=tk.X, padx=8, pady=2)
# ログ
log_f = ttk.LabelFrame(parent, text="解凍ログ", padding=4)
log_f.pack(fill=tk.BOTH, expand=True, padx=8, pady=4)
self.extract_log = tk.Text(log_f, height=8, font=("Courier New", 10),
state=tk.DISABLED, bg="#fafafa")
self.extract_log.pack(fill=tk.BOTH, expand=True)
def _browse_archive(self):
path = filedialog.askopenfilename(
filetypes=[("アーカイブ", "*.zip *.tar *.tar.gz *.tar.bz2 *.tgz"),
("すべて", "*.*")])
if path:
self.extract_file_var.set(path)
if not self.extract_out_var.get():
self.extract_out_var.set(os.path.dirname(path))
def _extract(self):
archive = self.extract_file_var.get().strip()
if not archive or not os.path.exists(archive):
messagebox.showwarning("警告", "アーカイブファイルを選択してください")
return
out = self.extract_out_var.get().strip() or os.path.dirname(archive)
if self.extract_subdir_var.get():
base = os.path.splitext(os.path.basename(archive))[0]
if base.endswith(".tar"):
base = base[:-4]
out = os.path.join(out, base)
os.makedirs(out, exist_ok=True)
self.extract_progress.start(10)
threading.Thread(target=self._do_extract,
args=(archive, out), daemon=True).start()
def _do_extract(self, archive, out):
try:
extracted = 0
if zipfile.is_zipfile(archive):
with zipfile.ZipFile(archive, "r") as zf:
members = zf.namelist()
for m in members:
zf.extract(m, out)
extracted += 1
self.root.after(0, self._log_extract, f" {m}")
elif tarfile.is_tarfile(archive):
with tarfile.open(archive) as tf:
members = tf.getmembers()
for m in members:
tf.extract(m, out)
extracted += 1
self.root.after(0, self._log_extract, f" {m.name}")
else:
raise ValueError("サポートされていない形式です")
self.root.after(0, self.extract_progress.stop)
self.root.after(0, self._log_extract,
f"\n✓ 解凍完了: {extracted} ファイル → {out}")
self.root.after(0, self.status_var.set,
f"解凍完了: {extracted} ファイル")
except Exception as e:
self.root.after(0, self.extract_progress.stop)
self.root.after(0, messagebox.showerror, "エラー", str(e))
def _log_extract(self, text):
self.extract_log.config(state=tk.NORMAL)
self.extract_log.insert(tk.END, text + "\n")
self.extract_log.see(tk.END)
self.extract_log.config(state=tk.DISABLED)
# ── 内容確認タブ ──────────────────────────────────────────────
def _build_view_tab(self, parent):
row = tk.Frame(parent, bg="#f8f9fc")
row.pack(fill=tk.X, padx=8, pady=8)
tk.Label(row, text="アーカイブ:", bg="#f8f9fc",
font=("Arial", 10)).pack(side=tk.LEFT)
self.view_file_var = tk.StringVar()
ttk.Entry(row, textvariable=self.view_file_var,
width=45).pack(side=tk.LEFT, padx=6, fill=tk.X, expand=True)
ttk.Button(row, text="📂 参照",
command=self._browse_view_archive).pack(side=tk.LEFT)
ttk.Button(row, text="🔍 内容確認",
command=self._view_archive).pack(side=tk.LEFT, padx=4)
cols = ("name", "size", "compressed", "type", "date")
self.view_tree = ttk.Treeview(parent, columns=cols, show="headings")
for c, h, w in [("name", "ファイル名", 280), ("size", "元サイズ", 80),
("compressed", "圧縮後", 80), ("type", "種別", 60),
("date", "更新日", 130)]:
self.view_tree.heading(c, text=h)
self.view_tree.column(c, width=w, minwidth=40)
sb = ttk.Scrollbar(parent, command=self.view_tree.yview)
self.view_tree.configure(yscrollcommand=sb.set)
sb.pack(side=tk.RIGHT, fill=tk.Y)
self.view_tree.pack(fill=tk.BOTH, expand=True, padx=8)
self.view_info_var = tk.StringVar()
tk.Label(parent, textvariable=self.view_info_var, bg="#f8f9fc",
font=("Arial", 9), fg="#555").pack(anchor="w", padx=8, pady=2)
def _browse_view_archive(self):
path = filedialog.askopenfilename(
filetypes=[("アーカイブ", "*.zip *.tar *.tar.gz *.tar.bz2 *.tgz"),
("すべて", "*.*")])
if path:
self.view_file_var.set(path)
self._view_archive()
def _view_archive(self):
path = self.view_file_var.get().strip()
if not path or not os.path.exists(path):
messagebox.showwarning("警告", "アーカイブファイルを選択してください")
return
self.view_tree.delete(*self.view_tree.get_children())
total_orig = 0
total_comp = 0
count = 0
try:
if zipfile.is_zipfile(path):
with zipfile.ZipFile(path) as zf:
for info in zf.infolist():
kind = "📁" if info.is_dir() else "📄"
date_str = f"{info.date_time[0]}/{info.date_time[1]:02d}/{info.date_time[2]:02d}"
self.view_tree.insert(
"", "end",
values=(info.filename,
self._fmt_size(info.file_size),
self._fmt_size(info.compress_size),
kind, date_str))
total_orig += info.file_size
total_comp += info.compress_size
count += 1
elif tarfile.is_tarfile(path):
with tarfile.open(path) as tf:
for m in tf.getmembers():
kind = "📁" if m.isdir() else "📄"
dt = datetime.fromtimestamp(m.mtime).strftime(
"%Y/%m/%d") if m.mtime else ""
self.view_tree.insert(
"", "end",
values=(m.name, self._fmt_size(m.size),
"—", kind, dt))
total_orig += m.size
count += 1
else:
messagebox.showerror("エラー", "サポートされていない形式です")
return
ratio = (1 - total_comp / total_orig) * 100 if total_orig else 0
self.view_info_var.set(
f"合計: {count} アイテム | "
f"元サイズ: {self._fmt_size(total_orig)} | "
f"圧縮後: {self._fmt_size(total_comp)} | "
f"圧縮率: {ratio:.1f}%")
self.status_var.set(f"確認完了: {count} アイテム")
except Exception as e:
messagebox.showerror("エラー", str(e))
def _fmt_size(self, size):
for unit in ["B", "KB", "MB", "GB"]:
if size < 1024:
return f"{size:.0f}{unit}"
size /= 1024
return f"{size:.1f}TB"
if __name__ == "__main__":
root = tk.Tk()
app = App35(root)
root.mainloop()
5. コード解説
ファイル圧縮・解凍ツールのコードを、実際に書かれている実装に沿って解説します。
クラス設計とコンストラクタ
App35 クラスにアプリの全機能をまとめています(メソッド19個・全401行)。__init__ ではタイトル「ファイル圧縮・解凍ツール」とウィンドウサイズ 860x600 を設定します。最後に _build_ui() で画面を組み立てます。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
ウィジェット構成
画面は tk.Frame×10・tk.Label×9・ttk.Notebook・ttk.Button×10・ttk.LabelFrame×4・tk.Listbox・ttk.Scrollbar×2・ttk.Style×2・ttk.Combobox・ttk.Spinbox・ttk.Entry×4・ttk.Progressbar×2・ttk.Checkbutton×2・tk.Text・ttk.Treeview で構成しています。見出し付きの枠(LabelFrame)は「圧縮するファイル/フォルダ」「圧縮設定」「オプション」「解凍ログ」のラベルでエリアを区切っています。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
イベント処理とボタンの接続
「➕ ファイルを追加」ボタン(_add_files_compress())・「📂 フォルダを追加」ボタン(_add_folder_compress())・「🗑 リストをクリア」ボタン(_clear_compress_list())・「📂」ボタン(_browse_compress_out())・「▶ 圧縮実行」ボタン(_compress())・「📂 参照」ボタン(_browse_archive())・「📂 参照」ボタン(lambda: self.extract_out_var.set(filedialog.askdirectory() or self.extract_out_var.get()))・「▶ 解凍実行」ボタン(_extract())・「📂 参照」ボタン(_browse_view_archive())・「🔍 内容確認」ボタン(_view_archive())を command= で接続しています。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
例外処理
try-except で Exception を捕捉しています。あわせて messagebox.showerror()・messagebox.showwarning() のダイアログで、ユーザーへの通知・確認を行います。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
6. ステップバイステップガイド
このアプリをゼロから自分で作る手順を解説します。コードをコピーするだけでなく、実際に手順を追って自分で書いてみましょう。
-
1ファイルを作成する
新しいファイルを作成して app35.py と保存します。使うのは
datetime・os・tarfile・threading・tkinter・zipfileだけなので、追加インストールは不要です。 -
2クラスの骨格を作る
App35クラスを定義し、__init__と、末尾のroot = tk.Tk()~mainloop()の最小構成を書いて、まず空のウィンドウが出ることを確認します。 -
3ウィンドウを設定する
title("ファイル圧縮・解凍ツール")・geometry("860x600")・configure(bg="#f8f9fc")をコンストラクタで設定します。 -
4画面部品を並べる
_build_ui()の中で、tk.Frame×10・tk.Label×9・ttk.Notebook・ttk.Button×10・ttk.LabelFrame×4・tk.Listbox・ttk.Scrollbar×2・ttk.Style×2・ttk.Combobox・ttk.Spinbox・ttk.Entry×4・ttk.Progressbar×2・ttk.Checkbutton×2・tk.Text・ttk.Treeviewを作って配置します(掲載コードと同じ並び順で書くとレイアウトが一致します)。まず表示だけ確認しましょう。 -
5イベントを接続する
「2. 機能一覧」で挙げた各ボタンを
command=で対応するメソッド(_add_files_compress()・_add_folder_compress()・_clear_compress_list()・_browse_compress_out()・_compress()・_browse_archive()・lambda: self.extract_out_var.set(filedialog.askdirectory() or self.extract_out_var.get())・_extract()・_browse_view_archive()・_view_archive())につなぎます。 -
6中心になるメソッドを実装する
アプリの本体である
_log_extract()(5行)・_view_archive()(49行)・_extract()(15行)など を実装します。 -
7動作確認する
python app35.pyで起動し、各ボタンが反応することを確認します。
7. カスタマイズアイデア
基本機能を習得したら、以下のカスタマイズに挑戦してみましょう。少しずつ機能を追加することで、Pythonのスキルが飛躍的に向上します。
💡 ダークモードを追加する
bg色・fg色を辞書で管理し、ボタン1つでダークモード・ライトモードを切り替えられるようにしましょう。
💡 データのエクスポート機能
計算結果をCSV・TXTファイルに保存するエクスポート機能を追加しましょう。filedialog.asksaveasfilename()でファイル保存ダイアログが使えます。
💡 一覧の内容をファイルに書き出す
tk.Listbox に溜めた内容は、アプリを閉じると消えます(このコードに保存処理はありません)。一覧を1行ずつテキストファイルへ書き出す保存ボタンを追加しましょう。保存先の選択には、既に import されている filedialog が使えます。
8. よくある問題と解決法
❌ 文字のフォントが崩れる・見た目が違う
原因:このコードは font=("Noto Sans JP", ...) のようにフォント名を直接指定しています。環境にこのフォントが入っていないと、OS が代わりのフォントで表示するため、見本と見た目が変わることがあります。
解決法:動作には支障ありません。気になる場合は font 引数の名前を自分の OS に入っているフォントへ書き換えるか、font 引数を省略してください。
❌ ウィンドウの大きさが画面に合わない
原因:geometry("860x600") の固定サイズで起動するためです。
解決法:この数値を書き換えて起動サイズを調整してください。なお、このコードにはウィンドウサイズを固定する設定(resizable の指定)が無いため、ウィンドウの端をドラッグしたサイズ変更は既定どおり可能です。
9. 練習問題
アプリの理解を深めるための練習問題です。気になるものから挑戦してみてください(課題3は定型の発展課題です)。「▶ 解凍実行」は、同じ名前のファイルがあっても確認なしで上書きします(課題1のとおり「既存ファイルを上書き」のチェックは読まれていません)。練習は、消えても困らない専用のフォルダを新しく作って行ってください。使うのは自分で作ったアーカイブだけにしてください(配布元の分からない ZIP・TAR は、中の名前が ../ や絶対パスになっていると、解凍先の外へファイルを書き出せてしまいます。とくに tarfile は Python 3.13 以前だとこれを止めません)。「🔍 内容確認」タブは中身を読むだけなので、先にそこで一覧を見ておくと安全です。追加のライブラリは要りません。
-
課題1:「既存ファイルを上書き」のチェックが効いていない
解凍タブには「既存ファイルを上書き」というチェックがあります。ところが
_do_extract()はこの値をどこでも読んでいません。チェックを外していても同じ名前のファイルは置き換わります。表示どおりに動かしてください。期待結果:解凍先にするフォルダへ
a.txt(中身はnew)を置く。同じ名前のa.txt(中身はold)だけを入れた ZIP を用意する。「アーカイブ名のサブフォルダに解凍」を外し、解凍先をそのフォルダにして、「既存ファイルを上書き」は外したまま「▶ 解凍実行」を押す。確認は出ず、a.txtの中身はoldに変わる。解凍ログにはa.txtと✓ 解凍完了: 1 ファイル → 解凍先のパスが出る。直したあとは中身がnewのまま残る。合格条件
- チェックを入れたときは、これまでどおり全部が上書きされる
- スキップしたファイルは「解凍完了: N ファイル」の N に数えない
- どのファイルをスキップしたのか、解凍ログを見れば分かる
ヒント①(どこを触るか): 触るのは
_do_extract()の2か所です。ZIP 側のfor m in members:のループと、TAR 側のfor m in members:のループです。
ヒント②(使うもの): 解凍先のパスはos.path.join(out, m)(TAR ならm.name)で作れます。os.path.exists()が真でself.extract_overwrite_var.get()が偽ならスキップし、理由をログに出します。_do_extract()は別スレッドで動くので、275行・282行と同じくself.root.after(0, self._log_extract, f" スキップ(既存): {m}")の形で呼んでください。
つまずきやすい点:zf.extract()にもtf.extract()にも「上書きしない」という指定はありません(呼んだ時点で置き換わります)。ZIP の中のフォルダは名前が/で終わり、os.path.exists()も真になりやすいので、フォルダまでスキップと数えると件数がずれます。self.extract_overwrite_varは_build_extract_tab()で作られたきり、どのメソッドからも読まれていません。 -
課題2:TAR を開くと圧縮率が必ず 100.0%
「🔍 内容確認」タブで
.tar.gzを開くと、下の行は圧縮率: 100.0%になります。TAR の分岐がtotal_compに何も足していないためです。正しい値か、集計できない印を出してください。期待結果:同じフォルダを ZIP と TAR.GZ の2つに圧縮し、「🔍 内容確認」で順に開く。ZIP は「圧縮後」列に数字が並び、圧縮率もそれらしい値になる。TAR.GZ は「圧縮後」列が全行
—で、下の行は圧縮後: 0B | 圧縮率: 100.0%。中身が1バイトも入っていないように読めるが、実際のファイルには中身がある。直したあとは、実際の圧縮率か—になる。合格条件
- ZIP を開いたときの表示は、これまでと同じ
- TAR.GZ の圧縮率が 100.0% にならない
- 元サイズの合計が0のときも、ゼロ除算で落ちない
ヒント①(どこを触るか): 触るのは
_view_archive()末尾の1か所です。ratioを求める行と、その下のself.view_info_var.set(...)です。
ヒント②(使うもの): TAR はメンバーごとの圧縮後サイズを持ちません。アーカイブ全体の大きさos.path.getsize(path)を使えば、まとめての圧縮率は出せます。集計できないときに圧縮率: —と出す手もあります。
つまずきやすい点:total_compの初期値は 0 で、(1 - 0 / total_orig) * 100はいつでも 100.0 です。エラーにならないぶん気づきにくい罠。.tar(無圧縮)は元サイズの合計よりファイルが少し大きくなり、全体で見た圧縮率がマイナスになることもあります。_fmt_size()は 1024 バイト未満を0Bの形で出すため、0 と「集計できない」の区別が付きません。 -
課題3:保存機能の追加
入力値や計算結果をファイルに保存する機能を追加しましょう。jsonやcsvモジュールを使います。
写経中に赤いエラー文が出たら、Pythonエラー一覧&解決法(英語メッセージ逆引き)で原因と直し方をすぐ確認できます。
このレベルのアプリが作れたら、入門書の次の「作るための本」へ進む時期です。実践におすすめのPython本(当サイトの参考書ランキング総合3〜4位)で自動化とコードの書き方の2冊を、その直後の用途別専門書の節でデータ分析・Web・AIの本を比較しています。