65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 和 WPF 中的矩阵风格雨

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (62投票s)

2019年9月2日

CPOL

6分钟阅读

viewsIcon

63407

downloadIcon

3450

Matrix数字雨、Matrix代码或有时称为绿色雨,是《黑客帝国》电影中出现的计算机代码

引言

如MSDN所述,DrawingVisual是一个轻量级的绘图类,用于渲染形状、图像或文本。此类被认为是轻量级的,因为它不提供布局、输入、焦点或事件处理,从而提高了其性能。

背景

在开始编码之前,我查阅了MSDN页面,以了解DrawingVisual对象WPF图形渲染概述的基础知识。

我们在WPF中常用的许多元素/控件,例如ButtonComboBoxShape等,都具有这些特性:

  • 可以由多个元素组成,每个组成元素都提供焦点方法、事件处理和许多功能,这使我们能够拥有大量的编程自由度,但如果只需要执行一些绘图操作,则会带来很多“开销”。
  • 扩展了普通对象,这些对象并未针对特定目的进行优化,而是用于通用服务。

DrawingVisual的目的是提出一种轻量级的对象绘图方法。

关于Matrix数字雨效果,我从CodePen获取了一些开发思路。CodePen是一个在线社区,用于测试和展示用户创建的HTML、CSS和JavaScript代码片段。

我假设读者了解WPF的Dispatcher。简而言之,当您执行一个WPF应用程序时,它会自动创建一个新的Dispatcher对象并调用其Run方法。所有视觉元素都将由Dispatcher线程创建,并且对视觉元素的所有修改都必须在Dispatcher线程上执行。

Using the Code

我的示例包含两个项目

1. MatrixRain

这是解决方案的核心。该项目实现了一个模拟Matrix数字雨效果的UserControl。该UserControl可以用于任何Window/Page等。

  1. 设置参数。

    SetParameter方法允许设置一些动画参数:

    ...
    public void SetParameter(int framePerSecond = 0, FontFamily fontFamily = null,
                             int fontSize = 0, Brush backgroundBrush = null, 
                             Brush textBrush = null, String characterToDisplay = "")
    ...
    • framePerSecond:每秒帧数刷新(此参数影响雨的“速度”)
    • fontFamily:使用的字体系列
    • fontSize:使用的字体大小
    • backgroundBrush:用于背景的笔刷
    • textBrush:用于文本的笔刷
    • characterToDisplay:雨滴将随机从此string中选择要显示的字符
  2. 开始动画。

    StartStop方法允许启动和停止动画。

    public void Start() {
        _DispatcherTimer.Start();
    }
    public void Stop() {
        _DispatcherTimer.Stop();
    }
    ...

    动画通过System.Timers.Timer进行控制。我更喜欢这个解决方案而不是System.Windows.Threading.DispatcherTimer,因为DispatcherTimer在每个Dispatcher循环的顶部都会重新评估,并且不保证计时器在时间间隔发生时精确执行。

    每次计时器触发,都会调用_DispatcherTimerTick(object sender, EventArgs e)方法。
    此方法不在Dispatcher线程上执行,因此第一件事是同步调用到Dispatcher线程,因为我们需要处理一些只有主线程才能访问的资源。

    ...
    
    private void _DispatcherTimerTick(object sender, EventArgs e)
    {
        if (!Dispatcher.CheckAccess()) {
            //synchronize on main thread
            System.Timers.ElapsedEventHandler dt = _DispatcherTimerTick;
            Dispatcher.Invoke(dt,sender,e);
            return;
        }
        ....
    }
  3. 绘制新帧。

    一旦来自计时器的调用到达Dispatcher线程,它会执行两个操作:

    1. 设计新帧

      该帧由_RenderDrops()方法创建。在这里,会创建一个新的DrawingVisual及其DrawingContext来绘制对象。DrawingContext允许绘制线条、椭圆、几何图形、图像等。

      DrawingVisual drawingVisual = new DrawingVisual();
      DrawingContext drawingContext = drawingVisual.RenderOpen();

      首先,该方法创建一个具有10%不透明度的黑色背景(稍后我将解释为什么设置为10%不透明度)。

      之后,我们遍历一个名为_Drops的数组。

      此数组代表绘制字母的列(参见图像中的红色列)。数组的值代表必须绘制新字母的行(参见图像中的蓝色圆圈)。当雨滴的值到达图像“底部”时,雨滴会立即从顶部重新开始,或者在经过一系列周期后随机重新开始。

      ...
      //looping over drops
      for (var i = 0; i < _Drops.Length; i++) {
          // new drop position
          double x = _BaselineOrigin.X + _LetterAdvanceWidth * i;
          double y = _BaselineOrigin.Y + _LetterAdvanceHeight * _Drops[i];
      
          // check if new letter does not goes outside the image
          if (y + _LetterAdvanceHeight < _CanvasRect.Height) {
              // add new letter to the drawing
              var glyphIndex = _GlyphTypeface.CharacterToGlyphMap[_AvaiableLetterChars[
                               _CryptoRandom.Next(0, _AvaiableLetterChars.Length - 1)]];
              glyphIndices.Add(glyphIndex);
              advancedWidths.Add(0);
              glyphOffsets.Add(new Point(x, -y));
          }
      
          //sending the drop back to the top randomly after it has crossed the image
          //adding a randomness to the reset to make the drops scattered on the Y axis
          if (_Drops[i] * _LetterAdvanceHeight > _CanvasRect.Height && 
                                                 _CryptoRandom.NextDouble() > 0.775) {
              _Drops[i] = 0;
          }
          //incrementing Y coordinate
          _Drops[i]++;
      }
      // add glyph on drawing context
      if (glyphIndices.Count > 0) {
          GlyphRun glyphRun = new GlyphRun(_GlyphTypeface,0,false,_RenderingEmSize,
                              glyphIndices,_BaselineOrigin,advancedWidths,glyphOffsets,
                              null,null,null,null,null);
          drawingContext.DrawGlyphRun(_TextBrush, glyphRun);
      }
      ...

      总结一下该方法:_RenderDrops()生成一个包含带不透明度背景和新雨滴字母的DrawingVisual。例如:


      帧1


      参考系 2


      帧3

      帧4
    2. 将新帧复制到前一帧之上

      如前所述,新帧只生成“新”字母,但我们如何让之前的字母逐渐消失?

      这是通过帧的背景实现的,该背景是黑色且具有10%的不透明度。当我们把新帧复制到前一帧之上时,混合效果就起作用了。“复制覆盖”会减弱之前字母的亮度,如下面的例子所示:


      最终帧1 = 黑色背景 + 帧1


      最终帧2 = 最终帧1 + 帧2


      最终帧3 = 最终帧2 + 帧3

      最终帧4 = 最终帧3 + 帧4

      附注:我将Drawing Visual渲染到一个RenderTargetBitmap。我可以直接将其应用于我的图像。

      _MyImage.Source = _RenderTargetBitmap

      这个解决方案的问题在于,在每个周期,该操作都会在每个周期分配大量内存。为了解决这个问题,我使用了WriteableBitmap,它只在初始化代码中分配一次内存。

      ...
      _WriteableBitmap.Lock();
      _RenderTargetBitmap.CopyPixels(new Int32Rect(0, 0, _RenderTargetBitmap.PixelWidth,
                                                  _RenderTargetBitmap.PixelHeight), 
                                     _WriteableBitmap.BackBuffer, 
                                     _WriteableBitmap.BackBufferStride * 
                                     _WriteableBitmap.PixelHeight,
                                     _WriteableBitmap.BackBufferStride);
      _WriteableBitmap.AddDirtyRect(new Int32Rect(0, 0, _RenderTargetBitmap.PixelWidth, 
                                                  _RenderTargetBitmap.PixelHeight));
      _WriteableBitmap.Unlock();
      ...

