'parameter is not valid' C# winforms graphics error

627 Views Asked by At

I am trying to create a game in C#, using winforms, where shapes randomly appears and you have to click the shapes (one at a time) as quickly as possible.

When the shape is created, a timer starts, this timer counts how long it took you to click the shape, then once clicked, the shape deletes. I am having trouble with the shape deleting part. I have the timer, and I have it waiting for the timer to be stopped, but whenever I try to delete the shape I get the following error at the line: g.clear(Color.black).

System.ArgumentException: 'Parameter is not valid.'

The full error trace is:

  System.ArgumentException
  HResult=0x80070057
  Message=Parameter is not valid.
  Source=System.Drawing
  StackTrace:
  at System.Drawing.Graphics.Clear(Color color)
  at NumbersGame.ShapesRound.<ShapesWindow_Paint>d__9.MoveNext() in
  C:\Users\Matthew\Desktop\AHProject\NumbersGame\NumbersGame\ShapesRound.cs:line 92 

This is all of my code for the game:

namespace NumbersGame
{
    public partial class ShapesRound : UserControl
    {
        public ShapesRound()
        {
            InitializeComponent();
        }
        public bool GameStarted = false;
        public GraphicsPath path;
        public Rectangle currentShape = new Rectangle();
        Stopwatch timer = new Stopwatch();
        public bool timerIsRunning = false;

        public bool ShapeClicked(Point location)
        {
            bool clicked = false;
            if (currentShape.Contains(location))
            {
                clicked = true;
            }
            return clicked;
        }

        private void ShapesRound_Load(object sender, EventArgs e)
        {
            path = new GraphicsPath();
            nameBox.Text = "Matthew";
            // nameBox.Text = Welcome.name;
            scoreBox.Text = Welcome.totalScore.ToString();
        }

        private void ShapesRound_MouseDown(object sender, MouseEventArgs e)
        {
            if (ShapeClicked(e.Location))
            {
                //   MessageBox.Show("CLICKED");
                //end timer
                timer.Stop();
                var timeP = timer.ElapsedMilliseconds / 1000;
                //   MessageBox.Show(timeP.ToString() + " SECONDS");  
            }
        }
    
        private async void ShapesWindow_Paint(object sender, PaintEventArgs e) 
        {
            if (GameStarted == false) { return; } //if the game hasnt started (ie the start button has not been clicked), do nothing
            else
            { 
                using (Graphics g = e.Graphics)
                {
                    g.SmoothingMode = SmoothingMode.AntiAlias;
                    currentShape = new Rectangle(10, 100, 75, 75); //assign coordinates to the global CurrentShape variable
                    g.FillEllipse(new SolidBrush(Color.Black), currentShape);
                    
                    //INVALID PAARAM?//fill the currentshape on screen
                    //start a timer 
                    timer.Start(); //start a timer
                    while (timer.IsRunning) //while the timer is running (ie shape isnt clicked) wait 
                    {
                        await Task.Delay(500);
                    }
                    g.Clear(Color.Black);
                    //    currentShape = null; 

                    //  MessageBox.Show("DELETING");
                    var bckCol = ShapesWindow.BackColor;

                    // e.Graphics.FillEllipse(new SolidBrush(bckCol), currentShape);
                    //     e.Graphics.Clear(Color.Black); //INVALID PAARAM?
                    ShapesWindow.Refresh();
                }              
            }        
        }
       
        private void StartButton_Click(object sender, EventArgs e)
        {
            GameStarted = true;
            ShapesWindow.Paint += new PaintEventHandler(ShapesWindow_Paint);
            
            ShapesWindow.Refresh();    
        }  
    }
}
2

There are 2 best solutions below

1
Gabriel Luci On

If I understand correctly, you are drawing a shape. Then when that shape is clicked, you want to delete that shape and draw a new one, and the process repeats.

If I'm correct, then reverse your logic. Right now, you are drawing a shape, waiting, then trying to clear. But you can clear first, then draw the shape. That way, when you detect a click, you just trigger a refresh.

private void ShapesRound_MouseDown(object sender, MouseEventArgs e)
{
    if (ShapeClicked(e.Location))
    {
        //   MessageBox.Show("CLICKED");
        //end timer
        timer.Stop();
        var timeP = timer.ElapsedMilliseconds / 1000;
        //   MessageBox.Show(timeP.ToString() + " SECONDS");
        
        ShapesWindow.Refresh();
    }
}

private async void ShapesWindow_Paint(object sender, PaintEventArgs e) 
{
    if (GameStarted == false) { return; } //if the game hasnt started (ie the start button has not been clicked), do nothing
    else
    { 
        using (Graphics g = e.Graphics)
        {
            g.Clear(Color.Black);
            
            g.SmoothingMode = SmoothingMode.AntiAlias;
            currentShape = new Rectangle(10, 100, 75, 75); //assign coordinates to the global CurrentShape variable
            g.FillEllipse(new SolidBrush(Color.Black), currentShape);
            
            //start a timer 
            timer.Start(); //start a timer
        }              
    }        
}

If there's ever a time you don't want to draw a new shape, then declare a bool value that will tell it to not draw a new shape. Set that whenever you don't want to draw a new value, and check that in your paint event after you clear, like:

...
g.Clear(Color.Black);
if (dontDrawNewShape) return;
...
2
adv12 On

This isn't really an answer to your original question but rather to your comment:

Maybe I'm missing something here. I'm trying to paint an object essentially when the last object has been clicked. That involves deleting the first object, and painting a new one, upon the click of the old one. How do you suggest I go about doing that? In a non-"argh" inducing way.

As rene and Flydog57 pointed out, your approach is very nonstandard. Rather than start from where you are, let me just expand on their comments and explain the standard way of structuring a Windows Forms app like the one you're trying to build.

  1. Keep track of the "state" of your control (what objects are visible, etc.) in member variables on the control.
  2. The Paint event is just for refreshing the control, or part of it, in one pass. If something that would affect the appearance of your control happens, call Invalidate() from whatever event handler has resulted in the state change and trust Windows to notify your control via the Paint event when it's convenient. Note: Invalidate() doesn't cause a synchronous draw like Refresh() does (which is usually overzealous); it instead queues up a redraw for when Windows is done handling higher-priority messages like mouse movements.
  3. Remember that the Graphics object you're passed in the Paint handler is only good for one redraw. It becomes invalid once you return from that handler or, as in your code, you start a new repaint (as you do with Refresh, making your Paint handler recursive, which is part of what was so argh-inducing about it).
  4. Keep in mind that what you draw in the Paint event handler isn't persistent. That is, if something occludes the window, or you drag it offscreen, or anything else happens that causes the video memory allocated to your control to be invalidated, Windows will fire the Paint event for you to redraw whatever was there before. This is another reason you want to keep track of state in a member variable--your control may be asked to redraw itself when its state has not changed. You can't rely on video memory to accurately represent what has been drawn in the past.
  5. If something needs to be animated or happen after an elapsed period, create a System.Windows.Forms.Timer as a member variable, register a Tick handler, and Start() the timer. The Tick handler should update the persistent state of the control as appropriate and call Invalidate(). If the timer was intended for a one-off action, Stop() it.
  6. For something like a hit test on a shape you've drawn, handle the control's Click or MouseDown event and test against the member variable that you're using to persist state. If your last drawn shape was a Rectangle, keep that Rectangle in a member variable and test the mouse location from the EventArgs against that Rectangle. If it's then time to draw a different shape, create a new persistent object that represents the new shape and call Invalidate().

Let me know if you have questions about this general outline and I'll update my answer accordingly.