WPF仿QQ聊天框表情文字混排实现

二话不说。先上图
这里写图片描述

图中分别有文件、文本+表情、纯文本的展示,对于同一个list不同的展示形式,很明显,应该用多个DataTemplate,那么也就需要DataTemplateSelector了:

class MessageDataTemplateSelector : DataTemplateSelector
    {
        public override System.Windows.DataTemplate SelectTemplate(object item, System.Windows.DependencyObject container)
        {
            Window win = Application.Current.MainWindow;
            var myUserNo = UserLoginInfo.GetInstance().UserNo;
            if (item!=null)
            {
                NIMIMMessage m = item as NIMIMMessage;
                if (m.SenderID==myUserNo)
                {
                    switch (m.MessageType)
                    {
                        case NIMMessageType.kNIMMessageTypeAudio:
                        case NIMMessageType.kNIMMessageTypeVideo:
                            return win.FindResource("self_media") as DataTemplate;
                        case NIMMessageType.kNIMMessageTypeFile:
                            return win.FindResource("self_file") as DataTemplate;
                        case NIMMessageType.kNIMMessageTypeImage:
                            return win.FindResource("self_image") as DataTemplate;
                        case NIMMessageType.kNIMMessageTypeText:
                            return win.FindResource("self_text") as DataTemplate;
                        default:
                            break;
                    }
                }
                else
                {
                    switch (m.MessageType)
                    {
                        case NIMMessageType.kNIMMessageTypeAudio:
                        case NIMMessageType.kNIMMessageTypeVideo:
                            return win.FindResource("friend_media") as DataTemplate;
                        case NIMMessageType.kNIMMessageTypeFile:
                            return win.FindResource("friend_file") as DataTemplate;
                        case NIMMessageType.kNIMMessageTypeImage:
                            return win.FindResource("friend_image") as DataTemplate;
                        case NIMMessageType.kNIMMessageTypeText:
                            return win.FindResource("friend_text") as DataTemplate;
                        default:
                            break;
                    }
                }
            }
            return null;
        }
    }

以上一共有8个DateTemplate,friend和self的区别就在于一个在左一个在右,我这边就放friend_text的样式代码好了,因为本篇主要说的是表情和文字的混排:

<Window.Resources>
<DataTemplate x:Key="friend_text">
            <Grid Margin="12 6">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="32"/>
                    <ColumnDefinition Width="*"/>
                </Grid.ColumnDefinitions>
                <Image Source="{Binding TalkID}"  HorizontalAlignment="Center" VerticalAlignment="Center">
                    <Image.Clip>
                        <EllipseGeometry RadiusX="16" RadiusY="16" Center="16 16"/>
                    </Image.Clip>
                </Image>
                <Grid HorizontalAlignment="Left" Grid.Column="1"  Background="Transparent" VerticalAlignment="Center" Margin="12 0 0 0">
                    <Border CornerRadius="8" Background="#F0F0F0" Padding="6" >
                        <xctk:RichTextBox FontSize="14"
                                          VerticalScrollBarVisibility="Auto"
                                          HorizontalScrollBarVisibility="Disabled"
                                          Text="{Binding TextContent,Converter={StaticResource ShowImageOrTextConverter}}"
                                          VerticalAlignment="Center"
                                          BorderThickness="0"
                                          IsReadOnly="True"
                                          Background="Transparent">
                            <FlowDocument Name="rtbFlowDoc" PageWidth="{Binding MessageWidth}"/>
                            <xctk:RichTextBox.TextFormatter>
                                 <xctk:XamlFormatter/>
                            </xctk:RichTextBox.TextFormatter>
                        </xctk:RichTextBox>
                    </Border>
                </Grid>
            </Grid>
        </DataTemplate>
</Window.Resources>

以上可以看到,我们使用了RichTextBox这个控件,不过并不是原生的,而是xceed.wpf.toolkit下的,所以别忘了引入命名空间:
xmlns:xctk=”http://schemas.xceed.com/wpf/xaml/toolkit”
为什么要引用这个控件,因为它支持绑定Text -:)

