前面我们已经学习了 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('推理')