在我们的《手写数字识别》项目中,需要提供一个用于手写数字的绘图板作为用户界面,便于获得用户手写的数字输入算法模型以便能够进行识别。这一章节,我们主要介绍关于绘图板开发过程中用到的相关技术。主要包括:
- 画布构建
- 图形绘制
- 相关事件
1. 画布构建
在 Tkinter 中各种图形的绘制需要在画布上进行,构建画布需要两个步骤:
- 构建主窗口
- 构建画布区域

import tkinter as tk
def test():
# 1. 构建主窗口
window = tk.Tk()
window.geometry('800x500+200+300')
window.resizable(False, False)
window.title('绘图板')
# 2. 构建画布区域
canvas = tk.Canvas()
canvas.config(bg='blue', width=300, height=200)
# 铺满整个屏幕
# canvas.pack(fill=tk.X, expand=True)
canvas.pack(side=tk.BOTTOM)
# 启动窗口主循环
window.mainloop()
if __name__ == '__main__':
test()
2. 图形绘制
这一部分我们将学习如何在 Canvas 上绘制图形、以及对图形的移动、删除操作,最后再介绍两种用于保存 Canvas 图像的方法。
import tkinter as tk
from PIL import Image
from PIL import ImageTk
from PIL import ImageGrab
from PIL import ImageDraw
# 1. 绘制图形
def test01():
window = tk.Tk()
window.geometry('800x500+200+300')
window.resizable(False, False)
window.title('绘图板')
canvas = tk.Canvas()
canvas.config(bg='black')
canvas.pack(fill=tk.BOTH, expand=True)
# 1.1 绘制矩形
# 需要左上角和右下角坐标
# fill : 指定矩形填充的颜色
# outline : 指定边边框的颜色
# width : 设置边框宽度
canvas.create_rectangle(50, 50, 150, 150 , fill='blue', outline='red', width=5)
# 1.2 绘制圆形
# 需要圆形外矩形左上角、右下角坐标
# fill : 指定矩形填充的颜色
# outline : 指定边边框的颜色
# width : 设置边框宽度
canvas.create_oval(210, 210, 300, 300, fill='yellow', outline='gray', width=5)
# 1.3 绘制线段
# 需要线段开始和结束位置
# fill :线段颜色
# width : 线段宽度
canvas.create_line(300, 100, 550, 150, fill='purple', width=10)
# 1.4 绘制多边形
# 需要多边形的多个点坐标
# fill : 指定矩形填充的颜色
# outline : 指定边边框的颜色
# width : 设置边框宽度
canvas.create_polygon(100, 200, 300, 200, 400, 150, outline='white', fill='red', width=5)
# 1.5 绘制图像
# 读取图像数据
image = Image.open('demo.jpeg')
# 转换为Tk图像格式
image = ImageTk.PhotoImage(image)
# 绘制图像需要指定绘制位置
# image : 指定要绘制的图像
# anchor : 指定图像的那个位置对齐到绘制位置
canvas.create_image(0, 0, image=image, anchor=tk.NW)
# 启动窗口主循环
window.mainloop()
# 2. 图形操作
def test02():
window = tk.Tk()
window.geometry('800x500+200+300')
window.resizable(False, False)
window.title('绘图板')
canvas = tk.Canvas()
canvas.config(bg='black')
canvas.pack(fill=tk.BOTH, expand=True)
# 绘制矩形
rect = canvas.create_rectangle(50, 50, 150, 150, fill='blue', outline='red', width=5)
print('rect', rect)
# 移动图形
# 方法一
# canvas.coords(rect, x1 + 300, y1 + 300, x2 + 300, y2 + 300)
# 方法二
canvas.addtag_withtag('rect1', rect)
canvas.move('rect1', 300, 100)
# 删除图形
# canvas.delete('all') # 删除所有图形
# canvas.delete(rect) # 删除指定图形
# 启动窗口主循环
window.mainloop()
# 3. 图像存储
def test03():
window = tk.Tk()
window.geometry('800x500+200+300')
window.resizable(False, False)
window.title('绘图板')
canvas = tk.Canvas()
canvas.config(bg='black')
canvas.pack(fill=tk.BOTH, expand=True)
rect = canvas.create_rectangle(50, 50, 150, 150, fill='blue', outline='red', width=5)
# 3.1 截图方式
def save_image1():
x = canvas.winfo_rootx()
y = canvas.winfo_rooty()
w = x + canvas.winfo_width()
h = y + canvas.winfo_height()
screenshot = ImageGrab.grab(bbox=(x, y, w, h))
screenshot.save('demo01.png')
button = tk.Button(text='截图保存', command=save_image1)
button.pack(side=tk.BOTTOM)
# 3.2 重绘方式
def save_image2():
w = canvas.winfo_width()
h = canvas.winfo_height()
# 创建一张空的图像对象
new_image = Image.new('RGB', (w, h), canvas['bg'])
draw = ImageDraw.Draw(new_image)
# 绘制图像上所有的对象
x1, y1, x2, y2 =canvas.coords(rect)
fill = canvas.itemcget(rect, 'fill')
width = canvas.itemcget(rect, 'width')
outline = canvas.itemcget(rect, 'outline')
draw.rectangle([x1, y1, x2, y2], outline=outline, fill=fill, width=int(float(width)))
# 存储图像
new_image.save('demo02.png')
button = tk.Button(text='重绘保存', command=save_image2)
button.pack(side=tk.TOP)
window.mainloop()
if __name__ == '__main__':
test03()
anchor 参数用于指定对象(例如文本、图像、多边形等)在给定坐标位置下的对齐方式。它控制对象相对于指定的坐标点的位置,类似于对象如何”锚定”到该坐标。anchor 参数的取值可以是字符串,通常为以下值之一:
n:北方(上边界) s:南方(下边界) w:西方(左边界) e:东方(右边界) nw:西北方(左上角) ne:东北方(右上角) sw:西南方(左下角) se:东南方(右下角) center:中心
3. 相关事件
<Button-1>:鼠标左键单击事件。 <Button-2>:鼠标中键单击事件。 <Button-3>:鼠标右键单击事件。 <Double-Button-1>:鼠标左键双击事件。 <Double-Button-2>:鼠标中键双击事件。 <Double-Button-3>:鼠标右键双击事件。 <Enter>:鼠标指针进入小部件的事件。 <Leave>:鼠标指针离开小部件的事件。 <KeyPress>:键盘按键按下事件。 <KeyRelease>:键盘按键释放事件。 <Motion>:鼠标移动事件。 <B1-Motion>:鼠标左键拖动 <B3-Motion>:鼠标右键拖动 <ButtonPress-1>: 鼠标左键按下 <ButtonPress-3>: 鼠标右键按下 <ButtonRelease-1>: 鼠标左键释放 <ButtonRelease-3>: 鼠标右键释放