上一篇已经提到过,我们的IM用的是网易云的SDK,在这个SDK里表情也是通过文本发送的,如[微笑]就代表微笑的表情。
那么问题就很明显了——怎么解析这段带有表情的文本,并且把表情显示出来。

private Text GenerateTextMessage(NIMTextMessage m, string senderId)
{
      Text text = new Text();
      text.TextContent = m.TextContent;
      text.TalkID = friendHeadUrl;
      if (!string.IsNullOrEmpty(senderId))
      {
            text.SenderID = senderId;
      }
      var txt = text.TextContent;
      int length = 0;
      if (txt.Contains("[") && txt.Contains("]"))
      {
            StringBuilder str = new StringBuilder();
            List<EmoticonText> emoticonText = new List<EmoticonText>();
            char[] chars = txt.ToCharArray();
            for (int i = 0; i < chars.Length; i++)
            {
                char c = chars[i];
                if (chars[i] == '[')
                {
                    emoticonText.Add(new EmoticonText { Key = "text", Value = str.ToString() });
                    str.Clear();
                    int f = txt.IndexOf(']', i);
                    string es = txt.Substring(i, f - i + 1);
                    XElement node = elementCollection.Where(a => a.Attribute("Tag").Value == es).FirstOrDefault();
                    if (node == null)
                    {
                        str.Append(es);
                        length += (f - i + 1) * 14;
                    }
                    else
                    {
                        emoticonText.Add(new EmoticonText { Key = "emoticon", Value = "../Resources/Emoticon/" + node.Attribute("File").Value });
                        i = f;
                        length += 32;
                    }
                }
                else
                {
                    str.Append(c);
                    length += 14;
                }
            }
            text.TextContent = JsonConvert.SerializeObject(emoticonText);
       }
       else
       {
            List<EmoticonText> textStr = new List<EmoticonText>();
            textStr.Add(new EmoticonText() { Key = "text", Value = txt });
            text.TextContent = JsonConvert.SerializeObject(textStr);
            length = txt.Length * 14;
       }
       length += 24;
       if (length < 38 * 14)
       {
             text.MessageWidth = length.ToString();
       }
       return text;
}

Text是自定义的一个实体,它包含需要绑定到xaml的属性。这里我用EmoticonText这个实体来区分是表情图片还是纯文本。另外,有的同学可能有疑问,这里的length是干嘛用的,看前面那个DataTemplate,其中PageWidth=”{Binding MessageWidth}”,所以这个length是计算当前RichTextBox宽度的,为什么要手动计算宽度呢?因为RichTextBox貌似没提供根据内容自适应宽度,如果我是用TextBox的话,其宽度就会根据其中显示内容的长短进行自适应;那为什么要乘14加28什么的呢?因为我这个是按字符个数来算宽度的,当以14为系数因子的时候,中文显示勉强满意,但是如果是纯英文或数字就不行了,这也是为什么截图里RichTextBox右边还空那么一块;最后加24是因为边距,38是一行最多显示38个中文,如果超过了38个中文还对其计算宽度的话,就会导致其不换行了。
如果有同学有自适应宽度更好的方法,欢迎不吝赐教唷!

都绑定好后,这个时候显示肯定还是不正确的,因为现在TextContent是一个Json字符串,所以我们还差一个Converter:

 class ShowImageOrText : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            var v = JsonConvert.DeserializeObject<List<EmoticonText>>((string)value);
            StringBuilder sb = new StringBuilder();
            foreach (var item in v)
            {
                if (item.Key=="text")
                {
                    sb.Append("<Run>");
                    sb.Append(item.Value);
                    sb.Append("</Run>");
                }
                else
                {
                    sb.Append("<Image Width=\"32\" Source=\"");
                    sb.Append(item.Value);
                    sb.Append("\"/>");
                }
            }
            return @"<Section xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation"" FontFamily=""Microsoft YaHei"" xml:space=""preserve"" TextAlignment=""Left"" LineHeight=""Auto""><Paragraph>" + sb.ToString() + "</Paragraph></Section>";
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

这段代码一看就懂,如果是文本则用run,如果是表情图片,则用image,最后将拼装好的xaml绑定到前端。

