前面我们已经学习了 Tkinter Canvas 控件相关的技术,现在我们将会使用前面学习的内容来实现用于手写数字的绘画板。绘画板主要包括四个部分,分别是:
- 主窗口界面
- 顶部工具栏
- 中心绘画区
- 底部状态栏

1. 主窗口界面
主界面编写时,包括三个文件:
- Config.py 配置文件
- MainFrame.py 主窗口类
- App.py 入口文件
Config.py 文件内容如下:
# 屏幕大小 SCREEN_W = 500 SCREEN_H = 575
MainFrame.py 文件内容如下:
import tkinter as tk
from Config import *
class MainFrame(tk.Tk):
def __init__(self):
super(MainFrame, self).__init__()
# 设置窗口尺寸位置
sw = self.winfo_screenwidth()
sh = self.winfo_screenheight()
px = int(sw/2 - SCREEN_W/2)
py = int(sh/2 - SCREEN_H/2)
self.geometry(f'{SCREEN_W}x{SCREEN_H}+{px}+{py}')
# 设置其他属性
self.resizable(False, False)
self.title('数字手写板')
# 初始化其他控件
self.init_widgets()
def init_widgets(self):
"""初始化窗口控件"""
pass
App.py 文件内容如下:
from MainFrame import MainFrame
if __name__ == '__main__':
main_frame = MainFrame()
main_frame.mainloop()
2. 顶部工具栏
TopBar 的 callbacks 参数为字典类型,根据 key 为顶部工具栏的每个按钮绑定点击事件。

import tkinter as tk
class TopBar(tk.Frame):
def __init__(self, callbacks={}):
super(TopBar, self).__init__()
self.callbacks = callbacks
self.init_widgets()
def init_widgets(self):
# 加载图标素材
self.load_images(scale=3)
# 创建按钮对象
self.buttons = {}
for name, image in self.images.items():
self.buttons[name] = tk.Button(self, command=self.callbacks.get(name, None), image=image)
self.buttons[name].pack(side=tk.LEFT)
def load_images(self, scale):
fnames = (('clear', 'source/clear.png'),
('save', 'source/save.png'),
('show', 'source/show.png'),
('open', 'source/open.png'),
('train', 'source/train.png'),
('predict', 'source/predict.png'))
self.images = {}
for name, fname in fnames:
self.images[name] = tk.PhotoImage(file=fname).subsample(scale, scale)
if __name__ == '__main__':
window = tk.Tk()
window.geometry('500x575+200+200')
window.resizable(False, False)
callbacks = {
'clear': lambda : print('清屏'),
'save': lambda : print('保存'),
'show': lambda : print('展示'),
'open': lambda : print('打开'),
'train': lambda : print('训练'),
'predict': lambda : print('预测'),
}
topbar = TopBar(callbacks=callbacks)
topbar.pack(side=tk.TOP, fill=tk.X)
window.mainloop()
3. 中心绘画区
这部分的代码比较多,主要包括:
- 标记区域
- 自由绘制
- 清空画布
- 打开图像
- 保存图像
3.1 标记区域
我们设置画布区域大小为 500×500,并将边框宽度设置为 0,保证存储的图像大小为 500×500。

