Initial commit: Python 图形编辑器 v1.0

This commit is contained in:
admin
2026-04-13 00:14:34 +08:00
commit ab57f0cd33
4 changed files with 1070 additions and 0 deletions
+713
View File
@@ -0,0 +1,713 @@
"""
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("<Button-1>", self._on_mouse_down)
self.canvas.bind("<B1-Motion>", self._on_mouse_drag)
self.canvas.bind("<ButtonRelease-1>", 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('<<ListboxSelect>>', 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()