下来问题就出现了,当run里面是中文时,前端会显示为???(几个中文就几个?),也就是XamlFormatter并不能正确解析中文,哦到开~尝试了修改各种Language属性以及xml:lang=”en-us”,都是徒劳- -!
根据官网(http://wpftoolkit.codeplex.com/wikipage?title=RichTextBox)介绍,我们是可以自定义formater的,那到底怎么自定义呢,在看了xctk:RichTextBox针对xaml的formatter这块的源码后才明白,
其编码格式用的ASCII,我们只要将其换成UTF8即可:

class RTBXamlFormatter : ITextFormatter
    {
        public string GetText(System.Windows.Documents.FlowDocument document)
        {
            TextRange tr = new TextRange(document.ContentStart, document.ContentEnd);
            using (MemoryStream ms = new MemoryStream())
            {
                tr.Save(ms, DataFormats.Xaml);
                return ASCIIEncoding.Default.GetString(ms.ToArray());
            }
        }

        public void SetText(System.Windows.Documents.FlowDocument document, string text)
        {
            try
            {
                if (String.IsNullOrEmpty(text))
                {
                    document.Blocks.Clear();
                }
                else
                {
                    TextRange tr = new TextRange(document.ContentStart, document.ContentEnd);
                    using (MemoryStream ms = new MemoryStream(Encoding.**UTF8**.GetBytes(text)))
                    {
                        tr.Load(ms, DataFormats.Xaml);
                    }
                }
            }
            catch
            {
                throw new InvalidDataException("Data provided is not in the correct Xaml format.");
            }
        }
    }

记得将DataTemplate中的 <xctk:XamlFormatter/>换成当前这个 <local:RTBXamlFormatter/>

-:)

以上2017-06-06
————————————————————————————————————————————————————

以下编辑于2017-10-25
看到有小伙伴在评论区提问,我这边就再更新一下吧。
在之前的版本上我又做了以下更改:
1.修改richtextbox宽度计算方法
2.修改GenerateTextMessage方法
3.修改richtextbox显示的TextFormatter

针对第1点,上面已经讲了,如果是纯文本或英文的话,宽度计算误差会比较大,导致界面比较丑,然后在网上找到了一个计算文本长度的方法,并加以整合:

 public static double CalcMessageWidth(Xceed.Wpf.Toolkit.RichTextBox t, double w)
        {
            TextRange range = new TextRange(t.Document.ContentStart, t.Document.ContentEnd);
            var text = range.Text;

            var formatText = GetFormattedText(t.Document);
            int count = SubstringCount(t.Text, "pict") / 2;
            return Math.Min(formatText.WidthIncludingTrailingWhitespace + 18 + count * 32, w);
        }

  public static FormattedText GetFormattedText(FlowDocument doc)
        {
            var output = new FormattedText(
                GetText(doc),
                System.Globalization.CultureInfo.CurrentCulture,
                doc.FlowDirection,
                new Typeface(doc.FontFamily, doc.FontStyle, doc.FontWeight, doc.FontStretch),
                doc.FontSize,
                doc.Foreground);

            int offset = 0;

            foreach (TextElement textElement in GetRunsAndParagraphs(doc))
            {
                var run = textElement as Run;

                if (run != null)
                {
                    int count = run.Text.Length;

                    output.SetFontFamily(run.FontFamily, offset, count);
                    output.SetFontSize(run.FontSize, offset, count);
                    output.SetFontStretch(run.FontStretch, offset, count);
                    output.SetFontStyle(run.FontStyle, offset, count);
                    output.SetFontWeight(run.FontWeight, offset, count);
                    output.SetForegroundBrush(run.Foreground, offset, count);
                    output.SetTextDecorations(run.TextDecorations, offset, count);

                    offset += count;
                }
                else
                {
                    offset += Environment.NewLine.Length;
                }
            }
            return output;
        }

  private static string GetText(FlowDocument doc)
        {
            var sb = new StringBuilder();
            foreach (TextElement text in GetRunsAndParagraphs(doc))
            {
                var run = text as Run;
                sb.Append(run == null ? Environment.NewLine : run.Text);
            }
            return sb.ToString();
        }

 private static IEnumerable<TextElement> GetRunsAndParagraphs(FlowDocument doc)
        {
            for (TextPointer position = doc.ContentStart;
                position != null && position.CompareTo(doc.ContentEnd) <= 0;
                position = position.GetNextContextPosition(LogicalDirection.Forward))
            {
                if (position.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.ElementEnd)
                {
                    var run = position.Parent as Run;

                    if (run != null)
                    {
                        yield return run;
                    }
                    else
                    {
                        var para = position.Parent as Paragraph;

                        if (para != null)
                        {
                            yield return para;
                        }
                        else
                        {
                            var lineBreak = position.Parent as LineBreak;

                            if (lineBreak != null)
                            {
                                yield return lineBreak;
                            }
                        }
                    }
                }
            }
        }

 public static int SubstringCount(string str, string substring)
        {
            if (str.Contains(substring))
            {
                string strReplaced = str.Replace(substring, "");
                return (str.Length - strReplaced.Length) / substring.Length;
            }
            return 0;
        }

