首页 > 其他分享 >WPF实现类似ChatGPT的逐字打印效果

WPF实现类似ChatGPT的逐字打印效果

时间:2023-08-10 19:58:25浏览次数:37  
标签:storyboard 逐字 void AssociatedObject TypingCharAnimationBehavior private source W

背景

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

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

技术要点与实现

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

  • 方法一:根据字符串长度n,添加n个关键帧DiscreteStringKeyFrame,第一帧的Value为字符串的第一个字符,紧接着的关键帧都比上一帧的Value多一个字符,直到最后一帧的Value是完整的目标字符串。实现效果如下所示:
  • 方法二:首先把TextBlock的字体颜色设置为透明,然后通过TextEffectPositionStartPositionCount属性控制应用动画效果的子字符串的起始位置以及长度,同时使用ColorAnimation设置TextEffectForeground属性由透明变为目标颜色(假定是黑色)。实现效果如下所示:
    image

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

标签:storyboard,逐字,void,AssociatedObject,TypingCharAnimationBehavior,private,source,W
From: https://www.cnblogs.com/czwy/p/17617600.html

相关文章

  • 定制 ChatGPT 以满足您的需求 自定义说明
    推荐:使用NSDT场景编辑器快速助你搭建可二次编辑的3D应用场景20月<>日,OpenAI 宣布他们正在引入带有自定义说明的新流程,以根据您的特定需求定制ChatGPT。什么是自定义说明?新的测试版自定义指令功能旨在通过防止用户在聊天会话之间重复常用指令来帮助用户充分利用ChatGPT。......
  • 微软面向企业的Private ChatGPT 参考应用 Chat Copilot
    这两天你可能看过这篇文章:微软面向企业的PrivateChatGPT开源!超详细安装流程反馈![1],这篇文章顶多就是一个AzureOpenAI服务的简单Demo,就连插件机制都没有,这也是促使我写这篇文章的因素之一。微软在Build大会上宣布了CopilotStack,CopilotStack被描述为一个人工智能开发框......
  • WPF实现跳动的字符效果
    本文将介绍一个好玩但实际作用可能不太大的动画效果:跳动的字符。为了提高动画效果的可重用性以及调用的灵活性,通过Behavior实现跳动的字符动画。先看下效果:技术要点与实现通过TextEffect的PositionStart和PositionCount属性控制应用动画效果的子字符串的起始位置以及长度,同时使......
  • ChatGPT提示词
    提示✂️充当Linux终端贡献者: @f 参考: https://www.engraved.blog/building-a-virtual-machine-inside/我希望你充当Linux终端。我将键入命令,您将回复终端应显示的内容。我希望你只回复一个唯一代码块中的终端输出,没有别的。不要写解释。除非我指示你这样做,否则不要键入命......
  • 文心一言 VS 讯飞星火 VS chatgpt (73)-- 算法导论7.1 4题
    四、如何修改QUICKSORT,使得它能够以非递增序进行排序?文心一言:快速排序(Quicksort)是一种常用的排序算法,基于分治策略进行设计。默认情况下,快速排序会以递增序进行排序。若想修改快速排序以实现非递增排序,我们需要调整比较和交换的逻辑。以下是修改后的非递增快速排序的Python代码......
  • 界面控件DevExpress WPF Chart组件——拥有超快的数据可视化库!
    DevExpressWPF Chart组件拥有超大的可视化数据集,并提供交互式仪表板与高性能WPF图表库。DevExpressCharts提供了全面的2D/3D图形集合,包括数十个UI定制和数据分析/数据挖掘选项。PS:DevExpressWPF拥有120+个控件和库,将帮助您交付满足甚至超出企业需求的高性能业务应用程序。......
  • 解决winform调用wpf窗体时原窗体缩小的问题
    在使用winform调用wpf窗体时,原来的winform窗体会缩小,同时分辨率会发生变化,用如下方法来解决这个问题。首先找到winform项目中的Properties ==>AssemblyInfo.cs,打开该文件,在末尾加入如下代码,之后重新运行即可。[assembly:System.Windows.Media.DisableDpiAwareness]//禁用WPF......
  • WPF自定义TreeView滚动条样式
     根据客户需求,要在TreeView目录树上显示10万+个节点,但是目录树显示10万加节点后,整个页面操作起来非常卡,所以给目录树增加了虚拟化设置。但是虚拟化设置一直没生效,后来经过排查发现是使用的自定义滚动条导致了虚拟化设置没有生效,后来自己写了一个滚动条样式,问题解决了。目录树虚......
  • ChatGPT:为未来赋能,为我们提供无限可能
    当说到"ChatGPT:为未来赋能,为我们提供无限可能"时,以下是一些可以列举的原因:1.自动化助手:ChatGPT可以作为自动化助手,为我们提供快速、准确、高效的问题解答和信息查询,在工作、学习和生活中节省时间和精力。2.个性化服务:ChatGPT可以根据个人需求和偏好进行定制化的对话和服务,提供个性......
  • WPF 密码提示框
    首先展示效果 如图密码提示框,可通过小眼睛进行显示和隐藏密码1、自定义控件publicclassTitlePasswordBox:TextBox{///<summary>///密码提示框提示语///</summary>publicstringTitle{get{return(strin......