C# - Non-blocking Per-Pixel Painting in Windows Forms

WinForms gives us the PictureBox control, into which we can easily load images of different formats, have them scaled for us, etc.
But, unfortunately, PictureBox gives us no obvious way to paint it ourself via some SetPixel method.
In this article i will thus show you how to implement your own SetPixel method. As you will know, when you directly execute a long-running function, the user interface will be unresponsive (you won't be able to move the window, or click anything on it) until the function has finished running. When painting something, your paint method might be very long running (for example if you do raytracing), so we will do our painting in a background thread. This will only add a few lines of code, but keep the application running smooth.
Also, since there are different ways to achieve our goals, i will split this article up into two parts, showing you different methods of how to achieve the same effect.

For the rest of this article, i assume that we have a Windows Forms project, and that the form contains a PictureBox named pictureBox1, and a Button (named button1), which will start our test-rendering.


Variant 1: Drawing each pixel directly

This variant is pretty slow, but it will immediately paint each pixel you set.

We begin with some data field declarations inside our main form:

        Bitmap bmp;
        Bitmap pixel;
        Graphics g;
        Thread t;
        delegate void PixelFunc(int x, int y, Color c);
and an Init function to clear and initialize those fields:
        private void Init ()
        {
            if (g!=null) g.Dispose();
            pictureBox1.Image = null;
            if (bmp != null) bmp.Dispose();
            bmp = new Bitmap(pictureBox1.Width, pictureBox1.Height);
            if (pixel != null) pixel.Dispose();
            pixel = new Bitmap(1, 1);
            pictureBox1.Image = bmp;
            g = pictureBox1.CreateGraphics();
        }
Bitmap is a class that comes with its own SetPixel method, which is what we will use. For this variant, we need two Bitmaps: the first, bmp, we use as the internal bitmap of our PictureBox. We create it with the same size as the picture box, and assign it to its Image property. The second Bitmap, pixel, is just a 1-pixel bitmap, which we will use to draw on the visible surface, which is represented by the Graphics object g, which we acquire from our PictureBox.
Now that we have our neccessary objects, we can write our SetPixel method:
        private void SetPixel(int x, int y, Color c)
        {
            lock (g)
            {
                pixel.SetPixel(0, 0, Color.Blue);
                g.DrawImageUnscaled(pixel, x, y);
                bmp.SetPixel(x, y, Color.Blue);
            }
        }
First we lock g. This is very important if we want to make our program multithreaded, because we will be invoking SetPixel asyncronously, which means the calling thread will not wait until SetPixel is finished before calling it again. To make sure that one SetPixel is only called once the previous call has completed, and no pixels are skipped, we have to lock. You could lock pixel, bmp, or some other object to achieve this, but you need to be aware that in some situations windows will use or query the PictureBox' Graphics object, for example when you drag the window to the edge of the screen, so that PictureBox leaves the screen. Such simultaneous accesses can lead to an exception, which we can easily prevent by locking around g.
Next, we set the single pixel of our Bitmap called 'pixel' to the color we supplied, and then we can draw this pixel to the visible surface of the PictureBox with a call to Graphics.DrawImageUnscaled. Finally, we draw our pixel to bmp. bmp is the 'data storage' of our PictureBox. When PictureBox needs to be refreshed, for example after another window was moved over it, it uses bmp to paint itself. This means that if we omit the call to bmp.SetPixel, our PictureBox would be empty after minimizing and restoring our application window, or after dragging another window over it.

Next in line is the Render function, which will draw some image using our SetPixel method:
        private void Render()
        {
            PixelFunc func = new PixelFunc(SetPixel);
            for (int x = 0; x < pictureBox1.Width; x++)
                for (int y = 0; y < pictureBox1.Height; y++)
                    try
                    {
                        pictureBox1.Invoke(func, x, y, Color.Blue);
                    }
                    catch (InvalidOperationException)
                    {
                        return;
                    }
        }
This method is pretty simple, just filling the PictureBox in blue. The important parts have to do with thread safety: first off, Render will be called from a background thread, but accessing Windows Forms controls from background threads can lead to errors or exceptions, so we have to call our SetPixel on the thread that pictureBox1 was created on. We do this by creating a delegate to our SetPixel method, and calling this delegate through pictureBox1.Invoke. PictureBox.Invoke can fail in the one case that neither PictureBox, nor its parents, have a valid window handle, which is the case when we close our application while Render() is running. To prevent an exception from popping up if this happens, we wrap the call to Invoke in a try/catch.

Here is the button click handler that starts our Render function in a new thread, aborting if it was already started, so we don't have multiple threads running when we click the button multiple times:
        private void button1_Click(object sender, EventArgs e)
        {
            Init();
            if ( t!= null && t.IsAlive) t.Abort();
            t = new Thread(new ThreadStart(Render));
            t.Start();
        }
And that was it. The only thing left is some cleanup code in Form.Closed:
        private void Form1_FormClosed(object sender, FormClosedEventArgs e)
        {
            g.Dispose();
            bmp.Dispose();
            pixel.Dispose();
        }
If you run this code and click the button, it will slowly fill the picture box in blue, and you can move the window around smoothly.

Variant 2: Drawing to a Memory Bitmap

Unfortunately, there is no really fast way of drawing pixels to the screen in WinForms. But drawing pixels to a bitmap that resides in memory is very fast, so we can speed up the process alot by rendering to a memory bitmap, and periodically drawing that bitmap to the screen with a Timer.
The following code will do the same as the code of variant 1, but much faster. The only drawback is that the on-screen image will be generated block by block on timer ticks, and not smoothly pixel-by-pixel.

We begin with our data fields and Init function:
        Bitmap bmp;
        Bitmap membmp;
        Graphics g;
        System.Windows.Forms.Timer UpdateTimer;
        delegate void PixelFunc(int x, int y, Color c);
 
        private void Init()
        {
            if (g != null) g.Dispose();
            pictureBox1.Image = null;
            if (bmp != null) bmp.Dispose();
            bmp = new Bitmap(pictureBox1.Width, pictureBox1.Height);
            if (membmp != null) membmp.Dispose();
            membmp = new Bitmap(pictureBox1.Width, pictureBox1.Height);
            pictureBox1.Image = bmp;
            g = Graphics.FromImage(bmp);
        }
Instead of a 1-pixel bitmap to copy to the visible screen, we now have a second Bitmap of the same size as the PictureBox. Also new is the Timer. And our Graphics object is not associated with the PictureBox anymore, but with the Bitmap 'bmp'.

SetPixel has become shorter than before:
        public void SetPixel(int x, int y, System.Drawing.Color col)
        {
            lock (membmp)
            {
                membmp.SetPixel(x, y, col);
            }
        }
since we now only need to set the pixel on the memory bitmap.
Render has become shorter too:
        private void Render(Object state)
        {
            PixelFunc func = new PixelFunc(SetPixel);
            for (int x = 0; x < pictureBox1.Width; x++)
                for (int y = 0; y < pictureBox1.Height; y++)
                    SetPixel(x, y, Color.Blue);
        }
since we no longer access a control in our SetPixel, we can call the method from our current thread and don't need to channel calls through Control.Invoke.
The parameter 'state' to Render is unused, and only there because this time i wanted to show you how to create the background thread on the ThreadPool, and the delegate we will use for this needs an Object parameter.

Here is our new button Click handler:
        private void button1_Click(object sender, EventArgs e)
        {
            Init();
 
            if (UpdateTimer == null)
            {
                UpdateTimer = new System.Windows.Forms.Timer();
                UpdateTimer.Interval = 100;
                UpdateTimer.Tick += new EventHandler(Timer_Tick);
                UpdateTimer.Start();
            }
            WaitCallback wc = new System.Threading.WaitCallback(Render);
            ThreadPool.QueueUserWorkItem(wc);
        }
This time, the handler first starts a new Timer, which will tick 10 times per second. After that, it starts our Render method in a background thread. This time, as mentioned, in the thread pool. Which method you use to start the thread is of course up to you.

The actual drawing to the screen now happens in the method that is called on each tick of the timer. The handler looks like this:
        private void Timer_Tick(Object sender, EventArgs e)
        {
            lock (membmp)
            {
                g.DrawImageUnscaled(membmp, 0, 0);
                pictureBox1.Refresh();
            }
        }
It's important that we lock around membmp, to make sure no SetPixel call draws to it while we are copying it to the PictureBox buffer. As mentioned before, this time g is not associated with pictureBox1 and draws to the screen, but draws to bmp. Finally, with a call to PictureBox.Refresh(), we let the picture box redraw itself with the contents of its buffer, which is bmp.
Note that Windows.Forms.Timer allows us inside its Tick event to access Windows Forms controls safely, without the need to execute our code through Control.Invoke as before.

Don't forget to release the resources in the end:
        private void Form1_FormClosed(object sender, FormClosedEventArgs e)
        {
            g.Dispose();
            bmp.Dispose();
            membmp.Dispose();
        }

No comments:

Post a Comment