A C# Oscilloscope Display In Windows Forms
Written by Mike James   
Thursday, 04 June 2015
Article Index
A C# Oscilloscope Display In Windows Forms
ScrollWindowEx

If you need a real time stripchart or oscilloscope style display for a Windows forms project then the good news is that it can be done without having to move outside of C#.

 

It's a simple enough idea - create a display something like a stripchart recorder or an oscilloscope display that works fast enough to be used for real time applications, preferably using nothing but .NET.

This may sound like an easy idea but there are lots of ways in which it can go wrong when speed is at issue. Let's take a look at two ways of doing the job - the first using the GDI+ and the second using the Windows API and for a project that makes use of the idea see Looking at Chaos.

 

sine

The finished sine wave display - it scrolls to the right.

Shifting A Bitmap

The simplest idea is to use a picturebox with a Bitmap object to implement graphic persistence.

The Bitmap can be moved in any direction by simply re-drawing it slightly displaced. The problem is that for the shift to be permanent, so that subsequent shifts are cumulative, you have to replace the bitmap with the shifted version and this means the creation of a lot of temporary objects and a lot of juggling.

For any of this to make sense you have to keep in mind that a Bitmap object is a wrapper for a deeper API bitmap and there are various layers of resources used in its construction. You also need to keep in mind the fact that the variables that you declare are references to the bitmaps and not the bitmaps themselves - this will become clear as the program is explained.

Start a new C# Windows forms project and place a button and a picturebox on the form. We could create a new control descended from the picturebox or a class from Bitmap but it's easier and just as good in this case to use extension methods to add a new Scroll method to the Bitmap class.

To add an extension method to an existing class all we need is to add a static class to the project. An extension method for MyClass then takes the form:

MyExtension(this MyClass, MyParametersList){};

Notice that you need to start the parameter list with "this" and the name of the class this is how the compiler links up the extension to the class it extends. It also has to be method of a static public class. 

To add a Scroll method to the Bitmap class all we need is a static class:

public static class MyExtensions
{

and a static method with the appropriate signature:

public static Bitmap Scroll(
                    this Bitmap BM,
                    int dx, int dy)
{

dx and dy are the amounts the bitmap will be shifted and the this Bitmap indicates the class that the method is added to.

Next we need a temporary bitmap object to hold the shifted bitmap. We create a Graphics object from the bitmap and draw the existing bitmap on with a shift:

 Bitmap BMTemp = new Bitmap(
                    BM.Width, BM.Height);
 Graphics G = Graphics.FromImage(BMTemp);
 G.DrawImage(BM, dx, dy);
 G.Dispose();
 return BMTemp;
}

By returning the temporary bitmap the shift is completed.

Notice that you can't draw a bitmap onto itself - the GDI+ doesn't like it and so you do need the temporary bitmap. Also notice that you do need the Dispose call to tidy up after the Graphics object - without it you will eventually run out of memory as the garbage collection system fails to deal with the resources created.

To try the Scroll method out we first create a bitmap for the Image of the picturebox to make the graphics persist:

private void button1_Click(
                object sender, EventArgs e)
{
 Bitmap BM=new Bitmap(
 pictureBox1.ClientSize.Width,
 pictureBox1.ClientSize.Height);
 pictureBox1.Image = BM;

Next we need something to plot and what could be more obvious than a sine wave:

for (int x = 0; x < 10000; x++)
{
 int y = (int)(Math.Sin((double)x / 50) *
               BM.Height / 2 + BM.Height / 2);
 BM.SetPixel(0, y,  Color.Black);

Don't worry about the details of how y is calculated - all that matters is to produce a reasonable value of y and a nice display. The important point is that we have set the pixel at (0,y) in the bitmap.

If you refresh the picturebox you would see a single point. And if the loop was repeated that point would move up and down as y varied to product a vertical line - but we intend to scroll the bitmap to move the plotted point to the left by one pixel:

BM = BM.Scroll(1, 0);

Notice this is a sneaky way of drawing to the same bitmap as BM is set to reference the returned bitmap.

Notice also that as BM is a reference to the bitmap we now have BM referencing the bitmap returned by the method and the picturebox referencing the original unshifted bitmap. To make them reference the same bitmap once again we need:

pictureBox1.Image = BM;

We also need an:

 Application.DoEvents();
}

Without a DoEvents you will discover that the application pauses for quite a while when the for loop completes as it processes the entire message queue that has been blocked for a long period. I discovered this by experimentation and at first I thought it was the garbage collection system causing the problem but no…

Of course to a certain extend the need for a DoEvents is created by the slightly artificial use of a for loop to generate the data. In a more realistic setting you would either use another thread or a timer to generate the data and then the main application thread wouldn't be blocked in this way.

 

sin2

 

If you try this out you will discover that it works as advertised but it is sometimes a little on the slow side.

You might think that it is something to do with creating and disposing of so many temporary objects but even a version that keeps the number of Graphics objects and Bitmaps down to two of each runs almost as slowly.

The full listing is:

namespace stripchart1
{
 public partial class Form1 : Form
 {
  public Form1()
  {
   InitializeComponent();
  }
  private void button1_Click(object sender,
                                EventArgs e)
  {
   Bitmap BM = new Bitmap(
   pictureBox1.ClientSize.Width,
   pictureBox1.ClientSize.Height);
   pictureBox1.Image = BM;
   for (int x = 0; x < 10000; x++)
   {
    int y = (int)(Math.Sin((double)x / 50) *
              BM.Height / 2 + BM.Height / 2);
    BM.SetPixel(0, y, Color.Black);
    BM = BM.Scroll(1, 0);
    pictureBox1.Image = BM;
    Application.DoEvents();
   }
  }
 }
 public static class MyExtensions
 {
  public static Bitmap Scroll(this Bitmap BM,
                               int dx, int dy)
  {
   Bitmap BMTemp = new Bitmap(
                      BM.Width, BM.Height);
   Graphics G = Graphics.FromImage(BMTemp);
   G.DrawImage(BM, dx, dy);
   G.Dispose();
   return BMTemp;
  }
 }
}

 

 

<ASIN:097937250X>
<ASIN:0321160770>

<ASIN:1590594452>



Last Updated ( Thursday, 04 June 2015 )