How to draw line and select it in Panel
Here is a suitable Line
class:
class Line
{
public Color LineColor { get; set; }
public float Linewidth { get; set; }
public bool Selected { get; set; }
public Point Start { get; set; }
public Point End { get; set; }
public Line(Color c, float w, Point s, Point e)
{ LineColor = c; Linewidth = w; Start = s; End = e; }
public void Draw(Graphics G)
{ using (Pen pen = new Pen(LineColor, Linewidth)) G.DrawLine(pen, Start, End); }
public bool HitTest(Point Pt)
{
// test if we fall outside of the bounding box:
if ((Pt.X < Start.X && Pt.X < End.X) || (Pt.X > Start.X && Pt.X > End.X) ||
(Pt.Y < Start.Y && Pt.Y < End.Y) || (Pt.Y > Start.Y && Pt.Y > End.Y))
return false;
// now we calculate the distance:
float dy = End.Y - Start.Y;
float dx = End.X - Start.X;
float Z = dy * Pt.X - dx * Pt.Y + Start.Y * End.X - Start.X * End.Y;
float N = dy * dy + dx * dx;
float dist = (float)( Math.Abs(Z) / Math.Sqrt(N));
// done:
return dist < Linewidth / 2f;
}
}
Define a List for the lines, probably at class level:
List<Line> lines = new List<Line>();
Here is how you can initialize it with a few lines:
for (int i = 0; i < 20; i++) lines.Add(new Line(Color.Black, 4f,
new Point(R.Next(panel1.Width), R.Next(panel1.Height)),
new Point(R.Next(panel1.Width), R.Next(panel1.Height))));
Here is the result of clicking on a crossing:
Whenever you add, change or remove a line you need to make the Panel
reflect the news by triggering the Paint
event:
panel1.Invalidate();
Here is the Paint
event of the Panel
:
private void panel1_Paint(object sender, PaintEventArgs e)
{
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
foreach (Line L in lines) L.Draw(e.Graphics);
}
In the MouseClick
event you do the test:
private void panel1_MouseClick(object sender, MouseEventArgs e)
{
foreach(Line L in lines)
L.LineColor = L.HitTest(e.Location) ? Color.Red : Color.Black;
panel1.Invalidate();
}
To avoid flicker don't use the basic Panel
class as it isn't doublebuffered
. Instead use either a PictureBox
or a Label
(with AutoSize=false
) or a doublebuffered Panel
subclass:
class DrawPanel : Panel
{ public DrawPanel () { DoubleBuffered = true; } }
Notes:
There is no such thing as a 'Line' in WinForms, only pixels of various colors. So to select a line you need to store it's two endpoints' coordinates and then find out if you have hit it when clicking.
The above example shows how to do it in math.
Instead one could test each line by drawing it onto a bitmap and test the pixel the mouse has clicked. But drawing those bitmaps would have to do math behind the scenes as well and also allocate space for the bitmaps, so the math will be more efficient..
Yes the
Line
class looks a little long for such a simple thing a s a line but look how short all the event codes now are! That's because the responsiblities are where they belong!Also note the the first rule of doing any drawing in WinForms is: Never cache or store a
Grahics
object. In fact you shouldn't ever useCreateGraphics
in the first place, as theGraphics
object will never stay in scope and the graphics it produces will not persist (i.e. survive a Minimize-maximize sequence)..Also note how I pass out the
e.Graphics
object of thePaint
event's parameters to theLine
instances so they can draw themselves with a currentGraphics
object!To select thinner lines it may help to modify the distance check a little..
The Math was taken directly form Wikipedia.
Select drawn figure within panel box
Your code has several issues, all of which will go away once you learn how to draw properly in winforms!
There are many posts describing it but what you need to understand that you really have these two options:
Draw onto the surface of the control. This is what you do, but you do it all wrong.
Draw into a
Bitmap
which is displayed in the control, like thePicturbox
'sImage
or aPanel
'sBackgroundImage
.
Option two is best for graphics that slowly add up and won't need to be corrected all the time.
Option one is best for interactive graphics, where the user will move things around a lot or change or delete them.
You can also mix the options by caching drawn graphics in a Bitmap
when they get too numerous.
Since you started with drawing onto the surface let's see how you should do it correctly:
The Golden Rule: All drawing needs to be done in the control's Paint
event or be triggered from there always using only the Paint
event's e.Graphics
object!
Instead you have created a Graphics
object by using control.CreateGraphics
. This is almost always wrong.
One consequence of the above rule is that the Paint
event needs to be able to draw all objects the user has created so far. So you will need to have class level lists to hold the necessary data: List<ActorClass>
and List<UseCaseClass>
. Then it can do maybe a
foreach(ActorClass actor in ActorList) actor.drawActor(e.Graphics)
etc.
Yes this fully repainting everything looks like a waste but it won't be a problem until you need to draw several hundreds of object.
But if you don't do it this way, nothing you draw persists.
Test it by running your present code and doing a Minimize/Maximize
sequence. Poof, all drawings are gone..
Now back to your original question: How to select an e.g. Actor?
This really gets simple as you can can iterate over the ActorList
in the MouseClick
event (do not use the Click
event, as it lacks the necessary parameters):
foreach (ActorClass actor in ActorList)
if (actor.rectangle.Contains e.Location)
{
// do stuff
break;
}
This is a simple implementation; you may want to refine it for the case of overlapping objects..
Now you could do things like maybe change the color of the rectangle or add a reference to the object in a currentActor
variable.
Whenever you have made any changes to your lists of things to draw, like adding or deleting a object or moving it or changing any (visible) properties you should trigger an update via the Paint
event by calling Invalidate
.
Btw: You asked about a PictureBox
in the title but use only a Panel
in the code. Using a PictureBox
is recommended as it is doublebuffered and also combines two Bitmaps to let you use both a caching Image
and a BackgroundImage
with maybe a nice paper..
As far as I can see your code so far lacks the necessary classes. When you write them add a Draw routine and either a reference to the Label
you add or simply use DrawString
to draw the text yourself..
Update:
After looking at your project, here a the minimal changes to make the drawing work:
// private Graphics graphics; // delete!!
Never try to cache a Graphics
object!
private void pl_Diagram_Paint(object sender, PaintEventArgs e)
{
pen = new Pen(Color.Black, 1);
DrawElements(e.Graphics); // pass out the 'good' object
//graphics = pl_Diagram.CreateGraphics(); // delete!
}
The same; pass the real Graphics
object into the drawing routine instead!
// actor
if (rb_Actor.Checked)
{
if (e.X <= 150)
{
var actor = new Actor(name, e.X, e.Y);
_actors.Add(actor);
pl_Diagram.Invalidate(); // trigger the paint event
//DrawElements();
}
}
// use case
if (rb_Use_Cases.Checked)
{
var useCase = new UseCase(name, e.X, e.Y);
_useCases.Add(useCase);
pl_Diagram.Invalidate(); // trigger the paint event
//DrawElements();
}
Instead of calling the routine directly we trigger the Paint
event, which then can pass a good Graphics
object to it.
public void DrawElements(Graphics graphics)
{
foreach (var actor in _actors)
{
DrawActor(graphics, actor);
}
foreach (var useCase in _useCases)
{
DrawUseCase(graphics, useCase);
}
}
We receive the Graphics object and pass it on..
private void DrawActor(Graphics graphics, Actor actor)
and
graphics.DrawEllipse(pen, (useCase.X - 60), (useCase.Y - 30), 120, 60);
After these few changes the drawing persists.
Replacing the Panel
by a Picturebox
is still recommended to avoid flicker during the redraw. (Or replace by a double-buffered Panel subclass..)
How to draw a line from point to point in C# with Panel, Graphics and Points
This code
if (prevPoint != null)
{
G.DrawLine(pen, (Point)prevPoint, pt);
prevPoint = pt;
}
will never run, since prevPoint is only set inside the if-statement. It should be
if (prevPoint != null)
{
G.DrawLine(pen, (Point)prevPoint, pt);
}
prevPoint = pt;
or skip the initial point
if(Shp.Count < 2) return;
var prevPoint = Shp[0];
foreach(Point pt in Shp.Skip(1)){
G.DrawLine(pen, prevPoint, pt);
}
Also note that:
G.DrawLine(pen, Shp[0], Shp[Shp.Count - 1]);
will always draw the same thing, so does not need to be inside the loop- Drawing inside a button event handler is probably not the way to go, since everything will be cleared when the control is redrawn. I would suggest attaching an event handler to the paint event. Or create a subclass of your panel an override
OnPaint
Initiate panel with drawings
You could create a bitmap and draw it instead.
But before you do that: DrawEllipse
is a little expensive. Use DrawLine
with a Pen
that has a dotted linestyle instead:
int onePercentWidth = panel1.ClientSize.Width / 100;
using (Pen my_pen = new Pen(Color.Gray, 1f))
{
my_pen.DashStyle = System.Drawing.Drawing2D.DashStyle.Custom;
my_pen.DashPattern = new float[] { 1F, onePercentWidth -1 };
for (int y = onePercentWidth; y < panel1.ClientSize.Height - 1; y += onePercentWidth)
e.Graphics.DrawLine(my_pen, 0, y, panel1.ClientSize.Width, y);
}
Note that I am using using
so I don't leak the Pen
and ClientSize
so I use only the inner width. Also note the exaplanation about the custom DashPattern
on MSDN
Related Topics
Load Local HTML File in a C# Webbrowser
Exclude Property from Serialization via Custom Attribute (JSON.Net)
Using Multiple Versions of the Same Dll
ASP.NET File Download from Server
Open File with Associated Application
How the Int.Tryparse Actually Works
Check Whether Internet Connection Is Available with C#
What Use Is the Aliases Property of Assembly References in Visual Studio 8
How to Convert Unicode Escape Sequences to Unicode Characters in a .Net String
What Are Independent Associations and Foreign Key Associations
What Characters Are Allowed in C# Class Name
C#: Class for Decoding Quoted-Printable Encoding
Handling Unhandled Exceptions Problem
How Exactly Do Static Fields Work Internally
Why Can't a Duplicate Variable Name Be Declared in a Nested Local Scope