WPF仿QQ聊天框表情文字混排实现
2017-06-06 16:42
489 查看
二话不说。先上图
图中分别有文件、文本+表情、纯文本的展示,对于同一个list不同的展示形式,很明显,应该用多个DataTemplate,那么也就需要DataTemplateSelector了:
以上一共有8个DateTemplate,friend和self的区别就在于一个在左一个在右,我这边就放friend_text的样式代码好了,因为本篇主要说的是表情和文字的混排:
以上可以看到,我们使用了RichTextBox这个控件,不过并不是原生的,而是xceed.wpf.toolkit下的,所以别忘了引入命名空间:
xmlns:xctk=”http://schemas.xceed.com/wpf/xaml/toolkit”
为什么要引用这个控件,因为它支持绑定Text -:)
上一篇已经提到过,我们的IM用的是网易云的SDK,在这个SDK里表情也是通过文本发送的,如[微笑]就代表微笑的表情。
那么问题就很明显了——怎么解析这段带有表情的文本,并且把表情显示出来。
Text是自定义的一个实体,它包含需要绑定到xaml的属性。这里我用EmoticonText这个实体来区分是表情图片还是纯文本。另外,有的同学可能有疑问,这里的length是干嘛用的,看前面那个DataTemplate,其中PageWidth=”{Binding MessageWidth}”,所以这个length是计算当前RichTextBox宽度的,为什么要手动计算宽度呢?因为RichTextBox貌似没提供根据内容自适应宽度,如果我是用TextBox的话,其宽度就会根据其中显示内容的长短进行自适应;那为什么要乘14加28什么的呢?因为我这个是按字符个数来算宽度的,当以14为系数因子的时候,中文显示勉强满意,但是如果是纯英文或数字就不行了,这也是为什么截图里RichTextBox右边还空那么一块;最后加24是因为边距,38是一行最多显示38个中文,如果超过了38个中文还对其计算宽度的话,就会导致其不换行了。
如果有同学有自适应宽度更好的方法,欢迎不吝赐教唷!
都绑定好后,这个时候显示肯定还是不正确的,因为现在TextContent是一个Json字符串,所以我们还差一个Converter:
这段代码一看就懂,如果是文本则用run,如果是表情图片,则用image,最后将拼装好的xaml绑定到前端。
下来问题就出现了,当run里面是中文时,前端会显示为???(几个中文就几个?),也就是XamlFormatter并不能正确解析中文,哦到开~尝试了修改各种Language属性以及xml:lang=”en-us”,都是徒劳- -!
根据官网(http://wpftoolkit.codeplex.com/wikipage?title=RichTextBox)介绍,我们是可以自定义formater的,那到底怎么自定义呢,在看了xctk:RichTextBox针对xaml的formatter这块的源码后才明白,
其编码格式用的ASCII,我们只要将其换成UTF8即可:
记得将DataTemplate中的
-:)
以上2017-06-06
————————————————————————————————————————————————————
以下编辑于2017-10-25
看到有小伙伴在评论区提问,我这边就再更新一下吧。
在之前的版本上我又做了以下更改:
1.修改richtextbox宽度计算方法
2.修改GenerateTextMessage方法
3.修改richtextbox显示的TextFormatter
针对第1点,上面已经讲了,如果是纯文本或英文的话,宽度计算误差会比较大,导致界面比较丑,然后在网上找到了一个计算文本长度的方法,并加以整合:
在richtextbox的TextChanged事件里调用以上CalcMessageWidth(第二个参数是你设定的消息最大宽度)方法就可以算出消息宽度了,再也不怕纯英文或数字了,但是这个方法也有些弊端:
a.只能计算text长度,不包含图片,于是我加了个SubstringCount(t.Text, “pict”) / 2方法来计算消息中表情的个数,并且加上了每个表情32的宽度。t.Textd得到的是richtextbox内容的rtf格式,里面pict代表图片。
b.由于是在TextChanged事件里调用的,所以每发或收一条消息,之前所有的消息都会触发,这样势必会多消耗一些资源。
针对第2点,既然已经用了特定的方法来计算宽度,那么GenerateTextMessage方法里的计算就可以去掉了:
有小伙伴问EmoticonText实体,它其实就是个key-value,跟converter里配套使用的:
针对第3点,之前是用xctk:XamlFormatter,发现还是有不少问题,于是就采用了xctk:RtfFormatter
既然换了xctk:RtfFormatter,那么绑定给Text的数据也要变了,修改converter:
这个converter将xaml转成rtf再绑定给richtextbox的Text,并且针对一些特殊字符做了特殊处理以及异常处理,小伙伴们使用时看情况修改~
好了,以上基本就是用到的所有方法了,也算是给源码了。
再上个图吧-:)
图中分别有文件、文本+表情、纯文本的展示,对于同一个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" Horizonta 4000 lScrollBarVisibility="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 = ne fd91 w 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,并且针对一些特殊字符做了特殊处理以及异常处理,小伙伴们使用时看情况修改~
好了,以上基本就是用到的所有方法了,也算是给源码了。
再上个图吧-:)
相关文章推荐
- 怎么用Java实现QQ消息输入框这种能表情图片和文字混排的效果
- 用WPF实现屏幕文字提示
- asp.net类似于QQ表情弹出框功能的实现方法
- QQ魔法表情实现原理
- iphone聊天用几种图形(表情)与文字混排的实现与比较 [复制链接]
- 用最简单的方法实现QQ魔法表情效果!
- 类似android手机QQ表情实现
- 类似qq聊天表情实现
- QQ魔法表情实现原理
- 向Richedit插入动态Gif的实现(关于QQ表情功能的制作)
- 【WPF】实现QQ中的分组面板
- Flex: flash聊天框 表情+文字 图文混排 (思路)
- WPF 实现屏幕文字提示
- QQ魔法表情实现原理(呵呵.很简单)
- Delphi RichEdit的实现MSN / QQ 中的动画表情
- richTextBox如何实现输入指定的字符显示一个表情图标?类似QQ那样?
- 【WPF】实现QQ中的分组面板(2)——添加动画
- 用WPF实现屏幕文字提示
- QQ魔法表情实现原理源代码下载
- iphone聊天用几种图形(表情)与文字混排的实现与比较