Modifying Graphics object from separate threads

K

koschwitz

Hi,

I hope you guys can help me make this simple application work. I'm
trying to create a form displaying 3 circles, which independently
change colors 3 times after a random time period has passed.
I'm struggling with making the delegate/invoke thing work, as I know
GUI objects aren't thread-safe. I don't quite understand the concept
I'm supposed to use to modify the GUI thread-safe.

Below is my form and my Circle class. Currently, the application
crashes - I think because multiple threads are trying to modify the
Graphics object.

Thanks in advance for any help.
-Koschwitz

------------ start Form.cs -----------------------
public partial class Form1 : Form
{

private Random rnd;
private Pen myPen;
public Bitmap DrawArea;
Circle c1,c2,c3;
Graphics xGraph;

public Form1()
{
InitializeComponent();
rnd = new Random((int)DateTime.Now.Ticks); // seeded with
ticks
myPen = new Pen(Color.Red);
DrawArea = new Bitmap(this.ClientRectangle.Width,
this.ClientRectangle.Height,
System.Drawing.Imaging.PixelFormat.Format24bppRgb); //
make a persistent drawing area
xGraph = Graphics.FromImage(DrawArea);
}

private void Form1_Load(object sender, System.EventArgs e)
{
InitializeDrawArea();
}

private void InitializeDrawArea()
{
xGraph.Clear(Color.White);
}

private void Form1_Closed(object sender, System.EventArgs e)
{
DrawArea.Dispose();
}

public delegate void DrawCircleDelegate(); // no idea if this
is correct

private void Form1_Paint(object sender,
System.Windows.Forms.PaintEventArgs e)
{
xGraph = e.Graphics;
xGraph.DrawImage(DrawArea, 0, 0, DrawArea.Width,
DrawArea.Height);
c1 = new Circle(xGraph, 450, 550, 900);
c2 = new Circle(xGraph, 450, 950, 500);
c3 = new Circle(xGraph, 450, 1350, 900);
Thread t1 = new Thread(new ThreadStart(c1.DrawCircle));
Thread t2 = new Thread(new ThreadStart(c2.DrawCircle));
Thread t3 = new Thread(new ThreadStart(c3.DrawCircle));
t1.Start();
t2.Start();
t3.Start();
xGraph.Dispose();
}
}
------------ end Form.cs -----------------------

------------ start Circle.cs --------------------
class Circle
{
public Graphics xGraph;
private Random rnd = new Random((int)DateTime.Now.Ticks); //
seeded with ticks
int r1, x1, y1;

public Circle(Graphics g, int r, int x, int y)
{
xGraph = g;
r1 = r;
x1 = x;
y1 = y;
}

public void DrawCircle()
{
SolidBrush Brush = new SolidBrush(Color.White);
for (int k = 1; k < 3; k++)
{
Brush.Color = Color.FromArgb(
(rnd.Next(0, 255)),
(rnd.Next(0, 255)),
(rnd.Next(0, 255)));
Control.Invoke(new DrawCircleDelegate(c1.DrawCircle),
new object[] { }); //no idea if this is correct
xGraph.FillEllipse(Brush, x1 - r1, y1 - r1, r1, r1);
Thread.Sleep(rnd.Next(5000,10000)); //random wait time
between 5 and 10 seconds
}
}
}
------------ end Circle.cs --------------------
 
P

Peter Duniho

I hope you guys can help me make this simple application work. I'm
trying to create a form displaying 3 circles, which independently
change colors 3 times after a random time period has passed.
I'm struggling with making the delegate/invoke thing work, as I know
GUI objects aren't thread-safe. I don't quite understand the concept
I'm supposed to use to modify the GUI thread-safe.

Below is my form and my Circle class. Currently, the application
crashes - I think because multiple threads are trying to modify the
Graphics object.

From the code you posted, it would take some significantl effort to figure
out for sure how the application "crashes". You haven't been specific as
to what that means, but it's likely you're hitting some sort of unhandled
exception. Still, I see at least two different ways the code could cause
an exception, plus a third problem that makes me wonder how the code
compiles at all.

I can make a guess, but without clarification from you it would be
impossible to say for sure.

Frankly the code looks like the work of a madman. No offense intended, of
course. But it's pretty much all wrong.

Here are some things that need fixing:

