PictureEdit/canvas_widget.py
2025-09-02 18:14:12 +08:00

278 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import tkinter as tk
from tkinter import ttk
from PIL import Image, ImageTk
from image_manager import ImageManager
from transform_utils import TransformHelper
import cv2
class CanvasWidget(tk.Canvas):
"""自定义画布组件,处理绘图和交互"""
def __init__(self, parent, project,main_window, *args, **kwargs):
super().__init__(parent, *args, **kwargs, bg="#f0f0f0", highlightthickness=1, highlightbackground="#ccc")
self.parent = parent # 这是Frame对象
self.main_window = main_window # 这是MainWindow对象
self.project = project
self.selected_item = None
self.dragging = False
self.drag_data = {"x": 0, "y": 0}
self.zoom = project.canvas_zoom
# 绑定事件
self.bind("<Button-1>", self.on_click)
self.bind("<B1-Motion>", self.on_drag)
self.bind("<ButtonRelease-1>", self.on_release)
self.bind("<MouseWheel>", self.on_zoom) # Windows
self.bind("<Button-4>", self.on_zoom) # Linux
self.bind("<Button-5>", self.on_zoom) # Linux
self.active_handle = None # 当前激活的控制点
# 图像缓存
self.image_cache = {}
# 初始绘制
self.draw_preview()
def canvas_to_template(self, x, y):
"""将画布坐标转换为模板坐标"""
scale = self.zoom
template_x = x / scale
template_y = y / scale
return template_x, template_y
def template_to_canvas(self, x, y):
"""将模板坐标转换为画布坐标"""
scale = self.zoom
canvas_x = x * scale
canvas_y = y * scale
return canvas_x, canvas_y
# 修改 canvas_widget.py 中的 get_foreground_at 方法,优化碰撞检测
def get_foreground_at(self, x, y):
"""获取指定坐标处的前景图,优化碰撞检测"""
template_x, template_y = self.canvas_to_template(x, y)
# 按图层顺序倒序检查(顶层优先)
for fg in sorted(self.project.foregrounds, key=lambda f: -f.z_index):
if not fg.visible:
continue
# 更精确的碰撞检测(考虑变换后的顶点)
min_x = min(v[0] for v in fg.vertices)
max_x = max(v[0] for v in fg.vertices)
min_y = min(v[1] for v in fg.vertices)
max_y = max(v[1] for v in fg.vertices)
if min_x <= template_x <= max_x and min_y <= template_y <= max_y:
return fg
return None
# 修改 canvas_widget.py 中的 on_click 方法,添加控制点检测
def on_click(self, event):
"""处理鼠标点击事件添加1. 检查是否点击了控制点
2. 检查是否点击了图片
3. 处理选择状态
"""
self.active_handle = None
# 先检查是否点击了已选中项的控制点
if self.selected_item:
handles = TransformHelper.calculate_handles(self.selected_item, self)
for handle in handles.values():
if handle.hit_test(event.x, event.y):
self.active_handle = handle.position
# 保持选中状态
self.dragging = True
self.drag_data["x"] = event.x
self.drag_data["y"] = event.y
self.draw_preview()
return
# 2. 检查是否点击了图片
fg = self.get_foreground_at(event.x, event.y)
if fg and not fg.locked:
self.selected_item = fg
self.drag_data["x"] = event.x
self.drag_data["y"] = event.y
self.dragging = True
self.main_window.update_property_panel(fg)
else:
self.selected_item = None
self.main_window.clear_property_panel()
self.draw_preview()
def on_drag(self, event):
"""处理拖拽事件"""
if self.dragging and self.selected_item:
if self.active_handle:
# 拖拽控制点
dx = event.x - self.drag_data["x"]
dy = event.y - self.drag_data["y"]
self.selected_item = TransformHelper.update_vertices(
self.selected_item, self.active_handle, dx, dy, self
)
else:
# 拖拽整个图片
dx = event.x - self.drag_data["x"]
dy = event.y - self.drag_data["y"]
t_dx, t_dy = self.canvas_to_template(dx, dy)
# 移动所有顶点
new_vertices = []
for x, y in self.selected_item.vertices:
new_vertices.append((x + t_dx, y + t_dy))
self.selected_item.vertices = new_vertices
# 更新拖拽起始位置
self.drag_data["x"] = event.x
self.drag_data["y"] = event.y
# 重绘
self.draw_preview()
# 更新属性面板
self.main_window.update_property_panel(self.selected_item)
def on_release(self, event):
"""处理鼠标释放事件"""
self.dragging = False
self.active_handle = None
def on_zoom(self, event):
"""处理缩放事件"""
# 计算缩放因子
if event.delta > 0 or event.num == 4: # 放大
self.zoom *= 1.1
else: # 缩小
self.zoom /= 1.1
# 限制缩放范围
self.zoom = max(0.1, min(self.zoom, 5.0))
# 更新项目的缩放值
self.project.canvas_zoom = self.zoom
# 重绘
self.draw_preview()
def draw_preview(self):
"""绘制预览内容"""
self.delete("all")
# 获取模板尺寸
template_w = self.project.template.width_px
template_h = self.project.template.height_px
# 计算画布上的模板尺寸
canvas_w = template_w * self.zoom
canvas_h = template_h * self.zoom
# 调整画布滚动区域
self.config(scrollregion=(0, 0, canvas_w, canvas_h))
# 绘制模板背景
self.create_rectangle(0, 0, canvas_w, canvas_h, fill=self.project.template.bg_color)
# 绘制背景图(如果有)
if self.project.template.bg_image:
# 使用更稳定的缓存键,包含图像的标识和尺寸
bg_key = (id(self.project.template.bg_image), template_w, template_h, self.zoom)
if bg_key not in self.image_cache or self.image_cache[bg_key][0] != self.zoom:
# 缩放背景图并缓存
scaled_bg = self.project.template.bg_image.resize(
(int(canvas_w), int(canvas_h)),
Image.Resampling.LANCZOS
)
self.image_cache[bg_key] = (self.zoom, ImageTk.PhotoImage(scaled_bg))
bg_img = self.image_cache[bg_key][1]
self.create_image(0, 0, image=bg_img, anchor=tk.NW)
# 按图层顺序绘制前景图
for fg in sorted(self.project.foregrounds, key=lambda f: f.z_index):
# 跳过隐藏的图片
if fg.hidden or not fg.visible:
continue
try:
# 检查缓存 (使用顶点元组作为键的一部分)
img_key = (fg.file_path, tuple(tuple(v) for v in fg.vertices), fg.angle, fg.mask_shape, self.zoom)
if img_key not in self.image_cache:
# 加载并处理图像
img = ImageManager.load_image(fg.file_path)
if img:
# 应用遮罩
masked_img = ImageManager.apply_mask(img, fg.mask_shape)
# 使用基于顶点的图像变形方法
transformed_img, (x, y) = ImageManager.transform_with_vertices(
masked_img, fg.vertices
)
# 确保图像具有透明度
if transformed_img.mode != 'RGBA':
transformed_img = transformed_img.convert('RGBA')
# 缩放到预览尺寸
scaled_img = transformed_img.resize(
(max(1, int(transformed_img.width * self.zoom)),
max(1, int(transformed_img.height * self.zoom))),
Image.Resampling.LANCZOS
)
# 缓存图像
self.image_cache[img_key] = ImageTk.PhotoImage(scaled_img)
# 获取画布坐标
min_x = min(v[0] for v in fg.vertices)
min_y = min(v[1] for v in fg.vertices)
canvas_x, canvas_y = self.template_to_canvas(min_x, min_y)
# 绘制图像
if img_key in self.image_cache:
img = self.image_cache[img_key]
# 使用包围盒左上角坐标绘制图像
self.create_image(canvas_x, canvas_y, image=img, anchor=tk.NW)
# 如果是选中状态,绘制边框
if self.selected_item == fg:
# 绘制变形后的边框
canvas_vertices = [
self.template_to_canvas(x, y)
for x, y in fg.vertices
]
# 绘制边框
self.create_polygon(
canvas_vertices,
outline="blue", width=2, dash=(4, 2)
)
# 绘制控制点
handles = TransformHelper.calculate_handles(fg, self)
for handle in handles.values():
self.create_oval(
handle.x - handle.radius,
handle.y - handle.radius,
handle.x + handle.radius,
handle.y + handle.radius,
fill="white", outline="blue", width=2
)
except Exception as e:
print(f"绘制图像失败: {e}")
# 绘制一个占位矩形
min_x = min(v[0] for v in fg.vertices)
min_y = min(v[1] for v in fg.vertices)
canvas_x, canvas_y = self.template_to_canvas(min_x, min_y)
# 计算宽度和高度
width = max(v[0] for v in fg.vertices) - min_x
height = max(v[1] for v in fg.vertices) - min_y
canvas_w, canvas_h = self.template_to_canvas(width, height)
self.create_rectangle(
canvas_x, canvas_y,
canvas_x + canvas_w, canvas_y + canvas_h,
outline="red", fill="#ffeeee", dash=(2, 2)
)
self.create_text(
canvas_x + canvas_w / 2, canvas_y + canvas_h / 2,
text="无法加载", fill="red"
)