Threading in Delphi for .NET - User Interface Issues
(Page 14 of 15 )
In order to optimize performance of the User Interface (UI), both WinForms and VCL applications have not implemented any thread-safety mechanisms. Any updates to a control must occur from the main user-interface thread.
For WinForm applications, most methods of the System.Windows.Forms.Control class are not thread-safe. There are, however, four methods that are safe to execute from any thread. These methods are identified in the following sections. Listing 14.14 contains the methods and properties of interest for this section.
Listing 14.14 Thread-safe System.Windows.Forms.Control Methods
System.Windows.Forms.Control = class (
System.ComponentModel.Component,
ISynchronizeInvoke, ...)
public
...
function Invoke(method: Delegate) : System.Object; overload;
function Invoke(method: Delegate;
args: array of System.Object) :
System.Object; overload;
function EndInvoke(asyncResult: IAsyncResult) :
System.Object;
function BeginInvoke(method: Delegate;
args: array of System.Object) :
IAsyncResult; overload;
function BeginInvoke(method: Delegate) : IAsyncResult;
overload;
property InvokeRequired: Boolean read;
end;
The System.Windows.Forms.Control.Invoke() Method
The most common way of ensuring that a control is updated from the user interface thread is to use one of the overloaded Invoke() methods. Invoke() requires a delegate and an optional array of arguments. When Invoke() is called, it guarantees that the delegate method is executed on the same thread that the control was created on. In addition, Invoke() blocks until the method is executed, thereby causing the worker thread to pause. (Listing 14.15 shows an example of calling the Invoke() method.)
Note: Never call Join() from the main thread that uses the Invoke() method to update the UI thread. This causes the application to hang because the main thread is waiting for the worker thread and the worker thread is waiting for the UI thread.
The System.Windows.Forms.Control.InvokeRequired Property
Suppose that a method will be shared between a user-interface thread and a worker thread. As previously discussed, the worker thread will need to use the Invoke() method to ensure that the update occurs on the proper thread. However, the user-interface thread does not need to use the Invoke() method. Fortunately, the InvokeRequired property provides the flexibility to know if the method can be called directly or if the Invoke() method must be used. Listing 14.15 shows how to use the InvokeRequired property.
Listing 14.15 Example of Invoke() and InvokeRequired
type
TMyUpdateDelegate = procedure of object;
procedure TWinForm.UpdateControls;
var
myDelegate: TMyUpdateDelegate;
tmpStr : string;
begin
if progBar.InvokeRequired then
begin
tmpStr := System.String.Format('{0} invoke required!',
System.Object(AppDomain.GetCurrentThreadID));
UpdateTextBox(System.Object(tmpStr));
myDelegate := @self.UpdateControls;
progBar.Invoke(System.Delegate(@myDelegate));
end
else
begin
progBar.Increment(1);
trackBar.Value := trackBar.Value + 1;
end;
end;
Find the code on the CD: \Code\Chapter 14\Ex08\.
Taken from the
ThreadingExamples project, the
UpdateControls() method is called from the main user-interface thread and other worker threads. If it is called from the worker thread, a delegate is used to call the same method on the main user-interface thread. Notice that two controls are being updated:
progBar and
trackBar. The only
InvokeRequired test is on the progress bar since both controls were created on the same thread.
The System.Windows.Forms.Control.BeginInvoke() Method
When it is not desirable to wait for the main thread to execute, use one of the overloaded BeginInvoke() methods. These methods do the same thing as Invoke() except that they execute the method asynchronously, without waiting. Make sure that the delegate method used is thread-safe. (Listing 14.16 shows an example of using BeginInvoke().)
The System.Windows.Forms.Control.EndInvoke() Method
Executing a method asynchronously is very powerful, especially if a return value is not needed. When a return value is needed from a method executed with BeginInvoke(), use the EndInvoke() method. EndInvoke() returns a System.Object, which represents the return value from the delegate executed asynchronously. Use EndInvoke() carefully because it will block if the method has not completed when EndInvoke() is called. Listing 14.16, which is also from the ThreadingExamples project, illustrates how to use the BeginInvoke() and EndInvoke() methods.
Listing 14.16 BeginInvoke()/EndInvoke() Example
procedure TWinForm.UpdateControlsNoWaiting;
var
myDelegate: TMyTextBoxDelegate;
tmpStr : string;
parms : array[0..0] of System.object;
lastAsyncInvoke: IAsyncResult;
begin
if textBox.InvokeRequired then
begin
tmpStr := System.String.Format(
'{0} Invoke required in UpdateControlsNoWaiting',
System.Object(AppDomain.GetCurrentThreadID));
myDelegate := @self.LongRunningMethod;
parms[0] := System.Object(tmpStr);
lastAsyncInvoke := textBox.BeginInvoke(
System.Delegate(@myDelegate), parms );
tmpStr := System.String.Format(
'{0} After BeginInvoke call in UpdateControlsNoWaiting',
System.Object(AppDomain.GetCurrentThreadID));
UpdateTextBox(System.Object(tmpStr));
// now let's wait for the async call to finish..
textBox.EndInvoke(lastAsyncInvoke);
tmpStr := System.String.Format(
'{0} After EndInvoke call in UpdateControlsNoWaiting',
System.Object(AppDomain.GetCurrentThreadID));
UpdateTextBox(System.Object(tmpStr));
end
else
UpdateTextBox(
'Invoke is not required in UpdateControlsNoWaiting');
end;
Note: Find the code on the CD: \Code\Chapter 14\Ex08\.
The System.Windows.Forms.Control.CreateGraphics() Method
Access to the GDI+ drawing subsystem is accomplished through the System.Drawing.Graphics class. An instance of this class is available by calling a control's CreateGraphics() method. The CreateGraphics() method is thread-safe, allowing multiple threads to update a GDI+ drawing surface without affecting each other. GDI+ resources are limited, so make sure to use the Dispose() method when finished with the Graphics instance.
One word of caution when using CreateGraphics(). Make sure that any drawing is done within a paint handler. Any updating of a control occurring outside of a paint handler will be erased when the next paint message is processed. Listing 14.17, also from the ThreadingExamples project, demonstrates how two threads can safely paint on a control at the same time. The painting does not occur within a paint handler, so the drawing is easily erased.
Listing 14.17 CreateGraphics() Example
1: procedure TWinForm.btnCreateGraphics_Click(sender: System.Object; e: System.EventArgs);
2: var
3: inst : array[0..1] of TMyGraphicsClass;
4: thrd : Thread;
5: i : integer;
6: begin
7: // this method creates a couple of TMyGraphicClass instances and executes
8: // the draw method on different threads
9: UpdateTextBox('Anything causing a repaint erase the graphics being currently drawn');
10: for i:=low(inst) to high(inst) do
11: begin
12: inst[i] := TMyGraphicsClass.Create;
13: inst[i].Control := Self.Panel1;
14: inst[i].DrawRectangle := (i = 0);
15:
16: thrd := Thread.Create(@inst[i].Draw);
17: thrd.Start;
18: end;
19: end;
20:
21: procedure TMyGraphicsClass.Draw;
22: var
23: gr : System.Drawing.Graphics;
24: clr : Color;
25: i : integer;
26: height : integer;
27: width : integer;
28: rnd : System.Random;
29: curX : integer;
30: curY : integer;
31: curW : integer;
32: curH : integer;
33: thrdID : System.Object;
34: tmpStr : string;
35: begin
36: // this method randomly draws either Red circles or Blue Rectangles
37: thrdID := System.Object(AppDomain.GetCurrentThreadId);
38: tmpStr := System.String.Format('{0} Graphics Class Thread Started',thrdId);
39: theWinFormInstance.UpdateTextBox(System.Object(tmpStr));
40:
41: try
42: clr := Color.Red;
43: if FDrawRectangle then
44: clr := Color.Blue;
45:
46: gr := FControl.CreateGraphics;
47: try
48: rnd := System.Random.Create;
49:
50: height := System.Convert.ToInt32(gr.VisibleClipBounds.Height);
51: width := System.Convert.ToInt32(gr.VisibleClipBounds.Width);
52:
53: for i:=1 to 10 do
54: begin
55: curX := rnd.Next(width);
56: curY := rnd.Next(height);
57: curW := rnd.Next(width div 3);
58: curH := rnd.Next(height div 3);
59:
60: if (curX + curW) > width then
61: curX := width - curW;
62:
63: if (curY + curH) > height then
64: curY := height - curH;
65:
66: if FDrawRectangle then
67: gr.FillRectangle(SolidBrush.Create(clr), curX, curY, curW, curH)
68: else
69: gr.FillEllipse(SolidBrush.Create(clr), curX, curY, curW, curH);
70:
71: Thread.Sleep(2000);
72: end;
73: finally
74: gr.Dispose;
75: end;
76: except
77: on e : exception do
78: begin
79: thrdID := System.Object(AppDomain.GetCurrentThreadId);
80: tmpStr := System.String.Format('{0} Graphics class thread Exception {1}', thrdID,
81: System.Object(e.message));
82: theWinFormInstance.UpdateTextBox(System.Object(tmpStr));
83: end;
84: end;
85:
86: thrdID := System.Object(AppDomain.GetCurrentThreadId);
87: tmpStr := System.String.Format('{0} Graphics Class Thread Finished',thrdId);
88: theWinFormInstance.UpdateTextBox(System.Object(tmpStr));
89: end;
Note: Find the code on the CD: \Code\Chapter 14\Ex08\.
Listing 14.17 creates two threads that draw either a rectangle or an ellipse. Before the Draw() method can paint on the control, it must first grab an instance to the Graphics class, which is illustrated on line 46. Once this instance is obtained, each thread is free to update the same control simultaneously.
This chapter is from Delphi for .NET Developer's Guide, by Xavier Pacheco (Sams, 2004, ISBN: 0-672-32443-1). Check it out at your favorite bookstore today.
Buy this book now. |
Next: Threading Exceptions >>
More .NET Articles
More By Xavier Pacheco