""" 简易图片修图工具
功能:加载图片、选择画笔大小和颜色、鼠标拖动修改像素
"""
import tkinter as tk
from tkinter import filedialog, colorchooser
from tkinter import ttk
from PIL import Image, ImageTk, ImageDraw
import os
class ImageEditor:
def __init__(self, root):
self.root = root
self.root.title("图片修图工具")
self.root.geometry("1200x800")
self.original_image = None
self.display_image = None
self.photo = None
self.image_path = None
self.zoom = 1.0
self.offset_x = 0
self.offset_y = 0
self.brush_size = 1
self.brush_color = "#FF0000"
self.is_drawing = False
self.last_x = None
self.last_y = None
self.is_panning = False
self.pan_start_x = None
self.pan_start_y = None
self.pan_offset_x = None
self.pan_offset_y = None
self.tool_mode = 'pan'
self.picked_color = None
self.is_picking = False
self.show_grid = False
self.color_stats = {}
self.selected_colors = []
self.highlight_colors = ["#00FF00", "#FF0000", "#0000FF", "#FFFF00", "#FF00FF", "#00FFFF", "#FF8800", "#8800FF"]
self.show_highlight = True
self.create_ui()
self.drag_drop_enabled = False
self.setup_drag_drop()
def setup_drag_drop(self):
"""设置拖放功能"""
try:
from tkinterdnd2 import DND_FILES
self.canvas.drop_target_register(DND_FILES)
self.canvas.dnd_bind('<<Drop>>', self.on_drop)
self.canvas.dnd_bind('<<DragEnter>>', self.on_drag_enter)
self.drag_drop_enabled = True
except Exception as e:
self.drag_drop_enabled = False
print(f"拖放功能不可用:{e}")
def create_ui(self):
control_frame = tk.Frame(self.root, bg="#f0f0f0", height=80)
control_frame.pack(side=tk.TOP, fill=tk.X)
control_frame.pack_propagate(False)
btn_frame = tk.Frame(control_frame, bg="#f0f0f0")
btn_frame.pack(side=tk.LEFT, padx=10, pady=10)
load_btn = tk.Button(
btn_frame, text="📁 加载图片", command=self.load_image, font=("Microsoft YaHei", 10), bg="#4a90d9", fg="white", width=12, height=2
)
load_btn.pack(side=tk.LEFT, padx=5)
save_btn = tk.Button(
btn_frame, text="💾 保存图片", command=self.save_image, font=("Microsoft YaHei", 10), bg="#50c878", fg="white", width=12, height=2
)
save_btn.pack(side=tk.LEFT, padx=5)
brush_frame = tk.Frame(control_frame, bg="#f0f0f0")
brush_frame.pack(side=tk.LEFT, padx=30, pady=10)
tk.Label(
brush_frame, text="画笔大小:", bg="#f0f0f0", font=("Microsoft YaHei", 10)
).pack(side=tk.LEFT, padx=5)
self.size_var = tk.IntVar(value=1)
size_scale = tk.Scale(
brush_frame, from_=1, to=50, orient=tk.HORIZONTAL, variable=self.size_var, command=self.update_brush_size, bg="#f0f0f0", length=150
)
size_scale.pack(side=tk.LEFT, padx=5)
self.size_label = tk.Label(
brush_frame, text=f"{self.brush_size}px", bg="#f0f0f0", font=("Microsoft YaHei", 10), width=5
)
self.size_label.pack(side=tk.LEFT, padx=5)
tk.Label(
brush_frame, text="画笔颜色:", bg="#f0f0f0", font=("Microsoft YaHei", 10)
).pack(side=tk.LEFT, padx=(20, 5))
self.color_btn = tk.Button(
brush_frame, bg=self.brush_color, command=self.choose_color, width=4, height=1, relief=tk.RAISED
)
self.color_btn.pack(side=tk.LEFT, padx=5)
eraser_btn = tk.Button(
brush_frame, text="🧽 橡皮擦", command=self.use_eraser, font=("Microsoft YaHei", 10), bg="#ff9500", fg="white", width=10, height=1
)
eraser_btn.pack(side=tk.LEFT, padx=10)
tool_frame = tk.Frame(brush_frame, bg="#f0f0f0")
tool_frame.pack(side=tk.LEFT, padx=10)
self.pan_tool_btn = tk.Button(
tool_frame, text="✋ 移动", command=lambda: self.set_tool('pan'), font=("Microsoft YaHei", 10), bg="#4CAF50", fg="white", width=8, height=1
)
self.pan_tool_btn.pack(side=tk.LEFT, padx=2)
self.brush_tool_btn = tk.Button(
tool_frame, text="🖌️ 画笔", command=lambda: self.set_tool('brush'), font=("Microsoft YaHei", 10), bg="#95a5a6", fg="white", width=8, height=1
)
self.brush_tool_btn.pack(side=tk.LEFT, padx=2)
self.picker_tool_btn = tk.Button(
tool_frame, text="💉 取色", command=lambda: self.set_tool('picker'), font=("Microsoft YaHei", 10), bg="#95a5a6", fg="white", width=8, height=1
)
self.picker_tool_btn.pack(side=tk.LEFT, padx=2)
grid_btn = tk.Button(
brush_frame, text="⚏ 网格", command=self.toggle_grid, font=("Microsoft YaHei", 10), bg="#9b59b6", fg="white", width=8, height=1
)
grid_btn.pack(side=tk.LEFT, padx=5)
right_tool_frame = tk.Frame(control_frame, bg="#f0f0f0")
right_tool_frame.pack(side=tk.RIGHT, padx=10, pady=10)
zoom_frame = tk.Frame(right_tool_frame, bg="#f0f0f0")
zoom_frame.pack(side=tk.LEFT, padx=10)
tk.Label(
zoom_frame, text="缩放:", bg="#f0f0f0", font=("Microsoft YaHei", 9)
).pack(side=tk.LEFT, padx=2)
zoom_fit_btn = tk.Button(
zoom_frame, text="适应", command=self.zoom_fit, font=("Microsoft YaHei", 8), bg="#3498db", fg="white", width=6, height=1
)
zoom_fit_btn.pack(side=tk.LEFT, padx=2)
zoom_100_btn = tk.Button(
zoom_frame, text="100%", command=self.zoom_100, font=("Microsoft YaHei", 8), bg="#3498db", fg="white", width=6, height=1
)
zoom_100_btn.pack(side=tk.LEFT, padx=2)
clear_btn = tk.Button(
right_tool_frame, text="🔄 重置图片", command=self.reset_image, font=("Microsoft YaHei", 10), bg="#e74c3c", fg="white", width=10, height=2
)
clear_btn.pack(side=tk.LEFT, padx=5)
canvas_frame = tk.Frame(self.root, bg="#ffffff")
canvas_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
self.canvas = tk.Canvas(
canvas_frame, bg="#ffffff", highlightthickness=0, cursor="fleur"
)
self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
color_panel = tk.Frame(canvas_frame, bg="#f5f5f5", width=250)
color_panel.pack(side=tk.RIGHT, fill=tk.Y)
color_panel.pack_propagate(False)
tk.Label(
color_panel, text="📊 颜色列表", bg="#f5f5f5", font=("Microsoft YaHei", 12, "bold")
).pack(side=tk.TOP, pady=10)
self.stats_label = tk.Label(
color_panel, text="加载图片后显示", bg="#f5f5f5", font=("Microsoft YaHei", 9), justify=tk.LEFT
)
self.stats_label.pack(side=tk.TOP, pady=5, padx=10, anchor=tk.W)
refresh_btn = tk.Button(
color_panel, text="🔄 刷新颜色", command=self.refresh_color_list, font=("Microsoft YaHei", 9), bg="#3498db", fg="white", width=15
)
refresh_btn.pack(side=tk.TOP, pady=5)
list_frame = tk.Frame(color_panel, bg="#f5f5f5")
list_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=5, pady=5)
scrollbar = tk.Scrollbar(list_frame)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.color_listbox = tk.Listbox(
list_frame, bg="white", font=("Microsoft YaHei", 9), yscrollcommand=scrollbar.set, height=20, selectmode=tk.MULTIPLE
)
self.color_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.config(command=self.color_listbox.yview)
self.color_listbox.bind('<<ListboxSelect>>', self.on_color_select)
picker_frame = tk.Frame(color_panel, bg="#f5f5f5")
picker_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=5, padx=5)
tk.Label(
picker_frame, text="💉 取色器预览:", bg="#f5f5f5", font=("Microsoft YaHei", 9)
).pack(side=tk.TOP, anchor=tk.W)
preview_container = tk.Frame(picker_frame, bg="#ffffff", relief=tk.SUNKEN, borderwidth=1)
preview_container.pack(side=tk.TOP, fill=tk.X, pady=3)
self.picked_color_preview = tk.Label(
preview_container, bg="#ffffff", width=5, height=2
)
self.picked_color_preview.pack(side=tk.LEFT, padx=5, pady=5)
self.picked_color_info = tk.Label(
preview_container, text="点击取色后\n点击图片获取颜色", bg="#ffffff", font=("Consolas", 9), justify=tk.LEFT, anchor=tk.W
)
self.picked_color_info.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5)
picker_btn_frame = tk.Frame(picker_frame, bg="#f5f5f5")
picker_btn_frame.pack(side=tk.TOP, pady=3)
use_picked_btn = tk.Button(
picker_btn_frame, text="使用此颜色", command=self.use_picked_color, font=("Microsoft YaHei", 8), bg="#3498db", fg="white", width=12
)
use_picked_btn.pack(side=tk.LEFT, padx=2)
copy_color_btn = tk.Button(
picker_btn_frame, text="📋 复制代码", command=self.copy_picked_color_code, font=("Microsoft YaHei", 8), bg="#27ae60", fg="white", width=10
)
copy_color_btn.pack(side=tk.LEFT, padx=2)
btn_frame = tk.Frame(color_panel, bg="#f5f5f5")
btn_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=10, padx=5)
select_frame = tk.Frame(btn_frame, bg="#f5f5f5")
select_frame.pack(side=tk.TOP, pady=2)
tk.Button(
select_frame, text="全选", command=self.select_all_colors, font=("Microsoft YaHei", 8), bg="#95a5a6", fg="white", width=5
).pack(side=tk.LEFT, padx=1)
tk.Button(
select_frame, text="清空", command=self.clear_color_selection, font=("Microsoft YaHei", 8), bg="#95a5a6", fg="white", width=5
).pack(side=tk.LEFT, padx=1)
tk.Button(
select_frame, text="反选", command=self.invert_color_selection, font=("Microsoft YaHei", 8), bg="#95a5a6", fg="white", width=5
).pack(side=tk.LEFT, padx=1)
self.highlight_var = tk.BooleanVar(value=True)
highlight_check = tk.Checkbutton(
btn_frame, text="高亮选中颜色", variable=self.highlight_var, command=self.toggle_highlight, bg="#f5f5f5", font=("Microsoft YaHei", 9)
)
highlight_check.pack(side=tk.TOP, pady=2)
replace_group_frame = tk.Frame(btn_frame, bg="#f5f5f5")
replace_group_frame.pack(side=tk.TOP, pady=5)
replace_btn = tk.Button(
replace_group_frame, text="🎨 选择颜色替换", command=self.replace_color, font=("Microsoft YaHei", 9), bg="#e67e22", fg="white", width=18
)
replace_btn.pack(side=tk.TOP, pady=2)
replace_picked_btn = tk.Button(
replace_group_frame, text="💉 替换为取色", command=self.replace_with_picked_color, font=("Microsoft YaHei", 9), bg="#9b59b6", fg="white", width=18
)
replace_picked_btn.pack(side=tk.TOP, pady=2)
transparent_btn = tk.Button(
replace_group_frame, text="🔳 设置为透明", command=self.replace_with_transparent, font=("Microsoft YaHei", 9), bg="#34495e", fg="white", width=18
)
transparent_btn.pack(side=tk.TOP, pady=2)
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)
self.canvas.bind("<Button-2>", self.on_pan_start)
self.canvas.bind("<Button-3>", self.on_pan_start)
self.canvas.bind("<B2-Motion>", self.on_pan_drag)
self.canvas.bind("<B3-Motion>", self.on_pan_drag)
self.canvas.bind("<ButtonRelease-2>", self.on_pan_end)
self.canvas.bind("<ButtonRelease-3>", self.on_pan_end)
self.canvas.bind("<MouseWheel>", self.on_zoom)
self.canvas.bind("<Button-4>", self.on_zoom)
self.canvas.bind("<Button-5>", self.on_zoom)
status_frame = tk.Frame(self.root, bg="#e0e0e0", height=30)
status_frame.pack(side=tk.BOTTOM, fill=tk.X)
status_frame.pack_propagate(False)
self.status_label = tk.Label(
status_frame, text="请加载图片 | 滚轮缩放 | 左键拖动 | 点击画笔按钮绘制 | 右键也可拖动", bg="#e0e0e0", font=("Microsoft YaHei", 9), anchor=tk.W
)
self.status_label.pack(side=tk.LEFT, padx=10)
self.size_info_label = tk.Label(
status_frame, bg="#e0e0e0", font=("Microsoft YaHei", 9)
)
self.size_info_label.pack(side=tk.RIGHT, padx=10)
self.show_placeholder()
def show_placeholder(self):
"""显示占位提示"""
self.canvas.delete("all")
self.canvas.create_text(
self.canvas.winfo_width() // 2 if self.canvas.winfo_width() > 1 else 400,
self.canvas.winfo_height() // 2 if self.canvas.winfo_height() > 1 else 300,
text="📁 点击上方按钮加载图片",
fill="#666666",
font=("Microsoft YaHei", 16),
tags="placeholder"
)
def toggle_grid(self):
"""切换像素网格显示"""
self.show_grid = not self.show_grid
if self.display_image:
self.display_image_on_canvas()
self.status_label.config(text="像素网格已" + ("开启" if self.show_grid else "关闭"))
def on_pan_start(self, event):
"""中键/右键平移开始"""
if self.display_image is None:
return
self.is_panning = True
self.pan_start_x = event.x
self.pan_start_y = event.y
self.pan_offset_x = self.offset_x
self.pan_offset_y = self.offset_y
self.canvas.config(cursor="fleur")
def on_pan_drag(self, event):
"""中键/右键平移拖动"""
if not self.is_panning or self.display_image is None:
return
dx = event.x - self.pan_start_x
dy = event.y - self.pan_start_y
self.offset_x = self.pan_offset_x + dx
self.offset_y = self.pan_offset_y + dy
self.display_image_on_canvas()
def on_pan_end(self, event):
"""中键/右键平移结束"""
self.is_panning = False
self.canvas.config(cursor="crosshair")
def zoom_fit(self):
"""适应窗口大小"""
if self.display_image is None:
return
canvas_width = self.canvas.winfo_width()
canvas_height = self.canvas.winfo_height()
if canvas_width > 1 and canvas_height > 1:
img_width, img_height = self.display_image.size
scale_x = (canvas_width - 40) / img_width
scale_y = (canvas_height - 40) / img_height
self.zoom = min(scale_x, scale_y, 1.0)
else:
self.zoom = 1.0
self.offset_x = 0
self.offset_y = 0
self.display_image_on_canvas()
self.size_info_label.config(
text=f"{self.display_image.size[0]} x {self.display_image.size[1]} px | 缩放:{int(self.zoom * 100)}%"
)
def zoom_100(self):
"""缩放到 100%"""
if self.display_image is None:
return
self.zoom = 1.0
canvas_width = self.canvas.winfo_width()
canvas_height = self.canvas.winfo_height()
display_width = self.display_image.width
display_height = self.display_image.height
if canvas_width > 1 and canvas_height > 1:
self.offset_x = (canvas_width - display_width) // 2
self.offset_y = (canvas_height - display_height) // 2
else:
self.offset_x = 10
self.offset_y = 10
self.display_image_on_canvas()
self.size_info_label.config(
text=f"{self.display_image.size[0]} x {self.display_image.size[1]} px | 缩放:100%"
)
def load_image(self):
"""加载图片"""
file_path = filedialog.askopenfilename(
title="选择图片",
filetypes=[
("图片文件", "*.png *.jpg *.jpeg *.bmp *.gif *.tiff *.webp"),
("所有文件", "*.*")
]
)
if file_path:
self.open_image(file_path)
def open_image(self, file_path):
"""打开图片"""
try:
self.image_path = file_path
img = Image.open(file_path)
if img.mode != "RGBA":
self.original_image = img.convert("RGBA")
else:
self.original_image = img.copy()
if self.original_image.mode == "RGBA":
white_bg = Image.new("RGB", self.original_image.size, (255, 255, 255))
white_bg.paste(self.original_image, mask=self.original_image.split()[3])
self.display_image = white_bg.convert("RGBA")
else:
self.display_image = self.original_image.copy()
canvas_width = self.canvas.winfo_width()
canvas_height = self.canvas.winfo_height()
if canvas_width > 1 and canvas_height > 1:
img_width, img_height = self.display_image.size
scale_x = (canvas_width - 40) / img_width
scale_y = (canvas_height - 40) / img_height
self.zoom = min(scale_x, scale_y, 1.0)
else:
self.zoom = 1.0
self.offset_x = 0
self.offset_y = 0
self.display_image_on_canvas()
self.refresh_color_list()
self.status_label.config(text=f"已加载:{os.path.basename(file_path)}")
self.size_info_label.config(
text=f"{self.display_image.size[0]} x {self.display_image.size[1]} px | 缩放:{int(self.zoom * 100)}%"
)
except Exception as e:
self.status_label.config(text=f"加载失败:{str(e)}")
def display_image_on_canvas(self):
"""在画布上显示图片"""
if self.display_image is None:
return
self.canvas.delete("all")
display_width = int(self.display_image.width * self.zoom)
display_height = int(self.display_image.height * self.zoom)
if self.zoom != 1.0:
if self.zoom >= 10.0:
resized = self.display_image.resize((display_width, display_height), Image.Resampling.NEAREST)
else:
resized = self.display_image.resize((display_width, display_height), Image.Resampling.LANCZOS)
else:
resized = self.display_image
if resized.mode == "RGBA":
white_bg = Image.new("RGB", resized.size, (255, 255, 255))
white_bg.paste(resized, mask=resized.split()[3])
resized = white_bg
self.photo = ImageTk.PhotoImage(resized)
canvas_width = self.canvas.winfo_width()
canvas_height = self.canvas.winfo_height()
if canvas_width > 1 and canvas_height > 1:
if self.offset_x == 0 and self.offset_y == 0:
self.offset_x = (canvas_width - display_width) // 2
self.offset_y = (canvas_height - display_height) // 2
else:
if self.offset_x == 0 and self.offset_y == 0:
self.offset_x = 10
self.offset_y = 10
self.canvas.create_image(
self.offset_x, self.offset_y, anchor=tk.NW, image=self.photo, tags="image"
)
if self.show_grid and self.zoom >= 2.0:
self.draw_pixel_grid()
if self.highlight_var.get() and self.selected_colors:
self.draw_color_highlight()
def draw_color_highlight(self):
"""绘制选中颜色的高亮(支持多色)"""
if self.display_image is None or not self.selected_colors:
return
width, height = self.display_image.size
pixel_size = max(1, self.zoom)
color_to_highlight = {}
for i, color in enumerate(self.selected_colors):
highlight_color = self.highlight_colors[i % len(self.highlight_colors)]
color_to_highlight[color] = highlight_color
for y in range(height):
for x in range(width):
pixel_color = self.display_image.getpixel((x, y))
if isinstance(pixel_color, int):
pixel_color = (pixel_color, pixel_color, pixel_color)
pixel_color = pixel_color[:3]
if pixel_color in color_to_highlight:
canvas_x = self.offset_x + x * pixel_size
canvas_y = self.offset_y + y * pixel_size
highlight_color = color_to_highlight[pixel_color]
self.canvas.create_rectangle(
canvas_x, canvas_y, canvas_x + pixel_size, canvas_y + pixel_size,
outline=highlight_color, width=max(1, int(pixel_size / 8)), tags="highlight"
)
def draw_pixel_grid(self):
"""绘制像素网格"""
if self.display_image is None:
return
canvas_width = self.canvas.winfo_width()
canvas_height = self.canvas.winfo_height()
grid_spacing = self.zoom
for x in range(self.display_image.width + 1):
canvas_x = self.offset_x + x * grid_spacing
if canvas_x >= 0 and canvas_x <= canvas_width:
self.canvas.create_line(
canvas_x, self.offset_y, canvas_x, self.offset_y + self.display_image.height * self.zoom,
fill="#cccccc", width=1, tags="grid"
)
for y in range(self.display_image.height + 1):
canvas_y = self.offset_y + y * grid_spacing
if canvas_y >= 0 and canvas_y <= canvas_height:
self.canvas.create_line(
self.offset_x, canvas_y, self.offset_x + self.display_image.width * self.zoom, canvas_y,
fill="#cccccc", width=1, tags="grid"
)
def update_brush_size(self, value):
"""更新画笔大小"""
self.brush_size = int(value)
self.size_label.config(text=f"{self.brush_size}px")
def choose_color(self):
"""选择颜色"""
color = colorchooser.askcolor(self.brush_color)[1]
if color:
self.brush_color = color
self.color_btn.config(bg=color)
def use_eraser(self):
"""使用橡皮擦(白色)"""
self.brush_color = "#FFFFFF"
self.color_btn.config(bg="#FFFFFF")
self.status_label.config(text="已切换到橡皮擦模式")
def reset_image(self):
"""重置图片"""
if self.original_image:
self.display_image = self.original_image.copy()
self.display_image_on_canvas()
self.status_label.config(text="图片已重置")
def pick_color_at(self, canvas_x, canvas_y):
"""在指定位置取色"""
img_x = int((canvas_x - self.offset_x) / self.zoom)
img_y = int((canvas_y - self.offset_y) / self.zoom)
if (img_x < 0 or img_x >= self.display_image.width or img_y < 0 or img_y >= self.display_image.height):
return
color = self.display_image.getpixel((img_x, img_y))
if isinstance(color, int):
color = (color, color, color)
color_rgb = color[:3]
self.picked_color = color_rgb
hex_color = f"#{color_rgb[0]:02X}{color_rgb[1]:02X}{color_rgb[2]:02X}"
self.picked_color_preview.config(bg=hex_color)
self.picked_color_info.config(
text=f"RGB({color_rgb[0]}, {color_rgb[1]}, {color_rgb[2]})\nHEX: {hex_color}\n坐标:({img_x}, {img_y})",
fg="black"
)
self.status_label.config(text=f"已取色:RGB({color_rgb[0]}, {color_rgb[1]}, {color_rgb[2]}) - 点击'使用此颜色'应用")
def use_picked_color(self):
"""使用取色器获取的颜色作为画笔颜色"""
if self.picked_color is None:
self.status_label.config(text="请先使用取色器获取颜色")
return
hex_color = f"#{self.picked_color[0]:02X}{self.picked_color[1]:02X}{self.picked_color[2]:02X}"
self.brush_color = hex_color
self.color_btn.config(bg=hex_color)
self.set_tool('brush')
self.status_label.config(text=f"已应用取色:RGB({self.picked_color[0]}, {self.picked_color[1]}, {self.picked_color[2]})")
def copy_picked_color_code(self):
"""复制取色器颜色代码到剪贴板"""
if self.picked_color is None:
self.status_label.config(text="请先使用取色器获取颜色")
return
r, g, b = self.picked_color
hex_color = f"#{r:02X}{g:02X}{b:02X}"
rgb_format = f"rgb({r}, {g}, {b})"
color_codes = f"{hex_color} {rgb_format}"
self.root.clipboard_clear()
self.root.clipboard_append(color_codes)
self.status_label.config(text=f"已复制:{hex_color} {rgb_format}")
def set_tool(self, tool):
"""切换工具模式"""
self.tool_mode = tool
self.pan_tool_btn.config(bg="#95a5a6")
self.brush_tool_btn.config(bg="#95a5a6")
if hasattr(self, 'picker_tool_btn'):
self.picker_tool_btn.config(bg="#95a5a6")
if tool == 'brush':
self.brush_tool_btn.config(bg="#4CAF50")
self.canvas.config(cursor="crosshair")
self.status_label.config(text="已切换到画笔工具 - 点击拖动绘制")
elif tool == 'pan':
self.pan_tool_btn.config(bg="#4CAF50")
self.canvas.config(cursor="fleur")
self.status_label.config(text="已切换到移动工具 - 拖动移动图片")
elif tool == 'picker':
self.picker_tool_btn.config(bg="#4CAF50")
self.canvas.config(cursor="crosshair")
self.status_label.config(text="已切换到取色器 - 点击图片获取颜色")
def on_mouse_down(self, event):
"""左键按下 - 根据工具模式执行不同操作"""
if self.display_image is None:
return
if self.tool_mode == 'pan':
self.is_panning = True
self.pan_start_x = event.x
self.pan_start_y = event.y
self.pan_offset_x = self.offset_x
self.pan_offset_y = self.offset_y
elif self.tool_mode == 'picker':
self.pick_color_at(event.x, event.y)
else:
self.is_drawing = True
self.last_x = event.x
self.last_y = event.y
self.draw_at(event.x, event.y)
def on_mouse_drag(self, event):
"""左键拖动 - 根据工具模式执行不同操作"""
if self.display_image is None:
return
if self.tool_mode == 'pan' and self.is_panning:
dx = event.x - self.pan_start_x
dy = event.y - self.pan_start_y
self.offset_x = self.pan_offset_x + dx
self.offset_y = self.pan_offset_y + dy
self.display_image_on_canvas()
elif self.tool_mode == 'brush' and self.is_drawing:
self.draw_line(self.last_x, self.last_y, event.x, event.y)
self.last_x = event.x
self.last_y = event.y
def on_mouse_up(self, event):
"""左键释放 - 结束当前操作"""
if self.tool_mode == 'pan':
self.is_panning = False
else:
self.is_drawing = False
self.last_x = None
self.last_y = None
if self.display_image and self.zoom >= 5.0:
self.canvas.delete("paint")
self.display_image_on_canvas()
def update_image_display_region(self, img_x, img_y, brush_size):
"""更新图片显示的特定区域(用于高倍放大时的实时反馈)"""
self.canvas.delete("image")
display_width = int(self.display_image.width * self.zoom)
display_height = int(self.display_image.height * self.zoom)
resized = self.display_image.resize((display_width, display_height), Image.Resampling.NEAREST)
if resized.mode == "RGBA":
white_bg = Image.new("RGB", resized.size, (255, 255, 255))
white_bg.paste(resized, mask=resized.split()[3])
resized = white_bg
self.photo = ImageTk.PhotoImage(resized)
self.canvas.create_image(
self.offset_x, self.offset_y, anchor=tk.NW, image=self.photo, tags="image"
)
if self.show_grid:
self.canvas.delete("grid")
self.draw_pixel_grid()
def canvas_to_image_coords(self, canvas_x, canvas_y):
"""画布坐标转图片坐标(精确到像素)"""
img_x = int((canvas_x - self.offset_x) / self.zoom)
img_y = int((canvas_y - self.offset_y) / self.zoom)
return img_x, img_y
def canvas_to_image_coords_exact(self, canvas_x, canvas_y):
"""画布坐标转图片坐标(精确位置,用于像素级编辑)"""
if self.zoom >= 10.0:
pixel_size = self.zoom
relative_x = canvas_x - self.offset_x
relative_y = canvas_y - self.offset_y
img_x = round((relative_x + pixel_size / 2) / pixel_size)
img_y = round((relative_y + pixel_size / 2) / pixel_size)
else:
img_x = int((canvas_x - self.offset_x) / self.zoom)
img_y = int((canvas_y - self.offset_y) / self.zoom)
return img_x, img_y
def draw_at(self, canvas_x, canvas_y):
"""在指定位置绘制"""
img_x, img_y = self.canvas_to_image_coords_exact(canvas_x, canvas_y)
if (img_x < 0 or img_x >= self.display_image.width or img_y < 0 or img_y >= self.display_image.height):
return
if self.zoom >= 10.0 and self.brush_size == 1:
actual_brush_size = 1
else:
actual_brush_size = max(1, int(self.brush_size / self.zoom))
color_rgb = self.hex_to_rgb(self.brush_color)
display_brush_size = max(1, int(self.brush_size))
self.canvas.create_oval(
canvas_x - display_brush_size // 2,
canvas_y - display_brush_size // 2,
canvas_x + display_brush_size // 2,
canvas_y + display_brush_size // 2,
fill=self.brush_color, tags="paint"
)
draw = ImageDraw.Draw(self.display_image)
if actual_brush_size == 1:
if img_x >= 0 and img_x < self.display_image.width and img_y >= 0 and img_y < self.display_image.height:
self.display_image.putpixel((img_x, img_y), color_rgb)
else:
draw.ellipse(
[
img_x - actual_brush_size // 2,
img_y - actual_brush_size // 2,
img_x + actual_brush_size // 2,
img_y + actual_brush_size // 2
],
fill=color_rgb
)
if self.zoom >= 10.0:
self.update_image_display_region(img_x, img_y, actual_brush_size)
elif self.zoom >= 5.0:
if hasattr(self, '_update_counter'):
self._update_counter += 1
else:
self._update_counter = 0
if self._update_counter % 3 == 0:
self.display_image_on_canvas()
def draw_line(self, x1, y1, x2, y2):
"""在两点之间绘制连续线条"""
distance = ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5
steps = max(1, int(distance))
for i in range(steps + 1):
t = i / steps if steps > 0 else 0
x = x1 + (x2 - x1) * t
y = y1 + (y2 - y1) * t
self.draw_at(x, y)
def hex_to_rgb(self, hex_color):
"""十六进制颜色转 RGB 元组"""
hex_color = hex_color.lstrip('#')
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
def on_zoom(self, event):
"""滚轮缩放"""
if self.display_image is None:
return
x = self.canvas.canvasx(event.x)
y = self.canvas.canvasy(event.y)
img_x = int((x - self.offset_x) / self.zoom)
img_y = int((y - self.offset_y) / self.zoom)
if event.num == 5 or event.delta < 0:
new_zoom = max(0.1, self.zoom * 0.9)
else:
max_zoom = 100.0
if self.display_image:
if self.display_image.width <= 112 and self.display_image.height <= 112:
max_zoom = 500.0
elif self.display_image.width < 200 and self.display_image.height < 200:
max_zoom = 300.0
new_zoom = min(max_zoom, self.zoom * 1.1)
self.zoom = new_zoom
canvas_width = self.canvas.winfo_width()
canvas_height = self.canvas.winfo_height()
display_width = int(self.display_image.width * self.zoom)
display_height = int(self.display_image.height * self.zoom)
self.offset_x = x - img_x * self.zoom
self.offset_y = y - img_y * self.zoom
max_offset_x = display_width - 50
max_offset_y = display_height - 50
self.offset_x = max(-max_offset_x, min(canvas_width - 50, self.offset_x))
self.offset_y = max(-max_offset_y, min(canvas_height - 50, self.offset_y))
self.display_image_on_canvas()
self.size_info_label.config(
text=f"{self.display_image.size[0]} x {self.display_image.size[1]} px | 缩放:{int(self.zoom * 100)}%"
)
def save_image(self):
"""保存图片"""
if self.display_image is None:
self.status_label.config(text="请先加载图片")
return
file_path = filedialog.asksaveasfilename(
title="保存图片",
defaultextension=".png",
filetypes=[
("PNG", "*.png"),
("JPEG", "*.jpg"),
("所有文件", "*.*")
]
)
if file_path:
try:
if file_path.lower().endswith(('.jpg', '.jpeg')):
rgb_image = Image.new("RGB", self.display_image.size, (255, 255, 255))
rgb_image.paste(self.display_image, mask=self.display_image.split()[3] if self.display_image.mode == "RGBA" else None)
rgb_image.save(file_path, quality=95)
else:
self.display_image.save(file_path)
self.status_label.config(text=f"已保存:{os.path.basename(file_path)}")
except Exception as e:
self.status_label.config(text=f"保存失败:{str(e)}")
def on_drag_enter(self, event):
"""拖拽进入"""
event.action = "copy"
def on_drop(self, event):
"""拖拽放下"""
files = event.data
file_list = []
if files.startswith('{') and files.endswith('}'):
import re
file_list = re.findall(r'\{([^{}]+)\}', files)
elif '{' in files or '}' in files:
files = files.replace('{', '').replace('}', '')
file_list = [files]
else:
file_list = [files]
for file_path in file_list:
file_path = file_path.strip().strip('"').strip("'")
if os.path.isfile(file_path):
ext = os.path.splitext(file_path)[1].lower()
if ext in ['.png', '.jpg', '.jpeg', '.bmp', '.gif', '.tiff', '.webp']:
self.open_image(file_path)
break
def refresh_color_list(self):
"""刷新颜色列表 - 统计图片中所有颜色"""
if self.display_image is None:
return
self.color_stats = {}
self.color_listbox.delete(0, tk.END)
width, height = self.display_image.size
for y in range(height):
for x in range(width):
color = self.display_image.getpixel((x, y))
if isinstance(color, int):
color = (color, color, color)
color = color[:3]
self.color_stats[color] = self.color_stats.get(color, 0) + 1
sorted_colors = sorted(self.color_stats.items(), key=lambda x: x[1], reverse=True)
unique_colors = len(sorted_colors)
total_pixels = width * height
for i, (color_rgb, count) in enumerate(sorted_colors):
hex_color = f"#{color_rgb[0]:02X}{color_rgb[1]:02X}{color_rgb[2]:02X}"
percent = (count / total_pixels) * 100
color_block = "■"
display_text = f"{color_block} RGB({color_rgb[0]},{color_rgb[1]},{color_rgb[2]}) - {count}px ({percent:.1f}%)"
self.color_listbox.insert(tk.END, display_text)
brightness = (color_rgb[0] * 299 + color_rgb[1] * 587 + color_rgb[2] * 114) / 1000
self.color_listbox.itemconfig(i, bg=hex_color)
if brightness > 128:
self.color_listbox.itemconfig(i, fg="#000000")
else:
self.color_listbox.itemconfig(i, fg="#FFFFFF")
self.stats_label.config(
text=f"总像素:{total_pixels}\n独特颜色:{unique_colors}"
)
def on_color_select(self, event):
"""颜色选择事件(支持多选)"""
selection = self.color_listbox.curselection()
sorted_colors = sorted(self.color_stats.items(), key=lambda x: x[1], reverse=True)
self.selected_colors = []
for index in selection:
if index < len(sorted_colors):
color_rgb = sorted_colors[index][0]
self.selected_colors.append(color_rgb)
if len(self.selected_colors) == 0:
self.status_label.config(text="未选择颜色")
elif len(self.selected_colors) == 1:
c = self.selected_colors[0]
self.status_label.config(
text=f"选中 1 个颜色:RGB({c[0]},{c[1]},{c[2]}) - {self.color_stats[c]} 像素"
)
else:
total_pixels = sum(self.color_stats[c] for c in self.selected_colors)
self.status_label.config(
text=f"选中 {len(self.selected_colors)} 个颜色 - 共 {total_pixels} 像素"
)
if self.highlight_var.get():
self.display_image_on_canvas()
def toggle_highlight(self):
"""切换高亮显示"""
self.show_highlight = self.highlight_var.get()
if self.display_image:
self.display_image_on_canvas()
def select_all_colors(self):
"""全选所有颜色"""
self.color_listbox.selection_set(0, tk.END)
self.on_color_select(None)
def clear_color_selection(self):
"""清空颜色选择"""
self.color_listbox.selection_clear(0, tk.END)
self.selected_colors = []
self.status_label.config(text="已清空颜色选择")
if self.highlight_var.get():
self.display_image_on_canvas()
def invert_color_selection(self):
"""反选颜色"""
total = self.color_listbox.size()
for i in range(total):
if self.color_listbox.selection_includes(i):
self.color_listbox.selection_clear(i)
else:
self.color_listbox.selection_set(i)
self.on_color_select(None)
def replace_color(self):
"""批量替换选中的颜色(支持多选)"""
if self.display_image is None or not self.selected_colors:
self.status_label.config(text="请先加载图片并选择颜色")
return
new_color = colorchooser.askcolor(
title="选择替换后的颜色",
initialcolor="#FF0000"
)[1]
if not new_color:
return
new_rgb = self.hex_to_rgb(new_color)
old_colors = self.selected_colors.copy()
width, height = self.display_image.size
replaced_count = 0
for y in range(height):
for x in range(width):
pixel_color = self.display_image.getpixel((x, y))
if isinstance(pixel_color, int):
pixel_color = (pixel_color, pixel_color, pixel_color)
pixel_color = pixel_color[:3]
if pixel_color in old_colors:
self.display_image.putpixel((x, y), new_rgb)
replaced_count += 1
self.display_image_on_canvas()
self.status_label.config(
text=f"已替换 {replaced_count} 像素 ({len(old_colors)} 种颜色) -> RGB({new_rgb[0]},{new_rgb[1]},{new_rgb[2]})"
)
self.refresh_color_list()
def replace_with_picked_color(self):
"""使用取色器颜色替换选中的颜色"""
if self.display_image is None or not self.selected_colors:
self.status_label.config(text="请先加载图片并选择颜色")
return
if self.picked_color is None:
self.status_label.config(text="请先使用取色器获取颜色")
return
new_rgb = self.picked_color
old_colors = self.selected_colors.copy()
width, height = self.display_image.size
replaced_count = 0
for y in range(height):
for x in range(width):
pixel_color = self.display_image.getpixel((x, y))
if isinstance(pixel_color, int):
pixel_color = (pixel_color, pixel_color, pixel_color)
pixel_color = pixel_color[:3]
if pixel_color in old_colors:
self.display_image.putpixel((x, y), new_rgb)
replaced_count += 1
self.display_image_on_canvas()
self.status_label.config(
text=f"已替换 {replaced_count} 像素 ({len(old_colors)} 种颜色) -> 取色器颜色 RGB({new_rgb[0]},{new_rgb[1]},{new_rgb[2]})"
)
self.refresh_color_list()
def replace_with_transparent(self):
"""将选中的颜色设置为透明"""
if self.display_image is None or not self.selected_colors:
self.status_label.config(text="请先加载图片并选择颜色")
return
old_colors = self.selected_colors.copy()
transparent_color = (0, 0, 0, 0)
width, height = self.display_image.size
replaced_count = 0
for y in range(height):
for x in range(width):
pixel_color = self.display_image.getpixel((x, y))
if isinstance(pixel_color, int):
pixel_color = (pixel_color, pixel_color, pixel_color)
pixel_color_rgb = pixel_color[:3]
if pixel_color_rgb in old_colors:
self.display_image.putpixel((x, y), transparent_color)
replaced_count += 1
self.display_image_on_canvas()
self.status_label.config(
text=f"已设置 {replaced_count} 像素 ({len(old_colors)} 种颜色) 为透明"
)
self.refresh_color_list()
def main():
try:
from tkinterdnd2 import TkinterDnD
root = TkinterDnD.Tk()
except ImportError:
root = tk.Tk()
app = ImageEditor(root)
root.mainloop()
if __name__ == "__main__":
main()