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, Drawing Visual 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:
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:
A. 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 = "")
...
The
The animation is controlled through System.Timers.Timer. I prefer this solution over System.Windows.Threading.DispatcherTimer because the
Every tick, the method
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.
3. Draw the new frame.
Once the call from the timer is on the dispatcher thread, it performs two operations:
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
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.
To recap the method,
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:
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.
This project references
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 usedfontSize
: Dimension of the font usedbackgroundBrush
: Brush used for the backgroundtextBrush
: Brush used for the textcharacterToDisplay
: The character used for the rain will be randomly chosen from thisstring
2. 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()) {
//synchronize on main thread
System.Timers.ElapsedEventHandler dt = _DispatcherTimerTick;
Dispatcher.Invoke(dt,sender,e);
return;
}
....
}
3. Draw the new frame.
Once the call from the timer is on the dispatcher thread, it performs two operations:
- Design the new frame
_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.
...
//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);
}
...
To recap the method,
_RenderDrops()
generates DrawingVisual
that contains a background with opacity and the new drops letters.- Copy the new frame over the previous one
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:
Final Frame1 = Black background + Frame1 | Final Frame 2 = Final Frame1 + Frame2 |
Final Frame 3 = Final Frame2 + Frame3 | Final Frame 4 = Final Frame3 + Frame4 |
_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();
...
2. 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);
- Two buttons:
Start
andStop
; command the animation:private void _StartButtonClick(object sender, RoutedEventArgs e) { mRain.Start(); } private void _StopButtonClick(object sender, RoutedEventArgs e) { mRain.Stop(); }
- Two buttons:
Set1
andSet2
; command the text color:private void _ChangeColorButtonClick(object sender, RoutedEventArgs e) { mRain.SetParameter(textBrush: ((Button)sender).Background); }
No comments:
Post a Comment