WPF實現(xiàn)類似ChatGPT逐字打印效果的示例代碼
背景
前一段時間ChatGPT類的應(yīng)用十分火爆,這類應(yīng)用在回答用戶的問題時逐字打印輸出,像極了真人打字回復(fù)消息。出于對這個效果的興趣,決定用WPF模擬這個效果。
真實的ChatGPT逐字輸出效果涉及其語言生成模型原理以及服務(wù)端與前端通信機制,本文不做過多闡述,重點是如何用WPF模擬這個效果。
技術(shù)要點與實現(xiàn)
對于這個逐字輸出的效果,我想到了兩種實現(xiàn)方法:
方法一:根據(jù)字符串長度n,添加n個關(guān)鍵幀DiscreteStringKeyFrame
,第一幀的Value
為字符串的第一個字符,緊接著的關(guān)鍵幀都比上一幀的Value
多一個字符,直到最后一幀的Value
是完整的目標(biāo)字符串。實現(xiàn)效果如下所示:
方法二:首先把TextBlock
的字體顏色設(shè)置為透明,然后通過TextEffect
的PositionStart
和PositionCount
屬性控制應(yīng)用動畫效果的子字符串的起始位置以及長度,同時使用ColorAnimation
設(shè)置TextEffect
的Foreground
屬性由透明變?yōu)槟繕?biāo)顏色(假定是黑色)。實現(xiàn)效果如下所示:
由于方案二的思路與WPF實現(xiàn)跳動的字符效果中的效果實現(xiàn)思路非常類似,具體實現(xiàn)不再詳述。接下來我們看一下方案一通過關(guān)鍵幀動畫拼接字符串的具體實現(xiàn)。
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); } }
由于每一幀都在修改TextBlock
的Text
屬性的值,如果TypingCharAnimationBehavior
直接綁定TextBlock
的Text
屬性,當(dāng)Text
屬性的數(shù)據(jù)源發(fā)生變化時,無法判斷是關(guān)鍵幀動畫修改的,還是外部數(shù)據(jù)源變化導(dǎo)致Text
的值被修改。因此這里用TextBlock
的Tag
屬性暫存要顯示的字符串內(nèi)容。調(diào)用的時候只需要把需要顯示的字符串變量綁定到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>
小結(jié)
兩種方案各有利弊:
關(guān)鍵幀動畫拼接字符串這個方法的優(yōu)點是最大程度還原了逐字輸出的過程,缺點是需要額外的屬性來輔助,另外遇到英文單詞換行時,會出現(xiàn)單詞從上一行行尾跳到下一行行首的問題;
通過TextEffect
設(shè)置字體顏色這個方法則相反,不需要額外的屬性輔助,并且不會出現(xiàn)單詞在輸入過程中從行尾跳到下一行行首的問題,開篇中兩種實現(xiàn)方法效果圖中能看出這一細微差異。但是一開始就把文字都渲染到界面上,只是通過透明的字體顏色騙過用戶的眼睛,逐字改變字體顏色模擬逐字打印的效果。
到此這篇關(guān)于WPF實現(xiàn)類似ChatGPT逐字打印效果的示例代碼的文章就介紹到這了,更多相關(guān)WPF逐字打印效果內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C# 使用動態(tài)庫DllImport("kernel32")讀寫ini文件的步驟
kernel32.dll是Windows中非常重要的32位動態(tài)鏈接庫文件,屬于內(nèi)核級文件,這篇文章主要介紹了C# 利用動態(tài)庫DllImport("kernel32")讀寫ini文件,需要的朋友可以參考下2023-05-05C# DataTable.Select()根據(jù)條件篩選數(shù)據(jù)問題
這篇文章主要介紹了C# DataTable.Select()根據(jù)條件篩選數(shù)據(jù)問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-01-01