I have a function that draw points on the screen. This function worked very well until I added the line with panelGraphics.RotateTransform. When this line is there, the process of doing one repaint is very long. My list of points contains about 5000 points and without the rotation, it's done in a couple of milliseconds but with that line it could take up to 500 ms which is VERY slow. Do you have any idea why RotateTransform is so slow and also, what can I do to optimize this?
private void panel1_Paint(object sender, PaintEventArgs e)
{
Pen myPen = new System.Drawing.Pen(System.Drawing.Color.Red);
SolidBrush myBrush = new System.Drawing.SolidBrush(System.Drawing.Color.Red);
Graphics panelGraphics = panel1.CreateGraphics();
panelGraphics.TranslateTransform((panel1.Width / 2) + _panW, (panel1.Height / 2) + _panH);
//Problematic line...
panelGraphics.RotateTransform(230 - Convert.ToInt32(_dPan), System.Drawing.Drawing2D.MatrixOrder.Prepend);
PointF ptPrevious = new PointF(float.MaxValue, float.MaxValue);
foreach (PointF pt in _listPoint)
{
panelGraphics.FillRectangle(myBrush, (pt.X / 25) * _fZoomFactor, (pt.Y / 25) * _fZoomFactor, 2, 2);
}
myBrush.Dispose();
myPen.Dispose();
panelGraphics.Dispose();
}
The reason is that each rectangle has to be rotated. Rotation can be a slow operation, especially for none square angles.
The better approach in this case is to first create a "hidden" bitmap which you draw the rectangles into. Then apply the rotation to your main graphics object and draw the hidden bitmap onto your main bitmap (control). Something like this -
private void panel1_Paint(object sender, PaintEventArgs e)
{
Pen myPen = new System.Drawing.Pen(System.Drawing.Color.Red);
SolidBrush myBrush = new System.Drawing.SolidBrush(System.Drawing.Color.Red);
Graphics panelGraphics = e.Graphics; //use the provided Graphics object
// create an internal bitmap to draw rectangles to
Bitmap bmp = new Bitmap(this.ClientSize.Width, _
this.ClientSize.Height, _
Imaging.PixelFormat.Format32bppPArgb);
using (Graphics g = Graphics.FromImage(bmp)) {
PointF ptPrevious = new PointF(float.MaxValue, float.MaxValue);
foreach (PointF pt in _listPoint) {
g.FillRectangle(myBrush, (pt.X / 25) * _fZoomFactor, _
(pt.Y / 25) * _fZoomFactor, 2, 2);
}
}
panelGraphics.TranslateTransform((panel1.ClientSize.Width / 2) + _panW, _
(panel1.ClientSize.Height / 2) + _panH);
//Problematic line...
panelGraphics.RotateTransform(230 - Convert.ToInt32(_dPan), _
System.Drawing.Drawing2D.MatrixOrder.Prepend);
panelGraphics.DrawImage(bmp, 0, 0); //adjust x/y as needed
bmp.Dispose;
myBrush.Dispose();
myPen.Dispose();
}
Related
This question already has an answer here:
Draw Rectangle inside picture box SizeMode Zoom
(1 answer)
Closed 3 years ago.
I have a PictureBox1 with it's sizemode set to Stretch and PictureBox1. The PictureBox1 contains an image and let's me select part of it and then crop it and store the cropped part inside PictureBox2. It works great when the sizemode is set to Stretch and the picture is not zoomed, but not when I zoom it or set the sizemode to zoom.
working example - sizemode set to 'stretch'
The code I use to crop part of the picture (original source)
try
{
float stretch1X = 1f * pictureBox1.Image.Width / pictureBox1.ClientSize.Width;
float stretch1Y = 1f * pictureBox1.Image.Height / pictureBox1.ClientSize.Height;
Point pt = new Point((int)(_mDown.X * stretch1X), (int)(_mDown.Y * stretch1Y));
Size sz = new Size((int)((_mCurr.X - _mDown.X) * stretch1X),
(int)((_mCurr.Y - _mDown.Y) * stretch1Y));
if (sz.Width > 0 && sz.Height > 0)
{
Rectangle rSrc = new Rectangle(pt, sz);
Rectangle rDest = new Rectangle(Point.Empty, sz);
Bitmap bmp = new Bitmap(sz.Width, sz.Height);
using (Graphics G = Graphics.FromImage(bmp))
G.DrawImage(pictureBox1.Image, rDest, rSrc, GraphicsUnit.Pixel);
return bmp;
}
return null;
}
catch (Exception ex)
{
throw ex;
}
How do I calculate it properly? How can I make the crop function work in a way so it lets the user zoom in/out and still crop the correct part of the picture?
You need to calculate the points using the stretch factor and maybe also the offset.
For Zoom there is only one factor as aspect ratio is always the same for Image and PictureBox, but there usually is an offset; for Stretch you need no offset but two factors.
Here is an example that goes all the way using two PictureBoxes two show a zoomed version and the cropped bitmap. It makes use of an all-purpose function ImageArea that determines size and offset.
Two class level variables:
Point pDown = Point.Empty;
Rectangle rect = Rectangle.Empty;
Three mouse events:
private void PictureBox1_MouseDown(object sender, MouseEventArgs e)
{
pDown = e.Location;
pictureBox1.Refresh();
}
private void PictureBox1_MouseMove(object sender, MouseEventArgs e)
{
if (!e.Button.HasFlag(MouseButtons.Left)) return;
rect = new Rectangle(pDown, new Size(e.X - pDown.X, e.Y - pDown.Y));
using (Graphics g = pictureBox1.CreateGraphics())
{
pictureBox1.Refresh();
g.DrawRectangle(Pens.Orange, rect);
}
}
private void PictureBox1_MouseUp(object sender, MouseEventArgs e)
{
Rectangle iR = ImageArea(pictureBox2);
rect = new Rectangle(pDown.X - iR.X, pDown.Y - iR.Y,
e.X - pDown.X, e.Y - pDown.Y);
Rectangle rectSrc = Scaled(rect, pictureBox2, true);
Rectangle rectDest = new Rectangle(Point.Empty, rectSrc.Size);
Bitmap bmp = new Bitmap(rectDest.Width, rectDest.Height);
using (Graphics g = Graphics.FromImage(bmp))
{
g.DrawImage(pictureBox2.Image, rectDest, rectSrc, GraphicsUnit.Pixel);
}
pictureBox2.Image = bmp;
}
Here is a useful function that returns the area of the actual image inside a picturebox for any sizemode..:
Rectangle ImageArea(PictureBox pbox)
{
Size si = pbox.Image.Size;
Size sp = pbox.ClientSize;
if (pbox.SizeMode == PictureBoxSizeMode.StretchImage)
return pbox.ClientRectangle;
if (pbox.SizeMode == PictureBoxSizeMode.Normal ||
pbox.SizeMode == PictureBoxSizeMode.AutoSize)
return new Rectangle(Point.Empty, si);
if (pbox.SizeMode == PictureBoxSizeMode.CenterImage)
return new Rectangle(new Point((sp.Width - si.Width) / 2,
(sp.Height - si.Height) / 2), si);
// PictureBoxSizeMode.Zoom
float ri = 1f * si.Width / si.Height;
float rp = 1f * sp.Width / sp.Height;
if (rp > ri)
{
int width = si.Width * sp.Height / si.Height;
int left = (sp.Width - width) / 2;
return new Rectangle(left, 0, width, sp.Height);
}
else
{
int height = si.Height * sp.Width / si.Width;
int top = (sp.Height - height) / 2;
return new Rectangle(0, top, sp.Width, height);
}
}
We only need the offset to determine the rectangle unscaled. We also need to scale it:
Rectangle Scaled(Rectangle rect, PictureBox pbox, bool scale)
{
float factor = GetFactor(pbox);
if (!scale) factor = 1f / factor;
return Rectangle.Round(new RectangleF(rect.X * factor, rect.Y * factor,
rect.Width * factor, rect.Height * factor));
}
For this need to know the scaling factor, which depends on the aspect ratio:
float GetFactor(PictureBox pBox)
{
if (pBox.Image == null) return 0;
Size si = pBox.Image.Size;
Size sp = pBox.ClientSize;
float ri = 1f * si.Width / si.Height;
float rp = 1f * sp.Width / sp.Height;
float factor = 1f * pBox.Image.Width / pBox.ClientSize.Width;
if (rp > ri) factor = 1f * pBox.Image.Height / pBox.ClientSize.Height;
return factor;
}
This solution will also work if the PictureBox is zoomed in or out by placing it inside a AutoScrolling Panel and changing the Pbox.Size.
Back here. Is there any way to improve the quality of the Arc?
I'm using e.Graphics.SmoothingMode = SmoothingMode.AntiAlias
This is the piece of code that creates the arc:
using (GraphicsPath gp = new GraphicsPath())
{
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
gp.Reset();
gp.AddPie(_OuterRectangle, (float)_Properties.Origin, (float)_Properties.GaugeType);
gp.Reverse();
gp.AddPie(_InnerRectangle, (float)_Properties.Origin, (float)_Properties.GaugeType);
gp.Reverse();
pArea.SetClip(gp);
using (Pen oPen = new Pen(this.ForeColor, 2f))
{
e.Graphics.DrawPath(oPen, gp);
}
e.Graphics.SetClip(ClientRectangle);
}
Thanks in advance.
EDIT:
I've did what LarsTech proposed and now the quality is perfect, but I'm not having the figure I need:
OuterRectangle: is the ClientRectangle area, that I'm manipulating it to make Width and Height the same lenght;
InnerRectangle: is 2/3ths of the ClientRectangle area, ergo, of the OuterRectangle;
Properties.Origin: is the angle where the arc starts. I have it in an enumerator as Cardinal Points, where North is 270, East is 0,
and so. In case of the figure, is SouthWest, 135 degrees;
Properties.GaugeType: is another enumerator that says if is Complete = 360, Half = 180, Quarter = 90, so with that I can determine the sweep angle. In case of the figure is ThreeQuarter, 270 degrees.
The problem:
When clipping a region of the current Graphics (Graphics.SetClip method), the resulting drawing loses quality, because the antialiasing effect generated by Graphics.SmoothingMode = SmoothingMode.AntiAlias is lost.
A possible solution is to avoid clipping the region defined by the GraphicsPath used to design the arcs (GraphicsPath.AddPie method); this, however, leaves the lines of the Pie visible, compromising the shape.
Another solution is to draw an ellipsis in the center of the arcs using the background color of the Canvas. Since the arcs are drawn using two rectangles, we can use the inner rectagle, inflate it (Rectangle.Inflate method) as needed (a fraction - Pen.Width / 2 - of the Pen size used for the ouline, usually).
This allows to delete the artifacts generated by the GraphicsPath shapes and to draw some other graphics content in the center of the shapes.
For example, using different Brushes:
LinearGradientBrush HatchBrush TextureBrush
Of course there are other methods to achieve the same result. We could draw the Arcs using the GraphicsPath.AddArc method, extract or calculate the first and last points of the Arcs and use them to draw two lines (GraphicsPath.AddLine) that will close the figures.
But, since we want to draw different graphics objects in the center of the arcs, these objects will cover the center area anyway.
How to use this code:
In a Form, add a TrackBar (named tbarSpeed, here)
Add a PictureBox (named Canvas), with Size (200, 200).
Wire up the TrackBar tbarSpeed_Scroll event and the Panel Canvas_Paint event.
using System.Drawing;
using System.Drawing.Drawing2D;
float GaugeValue = 88.0f;
float GaugeSweepAngle = 270.0f;
float GaugeStartAngle = 135.0F;
private void Canvas_Paint(object sender, PaintEventArgs e)
{
var canvas = sender as Control;
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
var outerRectangle = new Rectangle(10, 10, 180, 180);
var innerRectangle = new Rectangle(30, 30, 140, 140);
var blendRectangle = new Rectangle(10, 10, 180, 160);
var innerCenter = new PointF(outerRectangle.Left + (outerRectangle.Width / 2),
outerRectangle.Top + (outerRectangle.Height / 2));
float gaugeLength = (outerRectangle.Width / 2) - 2;
using (var path = new GraphicsPath())
{
path.AddPie(outerRectangle, GaugeStartAngle, GaugeSweepAngle);
path.AddPie(innerRectangle, GaugeStartAngle, GaugeSweepAngle);
innerRectangle.Inflate(-1, -1);
using (var pen = new Pen(Color.White, 3f))
using (var backgroundbrush = new SolidBrush(canvas.BackColor))
using (var gradientBrush = new LinearGradientBrush(blendRectangle,
Color.Green, Color.Red, LinearGradientMode.ForwardDiagonal))
{
var blend = new Blend()
{
Factors = new[] { 0.0f, 0.0f, 0.1f, 0.3f, 0.7f, 1.0f },
Positions = new[] { 0.0f, 0.2f, 0.4f, 0.6f, 0.8f, 1.0f }
};
gradientBrush.Blend = blend;
e.Graphics.FillPath(gradientBrush, path);
e.Graphics.DrawPath(pen, path);
e.Graphics.FillEllipse(backgroundbrush, innerRectangle);
using (var format = new StringFormat())
{
format.Alignment = StringAlignment.Center;
format.LineAlignment = StringAlignment.Center;
innerRectangle.Location = new Point(innerRectangle.X, innerRectangle.Y + canvas.Font.Height);
e.Graphics.DrawString(GaugeValue.ToString() + "%", canvas.Font, Brushes.White, innerRectangle, format);
}
using (var mx = new Matrix())
{
mx.RotateAt(GaugeStartAngle + 90 + (GaugeValue * (GaugeSweepAngle / 100)), innerCenter);
e.Graphics.Transform = mx;
e.Graphics.DrawLine(pen, innerCenter, new PointF(innerCenter.X, innerCenter.Y - gaugeLength));
e.Graphics.ResetTransform();
}
}
}
}
private void tbarSpeed_Scroll(object sender, EventArgs e)
{
GaugeValue = tbarSpeed.Value;
Canvas.Invalidate();
}
Sample code on PasteBin
I have a Label and I am trying to draw an inner circle (not filled) inside this Label, as big as possible.
I have tried two methods, one method applied to label1 and another one applied to label2. In both cases it does not work.
Note: the Label should keep its background color and content.
How can I get rid of this?
Code:
void DrawCircle1(Graphics g, Point centerPos, int radius, int cutOutLen)
{
RectangleF rectangle = new RectangleF(centerPos.X, centerPos.Y,
radius * 2,
radius * 2
);
// calculate the start angle
float startAngle = (float)(Math.Asin(
1f * (radius - cutOutLen) / radius) / Math.PI * 180);
using (System.Drawing.Drawing2D.GraphicsPath path = new System.Drawing.Drawing2D.GraphicsPath())
{
path.AddArc(rectangle, 180 - startAngle, 180 + 2 * startAngle);
path.CloseFigure();
//g.FillPath(Brushes.Yellow, path);
using (Pen p = new Pen(Brushes.Yellow))
{
g.DrawPath(new Pen(Brushes.Blue, 2), path);
}
}
}
private void DrawCircle2(PaintEventArgs e)
{
Label tempLabel = label2;
using (System.Drawing.SolidBrush myBrush = new System.Drawing.SolidBrush(System.Drawing.Color.Red))
{
using (System.Drawing.Pen myPen = new Pen(myBrush, 2))
{
e.Graphics.DrawEllipse(myPen, new System.Drawing.Rectangle(tempLabel.Location.X, tempLabel.Location.Y,
tempLabel.Width, tempLabel.Height));
}
}
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
DrawCircle1(e.Graphics, new Point(label1.Width/2, label1.Height/2), 10, 50);
DrawCircle2(e);
}
Below a screenshot:
You are drawing on Form not the Label. Instead of overriding OnPaint method of the Form, try to handle Paint event of Label controls. For example:
private void label1_Paint(object sender, PaintEventArgs e)
{
e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
e.Graphics.DrawEllipse(Pens.Red, 0, 0, label1.Height - 1, label1.Height - 1);
}
Same procedures you're using now, calling them from a control's Paint() event.
It's the same if you're creating a Custom control. Use the overridden OnPaint() event in this case.
In the control's Paint() event, call one/more methods to draw a shape on the control's surface.
private void label1_Paint(object sender, PaintEventArgs e)
{
DrawCircle1(e.Graphics, label1.ClientRectangle);
}
private void label2_Paint(object sender, PaintEventArgs e)
{
DrawCircle2(e.Graphics, label2.ClientRectangle);
}
Use the Control's ClientRectangle bounds to derive the size of the figure.
Here, the ClientRectangle is reduced by 1 when using Graphics.DrawEllipse() and by 2 when using Graphics.DrawPath(). The two methods calculate the pen size in relation to the drawing area in a slightly different manner.
private void DrawCircle1(Graphics g, RectangleF canvas)
{
canvas.Inflate(-2, -2);
g.SmoothingMode = SmoothingMode.AntiAlias;
using (GraphicsPath path = new GraphicsPath())
using (Pen p = new Pen(Color.Blue, 2)) {
path.StartFigure();
path.AddArc(canvas, 0, 360);
path.CloseFigure();
g.DrawPath(p, path);
}
}
private void DrawCircle2(Graphics g, RectangleF canvas)
{
canvas.Inflate(-1, -1);
g.SmoothingMode = SmoothingMode.AntiAlias;
using (Pen p = new Pen(Color.Red, 2)) {
g.DrawEllipse(p, canvas);
}
}
I have a list of Points that have been drawn on pictureBox1.
pictureBox1 has been transformed.
Now, I want to get XY coordinates of the point that was drawn as I hover over any drawn point.
When I hover over the pictureBox1, I am getting the XY of the pictureBox -- not a transformed XY.
Can you help me get to the transformed XY?
Thanks
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
int height = pictureBox1.ClientSize.Height / 2;
int width = pictureBox1.ClientSize.Width / 2;
//=====
//scale
//=====
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
e.Graphics.TranslateTransform(-width, -height);
e.Graphics.ScaleTransform(2f, 2f);
//===========
//draw center
//===========
e.Graphics.DrawLine(new Pen(Color.Black, 0.5f), new Point(width - 2, height), new Point(width + 2, height));
e.Graphics.DrawLine(new Pen(Color.Black, 0.5f), new Point(width, height - 2), new Point(width, height + 2));
//===========
//draw points
//===========
foreach (var p in Points)
{
Point[] pts = new Point[] { new Point(p.X, p.Y) };
Rectangle rc = new Rectangle(pts[0], new Size(1, 1));
e.Graphics.DrawRectangle(Pens.Red, rc);
}
}
As a variation to #Vitaly's answer you can do this:
After transforming the Graphics object you can save its transformation matrix e.Graphics.Transform in a variable:
Matrix matrix = null;
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
int height = pictureBox1.ClientSize.Height / 2;
int width = pictureBox1.ClientSize.Width / 2;
//=====
//scale
//=====
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
e.Graphics.TranslateTransform(-width, -height);
e.Graphics.ScaleTransform(2f, 2f);
matrix = e.Graphics.Transform; // save the transformation matrix!
...
This is necessary as the transfomation data are lost after the Paint event!
Note that the GraphicsState graphics.Save()&Restore() functions can't be used very well for this purpose, as it only puts the state on the stack for using it once, meaning it doesn't save these data in a persistent way.
Later you can use the Matrix and this function to either transform Points with the same matrix or reverse the transformation, e.g. for mouse coordinates:
PointF transformed(Point p0, bool forward)
{
Matrix m = matrix.Clone();
if (!forward) m.Invert();
var pt = new Point[] { p0 };
m.TransformPoints(pt);
return pt[0];
}
Now my MouseMove event shows the location both raw and re-transformed:
private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
{
label1.Text = e.Location + " <-> " + transformed(e.Location, false) ;
}
And to test the forward transformation you could add this to the end of the Paint event:
e.Graphics.ResetTransform();
for (int i = 0; i < Points.Count; i++)
{
Point[] pts = new Point[] { Point.Round(transformed(Points[i], true)) };
Rectangle rc = new Rectangle(pts[0], new Size(19, 19));
e.Graphics.DrawRectangle(Pens.Red, rc);
}
This first clears all the transformations and then paints larger Rectangles at the same locations by calling the transformed function.
Note that this will also work with a rotated Graphics object. (Although the last test does not draw the larger rectangles rotated, just moved to the right locations.)
Also note that I return PointF for better precision when scaling with fractions. You can use Point.Round (or Point.Truncate) to get Point.
Do have a look the the Matrix.Elements: They contain the numbers you have used:
float scaleX = matrix.Elements[0];
float scaleY = matrix.Elements[3];
float transX = matrix.Elements[4];
float transY = matrix.Elements[5];
Finally: It is well worth studying the many methods of Matrix..!
You can create a Matrix with necessary transformations and apply it in pictureBox1_Paint(...) via MultiplyTransform(...):
https://msdn.microsoft.com/en-us/library/bt34tx5d(v=vs.110).aspx
Then you can use Matrix::TransformPoints(...) to get transformed XY
I wanted to draw thick lines using Graphics.Lines() method. But it looks like the API has some bugs. If you try to render the user control with the following code, you would get weird looking image. I was wondering if there is some smoothing mode or something similar that could take care of this line drawing glitch.
private void UserControl1_Paint(object sender, PaintEventArgs e)
{
int n = 100;
Point[] points = new Point[n];
double x = 2;
int y = 50;
for (int i = 0; i < n; i++)
{
Point p = new Point();
p.X = 200 + (int)(i * x);
p.Y = 200 + (int)(Math.Sin(i * 0.2) * y);
points[i] = p;
}
Pen pen = new Pen(new SolidBrush(Color.Blue));
//Pen pen = new Pen(new LinearGradientBrush(new Point(0, 0), new Point(0, 100), Color.Black, Color.Red));
pen.Width = 200;
e.Graphics.DrawLines(pen, points);
}
You see the effect of GDI+ trying to draw end-caps on the line. That's not going to come to a good end with such a thick pen. About what you'd imagine from daVinci painting the Mona Lisa with a broom. Fix:
Pen pen = new Pen(new SolidBrush(Color.Blue));
pen.EndCap = System.Drawing.Drawing2D.LineCap.Square;
pen.StartCap = System.Drawing.Drawing2D.LineCap.Square;
Or draw a polygon instead so that GDI+ has a better idea what is front and back:
e.Graphics.DrawPolygon(pen, points);
Well, it doesn't look like a devil anymore. Keep the line width proportional to the details in the line.
Here is the result of your code drawing using a pen of width 200 (pixels):
And here it is at a width of 2:
The pen width property is usually pixels, but it is based on the Graphics object's PageUnit property (itself a GraphicsUnit property). Check to make sure you've set these values to what you want.