Title: Use multiple threads to draw a graph in C#
This example shows how you can use multiple threads to let a program draw a graph and perform other tasks at the same time. A thread is a path of execution through a single process. Multi-threading allows an application to perform more than one task at the same time within the same process.
(Actually depending on the computer, only one thread or even one process may be able to run at the same time. In that case the operating system quickly switches back and forth between processes and threads to give the illusion of them all running at the same time. Let's just ignore that issue and pretend they really do execute simultaneously.)
Probably the trickiest things about threads is that only the user interface (UI) thread can directly interact with the user interface. For example, if you start a separate thread, it cannot directly change a label or progress bar to show its status.
To work around this issue, the thread should use the form's Invoke method to tell the form's UI thread make the change.
Because the thread's code is located in the same Form class that also runs the UI thread, you may need a way to determine whether the code is running in the UI thread or some other thread. The form's InvokeRequired property returns true if the code is not running in the UI thread, so it must use Invoke to manipulate the user interface. (In other words, InvokeRequired returns true if Invoke is required. Duh.)
A common pattern for interacting with the user interface is to put the interaction (setting a label's text or whatever) in a method. The method checks InvokeRequired to see if it must use Invoke. If InvokeRequired is true, the method uses the Invoke method to invoke itself on the UI thread. If InvokeRequired is false, the method directly modifies the user interface.
(If this seems confusing, that's because it is. After you see the code, read this part again and it'll probably make more sense.)
This example invokes to the UI thread in two ways: to plot a point and to display a status message.
When you click the program's Graph button, the following code executes.
private Thread GraphThread;
// Start drawing the graph.
private void btnGraph_Click(object sender, EventArgs e)
{
// Uncomment the following two lines
// to see what happens without threading.
//DrawGraph();
//return;
if (GraphThread == null)
{
// The thread isn't running. Start it.
AddStatus("Starting thread");
GraphThread = new Thread(DrawGraph);
GraphThread.Priority = ThreadPriority.BelowNormal;
GraphThread.IsBackground = true;
GraphThread.Start();
AddStatus("Thread started");
btnGraph.Text = "Stop";
}
else
{
// The thread is running. Stop it.
AddStatus("Stopping thread");
GraphThread.Abort();
GraphThread = null;
AddStatus("Thread stopped");
btnGraph.Text = "Start";
}
}
This code checks whether the graphing thread is running. If the thread is not running (it doesn't exist), the program creates the thread and starts it. It passes the Thread class's constructor the DrawGraph method. That is the method that the thread executes.
If the thread is already running, the program stops it. In either case, the code calls AddStatus to add a status message to the TextBox at the bottom of the form.
The following code shows the DrawGraph method that the thread executes.
// Draw a graph until stopped.
private void DrawGraph()
{
try
{
// Generate pseudo-random values.
int y = YValue;
for (; ; )
{
// Generate the next value.
NewValue();
// Plot the new value.
PlotValue(y, YValue);
y = YValue;
}
}
catch (Exception ex)
{
AddStatus("[Thread] " + ex.Message);
}
}
This code simply enters an infinite loop where it calls NewValue to generate a new data value and PlotValue to display the value.
The following code shows the NewValue method.
// Generate the next value.
private Random Rnd = new Random();
private void NewValue()
{
// Delay a bit before calculating the value.
DateTime stop_time = DateTime.Now.AddMilliseconds(20);
while (DateTime.Now < stop_time) { };
// Calculate the next value.
YValue += Rnd.Next(-4, 5);
if (YValue < 0) YValue = 0;
if (YValue >= picGraph.ClientSize.Height - 1)
YValue = picGraph.ClientSize.Height - 1;
}
The NewValue method pauses briefly so the display doesn't go too quickly. It then generates a random number and saves it in the form-level variable YValue.
The following code shows the PlotValue method.
// Define a delegate type that takes two int parameters.
private delegate void PlotValueDelegate(int old_y, int new_y);
// Plot a new value.
private void PlotValue(int old_y, int new_y)
{
// See if we're on the worker thread and thus
// need to invoke the main UI thread.
if (this.InvokeRequired)
{
// Make arguments for the delegate.
object[] args = new object[] { old_y, new_y };
// Make the delegate.
PlotValueDelegate plot_value_delegate = PlotValue;
// Invoke the delegate on the main UI thread.
this.Invoke(plot_value_delegate, args);
// We're done.
return;
}
// Invoke not required. Go ahead and plot.
// Make the Bitmap and Graphics objects.
int wid = picGraph.ClientSize.Width;
int hgt = picGraph.ClientSize.Height;
Bitmap bm = new Bitmap(wid, hgt);
Graphics gr = Graphics.FromImage(bm);
// Move the old data one pixel to the left.
gr.DrawImage(picGraph.Image, -1, 0);
// Erase the right edge and draw guide lines.
gr.DrawLine(Pens.Blue, wid - 1, 0, wid - 1, hgt - 1);
for (int i = Ymid;
i <= picGraph.ClientSize.Height;
i += GridStep)
{
gr.DrawLine(Pens.LightBlue, wid - 2, i, wid - 1, i);
}
for (int i = Ymid; i >= 0; i -= GridStep)
{
gr.DrawLine(Pens.LightBlue, wid - 2, i, wid - 1, i);
}
// Plot a new pixel.
gr.DrawLine(Pens.White, wid - 2, old_y, wid - 1, new_y);
// Display the result.
picGraph.Image = bm;
picGraph.Refresh();
gr.Dispose();
}
The code first defines a delegate type to represent the kind of method that the code may need to invoke. PlotValueDelegate is basically a data type that represents a void method that takes two integers as parameters.
The PlotValue method first checks InvokeRequired and either invokes itself on the UI thread or updates the graph directly.
If InvokeRequired is true, the code creates an argument array to pass values to the invoked method. It makes a variable of type PlotValueDelegate, sets it equal to the PlotValue method, and uses Invoke to call that method on the UI thread, passing it the argument array.
When the call to PlotValue comes through on the UI thread, InvokeRequired is false. In that case, the method builds a new bitmap and draws the graph on it, adding the new point. It then displays the result.
The following AddStatus method adds a string to the TextBox at the bottom of the program's form.
// Define a delegate type that takes a string parameter.
private delegate void AddStatusDelegate(string txt);
// Add a status string to txtStatus.
private void AddStatus(string txt)
{
// See if we're on the worker thread and thus
// need to invoke the main UI thread.
if (this.InvokeRequired)
{
// Make arguments for the delegate.
object[] args = new object[] { txt };
// Make the delegate.
AddStatusDelegate add_status_delegate = AddStatus;
// Invoke the delegate on the main UI thread.
this.Invoke(add_status_delegate, args);
// We're done.
return;
}
// No Invoke required. Just display the message.
txtStatus.AppendText("\r\n" + txt);
txtStatus.Select(txtStatus.Text.Length, 0);
txtStatus.ScrollToCaret();
}
This code follows the same steps as the PlotValue method:
- If InvokeRequired is true:
- Make an array of parameters.
- Create a delegate variable and initialize it to point to the AddStatus method.
- Invoke the delegate, passing it the parameter array.
- If InvokeRequired is false:
- Update the user interface, in this case adding the new text at the end of the TextBox.
This example also uses a Timer to display the current time every second. If program called DrawGraph directly instead of invoking it in a separate thread, then the user interface would never get time to process its messages so you wouldn't be able to move, resize, or close the form. The Timer also wouldn't fire. Uncomment the indicated lines in the button's Click event handler to see what happens in that case.
Download the example to experiment with it and to see additional details.
|