Can a separate thread own a form control?

S

stumorgan

Basically what I have is a form with a graph on it which graphs data
that I'm reading from a USB device at 100 Hz (every 10ms). I have a
thread reading and parsing the data from the USB, but when it comes
time to draw that data on the graph the work is handled on the main UI
thread through a callback delegate since the form owns the graph
control. Is there a way I can have a separate thread own the graph
control and handle the drawing so that the rest of the UI is not
affected?

Thanks in advance,
Stu
 
S

stumorgan

What do you mean by "not affected"?  In what way is "the rest of the UI"  
being affected by your graphing that you believe would be addressed by  
moving the graphing to a different thread?

The short answer is: yes, you can create your graphing control on a  
different thread, and as long as you provide a message pump on that  
thread, it should work fine.

But I fail to see the advantage in doing so.  It would be helpful if you  
could be more specific about what problem it is you perceive and which you  
want to solve.

Pete

The redraw of the graph at 100 Hz is relatively slow and is
interfering with, for instance, clicking a button on the form or the
updating of textboxes. Doing this work on a separate thread should
help that, no? Also, in order to create the graphing control on the
thread does that mean that I have to instantiate set all of the
properties at run-time, or is there a way for me to still use the
designer?
 
S

stumorgan

I don't understand that statement.  Or, rather...it only makes sense tome  
if I assume that you have somehow blocked the main GUI thread with your  
data/redraw loop and are only processing _all_ events every 10ms.

Not that a 10ms delay is likely to be noticed by most users, but assuming 
you've designed your code correctly, the updating of the graph should not 
in any way interfere with the rest of the UI.  If it does, then you've got  
a mistake somewhere.


I suppose that depends on what the actual problem is.  If you've designed  
your code wrong and are blocking the main GUI thread for 10 ms at a time, 
then sure...you might be able to address that by moving the graph control 
to another thread.  But it would be better to just fix the design rather  
than apply another hack.

Without a concise-but-complete code sample demonstrating your problem,  
it's not possible to say for sure whether your code is wrong or not.  But  
here's a clue: if you are calling Application.DoEvents(),  
Control.Refresh(), or Control.Update(), your code is almost certainly  
wrong.


That depends on how you're using the Designer now.  The VS Designer hasno  
idea what thread you're going to create a control on.  Where you  
instantiate the control should have no bearing on how you design the  
control itself.  But if you're relying on the Designer to put the control  
into another control (e.g. a Form sub-class), and thus the  
Designer-generated code is currently what's instantiating the graph  
control then, yes...you'll have to take over that part of the process and 
that may mean writing all of the initialization explicitly rather than  
relying on the Designer.

You could address that issue by designing a UserControl that contains your  
graph control, and then instantiating the UserControl on the other  
thread.  In the Designer then you'd be able to set properties for the  
graph control as you did with the Form.  But again, that's just applying  
yet another hack to get around what is very likely a fundamental flaw in  
your basic design.

It would be better to identify the underlying cause here and address that 
rather than trying to move the control to a different thread.

Pete

I'm not making the main GUI thread sleep nor am I doing an
Application.DoEvents(). In the thread where I handle the data
parsing, I then make a call to the function which adds the data points
to the graph (which happens on the main GUI thread through the
delegate callback), then I do a Thread.Sleep(10) at the end of the
loop to tell that thread to sleep for 10ms. I have also added
textboxes on the form which I am now updating from the same thread
through a callback to the main UI thread, and now that I'm doing that
everything seems to be working fine. When I take out the call to the
function which updates the textboxes, everything is slow again (for
instance mousing over a button does not give it the highlighted
outline anywhere near as quick as it should). I'm really not sure
why... Perhaps it's something to do with the graph control that I'm
using (Dundas Charts)? Thanks for your help by the way.

-Stu
 
S

stumorgan

[...]
I'm not making the main GUI thread sleep nor am I doing an
Application.DoEvents().

Then it's difficult to understand your question.
In the thread where I handle the data
parsing, I then make a call to the function which adds the data points
to the graph (which happens on the main GUI thread through the
delegate callback), then I do a Thread.Sleep(10) at the end of the
loop to tell that thread to sleep for 10ms.

