Gitログビューアー
Gitリポジトリのコミット履歴・差分・ブランチをGUIで可視化するツール。subprocessでgitコマンドを実行する方法を学びます。
1. アプリ概要
Gitリポジトリのコミット履歴・差分・ブランチをGUIで可視化するツール。subprocessでgitコマンドを実行する方法を学びます。
このアプリは中級カテゴリに分類される実践的なGUIアプリです。使用ライブラリは tkinter(標準ライブラリ) で、難易度は ★★★ です。
このアプリは「ユーティリティ」カテゴリです。小回りの効くユーティリティは Python の真骨頂で、汎用的なユーザー操作を組み合わせる発想が他のアプリにもそのまま流用できます。tkinter(標準ライブラリ) を活かして実装するこの構造は、他のアプリにも応用が効きます。
完成形を見てから細部の解説に進む流れが効果的です。実行→気になる箇所を解説で確認→自分の手で改造、というサイクルで定着が早まります。
カスタマイズ章では具体的な拡張アイデアを示しています。一つ実装するごとに動作確認することで、変更が予期せぬ副作用を起こさないかも体感できます。
2. 機能一覧
- 「📂 参照」ボタン(
_browse_repo())・「🔄 読み込み」ボタン(_load_repo())・「絞り込み」ボタン(_load_commits())で操作 - イベント
<<ComboboxSelected>>から_load_commits()を実行 - イベント
<<TreeviewSelect>>から_on_commit_select()を実行 - イベント
<<TreeviewSelect>>から_on_file_select()を実行 ttk.Treeviewによる表形式の一覧表示filedialog.askdirectory()によるファイル選択ダイアログmessagebox.showerror()・messagebox.showwarning()によるダイアログ通知- ウィンドウはタイトル「Gitログビューアー」・サイズ 1060x790 で起動
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 subprocess
import os
import threading
import re
from datetime import datetime
class App38:
"""Gitログビューアー"""
def __init__(self, root):
self.root = root
self.root.title("Gitログビューアー")
self.root.geometry("1060x790")
self.root.minsize(1060, 790)
self.root.configure(bg="#1e1e1e")
self._repo_path = None
self._commits = []
self._build_ui()
def _build_ui(self):
# ツールバー
toolbar = tk.Frame(self.root, bg="#252526", pady=6)
toolbar.pack(fill=tk.X)
tk.Label(toolbar, text="🌿 Gitログビューアー",
font=("Noto Sans JP", 12, "bold"),
bg="#252526", fg="#4fc3f7").pack(side=tk.LEFT, padx=12)
# リポジトリパス
tk.Label(toolbar, text="リポジトリ:", bg="#252526", fg="#ccc",
font=("Arial", 9)).pack(side=tk.LEFT, padx=(12, 4))
self.repo_var = tk.StringVar()
ttk.Entry(toolbar, textvariable=self.repo_var,
width=40, font=("Arial", 10)).pack(side=tk.LEFT, padx=4)
ttk.Button(toolbar, text="📂 参照",
command=self._browse_repo).pack(side=tk.LEFT, padx=4)
ttk.Button(toolbar, text="🔄 読み込み",
command=self._load_repo).pack(side=tk.LEFT, padx=4)
# メインエリア: 垂直分割
main_paned = ttk.PanedWindow(self.root, orient=tk.VERTICAL)
main_paned.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)
# 上: コミットリスト
upper = tk.Frame(main_paned, bg="#1e1e1e")
main_paned.add(upper, weight=2)
self._build_commit_list(upper)
# 下: 詳細パネル
lower = tk.Frame(main_paned, bg="#1e1e1e")
main_paned.add(lower, weight=3)
self._build_detail_panel(lower)
self.status_var = tk.StringVar(value="リポジトリを選択してください")
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 _build_commit_list(self, parent):
# フィルターバー
filter_f = tk.Frame(parent, bg="#252526", pady=4)
filter_f.pack(fill=tk.X)
tk.Label(filter_f, text="ブランチ:", bg="#252526", fg="#ccc",
font=("Arial", 9)).pack(side=tk.LEFT, padx=(8, 2))
self.branch_var = tk.StringVar(value="HEAD")
self.branch_cb = ttk.Combobox(filter_f, textvariable=self.branch_var,
state="readonly", width=16)
self.branch_cb.pack(side=tk.LEFT, padx=4)
self.branch_cb.bind("<<ComboboxSelected>>",
lambda e: self._load_commits())
tk.Label(filter_f, text="作者:", bg="#252526", fg="#ccc",
font=("Arial", 9)).pack(side=tk.LEFT, padx=(8, 2))
self.author_var = tk.StringVar()
ttk.Entry(filter_f, textvariable=self.author_var,
width=14).pack(side=tk.LEFT, padx=2)
tk.Label(filter_f, text="🔍", bg="#252526",
fg="#ccc").pack(side=tk.LEFT, padx=(8, 2))
self.search_var = tk.StringVar()
ttk.Entry(filter_f, textvariable=self.search_var,
width=20).pack(side=tk.LEFT, padx=2)
ttk.Button(filter_f, text="絞り込み",
command=self._load_commits).pack(side=tk.LEFT, padx=6)
tk.Label(filter_f, text="件数:", bg="#252526", fg="#ccc",
font=("Arial", 9)).pack(side=tk.LEFT, padx=(8, 2))
self.limit_var = tk.IntVar(value=100)
ttk.Spinbox(filter_f, from_=10, to=1000,
textvariable=self.limit_var, width=6).pack(side=tk.LEFT)
# コミットテーブル
cols = ("hash", "message", "author", "date", "branch")
self.commit_tree = ttk.Treeview(parent, columns=cols,
show="headings", height=10)
for c, h, w in [("hash", "ハッシュ", 72), ("message", "コミットメッセージ", 400),
("author", "作者", 120), ("date", "日時", 140),
("branch", "ブランチ/タグ", 100)]:
self.commit_tree.heading(c, text=h, command=lambda _c=c: self._sort_commits(_c))
self.commit_tree.column(c, width=w, minwidth=40)
sb = ttk.Scrollbar(parent, command=self.commit_tree.yview)
self.commit_tree.configure(yscrollcommand=sb.set)
sb.pack(side=tk.RIGHT, fill=tk.Y)
self.commit_tree.pack(fill=tk.BOTH, expand=True, padx=4)
self.commit_tree.bind("<<TreeviewSelect>>", self._on_commit_select)
self.commit_tree.tag_configure("merge", foreground="#bb86fc")
self.commit_tree.tag_configure("normal", foreground="#c9d1d9")
def _build_detail_panel(self, parent):
detail_nb = ttk.Notebook(parent)
detail_nb.pack(fill=tk.BOTH, expand=True)
# 差分タブ
diff_tab = tk.Frame(detail_nb, bg="#0d1117")
detail_nb.add(diff_tab, text="差分 (diff)")
self.diff_text = tk.Text(diff_tab, bg="#0d1117", fg="#c9d1d9",
font=("Courier New", 11), relief=tk.FLAT,
state=tk.DISABLED, wrap=tk.NONE)
self.diff_text.tag_configure("add", foreground="#3fb950", background="#0f2b1a")
self.diff_text.tag_configure("del", foreground="#f97583", background="#2b0f0f")
self.diff_text.tag_configure("header", foreground="#569cd6")
self.diff_text.tag_configure("hunk", foreground="#e3b341")
h_sb = ttk.Scrollbar(diff_tab, orient=tk.HORIZONTAL,
command=self.diff_text.xview)
v_sb = ttk.Scrollbar(diff_tab, orient=tk.VERTICAL,
command=self.diff_text.yview)
self.diff_text.configure(xscrollcommand=h_sb.set,
yscrollcommand=v_sb.set)
v_sb.pack(side=tk.RIGHT, fill=tk.Y)
h_sb.pack(side=tk.BOTTOM, fill=tk.X)
self.diff_text.pack(fill=tk.BOTH, expand=True)
# ファイル一覧タブ
files_tab = tk.Frame(detail_nb, bg="#1e1e1e")
detail_nb.add(files_tab, text="変更ファイル")
self.files_tree = ttk.Treeview(files_tab,
columns=("status", "file"),
show="headings")
self.files_tree.heading("status", text="状態")
self.files_tree.heading("file", text="ファイルパス")
self.files_tree.column("status", width=60, minwidth=40)
self.files_tree.column("file", width=600)
f_sb = ttk.Scrollbar(files_tab, command=self.files_tree.yview)
self.files_tree.configure(yscrollcommand=f_sb.set)
f_sb.pack(side=tk.RIGHT, fill=tk.Y)
self.files_tree.pack(fill=tk.BOTH, expand=True, padx=4)
self.files_tree.bind("<<TreeviewSelect>>", self._on_file_select)
self.files_tree.tag_configure("A", foreground="#3fb950")
self.files_tree.tag_configure("M", foreground="#e3b341")
self.files_tree.tag_configure("D", foreground="#f97583")
# コミット情報タブ
info_tab = tk.Frame(detail_nb, bg="#1e1e1e")
detail_nb.add(info_tab, text="コミット詳細")
self.info_text = tk.Text(info_tab, bg="#1e1e1e", fg="#c9d1d9",
font=("Courier New", 11), relief=tk.FLAT,
state=tk.DISABLED)
self.info_text.pack(fill=tk.BOTH, expand=True, padx=4)
# グラフタブ
graph_tab = tk.Frame(detail_nb, bg="#1e1e1e")
detail_nb.add(graph_tab, text="グラフ")
self.graph_text = tk.Text(graph_tab, bg="#1e1e1e", fg="#c9d1d9",
font=("Courier New", 10), relief=tk.FLAT,
state=tk.DISABLED)
self.graph_text.pack(fill=tk.BOTH, expand=True, padx=4)
self._current_commit = None
self._current_files = []
def _browse_repo(self):
path = filedialog.askdirectory(title="Gitリポジトリを選択")
if path:
self.repo_var.set(path)
self._load_repo()
def _load_repo(self):
path = self.repo_var.get().strip()
if not path:
messagebox.showwarning("警告", "リポジトリパスを入力してください")
return
if not os.path.isdir(path):
messagebox.showerror("エラー", "フォルダが存在しません")
return
self._repo_path = path
threading.Thread(target=self._do_load_repo, daemon=True).start()
def _do_load_repo(self):
# ブランチ一覧
branches = self._git(["branch", "-a", "--format=%(refname:short)"])
if branches is None:
self.root.after(0, messagebox.showerror, "エラー",
"Gitリポジトリではないか、gitが見つかりません")
return
branch_list = ["HEAD"] + [b.strip() for b in branches.splitlines() if b.strip()]
self.root.after(0, self.branch_cb.configure, {"values": branch_list})
self.root.after(0, self._load_commits)
self.root.after(0, self._load_graph)
def _load_commits(self):
if not self._repo_path:
return
threading.Thread(target=self._do_load_commits, daemon=True).start()
def _do_load_commits(self):
branch = self.branch_var.get() or "HEAD"
limit = self.limit_var.get()
author = self.author_var.get().strip()
search = self.search_var.get().strip()
cmd = ["log", branch, f"--max-count={limit}",
"--pretty=format:%H|%s|%an|%ad|%D",
"--date=format:%Y/%m/%d %H:%M"]
if author:
cmd.append(f"--author={author}")
if search:
cmd.append(f"--grep={search}")
output = self._git(cmd)
if output is None:
self.root.after(0, self.status_var.set, "コミット取得エラー")
return
commits = []
for line in output.splitlines():
if "|" not in line:
continue
parts = line.split("|", 4)
if len(parts) >= 4:
commits.append({
"hash": parts[0][:8],
"full_hash": parts[0],
"message": parts[1],
"author": parts[2],
"date": parts[3],
"refs": parts[4] if len(parts) > 4 else "",
})
self._commits = commits
self.root.after(0, self._show_commits, commits)
def _show_commits(self, commits):
self.commit_tree.delete(*self.commit_tree.get_children())
for c in commits:
is_merge = c["message"].lower().startswith("merge")
tag = "merge" if is_merge else "normal"
self.commit_tree.insert("", "end",
iid=c["full_hash"],
values=(c["hash"], c["message"],
c["author"], c["date"],
c["refs"][:30]),
tags=(tag,))
self.status_var.set(f"{len(commits)} コミット")
def _sort_commits(self, col):
pass # シンプル実装
def _on_commit_select(self, event):
sel = self.commit_tree.selection()
if not sel:
return
full_hash = sel[0]
self._current_commit = full_hash
threading.Thread(target=self._load_commit_detail,
args=(full_hash,), daemon=True).start()
def _load_commit_detail(self, commit_hash):
# コミット詳細情報
info = self._git(["show", "--stat", "--format=full", commit_hash])
# 差分 (テキストファイルのみ)
diff = self._git(["diff-tree", "--no-commit-id", "-r", commit_hash])
show_diff = self._git(["show", "--format=", commit_hash])
# 変更ファイル一覧
name_status = self._git(["diff-tree", "--no-commit-id", "-r",
"--name-status", commit_hash])
self.root.after(0, self._show_commit_info, info or "")
self.root.after(0, self._show_diff, show_diff or "")
self.root.after(0, self._show_files, name_status or "")
def _show_commit_info(self, text):
self.info_text.config(state=tk.NORMAL)
self.info_text.delete("1.0", tk.END)
self.info_text.insert("1.0", text)
self.info_text.config(state=tk.DISABLED)
def _show_diff(self, text):
self.diff_text.config(state=tk.NORMAL)
self.diff_text.delete("1.0", tk.END)
for line in text.splitlines(keepends=True):
if line.startswith("+") and not line.startswith("+++"):
self.diff_text.insert(tk.END, line, "add")
elif line.startswith("-") and not line.startswith("---"):
self.diff_text.insert(tk.END, line, "del")
elif line.startswith("diff "):
self.diff_text.insert(tk.END, line, "header")
elif line.startswith("@@"):
self.diff_text.insert(tk.END, line, "hunk")
else:
self.diff_text.insert(tk.END, line)
self.diff_text.config(state=tk.DISABLED)
def _show_files(self, text):
self.files_tree.delete(*self.files_tree.get_children())
self._current_files = []
for line in text.splitlines():
parts = line.split("\t", 1)
if len(parts) == 2:
status, filepath = parts
self.files_tree.insert("", "end",
values=(status, filepath),
tags=(status[:1],))
self._current_files.append((status, filepath))
def _on_file_select(self, event):
sel = self.files_tree.selection()
if not sel or not self._current_commit:
return
idx = self.files_tree.index(sel[0])
if idx < len(self._current_files):
_, filepath = self._current_files[idx]
threading.Thread(target=self._load_file_diff,
args=(self._current_commit, filepath),
daemon=True).start()
def _load_file_diff(self, commit_hash, filepath):
diff = self._git(["show", f"{commit_hash}:{filepath}"])
file_diff = self._git(["show", "--format=", commit_hash, "--", filepath])
self.root.after(0, self._show_diff, file_diff or diff or "")
def _load_graph(self):
graph = self._git(["log", "--oneline", "--graph",
"--all", "--decorate",
"--max-count=50"])
if graph:
self.root.after(0, self._show_graph, graph)
def _show_graph(self, text):
self.graph_text.config(state=tk.NORMAL)
self.graph_text.delete("1.0", tk.END)
self.graph_text.insert("1.0", text)
self.graph_text.config(state=tk.DISABLED)
def _git(self, args):
"""gitコマンドを実行して出力を返す"""
try:
result = subprocess.run(
["git"] + args,
cwd=self._repo_path,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=30)
if result.returncode == 0:
return result.stdout
return None
except FileNotFoundError:
return None
except Exception:
return None
if __name__ == "__main__":
root = tk.Tk()
app = App38(root)
root.mainloop()
5. コード解説
Gitログビューアーのコードを、実際に書かれている実装に沿って解説します。
クラス設計とコンストラクタ
App38 クラスにアプリの全機能をまとめています(メソッド21個・全371行)。__init__ ではタイトル「Gitログビューアー」とウィンドウサイズ 1060x790 を設定します。あわせて状態を保持する変数(self._repo_path)を初期化し、最後に _build_ui() で画面を組み立てます。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
ウィジェット構成
画面は tk.Frame×8・tk.Label×7・ttk.Entry×3・ttk.Button×3・ttk.PanedWindow・ttk.Combobox・ttk.Spinbox・ttk.Treeview×2・ttk.Scrollbar×4・ttk.Notebook・tk.Text×3 で構成しています。PanedWindow を使っているので、ペインの境界をマウスでドラッグして幅を変えられます。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
イベント処理とボタンの接続
「📂 参照」ボタン(_browse_repo())・「🔄 読み込み」ボタン(_load_repo())・「絞り込み」ボタン(_load_commits())を command= で接続しています。また bind("<<ComboboxSelected>>", ...) から _load_commits()、bind("<<TreeviewSelect>>", ...) から _on_commit_select()、bind("<<TreeviewSelect>>", ...) から _on_file_select() を呼べるようにイベントも登録しています。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
例外処理
try-except で Exception・FileNotFoundError を捕捉しています。あわせて messagebox.showerror()・messagebox.showwarning() のダイアログで、ユーザーへの通知・確認を行います。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
6. ステップバイステップガイド
このアプリをゼロから自分で作る手順を解説します。コードをコピーするだけでなく、実際に手順を追って自分で書いてみましょう。
-
1ファイルを作成する
新しいファイルを作成して app38.py と保存します。使うのは
datetime・os・re・subprocess・threading・tkinterだけなので、追加インストールは不要です。 -
2クラスの骨格を作る
App38クラスを定義し、__init__と、末尾のroot = tk.Tk()~mainloop()の最小構成を書いて、まず空のウィンドウが出ることを確認します。 -
3ウィンドウを設定する
title("Gitログビューアー")・geometry("1060x790")・configure(bg="#1e1e1e")・minsize(1060, 790)をコンストラクタで設定します。 -
4画面部品を並べる
_build_ui()の中で、tk.Frame×8・tk.Label×7・ttk.Entry×3・ttk.Button×3・ttk.PanedWindow・ttk.Combobox・ttk.Spinbox・ttk.Treeview×2・ttk.Scrollbar×4・ttk.Notebook・tk.Text×3 を作って配置します(掲載コードと同じ並び順で書くとレイアウトが一致します)。まず表示だけ確認しましょう。 -
5イベントを接続する
「2. 機能一覧」で挙げた各ボタンを
command=で対応するメソッド(_browse_repo()・_load_repo()・_load_commits())につなぎます。bind("<<ComboboxSelected>>", ...)・bind("<<TreeviewSelect>>", ...)・bind("<<TreeviewSelect>>", ...)の登録も忘れずに。 -
6中心になるメソッドを実装する
アプリの本体である
_load_commits()(4行)・_show_commits()(12行)・_show_commit_info()(5行)など を実装します。 -
7動作確認する
python app38.pyで起動し、各ボタンが反応することを確認します。
7. カスタマイズアイデア
基本機能を習得したら、以下のカスタマイズに挑戦してみましょう。少しずつ機能を追加することで、Pythonのスキルが飛躍的に向上します。
💡 ダークモードを追加する
bg色・fg色を辞書で管理し、ボタン1つでダークモード・ライトモードを切り替えられるようにしましょう。
💡 結果をファイルに保存する
このコードに保存処理はないため、表示中の数値や結果はアプリを閉じると消えます。テキストファイルへ書き出して、次回起動時に読み込む機能を追加してみましょう。
💡 配色をまとめて管理する
背景色 #1e1e1e をはじめ、色コードが各所に直書きされています。色を1つの辞書にまとめると、テーマの切り替えが1か所の変更で済むようになります。
8. よくある問題と解決法
❌ 文字のフォントが崩れる・見た目が違う
原因:このコードは font=("Noto Sans JP", ...) のようにフォント名を直接指定しています。環境にこのフォントが入っていないと、OS が代わりのフォントで表示するため、見本と見た目が変わることがあります。
解決法:動作には支障ありません。気になる場合は font 引数の名前を自分の OS に入っているフォントへ書き換えるか、font 引数を省略してください。
❌ ウィンドウの大きさが画面に合わない
原因:geometry("1060x790") の固定サイズで起動するためです。
解決法:geometry() と minsize() の両方の数値を書き換えて起動サイズを調整してください。なお、このコードにはウィンドウサイズを固定する設定(resizable の指定)が無いため、ウィンドウの端をドラッグしたサイズ変更は既定どおり可能です。ただし minsize(1060, 790) を指定しているため、起動時の大きさより小さくは縮められません(縮めると画面部品が表示されなくなるのを防ぐためです)。
9. 練習問題
アプリの理解を深めるための練習問題です。気になるものから挑戦してみてください(課題3は定型の発展課題です)。どちらの課題も、git コマンドが使える環境と、読み込み済みの Git リポジトリが1つ要ります。このアプリが実行するのは履歴を読むコマンドだけで、リポジトリを書き換える操作はしません。
-
課題1:ヘッダをクリックしたら並び替わるようにする
コミット表のヘッダは押せますが、呼ばれる
_sort_commits()の中身はpassのままで何も起きません。その列の値で並び替え、同じ列をもう一度押したら逆順になるようにしてください。期待結果:「作者」ヘッダを1回押すと作者名の昇順に並び、もう一度押すと降順に戻る(下の帯の「N コミット」の数は変わらない)。
合格条件
- 並び替えたあとにコミットを選んでも、差分・変更ファイル・コミット詳細がこれまでどおり出る
- マージコミットの紫色は、並び替えたあとも残る
- 「🔄 読み込み」で読み直すと、git が返した元の並び(新しい順)に戻る
ヒント①(どこを触るか): 触るのは
_sort_commits()のpass # シンプル実装の1か所と、並び順を覚える変数を足す__init__の1行です。ヘッダのcommand=lambda _c=c: self._sort_commits(_c)から、押された列の名前が渡ってきます。
ヒント②(使うもの):self._commitsは辞書のリストなので、sorted(...)で並べ替えてself._show_commits(...)に渡すのがいちばん短い書き方です。_show_commits()は先頭でself.commit_tree.delete(*self.commit_tree.get_children())を実行するため、行が二重に増える心配はありません。
つまずきやすい点: 列の名前と辞書のキーはそろっていません。列は("hash", "message", "author", "date", "branch")ですが、辞書側の同じ項目はrefsという名前です。渡された名前をそのままキーにすると、「ブランチ/タグ」列でKeyError: 'branch'になります。日時は2026/09/10 12:34の形で桁がそろっているため、文字列のまま並べても日付の順になります。並び替わるのは「件数」で取ってきたぶんだけで、リポジトリ全体ではありません。 -
課題2:作者と検索の欄で Enter を押したら絞り込む
作者欄と 🔍 欄に文字を入れても、「絞り込み」ボタンを押すまで一覧は変わりません。どちらの欄でも Enter キーで絞り込みが走るようにしてください。
期待結果:作者欄に作者名を入れて Enter を押すと、その作者のコミットだけが残り、下の帯の「N コミット」がその件数に変わる。
合格条件
- 「絞り込み」ボタンは、これまでどおり同じ結果になる
- 欄を空にして Enter を押すと、絞り込みが外れて「件数」の上限まで戻る
- リポジトリを読み込む前に Enter を押しても、何も起きない
ヒント①(どこを触るか): 触るのは
_build_commit_list()の2か所です。作者欄と 🔍 欄のttk.Entry(...)を変数で受けてから、bind("<Return>", ...)を足します。
ヒント②(使うもの): 書き方はブランチのself.branch_cb.bind("<<ComboboxSelected>>", lambda e: self._load_commits())と同じです。bindのコールバックはイベントを1つ受け取るため、lambda e:をはさみます。
つまずきやすい点:ttk.Entry(...).pack(...)と1行で書くとpack()の戻り値のNoneになるので、いったん変数へ受けてからbindします。git の--author=は部分一致(正規表現)として扱われ、大文字と小文字は区別されます。作者欄と 🔍 欄の両方に文字が入っていると、2つの条件を同時に満たすコミットだけが残ります。 -
課題3:保存機能の追加
入力値や計算結果をファイルに保存する機能を追加しましょう。jsonやcsvモジュールを使います。
写経中に赤いエラー文が出たら、Pythonエラー一覧&解決法(英語メッセージ逆引き)で原因と直し方をすぐ確認できます。
このレベルのアプリが作れたら、入門書の次の「作るための本」へ進む時期です。実践におすすめのPython本(当サイトの参考書ランキング総合3〜4位)で自動化とコードの書き方の2冊を、その直後の用途別専門書の節でデータ分析・Web・AIの本を比較しています。