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

如下图


<https://img2018.cnblogs.com/blog/484187/201902/484187-20190219211827072-969082703.png>

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

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

于是动手就干。

 

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

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

C#
[TemplatePart(Name = TextBlockTemplateName, Type = typeof(TextBlock))] public
class ChatMessageControl : 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<string, string> Emotions = new Dictionary<string, string> {
["doge"] = "pack://application:,,,/WpfQQChat;component/Images/doge.png", ["喵喵"]
="pack://application:,,,/WpfQQChat;component/Images/喵喵.png" }; private
TextBlock _textBlock;static ChatMessageControl() {
DefaultStyleKeyProperty.OverrideMetadata(typeof(ChatMessageControl), new
FrameworkPropertyMetadata(typeof(ChatMessageControl))); } public string Text {
get => (string)GetValue(TextProperty); set => SetValue(TextProperty, value); }
public override void OnApplyTemplate() { _textBlock =
(TextBlock)GetTemplateChild(TextBlockTemplateName); UpdateVisual(); }private
static void OnTextChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e) {var obj = (ChatMessageControl)d;
obj.UpdateVisual(); }private void UpdateVisual() { if (_textBlock == null) {
return; } _textBlock.Inlines.Clear(); var buffer = new StringBuilder(); foreach
(var c in Text) { 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 = new Image { Width = 16, Height = 16, Source = new BitmapImage(new
Uri(Emotions[emotionName])) }; _textBlock.Inlines.Add(new
InlineUIContainer(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
<Style TargetType="local:ChatMessageControl"> <Setter Property="Template"> <
Setter.Value> <ControlTemplate TargetType="local:ChatMessageControl"> <TextBlock
x:Name="PART_TextBlock" TextWrapping="Wrap" /> </ControlTemplate> </Setter.Value
> </Setter> </Style>
没啥好说的,就是包了一层而已。

效果:


<https://img2018.cnblogs.com/blog/484187/201902/484187-20190219211829681-1366285343.png>

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

 

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


<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 class ChatMessageControlV2 : 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<string, string> Emotions =
new Dictionary<string, string> { ["doge"] = "
pack://application:,,,/WpfQQChat;component/Images/doge.png", ["喵喵"] = "
pack://application:,,,/WpfQQChat;component/Images/喵喵.png" }; private
RichTextBox _richTextBox;static ChatMessageControlV2() {
DefaultStyleKeyProperty.OverrideMetadata(typeof(ChatMessageControlV2), new
FrameworkPropertyMetadata(typeof(ChatMessageControlV2))); } public string Text {
get => (string)GetValue(TextProperty); set => SetValue(TextProperty, value); }
public override void OnApplyTemplate() { _richTextBox =
(RichTextBox)GetTemplateChild(RichTextBoxTemplateName); UpdateVisual(); }private
static void OnTextChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e) {var obj = (ChatMessageControlV2)d;
obj.UpdateVisual(); }private void UpdateVisual() { if (_richTextBox == null) {
return; } _richTextBox.Document.Blocks.Clear(); var paragraph = new Paragraph();
var buffer = new StringBuilder(); foreach (var c in Text) { 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 = new Image { Width = 16, Height = 16, Source = new BitmapImage(new
Uri(Emotions[emotionName])) }; paragraph.Inlines.Add(new
InlineUIContainer(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
<Style TargetType="local:ChatMessageControlV2"> <Setter Property="Foreground"
Value="Black" /> <Setter Property="Template"> <Setter.Value> <ControlTemplate
TargetType="local:ChatMessageControlV2"> <RichTextBox x:Name="PART_RichTextBox"
MinHeight="0" Background="Transparent" BorderBrush="Transparent" BorderThickness
="0" Foreground="{TemplateBinding Foreground}" IsReadOnly="True"> <
RichTextBox.Resources> <ResourceDictionary> <Style TargetType="Paragraph"> <
SetterProperty="Margin" Value="0" /> <Setter Property="Padding" Value="0" /> <
SetterProperty="TextIndent" Value="0" /> </Style> </ResourceDictionary> </
RichTextBox.Resources> <RichTextBox.ContextMenu> <ContextMenu> <MenuItem Command
="ApplicationCommands.Copy" Header="复制" /> </ContextMenu> </
RichTextBox.ContextMenu> </RichTextBox> </ControlTemplate> </Setter.Value> </
Setter> </Style>
XAML 稍微复杂一点,因为我们需要让一个文本框高仿成一个文字显示控件。

 

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


<https://img2018.cnblogs.com/blog/484187/201902/484187-20190219211831234-1446848419.png>

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


<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
class ChatMessageControlV3 : 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<string, string> Emotions = new Dictionary<
string, string> { ["doge"] = "
pack://application:,,,/WpfQQChat;component/Images/doge.png", ["喵喵"] = "
pack://application:,,,/WpfQQChat;component/Images/喵喵.png" }; private
RichTextBox _richTextBox;private TextBlock _textBlock; static
ChatMessageControlV3() { DefaultStyleKeyProperty.OverrideMetadata(typeof
(ChatMessageControlV3),new FrameworkPropertyMetadata(typeof
(ChatMessageControlV3))); }public string Text { get => (string
)GetValue(TextProperty);set => SetValue(TextProperty, value); } public override
void OnApplyTemplate() { _textBlock =
(TextBlock)GetTemplateChild(TextBlockTemplateName); _richTextBox=
(RichTextBox)GetTemplateChild(RichTextBoxTemplateName); UpdateVisual(); }private
static void OnTextChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e) {var obj = (ChatMessageControlV3)d;
obj.UpdateVisual(); }private void UpdateVisual() { if (_textBlock == null ||
_richTextBox ==null) { return; } _textBlock.Inlines.Clear();
_richTextBox.Document.Blocks.Clear();var paragraph = new Paragraph(); var
buffer =new StringBuilder(); foreach (var c in Text) { 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 = new Image { Width = 16, Height = 16 };// 占位图像不需要加载 Source 了
_textBlock.Inlines.Add(new InlineUIContainer(image)); } { var image = new Image
{ Width= 16, Height = 16, Source = new BitmapImage(new
Uri(Emotions[emotionName])) }; paragraph.Inlines.Add(new
InlineUIContainer(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
<Style TargetType="local:ChatMessageControlV3"> <Setter Property="Foreground"
Value="Black" /> <Setter Property="Template"> <Setter.Value> <ControlTemplate
TargetType="local:ChatMessageControlV3"> <Grid> <TextBlock x:Name
="PART_TextBlock" Padding="6,0,6,0" IsHitTestVisible="False" Opacity="0"
TextWrapping="Wrap" /> <RichTextBox x:Name="PART_RichTextBox" Width="{Binding
ElementName=PART_TextBlock, Path=ActualWidth}" MinHeight="0" Background
="Transparent" BorderBrush="Transparent" BorderThickness="0" Foreground="
{TemplateBinding Foreground}" IsReadOnly="True"> <RichTextBox.Resources> <
ResourceDictionary> <Style TargetType="Paragraph"> <Setter Property="Margin"
Value="0" /> <Setter Property="Padding" Value="0" /> <Setter Property
="TextIndent" Value="0" /> </Style> </ResourceDictionary> </
RichTextBox.Resources> <RichTextBox.ContextMenu> <ContextMenu> <MenuItem Command
="ApplicationCommands.Copy" Header="复制" /> </ContextMenu> </
RichTextBox.ContextMenu> </RichTextBox> </Grid> </ControlTemplate> </
Setter.Value> </Setter> </Style>
XAML 大体也是将两者结合起来,但是把 TextBlock 设置为隐藏(但占用布局),而 RichTextBox 则绑定 TextBlock 的宽度。

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

最后是我们的效果啦。


<https://img2018.cnblogs.com/blog/484187/201902/484187-20190219211833977-268364010.png>

 

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

友情链接
ioDraw流程图
API参考文档
OK工具箱
云服务器优惠
阿里云优惠券
腾讯云优惠券
华为云优惠券
站点信息
问题反馈
邮箱:ixiaoyang8@qq.com
QQ群:637538335
关注微信