Incorrect scaling of Pen when using Graphics.ScaleTransform - c#

I'm seeing strange behaviour when drawing a line with a scale transform (Graphics.ScaleTransform() - see MSDN) in my OnPaint() method.
When using a large y-scale factor for the ScaleTransform method, then if the x-scale is set above 1x, the line suddenly becomes much larger.
Setting the width of pen with which the line is drawn to -1 seems to get round the problem, but I do not want to draw a very thin line (the line must be printed later, 1px is too thin).
Here's some sample code to demonstrate the problem:
public class GraphicsTestForm : Form
{
private readonly float _lineLength = 300;
private readonly Pen _whitePen;
private Label _debugLabel;
public GraphicsTestForm()
{
ClientSize = new Size(300, 300);
Text = #"GraphicsTest";
SetStyle(ControlStyles.ResizeRedraw, true);
_debugLabel = new Label
{
ForeColor = Color.Yellow,
BackColor = Color.Transparent
};
Controls.Add(_debugLabel);
_lineLength = ClientSize.Width;
_whitePen = new Pen(Color.White, 1f); // can change pen width to -1
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
float scaleX = ClientSize.Width / _lineLength;
const int ScaleY = 100;
e.Graphics.Clear(Color.Black);
_debugLabel.Text = #"x-scale: " + scaleX;
// scale the X-axis so the line exactly fits the graphics area
// scale the Y-axis by scale factor
e.Graphics.ScaleTransform(scaleX, ScaleY);
float y = ClientSize.Height / (ScaleY * 2f);
e.Graphics.DrawLine(_whitePen, 0, y, _lineLength, y);
e.Graphics.ResetTransform();
}
}
I would like the line/pen to scale gracefully, without jumping in size so dramatically.
(Additionally, I noticed that when the line is very large, it is not drawn continuously across multiple monitors. Perhaps this is related?)

Try to change the pen width according to the scale:
_whitePen = new Pen(Color.White, 1f / ScaleY);
e.Graphics.DrawLine(_whitePen, 0, y, _lineLength, y);

I just compensated for the overall scaling in the pens line geometry;-
m_Pen->SetWidth(1.0f);
m_Pen->ScaleTransform(1.0f / ZoomX, 1.0f / ZoomY);

Related

How to draw a circle around the other distance in the image ? or any distance?

