I have several points, and I try to draw Bezier curve using code below
PathFigure pf = new PathFigure(points.From, ps, false); //ps - list of Bezier segments
PathFigureCollection pfc = new PathFigureCollection();
pfc.Add(pf);
var pge = new PathGeometry();
pge.Figures = pfc;
Path p = new Path();
p.Data = pge;
p.Stroke = new SolidColorBrush(Color.FromRgb(244, 111, 011));
My Bezier segments look like this
1,2,3 points - first segment
3,4,5 points - second
5,6,7.. ..
But I got this strange curve (here is 3 big (Nodes) and 7 small ellipse (is my points)):
The line you're getting is the union of three distinct Bezier curves - one for each group of three points. (One for each "Bezier segment"?)
If you want a single smooth curve, you need to pass your 9 (or more) points as a single collection of points (single "Bezier segment"?), not as groups of three points.
Edit: Apparently BezierSegment only supports three points, so no wonder this doesn't work. Even 'PolyBezierSegment' just gives a collection of Bezier segments rather than a single smooth Bezier...
So since WPF doesn't give you anything useful, I knocked something together using the maths here. It's a numeric solution, but it seems to be pretty performant even with enough points to look nice and smooth:
PolyLineSegment GetBezierApproximation(Point[] controlPoints, int outputSegmentCount)
{
Point[] points = new Point[outputSegmentCount + 1];
for (int i = 0; i <= outputSegmentCount; i++)
{
double t = (double)i / outputSegmentCount;
points[i] = GetBezierPoint(t, controlPoints, 0, controlPoints.Length);
}
return new PolyLineSegment(points, true);
}
Point GetBezierPoint(double t, Point[] controlPoints, int index, int count)
{
if (count == 1)
return controlPoints[index];
var P0 = GetBezierPoint(t, controlPoints, index, count - 1);
var P1 = GetBezierPoint(t, controlPoints, index + 1, count - 1);
return new Point((1 - t) * P0.X + t * P1.X, (1 - t) * P0.Y + t * P1.Y);
}
Using this,
private void Grid_Loaded(object sender, RoutedEventArgs e)
{
Point[] points = new[] {
new Point(0, 200),
new Point(0, 0),
new Point(300, 0),
new Point(350, 200),
new Point(400, 0)
};
var b = GetBezierApproximation(points, 256);
PathFigure pf = new PathFigure(b.Points[0], new[] { b }, false);
PathFigureCollection pfc = new PathFigureCollection();
pfc.Add(pf);
var pge = new PathGeometry();
pge.Figures = pfc;
Path p = new Path();
p.Data = pge;
p.Stroke = new SolidColorBrush(Color.FromRgb(255, 0, 0));
((Grid)sender).Children.Add(p);
}
gives
Since each of your curves has one control point (a point that influences the curve but isn't necessarily on the curve), you're using quadratic Bézier curves.
If you want to draw two quadratic curves that share an endpoint, and you want the joint to appear smooth, the control points on each side of the shared endpoint must be collinear with the endpoint. That is, the two control points and the endpoint between them must all lie on a straight line. Example:
The solid black discs are the endpoints. The hollow circles are the control points. The solid black line is the curve. The dotted lines show that each endpoint is collinear (on a straight line with) the control point on either side.
Related
Let's have a triangle with sides AB = 80, BC = 50, CA = 40.
It is necessary to draw a triangle whose sides would be equal to these values.Tried to do it via AddPolygon, but I think it's not quite the right solution
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
if (index == 1)
{
Point[] myArray =
{
new Point(80, 50),
new Point(50, 40),
new Point(40, 80),
};
GraphicsPath Path = new GraphicsPath();
Path.AddPolygon(myArray);
Pen P = new Pen(Color.Black, 5);
e.Graphics.DrawPath(P, Path);
}
}
Since numbers represent lengths, not coordinates, they cannot be used in the API directly: you need to do some math before you start drawing.
First, let's decide on the location and orientation of our triangle: vertex A would be at (0, 0), and side AB would lie on the X axis, so point B would be at (80, 0). Now it's all about figuring out the location of point C. You can figure it out by solving two equations together:
(x-80)2 + y2 = 402
x2 + y2 = 502
The math is easy enough for a seventh grader who's good at math, so I wouldn't bore you with the solution. It yields x=45.62 and y=20.45. Plug these numbers into your program to draw your triangle.
Obviously, this draws the triangle in only one, easy-to-do, orientation. To move the triangle, adjust coordinates of its vertices by the same number. Rotating is a lot trickier; you would need to look it up in your favorite book on analytic geometry.
I need to draw some text (a number) in the middle of a line drawn with Graphics.DrawLine like this:
1 and 2 are buttons. I achieved this by using the answer provided here.
The problem with this solution is that it doesn't take into account the fact that the line start can be vertically lower than the end point (in which case the text overlaps with the line and at a certain point disappears as here:
.
I know how to solve the main issue here about the start point being vertically lower but how can I make it so that it doesn't overlap with the line as in the following image?
Updated Based on comments.
I believe you're looking for something like below (please note I used test data, a little work will be required. This takes your 2 points, creates a median, measures your string, offsets the median, and draws the string.
private void Form1_Paint(object sender, PaintEventArgs e)
{
var pt1 = new Point(25, 25);
var pt2 = new Point(100, 10);
var ptMed = new Point((pt1.X + pt2.X) / 2, (pt1.Y + pt2.Y) / 2);
var g = e.Graphics;
var lbl = "1";
var offset = g.MeasureString(lbl, this.Font);
ptMed.Y -= (int)offset.Height;
ptMed.X -= (int)offset.Width;
var p = new Pen(Brushes.White);
g.DrawLine(p, pt1, pt2);
g.DrawString(lbl, this.Font, Brushes.White, ptMed);
}
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 ;-)
Ok so i have created a triangle but I cant for the life of me work out the coordinates to create a simple hexagon,
Point[] shape = new Point[3];
shape[0] = new Point(200, 100);
shape[1] = new Point(300, 200);
shape[2] = new Point(100, 200);
This makes a triangle but I cant figure out the x and y values for a hexagon, sounds like a simple question but my brain just isn't working correctly today, Below is the array for the hexagon I just can't figure out the values.
Point[] shape = new Point[6];
shape[0] = new Point(0, 0);
shape[1] = new Point(0, 0);
shape[2] = new Point(0, 0);
shape[3] = new Point(0, 0);
shape[4] = new Point(0, 0);
shape[5] = new Point(0, 0);
Any help would be great thanks!
Since I've already written a comment, I guess I should demonstrate that in some real code.
I created a WinForms application with a Panel object on which I can draw. Then I've overridden the Paint event on that to draw me a hexagon.
private void panel1_Paint(object sender, PaintEventArgs e)
{
var graphics = e.Graphics;
//Get the middle of the panel
var x_0 = panel1.Width / 2;
var y_0 = panel1.Height / 2;
var shape = new PointF[6];
var r = 70; //70 px radius
//Create 6 points
for(int a=0; a < 6; a++)
{
shape[a] = new PointF(
x_0 + r * (float)Math.Cos(a * 60 * Math.PI / 180f),
y_0 + r * (float)Math.Sin(a * 60 * Math.PI / 180f));
}
graphics.DrawPolygon(Pens.Red, shape);
}
This then draws
As I said, the key is to view the hexagon as a "discrete" circle. The points are all computed as being on the outer part of a perfect circle, which are then connected with a straight line. You can create all regular n-Point shapes with this technique (a pentagon e.g. as a 5-regular shape ;))
So, you just "inscribe" the 6 points in the circle to get your hexagon, as shown in this diagram with a regular 5-point shape:
Then remember that you can compute the (x,y) coordinates of a point given its polar coordinates (r, phi) as
To which you can also add an offset , which is in my case the center of the frame I'm drawing in.
Basically I have a form and am trying to "dim" areas of it to draw focus to a certain part of the form. To do this I'm using a Form with no border and 50% opacity, aligned with the actual form. The area I am trying to mask is the dark gray area, roughly, as pictured:
To get the "U"-shaped form, I'm using a GraphicsPath with AddPolygon, calculating the points of each vertex:
var p = new GraphicsPath();
var origin = new Point(Top, Left);
var maxExtentPt = new Point(origin.X + Width, origin.Y + Height);
Point[] points = {
origin,
new Point(origin.X + leftPanel.Width, origin.Y),
new Point(origin.X + leftPanel.Width, maxExtentPt.Y - bottomPanel.Height),
new Point(maxExtentPt.X - rightPanel.Width, maxExtentPt.Y- bottomPanel.Height),
new Point(maxExtentPt.X - rightPanel.Width, origin.Y),
new Point(maxExtentPt.X, origin.Y),
maxExtentPt,
new Point(origin.X, maxExtentPt.Y),
origin
};
p.AddPolygon(points);
overlayForm.Region = new Region(p);
overlayForm.Location = PointToScreen(Point.Empty);
The three panels in the code are what I am masking, so I am using their dimensions to calculate the points. Instead of getting my expected result, the mask looks like this, with its size changing as I resize the main form (I recalculate the region on Move and Resize):
Is there some limitation of GraphicsPath.AddPolygon that I'm not aware of? I double-checked (quadruple-checked, really) the results of my calculations, including taking the coordinates for each point and plugging them into Ipe to see if the shape was actually correct... It was. But not in my program!
Edit: Here are the values of each point, when I hit a breakpoint at p.AddPolygon(points); I'm starting in the upper left-hand corner and going around clockwise:
Looks like your points are wrong after all.
Everything ought to be in the coordinates of the ClientRectangle, so
Origin should not be new Point(Top, Left) which is the Location of the Form. It should be Point.Empty or (0,0). Or you could use the leftPanel.Location.
And
maxExtentPt = new Point(origin.X + Width, origin.Y + Height);
should read:
var maxExtentPt = new Point(origin.X + ClientSize.Width, origin.Y + ClientSize.Height);
(The difference is the size of border+title..)
Let me know if that works better!
You could also try doing it this way, basing everything off the Panels:
Form overlayForm = new Form();
overlayForm.Opacity = .5;
overlayForm.BackColor = Color.DarkGray;
overlayForm.StartPosition = FormStartPosition.Manual;
overlayForm.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None;
var p = new GraphicsPath();
var origin = new Point(0, 0);
var maxExtentPt = new Point(this.Width, this.Height);
Point[] points = {
origin,
new Point(leftPanel.Right, origin.Y),
new Point(leftPanel.Right, bottomPanel.Top),
new Point(rightPanel.Left, bottomPanel.Top),
new Point(rightPanel.Left, origin.Y),
new Point(rightPanel.Right, origin.Y),
maxExtentPt,
new Point(origin.X, maxExtentPt.Y),
origin
};
p.AddPolygon(points);
overlayForm.Region = new Region(p);
overlayForm.Location = this.PointToScreen(new Point(0, 0));
overlayForm.Show();