import tkinter as tk
def test01():
window = tk.Tk()
window.geometry('800x500+200+300')
window.resizable(False, False)
window.title('绘图板')
canvas = tk.Canvas()
canvas.config(bg='blue')
canvas.pack(fill=tk.X, expand=True)
button = tk.Button(text='按钮')
button.pack(side=tk.BOTTOM)
# 绑定事件(要指定给那个对象绑定)
button.bind('<Button-1>', lambda event: print('按钮左键点击事件'))
canvas.bind('<Button-3>', lambda event: print('画布右键点击事件'))
# canvas.bind('<ButtonPress-1>', lambda event: print('鼠标左键按下'))
# canvas.bind('<ButtonPress-3>', lambda event: print('鼠标右键按下'))
#
# canvas.bind('<ButtonRelease-1>', lambda event: print('鼠标左键释放'))
# canvas.bind('<ButtonRelease-3>', lambda event: print('鼠标右键释放'))
# canvas.bind('<Double-Button-1>', lambda event: print('画布左键双击事件'))
# canvas.bind('<Double-Button-3>', lambda event: print('画布右键双击事件'))
# canvas.bind('<Enter>', lambda event: print('鼠标指针进入画布'))
# canvas.bind('<Leave>', lambda event: print('鼠标指针离开画布'))
# canvas.bind('<Motion>', lambda event: print('鼠标指针在画布滑动'))
# canvas.bind('<B1-Motion>', lambda event: print('鼠标左键按下在画布滑动'))
# canvas.bind('<B3-Motion>', lambda event: print('鼠标右键按下在画布滑动'))
window.mainloop()
def test02():
window = tk.Tk()
window.geometry('800x500+200+300')
window.resizable(False, False)
window.title('绘图板')
canvas = tk.Canvas()
canvas.config(bg='black')
canvas.pack(fill=tk.BOTH, expand=True)
x, y = None, None
prev = None
items = []
current_tool = 'line'
def on_mouse_pressed(event):
nonlocal x, y
x = event.x
y = event.y
def on_mouse_released(event):
nonlocal prev
items.append(prev)
prev = None
def on_mouse_motion(event):
temp_x = event.x
temp_y = event.y
nonlocal prev
if prev is not None:
canvas.delete(prev)
if current_tool == 'rectangle':
prev = canvas.create_rectangle(x, y, temp_x, temp_y, outline='white', width=3)
if current_tool == 'circle':
prev = canvas.create_oval(x, y, temp_x, temp_y, outline='white', width=3)
if current_tool == 'line':
prev = canvas.create_line(x, y, temp_x, temp_y, fill='white', width=3)
canvas.bind('<B1-Motion>', on_mouse_motion)
canvas.bind('<ButtonPress-1>', on_mouse_pressed)
canvas.bind('<ButtonRelease-1>', on_mouse_released)
toolbar = tk.Frame()
toolbar.config(bg='red', width=300)
def save_image():
from tkinter.filedialog import asksaveasfilename
from PIL import ImageGrab
filename = asksaveasfilename()
if not filename:
return
x = canvas.winfo_rootx()
y = canvas.winfo_rooty()
w = x + canvas.winfo_width()
h = y + canvas.winfo_height()
screenshot = ImageGrab.grab(bbox=(x, y, w, h))
screenshot.save(filename)
def draw_rectangle():
nonlocal current_tool
current_tool = 'rectangle'
def draw_circle():
nonlocal current_tool
current_tool = 'circle'
def draw_line():
nonlocal current_tool
current_tool = 'line'
button1 = tk.Button(toolbar, text='矩形', command=draw_rectangle)
button1.pack(side=tk.LEFT)
button2 = tk.Button(toolbar, text='圆形', command=draw_circle)
button2.pack(side=tk.LEFT)
button3 = tk.Button(toolbar, text='线条', command=draw_line)
button3.pack(side=tk.LEFT)
button4 = tk.Button(toolbar, text='清空', command=lambda : canvas.delete('all'))
button4.pack(side=tk.LEFT)
button4 = tk.Button(toolbar, text='保存', command=save_image)
button4.pack(side=tk.LEFT)
toolbar.pack()
window.mainloop()
if __name__ == '__main__':
test02()



冀公网安备13050302001966号
图形绘制-图像储存
在进行画布图片保存时,因笔记本屏幕的显示进行了缩放,所以在获取画布的屏幕位置信息时应当进行相应的缩放操作。具体的缩放比可以进入屏幕显示设置中查看。
def save_image():
# 获得Canvas对象相对于窗口左上角的一个坐标位置(绘图区域)
x = canvas.winfo_rootx()*1.5
y = canvas.winfo_rooty()*1.5
w = x + canvas.winfo_width()
h = y + canvas.winfo_height()
screenshot = ImageGrab.grab(bbox=(x, y, w, h))
screenshot.save('demo01.png')