Interactive GUI should update around 60 times per second (once every 16 ms), but what happens if your graphics system takes longer than this to render an image? If you render graphics in the GUI thread, your GUI will become unresponsive. This project explores how to render graphics outside the GUI thread to keep the GUI responsive while CPU-intensive rendering happens in another thread. We use a Windows Forms example to demonstrate this concept, but the same code can be used in WPF applications.
Our application will achieve threaded rendering by creating a static RenderForever()
which runs outside the main GUI thread and continuously updates a Bitmap
that contains the most recent render. A Timer
in our Form will trigger the GUI thread to copy the latest rendered Bitmap
and display it in a Picturebox.

The result is a GUI which remains responsive even if the rendering system becomes extremely slow. Notice how the trackbar and the alpha label update smoothly even through the graphics only renders at a rate of a couple frames per second.

This method also improves the reverse case, allowing high-speed graphics model updates and rendering which is not slowed down by the GUI. In cases where the rendering system can run exceedingly fast, the graphics model can be throttled with a FPS limiter.
Code
The overall strategy here is to spin-off a renderer thread which will continuously work to draw on a rendering Bitmap (bmpLive
), then copy the output to a display Bitmap (bmpLast
) every time a render completes.
We use the lock
statement to ensure the Bitmap being written in one thread does not get simultaneously read by another thread. Notice that we lock the display Bitmap and not the rendering Bitmap to avoid ever having our GUI to wait on the renderer.
Use a Trackbar to Adjust Alpha
- Threads can only interact with
static
properties, sostarColor
will be defined as a class-level property rather than be determined inside the render method.
static Color starColor = Color.White;
private void trackBar1_Scroll(object sender, EventArgs e)
{
label1.Text = $"{trackBar1.Value}%";
byte alpha = (byte)(trackBar1.Value * 255 / 100);
starColor = Color.FromArgb(alpha, Color.White);
}
Startup
-
Threads can only interact with
static
properties, sostatic Bitmap
objects will be used to store active (bmpLive
) and display (bmpLast
) images. Same goes for thefield
. -
You can’t lock
null
objects, so bothBitmap
objects are populated with the program starts. -
The renderer thread will get started as soon as the program starts.
static readonly Field field = new Field(500);
static Bitmap bmpLive;
static Bitmap bmpLast;
public Form1()
{
InitializeComponent();
bmpLive = new Bitmap(pictureBox1.Width, pictureBox1.Height);
bmpLast = (Bitmap)bmpLive.Clone();
var renderThread = new Thread(new ThreadStart(RenderForever));
renderThread.Start();
}
Threaded Renderer
The renderer thread runs forever and performs these tasks:
- advances the graphics model
- draws on the render Bitmap
- locks the display Bitmap as it copies the render Bitmap onto it
- performs FPS rate-limiting* (optional)
*FPS rate limiting prevents the renderer from advancing the model and creating Bitmaps too frequently. If the renderer thread can render 1000 FPS but your GUI only displays 60 FPS, the FPS limiter can be used to prevent unnecessary renders.
💡 We use the
lock
statement to ensure the render thread doesn’t write to the Bitmap at the same time as the GUI thread is reading from it.
private static void RenderForever()
{
double maxFPS = 100;
double minFramePeriodMsec = 1000.0 / maxFPS;
Stopwatch stopwatch = Stopwatch.StartNew();
while (true)
{
// advance the model
field.Advance();
// Render on the "live" Bitmap
field.Render(bmpLive, starColor);
// Lock and update the "display" Bitmap
lock (bmpLast)
{
bmpLast.Dispose();
bmpLast = (Bitmap)bmpLive.Clone();
}
// FPS limiter
double msToWait = minFramePeriodMsec - stopwatch.ElapsedMilliseconds;
if (msToWait > 0)
Thread.Sleep((int)msToWait);
stopwatch.Restart();
}
}
Update the GUI with a Timer
⚠️ You must
Clone()
images you display in a Picturebox: The Picturebox class natively supports double buffering. This is excellent for preventing flicker, but it also means you can’t control when the image is painted (and its data read). To ensure a paint doesn’t occur on a Bitmap locked by another thread, clone theBitmap
as you assign it to the Picturebox. Be sure to properly dispose of the oldImage
which may already be assigned to it.
private void timer1_Tick(object sender, EventArgs e)
{
lock (bmpLast)
{
pictureBox1.Image?.Dispose();
pictureBox1.Image = (Bitmap)bmpLast.Clone();
}
}
Consider also Threading your Graphics Model
If your model advancement takes a lot of time, consider placing it in its own thread as well. This way your model, rendering system, and GUI can all be on separate threads.
Source Code
- GitHub: Form1.cs