zoukankan      html  css  js  c++  java
  • 【WPF】实现类似QQ聊天消息的界面

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

    如下图

    Snipaste_2019-02-19_19-33-22

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

    表情在传输过程中的实现参考了新浪微博,采用半角中括号代表表情的方式。例如:“abc[doge]def”就会显示 abc,然后一个2018new_doge02_org,再 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>

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

    效果:

    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 选择这功能加上。

  • 相关阅读:
    这才是你需要的最基础的.Net基础面试题(通俗易懂,最基础的.Net)2
    这才是你需要的最基础的.Net基础面试题(通俗易懂,最基础的.Net)2
    这才是你需要的最基础的.Net基础面试题(通俗易懂,最基础的.Net)2
    这才是你需要的最基础的.Net基础面试题(通俗易懂,最基础的.Net)1
    这才是你需要的最基础的.Net基础面试题(通俗易懂,最基础的.Net)1
    这才是你需要的最基础的.Net基础面试题(通俗易懂,最基础的.Net)1
    python-函数和函数式编程
    Python错误与异常
    IO系统-文件与目录操作
    IO系统-标准C的I/O和文件I/O
  • 原文地址:https://www.cnblogs.com/h82258652/p/10403426.html
Copyright © 2011-2022 走看看