最近公司有個項目,是要求實現相似 QQ 聊天這種功能的。git
以下圖github
這沒啥難的,稍微複雜的也就表情的解析而已。app
表情在傳輸過程當中的實現參考了新浪微博,採用半角中括號表明表情的方式。例如:「abc[doge]def」就會顯示 abc,而後一個,再 def。ide
因而動手就幹。佈局
建立一個模板控件來進行封裝,我就叫它 ChatMessageControl,有一個屬性 Text,表示消息內容。內部使用一個 TextBlock 來實現。性能
因而博主三下五除二就寫出瞭如下代碼:測試
C#ui
[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()); } }
由於這篇博文只是個演示,這裏博主就只放兩個表情好了,而且耦合在這個控件裏。spa
XAMLcode
<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>
沒啥好說的,就是包了一層而已。
效果:
自我感受良好,因而乎博主就提交代碼,發了個版本到測試環境了。
可是,次日,測試卻給博主提了個 bug。消息沒法選擇、複製。
在 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"> <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> </ControlTemplate> </Setter.Value> </Setter> </Style>
XAML 稍微複雜一點,由於咱們須要讓一個文本框高仿成一個文字顯示控件。
感受應該還行,而後跑起來以後
複製是能複製了,然而個人佈局呢?
由於一時間也沒想到解決辦法,因而博主只能回滾代碼,把 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 的內容會離左右有必定的距離,可是沒找到相關的屬性可以設置,若是正在看這篇博文的你,知道相關的屬性的話,能夠在評論區回覆一下,博主我將會萬分感激。
最後是咱們的效果啦。
最後,由於如今 WPF 是開源(https://github.com/dotnet/wpf)的了,所以已經蛋疼不已的博主果斷提了一個 issue(https://github.com/dotnet/wpf/issues/307),但願有遇到一樣困難的小夥伴能在上面支持一下,讓巨硬早日把 TextBlock 選擇這功能加上。