一个WinForm富文本编辑器控件

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容