Files
yidaima_tools/editor_gui.py

665 lines
25 KiB
Python
Raw Permalink Normal View History

2026-04-09 14:55:54 +08:00
"""
微信 Markdown 编辑器 GUI 模块
提供可视化编辑界面
"""
from __future__ import annotations
import tkinter as tk
from tkinter import ttk, messagebox, filedialog, scrolledtext, colorchooser
import tempfile
import webbrowser
import os
# 使用 tkinterweb 进行 HTML 预览(支持完整 HTML 渲染)
from tkinterweb import HtmlFrame
from markdown_editor import (
WechatMarkdownEditor, ThemeConfig, FontConfig,
CodeBlockConfig, EditorConfig
)
class MarkdownEditorWindow:
"""Markdown 编辑器窗口"""
def __init__(self, parent=None):
self.editor = WechatMarkdownEditor()
self.config = self.editor.get_config()
# 创建窗口
if parent:
self.window = tk.Toplevel(parent)
else:
self.window = tk.Tk()
self.window.title("微信 Markdown 编辑器")
self.window.geometry("1400x900")
self.window.minsize(1200, 700)
# 设置样式
self.style = ttk.Style()
self.style.configure("Title.TLabel", font=("Microsoft YaHei", 14, "bold"))
self.style.configure("Subtitle.TLabel", font=("Microsoft YaHei", 10))
self.create_widgets()
self.update_preview()
def create_widgets(self):
"""创建界面组件"""
# 主框架 - 使用 PanedWindow 实现可调整的分割
main_paned = ttk.PanedWindow(self.window, orient=tk.HORIZONTAL)
main_paned.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# === 左侧:编辑区 ===
left_frame = ttk.Frame(main_paned)
main_paned.add(left_frame, weight=1)
# 编辑区标题
ttk.Label(left_frame, text="Markdown 编辑区", style="Title.TLabel").pack(anchor=tk.W, pady=(0, 5))
# Markdown 编辑框
self.md_text = scrolledtext.ScrolledText(
left_frame,
wrap=tk.WORD,
font=("Consolas", 12),
undo=True,
maxundo=-1
)
self.md_text.pack(fill=tk.BOTH, expand=True)
self.md_text.bind("<KeyRelease>", lambda e: self.update_preview())
# 默认内容
default_content = """# 欢迎使用微信 Markdown 编辑器
这是一段示例文本支持 **粗体***斜体*`行内代码`
## 代码示例
```python
def hello_world():
print("Hello, World!")
return True
```
## 列表
- 项目 1
- 项目 2
- 项目 3
## 引用
> 这是一段引用文本
## 表格
| 列1 | 列2 | 列3 |
|-----|-----|-----|
| A | B | C |
| D | E | F |
---
在右侧设置面板中可以调整主题字体代码块样式等
"""
self.md_text.insert("1.0", default_content)
# === 中间:预览区 ===
center_frame = ttk.Frame(main_paned)
main_paned.add(center_frame, weight=1)
# 预览区标题和工具栏
preview_header = ttk.Frame(center_frame)
preview_header.pack(fill=tk.X, pady=(0, 5))
ttk.Label(preview_header, text="实时预览", style="Title.TLabel").pack(side=tk.LEFT)
# 刷新按钮
ttk.Button(preview_header, text="🔄 刷新", command=self.update_preview).pack(side=tk.RIGHT, padx=5)
ttk.Button(preview_header, text="📋 复制 HTML", command=self.copy_html).pack(side=tk.RIGHT, padx=5)
ttk.Button(preview_header, text="💾 导出 HTML", command=self.export_html).pack(side=tk.RIGHT, padx=5)
# 预览区域
preview_notebook = ttk.Notebook(center_frame)
preview_notebook.pack(fill=tk.BOTH, expand=True)
# HTML 渲染预览标签页
preview_frame = ttk.Frame(preview_notebook)
preview_notebook.add(preview_frame, text="效果预览")
# 使用 HtmlFrame 显示渲染后的 HTML
self.html_preview = HtmlFrame(preview_frame, messages_enabled=False)
self.html_preview.pack(fill=tk.BOTH, expand=True)
# HTML 源码标签页
html_frame = ttk.Frame(preview_notebook)
preview_notebook.add(html_frame, text="HTML 源码")
self.html_text = scrolledtext.ScrolledText(
html_frame,
wrap=tk.NONE,
font=("Consolas", 10),
state=tk.DISABLED
)
self.html_text.pack(fill=tk.BOTH, expand=True)
# 浏览器预览标签页
browser_frame = ttk.Frame(preview_notebook)
preview_notebook.add(browser_frame, text="浏览器预览")
ttk.Label(browser_frame, text="点击按钮在浏览器中预览效果:").pack(pady=20)
ttk.Button(browser_frame, text="🌐 在浏览器中打开", command=self.open_in_browser).pack(pady=10)
# === 右侧:设置面板 ===
right_frame = ttk.Frame(main_paned, width=350)
main_paned.add(right_frame, weight=0)
right_frame.pack_propagate(False)
# 设置面板标题
ttk.Label(right_frame, text="设置面板", style="Title.TLabel").pack(anchor=tk.W, pady=(0, 10))
# 创建设置区域的 Canvas 和 Scrollbar
settings_canvas = tk.Canvas(right_frame, highlightthickness=0)
scrollbar = ttk.Scrollbar(right_frame, orient=tk.VERTICAL, command=settings_canvas.yview)
settings_canvas.configure(yscrollcommand=scrollbar.set)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
settings_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# 设置内容框架
self.settings_frame = ttk.Frame(settings_canvas)
settings_canvas.create_window((0, 0), window=self.settings_frame, anchor=tk.NW, width=330)
# 绑定滚动
self.settings_frame.bind("<Configure>", lambda e: settings_canvas.configure(scrollregion=settings_canvas.bbox("all")))
settings_canvas.bind("<Configure>", lambda e: settings_canvas.itemconfig(settings_canvas.find_all()[0], width=e.width-5))
# 创建设置区域
self.create_theme_settings()
self.create_font_settings()
self.create_code_block_settings()
self.create_custom_css_settings()
def create_theme_settings(self):
"""创建主题设置区域"""
theme_frame = ttk.LabelFrame(self.settings_frame, text="主题设置", padding=10)
theme_frame.pack(fill=tk.X, pady=(0, 10))
# 主题选择
ttk.Label(theme_frame, text="选择主题:").pack(anchor=tk.W)
self.theme_var = tk.StringVar(value=self.config.theme)
themes = self.editor.theme_manager.get_all_themes()
theme_names = [f"{t.id}:{t.name}" for t in themes]
self.theme_combo = ttk.Combobox(
theme_frame,
values=theme_names,
textvariable=self.theme_var,
state="readonly"
)
self.theme_combo.pack(fill=tk.X, pady=(5, 10))
self.theme_combo.bind("<<ComboboxSelected>>", self.on_theme_change)
# 主题色
color_frame = ttk.Frame(theme_frame)
color_frame.pack(fill=tk.X, pady=5)
ttk.Label(color_frame, text="主题色:").pack(side=tk.LEFT)
self.primary_color_var = tk.StringVar(value=self.editor.theme_manager.get_current_theme().primary_color)
self.color_btn = tk.Button(
color_frame,
text=" ",
bg=self.primary_color_var.get(),
width=3,
command=self.choose_primary_color
)
self.color_btn.pack(side=tk.LEFT, padx=5)
ttk.Entry(color_frame, textvariable=self.primary_color_var, width=10).pack(side=tk.LEFT)
ttk.Button(color_frame, text="应用", command=self.apply_primary_color).pack(side=tk.LEFT, padx=5)
def create_font_settings(self):
"""创建字体设置区域"""
font_frame = ttk.LabelFrame(self.settings_frame, text="字体设置", padding=10)
font_frame.pack(fill=tk.X, pady=(0, 10))
# 正文字体
ttk.Label(font_frame, text="正文字体:").pack(anchor=tk.W)
self.body_font_var = tk.StringVar(value=self.config.font.body_font)
ttk.Entry(font_frame, textvariable=self.body_font_var).pack(fill=tk.X, pady=(0, 5))
# 正文字号
size_frame = ttk.Frame(font_frame)
size_frame.pack(fill=tk.X, pady=5)
ttk.Label(size_frame, text="正文字号:").pack(side=tk.LEFT)
self.body_size_var = tk.IntVar(value=self.config.font.body_size)
ttk.Spinbox(size_frame, from_=12, to=24, textvariable=self.body_size_var, width=5).pack(side=tk.LEFT, padx=5)
ttk.Label(size_frame, text="px").pack(side=tk.LEFT)
# 代码字体
ttk.Label(font_frame, text="代码字体:").pack(anchor=tk.W, pady=(10, 0))
self.code_font_var = tk.StringVar(value=self.config.font.code_font)
ttk.Entry(font_frame, textvariable=self.code_font_var).pack(fill=tk.X, pady=(0, 5))
# 行高
lh_frame = ttk.Frame(font_frame)
lh_frame.pack(fill=tk.X, pady=5)
ttk.Label(lh_frame, text="行高:").pack(side=tk.LEFT)
self.line_height_var = tk.DoubleVar(value=self.config.font.line_height)
ttk.Spinbox(lh_frame, from_=1.0, to=2.5, increment=0.1, textvariable=self.line_height_var, width=5).pack(side=tk.LEFT, padx=5)
# 应用按钮
ttk.Button(font_frame, text="应用字体设置", command=self.apply_font_settings).pack(fill=tk.X, pady=(10, 0))
def create_code_block_settings(self):
"""创建代码块设置区域"""
code_frame = ttk.LabelFrame(self.settings_frame, text="代码块设置", padding=10)
code_frame.pack(fill=tk.X, pady=(0, 10))
# 代码主题
ttk.Label(code_frame, text="代码高亮主题:").pack(anchor=tk.W)
self.code_theme_var = tk.StringVar(value=self.config.code_block.theme)
code_themes = self.config.code_block.available_themes
self.code_theme_combo = ttk.Combobox(
code_frame,
values=sorted(code_themes),
textvariable=self.code_theme_var,
state="readonly"
)
self.code_theme_combo.pack(fill=tk.X, pady=(5, 10))
self.code_theme_combo.bind("<<ComboboxSelected>>", lambda e: self.apply_code_settings())
# 代码块背景色
bg_frame = ttk.Frame(code_frame)
bg_frame.pack(fill=tk.X, pady=5)
ttk.Label(bg_frame, text="代码块背景:").pack(side=tk.LEFT)
self.code_bg_var = tk.StringVar(value=self.config.code_block.background)
self.code_bg_btn = tk.Button(
bg_frame,
text=" ",
bg=self.code_bg_var.get(),
width=3,
command=self.choose_code_bg
)
self.code_bg_btn.pack(side=tk.LEFT, padx=5)
ttk.Entry(bg_frame, textvariable=self.code_bg_var, width=10).pack(side=tk.LEFT)
def create_custom_css_settings(self):
"""创建自定义 CSS 设置区域"""
css_frame = ttk.LabelFrame(self.settings_frame, text="自定义 CSS", padding=10)
css_frame.pack(fill=tk.X, pady=(0, 10))
# 已保存的方案选择
scheme_frame = ttk.Frame(css_frame)
scheme_frame.pack(fill=tk.X, pady=(0, 10))
ttk.Label(scheme_frame, text="已保存的方案:").pack(side=tk.LEFT)
self.scheme_var = tk.StringVar()
self.scheme_combo = ttk.Combobox(
scheme_frame,
values=self._get_scheme_names(),
textvariable=self.scheme_var,
state="readonly",
width=20
)
self.scheme_combo.pack(side=tk.LEFT, padx=5)
ttk.Button(scheme_frame, text="加载", command=self.load_css_scheme).pack(side=tk.LEFT, padx=2)
ttk.Button(scheme_frame, text="删除", command=self.delete_css_scheme).pack(side=tk.LEFT, padx=2)
# CSS 编辑区
self.css_text = scrolledtext.ScrolledText(
css_frame,
wrap=tk.WORD,
font=("Consolas", 9),
height=8
)
self.css_text.pack(fill=tk.BOTH, expand=True)
self.css_text.insert("1.0", self.config.custom_css)
# 保存方案区域
save_frame = ttk.Frame(css_frame)
save_frame.pack(fill=tk.X, pady=(10, 0))
ttk.Label(save_frame, text="方案名称:").pack(side=tk.LEFT)
self.scheme_name_var = tk.StringVar()
ttk.Entry(save_frame, textvariable=self.scheme_name_var, width=20).pack(side=tk.LEFT, padx=5)
ttk.Button(save_frame, text="💾 保存方案", command=self.save_css_scheme).pack(side=tk.LEFT, padx=5)
# CSS 按钮
btn_frame = ttk.Frame(css_frame)
btn_frame.pack(fill=tk.X, pady=(10, 0))
ttk.Button(btn_frame, text="应用 CSS", command=self.apply_custom_css).pack(side=tk.LEFT, padx=(0, 5))
ttk.Button(btn_frame, text="导出 CSS", command=self.export_css).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="重置", command=self.reset_custom_css).pack(side=tk.LEFT, padx=5)
def _get_scheme_names(self):
"""获取方案名称列表(只显示名称)"""
schemes = self.editor.theme_manager.get_all_css_schemes()
return [s.name for s in schemes]
def on_theme_change(self, event=None):
"""主题变更处理"""
theme_id = self.theme_var.get().split(":")[0]
self.editor.theme_manager.set_theme(theme_id)
# 更新主题色显示
theme = self.editor.theme_manager.get_current_theme()
self.primary_color_var.set(theme.primary_color)
self.color_btn.config(bg=theme.primary_color)
self.update_preview()
def choose_primary_color(self):
"""选择主题色"""
color = colorchooser.askcolor(initialcolor=self.primary_color_var.get())
if color[1]:
self.primary_color_var.set(color[1])
self.color_btn.config(bg=color[1])
def apply_primary_color(self):
"""应用主题色"""
# 创建自定义主题
current_theme = self.editor.theme_manager.get_current_theme()
custom_theme = ThemeConfig(
id=f"custom_{current_theme.id}",
name=f"自定义 - {current_theme.name}",
primary_color=self.primary_color_var.get(),
background=current_theme.background,
text_color=current_theme.text_color,
link_color=current_theme.link_color,
code_bg=current_theme.code_bg
)
self.editor.theme_manager.add_custom_theme(custom_theme)
self.editor.theme_manager.set_theme(custom_theme.id)
self.update_preview()
messagebox.showinfo("成功", "主题色已应用!")
def apply_font_settings(self):
"""应用字体设置"""
self.editor.update_font_config(
body_font=self.body_font_var.get(),
code_font=self.code_font_var.get(),
body_size=self.body_size_var.get(),
line_height=self.line_height_var.get()
)
self.update_preview()
messagebox.showinfo("成功", "字体设置已应用!")
def choose_code_bg(self):
"""选择代码块背景色"""
color = colorchooser.askcolor(initialcolor=self.code_bg_var.get())
if color[1]:
self.code_bg_var.set(color[1])
self.apply_code_settings()
def apply_code_settings(self):
"""应用代码块设置"""
self.editor.update_code_block_config(
theme=self.code_theme_var.get(),
background=self.code_bg_var.get()
)
self.update_preview()
def apply_custom_css(self):
"""应用自定义 CSS"""
css = self.css_text.get("1.0", tk.END).strip()
self.editor.update_custom_css(css)
self.update_preview()
messagebox.showinfo("成功", "自定义 CSS 已应用!")
def reset_custom_css(self):
"""重置自定义 CSS"""
if messagebox.askyesno("确认", "确定要清空自定义 CSS 吗?"):
self.css_text.delete("1.0", tk.END)
self.editor.update_custom_css("")
self.update_preview()
def export_css(self):
"""导出 CSS 文件"""
file_path = filedialog.asksaveasfilename(
defaultextension=".css",
filetypes=[("CSS 文件", "*.css"), ("所有文件", "*.*")]
)
if file_path:
self.editor.export_css(file_path)
messagebox.showinfo("成功", f"CSS 已导出到:\n{file_path}")
def save_css_scheme(self):
"""保存 CSS 方案(同名更新,新名添加)"""
name = self.scheme_name_var.get().strip()
if not name:
messagebox.showwarning("提示", "请输入方案名称")
return
css = self.css_text.get("1.0", tk.END).strip()
# 收集当前字体和代码块配置
font_config = {
"body_font": self.body_font_var.get(),
"code_font": self.code_font_var.get(),
"body_size": self.body_size_var.get(),
"line_height": self.line_height_var.get()
}
code_block_config = {
"theme": self.code_theme_var.get(),
"background": self.code_bg_var.get()
}
# 获取当前主题和主题色
theme_id = self.theme_var.get().split(":")[0] if ":" in self.theme_var.get() else self.theme_var.get()
primary_color = self.primary_color_var.get()
# 检查是否已存在同名方案
existing_scheme = None
for scheme in self.editor.theme_manager.get_all_css_schemes():
if scheme.name == name:
existing_scheme = scheme
break
from markdown_editor import CssScheme
if existing_scheme:
# 更新现有方案
if messagebox.askyesno("确认", f"方案 '{name}' 已存在,是否更新?"):
# 删除旧方案,添加新方案(保留原 ID
self.editor.theme_manager.remove_css_scheme(existing_scheme.id)
updated_scheme = CssScheme(
id=existing_scheme.id,
name=name,
css=css,
font_config=font_config,
code_block_config=code_block_config,
theme_id=theme_id,
primary_color=primary_color
)
self.editor.theme_manager.add_css_scheme(updated_scheme)
messagebox.showinfo("成功", f"方案 '{name}' 已更新!")
else:
return
else:
# 添加新方案
import uuid
scheme_id = str(uuid.uuid4())[:8]
scheme = CssScheme(
id=scheme_id,
name=name,
css=css,
font_config=font_config,
code_block_config=code_block_config,
theme_id=theme_id,
primary_color=primary_color
)
self.editor.theme_manager.add_css_scheme(scheme)
messagebox.showinfo("成功", f"方案 '{name}' 已保存!")
# 更新下拉列表
self.scheme_combo['values'] = self._get_scheme_names()
self.scheme_name_var.set("")
def load_css_scheme(self):
"""加载 CSS 方案"""
scheme_name = self.scheme_var.get()
if not scheme_name:
messagebox.showwarning("提示", "请选择一个方案")
return
# 根据名称查找方案
scheme = None
for s in self.editor.theme_manager.get_all_css_schemes():
if s.name == scheme_name:
scheme = s
break
if not scheme:
messagebox.showerror("错误", "方案不存在")
return
# 应用 CSS
self.css_text.delete("1.0", tk.END)
self.css_text.insert("1.0", scheme.css)
# 应用字体配置
if scheme.font_config:
if "body_font" in scheme.font_config:
self.body_font_var.set(scheme.font_config["body_font"])
if "code_font" in scheme.font_config:
self.code_font_var.set(scheme.font_config["code_font"])
if "body_size" in scheme.font_config:
self.body_size_var.set(scheme.font_config["body_size"])
if "line_height" in scheme.font_config:
self.line_height_var.set(scheme.font_config["line_height"])
# 应用代码块配置
if scheme.code_block_config:
if "theme" in scheme.code_block_config:
self.code_theme_var.set(scheme.code_block_config["theme"])
if "background" in scheme.code_block_config:
self.code_bg_var.set(scheme.code_block_config["background"])
self.code_bg_btn.config(bg=scheme.code_block_config["background"])
# 应用主题和主题色
if scheme.theme_id:
self.theme_var.set(scheme.theme_id)
self.editor.theme_manager.set_theme(scheme.theme_id)
if scheme.primary_color:
self.primary_color_var.set(scheme.primary_color)
self.color_btn.config(bg=scheme.primary_color)
# 应用所有配置
self.apply_custom_css()
self.apply_font_settings()
self.apply_code_settings()
# 将方案名称写入方案名称框
self.scheme_name_var.set(scheme.name)
messagebox.showinfo("成功", f"方案 '{scheme.name}' 已加载!")
def delete_css_scheme(self):
"""删除 CSS 方案"""
scheme_name = self.scheme_var.get()
if not scheme_name:
messagebox.showwarning("提示", "请选择一个方案")
return
# 根据名称查找方案
scheme = None
for s in self.editor.theme_manager.get_all_css_schemes():
if s.name == scheme_name:
scheme = s
break
if not scheme:
messagebox.showerror("错误", "方案不存在")
return
if messagebox.askyesno("确认", f"确定要删除方案 '{scheme.name}' 吗?"):
self.editor.theme_manager.remove_css_scheme(scheme.id)
self.scheme_combo['values'] = self._get_scheme_names()
self.scheme_var.set("")
messagebox.showinfo("成功", "方案已删除!")
def update_preview(self):
"""更新预览"""
md_content = self.md_text.get("1.0", tk.END)
html = self.editor.render(md_content)
# 更新 HTML 源码显示
self.html_text.config(state=tk.NORMAL)
self.html_text.delete("1.0", tk.END)
self.html_text.insert("1.0", html)
self.html_text.config(state=tk.DISABLED)
# 更新 HTML 渲染预览
try:
# 清理 HTML 中的图片 URL移除反引号和多余空格
import re
cleaned_html = html
# 移除 src 属性中的反引号
cleaned_html = re.sub(r'src="\s*`\s*([^"`]+)\s*`\s*"', r'src="\1"', cleaned_html)
cleaned_html = re.sub(r"src='\s*`\s*([^'`]+)\s*`'", r'src="\1"', cleaned_html)
# 移除 src 属性值前后的空格
cleaned_html = re.sub(r'src="\s*([^"]+)\s*"', r'src="\1"', cleaned_html)
# 使用 tkinterweb 的 HtmlFrame 显示预览
# 创建临时文件并加载,以便图片能正确显示
import tempfile
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False, encoding='utf-8') as f:
f.write(cleaned_html)
temp_path = f.name
self.html_preview.load_file(temp_path)
except Exception as e:
print(f"[Editor] 预览更新失败: {e}")
def copy_html(self):
"""复制 HTML 到剪贴板"""
html = self.html_text.get("1.0", tk.END)
self.window.clipboard_clear()
self.window.clipboard_append(html)
messagebox.showinfo("成功", "HTML 已复制到剪贴板!")
def export_html(self):
"""导出 HTML 文件"""
file_path = filedialog.asksaveasfilename(
defaultextension=".html",
filetypes=[("HTML 文件", "*.html"), ("所有文件", "*.*")]
)
if file_path:
md_content = self.md_text.get("1.0", tk.END)
self.editor.export_html(md_content, file_path)
messagebox.showinfo("成功", f"HTML 已导出到:\n{file_path}")
def open_in_browser(self):
"""在浏览器中打开预览"""
md_content = self.md_text.get("1.0", tk.END)
html = self.editor.render(md_content)
# 创建临时文件
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False, encoding='utf-8') as f:
f.write(html)
temp_path = f.name
# 在浏览器中打开
webbrowser.open(f"file://{temp_path}")
def run(self):
"""运行编辑器"""
if not hasattr(self.window, '_is_main'):
self.window.mainloop()
def open_editor(parent=None):
"""打开 Markdown 编辑器窗口"""
editor = MarkdownEditorWindow(parent)
return editor
if __name__ == "__main__":
editor = MarkdownEditorWindow()
editor.run()