![]() |
|
|
#1 |
|
Guest
Posts: n/a
|
Modifying Graphics object from separate threads
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 -------------------- |
|
|
|
#2 |
|
Guest
Posts: n/a
|
Re: Modifying Graphics object from separate threads
On Sun, 02 Dec 2007 18:45:53 -0800, <koschwitz@gmx.de> wrote:
> 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 |
|
|
|
#3 |
|
Guest
Posts: n/a
|
Re: Modifying Graphics object from separate threads
On Sun, 02 Dec 2007 21:39:26 -0800, Peter Duniho
<NpOeStPeAdM@nnowslpianmk.com> wrote: > [...] > 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(fals e); 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 } } |
|
|
|
#4 |
|
Guest
Posts: n/a
|
Re: Modifying Graphics object from separate threads
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 |
|
|
|
#5 |
|
Guest
Posts: n/a
|
Re: Modifying Graphics object from separate threads
On Mon, 03 Dec 2007 22:16:59 -0800, <koschwitz@gmx.de> wrote:
> 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 |
|
|
|
#6 |
|
Guest
Posts: n/a
|
Re: Modifying Graphics object from separate threads
> 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. > > 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. Ok > > 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? 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. ;-) |
|
|
|
#7 |
|
Guest
Posts: n/a
|
Re: Modifying Graphics object from separate threads
On Tue, 04 Dec 2007 10:02:10 -0800, <koschwitz@gmx.de> wrote:
> [...] > 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 |
|
|
|
#8 |
|
Guest
Posts: n/a
|
Re: Modifying Graphics object from separate threads
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 |
|
|
|
#9 |
|
Guest
Posts: n/a
|
Re: Modifying Graphics object from separate threads
On Mon, 10 Dec 2007 23:30:35 -0800, <koschwitz@gmx.de> wrote:
> [...] > 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 |
|
|
|
#10 |
|
Guest
Posts: n/a
|
Re: Modifying Graphics object from separate threads
On Tue, 11 Dec 2007 00:03:36 -0800, Peter Duniho
<NpOeStPeAdM@nnowslpianmk.com> wrote: > [...] > (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 > > 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 |
|
![]() |
| Thread Tools | |
| Display Modes | |
|
|