* You are disposing the Graphics instance that you passed to your
threads to use, so if they get a chance to use it (I don't think they do,
but that's a separate issue) they would find it's disposed already and
unusable.

* You keep a Graphics instance in the classes. Not only would this
leads to an exception, it's also just not a good idea. The underlying
Windows OS object, a device context, is expensive and should only be kept
long enough for a given drawing operation or specific sequence of drawing
operations.

* You also fail to dispose of the Graphics instance you created in the
Form1 constructor, but since you shouldn't have the instance in the first
place, that won't matter once you fix the larger problem.

* You are starting threads in your OnPaint() method. This is
definitely not the way to draw. The OnPaint() method has one job: to draw
(or paint, if you like). It needs to draw, right away, and return.

* You are passing the PaintEventArgs.Graphics instance to your
threads. This instance, even if you did not dispose it yourself, would
not live long enough for any of the threads to use it. This is the likely
case no matter what you do, but you ensure it will be true by calling
Control.Invoke() before you use the Graphics instance, as well as by
sleeping in the method. Once the WM_PAINT message that is being handled
by OnPaint() has been finished, the Graphics instance will be disposed.

* You are calling Circle.DrawCircle() from within your
Circle.DrawCircle() method, and in a loop no less. This means that for
each call to DrawCircle(), you theoretically create two new calls to
DrawCircle() (the fact that you do it through Control.Invoke() doesn't
change this basic fact). I write "theoretically", because you never will
get to the second call, because the first call will just endlessly keep
going deeper and deeper. This will eventually cause a stack overflow
exception.

* But, I don't see how you even could get that far, because you are
using "c1.DrawCircle" as the method for your delegate instance, and I
don't see anywhere that "c1" is defined in that block of code. How does
that code even compile?

* And assuming there's some explanation for that compiling that I
don't get, how does "Control.Invoke" compile? Control.Invoke() isn't a
static method; you need a Control instance to call it.

* And last but certainly not least, you are creating a SolidBrush and
failing to dispose it. If you create an object that implements
IDisposable, you need to dispose it when you're done with it.

Finally, some things you should know, because you can avoid some of the
work you're doing or because it might affect your results:

* The Random class already seeds itself based on the time if you use
the parameterless constructor. There's no need for you to pass in a
time-dependent seed yourself.

* The Random.Next(Int32) overload is the same as calling
Random.Next(0, Int32). That is, if your range is from 0 to some number
(say, 255), you can just call Next(255).

* The maximum limit for the Random class is always an exclusive
limit. That is, the random number returned will never equal that number.
So if you want to randomly select from the full range of 0 through 255
inclusive, you need to pass 256 as the maximum, not 255.

* If you want the Image drawn at its actual size, there's no need to
specify the width and height to the Graphics.DrawImage() method. You can
just use an overload that takes just the position (e.g. DrawImage(Image,
Point), DrawImage(Image, Int32, Int32), etc.)

* You don't even need to use Invoke() with the Graphics class. It's
specifically the user-interface objects, like anything derived from
Control, that require that. In this case, you shouldn't be drawing from a
different thread anyway, but there's not a fundamental reason you can't
use a Graphics instance from a different thread than the one in which it
was created.

* The diameter of the circle you might actually draw is only half what
it _seems_ like you're looking for. Assuming you're passing a center
point and a radius to the Circle constructor, what the FillEllipse would
draw (if the code ever got there) is a circle centered on a point offset
by half the radius passed to the constructor in both the x and y
directions, and half the diameter. You probably want the width to be r1 *
2, not just r1.

As for helping with the broader question goes...

You'll need to be more specific about the exact behavior you want, because
I can't infer it from the code you posted. It _seems_ like you want some
number of randomly colored circles drawn in three specific locations on
your form at random intervals. Even from your description, I can't tell
whether each circle should change color three times, or you only want
three total color changes, once per circle, or you want three total color
changes, with the actual circle changed to also be selected randomly.

Because of all the problems, it's difficult to understand what exactly the
behavior you're looking for is. But I'm guessing that if you can explain
the specifics in a more detailed way, doing what you want would not be
nearly as complicated as the code you've written, and I'm happy to try to
help with that.

Pete
 
P

Peter Duniho

[...]
You'll need to be more specific about the exact behavior you want,
because I can't infer it from the code you posted. It _seems_ like you
want some number of randomly colored circles drawn in three specific
locations on your form at random intervals. Even from your description,
I can't tell whether each circle should change color three times, or you
only want three total color changes, once per circle, or you want three
total color changes, with the actual circle changed to also be selected
randomly.

For what it's worth, I made a guess and wrote a short demo application
that does what I _think_ you're trying to do. I've copied it below.
You'll need to create an empty project, add a new code file to the
project, paste this code into that file, and add references to System,
System.Drawing, and System.Windows.Forms. Once you've done all that, it
should compile and run directly.

The basic idea and the implementation is very simple: in the Circle class,
I create a timer that raises an event when it expires, letting me change
the color of the circle. The Circle class exposes an event that the code
using it can use to know when that happens.

Then the code using it, which in this case is a form class, subscribes to
the event when it creates each circle (it makes a list of three of them)..
In its OnPaint() method, it just draws the circles; a simple enumeration
and calling the Circle.Draw() method. The ColorChanged event handler in
the form class calls Control.Invalidate() to signal to Windows that the
circle that changed its color needs to be redrawn.

You'll note that whenever one circle changes its color, _all_ of the
circles are redrawn. This issue comes up as a basic fact of life in
Windows applications.

There are ways to optimize that kind of thing out, so that the drawing
code doesn't waste time calling into objects that don't really need
redrawing, but in most cases there is no need. The most expensive part of
the redrawing is actually copying bits to the screen buffer, and as long
as you only invalidate the areas that have actually changed, that
expensive part is essentially minimized as much as would be possible
anyway. By invalidating only the area that the circle uses, even though
Circle.Draw() is called on each circle, only the circle that actually
changed winds up affecting what's in the screen buffer.

Finally, I should apologize for a few things:

1) In the Circle class, there's a flag to keep track of whether the
code has actually started updating the color. I wanted _something_ like
that, because I didn't want the color changes to start happening until
we'd at least drawn the circle once (there can be delays starting a
Windows application, and I didn't want to miss a color change just because
a timer went off before the circles could even be drawn once). I don't
generally like these kinds of state flags, but sometimes they are a
reasonably simple way to implement some specific behavior like this.