public void DrawLine(PictureBox pb, Graphics g)
{
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
g.DrawEllipse(new Pen(Color.Red, 2f), 0, 0, pb.Size.Width, pb.Size.Height);
}
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
DrawLine(pictureBox1, e.Graphics);
}
the result is the red circle at the range a bit before of the 300 kilometers.
but what if i want to make the circle to be drawn on the circle of the 200 kilometers or even the inner circle the smaller one(100 kilometers) ?
how to calculate where to draw the circle and what size ?
the image size is 512x512
I added another cone called it innerOuterRect but how do i make it shorter or longer depending on kilometers distance from the center ?
later i want to add a textBox and when i enter kilometers for example 100 it will move the cone size to 100 kilometers radius and if change it to 21.3 kilometers so the cone and so on.
so the first cone will stay the same and the second one to be changed in kilometers.
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
var center = new PointF(pictureBox1.Width / 2.0f, pictureBox1.Height / 2.0f);
RectangleF outerRect = pictureBox1.ClientRectangle;
RectangleF innerOuterRect = pictureBox1.ClientRectangle;
outerRect.Inflate(-(radarThickness / 2.0f), -(radarThickness / 2.0f));
innerOuterRect.Inflate(-(radarThickness / 2.0f), -(radarThickness / 2.0f));
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
var pathOuter = new GraphicsPath();
var pathInner = new GraphicsPath();
pathOuter.AddEllipse(outerRect);
pathOuter.AddEllipse(innerOuterRect);
pathInner.StartFigure();
pathInner.AddArc(outerRect, coneRotationAngle, coneSweepAngle);
//pathInner.AddArc(innerOuterRect, coneRotationAngle, coneSweepAngle);
var arcPoints = pathInner.PathPoints;
PointF first = arcPoints[0];
PointF last = arcPoints[arcPoints.Length - 1];
pathInner.AddLines(new[] { center, last, center, first });
pathInner.CloseFigure();
var outerPen = new Pen(Color.FromArgb(100, Color.Red), radarThickness);
var innerBrush = new LinearGradientBrush(
center, first, Color.FromArgb(200, Color.Orange), Color.FromArgb(20, Color.Orange));
e.Graphics.FillPath(innerBrush, pathInner);
e.Graphics.DrawPath(outerPen, pathOuter);
}
To determine the scale between the outer region and the region that defines the cone of the Radar, you can take the maximum size of the outer region. For example:
The outer region's maximum size is 300km:
float radarSizeMeters = 300.0f * 1000f;
The cone's region is a third of the outer region or a specific size:
float radarConeSizeMeters = radarSizeMeters / 3.0f;
// Or use a fixed size, depending on the use case
float radarConeSizeMeters = 10.873f * 1000;
Then the scale is a value between .0f and 1.0f:
float radarConeScale = radarConeSizeMeters / radarSizeMeters;
Having determined the relation between the outer and inner regions, you then use this scale factor to scale the graphic elements that define the two regions:
var outerRegion = [Canvas].ClientRectangle;
var innerRegion = new RectangleF(0, 0,
outerRegion.Width * radarConeScale, outerRegion.Height * radarConeScale
);
Then set the new location, based on the calculated size.
The center point is known in advance and doesn't change:
var center = new PointF(canvas.Width / 2.0f, canvas.Height / 2.0f);
// [...]
innerRegion.Location = new PointF(
center.X - innerRegion.Width / 2.0f,
center.Y - innerRegion.Height / 2.0f
);
In the sample code I'm doing the same thing, but using RectangleF.Inflate(), to deflate the inner region by half of the calculated measure.
RectangleF.Inflate() resizes the rectangle proportionally and also moves the Location
You can specify:
radarSizeMeters, which represents the size in meters of the whole region
radarConeSizeMeters, represents the size in meters of the cone's region
coneSweepAngle, which defines the amplitude of the visible cone
radarSpeed is the speed of the rotation. To set the rounds per minute (RPM):
radarSpeed = (360.0f * [Rounds per minute]) / 60 / (1000 / [Timer Interval]);
with [Timer Interval] = 100 (approximated)
Note that I'm targeting .NET 7 and the language version is C# 11, nullable enabled. For example, this:
PointF last = arcPoints[^1]; is the same as PointF last = arcPoints[arcPoints.Length - 1];
=> You must declare the GraphicsPaths, Pens and Brushes with using statements.
Must, not should
public partial class frmRadar : Form {
System.Windows.Forms.Timer radarTimer = new System.Windows.Forms.Timer();
float coneSweepAngle = 36.0f;
float coneRotationAngle = .0f;
float radarSpeed = 1.98f; // ~3 RPM, based on Timer.Interval
float radarThickness = 5.0f;
float radarSizeMeters = 32.620f * 1000;
float radarConeSizeMeters = 10.873f * 1000;
Color radarConeColor = Color.Orange;
public frmRadar()
{
InitializeComponent();
radarTimer.Interval = 100;
radarTimer.Tick += RadarTimer_Tick;
}
private void RadarTimer_Tick(object? sender, EventArgs e)
{
coneRotationAngle += radarSpeed;
coneRotationAngle %= 360.0f;
canvas.Invalidate();
}
private void canvas_Paint(object? sender, PaintEventArgs e)
{
var center = new PointF(canvas.Width / 2.0f, canvas.Height / 2.0f);
RectangleF outerRect = canvas!.ClientRectangle;
outerRect.Inflate(-(radarThickness / 2.0f), -(radarThickness / 2.0f));
using var pathRadarOuterRegion = new GraphicsPath();
using var pathConeRegion = new GraphicsPath();
pathRadarOuterRegion.AddEllipse(outerRect);
float radarConeScale = radarConeSizeMeters / radarSizeMeters;
SizeF radarConeScaleSize = new(
(outerRect.Width - (outerRect.Width * radarConeScale)) / -2.0f,
(outerRect.Height - (outerRect.Height * radarConeScale)) / -2.0f);
var coneRect = outerRect;
coneRect.Inflate(radarConeScaleSize);
pathConeRegion.AddArc(coneRect, coneRotationAngle, coneSweepAngle);
var arcPoints = pathConeRegion.PathPoints;
PointF first = arcPoints[0];
PointF last = arcPoints[^1];
pathConeRegion.AddLines(new[] { last, center, first, center });
pathConeRegion.CloseFigure();
using var outerPen = new Pen(Color.FromArgb(100, Color.Red), radarThickness);
using var innerBrush = new LinearGradientBrush(
center, first, Color.FromArgb(200, radarConeColor), Color.FromArgb(20, radarConeColor));
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
e.Graphics.FillPath(innerBrush, pathConeRegion);
e.Graphics.DrawPath(outerPen, pathRadarOuterRegion);
}
}

