C# 和 WPF 中的矩阵风格雨






4.93/5 (62投票s)
Matrix数字雨、Matrix代码或有时称为绿色雨,是《黑客帝国》电影中出现的计算机代码
引言
如MSDN所述,DrawingVisual是一个轻量级的绘图类,用于渲染形状、图像或文本。此类被认为是轻量级的,因为它不提供布局、输入、焦点或事件处理,从而提高了其性能。
背景
在开始编码之前,我查阅了MSDN页面,以了解DrawingVisual对象和WPF图形渲染概述的基础知识。
我们在WPF中常用的许多元素/控件,例如Button
、ComboBox
、Shape
等,都具有这些特性:
- 可以由多个元素组成,每个组成元素都提供焦点方法、事件处理和许多功能,这使我们能够拥有大量的编程自由度,但如果只需要执行一些绘图操作,则会带来很多“开销”。
- 扩展了普通对象,这些对象并未针对特定目的进行优化,而是用于通用服务。
DrawingVisual
的目的是提出一种轻量级的对象绘图方法。
关于Matrix数字雨效果,我从CodePen获取了一些开发思路。CodePen是一个在线社区,用于测试和展示用户创建的HTML、CSS和JavaScript代码片段。
我假设读者了解WPF的Dispatcher。简而言之,当您执行一个WPF应用程序时,它会自动创建一个新的Dispatcher
对象并调用其Run
方法。所有视觉元素都将由Dispatcher线程创建,并且对视觉元素的所有修改都必须在Dispatcher线程上执行。
Using the Code
我的示例包含两个项目
1. MatrixRain
这是解决方案的核心。该项目实现了一个模拟Matrix数字雨效果的UserControl
。该UserControl
可以用于任何Window/Page等。
- 设置参数。
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
中选择要显示的字符
- 开始动画。
Start
和Stop
方法允许启动和停止动画。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; } .... }
- 绘制新帧。
一旦来自计时器的调用到达Dispatcher线程,它会执行两个操作:
- 设计新帧
该帧由
_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 - 将新帧复制到前一帧之上
如前所述,新帧只生成“新”字母,但我们如何让之前的字母逐渐消失?
这是通过帧的背景实现的,该背景是黑色且具有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
用户控件的潜力。代码没有注释,因为它非常简单,不需要注释。
- 在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"/> ...
- 在初始化期间,我从嵌入式资源中读取了一个特殊字体,并将其传递给
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。仅供个人用途免费使用。
- 两个按钮:
Start
和Stop
;控制动画。private void _StartButtonClick(object sender, RoutedEventArgs e) { mRain.Start(); } private void _StopButtonClick(object sender, RoutedEventArgs e) { mRain.Stop(); }
- 两个按钮:
Set1
和Set2
;控制文本颜色。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月 - 首次发布