searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

使用智能体生成应用

2026-06-30 18:40:55
1
0

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()
0条评论
0 / 1000
张****东
11文章数
0粉丝数
张****东
11 文章 | 0 粉丝
张****东
11文章数
0粉丝数
张****东
11 文章 | 0 粉丝
原创

使用智能体生成应用

2026-06-30 18:40:55
1
0

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()
文章来自个人专栏
文章 | 订阅
0条评论
0 / 1000
请输入你的评论
0
0