TxT2Docx/advanced_style_editor.py

636 lines
29 KiB
Python
Raw Normal View History

2025-09-26 15:49:06 +08:00
"""
高级样式编辑器模块
提供专业的样式编辑和实时预览功能让用户能够创建和定制个性化的文档样式
"""
import tkinter as tk
from tkinter import ttk, colorchooser, messagebox, simpledialog
from typing import Optional, Dict, Any
import copy
from style_manager import style_manager, DocumentStyle, FontStyle, ParagraphStyle
from config import Config
def open_advanced_editor(parent, style_name):
"""
打开高级样式编辑器
Args:
parent: 父窗口
style_name: 要编辑的样式名称
"""
if not style_name:
messagebox.showwarning("警告", "请先选择一个样式")
return None
# 获取样式
original_style = style_manager.get_style(style_name)
if not original_style:
messagebox.showerror("错误", f"找不到样式 '{style_name}'")
return None
editor = AdvancedStyleEditor(parent, original_style)
return editor
class AdvancedStyleEditor:
"""
高级样式编辑器类
"""
def __init__(self, parent, original_style):
self.parent = parent
self.original_style = original_style
self.current_style = copy.deepcopy(original_style)
# 确保样式对象完整
self._ensure_style_completeness()
# 创建编辑窗口
self.window = tk.Toplevel(parent)
self.window.title(f'高级样式编辑 - {self.current_style.name}')
self.window.geometry('1000x700')
self.window.transient(parent)
self.window.grab_set()
# 样式变量
self.style_vars = {}
self.heading_vars = {}
self._create_interface()
self._bind_events()
self._update_preview()
def _ensure_style_completeness(self):
"""确保样式对象的完整性"""
if not self.current_style.body_font:
self.current_style.body_font = FontStyle()
if not self.current_style.body_paragraph:
self.current_style.body_paragraph = ParagraphStyle()
if not self.current_style.heading_styles:
self.current_style.heading_styles = {}
# 确保至少有3级标题 - 使用正确的HeadingStyle对象
from style_manager import HeadingStyle
for level in range(1, 4):
if level not in self.current_style.heading_styles:
font_size = max(20 - level * 2, 12)
self.current_style.heading_styles[level] = HeadingStyle(
font=FontStyle(name="微软雅黑", size=font_size, bold=True),
paragraph=ParagraphStyle(line_spacing=1.3, space_before=12, space_after=6),
outline_level=level
)
def _create_interface(self):
"""创建界面"""
# 创建主布局
main_frame = ttk.Frame(self.window)
main_frame.pack(fill='both', expand=True, padx=10, pady=10)
# 左侧编辑区域
edit_frame = ttk.Frame(main_frame)
edit_frame.pack(side='left', fill='both', expand=True, padx=(0, 10))
# 右侧预览区域
preview_frame = ttk.LabelFrame(main_frame, text='实时预览', padding="10")
preview_frame.pack(side='right', fill='both', expand=True)
self._create_edit_area(edit_frame)
self._create_preview_area(preview_frame)
self._create_bottom_buttons()
def _create_edit_area(self, parent):
"""创建编辑区域"""
# 创建笔记本控件
notebook = ttk.Notebook(parent)
notebook.pack(fill='both', expand=True)
# 基本信息选项卡
basic_frame = ttk.Frame(notebook)
notebook.add(basic_frame, text='基本信息')
self._create_basic_tab(basic_frame)
# 正文样式选项卡
body_frame = ttk.Frame(notebook)
notebook.add(body_frame, text='正文样式')
self._create_body_tab(body_frame)
# 标题样式选项卡
heading_frame = ttk.Frame(notebook)
notebook.add(heading_frame, text='标题样式')
self._create_heading_tab(heading_frame)
def _create_basic_tab(self, parent):
"""创建基本信息选项卡"""
# 样式名称
name_frame = ttk.Frame(parent)
name_frame.pack(fill='x', padx=10, pady=5)
ttk.Label(name_frame, text='样式名称:', width=12).pack(side='left')
self.name_var = tk.StringVar(value=self.current_style.name)
name_entry = ttk.Entry(name_frame, textvariable=self.name_var)
name_entry.pack(side='left', fill='x', expand=True, padx=(5, 0))
# 样式描述
desc_frame = ttk.Frame(parent)
desc_frame.pack(fill='x', padx=10, pady=5)
ttk.Label(desc_frame, text='描述:', width=12).pack(side='left')
self.desc_var = tk.StringVar(value=self.current_style.description)
desc_entry = ttk.Entry(desc_frame, textvariable=self.desc_var)
desc_entry.pack(side='left', fill='x', expand=True, padx=(5, 0))
def _create_body_tab(self, parent):
"""创建正文样式选项卡"""
# 正文字体设置
font_frame = ttk.LabelFrame(parent, text='正文字体', padding="10")
font_frame.pack(fill='x', padx=10, pady=5)
# 字体名称
name_frame = ttk.Frame(font_frame)
name_frame.pack(fill='x', pady=2)
ttk.Label(name_frame, text='字体:', width=10).pack(side='left')
self.style_vars['font_name'] = tk.StringVar(value=self.current_style.body_font.name)
font_combo = ttk.Combobox(name_frame, textvariable=self.style_vars['font_name'], width=15,
values=['宋体', '微软雅黑', '黑体', '楷体', '华文细黑', 'Arial', 'Times New Roman'])
font_combo.pack(side='left', padx=(5, 10))
# 字号
ttk.Label(name_frame, text='大小:', width=5).pack(side='left')
self.style_vars['font_size'] = tk.IntVar(value=self.current_style.body_font.size)
size_spin = ttk.Spinbox(name_frame, from_=8, to=72, textvariable=self.style_vars['font_size'], width=5)
size_spin.pack(side='left', padx=(5, 0))
# 字体样式
style_frame = ttk.Frame(font_frame)
style_frame.pack(fill='x', pady=2)
self.style_vars['font_bold'] = tk.BooleanVar(value=self.current_style.body_font.bold)
ttk.Checkbutton(style_frame, text='粗体', variable=self.style_vars['font_bold']).pack(side='left', padx=(0, 10))
self.style_vars['font_italic'] = tk.BooleanVar(value=self.current_style.body_font.italic)
ttk.Checkbutton(style_frame, text='斜体', variable=self.style_vars['font_italic']).pack(side='left', padx=(0, 10))
# 字体颜色
ttk.Label(style_frame, text='颜色:', width=5).pack(side='left')
self.style_vars['font_color'] = tk.StringVar(value=self.current_style.body_font.color)
self.color_label = tk.Label(style_frame, text=' ', bg=self.style_vars['font_color'].get(), relief='solid', width=3)
self.color_label.pack(side='left', padx=(5, 5))
ttk.Button(style_frame, text='选择', command=self._choose_font_color).pack(side='left')
# 正文段落设置
para_frame = ttk.LabelFrame(parent, text='正文段落', padding="10")
para_frame.pack(fill='x', padx=10, pady=5)
# 行距
spacing_frame = ttk.Frame(para_frame)
spacing_frame.pack(fill='x', pady=2)
ttk.Label(spacing_frame, text='行距:', width=10).pack(side='left')
self.style_vars['line_spacing'] = tk.DoubleVar(value=self.current_style.body_paragraph.line_spacing)
line_spacing_spin = ttk.Spinbox(spacing_frame, from_=0.5, to=3.0, increment=0.1,
textvariable=self.style_vars['line_spacing'], width=8)
line_spacing_spin.pack(side='left', padx=(5, 10))
# 首行缩进
ttk.Label(spacing_frame, text='首行缩进:', width=10).pack(side='left')
self.style_vars['indent'] = tk.DoubleVar(value=self.current_style.body_paragraph.first_line_indent)
indent_spin = ttk.Spinbox(spacing_frame, from_=0.0, to=10.0, increment=0.5,
textvariable=self.style_vars['indent'], width=8)
indent_spin.pack(side='left', padx=(5, 0))
# 段间距
margin_frame = ttk.Frame(para_frame)
margin_frame.pack(fill='x', pady=2)
ttk.Label(margin_frame, text='段前:', width=10).pack(side='left')
self.style_vars['space_before'] = tk.IntVar(value=self.current_style.body_paragraph.space_before)
before_spin = ttk.Spinbox(margin_frame, from_=0, to=50, textvariable=self.style_vars['space_before'], width=8)
before_spin.pack(side='left', padx=(5, 10))
ttk.Label(margin_frame, text='段后:', width=10).pack(side='left')
self.style_vars['space_after'] = tk.IntVar(value=self.current_style.body_paragraph.space_after)
after_spin = ttk.Spinbox(margin_frame, from_=0, to=50, textvariable=self.style_vars['space_after'], width=8)
after_spin.pack(side='left', padx=(5, 0))
def _create_heading_tab(self, parent):
"""创建标题样式选项卡"""
# 创建滚动区域
canvas = tk.Canvas(parent)
scrollbar = ttk.Scrollbar(parent, orient="vertical", command=canvas.yview)
scrollable_frame = ttk.Frame(canvas)
scrollable_frame.bind(
"<Configure>",
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
)
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
# 为每个标题级别创建控件
for level in range(1, 4):
level_frame = ttk.LabelFrame(scrollable_frame, text=f'{level}级标题', padding="10")
level_frame.pack(fill='x', padx=10, pady=5)
heading_style = self.current_style.heading_styles.get(level)
if not heading_style:
continue
# 标题字体名称和大小
h_name_frame = ttk.Frame(level_frame)
h_name_frame.pack(fill='x', pady=2)
ttk.Label(h_name_frame, text='字体:', width=8).pack(side='left')
h_name_var = tk.StringVar(value=heading_style.font.name)
h_combo = ttk.Combobox(h_name_frame, textvariable=h_name_var, width=12,
values=['宋体', '微软雅黑', '黑体', '楷体'])
h_combo.pack(side='left', padx=(5, 10))
ttk.Label(h_name_frame, text='大小:', width=5).pack(side='left')
h_size_var = tk.IntVar(value=heading_style.font.size)
h_size_spin = ttk.Spinbox(h_name_frame, from_=8, to=72, textvariable=h_size_var, width=5)
h_size_spin.pack(side='left', padx=(5, 0))
# 标题颜色
h_color_frame = ttk.Frame(level_frame)
h_color_frame.pack(fill='x', pady=2)
ttk.Label(h_color_frame, text='颜色:', width=8).pack(side='left')
h_color_var = tk.StringVar(value=heading_style.font.color)
h_color_label = tk.Label(h_color_frame, text=' ', bg=h_color_var.get(), relief='solid', width=3)
h_color_label.pack(side='left', padx=(5, 5))
def make_choose_heading_color(var, label):
def choose_color():
color = colorchooser.askcolor(color=var.get())[1]
if color:
var.set(color)
label.config(bg=color)
self._update_preview()
return choose_color
ttk.Button(h_color_frame, text='选择',
command=make_choose_heading_color(h_color_var, h_color_label)).pack(side='left')
self.heading_vars[level] = {
'name': h_name_var,
'size': h_size_var,
'color': h_color_var
}
canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
def _create_preview_area(self, parent):
"""创建预览区域"""
# 创建预览容器
preview_container = ttk.Frame(parent)
preview_container.pack(fill='both', expand=True)
# 预览模式选择
mode_frame = ttk.Frame(preview_container)
mode_frame.pack(fill='x', pady=(0, 5))
self.preview_mode = tk.StringVar(value="rich")
ttk.Radiobutton(mode_frame, text="丰富预览", variable=self.preview_mode,
value="rich", command=self._update_preview).pack(side='left')
ttk.Radiobutton(mode_frame, text="文本预览", variable=self.preview_mode,
value="text", command=self._update_preview).pack(side='left', padx=(10, 0))
# 预览文本区域
text_frame = ttk.Frame(preview_container)
text_frame.pack(fill='both', expand=True)
self.preview_text = tk.Text(text_frame, wrap=tk.WORD, state='disabled',
font=('微软雅黑', 12), bg='white')
self.preview_text.pack(side='left', fill='both', expand=True)
# 滚动条
preview_scroll = ttk.Scrollbar(text_frame, orient="vertical", command=self.preview_text.yview)
preview_scroll.pack(side="right", fill="y")
self.preview_text.configure(yscrollcommand=preview_scroll.set)
# 初始化文本标签
self._init_text_tags()
def _create_bottom_buttons(self):
"""创建底部按钮"""
button_frame = ttk.Frame(self.window)
button_frame.pack(fill='x', padx=10, pady=10)
ttk.Button(button_frame, text='保存样式', command=self._save_style).pack(side='left', padx=5)
ttk.Button(button_frame, text='另存为', command=self._save_as_style).pack(side='left', padx=5)
ttk.Button(button_frame, text='重置', command=self._reset_style).pack(side='left', padx=5)
ttk.Button(button_frame, text='关闭', command=self.window.destroy).pack(side='right', padx=5)
ttk.Button(button_frame, text='应用并关闭', command=self._apply_and_close).pack(side='right', padx=5)
def _choose_font_color(self):
"""选择字体颜色"""
color = colorchooser.askcolor(color=self.style_vars['font_color'].get())[1]
if color:
self.style_vars['font_color'].set(color)
self.color_label.config(bg=color)
self._update_preview() # 立即更新预览
def _bind_events(self):
"""绑定事件"""
# 绑定所有变量的变化事件
for var in self.style_vars.values():
if hasattr(var, 'trace'):
var.trace('w', lambda *args: self._update_preview())
for level_vars in self.heading_vars.values():
for var in level_vars.values():
if hasattr(var, 'trace'):
var.trace('w', lambda *args: self._update_preview())
# 基本信息变量
self.name_var.trace('w', lambda *args: self._update_preview())
self.desc_var.trace('w', lambda *args: self._update_preview())
# 预览模式变化事件在_create_preview_area中已绑定
def _update_current_style(self):
"""更新当前样式对象"""
# 更新基本信息
self.current_style.name = self.name_var.get()
self.current_style.description = self.desc_var.get()
# 更新正文字体
self.current_style.body_font.name = self.style_vars['font_name'].get()
self.current_style.body_font.size = self.style_vars['font_size'].get()
self.current_style.body_font.bold = self.style_vars['font_bold'].get()
self.current_style.body_font.italic = self.style_vars['font_italic'].get()
self.current_style.body_font.color = self.style_vars['font_color'].get()
# 更新正文段落
self.current_style.body_paragraph.line_spacing = self.style_vars['line_spacing'].get()
self.current_style.body_paragraph.first_line_indent = self.style_vars['indent'].get()
self.current_style.body_paragraph.space_before = self.style_vars['space_before'].get()
self.current_style.body_paragraph.space_after = self.style_vars['space_after'].get()
# 更新标题样式
for level, vars_dict in self.heading_vars.items():
if level in self.current_style.heading_styles:
heading_style = self.current_style.heading_styles[level]
# 现在heading_style应该始终是HeadingStyle对象
heading_style.font.name = vars_dict['name'].get()
heading_style.font.size = vars_dict['size'].get()
heading_style.font.color = vars_dict['color'].get()
def _init_text_tags(self):
"""初始化文本标签样式"""
# 配置各种文本标签
self.preview_text.tag_configure('title', font=('微软雅黑', 14, 'bold'), foreground='#2E86AB')
self.preview_text.tag_configure('separator', foreground='#666666')
self.preview_text.tag_configure('heading1', font=('黑体', 18, 'bold'), foreground='#E74C3C')
self.preview_text.tag_configure('heading2', font=('微软雅黑', 16, 'bold'), foreground='#3498DB')
self.preview_text.tag_configure('heading3', font=('微软雅黑', 14, 'bold'), foreground='#2ECC71')
self.preview_text.tag_configure('body', font=('宋体', 12), foreground='#333333')
self.preview_text.tag_configure('bold', font=('微软雅黑', 12, 'bold'))
self.preview_text.tag_configure('italic', font=('微软雅黑', 12, 'italic'))
self.preview_text.tag_configure('quote', font=('楷体', 11, 'italic'), foreground='#7F8C8D',
background='#F8F9FA')
self.preview_text.tag_configure('code', font=('Courier New', 10),
background='#F4F4F4', foreground='#C0392B')
self.preview_text.tag_configure('info', font=('微软雅黑', 10), foreground='#95A5A6')
def _update_preview(self):
"""更新预览"""
self._update_current_style()
# 清空预览区域
self.preview_text.config(state='normal')
self.preview_text.delete(1.0, tk.END)
if self.preview_mode.get() == "rich":
self._create_rich_preview()
else:
self._create_text_preview()
self.preview_text.config(state='disabled')
def _create_rich_preview(self):
"""创建丰富的样式预览"""
# 样式标题
self.preview_text.insert(tk.END, f"样式预览:{self.current_style.name}\n", 'title')
self.preview_text.insert(tk.END, f"描述:{self.current_style.description}\n\n", 'info')
# 分隔线
self.preview_text.insert(tk.END, "" * 50 + "\n\n", 'separator')
# 动态更新标题样式标签
self._update_heading_tags()
# 一级标题预览
self.preview_text.insert(tk.END, "一级标题样式预览\n\n", 'heading1_live')
# 二级标题预览
self.preview_text.insert(tk.END, "二级标题样式预览\n\n", 'heading2_live')
# 三级标题预览
self.preview_text.insert(tk.END, "三级标题样式预览\n\n", 'heading3_live')
# 动态更新正文样式标签
self._update_body_tag()
# 正文段落预览
body_text = f"""正文段落样式预览:
这是正文内容的示例展示当前选择的字体和格式效果
字体{self.current_style.body_font.name} {self.current_style.body_font.size}pt
行距{self.current_style.body_paragraph.line_spacing}
首行缩进{self.current_style.body_paragraph.first_line_indent}字符
段前间距{self.current_style.body_paragraph.space_before}pt
段后间距{self.current_style.body_paragraph.space_after}pt
这是包含"""
self.preview_text.insert(tk.END, body_text, 'body_live')
# 粗体和斜体示例
self.preview_text.insert(tk.END, "粗体文字", 'bold_live')
self.preview_text.insert(tk.END, "", 'body_live')
self.preview_text.insert(tk.END, "斜体文字", 'italic_live')
self.preview_text.insert(tk.END, "的段落示例。\n\n", 'body_live')
# 引用块示例
self.preview_text.insert(tk.END, "引用块样式预览:\n", 'body_live')
self.preview_text.insert(tk.END, " 这是引用块的示例内容,展示引用文字的特殊格式效果。\n\n", 'quote')
# 代码块示例
self.preview_text.insert(tk.END, "代码块样式预览:\n", 'body_live')
self.preview_text.insert(tk.END, ' print("Hello, World!")\n # 这是代码块的示例\n\n', 'code')
# 列表示例
self.preview_text.insert(tk.END, "列表样式预览:\n", 'body_live')
self.preview_text.insert(tk.END, "• 无序列表项目1\n• 无序列表项目2\n• 无序列表项目3\n\n", 'body_live')
self.preview_text.insert(tk.END, "1. 有序列表项目1\n2. 有序列表项目2\n3. 有序列表项目3\n\n", 'body_live')
# 样式详情
details = f"""当前样式设置详情:
字体名称{self.current_style.body_font.name}
字体大小{self.current_style.body_font.size}pt
字体颜色{self.current_style.body_font.color}
是否粗体{'' if self.current_style.body_font.bold else ''}
是否斜体{'' if self.current_style.body_font.italic else ''}
行距{self.current_style.body_paragraph.line_spacing}
首行缩进{self.current_style.body_paragraph.first_line_indent}字符"""
self.preview_text.insert(tk.END, details, 'info')
def _create_text_preview(self):
"""创建简单文本预览"""
preview_content = f"""样式预览:{self.current_style.name}
描述{self.current_style.description}
# 一级标题样式预览
## 二级标题样式预览
### 三级标题样式预览
正文段落样式预览
这是正文内容的示例使用{self.current_style.body_font.name} {self.current_style.body_font.size}pt字体
行距为{self.current_style.body_paragraph.line_spacing}首行缩进{self.current_style.body_paragraph.first_line_indent}字符
段前间距{self.current_style.body_paragraph.space_before}pt段后间距{self.current_style.body_paragraph.space_after}pt
这是包含**粗体文字***斜体文字*的段落用于展示内联样式效果
引用块样式预览
> 这是引用块的示例内容展示引用文字的特殊格式效果
代码块样式预览
```
print("Hello, World!")
# 这是代码块的示例
```
列表样式预览
无序列表项目1
无序列表项目2
无序列表项目3
1. 有序列表项目1
2. 有序列表项目2
3. 有序列表项目3
字体设置详情
字体名称{self.current_style.body_font.name}
字体大小{self.current_style.body_font.size}pt
字体颜色{self.current_style.body_font.color}
粗体{'' if self.current_style.body_font.bold else ''}
斜体{'' if self.current_style.body_font.italic else ''}
"""
self.preview_text.insert(tk.END, preview_content)
def _update_heading_tags(self):
"""动态更新标题样式标签"""
for level in range(1, 4):
tag_name = f'heading{level}_live'
if level in self.current_style.heading_styles:
heading_style = self.current_style.heading_styles[level]
font_name = heading_style.font.name
font_size = heading_style.font.size
font_weight = 'bold' if heading_style.font.bold else 'normal'
font_color = heading_style.font.color
self.preview_text.tag_configure(tag_name,
font=(font_name, font_size, font_weight),
foreground=font_color)
def _update_body_tag(self):
"""动态更新正文样式标签"""
font_name = self.current_style.body_font.name
font_size = self.current_style.body_font.size
font_weight = 'bold' if self.current_style.body_font.bold else 'normal'
font_slant = 'italic' if self.current_style.body_font.italic else 'roman'
font_color = self.current_style.body_font.color
# 更新正文标签
self.preview_text.tag_configure('body_live',
font=(font_name, font_size, font_weight, font_slant),
foreground=font_color)
# 更新粗体标签
self.preview_text.tag_configure('bold_live',
font=(font_name, font_size, 'bold', font_slant),
foreground=font_color)
# 更新斜体标签
self.preview_text.tag_configure('italic_live',
font=(font_name, font_size, font_weight, 'italic'),
foreground=font_color)
def _save_style(self):
"""保存样式"""
try:
if not self.current_style.name.strip():
messagebox.showerror("错误", "请输入样式名称")
return
self._update_current_style()
# 尝试更新现有样式,如果失败则创建新样式
if self.current_style.name in style_manager.custom_styles:
success = style_manager.update_custom_style(self.current_style)
else:
success = style_manager.create_custom_style(self.current_style)
if success:
messagebox.showinfo("成功", f"样式 '{self.current_style.name}' 保存成功")
config.current_style = self.current_style.name
else:
if self.current_style.name in style_manager.builtin_styles:
messagebox.showerror("错误", "不能覆盖内置样式!请使用不同的样式名称或'另存为'功能。")
else:
messagebox.showerror("保存失败", "保存样式失败!请检查文件权限或磁盘空间。")
except Exception as e:
messagebox.showerror("错误", f"保存过程中发生异常: {str(e)}")
def _save_as_style(self):
"""另存为新样式"""
try:
new_name = simpledialog.askstring("另存为", "请输入新样式名称:")
if not new_name:
return
if new_name in style_manager.get_style_names():
if new_name in style_manager.builtin_styles:
messagebox.showerror("名称冲突", "不能使用内置样式名称!请使用不同的样式名称。")
else:
messagebox.showerror("名称已存在", f"样式名称 '{new_name}' 已经存在!请使用不同的名称。")
return
# 修改样式信息
self.current_style.name = new_name
self.current_style.description = "基于 " + self.original_style.name + " 定制"
self._update_current_style()
if style_manager.create_custom_style(self.current_style):
messagebox.showinfo("创建成功", f"样式 '{new_name}' 已成功创建!")
config_manager = Config()
config_manager.set('current_style', new_name)
self.window.destroy()
else:
messagebox.showerror("创建失败", "创建样式失败!请检查文件权限或磁盘空间。")
except Exception as e:
messagebox.showerror("错误", f"另存为过程中发生异常: {str(e)}")
def _reset_style(self):
"""重置样式"""
if messagebox.askyesno("确认重置", "确定要重置为原始样式吗?"):
self.current_style = copy.deepcopy(self.original_style)
self._ensure_style_completeness()
# 重新创建界面(简单的重置方法)
self.window.destroy()
self.__init__(self.parent, self.original_style)
def _apply_and_close(self):
"""应用并关闭"""
self._save_style()
self.window.destroy()