How can I fill the circular sector of an elliptic shape with a color gradient?

What I want to do is to create this rotating cone visual effect.
I had previously used DirectX for that.
What i have tried so far:
Even if I'm changing the thickness to 50 or more, the Arc is still not filled.
public partial class Form1 : Form
{
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
var g = e.Graphics;
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
var center = new Point(pictureBox1.Width / 2, pictureBox1.Height / 2);
var innerR = 30;
var thickness = 20;
var startAngle = 0;
var arcLength = 360;
var outerR = innerR + thickness;
var outerRect = new Rectangle
(center.X - outerR, center.Y - outerR, 2 * outerR, 2 * outerR);
var innerRect = new Rectangle
(center.X - innerR, center.Y - innerR, 2 * innerR, 2 * innerR);
using (var p = new GraphicsPath())
{
p.AddArc(outerRect, startAngle, arcLength);
p.AddArc(innerRect, startAngle + arcLength, -arcLength);
p.CloseFigure();
e.Graphics.FillPath(Brushes.Green, p);
e.Graphics.DrawPath(Pens.Green, p);
}
}
}
I want to be able to fill the arc even when the thickness is 20 or less.
Or when the value of the innerR radius changes.
The goal is to be able to fill the arc in any case.
Here's one method of drawing that cone.
It looks like a Radar sweep, so you may want to define the sweep angle and the rotation speed (how much the current rotation angle is increased based on the Timer's interval).
Using a standard System.Windows.Forms.Timer to invalidate the Canvas that contains the Image you're showing here.
The Radar contour (the external perimeter) is centered on the canvas and drawn in relation to the thickness specified (so it's always sized as the canvas bounds). It doesn't necessarily be a perfect circle, it can be elliptical (as in the image here)
The Cone section is drawn adding an Arc to a GraphicsPath and is closed drawing two lines, from the center point of the outer GraphicsPath to the starting and ending points of the Arc (I think this is a simple method to generate a curved conic figure, it can be used in different situations and lets you generate different shapes almost without calculations, see the code about this)
It's filled with a LinearGradientBrush, the section near the center has less transparency than the section near the border; adjust as required
Each time the rotation angle reaches 360°, it's reset to 0.
This is delegated to the Timer's Tick event handler
=> Built with .Net 7, but if you need to adapt it to .Net Framework, the only things to change are the syntax of the using blocks, remove the null-forgiving operator from here: canvas!.ClientRectangle and nullable reference types (e.g., change object? to just object)
public partial class SomeForm : Form {
public SomeForm() {
InitializeComponent();
radarTimer.Interval = 100;
radarTimer.Tick += RadarTimer_Tick;
}
float coneSweepAngle = 36.0f;
float coneRotationAngle = .0f;
float radarSpeed = 1.8f;
float radarThickness = 5.0f;
System.Windows.Forms.Timer radarTimer = new System.Windows.Forms.Timer();
private void RadarTimer_Tick(object? sender, EventArgs e) {
coneRotationAngle += radarSpeed;
coneRotationAngle %= 360.0f;
canvas.Invalidate();
}
private void canvas_Paint(object sender, PaintEventArgs e) {
var center = new PointF(canvas.Width / 2.0f, canvas.Height / 2.0f);
RectangleF outerRect = canvas!.ClientRectangle;
outerRect.Inflate(-(radarThickness / 2.0f), -(radarThickness / 2.0f));
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
using var pathOuter = new GraphicsPath();
using var pathInner = new GraphicsPath();
pathOuter.AddEllipse(outerRect);
pathInner.StartFigure();
pathInner.AddArc(outerRect, coneRotationAngle, coneSweepAngle);
var arcPoints = pathInner.PathPoints;
PointF first = arcPoints[0];
PointF last = arcPoints[arcPoints.Length - 1];
pathInner.AddLines(new[] { center, last, center, first });
pathInner.CloseFigure();
using var outerPen = new Pen(Color.FromArgb(100, Color.Red), radarThickness);
using var innerBrush = new LinearGradientBrush(
center, first, Color.FromArgb(200, Color.Orange), Color.FromArgb(20, Color.Orange));
e.Graphics.FillPath(innerBrush, pathInner);
e.Graphics.DrawPath(outerPen, pathOuter);
}
}
This is how it works:

Drawing sets of coordinates to pixels so they mutually scale

So I have a List<object> of longitude and latitude coordinates of two points, and I need to connect the line between them. The trick is to display all of the lines within a panel so that they are scaled within the panel's dimensions (converting coordinate numbers to match the pixels) and I almost got it. However I'm confounded by some unknown problem. The code is:
int canvasWidth = panel1.Width,
canvasHeight = panel1.Height;
var minX1 = tockeKoordinate.Min(x => x.startX);
var minX2 = tockeKoordinate.Min(x => x.endX);
var minX = Math.Min(minX1, minX2);
var maxX1 = tockeKoordinate.Max(x => x.startX);
var maxX2 = tockeKoordinate.Max(x => x.endX);
var maxX = Math.Max(maxX1, maxX2);
var maxY1 = tockeKoordinate.Max(x => x.startY);
var maxY2 = tockeKoordinate.Max(x => x.endY);
var maxY = Math.Max(maxY1, maxY2);
var minY1 = tockeKoordinate.Min(x => x.startY);
var minY2 = tockeKoordinate.Min(x => x.endY);
var minY = Math.Min(minY1, minY2);
double coordinatesWidth = Math.Abs(maxX - minX),
coordinatesHeight = Math.Abs(maxY - minY);
float coefWidth = (float)coordinatesWidth / canvasWidth,
coefHeight = (float)coordinatesHeight / canvasHeight;
Basically I check the List for minimum and maximum XY coordinates, so I know what the extreme values are. Then I use a coeficient value to recalculate the coords in pixels so that are within the panel. When I use this:
drawLine(Math.Abs((float)(line.startX - minX) / coefWidth),
Math.Abs((float)(line.startY - minY) / coefHeight),
Math.Abs((float)(line.endX - maxX) / coefWidth),
Math.Abs((float)(line.endY - maxY) / coefHeight));
which is in foreach loop that iterates trough all the elements from the List . The drawline() method is as follows:
private void drawLine(float startX, float startY, float endX, float endY)
{
PointF[] points =
{
new PointF(startX, startY),
new PointF(endX, endY),
};
g.DrawLine(myPen, points[0], points[1]);
}
WHen all of this is put together, I get this picture:
I know for a fact that the "lines" should be connected and form shapes, in this case they represent roads in a suburban area.
I figured that it treats every coordinate set like it is the only one and then scales it to the panel dimensions. Actually it should scale it in reference to all of the other coordinates
It should "zoom" them out and connect with each other, because that is the way I defined the panel dimensions and everything else.
EDIT: ToW's solution did the trick, with this line of code changed to use my List:
foreach (var line in tockeKoordinate)
{
gp.AddLine((float)(line.startX), (float)(line.startY), (float)(line.endX), (float)(line.endY));
gp.CloseFigure();
}
End result when working properly:
As far as I can see your best bet would be to add all those lines to a GraphicsPath.
After it is complete you can look at its bounding rectangle and compare it to the size your Panel offers.
Then you can calculate a scale for the Graphics object to draw with and also a translation.
Finally you draw the lines with Graphics.DrawPath.
All with just 2 division on your side :-)
Here is an example:
private void panel1_Paint(object sender, PaintEventArgs e)
{
Graphics G = e.Graphics;
Random R = new Random(13);
GraphicsPath gp = new GraphicsPath();
for (int i = 0; i < 23; i++)
{
gp.AddLine(R.Next(1234), R.Next(1234), R.Next(1234), R.Next(1234));
gp.CloseFigure(); // disconnect lines
}
RectangleF rect = gp.GetBounds();
float scale = Math.Min(1f * panel1.Width / rect.Width,
1f * panel1.Height / rect.Height);
using (Pen penUnscaled = new Pen(Color.Blue, 4f))
using (Pen penScaled = new Pen(Color.Red, 4f))
{
G.Clear(Color.White);
G.DrawPath(penUnscaled, gp);
G.ScaleTransform(scale, scale);
G.TranslateTransform(-rect.X, -rect.Y);
G.DrawPath(penScaled, gp);
}
}
A few notes:
The blue lines do not fit onto the panel
The red lines are scaled down to fit
The Pen is scaled along with the rest of the Graphics but won't go under 1f.
To create connected lines do add a PointF[] or, more convenient a List<PointF>.ToArray().
I really should have used panel1.ClientSize.Width instead of panel1.Width etc..; now it is off a tiny bit at the bottom; bad boy me ;-)

