








“别再用P图软件给老板写毛笔字了!3 秒钟,选好模板、敲好字、拖一拖,你的大作就能直接打印挂墙——隔壁老王都以为你偷偷练了十年颜柳欧赵!”
别眨眼,今天我们要拆的这段代码,就是一款用 Python 撸出来的在线书法生成器!它长了一张“程序员的脸”:Tkinter 的窗口、PIL 的滤镜、requests 的下载、BytesIO 的缓存……但干的全是艺术家的活:
下面,咱们把代码按“六大门派”切成六段,逐行解剖,层层递进。系好安全带,发车!
import 到 __main__ 的“一条龙”import tkinter as tkfrom tkinter import ttk, colorchooser, messagebox, filedialogfrom tkinter import fontfrom PIL import Image, ImageTk, ImageDraw, ImageFont, ImageGrabimport os, tempfile, traceback, datetime, requestsfrom io import BytesIOtkinterttk 让按钮更性感,colorchooser 调出调色板,messagebox 负责卖萌报错 | ||
tkinter.font | ||
ImageImageTk 转给 Tkinter,ImageDraw 写字,ImageFont 调字体,ImageGrab 截图 | ||
ostempfile 缓存下载的模板,requests 拉取图片,BytesIO 把网络流变成文件句柄 |
整个程序只有一个主角类——CalligraphyGenerator。它像一位导演:
__init__ 里搭舞台、选演员(变量初始化);create_widgets 里摆机位(左侧控制面板 + 右侧画布);__init__ 的三层“套娃”classCalligraphyGenerator:def__init__(self, root):# ① 主窗口属性 self.root = root self.root.title("书法生成器(3模板版)") self.root.geometry("1200x800") self.root.minsize(1000, 700)# ② 核心配置 self.default_font = "SimHei" self.font_family = self.default_font self.font_cache = {} # 字体绝对路径缓存# ③ 业务变量 self.current_text = "天道酬勤" self.font_size = 60 self.text_color = "#000000" self.text_position = (100, 100) ...root | title/geometry/minsize | ||
font_cache | |||
current_text/font_size/... |
接下来导演喊了声“Action!”——create_widgets() 开始布景。
defcreate_widgets(self):# 3.1 主框架:左右分栏 main_frame = ttk.Frame(self.root, padding="10") main_frame.pack(fill=tk.BOTH, expand=True)# 3.2 左侧控制面板 control_frame = ttk.LabelFrame(main_frame, text="书法设置", padding="10") control_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 10)) control_frame.configure(width=320) control_frame.pack_propagate(False) # 宽度写死 320px# 3.3 右侧预览区域 preview_frame = ttk.LabelFrame(main_frame, text="预览(拖动文字调整位置)", padding="10") preview_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)ttk.Framepack(fill=BOTH, expand=True) | ||
pack_propagate(False) | ||
LabelFrameCanvas + 双向滚动条 |
self.template_var = tk.StringVar()self.template_var.trace("w", self.on_template_selected) # 监听切换...ttk.Radiobutton(... text="模板1", value="0").pack(side=tk.LEFT, expand=True)trace("w", callback):一旦用户点选,on_template_selected 立即触发下载。0/1/2 对应 templates 列表,省得写一堆 if/else。defdownload_template(self, template_index):# 4.1 如果本地已缓存,直接复用if self.templates[template_index]['local_path'] and \ os.path.exists(self.templates[template_index]['local_path']): self.temp_image_path = self.templates[template_index]['local_path'] self.update_canvas_background()returnTrue# 4.2 否则从 CSDN 拉取 response = requests.get(self.templates[template_index]['url'], timeout=10)if response.status_code == 200: temp_file = tempfile.NamedTemporaryFile(suffix=".png", delete=False) temp_path = temp_file.name temp_file.close() Image.open(BytesIO(response.content)).save(temp_path) self.templates[template_index]['local_path'] = temp_path ...os.path.exists | ||
tempfile.NamedTemporaryFile(delete=False) | ||
defupdate_preview(self): self.canvas.delete("text_group") ... font_path = self.get_font_path(self.font_family) pil_font = ImageFont.truetype(font_path, self.font_size)# 5.1 逐字符渲染for i, char in enumerate(self.current_text): temp_size = int(self.font_size * 2) temp_img = Image.new('RGBA', (temp_size, temp_size), (0,0,0,0)) draw = ImageDraw.Draw(temp_img) draw.text((0, temp_size//2), char, font=pil_font, fill=self.text_color, anchor="lm")# 5.2 旋转 rotated_img = temp_img.rotate(self.rotation_angle, expand=True) tk_img = ImageTk.PhotoImage(rotated_img) self.character_images.append(tk_img)# 5.3 计算坐标if self.text_orientation == "horizontal": x = self.text_position[0] + i*(char_base_dim+self.character_spacing) y = self.text_position[1]else: x = self.text_position[0] y = self.text_position[1] + i*(char_base_dim+self.character_spacing) self.canvas.create_image(x, y, image=tk_img, tags="text_group", anchor=tk.NW)rotate(angle, expand=True) | expand=True | |
character_spacing 控制松紧 |
defon_drag_start(self, event):if"text_group"in self.canvas.gettags(self.canvas.find_withtag("current")): self.dragging = True self.drag_data = {"x": event.x, "y": event.y, "item": "text_group"}defon_drag_motion(self, event):if self.dragging: dx = event.x - self.drag_data["x"] dy = event.y - self.drag_data["y"] self.canvas.move("text_group", dx, dy) self.drag_data["x"], self.drag_data["y"] = event.x, event.yfind_withtag("current") 精准判断鼠标是否在文字上;canvas.move(tag, dx, dy) 让整组字符瞬间位移。defsave_history(self): state = {所有业务变量...} self.operation_history.append(state) self.history_index = len(self.operation_history) - 1defundo(self):if self.history_index > 0: self.history_index -= 1 state = self.operation_history[self.history_index]# 恢复所有变量并刷新界面save_history(),内存换用户体验;defsave_work(self): x = self.root.winfo_rootx() + self.preview_frame.winfo_x() y = self.root.winfo_rooty() + self.preview_frame.winfo_y() w = self.preview_frame.winfo_width() h = self.preview_frame.winfo_height() ImageGrab.grab(bbox=(x+10, y+30, x+w-20, y+h-40)).save(filename, 'png')winfo_rootx/y 获取屏幕绝对坐标;+10 +30 去掉边框;ImageGrab 直接截图,所见即所得。get_font_path 的“海底捞针”defget_font_path(self, font_name):if font_name in self.font_cache:return self.font_cache[font_name] font_dirs = ['C:\\Windows\\Fonts','C:\\Program Files\\Windows NT\\Fonts','/usr/share/fonts', ... ]for file in self.font_paths[font_name]:for font_dir in font_dirs: path = os.path.join(font_dir, file)if os.path.exists(path): self.font_cache[font_name] = pathreturn pathreturnNone最终目标
把代码丢进 PyInstaller,再打包一个 exe,你就是春节写对联最靓的仔!
点击【关注+收藏】获取最新的实战代码案例
用Python打造汉字笔画查询工具:从GUI界面到笔顺动画实现
Python超实用 Markdown 转富文本神器 —— 代码全解析
【实战1】
【实战2】
【实战3】
【实战4】

