Error message here!

Hide Error message here!

忘记密码?

Error message here!

请输入正确邮箱

Hide Error message here!

密码丢失?请输入您的电子邮件地址。您将收到一个重设密码链接。

Error message here!

返回登录

Close

【WPF】实现类似QQ聊天消息的界面

h82258652 2019-02-19 21:19:00 阅读数:228 评论数:0 点赞数:0 收藏数:0

最近公司有个项目,是要求实现类似 QQ 聊天这种功能的。

如下图

![Snipaste_2019-02-19_19-33-22](https://img2018.cnblogs.com/blog/484187/201902/484187-20190219211827072-969082703.png)

这没啥难的,稍微复杂的也就表情的解析而已。

表情在传输过程中的实现参考了新浪微博,采用半角中括号代表表情的方式。例如:“abc[doge]def”就会显示 abc,然后一个[![2018new_doge02_org](https://img2018.cnblogs.com/blog/484187/201902/484187-20190219211829267-2005729378.png "2018new_doge02_org")](https://img2018.cnblogs.com/blog/484187/201902/484187-20190219211828953-1855682369.png),再 def。

于是动手就干。

 

创建一个模板控件来进行封装,我就叫它 ChatMessageControl,有一个属性 Text,表示消息内容。内部使用一个 TextBlock 来实现。

于是博主三下五除二就写出了以下代码:

C/#[TemplatePart(Name = TextBlockTemplateName, Type = typeof(TextBlock))]public classChatMessageControl : Control {public static readonly DependencyProperty TextProperty =DependencyProperty.Register(nameof(Text),typeof(string), typeof(ChatMessageControl), new PropertyMetadata(default(string), OnTextChanged));private const string TextBlockTemplateName = "PART_TextBlock";private static readonly Dictionary Emotions = new Dictionary{ ["doge"] = "pack://application:,,,/WpfQQChat;component/Images/doge.png", ["喵喵"] = "pack://application:,,,/WpfQQChat;component/Images/喵喵.png"};privateTextBlock _textBlock;staticChatMessageControl() { DefaultStyleKeyProperty.OverrideMetadata(typeof(ChatMessageControl), new FrameworkPropertyMetadata(typeof(ChatMessageControl))); }public stringText {get => (string)GetValue(TextProperty);set =>SetValue(TextProperty, value); }public override voidOnApplyTemplate() { _textBlock=(TextBlock)GetTemplateChild(TextBlockTemplateName); UpdateVisual(); }private static voidOnTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {var obj =(ChatMessageControl)d; obj.UpdateVisual(); }private voidUpdateVisual() {if (_textBlock == null) {return; } _textBlock.Inlines.Clear();var buffer = newStringBuilder();foreach (var c inText) {switch(c) {case '[': _textBlock.Inlines.Add(buffer.ToString()); buffer.Clear(); buffer.Append(c);break;case ']':var current =buffer.ToString();if (current.StartsWith("[")) {var emotionName = current.Substring(1);if(Emotions.ContainsKey(emotionName)) {var image = newImage { Width= 16, Height= 16, Source= new BitmapImage(newUri(Emotions[emotionName])) }; _textBlock.Inlines.Add(newInlineUIContainer(image)); buffer.Clear();continue; } } buffer.Append(c); _textBlock.Inlines.Add(buffer.ToString()); buffer.Clear();break;default: buffer.Append(c);break; } } _textBlock.Inlines.Add(buffer.ToString()); } }

因为这篇博文只是个演示,这里博主就只放两个表情好了,并且耦合在这个控件里。

XAML

没啥好说的,就是包了一层而已。

效果:

![Snipaste_2019-02-19_20-11-40](https://img2018.cnblogs.com/blog/484187/201902/484187-20190219211829681-1366285343.png)

自我感觉良好,于是乎博主就提交代码,发了个版本到测试环境了。

 

但是,第二天,测试却给博主提了个 bug。消息无法选择、复制。

![17686](https://img2018.cnblogs.com/blog/484187/201902/484187-20190219211830586-1727741516.jpg)

在 UWP 里,TextBlock 控件是有 IsTextSelectionEnabled 属性的,然而 WPF 并没有。这下头大了,于是博主去查了一下 StackOverflow,大佬们回答都是说用一个 IsReadOnly 为 True 的 TextBox 来实现。因为我这里包含了表情,所以用 RichTextBox 来实现吧。不管行不行,先试试再说。

在原来的代码上修改一下,反正表情解析一样的,但这里博主为了方便写 blog,就新开一个控件好了。

C/#[TemplatePart(Name = RichTextBoxTemplateName, Type = typeof(RichTextBox))]public classChatMessageControlV2 : Control {public static readonly DependencyProperty TextProperty =DependencyProperty.Register(nameof(Text),typeof(string), typeof(ChatMessageControlV2), new PropertyMetadata(default(string), OnTextChanged));private const string RichTextBoxTemplateName = "PART_RichTextBox";private static readonly Dictionary Emotions = new Dictionary{ ["doge"] = "pack://application:,,,/WpfQQChat;component/Images/doge.png", ["喵喵"] = "pack://application:,,,/WpfQQChat;component/Images/喵喵.png"};privateRichTextBox _richTextBox;staticChatMessageControlV2() { DefaultStyleKeyProperty.OverrideMetadata(typeof(ChatMessageControlV2), new FrameworkPropertyMetadata(typeof(ChatMessageControlV2))); }public stringText {get => (string)GetValue(TextProperty);set =>SetValue(TextProperty, value); }public override voidOnApplyTemplate() { _richTextBox=(RichTextBox)GetTemplateChild(RichTextBoxTemplateName); UpdateVisual(); }private static voidOnTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {var obj =(ChatMessageControlV2)d; obj.UpdateVisual(); }private voidUpdateVisual() {if (_richTextBox == null) {return; } _richTextBox.Document.Blocks.Clear();var paragraph = newParagraph();var buffer = newStringBuilder();foreach (var c inText) {switch(c) {case '[': paragraph.Inlines.Add(buffer.ToString()); buffer.Clear(); buffer.Append(c);break;case ']':var current =buffer.ToString();if (current.StartsWith("[")) {var emotionName = current.Substring(1);if(Emotions.ContainsKey(emotionName)) {var image = newImage { Width= 16, Height= 16, Source= new BitmapImage(newUri(Emotions[emotionName])) }; paragraph.Inlines.Add(newInlineUIContainer(image)); buffer.Clear();continue; } } buffer.Append(c); paragraph.Inlines.Add(buffer.ToString()); buffer.Clear();break;default: buffer.Append(c);break; } } paragraph.Inlines.Add(buffer.ToString()); _richTextBox.Document.Blocks.Add(paragraph); } }

XAML

XAML 稍微复杂一点,因为我们需要让一个文本框高仿成一个文字显示控件。

 

感觉应该还行,然后跑起来之后

![Snipaste_2019-02-19_20-42-13](https://img2018.cnblogs.com/blog/484187/201902/484187-20190219211831234-1446848419.png)

复制是能复制了,然而我的布局呢?

![79521](https://img2018.cnblogs.com/blog/484187/201902/484187-20190219211832204-1108671003.gif)

 

因为一时间也没想到解决办法,于是博主只能回滚代码,把 bug 先晾在那里了。

经过了几天上班带薪拉屎之后,有一天博主在厕所间玩着宝石连连消的时候突然灵光一闪。对于 TextBlock 来说,只是不能选择而已,布局是没问题的。对于 RichTextBox 来说,布局不正确是由于 WPF 在测量与布局的过程中给它分配了无限大的宽度。那么,能不能将两者结合起来,TextBlock 做布局,RichTextBox 做功能呢?想到这里,博主关掉了宝石连连消,擦上屁股,开始干活。

C/#[TemplatePart(Name = TextBlockTemplateName, Type = typeof(TextBlock))] [TemplatePart(Name= RichTextBoxTemplateName, Type = typeof(RichTextBox))]public classChatMessageControlV3 : Control {public static readonly DependencyProperty TextProperty =DependencyProperty.Register(nameof(Text),typeof(string), typeof(ChatMessageControlV3), new PropertyMetadata(default(string), OnTextChanged));private const string RichTextBoxTemplateName = "PART_RichTextBox";private const string TextBlockTemplateName = "PART_TextBlock";private static readonly Dictionary Emotions = new Dictionary{ ["doge"] = "pack://application:,,,/WpfQQChat;component/Images/doge.png", ["喵喵"] = "pack://application:,,,/WpfQQChat;component/Images/喵喵.png"};privateRichTextBox _richTextBox;privateTextBlock _textBlock;staticChatMessageControlV3() { DefaultStyleKeyProperty.OverrideMetadata(typeof(ChatMessageControlV3), new FrameworkPropertyMetadata(typeof(ChatMessageControlV3))); }public stringText {get => (string)GetValue(TextProperty);set =>SetValue(TextProperty, value); }public override voidOnApplyTemplate() { _textBlock=(TextBlock)GetTemplateChild(TextBlockTemplateName); _richTextBox=(RichTextBox)GetTemplateChild(RichTextBoxTemplateName); UpdateVisual(); }private static voidOnTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {var obj =(ChatMessageControlV3)d; obj.UpdateVisual(); }private voidUpdateVisual() {if (_textBlock == null || _richTextBox == null) {return; } _textBlock.Inlines.Clear(); _richTextBox.Document.Blocks.Clear();var paragraph = newParagraph();var buffer = newStringBuilder();foreach (var c inText) {switch(c) {case '[': _textBlock.Inlines.Add(buffer.ToString()); paragraph.Inlines.Add(buffer.ToString()); buffer.Clear(); buffer.Append(c);break;case ']':var current =buffer.ToString();if (current.StartsWith("[")) {var emotionName = current.Substring(1);if(Emotions.ContainsKey(emotionName)) { {var image = newImage { Width= 16, Height= 16};//占位图像不需要加载 Source 了 _textBlock.Inlines.Add(newInlineUIContainer(image)); } {var image = newImage { Width= 16, Height= 16, Source= new BitmapImage(newUri(Emotions[emotionName])) }; paragraph.Inlines.Add(newInlineUIContainer(image)); } buffer.Clear();continue; } } buffer.Append(c); _textBlock.Inlines.Add(buffer.ToString()); paragraph.Inlines.Add(buffer.ToString()); buffer.Clear();break;default: buffer.Append(c);break; } } _textBlock.Inlines.Add(buffer.ToString()); paragraph.Inlines.Add(buffer.ToString()); _richTextBox.Document.Blocks.Add(paragraph); } }

C/# 代码相当于把两者结合起来而已。

XAML TextBlock"Padding="6,0,6,0"IsHitTestVisible="False"Opacity="0"TextWrapping="Wrap" /> RichTextBox"Width="{Binding ElementName=PART_TextBlock, Path=ActualWidth}"MinHeight="0"Background="Transparent"BorderBrush="Transparent"BorderThickness="0"Foreground="{TemplateBinding Foreground}"IsReadOnly="True">

XAML 大体也是将两者结合起来,但是把 TextBlock 设置为隐藏(但占用布局),而 RichTextBox 则绑定 TextBlock 的宽度。

至于为啥 TextBlock 有一个左右边距为 6 的 Padding 嘛。在运行之后,博主发现,RichTextBox 的内容会离左右有一定的距离,但是没找到相关的属性能够设置,如果正在看这篇博文的你,知道相关的属性的话,可以在评论区回复一下,博主我将会万分感激。

最后是我们的效果啦。

![Snipaste_2019-02-19_21-13-42](https://img2018.cnblogs.com/blog/484187/201902/484187-20190219211833977-268364010.png)

 

最后,因为现在 WPF 是开源(https://github.com/dotnet/wpf)的了,因此已经蛋疼不已的博主果断提了一个 issue(https://github.com/dotnet/wpf/issues/307),希望有遇到同样困难的小伙伴能在上面支持一下,让巨硬早日把 TextBlock 选择这功能加上。

版权声明
本文为[h82258652]所创,转载请带上原文链接,感谢
https://www.cnblogs.com/h82258652/p/10403426.html

编程之旅,人生之路,不止于编程,还有诗和远方。
阅代码原理,看框架知识,学企业实践;
赏诗词,读日记,踏人生之路,观世界之行;

支付宝红包,每日可领