OpenCV的全稱是Open Source Computer Vision Library,是一個跨平臺的電腦視覺庫。OpenCV是由英特爾公司發起並參與開發,以BSD許可證授權發行,可以在商業和研究領域中免費使用。OpenCV可用於開發實時的圖像處理電腦視覺以及模式識別程式。該程式庫也可以使用 ...
背景
前一段時間ChatGPT類的應用十分火爆,這類應用在回答用戶的問題時逐字列印輸出,像極了真人打字回覆消息。出於對這個效果的興趣,決定用WPF模擬這個效果。
真實的ChatGPT逐字輸出效果涉及其語言生成模型原理以及服務端與前端通信機制,本文不做過多闡述,重點是如何用WPF模擬這個效果。
技術要點與實現
對於這個逐字輸出的效果,我想到了兩種實現方法:
- 方法一:根據字元串長度n,添加n個關鍵幀
DiscreteStringKeyFrame
,第一幀的Value
為字元串的第一個字元,緊接著的關鍵幀都比上一幀的Value
多一個字元,直到最後一幀的Value
是完整的目標字元串。實現效果如下所示:
- 方法二:首先把
TextBlock
的字體顏色設置為透明,然後通過TextEffect
的PositionStart
和PositionCount
屬性控制應用動畫效果的子字元串的起始位置以及長度,同時使用ColorAnimation
設置TextEffect
的Foreground
屬性由透明變為目標顏色(假定是黑色)。實現效果如下所示:
由於方案二的思路與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);
}
}
由於每一幀都在修改TextBlock
的Text
屬性的值,如果TypingCharAnimationBehavior
直接綁定TextBlock
的Text
屬性,當Text
屬性的數據源發生變化時,無法判斷是關鍵幀動畫修改的,還是外部數據源變化導致Text
的值被修改。因此這裡用TextBlock
的Tag
屬性暫存要顯示的字元串內容。調用的時候只需要把需要顯示的字元串變數綁定到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
設置字體顏色這個方法則相反,不需要額外的屬性輔助,並且不會出現單詞在輸入過程中從行尾跳到下一行行首的問題,開篇中兩種實現方法效果圖中能看出這一細微差異。但是一開始就把文字都渲染到界面上,只是通過透明的字體顏色騙過用戶的眼睛,逐字改變字體顏色模擬逐字列印的效果。