背景

在WPF开发时,要实现网页上的富文本编辑器的效果,在文本框中既可以输入文字,也可以插入图片,需要用到RichTextBox控件。但是要把RichTextBox中的内容转成Html存储,或者将Html展示在RichTextBox中,网上没有现成的工具。

本文简单地介绍了通过C#实现了WPF中文字+图片的富文本转换功能。

实现思路

概述

首先构建一个包含RichTextBox的简单界面,输入文字/插入图片后,在控件的TextChanged Hook中断点调试,以理解程序运行时,RichTextBox暂存文字和图片的结构,其实与web页面有诸多相似之处。

RichTextBox中的Document属性就是用于存放DOM的,Document.Blocks内的Block为Paragraph时,就是用于存储文字和图片的。Paragraph可以理解为html中的p标签,内部的Inlines则是具体的文字/图片容器。存放文字的Run/Span和存放图片的InlineUIContianer都继承自Inline。

RichTextBox转Html

将RichTextBox实时转成Html很简单,在TextChanged Hook中监听RichTextBox中内容的改变,并调用RichTextHandler.FlowDocumentToHtml即可。

实现思路:遍历RichTextBox.Document.Blocks.Inlines,识别其中的文字和图片元素,拼接Html。

Html转RichTextBox

将Html转成RichTextBox比上一步更为复杂,我们可以在RichTextBox的Loaded Hook中,调用RichTextHandler.HtmlToFlowDocument实现。

实现思路(假设输入的Html为上一步转出的标准Html):

  1. 通过正则,匹配Html中的p标签

  2. 提取p标签中的内容,判断是否包含图片

  3. 通过正则,提取图片前的文字/图片url/图片后的文字,分别按序创建对应的容器,按照概述中的结构,完成Document.Blocks的装配,即可实现,详见实现代码步骤。

其他诸如新增按钮插入图片,ctrl V复制文本等,均可以采用此思路进行转换。

实现代码

public class RichTextBoxHandler
    {

        public static string FlowDocumentToHtml(FlowDocument fd)
        {
            try
            {
                string html = "";
                foreach (Block block in fd.Blocks)
                {
                    if (block is Paragraph)
                    {
                        html += "<p>";
                        Paragraph paragraph = (Paragraph)block;
                        foreach (Inline inline in paragraph.Inlines)
                        {
                            if (inline is Run)
                            {
                                html += HttpUtility.HtmlEncode(new TextRange(inline.ContentStart, inline.ContentEnd).Text);
                            }
                            if (inline is Span)
                            {
                                html += HttpUtility.HtmlEncode(new TextRange(inline.ContentStart, inline.ContentEnd).Text);
                            }
                            if (inline is InlineUIContainer)
                            {
                                InlineUIContainer uiContainer = (InlineUIContainer)inline;
                                if (uiContainer.Child is Image)
                                {
                                    Image image = (Image)uiContainer.Child;
                                    string source = image.ToolTip.ToString();
                                    string src = '/' + source.Replace(ClientDomain.nowExeDir, "").Replace("\\", @"/");
                                    html += $"<img src='{src}' />";
                                }
                            }
                        }
                        html += "</p>";
                    }
                }
                return html;
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
            }
        }

        public static void HtmlToFlowDocument(string html, RichTextBox rtb)
        {
            try
            {
                if (rtb == null) return;
                rtb.Document.Blocks.Clear();
                if (html == null) return;

                Regex pReg = new Regex(@"<p[^>]*?>[\w\W]*?<\/p>", RegexOptions.IgnoreCase);
                MatchCollection pCollection = pReg.Matches(html);

                if (pCollection.Count > 0)
                {
                    foreach (Match mt in pCollection)
                    {
                        Regex nopReg = new Regex(@"</?[p|P][^>]*>", RegexOptions.IgnoreCase);
                        string text = nopReg.Replace(mt.Groups[0].Value, "");//根据内容循序减掉
                        if (text.IndexOf("<img src=") > -1)
                        {
                            Regex imgReg = new Regex(@"<img\b[^<>]*?\bsrc[\s\t\r\n]*=[\s\t\r\n]*[""']?[\s\t\r\n]*(?<imgUrl>[^\s\t\r\n""'<>]*)[^<>]*?/?[\s\t\r\n]*>", RegexOptions.IgnoreCase);
                            MatchCollection imgMatchs = imgReg.Matches(text);//匹配内容中含有图片资源的集合

                            if (imgMatchs.Count > 0)
                            {
                                int index = 0;
                                foreach (Match imgmt in imgMatchs)
                                {//一段中可能存在多个图片,循环截取
                                    //本段总内容
                                    Paragraph p = new Paragraph();

                                    #region 图片前文本
                                    if (text.IndexOf(imgmt.Groups[0].Value) > 0)
                                    {
                                        string pretext = text.Substring(0, text.IndexOf(imgmt.Groups[0].Value));
                                        Run run = new Run();
                                        run.Text = HttpUtility.HtmlDecode(pretext);
                                        p.Inlines.Add(run);
                                        text = text.Substring(text.IndexOf(imgmt.Groups[0].Value));
                                    }
                                    #endregion

                                    #region 图片
                                    string imgUrl = imgmt.Groups[1].Value;
                                    string localsource = ClientDomain.nowExeDir + imgUrl.Replace(@"/", "\\");
                                    if (File.Exists(localsource))
                                    {
                                        Image img = new Image();
                                        img.ToolTip = localsource;
                                        BitmapImage bImg = new BitmapImage();
                                        img.IsEnabled = true;
                                        bImg.BeginInit();
                                        bImg.CacheOption = BitmapCacheOption.OnLoad;
                                        //注意:图片通过这种方式刷入缓存,可以有效解决删除本地图片的占用问题
                                        using (Stream ms = new MemoryStream(File.ReadAllBytes(localsource)))
                                        {
                                            bImg.StreamSource = ms;
                                            bImg.EndInit();
                                            bImg.Freeze();
                                        }
                                        img.Source = bImg;
                                        if (bImg.Height > 200 || bImg.Width > 200)
                                        {
                                            img.Height = bImg.Height * 0.3;
                                            img.Width = bImg.Width * 0.3;
                                        }
                                        img.Stretch = Stretch.Uniform;  //图片缩放模式
                                        InlineUIContainer uiContainer = new InlineUIContainer();
                                        uiContainer.Child = img;
                                        p.Inlines.Add(uiContainer);
                                    }
                                    text = text.Substring(imgmt.Groups[0].Value.Length);
                                    #endregion

                                    #region 图片后文本
                                    if (imgMatchs.Count == (index + 1))
                                    {
                                        if (!string.IsNullOrEmpty(text))
                                        {
                                            Run run = new Run();
                                            run.Text = HttpUtility.HtmlDecode(text);
                                            p.Inlines.Add(run);
                                        }
                                    }
                                    #endregion

                                    rtb.Document.Blocks.Add(p);
                                    index++;
                                }
                            }
                        }
                        else
                        {
                            Run run = new Run();
                            run.Text = HttpUtility.HtmlDecode(text);
                            Paragraph p = new Paragraph();
                            p.Inlines.Add(run);
                            rtb.Document.Blocks.Add(p);
                        }
                    }
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
            }
        }

    }

其他功能的实现

本文仅对RichTextBox和Html互转进行了简单的阐述,其他功能暂不做赘述,如果遇到问题,欢迎与博主一起探讨和指正!