You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Tkinter绘图应用如何实现多步撤销(Ctrl+Z)功能?

多步撤销/重做的Tkinter绘图实现方案

原代码的问题在于仅用单个变量存储最后一次绘图的线条,覆盖了历史记录,导致无法实现多步撤销。下面是优化后的实现,支持无限次撤销(直到无历史可撤),同时附带重做功能:

from tkinter import *


class PaintApp(Tk):
    def __init__(self):
        super().__init__()
        self.title("绘图板(Ctrl+Z撤销,Ctrl+Y重做)")
        
        # 存储每一步绘图的线条ID列表(撤销栈)
        self.undo_stack = []
        # 存储被撤销的绘图内容(重做栈)
        self.redo_stack = []
        # 当前正在绘制的线条ID列表
        self.current_lines = []
        # 上一个鼠标坐标
        self.last_pos = (0, 0)

        self.my_canvas = Canvas(self, bg='black')
        self.my_canvas.pack(fill=BOTH, expand=True)

        # 撤销/重做按钮
        btn_frame = Frame(self)
        btn_frame.pack(fill=X)
        self.undo_btn = Button(btn_frame, text="撤销(Ctrl+Z)", command=self.undo_action)
        self.undo_btn.pack(side=LEFT, padx=5, pady=5)
        self.redo_btn = Button(btn_frame, text="重做(Ctrl+Y)", command=self.redo_action)
        self.redo_btn.pack(side=LEFT, padx=5, pady=5)

        # 绑定鼠标事件
        self.my_canvas.bind('<B1-Motion>', self.paint)
        self.my_canvas.bind('<ButtonRelease-1>', self.finish_drawing)
        # 绑定键盘快捷键
        self.bind('<Control-z>', lambda e: self.undo_action())
        self.bind('<Control-y>', lambda e: self.redo_action())

    def paint(self, event):
        x, y = event.x, event.y
        if self.last_pos != (0, 0):
            # 创建线条并记录ID
            line_id = self.my_canvas.create_line(
                self.last_pos[0], self.last_pos[1], x, y,
                fill='white', width=5
            )
            self.current_lines.append(line_id)
        self.last_pos = (x, y)

    def finish_drawing(self, event):
        # 完成一次绘图,将当前线条列表存入撤销栈
        if self.current_lines:
            self.undo_stack.append(self.current_lines)
            self.current_lines = []
            # 有新绘图时清空重做栈(无法再重做之前的撤销操作)
            self.redo_stack.clear()
        self.last_pos = (0, 0)

    def undo_action(self):
        if self.undo_stack:
            # 取出最后一步绘图的线条
            last_drawing = self.undo_stack.pop()
            # 删除这些线条并存入重做栈
            self.my_canvas.delete(*last_drawing)
            self.redo_stack.append(last_drawing)

    def redo_action(self):
        if self.redo_stack:
            # 取出最后一次撤销的绘图内容
            undone_drawing = self.redo_stack.pop()
            # 重新绘制这些线条(注意:Canvas的create_line返回新ID,所以需要重新创建)
            new_lines = []
            for line_id in undone_drawing:
                # 获取原线条的坐标和属性
                coords = self.my_canvas.coords(line_id)
                fill = self.my_canvas.itemcget(line_id, 'fill')
                width = self.my_canvas.itemcget(line_id, 'width')
                new_line = self.my_canvas.create_line(coords, fill=fill, width=width)
                new_lines.append(new_line)
            # 将新的线条ID存入撤销栈
            self.undo_stack.append(new_lines)


if __name__ == '__main__':
    app = PaintApp()
    app.mainloop()

关键改进点:

  • 双栈结构:用undo_stack保存每一步完整的绘图动作(每步是一组线条ID),redo_stack保存被撤销的内容,实现撤销/重做双向操作。
  • 绘图动作边界:在鼠标释放时(finish_drawing)将当前绘制的所有线条作为一个完整动作存入撤销栈,确保每次撤销的是一整笔绘图,而不是单条线段。
  • 快捷键支持:绑定Ctrl+Z和Ctrl+Y,符合用户操作习惯。
  • 状态重置:每次新绘图后清空重做栈,避免逻辑混乱(比如画了新内容后,之前的撤销操作无法再重做)。

内容的提问来源于stack exchange,提问作者David 04

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.09 03:46:12