""" Python 图形编辑器 类似简化版 Photoshop 的绘图软件 """ import tkinter as tk from tkinter import ttk, colorchooser, messagebox, filedialog from PIL import Image, ImageDraw, ImageFilter, ImageEnhance, ImageTk import numpy as np import json from typing import List, Tuple, Optional from dataclasses import dataclass from enum import Enum class ToolType(Enum): """工具类型""" BRUSH = "brush" PENCIL = "pencil" ERASER = "eraser" LINE = "line" RECTANGLE = "rectangle" ELLIPSE = "ellipse" FILL = "fill" PICKER = "picker" TEXT = "text" @dataclass class Layer: """图层类""" id: int name: str image: Image.Image visible: bool = True opacity: float = 1.0 class GraphicEditor: """图形编辑器主类""" def __init__(self, root: tk.Tk): """ 初始化图形编辑器 Args: root: Tkinter 根窗口 """ self.root = root self.root.title("Python 图形编辑器") self.root.geometry("1400x900") # 图像参数 self.canvas_width = 800 self.canvas_height = 600 self.current_image: Optional[Image.Image] = None self.draw_image: Optional[Image.Image] = None self.draw_context: Optional[ImageDraw.ImageDraw] = None # 工具参数 self.current_tool = ToolType.BRUSH self.brush_size = 10 self.brush_color = "#000000" self.eraser_size = 20 # 绘图状态 self.start_x = 0 self.start_y = 0 self.is_drawing = False self.current_shape = None # 图层管理 self.layers: List[Layer] = [] self.active_layer_id = 0 self.layer_counter = 0 # 历史记录(撤销/重做) self.history: List[Image.Image] = [] self.history_index = -1 self.max_history = 20 # 历史记录 self.undo_stack = [] self.redo_stack = [] # 创建界面 self._create_ui() # 初始化画布 self._init_canvas() # 初始化图层 self._create_new_layer("背景") def _create_ui(self): """创建用户界面""" # 主框架 main_frame = ttk.Frame(self.root) main_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) # 左侧工具栏 self._create_toolbar(main_frame) # 中间画布区域 self._create_canvas_area(main_frame) # 右侧面板 self._create_right_panel(main_frame) # 底部状态栏 self._create_status_bar() def _create_toolbar(self, parent): """创建工具栏""" toolbar = ttk.Frame(parent, width=80) toolbar.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 5)) # 工具选择 tools_frame = ttk.LabelFrame(toolbar, text="工具", padding=5) tools_frame.pack(fill=tk.X, pady=(0, 10)) # 画笔工具 self.brush_btn = ttk.Button(tools_frame, text="🖌️ 画笔", command=lambda: self._set_tool(ToolType.BRUSH)) self.brush_btn.pack(fill=tk.X, pady=2) # 铅笔工具 self.pencil_btn = ttk.Button(tools_frame, text="✏️ 铅笔", command=lambda: self._set_tool(ToolType.PENCIL)) self.pencil_btn.pack(fill=tk.X, pady=2) # 橡皮擦 self.eraser_btn = ttk.Button(tools_frame, text="🧹 橡皮擦", command=lambda: self._set_tool(ToolType.ERASER)) self.eraser_btn.pack(fill=tk.X, pady=2) # 直线工具 self.line_btn = ttk.Button(tools_frame, text="📏 直线", command=lambda: self._set_tool(ToolType.LINE)) self.line_btn.pack(fill=tk.X, pady=2) # 矩形工具 self.rect_btn = ttk.Button(tools_frame, text="⬜ 矩形", command=lambda: self._set_tool(ToolType.RECTANGLE)) self.rect_btn.pack(fill=tk.X, pady=2) # 椭圆工具 self.ellipse_btn = ttk.Button(tools_frame, text="⭕ 椭圆", command=lambda: self._set_tool(ToolType.ELLIPSE)) self.ellipse_btn.pack(fill=tk.X, pady=2) # 填充工具 self.fill_btn = ttk.Button(tools_frame, text="🪣 填充", command=lambda: self._set_tool(ToolType.FILL)) self.fill_btn.pack(fill=tk.X, pady=2) # 取色器 self.picker_btn = ttk.Button(tools_frame, text="💉 取色器", command=lambda: self._set_tool(ToolType.PICKER)) self.picker_btn.pack(fill=tk.X, pady=2) # 颜色选择 color_frame = ttk.LabelFrame(toolbar, text="颜色", padding=5) color_frame.pack(fill=tk.X, pady=(0, 10)) self.color_btn = tk.Button(color_frame, bg=self.brush_color, command=self._choose_color, height=2) self.color_btn.pack(fill=tk.X, pady=2) # 画笔大小 size_frame = ttk.LabelFrame(toolbar, text="画笔大小", padding=5) size_frame.pack(fill=tk.X, pady=(0, 10)) self.size_slider = ttk.Scale(size_frame, from_=1, to=100, orient=tk.HORIZONTAL, command=self._update_brush_size) self.size_slider.set(self.brush_size) self.size_slider.pack(fill=tk.X) self.size_label = ttk.Label(size_frame, text=str(self.brush_size)) self.size_label.pack() # 操作按钮 ops_frame = ttk.LabelFrame(toolbar, text="操作", padding=5) ops_frame.pack(fill=tk.X, pady=(0, 10)) ttk.Button(ops_frame, text="↩️ 撤销", command=self._undo).pack(fill=tk.X, pady=2) ttk.Button(ops_frame, text="↪️ 重做", command=self._redo).pack(fill=tk.X, pady=2) ttk.Button(ops_frame, text="🗑️ 清除", command=self._clear_canvas).pack(fill=tk.X, pady=2) def _create_canvas_area(self, parent): """创建画布区域""" canvas_frame = ttk.Frame(parent) canvas_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) # 画布容器 self.canvas_container = tk.Canvas(canvas_frame, width=self.canvas_width, height=self.canvas_height, bg='gray') self.canvas_container.pack(padx=10, pady=10) # 创建 Tkinter 画布 self.canvas = tk.Canvas(self.canvas_container, width=self.canvas_width, height=self.canvas_height, bg='white', cursor='crosshair') self.canvas_container.create_window( self.canvas_width // 2, self.canvas_height // 2, window=self.canvas ) # 绑定事件 self.canvas.bind("", self._on_mouse_down) self.canvas.bind("", self._on_mouse_drag) self.canvas.bind("", self._on_mouse_up) def _create_right_panel(self, parent): """创建右侧面板""" right_panel = ttk.Frame(parent, width=250) right_panel.pack(side=tk.RIGHT, fill=tk.Y, padx=(5, 0)) # 图层面板 layers_frame = ttk.LabelFrame(right_panel, text="图层", padding=5) layers_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10)) # 图层列表 self.layers_listbox = tk.Listbox(layers_frame, height=10) self.layers_listbox.pack(fill=tk.X, pady=2) self.layers_listbox.bind('<>', self._on_layer_select) # 图层操作按钮 layers_btn_frame = ttk.Frame(layers_frame) layers_btn_frame.pack(fill=tk.X, pady=5) ttk.Button(layers_btn_frame, text="+", command=self._create_new_layer).pack(side=tk.LEFT, padx=2) ttk.Button(layers_btn_frame, text="-", command=self._delete_layer).pack(side=tk.LEFT, padx=2) ttk.Button(layers_btn_frame, text="👁️", command=self._toggle_layer_visibility).pack(side=tk.LEFT, padx=2) ttk.Button(layers_btn_frame, text="⬆️", command=self._move_layer_up).pack(side=tk.LEFT, padx=2) ttk.Button(layers_btn_frame, text="⬇️", command=self._move_layer_down).pack(side=tk.LEFT, padx=2) # 滤镜面板 filters_frame = ttk.LabelFrame(right_panel, text="滤镜", padding=5) filters_frame.pack(fill=tk.X, pady=(0, 10)) ttk.Button(filters_frame, text="🌫️ 高斯模糊", command=lambda: self._apply_filter("blur")).pack(fill=tk.X, pady=2) ttk.Button(filters_frame, text="🔍 锐化", command=lambda: self._apply_filter("sharpen")).pack(fill=tk.X, pady=2) ttk.Button(filters_frame, text="🌓 边缘检测", command=lambda: self._apply_filter("edge")).pack(fill=tk.X, pady=2) ttk.Button(filters_frame, text="🎨 浮雕", command=lambda: self._apply_filter("emboss")).pack(fill=tk.X, pady=2) # 调整面板 adjust_frame = ttk.LabelFrame(right_panel, text="图像调整", padding=5) adjust_frame.pack(fill=tk.X) ttk.Button(adjust_frame, text="☀️ 亮度", command=lambda: self._adjust_image("brightness")).pack(fill=tk.X, pady=2) ttk.Button(adjust_frame, text="🎭 对比度", command=lambda: self._adjust_image("contrast")).pack(fill=tk.X, pady=2) ttk.Button(adjust_frame, text="🎨 饱和度", command=lambda: self._adjust_image("color")).pack(fill=tk.X, pady=2) # 文件操作 file_frame = ttk.LabelFrame(right_panel, text="文件", padding=5) file_frame.pack(fill=tk.X, pady=(10, 0)) ttk.Button(file_frame, text="📂 新建", command=self._new_file).pack(fill=tk.X, pady=2) ttk.Button(file_frame, text="📂 打开", command=self._open_file).pack(fill=tk.X, pady=2) ttk.Button(file_frame, text="💾 保存", command=self._save_file).pack(fill=tk.X, pady=2) ttk.Button(file_frame, text="💾 另存为", command=self._save_file_as).pack(fill=tk.X, pady=2) ttk.Button(file_frame, text="📤 导出", command=self._export_file).pack(fill=tk.X, pady=2) def _create_status_bar(self): """创建状态栏""" self.status_bar = ttk.Label(self.root, text="就绪", relief=tk.SUNKEN) self.status_bar.pack(side=tk.BOTTOM, fill=tk.X) def _init_canvas(self): """初始化画布""" self.current_image = Image.new('RGB', (self.canvas_width, self.canvas_height), 'white') self._update_canvas_display() def _update_canvas_display(self): """更新画布显示""" if self.current_image: self.tk_image = ImageTk.PhotoImage(self.current_image) self.canvas.create_image(0, 0, anchor=tk.NW, image=self.tk_image) def _create_new_layer(self, name: str = None): """创建新图层""" if name is None: name = f"图层 {self.layer_counter + 1}" self.layer_counter += 1 layer = Layer( id=self.layer_counter, name=name, image=Image.new('RGBA', (self.canvas_width, self.canvas_height), (0, 0, 0, 0)) ) self.layers.append(layer) if not self.active_layer_id: self.active_layer_id = layer.id self._update_layers_listbox() self._status(f"创建新图层:{name}") def _update_layers_listbox(self): """更新图层列表""" self.layers_listbox.delete(0, tk.END) for layer in reversed(self.layers): visibility = "👁️" if layer.visible else "❌" marker = "▶️" if layer.id == self.active_layer_id else "" self.layers_listbox.insert(tk.END, f"{marker} {visibility} {layer.name}") def _set_tool(self, tool: ToolType): """设置当前工具""" self.current_tool = tool self._status(f"当前工具:{tool.value}") def _update_brush_size(self, value): """更新画笔大小""" self.brush_size = int(float(value)) self.size_label.config(text=str(self.brush_size)) def _choose_color(self): """选择颜色""" color = colorchooser.askcolor(title="选择颜色")[1] if color: self.brush_color = color self.color_btn.config(bg=color) self._status(f"选择颜色:{color}") def _on_mouse_down(self, event): """鼠标按下事件""" self.is_drawing = True self.start_x = event.x self.start_y = event.y # 保存当前状态用于撤销 self._save_history() if self.current_tool in [ToolType.BRUSH, ToolType.PENCIL, ToolType.ERASER]: self._draw_point(event.x, event.y) def _on_mouse_drag(self, event): """鼠标拖动事件""" if not self.is_drawing: return if self.current_tool in [ToolType.BRUSH, ToolType.PENCIL, ToolType.ERASER]: self._draw_line(self.start_x, self.start_y, event.x, event.y) elif self.current_tool in [ToolType.LINE, ToolType.RECTANGLE, ToolType.ELLIPSE]: self._draw_shape(event.x, event.y) self.start_x = event.x self.start_y = event.y def _on_mouse_up(self, event): """鼠标释放事件""" self.is_drawing = False if self.current_tool == ToolType.FILL: self._flood_fill(event.x, event.y) elif self.current_tool == ToolType.PICKER: self._pick_color(event.x, event.y) def _draw_point(self, x: int, y: int): """绘制点""" if self.current_tool == ToolType.ERASER: color = (255, 255, 255, 255) if self.draw_image.mode == 'RGBA' else (255, 255, 255) else: color = self._hex_to_rgb(self.brush_color) radius = self.brush_size if self.current_tool != ToolType.ERASER else self.eraser_size if self.draw_context: if self.current_tool == ToolType.PENCIL: self.draw_context.point((x, y), fill=color) else: self.draw_context.ellipse( [x - radius//2, y - radius//2, x + radius//2, y + radius//2], fill=color ) self._update_canvas_display() def _draw_line(self, x1: int, y1: int, x2: int, y2: int): """绘制线段""" if self.current_tool == ToolType.ERASER: color = (255, 255, 255, 255) if self.draw_image.mode == 'RGBA' else (255, 255, 255) else: color = self._hex_to_rgb(self.brush_color) width = self.brush_size if self.current_tool != ToolType.ERASER else self.eraser_size if self.draw_context: self.draw_context.line([x1, y1, x2, y2], fill=color, width=width) self._update_canvas_display() def _draw_shape(self, x: int, y: int): """绘制形状""" # 清除之前的预览 self._update_canvas_display() color = self._hex_to_rgb(self.brush_color) width = self.brush_size if self.draw_context: if self.current_tool == ToolType.LINE: self.draw_context.line([self.start_x, self.start_y, x, y], fill=color, width=width) elif self.current_tool == ToolType.RECTANGLE: self.draw_context.rectangle([self.start_x, self.start_y, x, y], outline=color, width=width) elif self.current_tool == ToolType.ELLIPSE: self.draw_context.ellipse([self.start_x, self.start_y, x, y], outline=color, width=width) self._update_canvas_display() def _flood_fill(self, x: int, y: int): """洪水填充算法""" if not self.current_image: return # 转换为 numpy 数组 img_array = np.array(self.current_image) # 获取目标颜色 target_color = img_array[y, x] fill_color = np.array(self._hex_to_rgb(self.brush_color) + [255]) # 如果颜色相同则不填充 if np.array_equal(target_color[:3], fill_color[:3]): return # BFS 填充 height, width = img_array.shape[:2] visited = np.zeros((height, width), dtype=bool) stack = [(x, y)] while stack: cx, cy = stack.pop() if cx < 0 or cx >= width or cy < 0 or cy >= height: continue if visited[cy, cx]: continue current_color = img_array[cy, cx] # 检查颜色是否相似 if np.linalg.norm(current_color[:3] - target_color[:3]) > 30: continue # 设置新颜色 img_array[cy, cx] = fill_color visited[cy, cx] = True # 添加相邻像素 stack.extend([(cx+1, cy), (cx-1, cy), (cx, cy+1), (cx, cy-1)]) # 更新图像 self.current_image = Image.fromarray(img_array) self._update_canvas_display() self._status("填充完成") def _pick_color(self, x: int, y: int): """取色""" if not self.current_image: return pixel = self.current_image.getpixel((x, y)) if len(pixel) == 4: pixel = pixel[:3] color = '#%02x%02x%02x' % pixel self.brush_color = color self.color_btn.config(bg=color) self._status(f"取色:{color}") def _apply_filter(self, filter_type: str): """应用滤镜""" if not self.current_image: return self._save_history() if filter_type == "blur": self.current_image = self.current_image.filter(ImageFilter.GaussianBlur(radius=3)) elif filter_type == "sharpen": self.current_image = self.current_image.filter(ImageFilter.SHARPEN) elif filter_type == "edge": self.current_image = self.current_image.filter(ImageFilter.FIND_EDGES) elif filter_type == "emboss": self.current_image = self.current_image.filter(ImageFilter.EMBOSS) self._update_canvas_display() self._status(f"应用滤镜:{filter_type}") def _adjust_image(self, adjust_type: str): """调整图像""" if not self.current_image: return self._save_history() if adjust_type == "brightness": enhancer = ImageEnhance.Brightness(self.current_image) self.current_image = enhancer.enhance(1.2) elif adjust_type == "contrast": enhancer = ImageEnhance.Contrast(self.current_image) self.current_image = enhancer.enhance(1.2) elif adjust_type == "color": enhancer = ImageEnhance.Color(self.current_image) self.current_image = enhancer.enhance(1.2) self._update_canvas_display() self._status(f"调整:{adjust_type}") def _save_history(self): """保存历史状态""" if self.current_image: self.undo_stack.append(self.current_image.copy()) if len(self.undo_stack) > self.max_history: self.undo_stack.pop(0) self.redo_stack.clear() def _undo(self): """撤销""" if self.undo_stack: self.redo_stack.append(self.current_image.copy()) self.current_image = self.undo_stack.pop() self._update_canvas_display() self._status("撤销") def _redo(self): """重做""" if self.redo_stack: self.undo_stack.append(self.current_image.copy()) self.current_image = self.redo_stack.pop() self._update_canvas_display() self._status("重做") def _clear_canvas(self): """清除画布""" self._save_history() self.current_image = Image.new('RGB', (self.canvas_width, self.canvas_height), 'white') self._update_canvas_display() self._status("画布已清除") def _on_layer_select(self, event): """图层选择事件""" selection = self.layers_listbox.curselection() if selection: index = len(self.layers) - 1 - selection[0] if 0 <= index < len(self.layers): self.active_layer_id = self.layers[index].id self._update_layers_listbox() def _toggle_layer_visibility(self): """切换图层可见性""" selection = self.layers_listbox.curselection() if selection: index = len(self.layers) - 1 - selection[0] if 0 <= index < len(self.layers): self.layers[index].visible = not self.layers[index].visible self._update_layers_listbox() self._status(f"图层 {self.layers[index].name} {'可见' if self.layers[index].visible else '隐藏'}") def _move_layer_up(self): """上移图层""" selection = self.layers_listbox.curselection() if selection and selection[0] > 0: index = selection[0] self.layers.reverse() self.layers[index], self.layers[index-1] = self.layers[index-1], self.layers[index] self.layers.reverse() self._update_layers_listbox() def _move_layer_down(self): """下移图层""" selection = self.layers_listbox.curselection() if selection and selection[0] < len(self.layers) - 1: index = selection[0] self.layers.reverse() self.layers[index], self.layers[index+1] = self.layers[index+1], self.layers[index] self.layers.reverse() self._update_layers_listbox() def _delete_layer(self): """删除图层""" selection = self.layers_listbox.curselection() if selection and len(self.layers) > 1: index = len(self.layers) - 1 - selection[0] deleted = self.layers.pop(index) self._update_layers_listbox() self._status(f"删除图层:{deleted.name}") def _new_file(self): """新建文件""" if messagebox.askyesno("新建", "确定要新建文件吗?未保存的内容将丢失。"): self._save_history() self.current_image = Image.new('RGB', (self.canvas_width, self.canvas_height), 'white') self._update_canvas_display() self._status("新建文件") def _open_file(self): """打开文件""" file_path = filedialog.askopenfilename( title="打开文件", filetypes=[ ("图像文件", "*.png *.jpg *.jpeg *.bmp *.gif"), ("所有文件", "*.*") ] ) if file_path: try: self._save_history() img = Image.open(file_path) img = img.convert('RGBA') img = img.resize((self.canvas_width, self.canvas_height)) self.current_image = img self._update_canvas_display() self._status(f"打开文件:{file_path}") except Exception as e: messagebox.showerror("错误", f"无法打开文件:{str(e)}") def _save_file(self): """保存文件""" self._save_file_as() def _save_file_as(self): """另存为""" file_path = filedialog.asksaveasfilename( title="保存文件", defaultextension=".png", filetypes=[ ("PNG 文件", "*.png"), ("JPEG 文件", "*.jpg"), ("BMP 文件", "*.bmp"), ("所有文件", "*.*") ] ) if file_path: try: self.current_image.save(file_path) self._status(f"文件已保存:{file_path}") except Exception as e: messagebox.showerror("错误", f"无法保存文件:{str(e)}") def _export_file(self): """导出文件""" file_path = filedialog.asksaveasfilename( title="导出文件", defaultextension=".png", filetypes=[ ("PNG 文件", "*.png"), ("JPEG 文件", "*.jpg"), ("PDF 文件", "*.pdf"), ("所有文件", "*.*") ] ) if file_path: try: self.current_image.save(file_path) self._status(f"文件已导出:{file_path}") except Exception as e: messagebox.showerror("错误", f"无法导出文件:{str(e)}") def _hex_to_rgb(self, hex_color: str) -> tuple: """十六进制颜色转 RGB""" hex_color = hex_color.lstrip('#') return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) def _status(self, message: str): """更新状态栏""" self.status_bar.config(text=message) def run(self): """运行应用程序""" self.root.mainloop() def main(): """主函数""" root = tk.Tk() editor = GraphicEditor(root) editor.run() if __name__ == "__main__": main()