I am working on an 'use-case-diagram-form' where an user can select an element and a modus
Just a simple form. It al works fine, made a class for each actor element and each use-case element. Both are added in a list after beeing created.
But somehow I just can't figure out how to select a created element and after do something with it.
classes i made:
class Actor
{
private static int _id;
private Panel _panel;
public string Name { get; private set; }
public int X { get; private set; }
public int Y { get; private set; }
public Actor(Panel panel, string name, int x, int y)
{
_id++;
_panel = panel;
Name = name;
X = x;
Y = y;
}
public void DrawActor()
{
// draw Actor
var graphics = _panel.CreateGraphics();
var pen = new Pen(Color.Black, 2);
graphics.DrawEllipse(pen, X - 10, Y - 30, 20, 20);
graphics.DrawLine(pen, X, Y - 10, X, Y + 20);
graphics.DrawLine(pen, X - 15, Y, X + 15, Y);
graphics.DrawLine(pen, X, Y + 20, X - 15, Y + 35);
graphics.DrawLine(pen, X, Y + 20, X + 15, Y + 35);
// rectangle around actor
graphics.DrawRectangle(pen, (X - 20), (Y - 30), 40, 80);
// setup font
var stringFont = new Font("Arial", 10);
// measure string
var textWith = graphics.MeasureString(Name, stringFont).Width;
// label
var label = new Label();
var actorText = (_id < 10 ? "0" : "") + _id.ToString() + "-" + Name;
label.Text = actorText;
label.Location = new Point(X - (Convert.ToInt32(textWith)/2), Y + 40);
label.AutoSize = true;
label.BorderStyle = BorderStyle.FixedSingle;
_panel.Controls.Add(label);
}
class UseCase
{
private static int _id;
private Panel _panel;
private string _name;
private int _x;
private int _y;
public UseCase(Panel panel, string name, int x, int y)
{
_id++;
_panel = panel;
_name = name;
_x = x;
_y = y;
}
public void DrawUseCase()
{
var graphics = _panel.CreateGraphics();
var pen = new Pen(Color.Black, 2);
graphics.DrawEllipse(pen, _x , _y , 120, 50);
// setup font
var stringFont = new Font("Arial", 10);
// measure string
var textWith = graphics.MeasureString(_name, stringFont).Width;
// label
var label = new Label();
var useCaseText = (_id < 10 ? "0" : "") + _id.ToString() + "-" + _name;
label.Text = useCaseText;
label.Location = new Point(_x - (Convert.ToInt32(textWith) / 2) + 60, _y + 20);
label.AutoSize = true;
label.BorderStyle = BorderStyle.FixedSingle;
_panel.Controls.Add(label);
}
}
Github repository:
https://github.com/JimVercoelen/use-case-helper
Thanks
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 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..)