Initial commit: Python 图形编辑器 v1.0
This commit is contained in:
@@ -0,0 +1,343 @@
|
|||||||
|
# Python 绘图软件(图形编辑器)
|
||||||
|
|
||||||
|
一个功能强大的 Python 图形编辑器,类似简化版的 Photoshop,提供丰富的绘图和图像处理功能。
|
||||||
|
|
||||||
|
## ✨ 功能特性
|
||||||
|
|
||||||
|
### 🎨 绘图工具
|
||||||
|
- **画笔工具** - 可调节大小和颜色的自由绘画
|
||||||
|
- **铅笔工具** - 精细的像素级绘图
|
||||||
|
- **橡皮擦** - 可调节大小的擦除工具
|
||||||
|
- **直线工具** - 绘制直线段
|
||||||
|
- **矩形工具** - 绘制矩形和正方形
|
||||||
|
- **椭圆工具** - 绘制圆形和椭圆
|
||||||
|
- **填充工具** - 智能区域填充(洪水填充算法)
|
||||||
|
- **取色器** - 从图像中吸取颜色
|
||||||
|
|
||||||
|
### 🖼️ 图层管理
|
||||||
|
- 创建/删除图层
|
||||||
|
- 图层可见性控制
|
||||||
|
- 图层上下移动
|
||||||
|
- 图层选择
|
||||||
|
- 透明图层支持
|
||||||
|
|
||||||
|
### 🌈 滤镜效果
|
||||||
|
- **高斯模糊** - 柔和模糊效果
|
||||||
|
- **锐化** - 增强图像细节
|
||||||
|
- **边缘检测** - 提取图像轮廓
|
||||||
|
- **浮雕** - 3D 浮雕效果
|
||||||
|
|
||||||
|
### 🎛️ 图像调整
|
||||||
|
- **亮度调整** - 增强/减弱亮度
|
||||||
|
- **对比度调整** - 调整对比度
|
||||||
|
- **饱和度调整** - 调整色彩饱和度
|
||||||
|
|
||||||
|
### 📁 文件操作
|
||||||
|
- 新建画布
|
||||||
|
- 打开图像文件(支持 PNG、JPG、BMP、GIF)
|
||||||
|
- 保存为多种格式
|
||||||
|
- 导出功能
|
||||||
|
- 撤销/重做(最多 20 步)
|
||||||
|
|
||||||
|
### 🎯 其他功能
|
||||||
|
- 自定义画笔大小(1-100)
|
||||||
|
- 颜色选择器
|
||||||
|
- 实时状态显示
|
||||||
|
- 800x600 画布尺寸
|
||||||
|
|
||||||
|
## 📦 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements_draw.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
主要依赖:
|
||||||
|
- **Pillow** - 图像处理
|
||||||
|
- **numpy** - 数值计算(用于填充算法)
|
||||||
|
- **tkinter** - GUI 界面(Python 自带)
|
||||||
|
|
||||||
|
## 🚀 快速开始
|
||||||
|
|
||||||
|
### 方法 1:运行主程序
|
||||||
|
```bash
|
||||||
|
python draw_app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方法 2:直接运行编辑器
|
||||||
|
```bash
|
||||||
|
python graphic_editor.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📖 使用指南
|
||||||
|
|
||||||
|
### 基本绘图流程
|
||||||
|
|
||||||
|
1. **选择工具**
|
||||||
|
- 点击左侧工具栏选择需要的工具
|
||||||
|
- 画笔/铅笔用于自由绘画
|
||||||
|
- 形状工具用于绘制几何图形
|
||||||
|
|
||||||
|
2. **设置颜色**
|
||||||
|
- 点击颜色框打开颜色选择器
|
||||||
|
- 或使用取色器从画布取色
|
||||||
|
|
||||||
|
3. **调整画笔大小**
|
||||||
|
- 拖动滑块调整画笔大小
|
||||||
|
- 实时显示当前大小数值
|
||||||
|
|
||||||
|
4. **开始绘画**
|
||||||
|
- 在画布上按住鼠标左键并拖动
|
||||||
|
- 绘制形状时释放鼠标完成绘制
|
||||||
|
|
||||||
|
### 图层操作
|
||||||
|
|
||||||
|
1. **创建新图层**
|
||||||
|
- 点击右侧图层面板的 "+" 按钮
|
||||||
|
- 新图层将出现在图层列表顶部
|
||||||
|
|
||||||
|
2. **选择图层**
|
||||||
|
- 点击图层列表中的图层名称
|
||||||
|
- 当前选中图层前有 ▶️ 标记
|
||||||
|
|
||||||
|
3. **显示/隐藏图层**
|
||||||
|
- 点击 👁️ 按钮切换可见性
|
||||||
|
- 隐藏的图层显示为 ❌
|
||||||
|
|
||||||
|
4. **调整图层顺序**
|
||||||
|
- 使用 ⬆️ 和 ⬇️ 按钮上下移动
|
||||||
|
- 上层图层会遮挡下层
|
||||||
|
|
||||||
|
5. **删除图层**
|
||||||
|
- 选择图层后点击 "-" 按钮
|
||||||
|
- 至少保留一个图层
|
||||||
|
|
||||||
|
### 应用滤镜
|
||||||
|
|
||||||
|
1. 选择要处理的图层
|
||||||
|
2. 在右侧滤镜面板选择滤镜
|
||||||
|
3. 滤镜会立即应用到当前图像
|
||||||
|
4. 可使用撤销功能恢复
|
||||||
|
|
||||||
|
### 图像调整
|
||||||
|
|
||||||
|
1. 选择要调整的图层
|
||||||
|
2. 在调整面板选择调整类型
|
||||||
|
3. 每次点击会增强效果(1.2 倍)
|
||||||
|
4. 可多次点击累加效果
|
||||||
|
|
||||||
|
### 撤销和重做
|
||||||
|
|
||||||
|
- **撤销** - 点击 "↩️ 撤销" 按钮
|
||||||
|
- **重做** - 点击 "↪️ 重做" 按钮
|
||||||
|
- 最多支持 20 步历史记录
|
||||||
|
|
||||||
|
## 💡 使用技巧
|
||||||
|
|
||||||
|
### 1. 精确绘图
|
||||||
|
- 使用铅笔工具进行像素级精确绘图
|
||||||
|
- 放大图像可获得更好的控制
|
||||||
|
|
||||||
|
### 2. 颜色搭配
|
||||||
|
- 使用取色器快速匹配颜色
|
||||||
|
- 常用颜色可以记录下来重复使用
|
||||||
|
|
||||||
|
### 3. 图层策略
|
||||||
|
- 背景放在最底层
|
||||||
|
- 主要元素放在中间层
|
||||||
|
- 细节和特效放在顶层
|
||||||
|
- 不同元素使用不同图层便于修改
|
||||||
|
|
||||||
|
### 4. 填充技巧
|
||||||
|
- 填充区域需要是封闭的
|
||||||
|
- 颜色差异过大会影响填充效果
|
||||||
|
- 可先用画笔勾勒轮廓再填充
|
||||||
|
|
||||||
|
### 5. 滤镜使用
|
||||||
|
- 轻微模糊可以平滑图像
|
||||||
|
- 锐化适合处理模糊的照片
|
||||||
|
- 边缘检测可用于提取线稿
|
||||||
|
- 滤镜效果可以叠加使用
|
||||||
|
|
||||||
|
## 🔧 技术架构
|
||||||
|
|
||||||
|
### 核心类
|
||||||
|
|
||||||
|
- **GraphicEditor** - 主编辑器类
|
||||||
|
- 管理所有 UI 组件
|
||||||
|
- 处理用户输入
|
||||||
|
- 协调各模块工作
|
||||||
|
|
||||||
|
- **Layer** - 图层数据类
|
||||||
|
- 存储图层图像数据
|
||||||
|
- 管理可见性和透明度
|
||||||
|
- 图层 ID 和名称
|
||||||
|
|
||||||
|
- **ToolType** - 工具枚举
|
||||||
|
- 定义所有可用工具
|
||||||
|
- 工具类型标识
|
||||||
|
|
||||||
|
### 关键技术
|
||||||
|
|
||||||
|
1. **Tkinter GUI**
|
||||||
|
- 跨平台 GUI 框架
|
||||||
|
- 事件驱动编程
|
||||||
|
- 画布组件绘图
|
||||||
|
|
||||||
|
2. **Pillow 图像处理**
|
||||||
|
- 图像创建和保存
|
||||||
|
- 滤镜应用
|
||||||
|
- 图像增强
|
||||||
|
|
||||||
|
3. **NumPy 数组操作**
|
||||||
|
- 像素级图像处理
|
||||||
|
- 洪水填充算法
|
||||||
|
- 颜色计算
|
||||||
|
|
||||||
|
4. **洪水填充算法**
|
||||||
|
- BFS(广度优先搜索)实现
|
||||||
|
- 颜色容差处理
|
||||||
|
- 性能优化
|
||||||
|
|
||||||
|
## 📊 性能说明
|
||||||
|
|
||||||
|
### 画布尺寸
|
||||||
|
- 默认:800x600 像素
|
||||||
|
- 可根据需要修改
|
||||||
|
|
||||||
|
### 内存占用
|
||||||
|
- 空画布:约 2MB
|
||||||
|
- 复杂图像:10-20MB
|
||||||
|
- 图层数量:建议不超过 20 个
|
||||||
|
|
||||||
|
### 响应性
|
||||||
|
- 画笔绘制:实时响应
|
||||||
|
- 滤镜应用:1-3 秒
|
||||||
|
- 文件保存:1-2 秒
|
||||||
|
|
||||||
|
## 🐛 已知限制
|
||||||
|
|
||||||
|
1. 不支持图层混合模式
|
||||||
|
2. 不支持文字工具(已预留接口)
|
||||||
|
3. 不支持选区工具
|
||||||
|
4. 不支持变换(缩放、旋转)
|
||||||
|
5. 滤镜效果不可调节参数
|
||||||
|
|
||||||
|
## 🚀 未来扩展
|
||||||
|
|
||||||
|
### 计划功能
|
||||||
|
- [ ] 文字工具
|
||||||
|
- [ ] 选区工具
|
||||||
|
- [ ] 图层混合模式
|
||||||
|
- [ ] 图像变换(缩放、旋转、裁剪)
|
||||||
|
- [ ] 可调节参数的滤镜
|
||||||
|
- [ ] 自定义画布尺寸
|
||||||
|
- [ ] 渐变填充
|
||||||
|
- [ ] 图案填充
|
||||||
|
- [ ] 画笔形状自定义
|
||||||
|
- [ ] 压力感应支持
|
||||||
|
|
||||||
|
### 性能优化
|
||||||
|
- [ ] 多线程处理
|
||||||
|
- [ ] GPU 加速
|
||||||
|
- [ ] 图像分块处理
|
||||||
|
- [ ] 内存优化
|
||||||
|
|
||||||
|
## 📝 示例项目
|
||||||
|
|
||||||
|
### 示例 1:简单绘图
|
||||||
|
```python
|
||||||
|
# 运行程序
|
||||||
|
python draw_app.py
|
||||||
|
|
||||||
|
# 1. 选择画笔工具
|
||||||
|
# 2. 选择颜色
|
||||||
|
# 3. 在画布上绘画
|
||||||
|
# 4. 保存为 PNG
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例 2:多层绘图
|
||||||
|
```python
|
||||||
|
# 1. 在背景层绘制底色
|
||||||
|
# 2. 新建图层绘制主体
|
||||||
|
# 3. 再新建图层添加细节
|
||||||
|
# 4. 调整图层顺序
|
||||||
|
# 5. 合并导出
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例 3:应用滤镜
|
||||||
|
```python
|
||||||
|
# 1. 打开图片
|
||||||
|
# 2. 应用高斯模糊
|
||||||
|
# 3. 应用边缘检测
|
||||||
|
# 4. 调整对比度
|
||||||
|
# 5. 保存结果
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎓 学习资源
|
||||||
|
|
||||||
|
### Tkinter 文档
|
||||||
|
- 官方文档:https://docs.python.org/3/library/tkinter.html
|
||||||
|
- Tkinter 教程:https://www.pythontutorial.net/tkinter/
|
||||||
|
|
||||||
|
### Pillow 文档
|
||||||
|
- 官方文档:https://pillow.readthedocs.io/
|
||||||
|
- 图像处理教程:https://pillow.readthedocs.io/en/stable/handbook/tutorial.html
|
||||||
|
|
||||||
|
### NumPy 文档
|
||||||
|
- 官方文档:https://numpy.org/doc/
|
||||||
|
- 数组操作指南:https://numpy.org/doc/stable/user/absolute_beginners.html
|
||||||
|
|
||||||
|
## 🤝 贡献
|
||||||
|
|
||||||
|
欢迎提交问题和功能建议!
|
||||||
|
|
||||||
|
## 📄 许可证
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
## 📧 联系方式
|
||||||
|
|
||||||
|
如有问题,请通过 Gitea 仓库提交 issue。
|
||||||
|
|
||||||
|
## 🖼️ 界面预览
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ Python 图形编辑器 │
|
||||||
|
├──────────┬──────────────────────────┬──────────────────┤
|
||||||
|
│ 工具 │ │ 图层 │
|
||||||
|
│ 🖌️ 画笔 │ │ ▶️👁️ 图层 1 │
|
||||||
|
│ ✏️ 铅笔 │ 画布区域 │ 👁️ 背景 │
|
||||||
|
│ 🧹 橡皮 │ (800x600) │ │
|
||||||
|
│ 📏 直线 │ │ + - 👁️ ⬆️ ⬇️ │
|
||||||
|
│ ⬜ 矩形 │ ├──────────────────┤
|
||||||
|
│ ⭕ 椭圆 │ │ 滤镜 │
|
||||||
|
│ 🪣 填充 │ │ 🌫️ 高斯模糊 │
|
||||||
|
│ 💉 取色 │ │ 🔍 锐化 │
|
||||||
|
├──────────┤ │ 🌓 边缘检测 │
|
||||||
|
│ 颜色 │ │ 🎨 浮雕 │
|
||||||
|
│ [颜色框] │ ├──────────────────┤
|
||||||
|
│ 大小 │ │ 图像调整 │
|
||||||
|
│ [滑块] │ │ ☀️ 亮度 │
|
||||||
|
├──────────┤ │ 🎭 对比度 │
|
||||||
|
│ 操作 │ │ 🎨 饱和度 │
|
||||||
|
│ ↩️ 撤销 │ ├──────────────────┤
|
||||||
|
│ ↪️ 重做 │ │ 文件 │
|
||||||
|
│ 🗑️ 清除 │ │ 📂 新建 │
|
||||||
|
└──────────┴──────────────────────────┤ 💾 保存 │
|
||||||
|
│ 📤 导出 │
|
||||||
|
┌─────────────────────────────────────┴──────────────────┤
|
||||||
|
│ 状态栏:就绪 │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 总结
|
||||||
|
|
||||||
|
这是一个功能完善的 Python 图形编辑器,适合:
|
||||||
|
- 学习图像处理原理
|
||||||
|
- 快速原型设计
|
||||||
|
- 简单图像编辑
|
||||||
|
- 教学演示
|
||||||
|
- 个人绘画
|
||||||
|
|
||||||
|
享受绘图的乐趣!🎨✨
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
"""
|
||||||
|
Python 绘图软件 - 图形编辑器
|
||||||
|
主入口文件
|
||||||
|
"""
|
||||||
|
|
||||||
|
from graphic_editor import main
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -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()
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
pandas>=2.0.0
|
||||||
|
numpy>=1.24.0
|
||||||
|
matplotlib>=3.7.0
|
||||||
|
requests>=2.31.0
|
||||||
|
Pillow>=10.0.0
|
||||||
Reference in New Issue
Block a user