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

来源:https://www.cnblogs.com/h82258652/archive/2019/02/19/10403426.html
-Advertisement-
Play Games

最近公司有個項目,是要求實現類似 QQ 聊天這種功能的。 如下圖 這沒啥難的,稍微複雜的也就表情的解析而已。 表情在傳輸過程中的實現參考了新浪微博,採用半形中括弧代表表情的方式。例如:“abc[doge]def”就會顯示 abc,然後一個,再 def。 於是動手就乾。 創建一個模板控制項來進行封裝,我 ...


最近公司有個項目,是要求實現類似 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 選擇這功能加上。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 對於經常調用的函數,特別是遞歸函數或計算密集的函數,記憶(緩存)返回值可以顯著提高性能。 ...
  • 前言 由於Activiti 預設使用的資料庫是H2資料庫,重啟服務後相關數據會丟失。為了永久保存,所以要配置關係型資料庫,這裡我們選擇 SqlServer ,有錢任性。 環境 Activiti6,SqlServer 2008 配置 文件 修改 然後,引入 lib下引入 sqljdbc4 4.0.ja ...
  • sys模塊 1 #!/usr/bin/env python 2 # -*- coding:utf-8 -*- 3 import sys 4 ''' 5 sys.argv : 在命令行參數是一個空列表,在其他中第一個列表元素程式本身的路徑 6 sys.exit(n) :退出程式,正常退出時exit(0 ...
  • des加密演算法有如下幾個要素: DES加密模式:這裡選ECB 填充:java是pkcs5padding,.net是pkcs7padding。網上說PKCS5Padding與PKCS7Padding基本上是可以通用的。 字元集:utf-8 輸出:base64、hex 密碼/Key:8個字元(共64位... ...
  • https://www.luogu.org/problemnew/show/P1020 (原題鏈接) 第一問就是求最長不上升子序列的長度,自然就想到了c++一本通里動態規劃里O(n^2)的演算法,但題目明確說明“為了讓大家更好地測試n方演算法,本題開啟spj,n方100分,nlogn200分每點兩問,按 ...
  • 如果你想抓取數據,又懶得寫代碼了,可以試試 web scraper 抓取數據。 相關文章: "最簡單的數據抓取教程,人人都用得上" "web scraper 進階教程,人人都用得上" 如果你在使用 web scraper 抓取數據,很有可能碰到如下問題中的一個或者多個,而這些問題可能直接將你計劃打亂 ...
  • Docker可以說是現在微服務,DevOps的基礎,咱們.Net Core自然也得上Docker。.Net Core發佈到Docker容器的教程網上也有不少,但是今天還是想來寫一寫。 你搜.Net core程式發佈到Docker網上一般常見的有兩種方案: 1、在本地編譯成Dll文件後通過SCP命令或 ...
  • Lambda表達式,是用來寫匿名方法的。 在委托用得比較多,因為委托是傳遞方法的。 定義幾個委托: public delegate void DoNoThing();//無參無返回值 public delegate void DoNoThingWithPara(sting name,int age) ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...