WPF實(shí)現(xiàn)類似ChatGPT逐字打印效果的示例代碼
背景
前一段時(shí)間ChatGPT類的應(yīng)用十分火爆,這類應(yīng)用在回答用戶的問題時(shí)逐字打印輸出,像極了真人打字回復(fù)消息。出于對(duì)這個(gè)效果的興趣,決定用WPF模擬這個(gè)效果。
真實(shí)的ChatGPT逐字輸出效果涉及其語言生成模型原理以及服務(wù)端與前端通信機(jī)制,本文不做過多闡述,重點(diǎn)是如何用WPF模擬這個(gè)效果。
技術(shù)要點(diǎn)與實(shí)現(xiàn)
對(duì)于這個(gè)逐字輸出的效果,我想到了兩種實(shí)現(xiàn)方法:
方法一:根據(jù)字符串長度n,添加n個(gè)關(guān)鍵幀DiscreteStringKeyFrame,第一幀的Value為字符串的第一個(gè)字符,緊接著的關(guān)鍵幀都比上一幀的Value多一個(gè)字符,直到最后一幀的Value是完整的目標(biāo)字符串。實(shí)現(xiàn)效果如下所示:

方法二:首先把TextBlock的字體顏色設(shè)置為透明,然后通過TextEffect的PositionStart和PositionCount屬性控制應(yīng)用動(dòng)畫效果的子字符串的起始位置以及長度,同時(shí)使用ColorAnimation設(shè)置TextEffect的Foreground屬性由透明變?yōu)槟繕?biāo)顏色(假定是黑色)。實(shí)現(xiàn)效果如下所示:

由于方案二的思路與WPF實(shí)現(xiàn)跳動(dòng)的字符效果中的效果實(shí)現(xiàn)思路非常類似,具體實(shí)現(xiàn)不再詳述。接下來我們看一下方案一通過關(guān)鍵幀動(dòng)畫拼接字符串的具體實(shí)現(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ā)生變化時(shí),無法判斷是關(guān)鍵幀動(dòng)畫修改的,還是外部數(shù)據(jù)源變化導(dǎo)致Text的值被修改。因此這里用TextBlock的Tag屬性暫存要顯示的字符串內(nèi)容。調(diào)用的時(shí)候只需要把需要顯示的字符串變量綁定到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)鍵幀動(dòng)畫拼接字符串這個(gè)方法的優(yōu)點(diǎn)是最大程度還原了逐字輸出的過程,缺點(diǎn)是需要額外的屬性來輔助,另外遇到英文單詞換行時(shí),會(huì)出現(xiàn)單詞從上一行行尾跳到下一行行首的問題;
通過TextEffect設(shè)置字體顏色這個(gè)方法則相反,不需要額外的屬性輔助,并且不會(huì)出現(xiàn)單詞在輸入過程中從行尾跳到下一行行首的問題,開篇中兩種實(shí)現(xiàn)方法效果圖中能看出這一細(xì)微差異。但是一開始就把文字都渲染到界面上,只是通過透明的字體顏色騙過用戶的眼睛,逐字改變字體顏色模擬逐字打印的效果。
到此這篇關(guān)于WPF實(shí)現(xiàn)類似ChatGPT逐字打印效果的示例代碼的文章就介紹到這了,更多相關(guān)WPF逐字打印效果內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
c#使用linq技術(shù)創(chuàng)建xml文件的小例子
c#使用linq技術(shù)創(chuàng)建xml文件的小例子,需要的朋友可以參考一下2013-03-03
C#連續(xù)任務(wù)Task.ContinueWith方法
這篇文章介紹了C#中的連續(xù)任務(wù)Task.ContinueWith方法,文中通過示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-04-04
使用C#從零到完整實(shí)現(xiàn)的并發(fā)單詞頻率統(tǒng)計(jì)器
在工業(yè)自動(dòng)化中,經(jīng)常需要處理日志文件、設(shè)備數(shù)據(jù)記錄,并統(tǒng)計(jì)關(guān)鍵字出現(xiàn)頻率,所以本文給大家使用C#從零到完整實(shí)現(xiàn)了一個(gè)并發(fā)單詞頻率統(tǒng)計(jì)器,需要的朋友可以參考下2026-06-06
Unity 從UI中拖拽對(duì)象放置并拖動(dòng)效果 附demo
最近新接了個(gè)需求,要求模擬場(chǎng)景并生成3D對(duì)象,對(duì)象可以跟隨鼠標(biāo)移動(dòng)效果,今天小編把我實(shí)現(xiàn)的demo分享到腳本之家平臺(tái),對(duì)Unity UI拖拽相關(guān)知識(shí)感興趣的朋友跟隨小編一起學(xué)習(xí)吧2021-05-05
C#使用JArray和JObject封裝JSON對(duì)象
這篇文章介紹了C#使用JArray和JObject封裝JSON對(duì)象的方法,文中通過示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-07-07
C#實(shí)現(xiàn)異步連接Sql Server數(shù)據(jù)庫的方法
這篇文章主要介紹了C#實(shí)現(xiàn)異步連接Sql Server數(shù)據(jù)庫的方法,涉及C#中await方法的相關(guān)使用技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-04-04
C#基于TCP協(xié)議的服務(wù)器端和客戶端通信編程的基礎(chǔ)教程
這篇文章主要介紹了C#基于TCP協(xié)議的服務(wù)器端和客戶端通信編程的基礎(chǔ)教程,文中講解了C#中TCP編程主要相關(guān)的TcpListener類與TcpClient類用法,需要的朋友可以參考下2016-04-04

