Slow adding of controls to a form - c#

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.

Related

C# - asynchronous drawing on a panel

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()

Refresh the canvas only for certain brushes

I'm trying to graph some circles and lines etc but I only want some lines to refresh on the canvas and the others not to, is there any way around this?
For example the mypen, mypen2 and mypen3, I want them to refresh on canvas but the graphics "g" a little further down I don't want to refresh, I want all the instances to show. How do I do this? Here is my code
private void drawlines()
{
canvas.Refresh();
int j = Int32.Parse(ivalue.Text);
float position1 = canvas.Width / 2;
float position2 = canvas.Height / 2;
float XX = (float)(Math.Round(position1 + Math.Sin(DegreeToRadian(j)) * 100));
float XY = (float)(Math.Round(position2 - Math.Cos(DegreeToRadian(j)) * 100));
float X2 = (position1 + XX);
float XY2 = XY;
System.Drawing.Pen myPen;
System.Drawing.Pen myPen2;
System.Drawing.Pen myPen3;
System.Drawing.Pen myPen4;
myPen = new System.Drawing.Pen(System.Drawing.Color.Red);
myPen2 = new System.Drawing.Pen(System.Drawing.Color.Blue);
myPen3 = new System.Drawing.Pen(System.Drawing.Color.Black);
myPen4 = new System.Drawing.Pen(System.Drawing.Color.Green);
System.Drawing.Graphics formGraphics = canvas.CreateGraphics();
formGraphics.DrawRectangle(myPen,XX, XY,3,3);
formGraphics.DrawRectangle(myPen2, canvas.Width / 2, XY, 3, 3);
formGraphics.DrawRectangle(myPen3, position1, position2, 3, 3);
formGraphics.DrawRectangle(myPen4, position1, XY2, 3, 3);
label1.Text = Convert.ToString(XY);
label1.Refresh();
listBox1.Items.Clear();
listBox1.Items.Add("XX=[" + XX + "] XY=[" + XY + "]");
}
private void Go_Click(object sender, EventArgs e)
{
for (int i = 0; i <= 360; i = i + 1)
{
drawlines();
int linearm = (canvas.Width / 2) - i;
ivalue.Text = Convert.ToString(i);
ivalue.Refresh();
int testx = Int32.Parse(label1.Text);
Graphics g;
g = canvas.CreateGraphics();
Pen p;
Rectangle r;
p = new Pen(Brushes.Green);
r = new Rectangle(linearm,testx, 1, 1);
g.DrawRectangle(p, r);
System.Threading.Thread.Sleep(15);
}
}
I assume you are using winforms? If so you need to change your code to work like this:
To be persistant everything need to be drawin in the Paint event and using its e.Graphics object. (This is the Golden Rule! Corollary: Never use System.Drawing.Graphics formGraphics = canvas.CreateGraphics();)
Everything you want to be drawn must be stored in Lists of classes, sufficient to hold all info you need.
If you were to draw only Rectangles in only one pen a List<Rectangle> would be enough, but for other shapes and pens you will want to create a class to hold those data.
Now you can:
Draw them all in the Paint event, iterating the List<your DrawItemClass>
Remove or set inactive those items in the List you don't want to be drawn any longer..

Initiate panel with drawings

