やろうと思った理由
- ファイル単位ほどではないが、その場その場での文字列の差分チェックを必要とする状況が多発している
- 文字列1つ1つが長く、ぱっと見でわかるようにしたい
作ったもの
・環境があれば、コピペしたら実行できます
tool.py
import tkinter as tk
from tkinter import ttk, scrolledtext
import json
import os
class DiffApp:
def __init__(self, root):
self.root = root
self.root.title("テキスト差分表示アプリ")
self.root.geometry("1000x700")
# 設定ファイルから項目名を読み込み
self.item_names = self.load_item_names()
# 3項目分のフレームを作成
self.items = []
for i in range(3):
frame = self.create_item_frame(i)
frame.pack(padx=10, pady=10, fill=tk.BOTH, expand=True)
def load_item_names(self):
"""設定ファイルから項目名を読み込み"""
config_file = "config.json"
# デフォルトの項目名
default_names = ["項目 1", "項目 2", "項目 3"]
# 設定ファイルが存在する場合は読み込み
if os.path.exists(config_file):
try:
with open(config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
return config.get('item_names', default_names)
except:
print("設定ファイルの読み込みに失敗しました。デフォルト値を使用します。")
return default_names
else:
# 設定ファイルが存在しない場合は作成
self.create_default_config(config_file, default_names)
return default_names
def create_default_config(self, config_file, default_names):
"""デフォルトの設定ファイルを作成"""
config = {
"item_names": default_names
}
try:
with open(config_file, 'w', encoding='utf-8') as f:
json.dump(config, f, ensure_ascii=False, indent=2)
print(f"{config_file} を作成しました。")
except:
print("設定ファイルの作成に失敗しました。")
def create_item_frame(self, index):
# メインフレーム
item_name = self.item_names[index] if index < len(self.item_names) else f"項目 {index + 1}"
# ヘッダーフレーム(項目名とコピーボタン)
header_frame = ttk.Frame(self.root)
header_frame.pack(padx=10, pady=(10, 0), fill=tk.X)
# 項目名ラベル
name_label = ttk.Label(header_frame, text=item_name,
font=('', 11, 'bold'))
name_label.pack(side=tk.LEFT, padx=5)
# 項目名コピーボタン
copy_name_btn = ttk.Button(header_frame, text="📋 項目名コピー",
command=lambda idx=index: self.copy_item_name(idx))
copy_name_btn.pack(side=tk.LEFT, padx=5)
# コンテンツフレーム
main_frame = ttk.Frame(self.root, relief=tk.RIDGE, borderwidth=2)
main_frame.pack(padx=10, pady=(0, 10), fill=tk.BOTH, expand=True)
content_frame = ttk.Frame(main_frame, padding=10)
content_frame.pack(fill=tk.BOTH, expand=True)
# 修正前フレーム
before_frame = ttk.Frame(content_frame)
before_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5)
ttk.Label(before_frame, text="修正前:").pack(anchor=tk.W)
before_text = scrolledtext.ScrolledText(before_frame, width=25, height=8)
before_text.pack(fill=tk.BOTH, expand=True)
# 修正後フレーム
after_frame = ttk.Frame(content_frame)
after_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5)
after_label_frame = ttk.Frame(after_frame)
after_label_frame.pack(fill=tk.X)
ttk.Label(after_label_frame, text="修正後:").pack(side=tk.LEFT)
# クリップボードコピーボタン
copy_btn = ttk.Button(after_label_frame, text="📋 コピー",
command=lambda idx=index: self.copy_to_clipboard(idx))
copy_btn.pack(side=tk.RIGHT)
after_text = scrolledtext.ScrolledText(after_frame, width=25, height=8)
after_text.pack(fill=tk.BOTH, expand=True)
# 差分表示フレーム
diff_frame = ttk.Frame(content_frame)
diff_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5)
ttk.Label(diff_frame, text="差分:").pack(anchor=tk.W)
diff_text = scrolledtext.ScrolledText(diff_frame, width=35, height=8,
bg="#f0f0f0", wrap=tk.WORD)
diff_text.pack(fill=tk.BOTH, expand=True)
# テキストウィジェットに変更イベントをバインド
before_text.bind('<KeyRelease>', lambda e, idx=index: self.update_diff(idx))
after_text.bind('<KeyRelease>', lambda e, idx=index: self.update_diff(idx))
# データを保存
self.items.append({
'name': item_name,
'before': before_text,
'after': after_text,
'diff': diff_text
})
return main_frame
def update_diff(self, index):
"""差分を計算して表示"""
item = self.items[index]
before_text = item['before'].get('1.0', tk.END).strip()
after_text = item['after'].get('1.0', tk.END).strip()
# 空の場合は処理しない
if not before_text and not after_text:
diff_widget = item['diff']
diff_widget.config(state=tk.NORMAL)
diff_widget.delete('1.0', tk.END)
diff_widget.config(state=tk.DISABLED)
return
# カンマで区切って要素のリストを作成
before_items = [item.strip() for item in before_text.split(',') if item.strip()]
after_items = [item.strip() for item in after_text.split(',') if item.strip()]
# セットに変換して差分を計算
before_set = set(before_items)
after_set = set(after_items)
# 削除された要素(修正前にあって修正後にない)
deleted = before_set - after_set
# 追加された要素(修正前になくて修正後にある)
added = after_set - before_set
# 差分テキストをクリア
diff_widget = item['diff']
diff_widget.config(state=tk.NORMAL)
diff_widget.delete('1.0', tk.END)
# 結果を表示
if deleted:
diff_widget.insert(tk.END, "【削除された内容】\n", 'header_removed')
diff_widget.insert(tk.END, "(修正前にあって修正後にない)\n", 'subheader')
for item_text in sorted(deleted):
diff_widget.insert(tk.END, f" - {item_text}\n", 'removed')
diff_widget.insert(tk.END, "\n")
if added:
diff_widget.insert(tk.END, "【追加された内容】\n", 'header_added')
diff_widget.insert(tk.END, "(修正前になくて修正後にある)\n", 'subheader')
for item_text in sorted(added):
diff_widget.insert(tk.END, f" + {item_text}\n", 'added')
if not deleted and not added:
diff_widget.insert(tk.END, "変更なし\n", 'nochange')
diff_widget.insert(tk.END, "(修正前と修正後が同じです)", 'subheader')
# タグで色付け
diff_widget.tag_config('header_removed', foreground='#d32f2f', font=('', 10, 'bold'))
diff_widget.tag_config('header_added', foreground='#388e3c', font=('', 10, 'bold'))
diff_widget.tag_config('subheader', foreground='#666', font=('', 8))
diff_widget.tag_config('removed', foreground='#d32f2f', background='#ffebee')
diff_widget.tag_config('added', foreground='#388e3c', background='#e8f5e9')
diff_widget.tag_config('nochange', foreground='#666', font=('', 9, 'bold'))
diff_widget.config(state=tk.DISABLED)
def copy_item_name(self, index):
"""項目名をクリップボードにコピー"""
item = self.items[index]
item_name = item['name']
# tkinterのクリップボードを使用
self.root.clipboard_clear()
self.root.clipboard_append(item_name)
self.root.update()
# フィードバック表示
self.show_feedback(f"「{item_name}」をコピーしました!")
def copy_to_clipboard(self, index):
"""修正後のテキストをクリップボードにコピー"""
item = self.items[index]
text = item['after'].get('1.0', tk.END).strip()
# tkinterのクリップボードを使用
self.root.clipboard_clear()
self.root.clipboard_append(text)
self.root.update()
# フィードバック表示
self.show_feedback(f"{item['name']} の修正後テキストをコピーしました!")
def show_feedback(self, message):
"""フィードバックメッセージを表示"""
feedback = tk.Toplevel(self.root)
feedback.title("通知")
feedback.geometry("350x80")
feedback.resizable(False, False)
# ウィンドウを中央に配置
feedback.transient(self.root)
x = self.root.winfo_x() + (self.root.winfo_width() // 2) - 175
y = self.root.winfo_y() + (self.root.winfo_height() // 2) - 40
feedback.geometry(f"+{x}+{y}")
ttk.Label(feedback, text=message, padding=20).pack()
# 1秒後に自動で閉じる
feedback.after(1000, feedback.destroy)
if __name__ == "__main__":
root = tk.Tk()
app = DiffApp(root)
root.mainloop()
作った感想
- 差分が明示的にわかりやすくなった
- いろいろとほしい機能突っ込んだので、使い勝手は良い
- 秘伝のたれ手法でいろいろ足したので、いろんな残骸とか残ったままだし、リファクタリングしなきゃなあと全体を見て思ったが、その作業はめんどくさいのでAI丸投げするかも
- 修正後の部分、どうせ別ツールで編集するなら、いっそスプシでもよかったのでは?