WPF applying geometry transform - c#

Why this geometry transform doesn't work?
private void DrawElement(DrawingVisual dv)
{
using (var dvc = dv.RenderOpen())
{
GeometryGroup gg = new GeometryGroup();
var pen = new Pen(Brushes.Black, 2);
TranslateTransform tt = new TranslateTransform(0, (!IsInverted ? -1 : 1) * _pixelsPerMillimeter);
TransformGroup tg = new TransformGroup();
Point lu, lb, ru, rb;
lu = new Point(rct_center.Left, (!IsMaxillar ? rct_center.Bottom - _pixelsPerMillimeter : _pixelsPerMillimeter + rct_center.Top));//
lb = new Point(lu.X, lu.Y + (!IsInverted ? -1 : 1) * rct_center.Height / 2);//
ru = new Point(rct_center.Right - _pixelsPerMillimeter, lu.Y);//
rb = new Point(ru.X, lb.Y);//
LineGeometry upperLeft = new LineGeometry(lu, new Point(lu.X + _pixelsPerMillimeter, lu.Y));
LineGeometry bottomLeft = new LineGeometry(lb, new Point(lb.X + _pixelsPerMillimeter, lb.Y));
LineGeometry upperRight = new LineGeometry(ru, new Point(ru.X + _pixelsPerMillimeter, ru.Y));
LineGeometry bottomRight = new LineGeometry(rb, new Point(rb.X + _pixelsPerMillimeter, rb.Y));
gg.Children.Add(upperLeft);
gg.Children.Add(bottomLeft);
gg.Children.Add(upperRight);
gg.Children.Add(bottomRight);
for (int j = 30 - 1; j >= 0; --j) //-1 because the start line isn't drawn.
{
dvc.DrawGeometry(Brushes.Transparent, pen, gg);
tg.Children.Add(tt);
gg.Transform = tg;
}
}
}
I want to draw repeatedly the 4 lines group below the previous or viceversa (if IsInverted).
When I debug the transformgroup matrix is modified, but the transformation isn't applied in the geometry group.
I'm newbie in wpf, from winforms and i'm a little bit lost without GDI GraphicsPath.
There is someway to do it better? Maybe i'm thinking a lot in GDI way to do it.

What you want to do here is use the rendering context's (dvc's) Push method instead of relying on the Transform of the geometry object itself. When using the drawing contexts, WPF relies on Push/Pop operations for setting the transform. You can see a related answer here: How do you apply a Scale Translation to a DrawingContext?
In addition, I think you need to study how C# handles reference types. The way you have it in your sample code, you are assigning the same instance of the transform to each geometry object, which I don't think was intended. I'm not sure if the Push method uses the context during the call or after; you'd have to experiment with that. Creating a new TranslateTransform inside your inner loop would be a sure way to avoid that. However, I agree with the commentators in that using some offset property would be more performant.

Related

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 string at the middle of line

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);
}

Creating a single hexagon in C# using DrawPolygon

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.

How to draw bezier curve by several points?

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.

Wpf animations from code

There was some similar threads, but I didn't find solution of my problem. It's my first post here.
Here's the thing:
Viewport3D viewPort3D;
GeometryModel3D geometryModel = new GeometryModel3D();
Transform3DGroup transform3DGroup = new Transform3DGroup();
...
// Rotation
RotateTransform3D rotateTransform3D = new RotateTransform3D();
AxisAngleRotation3D axisAngleRotation3d = new AxisAngleRotation3D();
axisAngleRotation3d.Axis = new Vector3D(0, 1, 0);
axisAngleRotation3d.Angle = angle;
rotateTransform3D.Rotation = axisAngleRotation3d;
transform3DGroup.Children.Add(rotateTransform3D);
// Translation
TranslateTransform3D translateTransform3D = new TranslateTransform3D();
translateTransform3D.OffsetX = offsetX;
transform3DGroup.Children.Add(translateTransform3D);
// Adding transforms
geometryModel.Transform = transform3DGroup;
Model3DGroup model3DGroup = new Model3DGroup();
model3DGroup.Children.Add( image.getGeometryModel3D() );
modelVisual3D.Content = model3DGroup;
viewPort3D.Children.Add( modelVisual3D );
And now I want to make translation using storyboard (because later I want to add also rotating to that storyboard):
Storyboard s = new Storyboard();
Transform3DGroup transform3DGroup = model3DGroup.Children.ElementAt(current).Transform as Transform3DGroup;
for (int j = 0; j < transform3DGroup.Children.Count; ++j)
{
if (transform3DGroup.Children.ElementAt(j) is TranslateTransform3D)
{
TranslateTransform3D translation = transform3DGroup.Children.ElementAt(j) as TranslateTransform3D;
DoubleAnimation doubleAnimation = new DoubleAnimation();
doubleAnimation.From = 0;
doubleAnimation.To = 2;
doubleAnimation.Duration = new Duration(TimeSpan.FromSeconds(1));
doubleAnimation.AutoReverse = true;
doubleAnimation.RepeatBehavior = RepeatBehavior.Forever;
s.Children.Add(doubleAnimation);
s.Duration = new Duration(TimeSpan.FromSeconds(1));
Storyboard.SetTarget(doubleAnimation, model3DGroup.Children.ElementAt(current));
Storyboard.SetTargetProperty(doubleAnimation, new PropertyPath("(Model3D.Transform).(Transform3DGroup.Children)[1].(TranslateTransform3D.OffsetX)"));
s.Begin(); // Exception during the execution.
}
}
Exception in the last line:
'[Unknown]' property value in the path
'(Model3D.Transform).(Transform3DGroup.Children)[1].(TranslateTransform3D.OffsetX)'
points to immutable instance of
'System.Windows.Media.Media3D.TranslateTransform3D'.
I took PropertyPath similar to the path generated in blend 4.
Thank you for any help.
I think because translate tranform 3d is an immutable instance it has to be indicated that it should be mutable while rendering / translation is taking place.
I guess
We can supply x:Name to that immutable TranslateTransform3D object, to make it mutable.
Bind to its property than to animate it.
E.g. in your case
NameScope.SetNameScope(this, new NameScope());
this.RegisterName("AxisRotation", MyAxisRotation3DObject.Rotation);
this.RegisterName("TranslateTransformation", MyTranslation3DObject);
This way we give names to Axis Rotation 3D and Translate Transform 3D objects and then in double animations refer them as Storyboard.SetTargetName(.., "AxisRotation") and Storyboard.SetTargetName(.., "TranslateTransformation") and access their direct properties such as Storyboard.SetTargetProperty(.., new PropertyPath("Angle")) and Storyboard.SetTargetProperty(.., new PropertyPath("OffsetX")) resp.
Your error states that TranslateTransform3D is immutable, what means, it cannot be changed. And you're trying to animate one of its properties, hence got the error.

Categories