MatrixRainWpfApp

此项目引用了MatrixRain,并展示了MatrixRain用户控件的潜力。代码没有注释,因为它非常简单,不需要注释。

  1. MainWindow.xaml中,将一个MatrixRain控件添加到窗口中。
    ...
    xmlns:MatrixRain="clr-namespace:MatrixRain;assembly=MatrixRain"
    ...
    <MatrixRain:MatrixRain x:Name="mRain" HorizontalAlignment="Left" Height="524" 
                           Margin="10,35,0,0" VerticalAlignment="Top" Width="1172"/>
    ...
  2. 在初始化期间,我从嵌入式资源中读取了一个特殊字体,并将其传递给MatrixRain控件。
    FontFamily rfam = new FontFamily(new Uri("pack://application:,,,"), 
                                     "./font/#Matrix Code NFI");
    mRain.SetParameter(fontFamily: rfam);

    请注意字体。这是我找到它的链接:https://www.1001fonts.com/matrix-code-nfi-font.html。仅供个人用途免费使用。

  3. 两个按钮:StartStop;控制动画。
    private void _StartButtonClick(object sender, RoutedEventArgs e)
    {
        mRain.Start();
    }
    
    private void _StopButtonClick(object sender, RoutedEventArgs e)
    {
        mRain.Stop();
    }
  4. 两个按钮:Set1Set2;控制文本颜色。
    private void _ChangeColorButtonClick(object sender, RoutedEventArgs e)
    {
        mRain.SetParameter(textBrush: ((Button)sender).Background);
    }

关注点

引用

这是你最后的机会。在此之后,就没有回头路了。你服下蓝色药丸——故事就结束了,你会在床上醒来,相信你愿意相信的一切。你服下红色药丸——你将留在仙境,我会让你看到兔子洞有多深。记住:我所提供的一切都是真相。仅此而已。

红色药丸是DrawingVisual,所以做出你的选择。

附注:我使用了一个自定义的“CryptoRandom”类(包含源代码)来生成随机字母,而不是标准的Random方法。这是因为Random方法生成的是“伪随机”数。如果您想深入了解,请访问此链接

如果您对修改有任何建议,请随时与我联系。

历史

  • 版本 1.1.0 - 2019年9月。改进
    • 应用程序自动在屏幕中心启动。
    • 最大化按钮现在显示应用程序全屏(按Esc键退出)。如果您为字母应用特殊的笔刷,系统可能会开始丢帧,因为我使用了RenderTargetBitmap,它在CPU上运行(在高分辨率下,这会降低性能)。
  • 版本 1.0.0 - 2019年8月 - 首次发布
© . All rights reserved.