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

在命令行中输入
npm install @tinymce/tinymce-vue tinymce

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

在wp项目下新建 文件夹,TinyMCEEdtior,将桌面TinyMCEEdtior-temp\node_modules 下的整个tinymce复制到 项目中TinyMCEEdtior文件夹下。



//这段代码将 在程序编译时候,把TinyMCEEdtior项目文件夹下的文件,全部复制到Bin 文件夹里。
<ItemGroup> <Content Include="TinyMCEEdtior\**\*.*"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </Content> </ItemGroup> <ItemGroup> <Content Update="TinyMCEEdtior\tinymce\editor.html"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </Content> </ItemGroup>
第三步骤,tinymce文件夹下 手动新建 editor.html ,然后黏贴以下代码

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>题库编辑器</title>
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
}
#editor-container {
height: 100%;
}
</style>
</head>
<body>
<div id="editor-container">
<textarea id="editor"></textarea>
</div>
<script src="tinymce.min.js"></script>
<script>
tinymce.init({
selector: '#editor',
license_key: 'gpl',
base_url: '.',
suffix: '.min',
height: '100%',
menubar: false,
plugins: 'lists table image link code',
// 工具栏里加上自定义按钮 mylocalimage
toolbar: 'undo redo | bold italic underline | bullist numlist | table image link mylocalimage | code',
branding: false,
promotion: false,
automatic_uploads: true,
file_picker_types: 'image',
file_picker_callback: function (callback, value, meta) {
if (meta.filetype === 'image') {
if (window.chrome && window.chrome.webview) {
window.__imagePickedCallback = callback;
window.chrome.webview.postMessage(JSON.stringify({ type: 'pickImage' }));
}
}
},
setup: function (editor) {
// 注册自定义工具栏按钮
editor.ui.registry.addButton('mylocalimage', {
icon: 'image',
tooltip: '插入本地图片',
onAction: function () {
if (window.chrome && window.chrome.webview) {
// 直接发消息,不经过 Insert/Edit Image 对话框
window.chrome.webview.postMessage(JSON.stringify({ type: 'pickImageDirect' }));
}
}
});
editor.on('init', function () {
if (window.chrome && window.chrome.webview) {
window.chrome.webview.postMessage(JSON.stringify({ type: 'ready' }));
}
});
editor.on('change keyup', function () {
var content = editor.getContent();
if (window.chrome && window.chrome.webview) {
window.chrome.webview.postMessage(JSON.stringify({ type: 'contentChanged', content: content }));
}
});
}
});
</script>
</body>
</html>
第四步骤,在nuget 中添加 WebViewe2 ,WebViewe2 运行基于本机电脑已经安装edge浏览器。
用户电脑很可能没有按住WebViewe2,最好采用Fixed Version模式。
考虑到你的目标客户是 B 端机构(学校、培训机构),这些环境往往网络管控严格、不允许随意联网下载组件,Fixed Version 更稳妥。具体做法:
- 去 Microsoft 官方 WebView2 下载页面,下载对应你目标系统架构(x64/ARM64)的 Fixed Version 运行时压缩包
- 把这个运行时文件夹整个放进你的安装包里
- 程序启动时指定运行时路径,而不是用系统全局安装的版本:

第五步骤,在项目中加入该控件
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="EditorWebView" /> </Grid> </UserControl>
C#
using System; using System.IO; using System.Text.Json; using System.Windows; using System.Windows.Controls; using Microsoft.Web.WebView2.Core; using Microsoft.Win32; namespace IndividualQAlibrary.Theme.Controls { public class BridgeMessage { public string Type { get; set; } public string Html { get; set; } } public partial class RichTextBoxMVVMEditorView : UserControl { private readonly string _imageStoreDir = Path.Combine(AppContext.BaseDirectory, "QAImages"); public RichTextBoxMVVMEditorView() { InitializeComponent(); Loaded += async (_, _) => { System.Diagnostics.Debug.WriteLine("【Loaded】事件触发"); await EditorWebView.EnsureCoreWebView2Async(); System.Diagnostics.Debug.WriteLine("【CoreWebView2】初始化完成"); Directory.CreateDirectory(_imageStoreDir); var tinymceDir = Path.Combine(AppContext.BaseDirectory, "TinyMCEEdtior", "tinymce"); EditorWebView.CoreWebView2.SetVirtualHostNameToFolderMapping( "tinymce-app", tinymceDir, CoreWebView2HostResourceAccessKind.Allow); EditorWebView.CoreWebView2.SetVirtualHostNameToFolderMapping( "qa-images", _imageStoreDir, CoreWebView2HostResourceAccessKind.Allow); System.Diagnostics.Debug.WriteLine("【虚拟域名映射】完成"); EditorWebView.CoreWebView2.WebMessageReceived += async (sender, e) => { var json = e.TryGetWebMessageAsString(); if (string.IsNullOrWhiteSpace(json)) return; BridgeMessage msg; try { msg = JsonSerializer.Deserialize<BridgeMessage>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); } catch (Exception ex) { return; } catch { return; } if (msg?.Type == "pickImage") { var virtualUrl = PickAndSaveImage(); if (virtualUrl == null) return; var fileName = virtualUrl.Substring(virtualUrl.LastIndexOf('/') + 1); var script = $"window.__imagePickedCallback && window.__imagePickedCallback('{virtualUrl}', {{ title: '{fileName}' }});"; await EditorWebView.CoreWebView2.ExecuteScriptAsync(script); } else if (msg?.Type == "pickImageDirect") { var virtualUrl = PickAndSaveImage(); if (virtualUrl == null) return; var script = $"tinymce.activeEditor.insertContent('<img src=\"{virtualUrl}\" alt=\"\" />');"; await EditorWebView.CoreWebView2.ExecuteScriptAsync(script); } }; EditorWebView.CoreWebView2.Navigate("https://tinymce-app/editor.html"); }; } private string PickAndSaveImage() { try { var dialog = new OpenFileDialog { Filter = "图片文件|*.png;*.jpg;*.jpeg;*.gif;*.bmp", Title = "选择图片" }; var ownerWindow = Window.GetWindow(this); bool? result = ownerWindow != null ? dialog.ShowDialog(ownerWindow) : dialog.ShowDialog(); if (result != true) return null; var ext = Path.GetExtension(dialog.FileName); var newFileName = $"{Guid.NewGuid()}{ext}"; var targetPath = Path.Combine(_imageStoreDir, newFileName); File.Copy(dialog.FileName, targetPath, overwrite: true); return $"https://qa-images/{newFileName}"; } catch (Exception ex) { return null; } } } }
引用
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号