You should be aware that Thread.Sleep() does not have enough precision for  
that to be a reliable way to ensure that a thread actually sleeps for  
10ms.  Due to the way the Windows thread scheduler works, a thread that 
calls Thread.Sleep(10) may not be run again for much longer (depending on 
what's going on in the system, 50ms, 100ms, or longer).
I have also added
textboxes on the form which I am now updating from the same thread
through a callback to the main UI thread, and now that I'm doing that
everything seems to be working fine.

As I mentioned, without a concise-but-complete code sample that reliably  
demonstrates the problem, it's not possible to comment with any real  
reliability.  Both because there's no way to know what your code is doing,  
and because there's no way to know for sure what the actual symptoms are. 
Likewise, it's impossible to say why adding text boxes would make your  
problem go away.

As long as we are relying on plain English descriptions of everything, you  
are not likely to get very useful answers.  It's just not possible.
When I take out the call to the
function which updates the textboxes, everything is slow again (for
instance mousing over a button does not give it the highlighted
outline anywhere near as quick as it should).  I'm really not sure
why...  Perhaps it's something to do with the graph control that I'm
using (Dundas Charts)?  Thanks for your help by the way.

You're welcome.  It could very well be some side-effect caused by the  
graph control you're using.  But again, impossible to say without seeing a  
concise-but-complete code sample.  A simple graph control could be written  
in quick order -- maybe 30 minutes or so -- and you could try replacing  
the third-party control with such a test control, to see if that makes the  
problem go away.

If it does, then you have a reliable indication that you should be taking 
this up with the author's of that third-party control.  If it doesn't,  
then you have taken one solid step toward having a suitable  
concise-but-complete code sample that you can post here for others to look  
at.  :)

Pete

This is obviously a trimmed down version of the whole thing, but
should show you what I'm doing. ProcessUsbData is the thread which
reads in the data and calls the functions to update the graph and
textboxes. If I take out the UpdateO2Textbox(O2) and UpdateCO2Textbox
(CO2) calls then the symptoms I'm seeing crop up. By the way, please
feel free to let me know if any of my code sucks and could be done in
a more efficient manner. For instance, if I'm supposed to be reading
from a USB once every 10 seconds is there a better way to do it than
doing Thread.Sleep(10) and then checking the inbound buffer? Thanks
again!

public partial class frmSampleForm : Form
{
private bool bAbortRead;

Thread BufferReader;
delegate void AddDataPointCallback(string series, decimal x,
decimal y);
delegate void UpdateO2TextboxCallback(decimal pValue);
delegate void UpdateCO2TextboxCallback(decimal pValue);

public frmTesting_VerifySetup(ref cExercise_Test pTest)
{
InitializeComponent();

ReadUSB();
}

private void ReadUSB()
{
// Start the thread which reads the inbound buffer
bAbortRead = false;
BufferReader = new Thread(new ThreadStart(ProcessUsbData));
BufferReader.Start();
}

private void ProcessUsbData()
{
string[] data;
decimal O2;
decimal CO2;
TimeSpan ts;
DateTime startTime = DateTime.Now;

while(!bAbortRead)
{
data = device.ReadLine().Split(new char[]{','});

if(data.Length > O2INDEX)
decimal.TryParse(data[O2INDEX], out O2);
else
O2 = 0;

if(data.Length > CO2INDEX)
decimal.TryParse(data[CO2INDEX], out CO2);
else
CO2 = 0;

ts = DateTime.Now - startTime;
if(O2 > 0 && O2 < 100)
{
AddDataPoint("O2", (decimal)(ts.TotalSeconds),
O2);
UpdateO2Textbox(O2);
}
if(CO2 > 0 && CO2 < 100)
{
AddDataPoint("CO2", (decimal)(ts.TotalSeconds),
CO2);
UpdateCO2Textbox(CO2);
}

Thread.Sleep(10);
}
}

private void AddDataPoint(string series, decimal x, decimal y)
{
// InvokeRequired required compares the thread ID of the
// calling thread to the thread ID of the creating thread.
// If these threads are different, it returns true.
if(chart1.InvokeRequired)
{
AddDataPointCallback d = new AddDataPointCallback
(AddDataPoint);
Invoke(d, new object[] { series, x, y });
}
else
{
chart1.Series[series].Points.AddXY(x, y);
chart1.Invalidate();
}
}

private void UpdateO2Textbox(decimal pValue)
{
// InvokeRequired required compares the thread ID of the
// calling thread to the thread ID of the creating thread.
// If these threads are different, it returns true.
if(txtO2.InvokeRequired)
{
UpdateO2TextboxCallback d = new UpdateO2TextboxCallback
(UpdateO2Textbox);
Invoke(d, new object[] { pValue });
}
else
{
txtO2.Text = pValue.ToString("0.00");
}
}

private void UpdateCO2Textbox(decimal pValue)
{
// InvokeRequired required compares the thread ID of the
// calling thread to the thread ID of the creating thread.
// If these threads are different, it returns true.
if(txtCO2.InvokeRequired)
{
UpdateCO2TextboxCallback d = new
UpdateCO2TextboxCallback(UpdateCO2Textbox);
Invoke(d, new object[] { pValue });
}
else
{
txtCO2.Text = pValue.ToString("0.00");
}
}

}
 
