原文: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,并且针对一些特殊字符做了特殊处理以及异常处理,小伙伴们使用时看情况修改~

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

再上个图吧-:)

WPF仿QQ聊天框表情文字混排实现的更多相关文章

  1. js五道经典练习题--第二道仿qq聊天框

    <!DOCTYPE html><html> <head> <meta charset="UTF-8"> <title>& ...

  2. Android特效专辑(六)——仿QQ聊天撒花特效,无形装逼,最为致命

    Android特效专辑(六)--仿QQ聊天撒花特效,无形装逼,最为致命 我的关于特效的专辑已经在CSDN上申请了一个专栏--http://blog.csdn.net/column/details/li ...

  3. Socket实现仿QQ聊天(可部署于广域网)附源码(1)-简介

    1.前言 本次实现的这个聊天工具是我去年c#程序设计课程所写的Socket仿QQ聊天,由于当时候没有自己的服务器,只能在机房局域网内进行测试,最近在腾讯云上买了一台云主机(本人学生党,腾讯云有个学生专 ...

  4. JS简单仿QQ聊天工具的制作

    刚接触JS,对其充满了好奇,利用刚学到的一点知识,写了一个简单的仿QQ聊天的东西,其中还有很多的不足之处,有待慢慢提高. 功能:1.在输入框中输入内容,点击发送,即可在上方显示所输入内容. 2.点击‘ ...

  5. 高仿qq聊天界面

    高仿qq聊天界面,给有需要的人,界面效果如下: 真心觉得做界面非常痛苦,给有需要的朋友. chat.xml <?xml version="1.0" encoding=&quo ...

  6. QQ聊天框变成方框口口口口的解决办法

    QQ聊天框变成方框口口口口的解决办法 安装了QQ拼音输入法6.0之后,发现 QQ聊天对话框好友名称变成框口口口口口,网上没有找到办法,卸载轻聊版,安装完整版9.03之后,再次启动就好了.

  7. 仿QQ聊天程序(java)

    仿QQ聊天程序 转载:牟尼的专栏 http://blog.csdn.net/u012027907 一.设计内容及要求 1.1综述 A.系统概述 我们要做的就是类似QQ这样的面向企业内部的聊天软件,基本 ...

  8. AS3聊天单行输入框图文混排完美实现

    几年前刚毕业.第一个游戏模块做的就是聊天.到如今.几个游戏写过几次聊天模块. 之前在4399做的<幻龙骑士>(又名<神骑士>),还有上周六刚上线的<疯狂的子弹>, ...

  9. 图片文字混排的垂直居中、inline-block块元素和行内元素混排的垂直居中问题

    图片.文字混排: 不管图片和文字的前后位置,都要给 图片 设置 vertical-algin,而不是谁在前面给谁设置. 此方法兼容IE7+ 和其它主流浏览器.IE7-没有测. inline-block ...

随机推荐

  1. Gremlin--一种支持对图表操作的语言

    Gremlin 是操作图表的一个非常有用的图灵完备的编程语言.它是一种Java DSL语言,对图表进行查询.分析和操作时使用了大量的XPath. Gremlin可用于创建多关系图表.因为图表.顶点和边 ...

  2. Javascript基础--函数(Function对象)

    1.函数是一段可执行的代码,函数可多次调用,模块化管理. 2.使用function语句,function funName([arg1][,arg2]....[,argn]){代码块}.所有版本可用,一 ...

  3. HTML:::before和::after伪元素的用法

    随笔 - 366  文章 - 0  评论 - 392 ::before和::after伪元素的用法   一.介绍 css3为了区分伪类和伪元素,伪元素采用双冒号写法. 常见伪类——:hover,:li ...

  4. TeamViewer 软件完全卸载

    TeamViewer 软件似乎用于商业环境中 - 彻底卸载 Windows 1. 检测为商业用途该软件似乎用于商业环境中.请注意:免费版仅供个人使用.您的会话将在 5 分钟后终止. 2.1 Close ...

  5. 类型信息(RTTI和反射)——反射

    运行时类型信息可以让你在程序运行时发现和使用类型信息. 在Java中运行时识别对象和类的信息有两种方式:传统的RTTI,以及反射.下面就来说说反射. 重点说说通过反射获取方法以及调用方法,即类方法提取 ...

  6. RC4 in TLS is Broken: Now What?

    https://community.qualys.com/blogs/securitylabs/2013/03/19/rc4-in-tls-is-broken-now-what RC4 has lon ...

  7. JavaScript 闭包的详细分享(三种创建方式)(附小实例)

    JavaScript闭包的详细理解 一.原理:闭包函数--指有权访问私有函数里面的变量和对象还有方法等:通俗的讲就是突破私有函数的作用域,让函数外面能够使用函数里面的变量及方法. 1.第一种创建方式 ...

  8. Android(java)学习笔记59:类继承的 注意事项

    1. 类继承的注意事项: /* 继承的注意事项: A:子类只能继承父类所有非私有的成员(成员方法和成员变量) B:子类不能继承父类的构造方法,但是可以通过super(马上讲)关键字去访问父类构造方法. ...

  9. 【[TJOI2018]异或】

    写板子了,可持久化\(Trie\)的板子了 其实和主席树写法类似,还是存好左右儿子之后存好权值 之后差分去查询就好了 这道题第一问我们直接\(dfs\)序转化成区间 第二问搞成\(x,y,lca(x, ...

  10. 【转】Xcode真机调试初体验

    1. 开发者证书(Certificates) 分为开发(iOS Development)和发布(iOS Distribution)两种,无论是真机调试,还是上传到App Store都需要该证书,是一个 ...