PictureEdit/canvas_widget.py

278 lines
11 KiB
Python
Raw Normal View History

2025-09-02 16:49:39 +08:00
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:
2025-09-02 18:14:12 +08:00
# 使用更稳定的缓存键,包含图像的标识和尺寸
bg_key = (id(self.project.template.bg_image), template_w, template_h, self.zoom)
2025-09-02 16:49:39 +08:00
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"
)