How to Draw Line and Select It in Panel

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:

Sample Image

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 use CreateGraphics in the first place, as the Graphics 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 the Paint event's parameters to the Line instances so they can draw themselves with a current Graphics 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 the Picturbox's Image or a Panel's BackgroundImage.

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 PictureBoxis 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



Leave a reply



Submit