B

Ben Voigt [C++ MVP]

This is obviously a trimmed down version of the whole thing, but
should show you what I'm doing. ProcessUsbData is the thread which
reads in the data and calls the functions to update the graph and
textboxes. If I take out the UpdateO2Textbox(O2) and UpdateCO2Textbox
(CO2) calls then the symptoms I'm seeing crop up. By the way, please
feel free to let me know if any of my code sucks and could be done in
a more efficient manner. For instance, if I'm supposed to be reading
from a USB once every 10 seconds is there a better way to do it than
doing Thread.Sleep(10) and then checking the inbound buffer? Thanks
again!

You don't need Thread.Sleep, because ReadLine should efficiently wait until
data is available.

bAbortRead should be volatile, because it changes from a different thread
than is reading it, with no synchronization.

You're making four cross-thread calls every time which is hurting
performance. And you should use MethodInvoker for maximum performance. Try
this:

volatile bool bAbortRead; // bool fields are initialized to false
by the compiler
private void ProcessUsbData()
{
string[] data;
DateTime startTime = DateTime.Now;

while(!bAbortRead)
{
data = device.ReadLine().Split(new char[]{','});

decimal ts = (DateTime.Now - startTime).TotalSeconds;

decimal O2 = 0;
if(data.Length > O2INDEX)
decimal.TryParse(data[O2INDEX], out O2);

decimal CO2 = 0;
if(data.Length > CO2INDEX)
decimal.TryParse(data[CO2INDEX], out CO2);

MethodInvoker updateGUI = delegate {
if(O2 > 0 && O2 < 100)
{
AddDataPoint("O2", ts, O2);
UpdateO2Textbox(O2);
}
if(CO2 > 0 && CO2 < 100)
{
AddDataPoint("CO2", ts, CO2);
UpdateCO2Textbox(CO2);
}
};

if (InvokeRequired)
Invoke(updateGUI);
else
updateGUI();
}
}