2) In the Circle class, I've used a single Random instance but since
Random isn't thread-safe (the instance members aren't, anyway), I then
need to lock around uses of the instance. I realize that the locking
complicates the code a bit, but the alternative would be to complicate the
code some other way in ensuring that multiple instances of Random all were
seeded with unique, independent numbers (which can actually get kind of
hairy).

3) In the Form1 class, I added a bunch of code to resize the circles
to fit the form nicely. I realize that wasn't necessary for the purpose
of the demonstration and in fact may have the tendency to distract from
what's actually important. But for me that was the part that made the
demo code actually interesting, and I like it better that way. I did put
all of that stuff into Visual Studio regions so it's easy to hide, and I
hope doing so mitigates whatever complication it might have caused in your
life. :)

I'm apologetic for all of those things only because they make the code
possibly more complicated than it need be. This isn't by any means the
only way to solve the problem you've described (or at least the one I
think you have :) ).

I find this implementation I've chosen to be the most modular and simple,
but the issues I was addressing with the first two above complicating
factors could have been avoided by doing an implementation that was
handled entirely using the Forms.Timer class instead of the
Threading.Timer class (and thus making everything run in the same
thread). I wanted my Circle class to be completely independent of the
System.Windows.Forms namespace, which is why I choose Threading.Timer and
the multi-thread issues that it brings with it.

Different people may have different preferences regarding those
trade-offs. :)

Hope it helps.

Pete



using System;
using System.Collections.Generic;
using System.Windows.Forms;
using System.Text;
using System.Drawing;
using System.Threading;
using System.ComponentModel;

namespace TestCircleColors
{
#region Stock Program-class-with-main-entry-point

static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}
}

#endregion

class Circle
{
// Shared random number generator, because it's a relatively simple
// way to avoid having multiple instances of RNGs all seeded tothe
// same time value (because they were all created at the same time)
static private Random _rnd = new Random();
static private object _objLock = new object();

// Have we started choosing new colors yet?
private bool _fStarted;

// Circle visual characteristics
private Color _color;
private int _radius;
private int _xCenter;
private int _yCenter;

// State variables for color changing
private int _ccolorUse = 4;
private System.Threading.Timer _timer;

// Event so that user of this class knows when color's changed
public event EventHandler ColorChanged;

public Rectangle Bounds
{
get { return new Rectangle(_xCenter - _radius, _yCenter -
_radius, _radius * 2, _radius * 2); }
}

public int Radius
{
get { return _radius; }
set { _radius = value; }
}

public Point Center
{
get { return new Point(_xCenter, _yCenter); }
set
{
_xCenter = value.X;
_yCenter = value.Y;
}
}

public Circle() : this(0, 0, 0) { }

public Circle(int radius, int xCenter, int yCenter)
{
_radius = radius;
_xCenter = xCenter;
_yCenter = yCenter;
_timer = new System.Threading.Timer(_ColorTimerCallback);
}

public void Draw(Graphics gfx, Font font)
{
// If this is the first time we've drawn, we'll need to choose
our
// initial color. Doing so will also start the timer for
picking
// the next color (see _NewColor() method).
if (!_fStarted)
{
_NewColor();
_fStarted = true;
}

using (SolidBrush brush = new SolidBrush(_color))
{
gfx.FillEllipse(brush, Bounds);

// The rest of this is drawing the text...strictly for
// informational purposes and not at all necessary.
string strCount = _ccolorUse.ToString();
SizeF szfText = gfx.MeasureString(strCount, font);
PointF ptfText = new PointF(_xCenter - szfText.Width / 2,
_yCenter - szfText.Height / 2);

gfx.DrawString(strCount, font, Brushes.Black, ptfText);
}
}

private void _NewColor()
{
// Random.Next() isn't thread-safe, so make sure the shared
// Random is used by only one thread at a time
lock (_objLock)
{
// Pick a new color.
_color = Color.FromArgb(_rnd.Next(0x1000000));
_color = Color.FromArgb(255, _color);

// Count down the color changes. If we have any left,
// start a timer so we know when to change it
if (--_ccolorUse > 0)
{
// Setting the due time with an infinite interval
causes
// the timer to just fire once, until it's changed
again
_timer.Change(_rnd.Next(5000, 10000),
Timeout.Infinite);
}
}

// The first time we're called, it would be a waste to inform
// the client that the color had changed, since we never had
// a previous valid color. Otherwise, let them know.
if (_fStarted)
{
_RaiseColorChanged();
}
}

// This method is called by the timer when it expires
private void _ColorTimerCallback(object obj)
{
_NewColor();
}

// This method is used to raise the event for the client
private void _RaiseColorChanged()
{
EventHandler handler = ColorChanged;

if (handler != null)
{
handler(this, new EventArgs());
}
}
}

