WPF实现类似ChatGPT逐字打印效果的示例代码

 更新时间:2023年08月11日 09:54:03   作者:czwy  
前一段时间ChatGPT类的应用十分火爆,这类应用在回答用户的问题时逐字打印输出,像极了真人打字回复消息,本文就来利用WPF模拟一下这种逐字打印的效果吧

背景

前一段时间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逐字打印效果的示例代码的文章就介绍到这了,更多相关WPF逐字打印效果内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • C# linq查询之动态OrderBy用法实例

    C# linq查询之动态OrderBy用法实例

    这篇文章主要介绍了C# linq查询之动态OrderBy用法,实例分析了C#采用linq方式查询时动态排序的相关技巧,需要的朋友可以参考下
    2015-06-06
  • C#异步执行任务的方法

    C#异步执行任务的方法

    这篇文章主要介绍了C#异步执行任务的方法,以一个简单实例形式分析了C#异步执行的实现技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-07-07
  • c#创建windows服务(Windows Services)详细步骤

    c#创建windows服务(Windows Services)详细步骤

    这篇文章主要介绍了c#创建windows服务(Windows Services)详细步骤,大家参考使用吧
    2013-12-12
  • c#中单例类与静态类的区别以及使用场景

    c#中单例类与静态类的区别以及使用场景

    这篇文章主要给大家介绍了关于c#中单例类与静态类的区别以及使用场景的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-01-01
  • C#使用Socket上传并保存图片的方法

    C#使用Socket上传并保存图片的方法

    这篇文章主要介绍了C#使用Socket上传并保存图片的方法,涉及C#使用Socket进行文件传输的相关技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-04-04
  • c#数据绑定之数据转化为信息的示例

    c#数据绑定之数据转化为信息的示例

    这篇文章主要介绍了c#数据绑定中的数据转化为信息的示例,需要的朋友可以参考下
    2014-04-04
  • C#获取App.Config配置项的方法总结

    C#获取App.Config配置项的方法总结

    在本篇内容里小编给大家分享了C#获取App.Config配置项的方法和相关知识点,需要的朋友们学习下。
    2019-03-03
  • 浅谈Java与C#的一些细微差别

    浅谈Java与C#的一些细微差别

    说起C#和Java这两门语言(语法,数据类型 等),个人以为,大概有90%以上的相似,甚至可以认为几乎一样。但是在工作中,我也发现了一些细微的差别
    2013-11-11
  • C#中foreach实现原理详解

    C#中foreach实现原理详解

    这篇文章主要为大家详细介绍了C#中foreach实现原理,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-09-09
  • C# 微信支付 wx.chooseWXPay 签名错误的解决方法

    C# 微信支付 wx.chooseWXPay 签名错误的解决方法

    本篇文章主要介绍了C# 微信支付 wx.chooseWXPay 签名错误的解决方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-12-12

最新评论