如何在wpf项目里插入Quill 编辑器

第一步骤,要安装Node.js,下载地址:https://nodejs.org/zh-cn/download

第二步骤,生成quill。

不需要懂前端构建工具链,只是借用 npm 当一个"下载器",比手动去 CDN 上一个个右键另存为更可靠(能保证版本一致、文件完整)。
在任意临时目录执行,我这里直接在桌面执行cmd,不需要在 WPF 项目里

QQ截图20260617122801

 


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

QQ截图20260617111930
安装完成后你需要的文件就在:

QQ截图20260617122916

 


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

QQ截图20260617112018

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

 

QQ截图20260617122321

 

第三步骤,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 更稳妥。具体做法:

  1. 去 Microsoft 官方 WebView2 下载页面,下载对应你目标系统架构(x64/ARM64)的 Fixed Version 运行时压缩包
  2. 把这个运行时文件夹整个放进你的安装包里
  3. 程序启动时指定运行时路径,而不是用系统全局安装的版本:

 

第五步骤,在nuget 中添加 WebViewe2 

QQ截图20260617123449

 第六步骤,在项目中加入该控件

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  

效果

QQ截图20260617133724

 

posted @ 2026-06-17 13:38  小林野夫  阅读(5)  评论(0)    收藏  举报
原文链接:https://www.cnblogs.com/cdaniu/