1 提出需求
代码对比工具通常是需要付费的,基本的文本对比只需要比较两个文件,标出相同行和不同行,不同行可以看细节区别就可以了
因此让智能体来实现一个此类应用。本文使用研发云的智能体,记录了软件生成过程。
2 告诉智能体要做什么,并且反复修正
1输入: 告诉智能体要实现一个windows版本的文本对比工具,有左右两个显示框,可以复制文本输入,然后对左右两边同行的对比,内容不同的文字进行标红处理
输出:智能体推荐了python实现,可以简单的实现界面应用,然后给出了示例代码。
2 这时对应用进行测试,输入两个文本,发现可以对不同文字进行标红。但它主动生成了一个对比按钮,需要点击才能对比。
于是输入新的需求:去掉按钮动态改变文本就自动对比
3 再次验证功能,有小bug就提示进行修复,没有显示行号,于是提示它增加行号。
4 对比功能完成后,提出需求新增一个底部空间,点击某行时,可以在底部显示细节。
5 再次提出需求增加一个对齐按钮,可以对选中的左右文本进行对齐。
经过几个回合的调整,终于达成需求。
6 再提出一个左右拷贝的需求。
7 提出一个智能对比的需求,可以减少手动对齐次数。
8 提出界面需求
3 心得
智能体经常出现反复的情况,下一次修改把之前改过的问题又恢复出来,有时陷入死循环,不得不手动恢复代码
但大致是减少很大的开发时间,只需个把小时的调试,就能完成人工几天才能完成的代码
4 附代码
import tkinter as tk
from tkinter import ttk
import difflib
import math
from threading import Timer
class TextComparator:
def __init__(self, root):
self.root = root
self.root.title("智能文本对比工具 - 字符级差异高亮(红色文字)")
self.root.geometry("900x750")
# 防抖计时器配置
self.debounce_timer = None
self.debounce_delay = 0.3 # 300毫秒延迟
# 选中行号跟踪
self.left_selected_line = None
self.right_selected_line = None
# 创建主框架
self.main_frame = ttk.Frame(root, padding="10")
self.main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
# 配置网格权重(增加中间列)
root.columnconfigure(0, weight=1)
root.rowconfigure(0, weight=1)
self.main_frame.columnconfigure(0, weight=0) # 左侧行号
self.main_frame.columnconfigure(1, weight=0) # 左侧复制按钮列
self.main_frame.columnconfigure(2, weight=1) # 左侧文本
self.main_frame.columnconfigure(3, weight=0) # 中间连接区域(固定宽度)
self.main_frame.columnconfigure(4, weight=0) # 右侧复制按钮列
self.main_frame.columnconfigure(5, weight=0) # 右侧行号
self.main_frame.columnconfigure(6, weight=1) # 右侧文本
# 存储按钮的 Canvas 引用
self.left_button_canvas = None
self.right_button_canvas = None
self.main_frame.rowconfigure(1, weight=1)
# --- 顶部标题 ---
ttk.Label(self.main_frame, text="左侧文本", font=("Arial", 12, "bold")).grid(
row=0, column=2, padx=5, pady=5, sticky=tk.W)
ttk.Label(self.main_frame, text="右侧文本", font=("Arial", 12, "bold")).grid(
row=0, column=6, padx=5, pady=5, sticky=tk.W)
# 存储按钮引用
self.left_copy_buttons = {} # 行号 -> 按钮 ID
self.right_copy_buttons = {} # 行号 -> 按钮 ID
# ==========================================
# 左侧文本区域 (包含行号 + 文本 + 双向滚动条)
# ==========================================
left_frame = ttk.Frame(self.main_frame)
left_frame.grid(row=1, column=2, padx=5, pady=5, sticky=(tk.W, tk.E, tk.N, tk.S))
# 创建左侧按钮 Canvas(用于精确对齐)
self.left_button_canvas = tk.Canvas(left_frame, width=20, height=0,
bg='#f8f9fa', highlightthickness=0)
self.left_button_canvas.pack(side=tk.LEFT, fill=tk.Y, padx=(5, 0))
# 左侧行号显示
self.left_line_numbers = tk.Text(left_frame, width=4, height=15, wrap=tk.NONE,
font=("Consolas", 10), bg="#f0f0f0", state='disabled')
self.left_line_numbers.pack(side=tk.LEFT, fill=tk.Y)
# 左侧文本容器
left_text_container = ttk.Frame(left_frame)
left_text_container.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# 左侧纵向滚动条
left_v_scrollbar = ttk.Scrollbar(left_text_container, orient=tk.VERTICAL)
left_v_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# 左侧横向滚动条
left_h_scrollbar = ttk.Scrollbar(left_text_container, orient=tk.HORIZONTAL)
left_h_scrollbar.pack(side=tk.BOTTOM, fill=tk.X)
# 左侧文本框
self.left_text = tk.Text(
left_text_container,
width=40,
height=15,
wrap=tk.NONE,
font=("Consolas", 10),
bg="white",
yscrollcommand=left_v_scrollbar.set,
xscrollcommand=left_h_scrollbar.set
)
self.left_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# 配置滚动条命令
left_v_scrollbar.config(command=self.left_text.yview)
left_h_scrollbar.config(command=self.left_text.xview)
# ==========================================
# 中间连接区域
# ==========================================
middle_frame = ttk.Frame(self.main_frame, width=50)
middle_frame.grid(row=1, column=3, padx=5, pady=5, sticky=(tk.W, tk.E, tk.N, tk.S))
middle_frame.columnconfigure(0, weight=1)
# 创建Canvas用于绘制连接线
self.connection_canvas = tk.Canvas(middle_frame, width=50, bg="#f8f9fa", highlightthickness=0)
self.connection_canvas.pack(fill=tk.BOTH, expand=True)
# ==========================================
# 右侧文本区域 (包含行号 + 文本 + 双向滚动条)
# ==========================================
right_frame = ttk.Frame(self.main_frame)
right_frame.grid(row=1, column=5, columnspan=2, padx=5, pady=5, sticky=(tk.W, tk.E, tk.N, tk.S))
# 创建右侧按钮 Canvas(用于精确对齐)
self.right_button_canvas = tk.Canvas(right_frame, width=20, height=0,
bg='#f8f9fa', highlightthickness=0)
self.right_button_canvas.pack(side=tk.LEFT, fill=tk.Y, padx=(5, 0))
# 右侧行号显示
self.right_line_numbers = tk.Text(right_frame, width=4, height=15, wrap=tk.NONE,
font=("Consolas", 10), bg="#f0f0f0", state='disabled')
self.right_line_numbers.pack(side=tk.LEFT, fill=tk.Y)
# 右侧文本容器
right_text_container = ttk.Frame(right_frame)
right_text_container.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# 右侧纵向滚动条
right_v_scrollbar = ttk.Scrollbar(right_text_container, orient=tk.VERTICAL)
right_v_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# 右侧横向滚动条
right_h_scrollbar = ttk.Scrollbar(right_text_container, orient=tk.HORIZONTAL)
right_h_scrollbar.pack(side=tk.BOTTOM, fill=tk.X)
# 右侧文本框
self.right_text = tk.Text(
right_text_container,
width=40,
height=15,
wrap=tk.NONE,
font=("Consolas", 10),
bg="white",
yscrollcommand=right_v_scrollbar.set,
xscrollcommand=right_h_scrollbar.set
)
self.right_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# 配置滚动条命令
right_v_scrollbar.config(command=self.right_text.yview)
right_h_scrollbar.config(command=self.right_text.xview)
# --- 按钮区域 ---
button_frame = ttk.Frame(self.main_frame)
button_frame.grid(row=2, column=0, columnspan=7, pady=10)
# 对齐按钮
align_btn = ttk.Button(
button_frame, text="对齐选中行", command=self.align_texts)
align_btn.pack(side=tk.LEFT, padx=5)
# 智能对齐按钮
smart_align_btn = ttk.Button(
button_frame, text="智能对齐", command=self.smart_align_texts)
smart_align_btn.pack(side=tk.LEFT, padx=5)
# --- 底部详情对比框 ---
detail_frame = ttk.LabelFrame(self.main_frame, text="选中行差异详情(上:左侧内容 | 下:右侧内容)", padding="5")
detail_frame.grid(row=3, column=0, columnspan=7, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(10, 5))
self.main_frame.rowconfigure(3, weight=0)
detail_frame.columnconfigure(0, weight=1)
detail_frame.rowconfigure(0, weight=1)
self.detail_text = tk.Text(
detail_frame, width=80, height=2, wrap=tk.NONE
)
self.detail_text.grid(row=0, column=0, padx=5, pady=5, sticky=(tk.W, tk.E, tk.N, tk.S))
# 详情框滚动条
detail_h_scrollbar = ttk.Scrollbar(detail_frame, orient=tk.HORIZONTAL, command=self.detail_text.xview)
detail_h_scrollbar.grid(row=1, column=0, sticky=(tk.W, tk.E))
self.detail_text.configure(xscrollcommand=detail_h_scrollbar.set)
detail_v_scrollbar = ttk.Scrollbar(detail_frame, orient=tk.VERTICAL, command=self.detail_text.yview)
detail_v_scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S))
self.detail_text.configure(yscrollcommand=detail_v_scrollbar.set)
# 配置高亮样式
# 底部详情框样式(保持不变)
self.detail_text.tag_configure("diff_left", foreground="red", background="#ffe6e6")
self.detail_text.tag_configure("diff_right", foreground="blue", background="#e6f3ff")
self.detail_text.tag_configure("line_label", foreground="green", background="#f0f0f0", font=("Arial", 9, "bold"))
# 主文本框的高亮样式(关键修改:文字颜色改为红色,背景色设为透明)
self.left_text.tag_configure("diff", foreground="red") # 只设置文字颜色为红色
self.right_text.tag_configure("diff", foreground="red") # 只设置文字颜色为红色
# 选中行的背景高亮样式(独立高亮)
self.left_text.tag_configure("left_selected_line", background="#fff5cc") # 左侧选中行高亮
self.right_text.tag_configure("right_selected_line", background="#e6f3ff") # 右侧选中行高亮
# 对齐空行的阴影底色样式
self.left_text.tag_configure("left_align_gap", background="#f0f0f0", foreground="#999999") # 左侧对齐空行阴影
self.right_text.tag_configure("right_align_gap", background="#f0f0f0", foreground="#999999") # 右侧对齐空行阴影
# 绑定事件
self.setup_text_change_listeners()
self.setup_click_listeners()
# 绑定复制按钮点击事件(需要传入行号)
# 注意:按钮状态会在文本变化和行选中时更新
# 绑定窗口大小变化事件
self.root.bind('<Configure>', self.on_window_resize)
# 初始化行号
self.update_line_numbers()
# 初始化行复制按钮
self.update_row_copy_buttons()
def update_row_copy_buttons(self):
"""更新每行的复制按钮显示状态(使用 Canvas 精确对齐)"""
if not self.left_button_canvas or not self.right_button_canvas:
return
# 获取左右文本内容
left_lines = self.left_text.get(1.0, tk.END).splitlines()
right_lines = self.right_text.get(1.0, tk.END).splitlines()
max_lines = max(len(left_lines), len(right_lines))
# 获取行高
try:
bbox = self.left_text.dlineinfo("1.0")
if bbox:
line_height = bbox[3]
else:
line_height = 20 # 默认行高
except:
line_height = 20
# 更新 Canvas 高度以匹配文本高度
canvas_height = max_lines * line_height + 10
self.left_button_canvas.config(height=canvas_height)
self.right_button_canvas.config(height=canvas_height)
# 清空 Canvas
self.left_button_canvas.delete("all")
self.right_button_canvas.delete("all")
# 为每一行创建按钮
for i in range(max_lines):
line_num = i + 1
left_content = left_lines[i] if i < len(left_lines) else ""
right_content = right_lines[i] if i < len(right_lines) else ""
# 计算按钮 Y 位置
y_pos = (line_num - 0.5) * line_height
# 如果内容不同,创建左侧按钮
if left_content != right_content:
# 左侧按钮(向右箭头)- 使用 create_window 创建按钮
btn_left = ttk.Button(
self.left_button_canvas,
text="→",
width=2,
command=lambda ln=line_num: self.copy_line(ln, 'left_to_right')
)
self.left_button_canvas.create_window(10, y_pos, window=btn_left, anchor=tk.CENTER)
self.left_copy_buttons[line_num] = btn_left
# 右侧按钮(向左箭头)
btn_right = ttk.Button(
self.right_button_canvas,
text="←",
width=2,
command=lambda ln=line_num: self.copy_line(ln, 'right_to_left')
)
self.right_button_canvas.create_window(10, y_pos, window=btn_right, anchor=tk.CENTER)
self.right_copy_buttons[line_num] = btn_right
def copy_line(self, line_num, direction):
"""复制指定行的内容"""
left_lines = self.left_text.get(1.0, tk.END).splitlines()
right_lines = self.right_text.get(1.0, tk.END).splitlines()
if direction == 'left_to_right':
if line_num <= len(left_lines):
line_content = left_lines[line_num - 1]
if line_num <= len(right_lines):
insert_pos = f"{line_num}.0"
self.right_text.delete(insert_pos, f"{line_num}.end")
self.right_text.insert(insert_pos, line_content)
else:
self.right_text.insert(tk.END, "\n" + line_content)
elif direction == 'right_to_left':
if line_num <= len(right_lines):
line_content = right_lines[line_num - 1]
if line_num <= len(left_lines):
insert_pos = f"{line_num}.0"
self.left_text.delete(insert_pos, f"{line_num}.end")
self.left_text.insert(insert_pos, line_content)
else:
self.left_text.insert(tk.END, "\n" + line_content)
# 更新行号显示
self.update_line_numbers()
# 重新对比文本
self.compare_texts()
# 重新创建行按钮
self.update_row_copy_buttons()
def setup_text_change_listeners(self):
"""设置文本变更监听器(用于实时对比)"""
events = ['<KeyRelease>', '<ButtonRelease-1>']
for event in events:
self.left_text.bind(event, self.on_text_changed)
self.right_text.bind(event, self.on_text_changed)
def setup_click_listeners(self):
"""设置点击监听器(用于选中行详情对比)"""
self.left_text.bind('<Button-1>', self.on_row_selected)
self.right_text.bind('<Button-1>', self.on_row_selected)
def on_text_changed(self, event=None):
"""文本变更事件处理(带防抖)"""
if self.debounce_timer:
self.debounce_timer.cancel()
self.debounce_timer = Timer(self.debounce_delay, self.debounced_compare)
self.debounce_timer.start()
# 更新行号显示
self.update_line_numbers()
# 更新行复制按钮
self.update_row_copy_buttons()
def debounced_compare(self):
"""防抖后的对比执行"""
self.root.after(0, self.compare_texts)
def update_line_numbers(self):
"""更新左右文本框的行号显示"""
# 更新左侧行号
left_content = self.left_text.get(1.0, tk.END)
left_lines = left_content.split('\n')
left_line_count = len(left_lines) if left_content else 1
self.left_line_numbers.config(state='normal')
self.left_line_numbers.delete(1.0, tk.END)
for i in range(1, left_line_count):
self.left_line_numbers.insert(tk.END, f"{i}\n")
self.left_line_numbers.config(state='disabled')
# 更新右侧行号
right_content = self.right_text.get(1.0, tk.END)
right_lines = right_content.split('\n')
right_line_count = len(right_lines) if right_content else 1
self.right_line_numbers.config(state='normal')
self.right_line_numbers.delete(1.0, tk.END)
for i in range(1, right_line_count):
self.right_line_numbers.insert(tk.END, f"{i}\n")
self.right_line_numbers.config(state='disabled')
def on_row_selected(self, event):
"""处理行选中事件,更新底部详情框并独立高亮选中行"""
# 获取点击位置的行号
click_pos = event.widget.index("@%d,%d" % (event.x, event.y))
line_num = int(click_pos.split('.')[0])
# 判断是哪个文本框被点击
if event.widget == self.left_text:
# 清除左侧之前的选中行高亮
self.left_text.tag_remove("left_selected_line", "1.0", tk.END)
# 高亮左侧文本框的选中行
if line_num >= 1:
self.left_text.tag_add("left_selected_line", f"{line_num}.0", f"{line_num}.end")
self.left_selected_line = line_num
elif event.widget == self.right_text:
# 清除右侧之前的选中行高亮
self.right_text.tag_remove("right_selected_line", "1.0", tk.END)
# 高亮右侧文本框的选中行
if line_num >= 1:
self.right_text.tag_add("right_selected_line", f"{line_num}.0", f"{line_num}.end")
self.right_selected_line = line_num
# 绘制连接线(如果两侧都有选中行)
self.draw_connection_line()
# 获取左右文本内容
left_lines = self.left_text.get(1.0, tk.END).splitlines()
right_lines = self.right_text.get(1.0, tk.END).splitlines()
# 根据点击的文本框确定要显示的行
if event.widget == self.left_text:
# 显示左侧选中行的对比
line_left = left_lines[line_num - 1] if line_num <= len(left_lines) else ""
line_right = right_lines[line_num - 1] if line_num <= len(right_lines) else ""
self.update_detail_view(line_num, line_left, line_right)
elif event.widget == self.right_text:
# 显示右侧选中行的对比
line_left = left_lines[line_num - 1] if line_num <= len(left_lines) else ""
line_right = right_lines[line_num - 1] if line_num <= len(right_lines) else ""
self.update_detail_view(line_num, line_left, line_right)
def update_detail_view(self, line_num, left_content, right_content):
"""更新底部详情框的显示内容并高亮差异"""
self.detail_text.config(state='normal')
self.detail_text.delete(1.0, tk.END)
# 清除旧标签
for tag in self.detail_text.tag_names():
self.detail_text.tag_delete(tag)
# 重新配置标签
self.detail_text.tag_configure("diff_left", foreground="red", background="#ffe6e6")
self.detail_text.tag_configure("diff_right", foreground="blue", background="#e6f3ff")
self.detail_text.tag_configure("line_label", foreground="green", background="#f0f0f0", font=("Arial", 9, "bold"))
# 插入左侧内容
self.detail_text.insert(tk.END, f"第{line_num}行 - 左侧: ", "line_label")
left_start = self.detail_text.index(tk.INSERT)
self.detail_text.insert(tk.END, left_content + "\n")
left_end = self.detail_text.index(tk.INSERT)
# 插入右侧内容
self.detail_text.insert(tk.END, f"第{line_num}行 - 右侧: ", "line_label")
right_start = self.detail_text.index(tk.INSERT)
self.detail_text.insert(tk.END, right_content)
right_end = self.detail_text.index(tk.INSERT)
# 如果内容不同,计算并高亮差异
if left_content != right_content:
matcher = difflib.SequenceMatcher(None, left_content, right_content)
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
if tag != 'equal':
# 高亮左侧差异
if i1 < i2:
left_diff_start = f"{left_start}+{i1}c"
left_diff_end = f"{left_start}+{i2}c"
self.detail_text.tag_add("diff_left", left_diff_start, left_diff_end)
# 高亮右侧差异
if j1 < j2:
right_diff_start = f"{right_start}+{j1}c"
right_diff_end = f"{right_start}+{j2}c"
self.detail_text.tag_add("diff_right", right_diff_start, right_diff_end)
self.detail_text.config(state='disabled')
def compare_texts(self):
"""执行文本对比并高亮差异(字符级高亮,红色文字)"""
# 1. 清除旧的高亮标签
self.left_text.tag_remove("diff", "1.0", tk.END)
self.right_text.tag_remove("diff", "1.0", tk.END)
# 2. 获取所有行
left_lines = self.left_text.get(1.0, tk.END).splitlines()
right_lines = self.right_text.get(1.0, tk.END).splitlines()
# 3. 逐行对比
max_lines = max(len(left_lines), len(right_lines))
for i in range(max_lines):
line_num = i + 1
left_line = left_lines[i] if i < len(left_lines) else ""
right_line = right_lines[i] if i < len(right_lines) else ""
# 如果两行完全相同,跳过
if left_line == right_line:
continue
# 使用 difflib 进行字符级对比
matcher = difflib.SequenceMatcher(None, left_line, right_line)
# 遍历操作码
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
# tag 可以是 'equal', 'replace', 'delete', 'insert'
# 我们只关心非 'equal' 的部分
# --- 处理左侧文本的高亮 ---
if tag in ('replace', 'delete'):
# 左侧有差异字符 (i1 到 i2)
# 计算 Tkinter 索引: 行号.列号
start_idx = f"{line_num}.{i1}"
end_idx = f"{line_num}.{i2}"
self.left_text.tag_add("diff", start_idx, end_idx)
# --- 处理右侧文本的高亮 ---
if tag in ('replace', 'insert'):
# 右侧有差异字符 (j1 到 j2)
start_idx = f"{line_num}.{j1}"
end_idx = f"{line_num}.{j2}"
self.right_text.tag_add("diff", start_idx, end_idx)
def on_window_resize(self, event=None):
"""窗口大小变化时重新绘制连接线"""
if hasattr(self, 'connection_canvas') and self.left_selected_line is not None and self.right_selected_line is not None:
self.root.after(100, self.draw_connection_line) # 延迟重绘,确保尺寸已更新
def draw_connection_line(self):
"""绘制左右选中行的连接线"""
# 清除之前的连接线
self.connection_canvas.delete("all")
# 检查是否两侧都有选中行
if self.left_selected_line is None or self.right_selected_line is None:
return
# 获取Canvas尺寸
canvas_width = self.connection_canvas.winfo_width()
canvas_height = self.connection_canvas.winfo_height()
# 如果Canvas还没有渲染完成,延迟绘制
if canvas_width <= 1 or canvas_height <= 1:
self.root.after(100, self.draw_connection_line)
return
# 获取文本框的实际行高(更精确的计算)
# 使用文本框的dlineinfo方法获取行的位置信息
try:
# 尝试获取第一行的位置信息来计算行高
bbox_left = self.left_text.dlineinfo("1.0")
if bbox_left:
line_height = bbox_left[3] # 行高
# 计算左侧选中行的Y坐标(相对于Canvas中心)
left_y = (self.left_selected_line - 0.5) * line_height
else:
# 如果无法获取行信息,使用近似计算
left_lines = self.left_text.get(1.0, tk.END).splitlines()
line_height = canvas_height / max(len(left_lines), 1)
left_y = (self.left_selected_line - 0.5) * line_height
except:
# 异常处理,使用近似计算
left_lines = self.left_text.get(1.0, tk.END).splitlines()
line_height = canvas_height / max(len(left_lines), 1)
left_y = (self.left_selected_line - 0.5) * line_height
try:
# 尝试获取右侧文本框的行信息
bbox_right = self.right_text.dlineinfo("1.0")
if bbox_right:
line_height_right = bbox_right[3]
# 计算右侧选中行的Y坐标
right_y = (self.right_selected_line - 0.5) * line_height_right
else:
right_lines = self.right_text.get(1.0, tk.END).splitlines()
line_height_right = canvas_height / max(len(right_lines), 1)
right_y = (self.right_selected_line - 0.5) * line_height_right
except:
right_lines = self.right_text.get(1.0, tk.END).splitlines()
line_height_right = canvas_height / max(len(right_lines), 1)
right_y = (self.right_selected_line - 0.5) * line_height_right
# 绘制连接线
# 左侧起点 (x=0, y=left_y)
# 右侧终点 (x=canvas_width, y=right_y)
# 绘制简单的直线连接(更稳定)
# 左侧起点 (x=5, y=left_y)
# 右侧终点 (x=canvas_width-5, y=right_y)
# 绘制连接线
self.connection_canvas.create_line(
5, left_y,
canvas_width-5, right_y,
fill="#007acc",
width=2,
arrow=tk.BOTH,
arrowshape=(8, 10, 3)
)
# 在连接线中点添加说明文字
mid_x = canvas_width / 2
mid_y = (left_y + right_y) / 2
# 计算文字角度(根据连线斜率)
angle = 0
if abs(right_y - left_y) > 10: # 只有当有明显斜率时才旋转
angle = math.degrees(math.atan2(right_y - left_y, canvas_width - 10))
self.connection_canvas.create_text(
mid_x, mid_y,
text=f"L{self.left_selected_line} ↔ R{self.right_selected_line}",
fill="#333333",
font=("Arial", 8),
angle=angle
)
def align_texts(self):
"""对齐文本功能:根据选中的行号,在行号小的一边插入空行对齐"""
# 检查是否有选中的行
if self.left_selected_line is None or self.right_selected_line is None:
return
left_line = self.left_selected_line
right_line = self.right_selected_line
# 比较行号大小
if left_line < right_line:
# 左侧行号较小,需要在左侧插入空行
lines_to_insert = right_line - left_line
for i in range(lines_to_insert):
# 在选中行之前插入空行
insert_pos = f"{left_line + i}.0"
self.left_text.insert(insert_pos, "\n")
# 应用阴影底色样式
self.left_text.tag_add("left_align_gap", insert_pos, f"{insert_pos}+1c")
# 更新左侧选中行号(因为插入了空行,选中行号需要增加)
self.left_selected_line = right_line
elif left_line > right_line:
# 右侧行号较小,需要在右侧插入空行
lines_to_insert = left_line - right_line
for i in range(lines_to_insert):
# 在选中行之前插入空行
insert_pos = f"{right_line + i}.0"
self.right_text.insert(insert_pos, "\n")
# 应用阴影底色样式
self.right_text.tag_add("right_align_gap", insert_pos, f"{insert_pos}+1c")
# 更新右侧选中行号(因为插入了空行,选中行号需要增加)
self.right_selected_line = left_line
else:
# 行号相同,无需对齐
return
# 更新行号显示
self.update_line_numbers()
# 重新对比文本
self.compare_texts()
# 重新绘制连接线(此时选中行号已更新为对齐后的行号)
self.draw_connection_line()
def smart_align_texts(self):
"""智能对齐:遍历左右文本,对全量相等或相似度 90% 以上的行进行对齐(空行不参与)"""
# 获取左右文本内容
left_lines = self.left_text.get(1.0, tk.END).splitlines()
right_lines = self.right_text.get(1.0, tk.END).splitlines()
# 记录已处理的行,避免重复处理
left_processed = set()
right_processed = set()
# 存储需要对齐的行对 (left_line_num, right_line_num)
align_pairs = []
# 遍历左侧每一行(跳过空行)
for i, left_line in enumerate(left_lines):
left_line_num = i + 1
# 跳过空行
if not left_line.strip():
continue
if left_line_num in left_processed:
continue
best_match = None
best_similarity = 0
best_right_line_num = None
# 在右侧寻找最佳匹配(跳过空行)
for j, right_line in enumerate(right_lines):
right_line_num = j + 1
# 跳过空行
if not right_line.strip():
continue
if right_line_num in right_processed:
continue
# 计算相似度
similarity = difflib.SequenceMatcher(None, left_line, right_line).ratio()
# 全量相等或相似度>=90% 且是最佳匹配
if (similarity >= 0.9 and similarity > best_similarity):
best_similarity = similarity
best_match = right_line
best_right_line_num = right_line_num
# 如果找到符合条件的匹配
if best_match is not None and best_similarity >= 0.9:
align_pairs.append((left_line_num, best_right_line_num))
left_processed.add(left_line_num)
right_processed.add(best_right_line_num)
# 如果没有找到需要对齐的行对,退出
if not align_pairs:
return
# 按左侧行号排序
align_pairs.sort(key=lambda x: x[0])
# 执行对齐操作(从前往后处理,但需要跟踪已插入的空行数量)
left_inserted_count = 0 # 左侧已插入的空行总数
right_inserted_count = 0 # 右侧已插入的空行总数
for left_line_num, right_line_num in align_pairs:
# 计算当前实际行号(考虑之前插入的空行)
current_left_line = left_line_num + left_inserted_count
current_right_line = right_line_num + right_inserted_count
# 比较当前行号,在较小的一边插入空行
if current_left_line < current_right_line:
lines_to_insert = current_right_line - current_left_line
for i in range(lines_to_insert):
insert_pos = f"{current_left_line + i}.0"
self.left_text.insert(insert_pos, "\n")
# 应用阴影底色样式
self.left_text.tag_add("left_align_gap", insert_pos, f"{insert_pos}+1c")
left_inserted_count += lines_to_insert
elif current_left_line > current_right_line:
lines_to_insert = current_left_line - current_right_line
for i in range(lines_to_insert):
insert_pos = f"{current_right_line + i}.0"
self.right_text.insert(insert_pos, "\n")
# 应用阴影底色样式
self.right_text.tag_add("right_align_gap", insert_pos, f"{insert_pos}+1c")
right_inserted_count += lines_to_insert
# 删除两侧都是空行的行(从后往前删除,避免行号变化)
self.remove_double_empty_lines()
# 更新行号显示
self.update_line_numbers()
# 重新对比文本
self.compare_texts()
# 设置选中行为第一对对齐的行
if align_pairs:
# 重新获取行号(因为删除空行后行号可能变化)
left_lines = self.left_text.get(1.0, tk.END).splitlines()
right_lines = self.right_text.get(1.0, tk.END).splitlines()
# 找到第一对非空行的对齐位置
for i in range(min(len(left_lines), len(right_lines))):
if left_lines[i].strip() or right_lines[i].strip():
self.left_selected_line = i + 1
self.right_selected_line = i + 1
break
else:
# 如果都是空行,设置为第一行
self.left_selected_line = 1
self.right_selected_line = 1
# 重新绘制连接线
self.draw_connection_line()
# 更新详情框
line_left = left_lines[self.left_selected_line - 1] if self.left_selected_line <= len(left_lines) else ""
line_right = right_lines[self.right_selected_line - 1] if self.right_selected_line <= len(right_lines) else ""
self.update_detail_view(self.left_selected_line, line_left, line_right)
def remove_double_empty_lines(self):
"""删除两侧相同位置都是空行的行(从后往前处理)"""
# 获取当前文本内容
left_lines = self.left_text.get(1.0, tk.END).splitlines()
right_lines = self.right_text.get(1.0, tk.END).splitlines()
# 找出两侧都是空行的位置(从后往前)
max_lines = max(len(left_lines), len(right_lines))
lines_to_delete = []
for i in range(max_lines):
left_line = left_lines[i] if i < len(left_lines) else ""
right_line = right_lines[i] if i < len(right_lines) else ""
# 如果两侧都是空行(或都不存在)
if not left_line.strip() and not right_line.strip():
lines_to_delete.append(i + 1) # 行号从 1 开始
# 从后往前删除(避免行号变化)
for line_num in reversed(lines_to_delete):
# 删除左侧的空行
if line_num <= len(self.left_text.get(1.0, tk.END).splitlines()):
start_pos = f"{line_num}.0"
end_pos = f"{line_num}.1"
try:
self.left_text.delete(start_pos, end_pos)
# 清除该行的阴影样式
self.left_text.tag_remove("left_align_gap", start_pos, end_pos)
except:
pass
# 删除右侧的空行
if line_num <= len(self.right_text.get(1.0, tk.END).splitlines()):
start_pos = f"{line_num}.0"
end_pos = f"{line_num}.1"
try:
self.right_text.delete(start_pos, end_pos)
# 清除该行的阴影样式
self.right_text.tag_remove("right_align_gap", start_pos, end_pos)
except:
pass
def copy_left_to_right(self):
"""将左侧选中行内容复制到右侧"""
if self.left_selected_line is None:
return
# 获取左侧选中行内容
left_lines = self.left_text.get(1.0, tk.END).splitlines()
if self.left_selected_line <= len(left_lines):
line_content = left_lines[self.left_selected_line - 1]
# 如果右侧对应行存在,替换它;否则插入新行
right_lines = self.right_text.get(1.0, tk.END).splitlines()
if self.left_selected_line <= len(right_lines):
# 替换右侧对应行
insert_pos = f"{self.left_selected_line}.0"
self.right_text.delete(insert_pos, f"{self.left_selected_line}.end")
self.right_text.insert(insert_pos, line_content)
else:
# 在右侧末尾插入新行
self.right_text.insert(tk.END, "\n" + line_content)
# 更新行号显示
self.update_line_numbers()
# 重新对比文本
self.compare_texts()
# 更新详情框
self.update_detail_view(self.left_selected_line, line_content, line_content)
def copy_right_to_left(self):
"""将右侧选中行内容复制到左侧"""
if self.right_selected_line is None:
return
# 获取右侧选中行内容
right_lines = self.right_text.get(1.0, tk.END).splitlines()
if self.right_selected_line <= len(right_lines):
line_content = right_lines[self.right_selected_line - 1]
# 如果左侧对应行存在,替换它;否则插入新行
left_lines = self.left_text.get(1.0, tk.END).splitlines()
if self.right_selected_line <= len(left_lines):
# 替换左侧对应行
insert_pos = f"{self.right_selected_line}.0"
self.left_text.delete(insert_pos, f"{self.right_selected_line}.end")
self.left_text.insert(insert_pos, line_content)
else:
# 在左侧末尾插入新行
self.left_text.insert(tk.END, "\n" + line_content)
# 更新行号显示
self.update_line_numbers()
# 重新对比文本
self.compare_texts()
# 更新详情框
self.update_detail_view(self.right_selected_line, line_content, line_content)
def clear_texts(self):
"""清空所有文本框"""
self.left_text.delete(1.0, tk.END)
self.right_text.delete(1.0, tk.END)
self.detail_text.config(state='normal')
self.detail_text.delete(1.0, tk.END)
self.detail_text.config(state='disabled')
# 重置选中行号
self.left_selected_line = None
self.right_selected_line = None
# 清除连接线
self.connection_canvas.delete("all")
self.update_line_numbers()
if __name__ == "__main__":
root = tk.Tk()
app = TextComparator(root)
root.mainloop()