This article is intended for those people who want to understand how DrawingVisual works in WPF. I assume the reader knows WPF dispatcher, and provide a sample made up of two projects that I run through step by step.
Introduction
As described by MSDN, DrawingVisual is a lightweight drawing class that is used to render shapes, images, or text. This class is considered lightweight because it does not provide layout, input, focus, or event handling, which improves its performance.
Background
Before I started coding, I consulted MSDN page to understand the basic of DrawingVisual Objects and WPF Graphics Rendering Overview.
Many of the elements/controls that we commonly use in WPF like Button
, ComboBox
, Shape
, and others have these characteristics:
- Can be composed by multiple elements, each of the composing elements provide focus method, event handling and many features which allow us to have a lot of freedom of programming but with a lot of "overhead" if we just need to perform some drawing.
- Extend common objects which are not optimized for a specific purpose but for generic service.
The scope of DrawingVisual
is to propose a lightweight approach to object drawing.
Regarding the matrix rain effect, I take some ideas on how to develop it from CodePen, which is an online community for testing and showcasing user-created HTML, CSS and JavaScript code snippets.
I assume the reader knows WPF dispatcher. Briefly, when you execute a WPF application, it automatically creates a new Dispatcher
object and calls its Run
method. All the visual elements will be created by the dispatcher thread and all the modification to visual elements must be executed on Dispatcher thread.
Using the Code
My sample is made up of two projects:
1. MatrixRain
This is the core of the solution. This project implements a UserControl
that simulates the Matrix digital rain effect. The UserControl
can be used in any Window/Page, etc.
- Set up parameter.
The SetParameter
method allows to set up some animation parameter:
...
public void SetParameter(int framePerSecond = 0, FontFamily fontFamily = null,
int fontSize = 0, Brush backgroundBrush = null,
Brush textBrush = null, String characterToDisplay = "")
...
framePerSecond
: Frame per second refresh (this parameter affect the "speed" of the rain) fontFamily
: Font family used fontSize
: Dimension of the font used backgroundBrush
: Brush used for the background textBrush
: Brush used for the text characterToDisplay
: The character used for the rain will be randomly chosen from this string
- Start the animation.
The Start
and Stop
methods allow to start and stop the animation:
public void Start() {
_DispatcherTimer.Start();
}
public void Stop() {
_DispatcherTimer.Stop();
}
...
The animation is controlled through System.Timers.Timer. I prefer this solution over System.Windows.Threading.DispatcherTimer because the DispatcherTimer
is re-evaluated at the top of every Dispatcher loop and the timer is not guaranteed to execute exactly when the time interval occurs.
Every tick, the method _DispatcherTimerTick(object sender, EventArgs e)
is called.
This method is not executed on the Dispatcher thread so the first thing is to sync the call on the Dispatcher thread because we need to work with some resources accessible only by the main thread.
...
private void _DispatcherTimerTick(object sender, EventArgs e)
{
if (!Dispatcher.CheckAccess()) {
System.Timers.ElapsedEventHandler dt = _DispatcherTimerTick;
Dispatcher.Invoke(dt,sender,e);
return;
}
....
}
- Draw the new frame.
Once the call from the timer is on the dispatcher thread, it performs two operations:
- Design the new frame
The frame is created by the method _RenderDrops()
. Here is a new DrawingVisual
and its DrawingContext are created to draw objects. The drawing context allows drawing line, ellipse, geometry, images and many more.
DrawingVisual drawingVisual = new DrawingVisual();
DrawingContext drawingContext = drawingVisual.RenderOpen();
First, the method creates a black background with a 10% of opacity (I will explain later why I put 10% opacity).
After this, we scroll through an array called _Drops
.
This array represents the column along which the letters are drawn (see the red column in the image). The value of the array represents the row (see the blue circle in the image) where a new letter must be drawn. When the value of the drop reaches the 'bottom' of the image, the drop re-starts from the top immediately or randomly after a series of cycle.
...
for (var i = 0; i < _Drops.Length; i++) {
double x = _BaselineOrigin.X + _LetterAdvanceWidth * i;
double y = _BaselineOrigin.Y + _LetterAdvanceHeight * _Drops[i];
if (y + _LetterAdvanceHeight < _CanvasRect.Height) {
var glyphIndex = _GlyphTypeface.CharacterToGlyphMap[_AvaiableLetterChars[
_CryptoRandom.Next(0, _AvaiableLetterChars.Length - 1)]];
glyphIndices.Add(glyphIndex);
advancedWidths.Add(0);
glyphOffsets.Add(new Point(x, -y));
}
if (_Drops[i] * _LetterAdvanceHeight > _CanvasRect.Height &&
_CryptoRandom.NextDouble() > 0.775) {
_Drops[i] = 0;
}
_Drops[i]++;
}
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);
}
...
To recap the method, _RenderDrops()
generates DrawingVisual
that contains a background with opacity and the new drops letters. For example:
- Copy the new frame over the previous one
As seen before, the new frame only generates the "new" letter, but how can we fade away the previous letters?
This is performed by the background of the frame which is black with 10% opacity. When we copy a new frame over the previous frame, the blending makes the trick. The "copy over" weakens the previous letters luminance as shown in this example:
P.S.: I render the Drawing Visual on a RenderTargetBitmap. I could apply this directly on my image:
_MyImage.Source = _RenderTargetBitmap
The problem with this solution is that at every cycle, this operation allocates a lot of memory at every cycle. To overlap this problem, I use WriteableBitmap which is allocated in memory only once in the initialization code.
...
_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
This project references MatrixRain
and showcases the potentiality of MatrixRain
user control. The code is not commented, because it is so simple that does not need to be.
- In the MainWindow.xaml, a
MatrixRain
control is added to the window:
...
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"/>
...
- During Initialization, I read a special font from the embedded resources and pass it to
MatrixRain
control:
FontFamily rfam = new FontFamily(new Uri("pack://application:,,,"),
"./font/#Matrix Code NFI");
mRain.SetParameter(fontFamily: rfam);
Please pay attention to the font. This is the link where I found it: https://www.1001fonts.com/matrix-code-nfi-font.html. This is free to use only for personal purposes.
- Two buttons:
Start
and Stop
; command the animation:
private void _StartButtonClick(object sender, RoutedEventArgs e)
{
mRain.Start();
}
private void _StopButtonClick(object sender, RoutedEventArgs e)
{
mRain.Stop();
}
- Two buttons:
Set1
and Set2
; command the text color:
private void _ChangeColorButtonClick(object sender, RoutedEventArgs e)
{
mRain.SetParameter(textBrush: ((Button)sender).Background);
}
Points of Interest
Quote:
This is your last chance. After this, there is no turning back. You take the blue pill - the story ends, you wake up in your bed and believe whatever you want to believe. You take the red pill - you stay in Wonderland, and I show you how deep the rabbit hole goes. Remember: all I'm offering is the truth. Nothing more.
The red pill is the DrawingVisual
, so make your choice.
P.S.: To generate the letter random, I used a personal "CryptoRandom
" class (source included) instead of the canonical Random
method. This because Random
method generates a 'pseudo random' number. Follow this link if you want to dig in.
If you have any suggestions for modifications, please don't hesitate to contact me.
History
- Version 1.1.0 - September 2019. Improvements:
- The application automatically starts at the center of the screen.
- The maximize button now shows the application fullscreen (Esc button to exit). If you apply special Brush for the letters, the system can start to drop frames because I'm using RenderTargetBitmap which operates in CPU (with big resolution, this can slow down the performance).
- Version 1.0.0 - August 2019 - First release