Modifying Graphics object from separate threads

Collapse
This topic is closed.
X
X
 
  • Time
  • Show
Clear All
new posts
  • koschwitz@gmx.de

    #1

    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()
    {
    InitializeCompo nent();
    rnd = new Random((int)Dat eTime.Now.Ticks ); // seeded with
    ticks
    myPen = new Pen(Color.Red);
    DrawArea = new Bitmap(this.Cli entRectangle.Wi dth,
    this.ClientRect angle.Height,
    System.Drawing. Imaging.PixelFo rmat.Format24bp pRgb); //
    make a persistent drawing area
    xGraph = Graphics.FromIm age(DrawArea);
    }

    private void Form1_Load(obje ct sender, System.EventArg s e)
    {
    InitializeDrawA rea();
    }

    private void InitializeDrawA rea()
    {
    xGraph.Clear(Co lor.White);
    }

    private void Form1_Closed(ob ject sender, System.EventArg s e)
    {
    DrawArea.Dispos e();
    }

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

    private void Form1_Paint(obj ect sender,
    System.Windows. Forms.PaintEven tArgs e)
    {
    xGraph = e.Graphics;
    xGraph.DrawImag e(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)Dat eTime.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(Colo r.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 DrawCircleDeleg ate(c1.DrawCirc le),
    new object[] { }); //no idea if this is correct
    xGraph.FillElli pse(Brush, x1 - r1, y1 - r1, r1, r1);
    Thread.Sleep(rn d.Next(5000,100 00)); //random wait time
    between 5 and 10 seconds
    }
    }
    }
    ------------ end Circle.cs --------------------
  • Peter Duniho

    #2
    Re: Modifying Graphics object from separate threads

    On Sun, 02 Dec 2007 18:45:53 -0800, <koschwitz@gmx. dewrote:
    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.DrawCirc le() from within your
    Circle.DrawCirc le() 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.DrawCir cle" 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(Int 32) 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.DrawIm age() 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

    Comment

    • Peter Duniho

      #3
      Re: Modifying Graphics object from separate threads

      On Sun, 02 Dec 2007 21:39:26 -0800, Peter Duniho
      <NpOeStPeAdM@nn owslpianmk.comw rote:
      [...]
      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.Invalid ate() 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.Collecti ons.Generic;
      using System.Windows. Forms;
      using System.Text;
      using System.Drawing;
      using System.Threadin g;
      using System.Componen tModel;

      namespace TestCircleColor s
      {
      #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.Ena bleVisualStyles ();
      Application.Set CompatibleTextR enderingDefault (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.Threadin g.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(_xCen ter - _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.Threadin g.Timer(_ColorT imerCallback);
      }

      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(_col or))
      {
      gfx.FillEllipse (brush, Bounds);

      // The rest of this is drawing the text...strictly for
      // informational purposes and not at all necessary.
      string strCount = _ccolorUse.ToSt ring();
      SizeF szfText = gfx.MeasureStri ng(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(0x100 0000));
      _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.Infinit e);
      }
      }

      // 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)
      {
      _RaiseColorChan ged();
      }
      }

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

      // This method is used to raise the event for the client
      private void _RaiseColorChan ged()
      {
      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.Sqr t(3) + 1),
      new PointF(2, 1),
      new PointF(3, (float)Math.Sqr t(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 _ratioAspectTri angleBox = 4 /
      (Math.Sqrt(3) + 2);

      #endregion

      private List<Circle_rgc ircle = new List<Circle>(3) ;

      public Form1()
      {
      InitializeCompo nent();

      // 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.ColorCha nged += _HandleColorCha nged;
      _rgcircle.Add(c ircle);
      }

      // 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(PaintEv entArgs e)
      {
      base.OnPaint(e) ;

      // Very simple: just draw each circle
      foreach (Circle circle in _rgcircle)
      {
      circle.Draw(e.G raphics, 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 _HandleColorCha nged(object sender, EventArgs e)
      {
      Circle circle = (Circle)sender;

      Invalidate(circ le.Bounds);
      }

      #region Auto-circle methods

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

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

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

      // Fit the triangle in the space that's left
      if (_ratioAspectTr iangleBox < (double)rectT.W idth /
      rectT.Height)
      {
      rectT = new Rectangle(0, 0, (int)(rectT.Hei ght *
      _ratioAspectTri angleBox + 0.5), rectT.Height);
      }
      else
      {
      rectT = new Rectangle(0, 0, rectT.Width,
      (int)(rectT.Hei ght / _ratioAspectTri angleBox + 0.5));
      }

      // Center the triangle in the form
      rectT.Offset((C lientSize.Width - rectT.Width) / 2,
      (ClientSize.Hei ght - 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(circ le.Bounds);
      circle.Center = new Point((int)(rad ius * ptf.X)
      + rectT.Left, (int)(radius * ptf.Y) + rectT.Top);
      circle.Radius = radius;
      Invalidate(circ le.Bounds);
      }
      }

      #endregion

      #region VS Designer stuff

      /// <summary>
      /// Required designer variable.
      /// </summary>
      private System.Componen tModel.IContain er 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.Disp ose();
      }
      base.Dispose(di sposing);
      }

      #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 InitializeCompo nent()
      {
      this.components = new System.Componen tModel.Containe r();
      this.AutoScaleM ode = System.Windows. Forms.AutoScale Mode.Font;
      this.Text = "Form1";
      }

      #endregion

      #endregion
      }
      }

      Comment

      • koschwitz@gmx.de

        #4
        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(0x100 0000));
        //_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


        Comment

        • Peter Duniho

          #5
          Re: Modifying Graphics object from separate threads

          On Mon, 03 Dec 2007 22:16:59 -0800, <koschwitz@gmx. dewrote:
          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(0x100 0000));
          //_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<Colorwith 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

          Comment

          • koschwitz@gmx.de

            #6
            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. ;-)

            Comment

            • koschwitz@gmx.de

              #7
              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



              Comment

              • Peter Duniho

                #8
                Re: Modifying Graphics object from separate threads

                On Mon, 10 Dec 2007 23:30:35 -0800, <koschwitz@gmx. dewrote:
                [...]
                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.Threadin g.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._HandleCo lorChanged() method to look like this:

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

                Invoke((MethodI nvoker)delegate () { Invalidate(circ le.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

                Comment

                • koschwitz@gmx.de

                  #9
                  Re: Modifying Graphics object from separate threads

                  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


                  Comment

                  • Peter Duniho

                    #10
                    Re: Modifying Graphics object from separate threads

                    On Fri, 14 Dec 2007 15:08:14 -0800, <koschwitz@gmx. dewrote:
                    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

                    Comment

                    Working...