Select drawn figure within panel box

2019-09-08 18:46发布

问题:

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

回答1:

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..)