Click here to Skip to main content
15,867,453 members
Articles / Desktop Programming / WPF

Matrix-Style Rain in C# with WPF

Rate me:
Please Sign up or sign in to vote.
4.93/5 (62 votes)
18 Sep 2019CPOL6 min read 58.9K   3.3K   81   22
Matrix digital rain, Matrix code or sometimes green rain, is the computer code featured in the Matrix movies
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.

Image 1

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.

  1. Set up parameter.

    The SetParameter method allows to set up some animation parameter:

    C#
    ...
    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
  2. Start the animation.

    The Start and Stop methods allow to start and stop the animation:

    C#
    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.

    C#
    ...
    
    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:

    1. 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.

      C#
      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.

      Image 2

      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.

      C#
      ...
      //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. For example:

      Image 3
      Frame1

      Image 4
      Frame 2

      Image 5
      Frame 3
      Image 6
      Frame 4
    2. 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:

      Image 7
      Final Frame1 = Black background + Frame1

      Image 8
      Final Frame 2 = Final Frame1 + Frame2

      Image 9
      Final Frame 3 = Final Frame2 + Frame3
      Image 10
      Final Frame 4 = Final Frame3 + Frame4

      P.S.: I render the Drawing Visual on a RenderTargetBitmap. I could apply this directly on my image:

      C#
      _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.

      C#
      ...
      _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.

  1. In the MainWindow.xaml, a MatrixRain control is added to the window:
    XML
    ...
    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. During Initialization, I read a special font from the embedded resources and pass it to MatrixRain control:
    C#
    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.

  3. Two buttons: Start and Stop; command the animation:
    C#
    private void _StartButtonClick(object sender, RoutedEventArgs e)
    {
        mRain.Start();
    }
    
    private void _StopButtonClick(object sender, RoutedEventArgs e)
    {
        mRain.Stop();
    }
  4. Two buttons: Set1 and Set2; command the text color:
    C#
    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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Team Leader ATS med
Italy Italy
I started programming at Teinos, in a 5 person software working team in march 2004. I got some experience interfacing our main program to various external applications. All of these experiences allowed me to get in touch with many people: end-user, technician and commercial one; learning how to interface myself with these people. After some period I was moved to 'single' software application development. This led me to learn the whole process of software development. From the definition of the specifications to the software release and many times also the installation of the product to the end-user.

In 2009, I moved to ATS. I was charged as Lead Software Developer in a team for a long term new project: Primo (released for the first time in 2010-Q3). The software part for this project was completely written from scratch. These experience led me to understand how to organize the work of a team and take critical decision.

In 2014, in collaboration with a multinational corporation, we started the development of a completely new machine: ARCO FP (released for the first time in 2015-Q3). The software part for this project was completely written from scratch. This experience teaches me how to integrate our company with the needs and rules of other company and how to mitigate different approaches to the production phases.

Comments and Discussions

 
PraiseMy vote of 5 Pin
AchLog24-Sep-19 3:44
AchLog24-Sep-19 3:44 
GeneralRe: My vote of 5 Pin
Andreoli Carlo24-Sep-19 22:20
professionalAndreoli Carlo24-Sep-19 22:20 
QuestionThis project builds successfully and runs well. but.... Pin
Member 1373609922-Sep-19 20:10
Member 1373609922-Sep-19 20:10 
AnswerRe: This project builds successfully and runs well. but.... Pin
Andreoli Carlo23-Sep-19 2:59
professionalAndreoli Carlo23-Sep-19 2:59 
QuestionNot able to download Pin
chouser20-Sep-19 5:47
chouser20-Sep-19 5:47 
AnswerRe: Not able to download Pin
Andreoli Carlo21-Sep-19 11:13
professionalAndreoli Carlo21-Sep-19 11:13 
GeneralRe: Not able to download Pin
chouser21-Sep-19 11:54
chouser21-Sep-19 11:54 
QuestionMulti-screen support Pin
Jono7AC19-Sep-19 2:45
professionalJono7AC19-Sep-19 2:45 
Suggestionadapting content to resize Pin
claudetom0118-Sep-19 2:51
claudetom0118-Sep-19 2:51 
QuestionVersion 1.1 has error Pin
BMicka17-Sep-19 23:30
BMicka17-Sep-19 23:30 
AnswerRe: Version 1.1 has error Pin
Andreoli Carlo18-Sep-19 2:47
professionalAndreoli Carlo18-Sep-19 2:47 
QuestionCool, Let's Go Big... Pin
Bud Staniek8-Sep-19 11:36
Bud Staniek8-Sep-19 11:36 
QuestionFinally something useful! Pin
Arthur9000005-Sep-19 21:57
Arthur9000005-Sep-19 21:57 
GeneralMy vote of 5 Pin
BillWoodruff5-Sep-19 19:59
professionalBillWoodruff5-Sep-19 19:59 
GeneralRe: My vote of 5 Pin
Andreoli Carlo16-Sep-19 23:07
professionalAndreoli Carlo16-Sep-19 23:07 
QuestionThank You! Great Value-Added Feature For Products Pin
Hyland Computer Systems4-Sep-19 19:42
Hyland Computer Systems4-Sep-19 19:42 
QuestionI did a kind of flocking mouse avoidance thing while back which was fun Pin
Sacha Barber4-Sep-19 9:23
Sacha Barber4-Sep-19 9:23 
AnswerRe: I did a kind of flocking mouse avoidance thing while back which was fun Pin
Andreoli Carlo4-Sep-19 20:50
professionalAndreoli Carlo4-Sep-19 20:50 
QuestionTitle spelling error Pin
jWilliamDunn3-Sep-19 12:51
professionaljWilliamDunn3-Sep-19 12:51 
AnswerRe: Title spelling error Pin
Andreoli Carlo3-Sep-19 20:02
professionalAndreoli Carlo3-Sep-19 20:02 
QuestionAwesome fun Pin
GerVenson3-Sep-19 8:49
professionalGerVenson3-Sep-19 8:49 
SuggestionRe: Awesome fun Pin
foo21005-Sep-19 23:24
foo21005-Sep-19 23:24 
Use it as a queue. Make subscribers to OCR video stream and decode `matrix code` to read enterprise messeages that are sent via queue!

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.