""" 微信 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("", 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("", lambda e: settings_canvas.configure(scrollregion=settings_canvas.bbox("all"))) settings_canvas.bind("", 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("<>", 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("<>", 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()