Reputation: 3
I'm trying to make a program that is able to read a dxf file and plot it's contents but when I try to draw the figures in the window's paint event, nothing is drawn unless I use this.Invalidate();
and this doesn't completely work because the objects appear to blink on the screen. The coordinates to draw the lines are stored in a list declared in the window class.
private void InitialWindow_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
Pen blackPen = new Pen(Color.Black, 1);
if (entities.Count != 0)
{
for (int i = 0; i < entities.Count; i++)
{
for (int k = 0; k < entities[i].path.Count - 1; k++)
{
g.DrawLine(blackPen, D2F(entities[i].path[k]), D2F(entities[i].path[k + 1]));
}
g.DrawLine(blackPen, D2F(entities[i].path[0]), D2F(entities[i].path.Last()));
}
}
}
2DF is a function that converts a point-like type of data to PointF so it can be used in DrawLine. If I try to draw outside the for loops the lines are displayed correctly on the screen. Thanks in advance for any help with it.
Upvotes: 0
Views: 149
Reputation: 29244
I suggest you create a class to handle the model-to-pixel conversions (and in reverse) instead of a function D2F()
. A class it going to give you a lot more flexibility and it will retain a state of the current scaling values
public class Canvas
{
public Canvas()
{
Target = new Rectangle(0, 0, 1, 1);
Scale = 1;
}
public Rectangle Target { get; set; }
public float Scale { get; set; }
public void SetModelBounds(float width, float height)
{
Scale = Math.Min(Target.Width / width, Target.Height / height);
}
public PointF ToPixel(Vector2 point)
{
var center = new PointF(Target.Left + Target.Width / 2, Target.Top + Target.Height / 2);
return new PointF(center.X + Scale * point.X, center.Y - Scale * point.Y);
}
public Vector2 FromPixel(Point pixel)
{
var center = new PointF(Target.Left + Target.Width / 2, Target.Top + Target.Height / 2);
return new Vector2((pixel.X - center.X) / Scale, -(pixel.Y - center.Y) / Scale);
}
}
This is setup for each paint event by calling for example
Canvas.Target = this.ClientRectangle;
Canvas.SetModelBounds(2f, 2f);
The above code is going to place coordinates (-1,-1) on the bottom left of the form surface, and (1,1) on the top right. The pixels per model unit are kept the same for x-axis and y-axis (see Canvas.Scale
).
Now for the drawing, also use am Entity
class to hold the drawing geometry, and various other properties such as color, and if the shape is closed or not. Note that if it defines a Render(Graphics g, Canvas canvas)
method, then it can be called by the form and each entity can handle its own drawing (for the most modular design)
Here is an example:
public class Entity
{
Entity(bool closed, Color color, params Vector2[] path)
{
Color = color;
Path = new List<Vector2>(path);
Closed = closed;
}
public Color Color { get; set; }
public List<Vector2> Path { get; }
public bool Closed { get; set; }
public void Render(Graphics g, Canvas canvas)
{
using (Pen pen = new Pen(Color, 1))
{
var points = Path.Select(pt => canvas.ToPixel(pt)).ToArray();
if (Closed)
{
g.DrawPolygon(pen, points);
}
else
{
g.DrawLines(pen, points);
}
}
}
public static Entity Triangle(Color color, Vector2 center, float width, float height)
{
return new Entity(true, color, new Vector2[] {
new Vector2(center.X-width/2, center.Y - height/3),
new Vector2(center.X+width/2, center.Y - height/3),
new Vector2(center.X, center.Y+2*height/3) });
}
public static Entity Rectange(Color color, Vector2 center, float width, float height)
{
return new Entity(true, color, new Vector2[] {
new Vector2(center.X-width/2, center.Y-height/2),
new Vector2(center.X+width/2, center.Y-height/2),
new Vector2(center.X+width/2, center.Y+height/2),
new Vector2(center.X-width/2, center.Y+height/2) });
}
public static Entity Polygon(Color color, params Vector2[] path)
=> new Entity(true, color, path);
public static Entity Polyline(Color color, params Vector2[] path)
=> new Entity(false, color, path);
}
The above is used in the form as follows
public partial class Form1 : Form
{
public Canvas Canvas { get; }
public List<Entity> Entities { get; }
public Form1()
{
InitializeComponent();
Entities = new List<Entity>();
Canvas = new Canvas();
}
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
this.Resize += (s, ev) => Invalidate();
this.Paint += (s, ev) =>
{
ev.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
Canvas.Target = ClientRectangle;
Canvas.SetModelBounds(2f, 2f);
foreach (var item in Entities)
{
item.Render(ev.Graphics, Canvas);
}
};
Entities.Add( .. )
}
}
As you can see the Paint
event calls the Render()
method for each entity.
In fact, you can generalize this model using an interface IRender
. In the example below besides Entity
that implements the interface, I define a class to draw the coordinate axis called Axes
.
public interface IRender
{
void Render(Graphics g, Canvas canvas);
}
public class Axes : IRender
{
public void Render(Graphics g, Canvas canvas)
{
PointF origin = canvas.ToPixel(Vector2.Zero);
PointF xpoint = canvas.ToPixel(Vector2.UnitX);
PointF ypoint = canvas.ToPixel(Vector2.UnitY);
using (Pen pen = new Pen(Color.Black, 0))
{
pen.CustomEndCap = new AdjustableArrowCap(2f, 5f);
g.DrawLine(pen, origin, xpoint);
g.DrawLine(pen, origin, ypoint);
}
}
}
public class Entity : IRender
{
...
}
and now you can draw with quite a bit of varying things on the screen with the above framework. Here is an example that draws the axis (of unit size) and a few sample entities.
Entities.Add(Entity.Polygon(Color.Orange, Vector2.Zero, -Vector2.UnitX, -Vector2.One));
Entities.Add(Entity.Rectange(Color.Blue, new Vector2(0.5f, 0.5f), 0.25f, 0.25f));
Entities.Add(Entity.Triangle(Color.Red, new Vector2(0.5f, 0.75f), 0.25f, 0.25f));
Entities.Add(Entity.Polyline(Color.Magenta,
new Vector2(0f, -0.2f), new Vector2(0f, -0.4f), new Vector2(0.2f, -0.4f),
new Vector2(0.2f, -0.2f), new Vector2(0.4f, -0.2f), new Vector2(0.4f, -0.4f)));
Upvotes: 1