public partial class Form1 : Form
{
// The "Auto-circle" regions contain code that isn't really
// part of the example, so much as it's my own desire to make
// the demo look a little nicer. The code in those regions can
// safely be ignored, at least as far as the question of how
// to implement color-changing circles goes.

#region Auto-circle fields

// Percentage of form size used for margin
private const float kpctPadding = 0.05f;

// Locations of triangles within our 4-unit rectangle
private readonly PointF[] _rgptf = new PointF[] {
new PointF(1, (float)Math.Sqrt(3) + 1),
new PointF(2, 1),
new PointF(3, (float)Math.Sqrt(3) + 1) };

// This is the aspect ratio of a box that will fully contain three
// circles with their centers on the corners of an equilateral
triangle
// and with radiuses equal to half the length of a leg of the
triangle
// (that is, each circle touching each other)
private readonly double _ratioAspectTriangleBox = 4 /
(Math.Sqrt(3) + 2);

#endregion

private List<Circle> _rgcircle = new List<Circle>(3);

public Form1()
{
InitializeComponent();

// Looks nicer. :)
DoubleBuffered = true;

// Create a new circle for each point we initialized
for (int icircle = 0; icircle < _rgptf.Length; icircle++)
{
Circle circle = new Circle();

circle.ColorChanged += _HandleColorChanged;
_rgcircle.Add(circle);
}

// Initialize the actual in-form positions
// and sizes of each circle. If you hard-code
// the circle position and sizes, this isn't needed.
_UpdateCircles();
}

protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);

// Very simple: just draw each circle
foreach (Circle circle in _rgcircle)
{
circle.Draw(e.Graphics, Font);
}
}

// When the circle's color changes, this method will
// be called. In it, all you need to do is invalidate
// the area on the form the circle is drawn in.
private void _HandleColorChanged(object sender, EventArgs e)
{
Circle circle = (Circle)sender;

Invalidate(circle.Bounds);
}

#region Auto-circle methods

protected override void OnResize(EventArgs e)
{
base.OnResize(e);
_UpdateCircles();
}

private void _UpdateCircles()
{
Rectangle rectT = new Rectangle(new Point(), ClientSize);
int cxyPadding = -(int)Math.Max(kpctPadding * Size.Width,
kpctPadding * Size.Height);
int radius;

// Leave a margin within the form
rectT.Inflate(cxyPadding, cxyPadding);

// Fit the triangle in the space that's left
if (_ratioAspectTriangleBox < (double)rectT.Width /
rectT.Height)
{
rectT = new Rectangle(0, 0, (int)(rectT.Height *
_ratioAspectTriangleBox + 0.5), rectT.Height);
}
else
{
rectT = new Rectangle(0, 0, rectT.Width,
(int)(rectT.Height / _ratioAspectTriangleBox + 0.5));
}

// Center the triangle in the form
rectT.Offset((ClientSize.Width - rectT.Width) / 2,
(ClientSize.Height - rectT.Height) / 2);

// Update the circles
radius = rectT.Width / 4;
for (int icircle = 0; icircle < 3; icircle++)
{
Circle circle = _rgcircle[icircle];
PointF ptf = _rgptf[icircle];

Invalidate(circle.Bounds);
circle.Center = new Point((int)(radius * ptf.X)
+ rectT.Left, (int)(radius * ptf.Y) + rectT.Top);
circle.Radius = radius;
Invalidate(circle.Bounds);
}
}

#endregion

#region VS Designer stuff

/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;

/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be
disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}

#region Windows Form Designer generated code

/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.Text = "Form1";
}

#endregion

#endregion
}
}
 
K

koschwitz

Wow Peter, I can't thank you enough.
Seems like you had a little extra time available? I didn't expect
someone to give such a constructive answer... Again, thanks for all
the work you put into this.

Not sure if you care, but for your information: musicians are going to
play to these circles in a music class next week. :)

I like your -clean- implementation of the Draw() function and the
triangle idea. It indeed does look really nice.

There are 2 more things I'll need to add, and I hope you can help me
again:
The background should gradually turn from white to black while the
count down is running, and the last color change (after a set time)
should turn all circles black (one by one).

Now, I will have to add a second timer to time the total running time,
correct?

To make the color change to black work I replaced
---- start ----
// Pick a new color.
//_color = Color.FromArgb(_rnd.Next(0x1000000));
//_color = Color.FromArgb(255, _color);
---- end ----
with
---- start ----
int CMYcolor = _rnd.Next(1, 4);
if (CMYcolor == 1) { _color = Color.Cyan; }
if (CMYcolor == 2) { _color = Color.Magenta; }
if (CMYcolor == 3) { _color = Color.Yellow; }
if (_ccolorUse == 1) { _color = Color.Black; }
---- end ----
to have the circles turn black at the last count. Also, I am only
allowed to choose between the three CMY colors (no more random
colors).

How would I implement a second timer that sets the _ccolorUse = 1 (end
of the show) after a set time (e.g. 1 minute)? And how could I have
the background gradually turn from white to black during the set time
(e.g. 1 minute)?

Thanks again for all your help Peter,
-Mark
 
P

Peter Duniho

Wow Peter, I can't thank you enough.

You're welcome. :)
Seems like you had a little extra time available?

