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();
}
}
}
CodePudding user response:
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;
...
CodePudding user response:
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.
- Keep track of the "state" of your control (what objects are visible, etc.) in member variables on the control.
- The
Paintevent is just for refreshing the control, or part of it, in one pass. If something that would affect the appearance of your control happens, callInvalidate()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 likeRefresh()does (which is usually overzealous); it instead queues up a redraw for when Windows is done handling higher-priority messages like mouse movements. - Remember that the
Graphicsobject you're passed in thePainthandler 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 withRefresh, making yourPainthandler recursive, which is part of what was so argh-inducing about it). - Keep in mind that what you draw in the
Paintevent 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 thePaintevent 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. - If something needs to be animated or happen after an elapsed period, create a
System.Windows.Forms.Timeras a member variable, register aTickhandler, andStart()the timer. TheTickhandler should update the persistent state of the control as appropriate and callInvalidate(). If the timer was intended for a one-off action,Stop()it. - For something like a hit test on a shape you've drawn, handle the control's
ClickorMouseDownevent and test against the member variable that you're using to persist state. If your last drawn shape was aRectangle, keep thatRectanglein a member variable and test the mouse location from theEventArgsagainst thatRectangle. If it's then time to draw a different shape, create a new persistent object that represents the new shape and callInvalidate().
Let me know if you have questions about this general outline and I'll update my answer accordingly.
