一个WinForm富文本编辑器控件

C#

浏览数:546

2019-8-31

WinForm 上的富文本编辑器简直不要太少,虽然有 RichEdit,但是这个鬼极难用而且复杂,在插入图片和表格的时候简直抓狂,还要理解复杂的 RTF 格式。

我希望有一个文本控件,包括基本的格式设置,图片表格插入等,能够自定义打开文件,保存和插入图片等功能,并且它的依赖项要尽可能少,因为是 WinForm 控件,也不用需要跨 Linux 和 Osx 平台,只用在 Windows 下保持兼容就行。这么一来,似乎并没有好的免费控件可用了 …

但是,Js 的这类控件就比比皆是了,有没有办法移到 C# WinForm 上来用呢,答案当然是 YES!

首先要显示 HTML页面和JS的执行,必须要由 WebBrowser 控件承载,所以我们的整个编辑器都会在 WebBrowser 中呈现。接下来是编辑器控件了,尽量轻量级,最好是美观点,文档全面,接口丰富的,我找到我用过的一款。

summernote https://github.com/summernote/summernote/

接下来是编辑器的界面了,创建一个 HTML 页面,呈现编辑器,并设置编译方式为嵌入的资源,将所有的脚本文件内容全部和样式内容写在这个 HTML 页面中,这样一来,页面可能达到惊人的几百 KB,不过这没关系,除了脚本之外,还会有字体资源,关于字体资源如何嵌入在 CSS 中,可以通过下列方式:

@font-face {
    font-family: "name";
    font-style: normal;
    font-weight: normal;
    src: url(data:font/truetype;charset=utf-8;base64,XXX);
}

需要注意的是 WebBrowser 是 IE 核心,所以只需要 eot 格式的字体即可。关于如何将字体生出你个 Base64 字符串,猛击 http://www.motobit.com/util/base64-decoder-encoder.asp

编辑器的事件,我们写成接口,由调用方自行实,分别是保存按钮、打开文件按钮、插入图片按钮,异常等事件。

 public interface IKBrowserEventListener
 {
     void onSaveClicked();
     void onOpenFileClicked();
     void onInsertImageClicked();
     void onError(Exception ex);
 }

如何直接使用 WebBrowser 控件的话,会有一些奇怪的问题,比如阻止脚本错误执行的对话框依旧会执行 … 可是直接使用 COM+ 接口 IWebBrowser2,需要引用 Microsoft Internet Controls。

public class KBrowser : System.Windows.Forms.WebBrowser
{
    private SHDocVw.IWebBrowser2 Iwb2;

    public KBrowser()
    {
        NewWindow += KBrowser_NewWindow;
    }

    private void KBrowser_NewWindow(object sender, System.ComponentModel.CancelEventArgs e)
    {
        KBrowser kb = sender as KBrowser;
        string url = kb.StatusText;
        Navigate(url);
        e.Cancel = true;
    }

    protected override void AttachInterfaces(object nativeActiveXObject)
    {
        Iwb2 = (SHDocVw.IWebBrowser2)nativeActiveXObject;
        Iwb2.Silent = true;
        base.AttachInterfaces(nativeActiveXObject);
    }

    protected override void DetachInterfaces()
    {
        Iwb2 = null;
        base.DetachInterfaces();
    } 
}

接下来编辑器控件可以用用户控件,拖一个 KBrowser即可,Dock 为 Fill 铺满整个控件。它至少拥有下列属性:

/// <summary>
/// 编辑器的事件监听器
/// </summary>
public IKBrowserEventListener KBrowserEventListener { get; set; }
/// <summary>
/// 获取或设置编辑器中Html值
/// </summary>
public string Html
{
    get
    {
        try
        {
            return kBrowser1.Document.InvokeScript("getHtml", null).ToString();
        }
        catch (Exception ex)
        {
            onError(ex);
            return "";
        }
    }
    set
    {
        try
        {
            kBrowser1.Document.InvokeScript("setHtml", new string[] { value });
        }
        catch (Exception ex)
        {
            onError(ex);
        }
    }
}

在这个 Html 的属性中,包括了 JS 和 C# 的互调用代码,这里是在 C# 中调用 JS 的一个方法,并且一个有返回值但无参数,一个有参数但无返回值。

如果在 JS 里调用 C#,需要将类设置为 ComVisible(true),应用到方法级不知是否也可以,没试过。然后 window.external.XXX() 的方式调用,XXX 是 C# 的方法。有没有参看重载就知道了。

在编辑器控件 OnLoad 时加载 Html 编辑器,因为是嵌入的资源,所以不是通过 File IO 的方式。

 private void KEditor_Load(object sender, EventArgs e)
 {
     try
     {
         Stream sm = Assembly.GetExecutingAssembly().GetManifestResourceStream("Knote.Widgets.Resources.editor.html");
         byte[] bs = new byte[sm.Length];
         sm.Read(bs, 0, (int)sm.Length);
         sm.Close();
         UTF8Encoding con = new UTF8Encoding();
         string str = con.GetString(bs);
         kBrowser1.DocumentText = str;
     }
     catch (Exception ex)
     {
         onError(ex);
     }
 }

然后添加一些方法供 JS 调用,基本就是上述接口中的方法,调用前一定要判断是否空指针。

/// <summary>
/// 保存按钮点击的事件,请不要调用,而是使用监听器
/// </summary>
public void onSaveButtonClick()
{
    if (KBrowserEventListener != null)
        KBrowserEventListener.onSaveClicked();
}

/// <summary>
/// 打开文件按钮点击的事件,请不要调用,而是使用监听器
/// </summary>
public void onOpenFileButtonClick()
{
    if (KBrowserEventListener != null)
        KBrowserEventListener.onOpenFileClicked();
}

/// <summary>
/// 插入图片按钮点击的事件,请不要调用,而是使用监听器
/// </summary>
public void onInsertPictureButtonClick()
{
    if (KBrowserEventListener != null)
        KBrowserEventListener.onInsertImageClicked();
}

插入普通文本和插入 HTML 源代码。

/// <summary>
/// 插入一个节点,它将由 div 元素包裹
/// </summary>
/// <param name="html"></param>
public void InsertNode(string html)
{
    try
    {
        kBrowser1.Document.InvokeScript("insertNode", new string[] { html });
    }
    catch (Exception ex)
    {
        onError(ex);
    }
}

/// <summary>
/// 插入文本
/// </summary>
/// <param name="text"></param>
public void InsertText(string text)
{
    try
    {
        kBrowser1.Document.InvokeScript("insertText", new string[] { text });
    }
    catch (Exception ex)
    {
        onError(ex);
    }
}

为什么是 insertText 和 insertNode,这个是 JS 控件决定的,知道流程后,就可以封装任意编辑器了,最终完成效果如下,并且设计阶段也是所见即所得。

WinForm 编辑器

封装后只有一个 DLL,地址 https://github.com/yahch/kwig

作者:天兵公园