在richtextbox的TextChanged事件里调用以上CalcMessageWidth(第二个参数是你设定的消息最大宽度)方法就可以算出消息宽度了,再也不怕纯英文或数字了,但是这个方法也有些弊端:
a.只能计算text长度,不包含图片,于是我加了个SubstringCount(t.Text, “pict”) / 2方法来计算消息中表情的个数,并且加上了每个表情32的宽度。t.Textd得到的是richtextbox内容的rtf格式,里面pict代表图片。
b.由于是在TextChanged事件里调用的,所以每发或收一条消息,之前所有的消息都会触发,这样势必会多消耗一些资源。

针对第2点,既然已经用了特定的方法来计算宽度,那么GenerateTextMessage方法里的计算就可以去掉了:

 private VText GenerateTextMessage(NIMTextMessage m)
        {
            VText text = new VText();
            var txt = m.TextContent;
            if (txt.Contains("[") && txt.Contains("]"))
            {
                List<EmoticonText> emoticonText = new List<EmoticonText>();
                char[] chars = txt.ToCharArray();
                for (int i = 0; i < chars.Length; i++)
                {
                    char c = chars[i];
                    if (chars[i] == '[')
                    {
                        int f = txt.IndexOf(']', i);
                        if (f < 0)
                        {
                            emoticonText.Add(new EmoticonText { Key = "text", Value = c.ToString() });
                        }
                        else
                        {
                            string es = txt.Substring(i, f - i + 1);
                            XElement node = elementCollection.Where(a => a.Attribute("Tag").Value == es).FirstOrDefault();
                            if (node == null)
                            {
                                emoticonText.Add(new EmoticonText { Key = "text", Value = c.ToString() });
                            }
                            else
                            {
                                emoticonText.Add(new EmoticonText { Key = "emoticon", Value = "../Resources/Emoticon/" + node.Attribute("File").Value });
                                i = f;
                            }
                        }
                    }
                    else
                    {
                        emoticonText.Add(new EmoticonText { Key = "text", Value = c.ToString() });
                        var emoticonWord = emoticonText.Where(p => p.Value == "\r").FirstOrDefault();
                        emoticonText.Remove(emoticonWord);
                    }

                }
                text.TextContent = JsonConvert.SerializeObject(emoticonText);
            }
            else
            {
                List<EmoticonText> textStr = new List<EmoticonText>();
                textStr.Add(new EmoticonText() { Key = "text", Value = txt });
                text.TextContent = JsonConvert.SerializeObject(textStr);
            }
            return text;
        }

有小伙伴问EmoticonText实体,它其实就是个key-value,跟converter里配套使用的:

  public class EmoticonText
    {
        public string Key { get; set; }

        public string Value { get; set; }
    }

针对第3点,之前是用xctk:XamlFormatter,发现还是有不少问题,于是就采用了xctk:RtfFormatter

    <Border CornerRadius="8" Background="#F0F0F0" Padding="6"  HorizontalAlignment="Left" Margin="0 4 0 0">
                        <xctk:RichTextBox VerticalScrollBarVisibility="Auto"
                                          HorizontalScrollBarVisibility="Disabled"
                                          Text="{Binding TextContent,Converter={StaticResource ShowImageOrTextConverter}}"
                                          VerticalAlignment="Center"
                                          BorderThickness="0"
                                          IsReadOnly="True"
                                          Background="Transparent"
                                          TextChanged="RichTextBox_TextChanged_1">
                            <xctk:RichTextBox.TextFormatter>
                                <xctk:RtfFormatter />
                            </xctk:RichTextBox.TextFormatter>
                        </xctk:RichTextBox>
                    </Border>