I wish. But perhaps you're familiar with compulsive behavior? :) I
don't believe that I'd qualify as a mental health risk, but I admit to a
certain degree of compulsiveness. Some problems just seem so in need of
an elegant solution that I can't help myself; in those occasions, I am
close to not having a choice in the matter. :)

That said, it didn't really take me that long. The hardest part for me
was working out the math for auto-sizing the circles. The basic timing
stuff is well within my common experience and took hardly longer to do
than the time it took to type it in. Experience does pay off sometimes.
:)
I didn't expect
someone to give such a constructive answer... Again, thanks for all
the work you put into this.

Not sure if you care, but for your information: musicians are going to
play to these circles in a music class next week. :)

Heh. Funny how things intersect. I myself had a music class (a long time
ago) in which I was tasked with coming up with something like that. We
each (my classmates and I) had to come up with some sort of "abstract"
music generation system. This being pre-Windows, and so of course very
pre-.NET :), mine involved helpers to watch the traffic outside the
classroom window, mapping car types, colors, speed to musical notes.

I guess music teachers are still doing that sort of thing. :)
I like your -clean- implementation of the Draw() function and the
triangle idea. It indeed does look really nice.

There are 2 more things I'll need to add, and I hope you can help me
again:
The background should gradually turn from white to black while the
count down is running, and the last color change (after a set time)
should turn all circles black (one by one).

Now, I will have to add a second timer to time the total running time,
correct?

That's likely the most straightforward solution, yes.
To make the color change to black work I replaced
---- start ----
// Pick a new color.
//_color = Color.FromArgb(_rnd.Next(0x1000000));
//_color = Color.FromArgb(255, _color);
---- end ----
with
---- start ----
int CMYcolor = _rnd.Next(1, 4);
if (CMYcolor == 1) { _color = Color.Cyan; }
if (CMYcolor == 2) { _color = Color.Magenta; }
if (CMYcolor == 3) { _color = Color.Yellow; }
if (_ccolorUse == 1) { _color = Color.Black; }
---- end ----
to have the circles turn black at the last count. Also, I am only
allowed to choose between the three CMY colors (no more random
colors).

As a note: I would initialize a List<Color> with the colors you want to
use, and then just index that list according to the random number you
generate (but choose a number between 0 and 3, not 1 and 4 :) ). Much
nicer-looking code than the if() statements. Alternatively, at the very
least those if() statements ought to be a single switch().
How would I implement a second timer that sets the _ccolorUse = 1 (end
of the show) after a set time (e.g. 1 minute)? And how could I have
the background gradually turn from white to black during the set time
(e.g. 1 minute)?

You'd need to be more specific about "after a set time". Is the total
duration of the show supposed to be 1 minute? Is that "set time" 1 minute
after the last color change? Are all the circles supposed to change to
black at the same time? Or are they each going to change to black 1
minute after each's last color change?

How best to implement that will depend on the specifics of the behavior
you want. In no case should it be all that complicated, but it's hard to
offer good advice without knowing the specifics.

For the background, I would probably try to use the Forms.Timer as my
first attempt. In a forms application, it's a natural choice for
relatively low-resolution timings, not only because it's in the same
namespace as the other forms stuff, but because it avoids cross-thread
issues (the timer events are raised on the main GUI thread).

My desire to keep the Circle class completely independent of the forms
steered me toward the Threading.Timer class there, but in the case of the
background you should be able to handle that entirely without any new
classes (just put the logic in the Form1 class). The timer interval is
easy to calculate from the number of shades of grey you want to use as you
fade to black and the total duration of the fade. Just handle the timer
event and with each tick of the timer, change the background color of the
form to one shade closer to black.

As with the circles, it's difficult to offer anything more specific than
that because I don't know when the background is supposed to start fading
to black. But presumably it's tied to the behavior of the circles
somehow, and given that it should be relatively simple to modify the code
I provided so that it provides the form with the necessary signal for it
to start fading out at the appropriate time.

If you can elaborate on the above questions, I'm happy to offer whatever
other advice I can.

Pete
 
K

koschwitz

I wish. But perhaps you're familiar with compulsive behavior? :) I
don't believe that I'd qualify as a mental health risk, but I admit to a
certain degree of compulsiveness. Some problems just seem so in need of
an elegant solution that I can't help myself; in those occasions, I am
close to not having a choice in the matter. :)
Well, your compulsive behavior did help me out a lot, so I wouldn't
say you'd qualify as a mental health risk. Not sure what other people
think about it though ;-)
Heh. Funny how things intersect. I myself had a music class (a long time
ago) in which I was tasked with coming up with something like that. We
each (my classmates and I) had to come up with some sort of "abstract"
music generation system. This being pre-Windows, and so of course very
pre-.NET :), mine involved helpers to watch the traffic outside the
classroom window, mapping car types, colors, speed to musical notes.
I guess music teachers are still doing that sort of thing. :)

This is for a friend's aleatoric piece (http://en.wikipedia.org/wiki/
Aleatoric_music) which explains all the randomness. Musicians will
play different melodies, depending on which color his/her circle has.
Percussionists will also play something special, if the circles happen
to have the same color/all different colors etc.
That's likely the most straightforward solution, yes. Ok