public partial class frmSampleForm : Form
{
private bool bAbortRead;

Thread BufferReader;
delegate void AddDataPointCallback(string series, decimal x,
decimal y);
delegate void UpdateO2TextboxCallback(decimal pValue);
delegate void UpdateCO2TextboxCallback(decimal pValue);

public frmTesting_VerifySetup(ref cExercise_Test pTest)
{
InitializeComponent();

ReadUSB();
}

private void ReadUSB()
{
// Start the thread which reads the inbound buffer
bAbortRead = false;
BufferReader = new Thread(new ThreadStart(ProcessUsbData));
BufferReader.Start();
}

private void ProcessUsbData()
{
string[] data;
decimal O2;
decimal CO2;
TimeSpan ts;
DateTime startTime = DateTime.Now;

while(!bAbortRead)
{
data = device.ReadLine().Split(new char[]{','});

if(data.Length > O2INDEX)
decimal.TryParse(data[O2INDEX], out O2);
else
O2 = 0;

if(data.Length > CO2INDEX)
decimal.TryParse(data[CO2INDEX], out CO2);
else
CO2 = 0;

ts = DateTime.Now - startTime;
if(O2 > 0 && O2 < 100)
{
AddDataPoint("O2", (decimal)(ts.TotalSeconds),
O2);
UpdateO2Textbox(O2);
}
if(CO2 > 0 && CO2 < 100)
{
AddDataPoint("CO2", (decimal)(ts.TotalSeconds),
CO2);
UpdateCO2Textbox(CO2);
}

Thread.Sleep(10);
}
}

private void AddDataPoint(string series, decimal x, decimal y)
{
// InvokeRequired required compares the thread ID of the
// calling thread to the thread ID of the creating thread.
// If these threads are different, it returns true.
if(chart1.InvokeRequired)
{
AddDataPointCallback d = new AddDataPointCallback
(AddDataPoint);
Invoke(d, new object[] { series, x, y });
}
else
{
chart1.Series[series].Points.AddXY(x, y);
chart1.Invalidate();
}
}

private void UpdateO2Textbox(decimal pValue)
{
// InvokeRequired required compares the thread ID of the
// calling thread to the thread ID of the creating thread.
// If these threads are different, it returns true.
if(txtO2.InvokeRequired)
{
UpdateO2TextboxCallback d = new
UpdateO2TextboxCallback (UpdateO2Textbox);
Invoke(d, new object[] { pValue });
}
else
{
txtO2.Text = pValue.ToString("0.00");
}
}

private void UpdateCO2Textbox(decimal pValue)
{
// InvokeRequired required compares the thread ID of the
// calling thread to the thread ID of the creating thread.
// If these threads are different, it returns true.
if(txtCO2.InvokeRequired)
{
UpdateCO2TextboxCallback d = new
UpdateCO2TextboxCallback(UpdateCO2Textbox);
Invoke(d, new object[] { pValue });
}
else
{
txtCO2.Text = pValue.ToString("0.00");
}
}

}
 
R

Registered User

Basically what I have is a form with a graph on it which graphs data
that I'm reading from a USB device at 100 Hz (every 10ms). I have a
thread reading and parsing the data from the USB, but when it comes
time to draw that data on the graph the work is handled on the main UI
thread through a callback delegate since the form owns the graph
control. Is there a way I can have a separate thread own the graph
control and handle the drawing so that the rest of the UI is not
affected?
Having followed the thread these thoughts come to mind. The issue
seems to be the side effects of trying to maintain the data display in
real-time. I recently ran into a similar issue writing a CAT
controller/monitor for a device with minimal documentation.
http://www.subdevo.com/FT897DCAT/

The device has thirteen buttons, seven rotary controls (three
concentric plus one), six LEDs and a ~2x3 inch LCD screen. All the
controls are multifunction. One of the things the LCD display can do
is act as a spectrum scope. This gives an idea of how much and the
kind of information the UI might be expected to display.

Each request/response pair exchanged describes part of the current or
desired state of the device. The state information is sent to the
device using five byte requests. Depending upon the request the
response may be zero to five bytes with a RTT of up to ~5 ms. Multiple
requests are needed to fully describe the device's state. At any time
the polling may be interrupted to send a request to the device to
change state.

Trying to update the numerous UI controls in real-time resulted in a
sluggish UI. My solution was to create an type which represents the
device's state to the UI. It is a composite object that essentially is
a middle tier between the device and UI. A worker thread (the
controller thread) runs a loop that manages various custom thread
objects. The callbacks go to the worker thread not the UI thread. Each
polled response partially updates the state.

This scheme lets the BL/TL/device wrappers to be packaged UI-free. A
timer in the UI fires periodically to update numerous UI controls with
data read from properties the clean, simple interface the state object
presents to the UI.. Changes to the device state are made from the UI
by changing state object's properties.

regards
A.G.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top