既然换了xctk:RtfFormatter,那么绑定给Text的数据也要变了,修改converter:

  class ShowImageOrText : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            var v = JsonConvert.DeserializeObject<List<EmoticonText>>((string)value);
            StringBuilder sb = new StringBuilder();
            foreach (var item in v)
            {
                if (item.Key == "text")
                {
                    sb.Append("<Run>");
                    sb.Append(item.Value.Replace("<", "LessSymbol").Replace("\r\n", "</Run><LineBreak/><Run>").Replace("\n", "</Run><LineBreak/><Run>"));
                    sb.Append("</Run>");
                }
                else
                {
                    sb.Append("<Image Width=\"32\" Source=\"");
                    sb.Append(item.Value);
                    sb.Append("\"/>");
                }
            }
            var str = @"<Section xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation"" FontFamily=""Microsoft YaHei"" FontSize=""14"" xml:space=""preserve"" TextAlignment=""Left"" LineHeight=""Auto""><Paragraph>" + sb.ToString() + "</Paragraph></Section>";
            return ConvertXamlToRtf(str);
        }

        /// <summary>
        /// https://code.msdn.microsoft.com/windowsdesktop/Converting-between-RTF-and-aaa02a6e
        /// </summary>
        /// <param name="xamlText"></param>
        /// <returns></returns>
        private static string ConvertXamlToRtf(string xamlText)
        {

            var richTextBox = new RichTextBox();
            if (string.IsNullOrEmpty(xamlText)) return "";
            var textRange = new TextRange(richTextBox.Document.ContentStart, richTextBox.Document.ContentEnd);

            try
            {
                using (var xamlMemoryStream = new MemoryStream())
                {
                    using (var xamlStreamWriter = new StreamWriter(xamlMemoryStream))
                    {
                        xamlStreamWriter.Write(xamlText.Replace("&", "AndSymbol"));
                        xamlStreamWriter.Flush();
                        xamlMemoryStream.Seek(0, SeekOrigin.Begin);
                        textRange.Load(xamlMemoryStream, DataFormats.Xaml);
                    }
                }
            }
            catch (Exception)
            {
                var str = @"<Section xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation"" FontFamily=""Microsoft YaHei"" FontSize=""14"" xml:space=""preserve"" TextAlignment=""Left"" LineHeight=""Auto""><Paragraph><Run>该信息包含特殊字符,无法显示</Run></Paragraph></Section>";
                using (var xamlMemoryStream = new MemoryStream())
                {
                    using (var xamlStreamWriter = new StreamWriter(xamlMemoryStream))
                    {
                        xamlStreamWriter.Write(str);
                        xamlStreamWriter.Flush();
                        xamlMemoryStream.Seek(0, SeekOrigin.Begin);
                        textRange.Load(xamlMemoryStream, DataFormats.Xaml);
                    }
                }
            }


            using (var rtfMemoryStream = new MemoryStream())
            {
                textRange = new TextRange(richTextBox.Document.ContentStart, richTextBox.Document.ContentEnd);
                textRange.Save(rtfMemoryStream, DataFormats.Rtf);
                rtfMemoryStream.Seek(0, SeekOrigin.Begin);
                using (var rtfStreamReader = new StreamReader(rtfMemoryStream))
                {
                    return rtfStreamReader.ReadToEnd().Replace("AndSymbol", "&").Replace("LessSymbol", "<");
                }
            }
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

这个converter将xaml转成rtf再绑定给richtextbox的Text,并且针对一些特殊字符做了特殊处理以及异常处理,小伙伴们使用时看情况修改~

好了,以上基本就是用到的所有方法了,也算是给源码了。

再上个图吧-:)
这里写图片描述

猜你喜欢

转载自blog.csdn.net/leebin_20/article/details/72884892