You'd need to be more specific about "after a set time". Is the total
duration of the show supposed to be 1 minute? Is that "set time" 1 minute
after the last color change? Are all the circles supposed to change to
black at the same time? Or are they each going to change to black 1
minute after each's last color change?
The total duration may be 1 minute, but the composer isn't sure yet.
This value should be flexible. After the total duration has expired,
the 3 circles should turn black one by one (5-10 ms after the last
color change).
As with the circles, it's difficult to offer anything more specific than
that because I don't know when the background is supposed to start fading
to black. But presumably it's tied to the behavior of the circles
somehow, and given that it should be relatively simple to modify the code
I provided so that it provides the form with the necessary signal for it
to start fading out at the appropriate time.
I think given that there's a second timer (for the total duration) it
most likely shouldn't be tied to the circles.
The fading should occur during the whole show, starting right away and
ending after the preset total duration (e.g. 1 minute).
With the circles turning black after the total duration, the piece
ends with a completely black screen.
If you can elaborate on the above questions, I'm happy to offer whatever
other advice I can.
Thanks in advance Peter - I'll try to follow your advise tonight,
unless you already have a very simple idea on how to implement
this. ;-)
 
P

Peter Duniho

[...]
The total duration may be 1 minute, but the composer isn't sure yet.
This value should be flexible. After the total duration has expired,
the 3 circles should turn black one by one (5-10 ms after the last
color change).

It sounds as though you will want the program to present some UI to allow
the user to enter a duration, and then base the remaining time
calculations on that. Fortunately, one of .NET's strengths is that it's
pretty easy to do something like that.

For your own application, I think the simplest approach would be to add a
button and a text field to the main form. The text field would be used to
enter some duration. The user would click the button, at which point the
program would use the value in the text field to initialize the process,
hide the button and text field, and start doing the color-changing circles.

None of that should be all that challenging; the trickiest part is
probably getting your duration from the text field, and even that's not
too hard. For something simple like this, I like to just parse the text
as a TimeSpan (using TimeSpan.TryParse()). Then you can enter times as
"hh:mm:ss" (where the letters are of course replaced by actual digits
representing the time value you want).

Note: in the code I posted, the circles start their color-changing as soon
as they are first drawn. So one approach to the above would be to not
actually create the circles until the user's clicked the start button you
add.
I think given that there's a second timer (for the total duration) it
most likely shouldn't be tied to the circles.

Yes, given what you've written it sounds as through the fade is a single
independent number.

However, now that you've elaborated, it sounds to me as though you will
want the circle's timing to be connected to the total duration somehow, so
that they are assured of finishing their cycle at the same time that the
fade completes.

This is again not difficult to do, but you will want to consider the
aesthetics that you're looking for. Strictly speaking, you could just
partition the total duration into three intervals of random length, but
that could result in an arbitrarily short interval at the end. Given that
your minimum time for a color change in the original example is 5 seconds,
I think you'd rather subtract five seconds from the total duration before
partitioning, so that you're assured you'll always have at least that long
between the last color of a circle and it switching to black.

For partitioning, you can just choose three random numbers using an
arbitrary range (say, 0-100...the size of the range is important only with
respect to the degree of granularity you want in selecting the color cycle
durations), and use those numbers as weights from the sum of all three,
applied to the total duration. For example:

TimeSpan[] ColorDurations(TimeSpan tsTotal, int ccolor)
{
int[] rgweightTime = new int[ccolor];
int weightTotal = 0;
TimeSpan[] rgtsRet = new TimeSpan[ccolor];
TimeSpan tsLeft;

// Pick a randomly selected weight for each color
for (int icolor = 0; icolor < ccolor; icolor++)
{
weightTotal += rgweightTime[icolor] = _rnd.Next(0, 100);
}

// Ensure a minimum five second interval at the end
tsTotal = tsTotal - new TimeSpan(0, 0, 5);
tsLeft = tsTotal;

// Convert the weights into actual time durations for each color
for (int icolor = 0; icolor < ccolor - 1; icolor++)
{
rgtsRet[icolor] = new TimeSpan((float)tsTotal.Ticks *
rgweightTime[icolor] / weightTotal);
tsLeft -= rgtsRet[icolor];
}

// Ensures that whatever rounding error might occur above, we
// still completely consume the total duration given to us
rgtsRet[ccolor - 1] = tsLeft;

return rgtsRet;
}

Finally, given the requirement that the circles switch to black with the
screen fading to black, it seems to me that you don't need the circles to
incorporate their own timing for the final black color. They should just
switch to the colors as necessary during the piece, when the timing
selected to ensure they are done switching at least five second before the
end of the piece (as mentioned above), and then let the code that handles
the fading of the background also set all of the circles to black when the
background is finally set to black.

Using the code I posted, you'll need to add a Color property to the Circle
class so that you can set the color, of course. For best results, you'll
probably want to call _RaiseColorChanged() from the setter of the property
as well. :)
Thanks in advance Peter - I'll try to follow your advise tonight,
unless you already have a very simple idea on how to implement
this. ;-)

Well, of course I do. But hopefully the above gets you heading in the
right direction and you're able to finish these details on your own. My
compulsion only goes so far. As they say, "I leave the rest as an
exercise for the reader". :)

