经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » C# » 查看文章
WPF实现类似ChatGPT的逐字打印效果
来源:cnblogs  作者:czwy  时间:2023/8/11 8:36:39  对本文有异议

背景

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

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

技术要点与实现

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

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

由于方案二的思路与WPF实现跳动的字符效果中的效果实现思路非常类似,具体实现不再详述。接下来我们看一下方案一通过关键帧动画拼接字符串的具体实现。

  1. public class TypingCharAnimationBehavior : Behavior<TextBlock>
  2. {
  3. private Storyboard _storyboard;
  4. protected override void OnAttached()
  5. {
  6. base.OnAttached();
  7. this.AssociatedObject.Loaded += AssociatedObject_Loaded; ;
  8. this.AssociatedObject.Unloaded += AssociatedObject_Unloaded;
  9. BindingOperations.SetBinding(this, TypingCharAnimationBehavior.InternalTextProperty, new Binding("Tag") { Source = this.AssociatedObject });
  10. }
  11. private void AssociatedObject_Unloaded(object sender, RoutedEventArgs e)
  12. {
  13. StopEffect();
  14. }
  15. private void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
  16. {
  17. if (IsEnabled)
  18. BeginEffect(InternalText);
  19. }
  20. protected override void OnDetaching()
  21. {
  22. base.OnDetaching();
  23. this.AssociatedObject.Loaded -= AssociatedObject_Loaded;
  24. this.AssociatedObject.Unloaded -= AssociatedObject_Unloaded;
  25. this.ClearValue(TypingCharAnimationBehavior.InternalTextProperty);
  26. if (_storyboard != null)
  27. {
  28. _storyboard.Remove(this.AssociatedObject);
  29. _storyboard.Children.Clear();
  30. }
  31. }
  32. private string InternalText
  33. {
  34. get { return (string)GetValue(InternalTextProperty); }
  35. set { SetValue(InternalTextProperty, value); }
  36. }
  37. private static readonly DependencyProperty InternalTextProperty =
  38. DependencyProperty.Register("InternalText", typeof(string), typeof(TypingCharAnimationBehavior),
  39. new PropertyMetadata(OnInternalTextChanged));
  40. private static void OnInternalTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  41. {
  42. var source = d as TypingCharAnimationBehavior;
  43. if (source._storyboard != null)
  44. {
  45. source._storyboard.Stop(source.AssociatedObject);
  46. source._storyboard.Children.Clear();
  47. }
  48. source.SetEffect(e.NewValue == null ? string.Empty : e.NewValue.ToString());
  49. }
  50. public bool IsEnabled
  51. {
  52. get { return (bool)GetValue(IsEnabledProperty); }
  53. set { SetValue(IsEnabledProperty, value); }
  54. }
  55. public static readonly DependencyProperty IsEnabledProperty =
  56. DependencyProperty.Register("IsEnabled", typeof(bool), typeof(TypingCharAnimationBehavior), new PropertyMetadata(true, (d, e) =>
  57. {
  58. bool b = (bool)e.NewValue;
  59. var source = d as TypingCharAnimationBehavior;
  60. source.SetEffect(source.InternalText);
  61. }));
  62. private void SetEffect(string text)
  63. {
  64. if (string.IsNullOrEmpty(text) || this.AssociatedObject.IsLoaded == false)
  65. {
  66. StopEffect();
  67. return;
  68. }
  69. BeginEffect(text);
  70. }
  71. private void StopEffect()
  72. {
  73. if (_storyboard != null)
  74. {
  75. _storyboard.Stop(this.AssociatedObject);
  76. }
  77. }
  78. private void BeginEffect(string text)
  79. {
  80. StopEffect();
  81. int textLength = text.Length;
  82. if (textLength < 1 || IsEnabled == false) return;
  83. if (_storyboard == null)
  84. _storyboard = new Storyboard();
  85. double duration = 0.15d;
  86. StringAnimationUsingKeyFrames frames = new StringAnimationUsingKeyFrames();
  87. Storyboard.SetTargetProperty(frames, new PropertyPath(TextBlock.TextProperty));
  88. frames.Duration = TimeSpan.FromSeconds(textLength * duration);
  89. for(int i=0;i<textLength;i++)
  90. {
  91. frames.KeyFrames.Add(new DiscreteStringKeyFrame()
  92. {
  93. Value = text.Substring(0,i+1),
  94. KeyTime = TimeSpan.FromSeconds(i * duration),
  95. });
  96. }
  97. _storyboard.Children.Add(frames);
  98. _storyboard.Begin(this.AssociatedObject, true);
  99. }
  100. }

由于每一帧都在修改TextBlockText属性的值,如果TypingCharAnimationBehavior直接绑定TextBlockText属性,当Text属性的数据源发生变化时,无法判断是关键帧动画修改的,还是外部数据源变化导致Text的值被修改。因此这里用TextBlockTag属性暂存要显示的字符串内容。调用的时候只需要把需要显示的字符串变量绑定到Tag,并在TextBlock添加Behavior即可,代码如下:

  1. <TextBlock x:Name="source"
  2. IsEnabled="True"
  3. Tag="{Binding TypingText, ElementName=self}"
  4. TextWrapping="Wrap">
  5. <i:Interaction.Behaviors>
  6. <local:TypingCharAnimationBehavior IsEnabled="True" />
  7. </i:Interaction.Behaviors>
  8. </TextBlock>

小结

两种方案各有利弊:

  • 关键帧动画拼接字符串这个方法的优点是最大程度还原了逐字输出的过程,缺点是需要额外的属性来辅助,另外遇到英文单词换行时,会出现单词从上一行行尾跳到下一行行首的问题;
  • 通过TextEffect设置字体颜色这个方法则相反,不需要额外的属性辅助,并且不会出现单词在输入过程中从行尾跳到下一行行首的问题,开篇中两种实现方法效果图中能看出这一细微差异。但是一开始就把文字都渲染到界面上,只是通过透明的字体颜色骗过用户的眼睛,逐字改变字体颜色模拟逐字打印的效果。

原文链接:https://www.cnblogs.com/czwy/p/17617600.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号