WPF實現類似ChatGPT的逐字列印效果

来源:https://www.cnblogs.com/czwy/archive/2023/08/10/17617600.html
-Advertisement-
Play Games

OpenCV的全稱是Open Source Computer Vision Library,是一個跨平臺的電腦視覺庫。OpenCV是由英特爾公司發起並參與開發,以BSD許可證授權發行,可以在商業和研究領域中免費使用。OpenCV可用於開發實時的圖像處理電腦視覺以及模式識別程式。該程式庫也可以使用 ...


背景

前一段時間ChatGPT類的應用十分火爆,這類應用在回答用戶的問題時逐字列印輸出,像極了真人打字回覆消息。出於對這個效果的興趣,決定用WPF模擬這個效果。

真實的ChatGPT逐字輸出效果涉及其語言生成模型原理以及服務端與前端通信機制,本文不做過多闡述,重點是如何用WPF模擬這個效果。

技術要點與實現

對於這個逐字輸出的效果,我想到了兩種實現方法:

  • 方法一:根據字元串長度n,添加n個關鍵幀DiscreteStringKeyFrame,第一幀的Value為字元串的第一個字元,緊接著的關鍵幀都比上一幀的Value多一個字元,直到最後一幀的Value是完整的目標字元串。實現效果如下所示:
  • 方法二:首先把TextBlock的字體顏色設置為透明,然後通過TextEffectPositionStartPositionCount屬性控制應用動畫效果的子字元串的起始位置以及長度,同時使用ColorAnimation設置TextEffectForeground屬性由透明變為目標顏色(假定是黑色)。實現效果如下所示:
    image

由於方案二的思路與WPF實現跳動的字元效果中的效果實現思路非常類似,具體實現不再詳述。接下來我們看一下方案一通過關鍵幀動畫拼接字元串的具體實現。

public class TypingCharAnimationBehavior : Behavior<TextBlock>
{
    private Storyboard _storyboard;

    protected override void OnAttached()
    {
        base.OnAttached();

        this.AssociatedObject.Loaded += AssociatedObject_Loaded; ;
        this.AssociatedObject.Unloaded += AssociatedObject_Unloaded;
        BindingOperations.SetBinding(this, TypingCharAnimationBehavior.InternalTextProperty, new Binding("Tag") { Source = this.AssociatedObject });
    }

    private void AssociatedObject_Unloaded(object sender, RoutedEventArgs e)
    {
        StopEffect();
    }

    private void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
    {
        if (IsEnabled)
            BeginEffect(InternalText);
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();

        this.AssociatedObject.Loaded -= AssociatedObject_Loaded;
        this.AssociatedObject.Unloaded -= AssociatedObject_Unloaded;
        this.ClearValue(TypingCharAnimationBehavior.InternalTextProperty);

        if (_storyboard != null)
        {
            _storyboard.Remove(this.AssociatedObject);
            _storyboard.Children.Clear();
        }
    }

    private string InternalText
    {
        get { return (string)GetValue(InternalTextProperty); }
        set { SetValue(InternalTextProperty, value); }
    }

    private static readonly DependencyProperty InternalTextProperty =
    DependencyProperty.Register("InternalText", typeof(string), typeof(TypingCharAnimationBehavior),
    new PropertyMetadata(OnInternalTextChanged));

    private static void OnInternalTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var source = d as TypingCharAnimationBehavior;
        if (source._storyboard != null)
        {
            source._storyboard.Stop(source.AssociatedObject);
            source._storyboard.Children.Clear();
        }
        source.SetEffect(e.NewValue == null ? string.Empty : e.NewValue.ToString());
    }

    public bool IsEnabled
    {
        get { return (bool)GetValue(IsEnabledProperty); }
        set { SetValue(IsEnabledProperty, value); }
    }

    public static readonly DependencyProperty IsEnabledProperty =
        DependencyProperty.Register("IsEnabled", typeof(bool), typeof(TypingCharAnimationBehavior), new PropertyMetadata(true, (d, e) =>
        {
            bool b = (bool)e.NewValue;
            var source = d as TypingCharAnimationBehavior;
            source.SetEffect(source.InternalText);
        }));

    private void SetEffect(string text)
    {
        if (string.IsNullOrEmpty(text) || this.AssociatedObject.IsLoaded == false)
        {
            StopEffect();
            return;
        }

        BeginEffect(text);

    }

    private void StopEffect()
    {
        if (_storyboard != null)
        {
            _storyboard.Stop(this.AssociatedObject);
        }
    }

    private void BeginEffect(string text)
    {
        StopEffect();

        int textLength = text.Length;
        if (textLength < 1  || IsEnabled == false) return;

        if (_storyboard == null)
            _storyboard = new Storyboard();
        double duration = 0.15d;

        StringAnimationUsingKeyFrames frames = new StringAnimationUsingKeyFrames();

        Storyboard.SetTargetProperty(frames, new PropertyPath(TextBlock.TextProperty));

        frames.Duration = TimeSpan.FromSeconds(textLength * duration);

        for(int i=0;i<textLength;i++)
        {
            frames.KeyFrames.Add(new DiscreteStringKeyFrame()
            {
                Value = text.Substring(0,i+1),
                KeyTime = TimeSpan.FromSeconds(i * duration),
            });
        }

        _storyboard.Children.Add(frames);
        _storyboard.Begin(this.AssociatedObject, true);
    }
}