Pete
 
K

koschwitz

Hi Pete,

Thanks again and a lot for your help - sorry it took me a couple of
days to respond.

The UI is a good idea, and I've decided to have the composer enter the
number of seconds (for simplicity) he'd like the piece to run.

However, the background fading is something I have no idea how to
start on. I've done this in Flash before, and it was really easy. ;)
Anyways, I know I'll have to create another thread drawing rectangles
in different shades of gray, or should I set the form's background
color instead?

totalSeconds / 255 * 1000 should give me the milliseconds after which
I will subtract 1 from each the R, G, B values of the background
color, getting to 0 after the total duration. I'm just not sure if I
should do this event-driven, like the circles, or if there's another,
perhaps much easier solution to this. I wish I could just run this in
the main thread, and make it sleep (Thread.Sleep()) for the amount of
milliseconds I calculate with the amount from above - but it didn't
work when I tried it.

Hoping you're still a bit interested,
-Mark
 
P

Peter Duniho

[...]
Anyways, I know I'll have to create another thread drawing rectangles
in different shades of gray, or should I set the form's background
color instead?

I would set the background color. Otherwise, you just add more stuff you
need to draw, without really needing to.
totalSeconds / 255 * 1000 should give me the milliseconds after which
I will subtract 1 from each the R, G, B values of the background
color, getting to 0 after the total duration. I'm just not sure if I
should do this event-driven, like the circles, or if there's another,
perhaps much easier solution to this. I wish I could just run this in
the main thread, and make it sleep (Thread.Sleep()) for the amount of
milliseconds I calculate with the amount from above - but it didn't
work when I tried it.

No, you're right it wouldn't. Your main thread must always handle
whatever event is going on and then return asap. Otherwise, the graphical
part of the program just stops working.

However, you _can_ get this to work in the main thread. Just use the
Timer class found in the System.Windows.Forms namespace. It is similar
to, but not exactly the same as, the System.Threading.Timer class that's
already in use for the circles. The basic idea will be to set the timer
to fire repeatedly using the duration you've calculated above. Each time
the timer event is raised, you'll set the background to one shade closer
to black.

The main difference between the two Timer classes is that the Forms.Timer
class will raise the event on your main GUI thread, eliminating any need
to worry about the cross-thread stuff that was needed with the
Threading.Timer class.

(Or rather, I should say: "that _should have been_ needed with the
Threading.Timer class". Looking at the code I posted, I see that I forgot
to use Invoke() in the event handler dealing with the ColorChanged event..
That not only shouldn't have worked, it should have raised a cross-thread
MDA exception when I ran the program in the debugger. I have no idea why
it worked without the exception happening. It's _possible_ that for some
reason I don't understand, it was actually legal and I just didn't know
it. In the meantime, you should probably change the
Form1._HandleColorChanged() method to look like this:

private void _HandleColorChanged(object sender, EventArgs e)
{
Circle circle = (Circle)sender;

Invoke((MethodInvoker)delegate() { Invalidate(circle.Bounds);
});
}

That way you're assured that the call to Invalidate() will always happen
on the right thread. Sorry for the confusion. If I have time to look
into it and I actually find an answer, I'll get back to you here on why it
worked without the Invoke().)

Note: I would just keep a single counter for the background color.
Subtract one from it, then set the background color to "new
Color.FromArgb(255, shade, shade, shade)". That's readable, and yet still
reasonably efficient (and especially with respect to not storing three
different variables that are always the same value :) ).
Hoping you're still a bit interested,

No, not any more. I sat here doing nothing else but waiting for your
reply, and after two days I got hungry and gave up.

:) Just kidding. It doesn't matter to me how long it takes for you to
follow up or even if you do. I'm happy to try to help if I can.

Pete
 
P

Peter Duniho

[...]
(Or rather, I should say: "that _should have been_ needed with the
Threading.Timer class". Looking at the code I posted, I see that I
forgot to use Invoke() in the event handler dealing with the
ColorChanged event. That not only shouldn't have worked, it should have
raised a cross-thread MDA exception when I ran the program in the
debugger. I have no idea why it worked without the exception
happening. It's _possible_ that for some reason I don't understand, it
was actually legal and I just didn't know it. In the meantime, you
should probably change the Form1._HandleColorChanged() method to look
like this:

private void _HandleColorChanged(object sender, EventArgs e)
{
Circle circle = (Circle)sender;

Invoke((MethodInvoker)delegate()
{ Invalidate(circle.Bounds); });
}

That way you're assured that the call to Invalidate() will always happen
on the right thread. Sorry for the confusion. If I have time to look
into it and I actually find an answer, I'll get back to you here on why
it worked without the Invoke().)

Okay...if you've seen my other thread, you know why the MDA exception
didn't happen. There isn't one, it's a regular exception, and the Control
class specifically does not throw it when calling Invalidate().

For the moment, I'm going to assume that means that Invalidate() is safe
to call without Invoke(), which is why everything works without Invoke()..
The docs contradict this assumption, but in this case it seems like
behavior trumps documentation. :)

