如何在wpf项目里插入Quill 编辑器
第一步骤,要安装Node.js,下载地址:https://nodejs.org/zh-cn/download
第二步骤,生成quill。
不需要懂前端构建工具链,只是借用 npm 当一个"下载器",比手动去 CDN 上一个个右键另存为更可靠(能保证版本一致、文件完整)。
在任意临时目录执行,我这里直接在桌面执行cmd,不需要在 WPF 项目里

mkdir quill-temp && cd quill-temp
npm init -y
npm install quill

安装完成后你需要的文件就在:

quill-temp/node_modules/quill/dist/
├── quill.js ← 主库(未压缩,方便调试)
├── quill.js.map
├── quill.snow.css ← snow 主题样式(你用的工具栏样式)
├── quill.bubble.css
├── quill.core.css
└── ...

在项目中新建QuillEditor 文件夹,并且把 quill.js 和 quill.snow.css 这两个文件复制到里面。这三个文件都要选中 生成操作:内容。

第三步骤,QuillEditor 文件夹手动新建 editor.html ,然后贴入一下代码。
<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8" /> <title>Editor</title> <link rel="stylesheet" href="quill.snow.css"> <style> html, body { margin: 0; padding: 0; height: 100%; } #editor-container { height: 100%; display: flex; flex-direction: column; } #toolbar { flex-shrink: 0; } #editor { flex: 1; overflow-y: auto; } </style> </head> <body> <div id="editor-container"> <div id="toolbar"></div> <div id="editor"></div> </div> <script src="quill.js"></script> <script> // 图片插入交给 C# 端处理,而不是用 Quill 默认的 base64 内嵌 function imageHandler() { postToHost({ type: 'requestImage' }); } const toolbarOptions = [ ['bold', 'italic', 'underline', 'strike'], [{ 'script': 'sub' }, { 'script': 'super' }], ['clean'], ['image'] ]; const quill = new Quill('#editor', { theme: 'snow', modules: { toolbar: { container: toolbarOptions, handlers: { image: imageHandler } } } }); // 统一的发送消息函数,脱离 WebView2 环境时不报错 function postToHost(msg) { if (window.chrome && window.chrome.webview) { window.chrome.webview.postMessage(msg); } } // C# 端选完图片后调用这个函数把路径插进编辑器 function insertImageAtCursor(path) { const range = quill.getSelection(true); quill.insertEmbed(range.index, 'image', path); } // 内容变化时通知 C#(用于双向绑定) quill.on('text-change', function () { postToHost({ type: 'contentChanged', html: quill.root.innerHTML }); }); // 提供给 C# 主动调用的取值/赋值方法 function getContent() { return quill.root.innerHTML; } function setContent(html) { quill.root.innerHTML = html; } </script> </body> </html>
第四步骤,在nuget 中添加 WebViewe2 ,WebViewe2 运行基于本机电脑已经安装edge浏览器。
用户电脑很可能没有按住WebViewe2,最好采用Fixed Version模式。
考虑到你的目标客户是 B 端机构(学校、培训机构),这些环境往往网络管控严格、不允许随意联网下载组件,Fixed Version 更稳妥。具体做法:
- 去 Microsoft 官方 WebView2 下载页面,下载对应你目标系统架构(x64/ARM64)的 Fixed Version 运行时压缩包
- 把这个运行时文件夹整个放进你的安装包里
- 程序启动时指定运行时路径,而不是用系统全局安装的版本:
第五步骤,在nuget 中添加 WebViewe2

