【WPF】實現相似QQ聊天消息的界面

最近公司有個項目,是要求實現相似 QQ 聊天這種功能的。git

以下圖github

Snipaste_2019-02-19_19-33-22

這沒啥難的,稍微複雜的也就表情的解析而已。app

表情在傳輸過程當中的實現參考了新浪微博,採用半角中括號表明表情的方式。例如:「abc[doge]def」就會顯示 abc,而後一個2018new_doge02_org,再 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>

沒啥好說的,就是包了一層而已。

效果:

Snipaste_2019-02-19_20-11-40

自我感受良好,因而乎博主就提交代碼,發了個版本到測試環境了。

 

可是,次日,測試卻給博主提了個 bug。消息沒法選擇、複製。

17686

在 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 稍微複雜一點,由於咱們須要讓一個文本框高仿成一個文字顯示控件。

 

感受應該還行,而後跑起來以後

Snipaste_2019-02-19_20-42-13

複製是能複製了,然而個人佈局呢?

79521

 

由於一時間也沒想到解決辦法,因而博主只能回滾代碼,把 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 的內容會離左右有必定的距離,可是沒找到相關的屬性可以設置,若是正在看這篇博文的你,知道相關的屬性的話,能夠在評論區回覆一下,博主我將會萬分感激。

最後是咱們的效果啦。

Snipaste_2019-02-19_21-13-42

 

最後,由於如今 WPF 是開源(https://github.com/dotnet/wpf)的了,所以已經蛋疼不已的博主果斷提了一個 issue(https://github.com/dotnet/wpf/issues/307),但願有遇到一樣困難的小夥伴能在上面支持一下,讓巨硬早日把 TextBlock 選擇這功能加上。

相關文章
相關標籤/搜索