I am now drawing to a panel some dots to indicate a sort of dotted grid with 1% of margin of total panel width.
This is what I am doing now:
private void panel1_Paint(object sender, PaintEventArgs e)
{
Pen my_pen = new Pen(Color.Gray);
int x,y;
int k = 1 ,t = 1;
int onePercentWidth = panel1.Width / 100;
for (y = onePercentWidth; y < panel1.Height-1; y += onePercentWidth)
{
for (x = onePercentWidth; x < panel1.Width-1; x += onePercentWidth)
{
e.Graphics.DrawEllipse(my_pen, x, y, 1, 1);
}
}
}
What is bothering me is that when the app starts I can see the dots being drawn on the panel. Even if it is very quick it still bothers me a lot.
Is it possible to draw the dots on the panel and load it directly drawn?
Thank you for the help
You could create a bitmap and draw it instead.
But before you do that: DrawEllipse is a little expensive. Use DrawLine with a Pen that has a dotted linestyle instead:
int onePercentWidth = panel1.ClientSize.Width / 100;
using (Pen my_pen = new Pen(Color.Gray, 1f))
{
my_pen.DashStyle = System.Drawing.Drawing2D.DashStyle.Custom;
my_pen.DashPattern = new float[] { 1F, onePercentWidth -1 };
for (int y = onePercentWidth; y < panel1.ClientSize.Height - 1; y += onePercentWidth)
e.Graphics.DrawLine(my_pen, 0, y, panel1.ClientSize.Width, y);
}
Note that I am using using so I don't leak the Pen and ClientSize so I use only the inner width. Also note the exaplanation about the custom DashPattern on MSDN

C# Graphics redrawing

I'm writing a little game using C#. In fact, it just have to draw corolful circles every time user moves a mouse (or just every n milliseconds) according to the mouse location. The problem is, i have to redraw the whole pictureBox every single period of time. I know there are .Invalidate() and .Refresh() options for that, but it seems like I need to re-create Graphics object every single time I need to redraw something, that happens every second.
private void redrawCircles(int distance)
{
prevdist = distance / 5;
g = Graphics.FromImage(pictureBox1.Image);
for (int i = 0; i < n - 1; i++)
{
brushes[i] = brushes[i + 1];
}
brushes[n - 1] = BrushFromDistance(distance);
for (int i = 0; i < n; ++i)
{
g.FillEllipse(brushes[i], startX + i * rad, startY + i * rad, 2 * diag - 2 * i * rad, 2 * diag - 2 * i * rad);
}
g.Dispose();
pictureBox1.Refresh();
}
where g is:
public static System.Drawing.Graphics g;
redrawCircles is called in the MouseMove event handler, and i'm planning to cal it in Timer.Tick event handler. So it's called very often. Re-creating Graphics object seems not effective.
Do I really need re-creating Graphics object in that situation or there is an easier way?

Draw types of signals with changing amplitude and frequency on-the-fly

