在我们的《手写数字识别》项目中,需要提供一个用于手写数字的绘图板作为用户界面,便于获得用户手写的数字输入算法模型以便能够进行识别。这一章节,我们主要介绍关于绘图板开发过程中用到的相关技术。主要包括:
- 画布构建
- 图形绘制
- 相关事件
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()
图形绘制-图像储存
在进行画布图片保存时,因笔记本屏幕的显示进行了缩放,所以在获取画布的屏幕位置信息时应当进行相应的缩放操作。具体的缩放比可以进入屏幕显示设置中查看。
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')