由於每一幀都在修改TextBlockText屬性的值,如果TypingCharAnimationBehavior直接綁定TextBlockText屬性,當Text屬性的數據源發生變化時,無法判斷是關鍵幀動畫修改的,還是外部數據源變化導致Text的值被修改。因此這裡用TextBlockTag屬性暫存要顯示的字元串內容。調用的時候只需要把需要顯示的字元串變數綁定到Tag,併在TextBlock添加Behavior即可,代碼如下:

<TextBlock x:Name="source"
            IsEnabled="True"
            Tag="{Binding TypingText, ElementName=self}"
            TextWrapping="Wrap">
    <i:Interaction.Behaviors>
        <local:TypingCharAnimationBehavior IsEnabled="True" />
    </i:Interaction.Behaviors>
</TextBlock>

小結

兩種方案各有利弊:

  • 關鍵幀動畫拼接字元串這個方法的優點是最大程度還原了逐字輸出的過程,缺點是需要額外的屬性來輔助,另外遇到英文單詞換行時,會出現單詞從上一行行尾跳到下一行行首的問題;
  • 通過TextEffect設置字體顏色這個方法則相反,不需要額外的屬性輔助,並且不會出現單詞在輸入過程中從行尾跳到下一行行首的問題,開篇中兩種實現方法效果圖中能看出這一細微差異。但是一開始就把文字都渲染到界面上,只是通過透明的字體顏色騙過用戶的眼睛,逐字改變字體顏色模擬逐字列印的效果。

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

-Advertisement-
Play Games
更多相關文章
  • 映射列值是指將一個列中的某些特定值映射為另外一些值,常用於數據清洗和轉換。 使用映射列值的場景有很多,以下是幾種常見的場景: 1. 將字元串類型的列中的某些值映射為數字。例如,將“男”和“女”分別映射為 0 和 1,以便進行機器學習演算法的訓練和預測。 2. 將縮寫替換為全稱。例如,將“USA”和“U ...
  • PeFile模塊是`Python`中一個強大的攜帶型第三方`PE`格式分析工具,用於解析和處理`Windows`可執行文件。該模塊提供了一系列的API介面,使得用戶可以通過`Python`腳本來讀取和分析PE文件的結構,包括文件頭、節表、導入表、導出表、資源表、重定位表等等。此外,PEfile模塊還... ...
  • 源碼請到:自然語言處理練習: 學習自然語言處理時候寫的一些代碼 (gitee.com) 數據來源: 搜狗新聞語料庫 由於鏈接失效,現在使用百度網盤分享 鏈接:https://pan.baidu.com/s/1RTx2k7V3Ujgg9-Rv8I8IRA?pwd=ujn3 提取碼:ujn3 停用詞 來 ...
  • ## 前言 C++可以動態的分類記憶體(但是得主動釋放記憶體,避免記憶體泄漏),而java並不能這樣,java的記憶體分配和垃圾回收統一由JVM管理,是不是java就不能操作記憶體呢?當然有其他辦法可以操作記憶體,接下來有請`Unsafe`出場,我們一起看看`Unsafe`是如何花式操作記憶體的。 ## Unsa ...
  • 來源:juejin.cn/post/7139202066362138654 昨天線上容器突然cpu飆升,也是第一次排查這種問題所以記錄一下~ ## 前言 首先問題是這樣的,周五正在寫文檔,突然收到了線上報警,發現cpu占用達到了90多,上平臺監控系統查看容器,在jvm監控中發現有一個pod在兩個小時 ...
  • 在.NET中,理解對象的記憶體佈局是非常重要的,這將幫助我們更好地理解.NET的運行機制和優化代碼,本文將介紹.NET中的對象記憶體佈局。 .NET中的數據類型主要分為兩類,值類型和引用類型。值類型包括了基本類型(如int、bool、double、char等)、枚舉類型(enum)、結構體類型(stru ...
  • 在開發`winfrom`應用時,經常遇到異常:`System.InvalidOperationException:“線程間操作無效: 從不是創建控制項“xxxx”的線程訪問它。`出現這個異常的原因是創建這個UI的線程,和當前訪問這個UI的線程不會是同一個。Winform為了防止線程不安全,因此對這個跨... ...
  • # Unity AssetPostprocessor模型相關函數詳解 在Unity中,AssetPostprocessor是一個非常有用的工具,它可以在導入資源時自動執行一些操作。在本文中,我們將重點介紹AssetPostprocessor中與模型相關的函數,並提供多個使用例子。 ## OnPost ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...