As title said:
I have form with 2 trackbars. One for frequency and one for amplitude. I set up timer for on-the-fly changing.
private void timer1_Tick(object sender, EventArgs e)
{
float amplitude, frequency;
amplitude = Convert.ToSingle(trackBar1.Value) / 100;
label1.Text = amplitude.ToString() + " V";
frequency = trackBar2.Value;
label2.Text = frequency.ToString() + " Hz";
}
I have also 4 radio-buttons to decide, which type of signal will be displayed (sine, square, triangle, sawthoot)
Now I have this implemented with ImageList (change image of signal).
How can I draw type of signal and regulate it with with trackbars? So it will be like in osciloscope.
Thanks for your answers and code.
Lets start by creating the different signal types, this is a function that creates one wavelength of amplitude 1:
private PointF[] CreateBaseSignal(SignalType signalType)
{
switch (signalType)
{
case SignalType.Sine:
const int oversampling = 32;
PointF[] signal = new PointF[oversampling];
for (int i = 0; i < signal.Length; i++)
{
signal[i].X = (float) i / oversampling;
signal[i].Y = Convert.ToSingle(Math.Sin((double) i / oversampling * 2 * Math.PI));
}
return signal;
case SignalType.Square:
return new PointF[]
{
new PointF(0.0f, -1.0f),
new PointF(0.5f, -1.0f),
new PointF(0.5f, 1.0f),
new PointF(1.0f, 1.0f),
};
case SignalType.Triangle:
return new PointF[]
{
new PointF(0.0f, -1.0f),
new PointF(0.5f, 1.0f),
};
case SignalType.Sawtooth:
return new PointF[]
{
new PointF(0.0f, -1.0f),
new PointF(1.0f, 1.0f),
};
default:
throw new ArgumentException("Invalid signal type", "signalType");
}
}
Then we create the actual signal with the selected amplitude and frequency:
private PointF[] CreateSignal(PointF[] baseSignal, float frequency, float amplitude)
{
PointF[] signal = new PointF[Convert.ToInt32(Math.Ceiling(baseSignal.Length * frequency))];
for(int i = 0; i < signal.Length; i++)
{
signal[i].X = baseSignal[i % baseSignal.Length].X / frequency + (i / baseSignal.Length) / frequency;
signal[i].Y = baseSignal[i % baseSignal.Length].Y * amplitude;
}
return signal;
}
Before attempting to plot this signal to a PictureBox, we scale the signal to fit the width and height:
private PointF[] ScaleSignal(PointF[] signal, int width, int height)
{
const float maximumAmplitude = 10.0f;
PointF[] scaledSignal = new PointF[signal.Length];
for(int i = 0; i < signal.Length; i++)
{
scaledSignal[i].X = signal[i].X * width;
scaledSignal[i].Y = signal[i].Y * height / 2 / maximumAmplitude;
}
return scaledSignal;
}
Using Graphics.DrawLine to plot the signal is way better than Bitmap.SetPixel, since the data points will be connected even at high frequencies. Bitmap.SetPixel is also very slow, you really need to use Bitmap.LockBits and unsafe code for manipulating single pixels to achieve any decent performance. Using Graphics.DrawLine, you also have control over line width, anti-aliasing etc.
Since we have stored the signal in a PointF array, we can use the simple Graphics.DrawLines method to plot the signal instead of iterating over the data points:
private void PlotSignal(PointF[] signal, PictureBox pictureBox)
{
Bitmap bmp = new Bitmap(pictureBox.ClientSize.Width, pictureBox.ClientSize.Height);
signal = ScaleSignal(signal, bmp.Width, bmp.Height); // Scale signal to fit image
using(Graphics gfx = Graphics.FromImage(bmp))
{
gfx.SmoothingMode = SmoothingMode.HighQuality;
gfx.TranslateTransform(0, bmp.Height / 2); // Move Y=0 to center of image
gfx.ScaleTransform(1, -1); // Make positive Y axis point upward
gfx.DrawLine(Pens.Black, 0, 0, bmp.Width, 0); // Draw zero axis
gfx.DrawLines(Pens.Blue, signal); // Draw signal
}
// Make sure the bitmap is disposed the next time around
Image old = pictureBox.Image;
pictureBox.Image = bmp;
if(old != null)
old.Dispose();
}
If you redraw the signal often, you probably want to reuse the the Bitmap and Graphics objects instead of creating new ones each time. Just remember to call Graphics.Clear between each redraw.
Putting everything together in one big statement:
PlotSignal(
CreateSignal(
CreateBaseSignal(signalType),
frequency,
amplitude),
thePictureBox);
If you're after a fast plotting library, I really like Dynamic Data Display
Dynamic Data Display
This is a WPF component, but for fast, smooth drawing applications I really think it is worthwhile to port to WPF sooner rathar than later. It feels like you're not too far into your project at the moment anyway.
Development for WPF seems to have stopped for this component (although it continues to be worked on for Silverlight). The documentation is terrible but the source code is available from the link above so you can extend it as needed (it's quite well written and very extensible) and the source is invaluable as a substitute for the near complete lack of any documentation.
Assuming you want to plot a sin wave on a picture box control, create a picture box control on your form then perform the following:
int width = pictureBox1.Width;
int height = pictureBox1.Height;
Bitmap b = new Bitmap(width, height);
for (int i = 0; i < width; i++)
{
int y = (int)((Math.Sin((double)i * 2.0 * Math.PI / width) + 1.0) * (height - 1) / 2.0);
b.SetPixel(i, y, System.Drawing.Color.Red);
}
pictureBox1.Image = b;

Categories