WinForm 上的富文本編輯器簡直不要太少桶雀,雖然有 RichEdit氧猬,但是這個鬼極難用而且復(fù)雜盅抚,在插入圖片和表格的時候簡直抓狂,還要理解復(fù)雜的 RTF 格式妄均。
我希望有一個文本控件哪自,包括基本的格式設(shè)置壤巷,圖片表格插入等瞧毙,能夠自定義打開文件,保存和插入圖片等功能矩动,并且它的依賴項要盡可能少释漆,因為是 WinForm 控件,也不用需要跨 Linux 和 Osx 平臺示姿,只用在 Windows 下保持兼容就行逊笆。這么一來,似乎并沒有好的免費控件可用了 ...
但是荧琼,Js 的這類控件就比比皆是了差牛,有沒有辦法移到 C# WinForm 上來用呢堰乔,答案當(dāng)然是 YES!
首先要顯示 HTML頁面和JS的執(zhí)行,必須要由 WebBrowser 控件承載侦讨,所以我們的整個編輯器都會在 WebBrowser 中呈現(xiàn)苟翻。接下來是編輯器控件了,盡量輕量級沈条,最好是美觀點诅炉,文檔全面屋厘,接口豐富的月而,我找到我用過的一款父款。
summernote https://github.com/summernote/summernote/
接下來是編輯器的界面了,創(chuàng)建一個 HTML 頁面憨攒,呈現(xiàn)編輯器浓恶,并設(shè)置編譯方式為嵌入的資源,將所有的腳本文件內(nèi)容全部和樣式內(nèi)容寫在這個 HTML 頁面中包晰,這樣一來伐憾,頁面可能達(dá)到驚人的幾百 KB,不過這沒關(guān)系树肃,除了腳本之外胸嘴,還會有字體資源,關(guān)于字體資源如何嵌入在 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 格式的字體即可。關(guān)于如何將字體生出你個 Base64 字符串闸婴,猛擊 http://www.motobit.com/util/base64-decoder-encoder.asp
編輯器的事件芍躏,我們寫成接口,由調(diào)用方自行實溺欧,分別是保存按鈕、打開文件按鈕芥牌、插入圖片按鈕聂使,異常等事件。
public interface IKBrowserEventListener
{
void onSaveClicked();
void onOpenFileClicked();
void onInsertImageClicked();
void onError(Exception ex);
}
如何直接使用 WebBrowser 控件的話弃理,會有一些奇怪的問題屎蜓,比如阻止腳本錯誤執(zhí)行的對話框依舊會執(zhí)行 ... 可是直接使用 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>
/// 編輯器的事件監(jiān)聽器
/// </summary>
public IKBrowserEventListener KBrowserEventListener { get; set; }
/// <summary>
/// 獲取或設(shè)置編輯器中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# 的互調(diào)用代碼榨婆,這里是在 C# 中調(diào)用 JS 的一個方法,并且一個有返回值但無參數(shù),一個有參數(shù)但無返回值烟央。
如果在 JS 里調(diào)用 C#歪脏,需要將類設(shè)置為 ComVisible(true),應(yīng)用到方法級不知是否也可以钞艇,沒試過。然后 window.external.XXX() 的方式調(diào)用挺物,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 調(diào)用豪娜,基本就是上述接口中的方法绒疗,調(diào)用前一定要判斷是否空指針。
/// <summary>
/// 保存按鈕點擊的事件惕虑,請不要調(diào)用磨镶,而是使用監(jiān)聽器
/// </summary>
public void onSaveButtonClick()
{
if (KBrowserEventListener != null)
KBrowserEventListener.onSaveClicked();
}
/// <summary>
/// 打開文件按鈕點擊的事件,請不要調(diào)用伟叛,而是使用監(jiān)聽器
/// </summary>
public void onOpenFileButtonClick()
{
if (KBrowserEventListener != null)
KBrowserEventListener.onOpenFileClicked();
}
/// <summary>
/// 插入圖片按鈕點擊的事件脐嫂,請不要調(diào)用账千,而是使用監(jiān)聽器
/// </summary>
public void onInsertPictureButtonClick()
{
if (KBrowserEventListener != null)
KBrowserEventListener.onInsertImageClicked();
}
插入普通文本和插入 HTML 源代碼。
/// <summary>
/// 插入一個節(jié)點鞭衩,它將由 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 控件決定的瑞佩,知道流程后坯台,就可以封裝任意編輯器了,最終完成效果如下御雕,并且設(shè)計階段也是所見即所得滥搭。
封裝后只有一個 DLL瑟匆,地址 https://github.com/yahch/kwig