import tkinter as tk
class CenCanvas(tk.Canvas):
def __init__(self):
super(CenCanvas, self).__init__()
self.config(highlightthickness=0, bg='white', width=500, height=500)
# 显示绘图区域
self.draw_area()
def draw_area(self):
width = 300
height = 300
x1, y1 = int(500/2 - width/2), int(500/2 - height/2)
x2, y2 = x1 + width, y1 + height
self.area = self.create_rectangle(x1, y1, x2, y2, outline="gray", width=2, dash=(15, 15))
3.2 自由绘制
当鼠标左键按下滑动时,在鼠标经过的位置绘制半径为 6 的圆,从而实现自由绘制图案。这里需要注意的是,当滑动鼠标过快时,上一个绘制的点和当前绘制的点之间会存在较大的空隙,我们这里会使用插值的方式在两个点之间补充绘制多个点。
import tkinter as tk
class CenCanvas(tk.Canvas):
def __init__(self):
super(CenCanvas, self).__init__()
self.config(highlightthickness=0, bg='white', width=500, height=500)
# 绑定各种事件
self.bind_events()
# 存储绘制对象
self.items = []
...
# 存储绘制位置
self.px = None
self.py = None
....
def bind_events(self):
self.bind('<B1-Motion>', self.mouse_motion)
self.bind('<ButtonRelease-1>', self.mouse_release)
def mouse_release(self, event):
self.px = None
self.py = None
def mouse_motion(self, event):
cx, cy = event.x, event.y
# 绘制圆半径
radius = 6
draw_points = [(cx, cy)]
# 计算插值
if self.px and self.py:
num_steps = int(max(abs(self.px - cx), abs(self.py-cy)) / 3)
if num_steps > 1:
points = self.insert_point((self.px, self.py), (cx, cy), num_steps)
draw_points.extend(points)
# 绘制点
for point in draw_points:
point = np.array(point)
point = np.hstack((point - radius, point + radius)).tolist()
item = self.create_oval(point, fill='#b33939', outline='#b33939')
self.items.append(item)
self.px, self.py = cx, cy
def insert_point(self, point1, point2, num_steps):
x1, y1 = point1
x2, y2 = point2
step_x = (x2 - x1) / num_steps
step_y = (y2 - y1) / num_steps
points = []
for index in range(num_steps):
ix = x1 + index * step_x
iy = y1 + index * step_y
points.append((ix, iy))
return points
if __name__ == '__main__':
window = tk.Tk()
window.geometry('500x575+200+200')
window.resizable(False, False)
canvas = CenCanvas()
canvas.pack()
frame = tk.Frame()
window.mainloop()
3.3 清空画布
import tkinter as tk
import numpy as np
from tkinter.filedialog import askopenfilename
from tkinter.filedialog import asksaveasfilename
from PIL import Image, ImageTk, ImageDraw
class CenCanvas(tk.Canvas):
...
def clear_canvas(self):
for item in self.items:
self.delete(item)
self.items.clear()
...
if __name__ == '__main__':
window = tk.Tk()
window.geometry('500x575+200+200')
window.resizable(False, False)
canvas = CenCanvas()
canvas.pack()
frame = tk.Frame()
clear_button = tk.Button(frame, text='清空', command=lambda : canvas.clear_canvas())
clear_button.pack(side=tk.LEFT)
frame.pack(side=tk.BOTTOM)
window.mainloop()
3.4 打开图像
import tkinter as tk
import numpy as np
from tkinter.filedialog import askopenfilename
from tkinter.filedialog import asksaveasfilename
from PIL import Image, ImageTk, ImageDraw
class CenCanvas(tk.Canvas):
...
def open(self):
# 1. 显示选择图像的对话框(图片路径)
default_path = os.path.abspath('data/train')
filename = askopenfilename(initialdir=default_path)
if not filename:
return
# 2. 读取图像数据,并进行转换
image = Image.open(filename)
photo = ImageTk.PhotoImage(image)
# 持有打开的图像,避免销毁
self.image_hold = photo
# 3. 将转换后的图像数据绘制到画布上
if hasattr(self, 'image_item') and self.image_item is not None:
self.delete(self.image_item)
if self.image_item in self.items:
self.items.remove(self.image_item)
self.image_item = self.create_image(0, 0, image=photo, anchor=tk.NW)
self.items.append(self.image_item)
if __name__ == '__main__':
window = tk.Tk()
window.geometry('500x575+200+200')
window.resizable(False, False)
canvas = CenCanvas()
canvas.pack()
frame = tk.Frame()
frame.pack(side=tk.BOTTOM)
clear_button = tk.Button(frame, text='清空', command=lambda : canvas.clear_canvas())
clear_button.pack(side=tk.LEFT)
open_button = tk.Button(frame, text='打开', command=lambda: canvas.open())
open_button.pack(side=tk.LEFT)
window.mainloop()
3.5 保存图像
import tkinter as tk
import numpy as np
from tkinter.filedialog import askopenfilename
from tkinter.filedialog import asksaveasfilename
from PIL import Image, ImageTk, ImageDraw
class CenCanvas(tk.Canvas):
...
def generate_image(self):
image = Image.new('RGB', (500, 500), self['bg'])
draw = ImageDraw.Draw(image)
for item in self.items:
if self.type(item) == 'image':
image.paste(self.image_data, box=self.bbox(item))
else:
point = self.coords(item)
color = self.itemcget(item, 'fill')
draw.ellipse(point, fill=color)
return image
def save(self):
import os
data_path = os.path.dirname(os.path.abspath(__file__)) + '\\data\\train'
print(data_path)
save_path = asksaveasfilename(initialdir=data_path, title='存储为', defaultextension=".png")
if not save_path:
return
image = self.generate_image()
image.save(save_path, "png")
if __name__ == '__main__':
window = tk.Tk()
window.geometry('500x575+200+200')
window.resizable(False, False)
canvas = CenCanvas()
canvas.pack()
frame = tk.Frame()
frame.pack(side=tk.BOTTOM)
clear_button = tk.Button(frame, text='清空', command=lambda : canvas.clear_canvas())
clear_button.pack(side=tk.LEFT)
open_button = tk.Button(frame, text='打开', command=lambda: canvas.open())
open_button.pack(side=tk.LEFT)
save_button = tk.Button(frame, text='保存', command=lambda: canvas.save())
save_button.pack(side=tk.LEFT)
window.mainloop()
4. 底部状态栏

import tkinter as tk
class StatusBar(tk.Frame):
def __init__(self):
super(StatusBar, self).__init__()
self.config(bg='#d2dae2', height=30)
self.status = tk.Label(self, text='准备就绪')
self.status.config(bg='#d2dae2')
self.status.pack(side=tk.LEFT)
def set_status(self, text):
self.status['text'] = text
if __name__ == '__main__':
window = tk.Tk()
window.geometry('500x575+200+300')
window.resizable(False, False)
sbar = StatusBar()
sbar.pack(side=tk.BOTTOM, fill=tk.X)
button = tk.Button(text='按钮', command=lambda : sbar.set_status('修改文本'))
button.pack()
window.mainloop()
5. 整合界面
修改 MainFrame.py 文件内容如下:
import tkinter as tk
from Config import *
from TopBar import TopBar
from CenCanvas import CenCanvas
from StatusBar import StatusBar
class MainFrame(tk.Tk):
....
def init_widgets(self):
"""初始化窗口控件"""
callbacks = {
'clear': self.clear,
'save': self.save,
'show': self.show,
'open': self.open,
'train': self.train,
'predict': self.predict,
}
self.tbar = TopBar(callbacks=callbacks)
self.tbar.pack(side=tk.TOP, fill=tk.X)
self.ccav = CenCanvas()
self.ccav.pack()
self.sbar = StatusBar()
self.sbar.pack(side=tk.BOTTOM, fill=tk.X)
# 下面为按钮绑定函数
def clear(self):
print('清屏')
def save(self):
print('保存')
def show(self):
print('展开')
def open(self):
print('打开')
def train(self):
print('训练')
def predict(self):
print('推理')



冀公网安备13050302001966号