lesson26-2:使用Tkinter打造简易画图软件优化版
目录
前言
功能概览
核心代码解析
程序架构设计
菜单系统实现
画布与绘图核心
数据持久化实现
使用指南
基本操作流程
状态提示
功能扩展建议
总结
整体预览及解析
一、整体结构(鸟瞰图)
二、成员变量一览(大脑地图)
三、四大私有初始化函数
2️⃣ __init_canvas() —— 画布
3️⃣ __init_event() —— 事件绑定
4️⃣ __init_status() —— 状态栏
四、三大鼠标事件(绘图核心)
🔹 __mouse_down(e)
🔹 __mouse_move(e)
🔹 __mouse_up(e)
五、撤销 & 清空
🔸 __undo()
🔸 __clear()
六、保存 & 加载(持久化)
🔸 __save_data()
🔸 __load_data()
七、数据流向图(一图胜千言)
前言
在GUI编程的世界里,Tkinter作为Python自带的标准库,以其简洁易用的特点成为入门者的首选。本文将深入解析一段基于Tkinter开发的简易画图软件代码,带你了解如何从零开始构建一个具备基本绘图功能的桌面应用。该软件支持直线、矩形、椭圆等基本图形绘制,提供颜色选择、文件保存/加载、撤销/清空等核心功能,适合作为Tkinter事件处理与图形编程的学习案例。
功能概览
这款画图软件的核心功能围绕"绘制-编辑-保存"的工作流设计,主要包含以下模块:
- 图形绘制系统:支持直线、矩形、椭圆三种基础图形,通过鼠标拖拽实现动态绘制
- 颜色管理:内置红绿蓝三色快速选择及自定义颜色拾取器
- 文件操作:采用pickle序列化实现图形数据的保存(.zzy格式)与加载
- 编辑工具:提供撤销(单步)和清空画布功能
- 交互反馈:状态栏实时显示当前操作状态,提升用户体验
核心代码解析
程序架构设计
代码采用面向对象思想,通过Draw
类封装所有功能,主要包含以下组件初始化方法:
class Draw:
def __init__(self):
self.__root = tkinter.Tk()
self.__root.title('画图画画图')
self.__init_menu() # 初始化菜单栏
self.__init_canvas() # 初始化画布
self.__init_event() # 绑定事件处理
self.__init_status() # 初始化状态栏# 核心状态变量
self.__current_select_shape = None # 当前选择图形
self.__current_select_color = "black" # 当前选择颜色
self.__start_x, self.__start_y = None, None # 鼠标起始坐标
self.__current_shape_id = None # 当前绘制图形ID
self.__shape_datas = [] # 存储所有图形数据
菜单系统实现
菜单栏采用层级结构设计,通过tkinter.Menu
实现:
def __init_menu(self):
self.__main_menu = tkinter.Menu(master=self.__root)# 文件菜单:包含打开/保存功能
self.__file_menu = tkinter.Menu(master=self.__main_menu, tearoff=False)
self.__file_menu.add_command(label="打开", command=self.__load_data)
self.__file_menu.add_command(label="保存", command=self.__save_data)# 图形菜单:选择绘制图形类型
self.__shape_menu = tkinter.Menu(master=self.__main_menu, tearoff=False)
self.__shape_menu.add_command(label="直线", command=lambda: self.__set_current_select_shape(LINE))
# 矩形、椭圆菜单项类似...# 颜色菜单:预设颜色+自定义选择
self.__color_menu = tkinter.Menu(master=self.__main_menu, tearoff=False)
self.__color_menu.add_command(label="红色", command=lambda: self.__set_current_select_color("#FF0000"))
# 绿色、蓝色及自定义颜色项...# 其他菜单:撤销/清空/退出
self.__other_menu = tkinter.Menu(master=self.__main_menu, tearoff=False)
# 菜单项添加...self.__root.config(menu=self.__main_menu)
画布与绘图核心
画布组件是绘图功能的核心载体,通过绑定鼠标事件实现交互绘制:
def __init_canvas(self):
self.__canvas = tkinter.Canvas(width=600, height=400, background="lightgray")
self.__canvas.pack(fill="both", expand=True)# 鼠标事件处理
def __mouse_down(self, e):
if self.__current_select_shape is not None:
self.__start_x = e.x
self.__start_y = e.y
# 根据选择的图形类型创建初始图形
if self.__current_select_shape == LINE:
self.__current_shape_id = self.__canvas.create_line(
self.__start_x, self.__start_y, e.x, e.y, fill=self.__current_select_color)def __mouse_move(self, e):
# 拖拽鼠标时更新图形坐标
if self.__current_select_shape is not None:
self.__canvas.coords(self.__current_shape_id, self.__start_x, self.__start_y, e.x, e.y)def __mouse_up(self, e):
# 释放鼠标时保存图形数据
if self.__current_select_shape is not None:
self.__shape_datas.append(
(self.__current_shape_id, self.__current_select_shape,
self.__start_x, self.__start_y, e.x, e.y, self.__current_select_color))
数据持久化实现
通过pickle模块实现图形数据的序列化与反序列化:
def __save_data(self):
file_path = filedialog.asksaveasfilename(
title='保存为', defaultextension='.zzy', filetypes=[('新画图', '*.zzy')])
if file_path:
with open(file_path, "wb") as f:
pickle.dump(self.__shape_datas, f)
messagebox.showinfo("操作", "保存成功")def __load_data(self):
file_path = filedialog.askopenfilename(filetypes=[('新画图', '*.zzy')])
if file_path:
self.__clear()
with open(file_path, "rb") as f:
self.__shape_datas = pickle.load(f)
# 重新绘制加载的图形
for data in self.__shape_datas:
if data[1] == LINE:
self.__canvas.create_line(data[2], data[3], data[4], data[5], fill=data[-1])
# 矩形、椭圆绘制类似...
使用指南
基本操作流程
- 选择图形:从"图形"菜单中选择直线、矩形或椭圆
- 选择颜色:通过"颜色"菜单选择预设颜色或自定义颜色
- 绘制图形:在画布上按住鼠标左键拖拽,释放后完成绘制
- 文件操作:通过"文件"菜单保存(.zzy格式)或加载绘图文件
- 编辑操作:使用"其他"菜单中的撤销(单步)和清空功能
状态提示
状态栏会实时显示当前操作状态,例如:
- "选择直线,点击开始绘制"
- "选择矩形,点击开始绘制"
- "选择图形,开始绘制"(未选择图形时)
功能扩展建议
该简易画图软件可从以下方向进行功能扩展:
- 图形扩展:添加圆形、多边形、文本等更多图形元素
- 样式增强:支持线条粗细调整、填充颜色、虚线样式等
- 编辑功能:实现图形移动、缩放、旋转,多级撤销/重做
- 快捷键支持:为常用操作添加键盘快捷键
- 图像导出:支持导出为PNG/JPG等通用图像格式
- 图层管理:实现图形分层绘制与管理
总结
本文通过解析基于Tkinter的简易画图软件代码,展示了如何利用Python的标准GUI库实现基本的图形绘制功能。该案例涵盖了Tkinter的核心知识点,包括窗口组件布局、事件绑定、画布绘图、菜单系统及文件操作等。通过学习这段代码,不仅可以掌握Tkinter的基本使用方法,还能理解GUI应用的事件驱动编程思想。
对于Python初学者来说,这个项目是一个很好的实践案例,既有趣味性又能锻炼编程能力。你可以基于此代码进行二次开发,逐步添加更复杂的功能,最终打造属于自己的完整画图应用。
# 程序入口
if __name__ == "__main__":
draw = Draw()
draw.main_loop()
通过运行上述代码,你将获得一个功能完整的简易画图软件
整体预览及解析
"""
画图软件功能:菜单文件打开保存图形直线矩形椭圆...颜色红色绿色蓝色自定义其他撤销清除退出画布状态栏
"""import tkinter
from tkinter import colorchooser
from tkinter import filedialog
from tkinter import messagebox
import pickleLINE = "line"
RECT = "rect"
OVAL = "oval"class Draw:def __init__(self):self.__root = tkinter.Tk()self.__root.title('画图画画图')self.__init_menu()self.__init_canvas()self.__init_event()self.__init_status()self.__current_select_shape = Noneself.__current_select_color = "black"self.__start_x = Noneself.__start_y = Noneself.__current_shape_id = Noneself.__shape_datas = []def __save_data(self):file_path = filedialog.asksaveasfilename(title='保存为',defaultextension='.zzy',filetypes=[('新画图', '*.zzy')])if file_path:with open(file_path, "wb") as f:pickle.dump(self.__shape_datas, f)messagebox.showinfo("操作", "保存成功")def __load_data(self):file_path = filedialog.askopenfilename(title='请选择文件',filetypes=[('新画图', '*.zzy')])if file_path:self.__clear()with open(file_path, "rb") as f:self.__shape_datas = pickle.load(f)for data in self.__shape_datas:if data[1] == LINE:self.__current_shape_id = self.__canvas.create_line(data[2], data[3], data[4], data[5],fill=data[-1])elif data[1] == RECT:self.__current_shape_id = self.__canvas.create_rectangle(data[2], data[3], data[4], data[5],outline=data[-1])elif data[1] == OVAL:self.__current_shape_id = self.__canvas.create_oval(data[2], data[3], data[4], data[5],outline=data[-1])def __set_current_select_shape(self, shape):self.__current_select_shape = shapeif shape is None:self.__status.config(text="选择图形,开始绘制")elif shape == LINE:self.__status.config(text="选择直线,点击开始绘制")elif shape == RECT:self.__status.config(text="选择矩形,点击开始绘制")elif shape == OVAL:self.__status.config(text="选择椭圆,点击开始绘制")def __set_current_select_color(self, color):self.__current_select_color = colorprint(f"当前选择颜色{self.__current_select_color}")def __choose_color(self):result = colorchooser.askcolor()if result[1]:self.__set_current_select_color(result[1])def __init_menu(self):self.__main_menu = tkinter.Menu(master=self.__root)self.__file_menu = tkinter.Menu(master=self.__main_menu, tearoff=False)self.__file_menu.add_command(label="打开", command=self.__load_data)self.__file_menu.add_command(label="保存", command=self.__save_data)self.__main_menu.add_cascade(menu=self.__file_menu, label="文件")self.__shape_menu = tkinter.Menu(master=self.__main_menu, tearoff=False)self.__shape_menu.add_command(label="直线", command=lambda: self.__set_current_select_shape(LINE))self.__shape_menu.add_command(label="矩形", command=lambda: self.__set_current_select_shape(RECT))self.__shape_menu.add_command(label="椭圆", command=lambda: self.__set_current_select_shape(OVAL))self.__main_menu.add_cascade(menu=self.__shape_menu, label="图形")self.__color_menu = tkinter.Menu(master=self.__main_menu, tearoff=False)self.__color_menu.add_command(label="红色", command=lambda: self.__set_current_select_color("#FF0000"))self.__color_menu.add_command(label="绿色", command=lambda: self.__set_current_select_color("#00FF00"))self.__color_menu.add_command(label="蓝色", command=lambda: self.__set_current_select_color("#0000FF"))self.__color_menu.add_separator()self.__color_menu.add_command(label="自定义", command=self.__choose_color)self.__main_menu.add_cascade(menu=self.__color_menu, label="颜色")self.__other_menu = tkinter.Menu(master=self.__main_menu, tearoff=False)self.__other_menu.add_command(label="撤销", command=self.__undo)self.__other_menu.add_command(label="清空", command=self.__clear)self.__other_menu.add_command(label="退出", command=self.__root.destroy)self.__main_menu.add_cascade(menu=self.__other_menu, label="其他")self.__root.config(menu=self.__main_menu)def __init_canvas(self):self.__canvas = tkinter.Canvas(width=600, height=400, background="lightgray")self.__canvas.pack(fill="both", expand=True)def __mouse_down(self, e):if self.__current_select_shape is not None:self.__start_x = e.xself.__start_y = e.yif self.__current_select_shape == LINE:self.__current_shape_id = self.__canvas.create_line(self.__start_x, self.__start_y, e.x, e.y,fill=self.__current_select_color)elif self.__current_select_shape == RECT:self.__current_shape_id = self.__canvas.create_rectangle(self.__start_x, self.__start_y, e.x, e.y,outline=self.__current_select_color)elif self.__current_select_shape == OVAL:self.__current_shape_id = self.__canvas.create_oval(self.__start_x, self.__start_y, e.x, e.y,outline=self.__current_select_color)def __mouse_move(self, e):if self.__current_select_shape is not None:self.__canvas.coords(self.__current_shape_id, self.__start_x, self.__start_y, e.x, e.y)def __mouse_up(self, e):if self.__current_select_shape is not None and self.__start_x is not None and self.__start_y is not None:self.__shape_datas.append((self.__current_shape_id, self.__current_select_shape, self.__start_x, self.__start_y, e.x, e.y,self.__current_select_color))print(self.__shape_datas)self.__start_x = Noneself.__start_y = Noneself.__current_shape_id = Nonedef __undo(self):if self.__shape_datas:last_shape = self.__shape_datas.pop()last_shape_id = last_shape[0]self.__canvas.delete(last_shape_id)print(self.__shape_datas)def __clear(self):self.__shape_datas.clear()self.__canvas.delete("all")def __init_event(self):self.__canvas.bind("<Button-1>", func=self.__mouse_down)self.__canvas.bind("<B1-Motion>", func=self.__mouse_move)self.__canvas.bind("<ButtonRelease-1>", func=self.__mouse_up)self.__canvas.bind("<ButtonRelease-3>", func=lambda e: self.__set_current_select_shape(None))def __init_status(self):self.__status = tkinter.Label(master=self.__root, text="选择图形,开始绘制")self.__status.pack(side="left")def main_loop(self):self.__root.mainloop()draw = Draw()
draw.main_loop()
一、整体结构(鸟瞰图)
模块 | 作用 |
---|---|
常量区 (LINE /RECT /OVAL ) | 用字符串标识当前要画的形状,避免魔法字符串。 |
类 Draw | 唯一顶层类,把窗口、菜单、画布、状态栏、事件、数据全部包在一起。 |
私有方法 __xxx | 以双下划线开头,表示只在类内部使用,外部不应调用。 |
二、成员变量一览(大脑地图)
变量 | 类型/初始值 | 作用 |
---|---|---|
__root | tk.Tk() | 根窗口 |
__canvas | tk.Canvas(...) | 600×400 的画布,真正画图形的地方 |
__current_select_shape | None →"line"/"rect"/"oval" | 当前选中的形状 |
__current_select_color | "black" | 当前选中的颜色 |
__start_x/y | None →int | 鼠标按下时的坐标 |
__current_shape_id | None →int | 正在绘制的图形的 canvas id |
__shape_datas | [] | 核心数据仓库,保存所有已绘制图形的完整信息(用于撤销/保存/加载) |
三、四大私有初始化函数
1️⃣ __init_menu()
—— 菜单树构建
文件
打开 →
__load_data()
保存 →
__save_data()
图形
直线/矩形/椭圆 → 只改
__current_select_shape
颜色
红/绿/蓝 + 自定义 → 只改
__current_select_color
其他
撤销 →
__undo()
清空 →
__clear()
退出 →
root.destroy
tearoff=False
禁用可撕拉菜单,防止界面分离。
2️⃣ __init_canvas()
—— 画布
-
宽高 600×400,背景浅灰。
-
pack(fill="both", expand=True)
让画布随窗口一起拉伸。
3️⃣ __init_event()
—— 事件绑定
事件 | 回调 | 说明 |
---|---|---|
<Button-1> | __mouse_down | 左键按下:开始画 |
<B1-Motion> | __mouse_move | 左键拖动:实时预览 |
<ButtonRelease-1> | __mouse_up | 左键松开:完成图形并记录 |
<ButtonRelease-3> | lambda | 右键松开:取消当前形状选择 |
4️⃣ __init_status()
—— 状态栏
最底部放一条 Label
,提示用户当前选中了什么形状/颜色。
四、三大鼠标事件(绘图核心)
🔹
__mouse_down(e)
如果已选中形状(非
None
):
记录起点
(start_x, start_y)
立刻在画布上 创建一条长度为 0 的图形(直线/矩形/椭圆),得到
__current_shape_id
颜色使用
__current_select_color
🔹
__mouse_move(e)
只要鼠标移动,就用
canvas.coords(...)
实时改变刚才那条图形的终点坐标,实现“橡皮筋”效果。🔹
__mouse_up(e)
图形最终定型,把一条完整记录塞进
__shape_datas
:
(id, 形状常量, x0, y0, x1, y1, 颜色)
立刻打印到终端方便调试。
清空
start_x/start_y/current_shape_id
,为下一次绘制做准备。
五、撤销 & 清空
🔸 __undo()
-
从
__shape_datas
弹出最后一条记录 → 拿到id
→canvas.delete(id)
-
真正删除了画布上的图形,也删除了内存里的记录。
-
再次打印剩余数据,方便验证。
🔸 __clear()
-
直接
canvas.delete("all")
清空画布 -
__shape_datas.clear()
把列表也清空 → 撤销栈归零。
六、保存 & 加载(持久化)
🔸 __save_data()
-
filedialog.asksaveasfilename
弹出保存窗口,默认扩展名.zzy
(自定义二进制 pickle)。 -
把
__shape_datas
原封不动 pickle 到文件。 -
成功后用
messagebox.showinfo
弹窗提示。
🔸 __load_data()
-
弹窗让用户选
.zzy
文件 → 反序列化得到__shape_datas
。 -
先
__clear()
清空画布和列表,防止旧数据残留。 -
遍历列表,根据每条记录的
(形状, x0, y0, x1, y1, 颜色)
重新在画布上画一遍,实现“打开”功能。
七、数据流向图(一图胜千言)
用户操作│├─菜单:选择形状/颜色 → 修改 __current_select_shape / __current_select_color│├─鼠标:按下 → 创建图形 id│ ││ └─移动 → 实时更新坐标│ ││ └─松开 → 记录到 __shape_datas│├─撤销 → 弹出 & delete│└─保存 → pickle.dump(__shape_datas)│└─加载 → pickle.load → 重新绘制