背景

前一段时间ChatGPT类的应用十分火爆,这类应用在回答用户的问题时逐字打印输出,像极了真人打字回复消息。出于对这个效果的兴趣,决定用WPF模拟这个效果。

真实的ChatGPT逐字输出效果涉及其语言生成模型原理以及服务端与前端通信机制,本文不做过多阐述,重点是如何用WPF模拟这个效果。

技术要点与实现

对于这个逐字输出的效果,我想到了两种实现方法:

  • 方法一:根据字符串长度n,添加n个关键帧DiscreteStringKeyFrame,第一帧的Value为字符串的第一个字符,紧接着的关键帧都比上一帧的Value多一个字符,直到最后一帧的Value是完整的目标字符串。实现效果如下所示:

  • 方法二:首先把TextBlock的字体颜色设置为透明,然后通过TextEffectPositionStartPositionCount属性控制应用动画效果的子字符串的起始位置以及长度,同时使用ColorAnimation设置TextEffectForeground属性由透明变为目标颜色(假定是黑色)。实现效果如下所示:

由于方案二的思路与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设置字体颜色这个方法则相反,不需要额外的属性辅助,并且不会出现单词在输入过程中从行尾跳到下一行行首的问题,开篇中两种实现方法效果图中能看出这一细微差异。但是一开始就把文字都渲染到界面上,只是通过透明的字体颜色骗过用户的眼睛,逐字改变字体颜色模拟逐字打印的效果。

WPF实现类似ChatGPT的逐字打印效果的更多相关文章

  1. 打造自己的ChatGPT:逐字打印的流式处理

    接口的延迟 在调用OpenAI的接口时,不免会有很慢的感觉,抛去地理位置上的网络延迟,大量的延迟往往发生在响应生成的过程中. 因此,如果使用同步接口的话,需要等待响应完全生成之后才能最终显示输出结果, ...

  2. WPF如何实现类似iPhone界面切换的效果(转载)

    WPF如何实现类似iPhone界面切换的效果 (version .1) 转自:http://blog.csdn.net/fallincloud/article/details/6968764 在论坛上 ...

  3. jQuery实现逐字输入效果

    之前做了个测试小游戏(姑且叫游戏吧)为了增加神秘性,就想给她加个逐字输入效果:刚好在网上找到一个挺好用的,于是就发扬拿来主义:按照自己的喜好做了一丢丢的修改(勉强算是吧\( ̄︶ ̄)> ). 代码 ...

  4. WPF中,如何将Vista Aero效果扩展到整个窗口

    原文:WPF中,如何将Vista Aero效果扩展到整个窗口   WPF中,如何将Vista Aero效果扩展到整个窗口                                         ...

  5. WPF编游戏系列 之二 图标效果

    原文:WPF编游戏系列 之二 图标效果        本篇将要实现图标的两个效果:1. 显示图标标签,2. 图标模糊效果.在上一篇中提到Image没有HTML <img>的Title属性( ...

  6. 基于C#在WPF中使用斑马打印机进行打印【转】

    原文链接:http://ju.outofmemory.cn/entry/132476 最近在项目中接手了一个比较有挑战性的模块——用斑马打印机将需要打印的内容打印出来.苦苦折腾了两天,总算有所收获,就 ...

  7. Duilib实现类似电脑管家扫描目录效果

    实现原理: 1.后台开线程遍历目录,遍历出一个文件路径在界面上更新显示(通过发消息通知主界面) 2.需要扩展一下Duilib控件,在此我扩展了CLabelUI,重写了PaintText函数 扩展控件的 ...

  8. Adobe Edge Animate –地球自转动画的实现,类似flash遮罩层的效果

    Adobe Edge Animate –地球自转动画的实现,类似flash遮罩层的效果 版权声明: 本文版权属于 北京联友天下科技发展有限公司. 转载的时候请注明版权和原文地址. 目前Edge的功能尚 ...

  9. 在WPF中使用PlaneProjection模拟动态3D效果

    原文:在WPF中使用PlaneProjection模拟动态3D效果 虽然在WPF中也集成了3D呈现的功能,在简单的3D应用中,有时候并不需要真实光影的3D场景.毕竟使用3D引擎会消耗很多资源,有时候使 ...

  10. PHP实现类似题库抽题效果

    PHP实现类似题库抽题效果 大家好,我顾某人又回来了,最近学了一点PHP,然后就想写个简单小例子试试,于是就写了一个类似于从题库抽题的东西,大概就是先输入需要抽题的数量,然后从数据库中随机抽取题目. ...

随机推荐

  1. pycharm eslint 关闭

    pycharm 关闭eslint 文件->设置->语言和框架->JavaScript->代码质量工具->ESLint

  2. Ubuntu22.04 安装单机版kubernetes

    前言 上期讲到要实现.net 6框架下的EF Core操作数据库基本增删改查,没有及时兑现.没有兑现的原因就是因为安装kubernetes.安装kubernetes的过程是灾难性的,也是十分顺利的.灾 ...

  3. 2023-05-29:给你一个由 n 个正整数组成的数组 nums 你可以对数组的任意元素执行任意次数的两类操作 如果元素是 偶数 ,除以 2 例如,如果数组是 [1,2,3,4] 那么你可以对最后一

    七.设计算法,仅使用三次实数乘法即可完成复数 a+bi和c+di 相乘.算法需接收a.b.c和d 为输入,分别生成实部 ac-bd 和虚部ad+bc. 文心一言: 可以使用如下算法来计算复数 a+bi ...

  4. UART-UART非常见波特率调试应用笔记

    UART非常见波特率调试 应用笔记 串口通信中的波特率选择,对于确保可靠的数据传输至关重要.波特率是衡量单位时间内传输的比特数,常见的波特率包括300.1200.2400.9600.115200等.不 ...

  5. 2个场景实例讲解GaussDB(DWS)基表统计信息估算不准的处理方案

    摘要:通过2个实例场景讲解GaussDB(DWS)运维解决方案. 本文分享自华为云社区<GaussDB(DWS)运维 -- 基表统计信息估算不准的常见场景及处理方案>,作者:譡里个檔. 场 ...

  6. input 文件上传 formdata

    需求背景 后端给定接口 传xlsx文件 参数:后台需要的参数 格式: formdata 需要   token 1 saveEditIn (e) { 2 this.sheetAll = [] 3 // ...

  7. Tab切换以及倒计时组件封装

    1.Tab组件 功能 支持默认选中tab 子元素可以是文本或者图片 自定义tab的数量,并自适应展示 实现方式 用ul > li标签遍历传入的tabs数组参数渲染 判断是否传入背景,未传则显示文 ...

  8. 安装部署 Kubernetes 仪表板(Dashboard)

    简介 Kubernetes 仪表板(Dashboard)是基于网页的 Kubernetes 用户界面. 你可以使用仪表板: 展示了 Kubernetes 集群中的资源状态信息和所有报错信息. 把容器应 ...

  9. 利用代码生成工具快速生成基于SqlSugar框架的Winform界面项目

    我们接触一个新事物的时候,如果一个事物能够给我们带来非常直观的感官认识,那么我们就很容易接受,反之可能需要很长时间的潜移默化的了解认识才能接受.万物化繁为简,透过本质看表象,往往也是一个认知迭代深入的 ...

  10. SpringBoot 如何优雅的进行全局异常处理?

    在SpringBoot的开发中,为了提高程序运行的鲁棒性,我们经常需要对各种程序异常进行处理,但是如果在每个出异常的地方进行单独处理的话,这会引入大量业务不相关的异常处理代码,增加了程序的耦合,同时未 ...