第六步骤,在项目中加入该控件
xaml
<UserControl x:Class="IndividualQAlibrary.Theme.Controls.RichTextBoxMVVMEditorView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:i="http://schemas.microsoft.com/xaml/behaviors" xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf" mc:Ignorable="d" x:Name="Editor" d:DesignHeight="450" d:DesignWidth="800"> <Grid> <wv2:WebView2 x:Name="QuillWebView" HorizontalAlignment="Stretch" /> </Grid> </UserControl>
C#
using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Text.Json; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Media; using System.Windows.Media.Imaging; using Microsoft.Web.WebView2.Core; using Microsoft.Win32; using System.IO; using System.Text.Json; using Microsoft.Web.WebView2.Core; namespace IndividualQAlibrary.Theme.Controls { public class BridgeMessage { public string Type { get; set; } public string Html { get; set; } } public partial class RichTextBoxMVVMEditorView : UserControl { public RichTextBoxMVVMEditorView() { InitializeComponent(); Loaded += OnLoaded; } private async void OnLoaded(object sender, RoutedEventArgs e) { // 触发 CoreWebView2 初始化,必须 await 完成 await QuillWebView.EnsureCoreWebView2Async(null); QuillWebView.CoreWebView2.Settings.AreDevToolsEnabled = true; // 此时 CoreWebView2 才不为 null SetupVirtualHost(); SetupMessageBridge(); // 用伪域名加载页面,而不是 file:// 或本地路径 QuillWebView.Source = new Uri("https://quill.localapp/editor.html"); } private void SetupVirtualHost() { var assetsPath = Path.Combine( AppDomain.CurrentDomain.BaseDirectory, "QuillEditor"); // 改成实际的文件夹名 QuillWebView.CoreWebView2.SetVirtualHostNameToFolderMapping( "quill.localapp", assetsPath, CoreWebView2HostResourceAccessKind.Allow); } private void SetupMessageBridge() { QuillWebView.CoreWebView2.WebMessageReceived += OnWebMessageReceived; } private string PickAndSaveImageFile() { // TODO: 实现弹出文件选择框、保存图片到本地目录的逻辑 return null; } private async void OnWebMessageReceived( object sender, CoreWebView2WebMessageReceivedEventArgs e) { string json = e.WebMessageAsJson; var msg = JsonSerializer.Deserialize<BridgeMessage>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); switch (msg.Type) { case "requestImage": var localPath = PickAndSaveImageFile(); if (localPath != null) { var safePath = JsonSerializer.Serialize(localPath); await QuillWebView.CoreWebView2.ExecuteScriptAsync( $"insertImageAtCursor({safePath})"); } break; case "contentChanged": // TODO: 确认 ViewModel 类型后再启用这段同步逻辑 // if (DataContext is XxxViewModel vm) // { // vm.RichContent = msg.Html; // } UpdateContentFromJs(msg.Html); break; } } public static readonly DependencyProperty RichContentProperty = DependencyProperty.Register( nameof(RichContent), typeof(string), typeof(RichTextBoxMVVMEditorView), new FrameworkPropertyMetadata( string.Empty, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnRichContentChanged)); public string RichContent { get => (string)GetValue(RichContentProperty); set => SetValue(RichContentProperty, value); } private bool _isUpdatingFromJs; // 防止 JS→C#→JS 的循环回写 private static async void OnRichContentChanged( DependencyObject d, DependencyPropertyChangedEventArgs e) { var control = (RichTextBoxMVVMEditorView)d; if (control._isUpdatingFromJs) return; // ViewModel 改值是因为 JS 推过来的,不要再推回去 if (control.QuillWebView.CoreWebView2 != null) { await control.SetEditorContentAsync((string)e.NewValue); } } // JS 端内容变化时调用这个,更新依赖属性,但要标记来源避免死循环 private void UpdateContentFromJs(string html) { _isUpdatingFromJs = true; RichContent = html; _isUpdatingFromJs = false; } public async Task SetEditorContentAsync(string html) { if (QuillWebView.CoreWebView2 == null) return; var safeHtml = JsonSerializer.Serialize(html ?? string.Empty); await QuillWebView.CoreWebView2.ExecuteScriptAsync( $"setContent({safeHtml})"); } } }
引用
xmlns:controls="clr-namespace:IndividualQAlibrary.Theme.Controls" <controls:RichTextBoxMVVMEditorView Grid.Row="2" Margin="20,0,20,0" Grid.ColumnSpan="2"/> <TextBlock Grid.Row="3" Margin="20,0,0,0" VerticalAlignment="Center" FontSize="16" Foreground="#3E4A58" FontWeight="Bold" Grid.ColumnSpan="2">正确答案·选项 : </TextBlock> <controls:RichTextBoxMVVMEditorView
效果


浙公网安备 33010602011771号