Related
In my Winforms application I'm attempting to recreate the Monte Carlo Method to approximate PI. The form itself consists of a box in which the user provides the amount of points and a panel, on which I want to draw. For this example though, let's assume the amount is going to be constant.
private int Amount = 10000;
private int InCircle = 0, Points = 0;
private Pen myPen = new Pen(Color.White);
private void DrawingPanel_Paint(object sender, PaintEventArgs e)
{
int w = DrawingPanel.Width, h = DrawingPanel.Height;
e.Graphics.TranslateTransform(w / 2, h / 2);
//drawing the square and circle in which I will display the points
var rect = new Rectangle(-w / 2, -h / 2, w - 1, h - 5);
e.Graphics.DrawRectangle(myPen, rect);
e.Graphics.DrawEllipse(myPen, rect);
double PIE;
int X, Y;
var random = new Random();
for (int i = 0; i < Amount; i++)
{
X = random.Next(-(w / 2), (w / 2) + 1);
Y = random.Next(-(h / 2), (h / 2) + 1);
Points++;
if ((X * X) + (Y * Y) < (w / 2 * h / 2))
{
InCircle++;
e.Graphics.FillRectangle(Brushes.LimeGreen, X, Y, 1, 1);
}
else
{
e.Graphics.FillRectangle(Brushes.Cyan, X, Y, 1, 1);
}
//just so that the points appear with a tiny delay
Thread.Sleep(1);
}
PIE = 4 * ((double)InCircle/(double)Points);
}
And this works. The visualization is great.
However, now I would like to recreate this asynchronously, so that while this is being drawn in the background, the app is still responsible and the user can do something else, or even just move the window around.
Initially I made a second method that does the drawing, which I call from the Event Handler:
private double Calculate(PaintEventArgs e)
{
int w = DrawingPanel.Width, h = DrawingPanel.Height;
double PIE;
int X, Y;
var random = new Random();
for (int i = 0; i < Amount; i++)
{
X = random.Next(-(w / 2), (w / 2) + 1);
Y = random.Next(-(h / 2), (h / 2) + 1);
Points++;
if ((X * X) + (Y * Y) < (w / 2 * h / 2))
{
InCircle++;
e.Graphics.FillRectangle(Brushes.LimeGreen, X, Y, 1, 1);
}
else
{
e.Graphics.FillRectangle(Brushes.Cyan, X, Y, 1, 1);
}
Thread.Sleep(1);
}
PIE = 4 * ((double)InCircle/(double)Points);
return PIE;
}
private void DrawingPanel_Paint(object sender, PaintEventArgs e)
{
int w = DrawingPanel.Width, h = DrawingPanel.Height;
e.Graphics.TranslateTransform(w / 2, h / 2);
var rect = new Rectangle(-w / 2, -h / 2, w - 1, h - 5);
e.Graphics.DrawRectangle(myPen, rect);
e.Graphics.DrawEllipse(myPen, rect);
var result = Calculate(e);
}
And this worked fine as well. Until I made the event handler async.
private async void DrawingPanel_Paint(object sender, PaintEventArgs e) {...}
Now, when I try running the Calculate method, either through Task.Run, or when I change its return type to Task and start that, I get the error: "Parameter is not valid" in the following line:
e.Graphics.FillRectangle(Brushes.LimeGreen, X, Y, 1, 1);
Now the question, is it possible to draw on a panel asynchronously, so that other parts of the app are not locked? And if not, is there a way to recreate this algorithm using any other way (not necessarily a panel)? Cheers.
You must not draw in a form or control from another thread. In debugging mode, WinForms will raise an exception if you do.
The best approach would be to use a Timer component. On each tick of the timer do one step from your loop. You will have to, of course, move the look counter as a global variable.
The issues other posters raise are completely valid, but this is actually completely achievable if you alter strategy a little.
While you can't draw to a UI Graphics context on another thread, there is nothing stopping you drawing to a non-UI one. So what you could do is to have a buffer to which you draw:
private Image _buffer;
You then decide what your trigger event for beginning drawing is; let's assume here it's a button click:
private async void button1_Click(object sender, EventArgs e)
{
if (_buffer is null)
{
_buffer = new Bitmap(DrawingPanel.Width, DrawingPanel.Height);
}
timer1.Enabled = true;
await Task.Run(() => DrawToBuffer(_buffer));
timer1.Enabled = false;
DrawingPanel.Invalidate();
}
You'll see the timer there; you add a Timer to the form and set it to match the drawing refresh rate you want; so 25 frames/second would be 40ms. In the timer event, you simply invalidate the panel:
private void timer1_Tick(object sender, EventArgs e)
{
DrawingPanel.Invalidate();
}
Most of your code just moves as-is into the DrawToBuffer() method, and you grab the Graphics from the buffer instead of from the UI element:
private void DrawToBuffer(Image image)
{
using (Graphics graphics = Graphics.FromImage(image))
{
int w = image.Width;
int h = image.Height;
// Your code here
}
}
Now all you need is to change the panel paint event to copy from the buffer:
private void DrawingPanel_Paint(object sender, PaintEventArgs e)
{
if (!(_buffer is null))
{
e.Graphics.DrawImageUnscaled(_buffer, 0, 0);
}
}
Copying the buffer is super-fast; probably uses BitBlt() underneath.
A caveat is you now need to be a little more careful about UI changes. E.g. if it is possible to change the size of your DrawingPanel half-way through a buffer render, you need to cater for that. Also, you need to prevent 2 buffer updates happening simultaneously.
There might be other things you need to cater for; e.g. you might need to DrawImage() instead of DrawImageUnscaled(), etc. I'm not claiming the above code is perfect, just something to give you an idea to work with.
You cannot make it asynchronously, because all drawings have to be made on UI thread.
What you can do is to utilize Application.DoEvents() to signal UI that it can process pending messages:
for (int i = 0; i < Amount; i++)
{
// consider extracting it to a function and separate calculation\logic from drawing
X = random.Next(-(w / 2), (w / 2) + 1);
Y = random.Next(-(h / 2), (h / 2) + 1);
Points++;
if ((X * X) + (Y * Y) < (w / 2 * h / 2)) // and this too
{
InCircle++;
e.Graphics.FillRectangle(Brushes.LimeGreen, X, Y, 1, 1);
}
else
{
// just another one suggestion - you repeat your code, only color changes
e.Graphics.FillRectangle(Brushes.Cyan, X, Y, 1, 1);
}
Thread.Sleep(1); // do you need it? :)
Application.DoEvents(); //
}
It will make your application responsive during drawing.
Read more about Application.DoEvents() here:
Use of Application.DoEvents()
I have created a somewhat complete application which allows me to create a map (.txt file with information about all the points of the map), load it and draw it.
My solution for this was, inside the windows forms application, to put a panel (since I need to be able to move on map) and inside that panel pictureboxes(since I want to put a background image and image on them) that represent points of map in size 50x50 pixels. The problem I am facing now is increased load time for my application, since I am loading pictures into the pictureboxes...
Does anyone have any alternative suggestion to what I have been attempting?
Visual representation might help:
The code, as requested: (well, some of it)
private void Load_Map()
{
for (int i = Y - 12; i < Y + 12; i++)
{
if ((i >= 0) & (i < Int32.Parse(MP.Mheight)))
{
string Line = xline[i];
for (int j = X - 12; j < X + 12; j++)
{
if ((j >= 0) & (j < Int32.Parse(MP.Mwidth)))
{
int X = i * Int32.Parse(MP.Mwidth) + j;
int Z = Int32.Parse(Line[j].ToString());
Map_Location[X] = Z;
Color H = new Color();
Map_Point(j, i, Map_Height(Z, H), 50);
}
}
}
}
}
Creating points:
private void Map_Point(int X, int Y, Color H, int Point_Size)
{
PictureBox MP = new PictureBox();
MP.Name = Map_Coordinates(X, Y);
MP.Size = new Size(Point_Size, Point_Size);
MP.Location = new Point(Y * (Point_Size + 1) + 4, X * (Point_Size + 1) + 4);
MP.BackColor = H;
Control MW = this.Controls["WorldMap"];
MW.Controls.Add(MP);
}
You'll be better off creating a custom control by deriving from System.Windows.Forms.Control and overriding the OnPaint method and doing your own drawing and handling click events yourself.
Using a large number of WinForms controls the way you're doing is an exercise in pain, as WinForms will create a hWnd object for each control, and WinForms doesn't scale too well, unfortunately.
You should be using System.Drawing.Graphics
Here are the MSDN Tutorials for it.
It has a method called DrawImage , which you can use instead of a picture box. For the grid you should be drawing it as a rectangle with a color for the background and vertical/horizontal lines to make the grid.
I'm trying to draw the Mandelbrot fractal, using the following method that I wrote:
public void Mendelbrot(int MAX_Iterations)
{
int iterations = 0;
for (float x = -2; x <= 2; x += 0.001f)
{
for (float y = -2; y <= 2; y += 0.001f)
{
Graphics gpr = panel.CreateGraphics();
//System.Numerics
Complex C = new Complex(x, y);
Complex Z = new Complex(0, 0);
for (iterations = 0; iterations < MAX_Iterations && Complex.Abs(Z) < 2; Iterations++)
Z = Complex.Pow(Z, 2) + C;
//ARGB color based on Iterations
int r = (iterations % 32) * 7;
int g = (iterations % 16) * 14;
int b = (iterations % 128) * 2;
int a = 255;
Color c = Color.FromArgb(a,r,g,b);
Pen p = new Pen(c);
//Tranform the coordinates x(real number) and y(immaginary number)
//of the Gauss graph in x and y of the Cartesian graph
float X = (panel.Width * (x + 2)) / 4;
float Y = (panel.Height * (y + 2)) / 4;
//Draw a single pixel using a Rectangle
gpr.DrawRectangle(p, X, Y, 1, 1);
}
}
}
It works, but it's slow, because I need to add the possibility of zooming. Using this method of drawing it isn't possible, so I need something fast. I tried to use a FastBitmap, but it isn't enough, the SetPixel of the FastBitmap doesn't increase the speed of drawing. So I'm searching for something very fast, I know that C# isn't like C and ASM, but it would be interesting do this in C# and Winforms.
Suggestions are welcome.
EDIT: Mendelbrot Set Zoom Animation
I assume it would be significantly more efficient to first populate your RGB values into a byte array in memory, then write them in bulk into a Bitmap using LockBits and Marshal.Copy (follow the link for an example), and finally draw the bitmap using Graphics.DrawImage.
You need to understand some essential concepts, such as stride and image formats, before you can get this to work.
As comment said put out CreateGraphics() out of the double loop, and this is already a good imrovement.
But also
Enable double buffering
For zooming use MatrixTransformation functions like:
ScaleTransform
RotateTransform
TranslateTransform
An interesting article on CodeProject can be found here. It goes a little bit further than just function calls, by explaining actually Matrix calculus ( a simple way, don't worry), which is good and not difficult to understand, in order to know what is going on behind the scenes.
I've been racking my brain trying to figure out how to animate an effect. This is related to a question I asked on math.stackexchange.com.
https://math.stackexchange.com/questions/91120/equal-division-of-rectangles-to-make-total/
As a side note, I didn't implement the drawing algorithm that was defined on the question above -- instead using my own in order to change the perspective to make it look more condensed.
I've been able to draw a stationary 3d style effect, but I am having trouble wrapping my brain around the logic to make the lines below look like they are coming towards you.
My code is as follows,
List<double> sizes = new List<double>();
private void Form1_Load(object sender, EventArgs e)
{
for (int y = 1; y < 10; y++)
{
double s = ((240 / 2) / y) / 4;
sizes.Add(s);
}
sizes.Add(0);
}
int offset = 0;
private void button1_Click(object sender, EventArgs e)
{
Bitmap b = new Bitmap(320, 480);
Graphics g = Graphics.FromImage(b);
Color firstColor = Color.DarkGray;
Color secondColor = Color.Gray;
Color c = firstColor;
int yOffset = 0;
for(int i = 0; i < sizes.Count; i++)
{
c = (i % 2 == 0) ? firstColor : secondColor;
int y = (int)Math.Round(b.Height - yOffset - sizes[i]);
int height = (int)Math.Round(sizes[i]);
g.FillRectangle(new SolidBrush(c), new Rectangle(0, y + offset, b.Width, height + offset));
yOffset += (int)sizes[i];
}
this.BackgroundImage = b;
offset+=1;
}
Each button click should cause the rectangles to resize and move closer. However, my rectangles aren't growing as they should. My logic draws fine, but simply doesn't work as far as moving goes.
So my question is:
Is there an existing algorithm for this effect that I am not aware of, or is this something pretty simple that I'm over thinking? Any help in correcting my logic or pointing me in the right direction would be very appreciated.
Interesting...
(video of the answer here: http://youtu.be/estq62yz7v0)
I would do it like that:
First, drop all RECTANGLE drawing and draw your effect line by line. Like so:
for (int y=start;y<end;y++)
{
color = DetermineColorFor(y-start);
DrawLine(left, y, right, y, color);
}
This is of course pseudo-code not to be troubled with GDI+ or something.
Everything is clear here, except on how to code DetermineColorFor() method. That method will have to return color of the line at specified PROJECTED height.
Now, on the picture, you have:
you point of view (X) - didn't know how to draw an eye
red line (that's your screen - projection plane)
your background (alternating stripes at the bottom)
and few projecting lines that should help you devise the DetermineColorFor() method
Hint - use triangle similarity to go from screen coordinates to 'bar' coordinates.
Next hint - when you are in 'bar' coordinates, use modulo operator to determine color.
I'll add more hints if needed, but it would be great if you solved this on your own.
I was somehow inspired by the question, and have created a code for the solution. Here it is:
int _offset = 0;
double period = 20.0;
private void timer1_Tick(object sender, EventArgs e)
{
for (int y = Height / 3; y < Height; y++)
{
using (Graphics g = CreateGraphics())
{
Pen p = new Pen(GetColorFor(y - Height / 3));
g.DrawLine(p, 0, y, Width, y);
p.Dispose();
}
}
_offset++;
}
private Color GetColorFor(int y)
{
double d = 10.0;
double h = 20.0;
double z = 0.0;
if (y != 0)
{
z = d * h / (double)y + _offset;
}
double l = 128 + 127 * Math.Sin(z * 2.0 * Math.PI / period);
return Color.FromArgb((int)l, (int)l, (int)l);
}
Experiment with:
d - distance from the eye to the projection screen
h - height of the eye from the 'bar'
period - stripe width on the 'bar'
I had a timer on the form and event properly hooked. Timer duration was 20ms.
Considering that you're talking here about 2D rendering, as much as I understodd, to me it seems that you're gonna to reenvent the wheel. Cause what you need, IMHO; is use Matrix Transformations already available in GDI+ for 2D rendering.
Example of aplying it in GDI+ : GDI+ and MatrixTranformations
For this they use System.Drawing.Drawing2D.Matrix class, which is inside Graphics.
The best ever 2D rendering framework I ever used is Piccolo2D framework which I used with great success in big real production project. Definitely use this for your 2D rendering projects, but first you need to study it little bit.
Hope this helps.
Why does the paint even take so long?
public Form1()
{
InitializeComponent();
SuspendLayout();
double scale = ClientSize.Width / 11;
for (int i = 1; i < 10; i++)
{
for (int j = 1; j < 10; j++)
{
everybox[i - 1, j - 1] = new TextBox
{
Location = new Point((int)(scale * i), (int)(scale * j)),
Size = new Size((int)scale - 2, (int)scale - 2),
Multiline = true
};
Controls.Add(everybox[i - 1, j - 1]);
}
}
ResumeLayout();
}
private void Form1_Paint(object sender, PaintEventArgs e)
{
float scale = ClientSize.Width / 11;
Graphics g = this.CreateGraphics();
int counter = 0;
for (float i = scale; i <= this.ClientSize.Width - scale; i += scale)
{
counter++;
if ((counter - 1) % 3 != 0)
{
g.DrawLine(new Pen(Color.Black), new Point((int)i, (int)scale),
new Point((int)i, ClientSize.Width - (int)scale));
g.DrawLine(new Pen(Color.Black), new Point((int)scale, (int)i),
new Point(ClientSize.Width - (int)scale, (int)i));
}
else
{
g.DrawLine(new Pen(Color.Black, 3f), new Point((int)i, (int)scale),
new Point((int)i, ClientSize.Width - (int)scale));
g.DrawLine(new Pen(Color.Black, 3f), new Point((int)scale, (int)i),
new Point(ClientSize.Width - (int)scale, (int)i));
}
}
}
It is rather annoying, and causes noticeable lag. everybox is a TextBox[9,9] object.
Per my comment, change:
Graphics g = this.CreateGraphics();
to
e.Graphics
Paint can definitely get called alot and if you are getting too many calls, it probably has nothing to do with this bit of code. One thing that would help the performance of this particular bit is to try reducing the amount of work you do...
Graphics g = e.Graphics;
Pen bp = new Pen(Color.Black, 3f);
Point start = new Point(0,0);
Point stop = new Point(0,0);
for (float i = scale; i <= this.ClientSize.Width - scale; i += scale)
{
int iAsInt = (int)i;
int scaleAsInt = (int)scale;
int w = ClientSize.Width;
counter++;
if ((counter - 1) % 3 != 0)
{
start.X = iAsInt;
start.Y = scaleAsInt;
stop.X = iAsInt;
stop.Y = w-scaleAsInt;
g.DrawLine(Pens.Black, start, stop);
start.X = scaleAsInt;
start.Y = iAsInt;
stop.X = w-scaleAsInt;
stop.Y = iAsInt;
g.DrawLine(Pens.Black, start, stop);
// Note: this looks like more work, but it is actually less
// your code still has to make all the assignments in addition to
// newing up the points (and later having to garbage collect them)
}
else
{
// TODO: reuse the start/stop points here
g.DrawLine(bp, new Point(iAsInt, scaleAsInt), new Point(iAsInt, w - scaleAsInt);
g.DrawLine(bp, new Point(scaleAsInt, iAsInt), new Point(w - scaleAsInt, iAsInt));
}
}
To specifically stop the overdrawing of your lines, look at the ClipRectangle member of PaintEventArgs. If part of your line falls within the area of the clip rectangle, redraw it.
A possible reason is because you are trying to draw too many heavy-weight components. If my math is correct you are redrawing 9 * 9 * 9 * 9 = 6561 objects. WinForms are not designed to support redrawing of that many components in the efficient way.
You may need to think if you really need to use that many heavy-weight graphic components with WinForms. There might be lighter components or you can switch to XNA (which has camera, views etc - all of that reduce the number of objects needed to be redrawn) or WPF depending on the context.
The code you posted causes the paint event to fire 81 times (9*9). Once for each control being added to the form. Any more times are due to something that invalidates the form, like the mouse moving over it, another window moving over it, or the form resizing. Some code you aren't showing us may be responsible.