I wish I understood better the exact source of the cross-thread
restriction in .NET. There are cross-thread issues in the native Win32
API as well, but the functions used to send or post messages to the window
handle that for you. Why .NET doesn't take the same approach I don't
know, and unfortunately I haven't found a decent discussion on the topic..
I did find one article in which a person asserted this was to protect
against a race condition with multiple threads trying to send sequences of
messages to a window, but the fact is since you can use Invoke(), that
possible race condition could still happen. Using Invoke() doesn't solve
it, you just need to avoid the race in the first place.

And I apologize if the thread stuff is over your head. I'm writing the
detailed part here as much in hopes that someone who _does_ understand the
cross-thread stuff for the Control class better will pipe up and
elaborate. For the longest time I've just taken it as granted that using
Invoke() was required when calling any method other than those documented
as "thread safe" (see
<http://msdn2.microsoft.com/en-us/library/system.windows.forms.control.invokerequired(VS.90).aspx>
for an example), but now I wonder if that's actually true. It does make
sense that Invalidate() might not be subject to the same limitation, since
it doesn't involve the message queue for the window (which is the main
thing that causes a window to be tied to a thread in the first place).

Anyway, the long story is, I don't think you need to change the code as
I've suggested after all. But it won't hurt if you want to do it anyway..
:)

Pete
 
F

Freddy Potargent

And I apologize if the thread stuff is over your head. I'm writing the
detailed part here as much in hopes that someone who _does_ understand
the cross-thread stuff for the Control class better will pipe up and
elaborate. For the longest time I've just taken it as granted that
using Invoke() was required when calling any method other than those
documented as "thread safe" (see
<http://msdn2.microsoft.com/en-us/library/system.windows.forms.control.invokerequired(VS.90).aspx>
for an example), but now I wonder if that's actually true. It does make
sense that Invalidate() might not be subject to the same limitation,
since it doesn't involve the message queue for the window (which is the
main thing that causes a window to be tied to a thread in the first
place).

Hi Peter,

I don't claim I understand the cross-thread stuff and i don't know if the
following is really biting you but I once was, so ...

In short (the important part is halfway down the description of
InvokeRequired), if the Win32 handle is not yet created InvokeRequired can
return false, indicating you're safe to modify the control and then
there's also no exception fired if you do. *But* this causes the handle to
be created on the calling thread if you access any other method/property
that needs the win32 handle, ie on a thread without message pump ... not a
good thing of course.

So as they advise, you should also check IsHandleCreated if InvokeRequired
returns false and if you're not sure the underlying handle is already
created.

I would be interested if this is really the cause of your problem because
it would mean I'll have to look at my app a bit more closely too. :-/
 
P

Peter Duniho

[...]
So as they advise, you should also check IsHandleCreated if
InvokeRequired returns false and if you're not sure the underlying
handle is already created.

I would be interested if this is really the cause of your problem
because it would mean I'll have to look at my app a bit more closely
too. :-/

No, it's not the issue I'm seeing, though thanks for the suggestion. I
could have missed something, but I did read the documentation carefully
and I was already aware of the issues with respect to a
not-fully-initialized control. I try to never write code that accesses a
control before it's been completely initialized. So far, I've been
successful. :)

You can see the code that the OP and I are talking about if you check back
several posts in this thread; the sample code I posted is what I'm
referring to. It will never fire the event that's the potential problem
until the first time that the control in question (the form instance) is
drawn, ensuring that the control is completely initialized by then.

The more I think about it, the more I think that the requirement to call
Invoke() is not precisely the same as a method or property being
thread-safe, in spite of the documentation using the two ideas in an
apparently synonymous manner. In the former, I suspect that the
requirement to call Invoke() exists mainly (solely?) for situations that
would wind up calling the window procedure for the control. In the
latter, it would be more an issue of having multiple threads trying to
call the same method or property at the same time.

Since Invalidate() doesn't wind up calling the window procedure, I don't
run into the first issue. There is in fact a potential issue with respect
to it being called simultaneously from multiple threads, now that I think
about it, but the docs don't say anything one way or the other that
indicates whether this is a thread-safe operation. It probably comes down
to whether the underlying native OS call InvalidateRect() is thread-safe,
which I don't know off the top of my head.

Pete
 
K

koschwitz

Well Pete,

I just wanted to thank you again for helping me out here - I did what
you suggested and it worked right away.
Without your help I wouldn't have known where to start, and I honestly
didn't expect someone in a news group to spend so much time on this.

The piece was performed last Wednesday and the program worked like a
charm. If you'd like to hear a recording of it, please let me know.

Happy Holidays to you & your family!
-Mark
 
P

Peter Duniho

I just wanted to thank you again for helping me out here - I did what
you suggested and it worked right away.
Without your help I wouldn't have known where to start, and I honestly
didn't expect someone in a news group to spend so much time on this.

I have found that I frequently learn something new trying to explain
something I already know to someone. This was no exception (note the
little tangent regarding Invalidate() and thread-safety). You're welcome
for the help, but I hope you don't think I'm being entirely selfless. :)
The piece was performed last Wednesday and the program worked like a
charm. If you'd like to hear a recording of it, please let me know.

Sure, if it's convenient I think it'd be fun to hear.

Thanks,
Pete
 

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