Thick Line Drawing problem in C#.NET

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.

Rotating label text and buttons through 90 degrees

I am trying to rotate a label through 90 degrees. At the moment I can take the text from the label and rotate that, but what I want to do is actually rotate a label of my choice, or if I were to be really flashy, lets say a button control. So using the code below, how can I modify it so that I can feed it a control and get it to rotate it?
protected override void OnPaint(PaintEventArgs e)
{
Graphics graphics = e.Graphics;
string text = label4.Text;
StringFormat stringFormat = new StringFormat();
stringFormat.Alignment = StringAlignment.Center;
stringFormat.Trimming = StringTrimming.None;
Brush textBrush = new SolidBrush(this.ForeColor);
//Getting the width and height of the text, which we are going to write
float width = graphics.MeasureString(text, this.Font).Width;
float height = graphics.MeasureString(text, this.Font).Height;
//The radius is set to 0.9 of the width or height, b'cos not to
//hide and part of the text at any stage
float radius = 0f;
if (ClientRectangle.Width < ClientRectangle.Height)
{
radius = ClientRectangle.Width * 0.9f / 2;
}
else
{
radius = ClientRectangle.Height * 0.9f / 2;
}
int rotationAngle = 90;
double angle = (rotationAngle / 180) * Math.PI;
graphics.TranslateTransform(
(ClientRectangle.Width + (float)(height * Math.Sin(angle)) - (float)(width * Math.Cos(angle))) / 2,
(ClientRectangle.Height - (float)(height * Math.Cos(angle)) - (float)(width * Math.Sin(angle))) / 2);
graphics.RotateTransform((float)rotationAngle);
graphics.DrawString(text, this.Font, textBrush, 0, 0);
graphics.ResetTransform();
}
Standard windows forms controls (such as a label and button) are rendered by the operating system itself, windows forms doesn't do the actual drawing.
Therefore, unfortunately, you have no control over aspects such as rotation and scaling with these sorts of controls. This is just a limitation of Windows Forms itself and is one of the major reasons Microsoft created WPF.
WPF controls are entirely rendered by WPF (using DirectX behind the scenes). WPF supports all the standard 2D (and 3D) transaformations such as scaling, rotation and translation.
Altrrnatively in windows forms you could create a custom control that you render using GDI+ and can rotate and scale as required. Of course now you're doing all the work yourself which it seems is not what you want.
You could use WPF instead of WinForms...then its a simple transform ;)

Categories