Laden...

[Tutorial] Gezeichnete Objekte mit der Maus verschieben

Erstellt von progger vor 17 Jahren Letzter Beitrag vor 17 Jahren 61.602 Views
progger Themenstarter:in
1.271 Beiträge seit 2005
vor 17 Jahren
[Tutorial] Gezeichnete Objekte mit der Maus verschieben

Vielen Dank an die Poweruser fürs Probelesen!

In seinem Tutorial Zeichnen in Windows-Programmen (Paint/OnPaint, PictureBox) hat herbivore beschrieben, wie man in einer Windows-Anwendung zeichnen sollte.
Ich möchte das ganze jetzt weiterführen und erklären, wie man mit Hilfe der Maus gezeichnete Objekte, wie Rechtecke, Linien und Kreise, verschieben und verändern kann.
Das ganze basiert auf herbivore's Anleitung, die man also gelesen haben sollte.

Wie finde ich heraus, ob die Maus über einem gezeichneten Objekt ist?

Dafür kann man die Klasse GraphicsPath im System.Drawing.Drawing2D-Namespace verwenden. Diese speichert geometrische Figuren in Form von Linien und Kurven zwischen bestimmten Punkten.
Wir fügen der MyGraphicObject-Basisklasse eine Eigenschaft für den GraphicsPath hinzu:

GraphicsPath _path;

protected GraphicsPath Path {
    get { return _path; }
}

Mit der Methode IsOutlineVisible kann man feststellen, ob ein angegebener Punkt auf der Linie, die das Objekt umgibt, liegt, wenn man diese mit dem angegebenen Pen-Objekt gezeichnet hat.
Dazu fügen wir der Basisklasse eine Methode Hit hinzu:

public virtual bool Hit(Point pt) {
    return Path.IsOutlineVisible(pt, _pen);
}

Wenn man diese Methode so verwendet, fällt einem schnell auf, wie schwer es ist, die Maus genau so über eine Linie zu bewegen, dass Hit true zurückgibt. Deswegen behaupten wir nun die Linie mit einem dickeren Pen-Objekt gezeichnet zu haben. Dann ist der Bereich, in den man die Maus bewegen muss um true zu erhalten um einiges größer.
Für die IsOutlineVisible-Methode wird nur die Breite des angegebenen Pen-Objekts verwendet, somit ist die Farbe irrelevant. Als Breite habe ich mit dem Wert 4 ganz gute Ergebnisse erzielt. Am besten spielt man mit dem Parameter ein bisschen rum, um das gewünschte Ergebnis zu erzielen.

Pen _hitTestPen = new Pen(Brushes.Black, 4);

public virtual bool Hit(Point pt) {
    return Path.IsOutlineVisible(pt, _hitTestPen);
}

Ob ein Punkt innerhalb eines Objekts liegt, kann man mit Hilfe der IsVisible-Methode herausfinden.
Ich habe der Beispielanwendung auch eine Methode Contains hinzugefügt, diese spielt für den Rest aber keine Rolle mehr:

public virtual bool Contains(Point pt) {
    return Path.IsVisible(pt);
}

Woher bekomme ich für meine Objekte einen GraphicsPath?

Die GraphicsPath-Klasse bietet verschiedene Methoden an (beginnen alle mit "Add" , z.B. AddLine oder AddRectangle), mit denen man geometrische Objekte, z.B. Rechteck, Ellipse, zu dem Pfad hinzufügen kann.
Anstatt sich bei jedem MyGraphicObject die Daten als Koordinaten, Rectangle-Struktur oder Ähnlichem zu merken, fügt man sie zu einem GraphicsPath hinzu.
Hier z.B. die Änderung am Konstruktor von herbivore’s MyRectangle-Klasse:

public MyRectangle(Pen pen, Rectangle rect) : base (pen) {
    Path.AddRectangle(rect);
}

Ist das mit dem GraphicsPath nicht viel komplizierter?

Nein, ist es nicht. Im Gegenteil: Da man nicht mehr lauter einzelne Strukturen und Integer-Variablen braucht, um die Angaben zu den einzelnen Objekten zu speichern, geht einiges viel leichter.
Bei herbivore’s Beispielanwendung mussten noch alle Objekte die Draw-Methode der Basisklasse implementieren. Dies ist nun nicht mehr nötig, da man schon in der Basisklasse den GraphicsPath zeichnen kann:

public virtual void Draw(Graphics g) {
    g.DrawPath(_pen, _path);
}

Auch Hit und Contains können direkt in der Basisklasse implementiert werden. Das Verschieben wird auch um einiges erleichtert, da man einen GraphicsPath ganz einfach mit einer Matrix transformieren kann. So arbeitet auch die Move-Methode, die wir der Basisklasse hinzufügen:

public virtual void Move(int deltaX, int deltaY) {
    Matrix mat = new Matrix();
    mat.Translate(deltaX, deltaY);
    _path.Transform(mat);
}

Und wie kann ich jetzt ein Objekt mit der Maus verschieben?

Dazu brauchen wir folgende Maus-Ereignisse unseres Formulars: MouseDown, MouseMove und MouseUp.
Das Verschieben läuft nun wie folgt ab:*Im MouseDownEventHandler testen wir, ob die Maus sich auf einem Objekt befindet. Wenn ja merken wir es uns und auch die Position der Maus. *Im MouseMoveEventHandler prüfen wir zuerst, ob wir uns ein Objekt zum Verschieben gemerkt haben (sprich, ob wir im Moment ein Objekt verschieben wollen). Wenn dies der Fall ist, rechnen wir den Unterschied zwischen der aktuellen Maus-Position und der letzten aus und verschieben das Objekt um diese Differenz. Dann speichern wir die Maus-Position als die letzte und zum Schluss lassen wir das Formular noch neu zeichnen, damit die Änderungen auch sichtbar werden. *Im MouseUpEventHandler setzen wir einfach die Variable, in der wir das Objekt zum Verschieben speichern, gleich null.

Lange Rede, kurzer Sinn: Hier ist der Code dazu:

Point _lastMouseLocation;
MyGraphicObject _movingGraphicObject;

protected override void OnMouseDown(MouseEventArgs e) {
    base.OnMouseDown(e);
    // Wenn die Maus außerhalb der Zeichenfläche gedrückt wurde, wird abgebrochen.
    if (!_canvas.Contains(e.Location)) return;
    // Anderenfalls wird die Liste mit den gezeichneten Objekten von hinten (damit
    // das oberste Objekte gefunden wird) durchgegangen und geprüft, über welchem
    // Objekt die Maus sich befindet. 
    for (int i = _graphicObjects.Count - 1; i >= 0; i--) {
        MyGraphicObject go = _graphicObjects[i];
        if (go.Hit(e.Location)) {
            _movingGraphicObject = go;
            break;
        }
    }
    _lastMouseLocation = e.Location;
}

protected override void OnMouseMove(MouseEventArgs e) {
    base.OnMouseMove(e);
    if (_movingGraphicObject != null) {
        // Wenn gerade ein Objekt verschoben werden soll, wird die Differenz zur letzten
        // Mausposition ausgerechnet und das Objekt um diese verschoben.
        _movingGraphicObject.Move(e.X - _lastMouseLocation.X, e.Y - _lastMouseLocation.Y);
        _lastMouseLocation = e.Location;
        // Hier könnte man noch optimieren, indem man immer nur den Bereich
        // neuzeichnet, in dem das Objekt bewegt wurde.
        this.Invalidate();
    }
}

protected override void OnMouseUp(MouseEventArgs e) {
    base.OnMouseUp(e);
    _movingGraphicObject = null;
}

Funktioniert das auch für Text?

Ja. Die GraphicsPath-Klasse besitzt eine Methode AddString. Dabei wird der angegebene Text in Linien und Kurven umgewandelt, die in dem Pfad gespeichert werden. Dabei werden auch unterschiedliche Schriftarten sowie Stile (fett, kursiv, usw.) berücksichtigt.
Wir fügen dem Beispiel eine Klasse MyText hinzu. Nun müssen wir einige Implementierungen der Basisklasse verändern:*Draw: Bei den anderen Objekten haben wir nur die Linie gezeichnet, die das Objekt umgibt (z.B. die Kreislinie). Das kann man bei Text auch machen und bekommt dann eine Outline-Schrift. Hier wollen wir aber den Text normal (also ausgefüllt) zeichnen und verwenden daher FillPath statt DrawPath. Wenn man beides kombinieren würde, könnte man z.B. eine blau gefüllte Schrift mit einer schwarzen Umrandung zeichnen.

public override void Draw(Graphics g) {
    g.FillPath(Pen.Brush, Path);
}

*Hit: Wir überfahren die Schrift nicht nur, wenn wir die Maus über die Linie bewegen, sondern auch, wenn wir uns über dem Inneren befinden. Das heißt Hit sollte nun true zurückliefern, wenn die Maus sich entweder über der Linie oder über dem Inneren befindet:

public override bool Hit(Point pt) {
    return Contains(pt) || base.Hit(pt);
}

Wie kann ich das Zeichnen optimieren, wenn der Bildaufbau beim Verschieben zu langsam ist?

Eine z.T. erhebliche Beschleunigung der Zeichnung lässt sich erreichen, wenn pro Zeichenvorgang nicht das gesamte Control neu gezeichnet wird, sondern nur die Bereiche, wo sich tatsächlich Zeichnungsobjekte befinden bzw. nur die Bereiche, die sich überhaupt geändert haben. Dieses Konzept wird im Snippet "Gezieltes OwnerDrawing" - schnelles Zeichnen bewegter Objekte von ErfinderDesRades umgesetzt.

Ende

Dieser Artikel zeigt eine Methode auf, mit der man mit Hilfe der Maus mit gezeichneten Objekten interagieren kann.

Wer sich weiter mit dem Thema beschäftigen möchte, sollte mal ein Blick auf folgende Diskussionen aus diesem Forum werfen:

A wise man can learn more from a foolish question than a fool can learn from a wise answer!
Bruce Lee

Populanten von Domizilen mit fragiler, transparenter Außenstruktur sollten sich von der Translation von gegen Deformierung resistenter Materie distanzieren!
Wer im Glashaus sitzt, sollte nicht mit Steinen werfen.

progger Themenstarter:in
1.271 Beiträge seit 2005
vor 17 Jahren

Beispielanwendung

Im Grunde ist mein Beispiel herbivore's sehr ähnlich. Ich hab aber zu dem Rechteck auch noch Kreis und Linie als graphische Objekte hinzugefügt.
In dem Fenster sind drei Buttons, mit denen man zufällig erstellte Objekte hinzufügen kann. Darunter befindet sich eine TextBox und noch ein Button, mit deren Hilfe man Texte hinzufügenkann.
Das Verschieben geht ganz einfach mit der Maus.

Bitte beachtet auch die Kommentare im Code!

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;

public abstract class MyGraphicObject {
    /// <summary>
    /// Dieses Pen-Objekt wird verwendet um zu überprüfen, ob ein
    /// Punkt über der Linie des Objekts liegt. Die Farbe ist
    /// hierfür irrelevant. Als Breite hat sich 4 als sinnvoll erwiesen.
    /// </summary>
    static Pen HitTestPen = new Pen(Brushes.Black, 4);
    Pen _pen;
    bool _bVisible = true;
    GraphicsPath _path = new GraphicsPath();

    public MyGraphicObject(Pen pen) {
        _pen = pen;
    }

    protected GraphicsPath Path {
        get { return _path; }
    }

    public Pen Pen {
        get { return _pen; }
    }

    public bool Visible {
        get { return _bVisible; }
        set { _bVisible = value; }
    }

    /// <summary>
    /// Befindet sich der angegebene Punkt über der Linie des Objekts?
    /// </summary>
    public virtual bool Hit(Point pt) {
        return _path.IsOutlineVisible(pt, HitTestPen);
    }

    /// <summary>
    /// Befindet sich der angegebene Punkt innerhalb des Objekts?
    /// </summary>
    public virtual bool Contains(Point pt) {
        return _path.IsVisible(pt);
    }

    public virtual void Draw(Graphics g) {
        g.DrawPath(_pen, _path);
    }

    /// <summary>
    /// Bewegt das Objekt um deltaX in x-Richtung und deltaY in y-Richtung.
    /// </summary>
    public virtual void Move(int deltaX, int deltaY) {
        Matrix mat = new Matrix();
        mat.Translate(deltaX, deltaY);
        _path.Transform(mat);
    }
}

public class MyRectangle : MyGraphicObject {
    public MyRectangle(Pen pen, Rectangle rect)
        : base(pen) {
        Path.AddRectangle(rect);
    }
}

public class MyCircle : MyGraphicObject {
    public MyCircle(Pen pen, Point center, int radius)
        : base(pen) {
        Path.AddEllipse(center.X - radius, center.Y - radius, 2 * radius, 2 * radius);
    }
}

public class MyLine : MyGraphicObject {
    public MyLine(Pen pen, Point start, Point end)
        : base(pen) {
        Path.AddLine(start, end);
    }
}

public class MyText : MyGraphicObject {
    public MyText(Pen pen, string text, FontFamily family, FontStyle style, float emSize, Point origin)
        : base(pen) {
        Path.AddString(text, family, (int)style, emSize, origin, null);
    }

    public override void Draw(Graphics g) {
        g.FillPath(Pen.Brush, Path);
    }

    public override bool Hit(Point pt) {
        return Contains(pt) || base.Hit(pt);
    }
}

public class MainForm : Form {
    Button _btnAddRectangle;
    Button _btnAddCircle;
    Button _btnAddLine;
    Button _btnAddText;
    TextBox _txtText;
    Label _lblMouseLocation;
    List<MyGraphicObject> _graphicObjects = new List<MyGraphicObject>();
    Rectangle _canvas;
    Random _random = new Random();

    public MainForm() {
        InitializeComponent();
    }

    private void InitializeComponent() {
        _lblMouseLocation = new Label();
        _btnAddCircle = new Button();
        _btnAddLine = new Button();
        _btnAddRectangle = new Button();
        _btnAddText = new Button();
        _txtText = new TextBox();

        // Label, das die Maus-Position anzeigt
        _lblMouseLocation.AutoSize = true;
        _lblMouseLocation.Location = new Point(10, 195);
        _lblMouseLocation.Anchor = AnchorStyles.Left | AnchorStyles.Bottom;

        // Button um einen zufälligen Kreis hinzuzufügen
        _btnAddCircle.Text = "Add Circle";
        _btnAddCircle.Width = 100;
        _btnAddCircle.Location = new Point(10, 210);
        _btnAddCircle.Anchor = AnchorStyles.Left | AnchorStyles.Bottom;
        _btnAddCircle.Click += new EventHandler(AddCircle);

        // Button um eine zufällige Linie hinzuzufügen
        _btnAddLine.Text = "Add Line";
        _btnAddLine.Width = 100;
        _btnAddLine.Location = new Point(120, 210);
        _btnAddLine.Anchor = AnchorStyles.Left | AnchorStyles.Bottom;
        _btnAddLine.Click += new EventHandler(AddLine);

        // Button um ein zufälliges Rechteck hinzuzufügen
        _btnAddRectangle.Text = "Add Rectangle";
        _btnAddRectangle.Width = 100;
        _btnAddRectangle.Location = new Point(230, 210);
        _btnAddRectangle.Anchor = AnchorStyles.Left | AnchorStyles.Bottom;
        _btnAddRectangle.Click += new EventHandler(AddRectangle);

        // Button um Text an einer zufälligen Position hinzuzufügen
        _btnAddText.Text = "Add Text";
        _btnAddText.Width = 100;
        _btnAddText.Location = new Point(120, 235);
        _btnAddText.Anchor = AnchorStyles.Left | AnchorStyles.Bottom;
        _btnAddText.Click += new EventHandler(AddText);

        // TextBox, in die man den text eingeben kann der durch _btnAddText hinzugefügt wird
        _txtText.Text = "Text";
        _txtText.Width = 100;
        _txtText.Location = new Point(10, 237);
        _txtText.Anchor = AnchorStyles.Left | AnchorStyles.Bottom;

        this.Controls.Add(_lblMouseLocation);
        this.Controls.Add(_btnAddCircle);
        this.Controls.Add(_btnAddLine);
        this.Controls.Add(_btnAddRectangle);
        this.Controls.Add(_btnAddText);
        this.Controls.Add(_txtText);
        this.Size = new Size(400, 320);
        // Damit geht das Neuzeichnen viel flüssiger
        this.DoubleBuffered = true;
    }

    protected override void OnPaint(PaintEventArgs e) {
        base.OnPaint(e);
        e.Graphics.SmoothingMode = SmoothingMode.HighQuality;
        // Der Bereich, der neugezeichnet werden soll, wird auf die Zeichenfläche
        // beschränkt, damit Objekte, die darüber hinausschauen abgeschnitten werden.
        Region clip = new Region(_canvas);
        clip.Intersect(e.Graphics.Clip);
        e.Graphics.Clip = clip;
        e.Graphics.Clear(Color.White);
        foreach (MyGraphicObject go in _graphicObjects) {
            go.Draw(e.Graphics);
        }
    }

    Point _lastMouseLocation;
    MyGraphicObject _movingGraphicObject;

    protected override void OnMouseDown(MouseEventArgs e) {
        base.OnMouseDown(e);
        // Wenn die Maus außerhalb der Zeichenfläche gedrückt wurde, wird abgebrochen.
        if (!_canvas.Contains(e.Location)) return;
        // Anderenfalls wird die Liste mit den gezeichneten Objekten von hinten (damit
        // das oberste Objekte gefunden wird) durchgegangen und geprüft, über welchem
        // Objekt die Maus sich befindet. 
        for (int i = _graphicObjects.Count - 1; i >= 0; i--) {
            MyGraphicObject go = _graphicObjects[i];
            if (go.Hit(e.Location)) {
                _movingGraphicObject = go;
                break;
            }
        }
        _lastMouseLocation = e.Location;
    }

    protected override void OnMouseMove(MouseEventArgs e) {
        base.OnMouseMove(e);
        if (_movingGraphicObject != null) {
            // Wenn gerade ein Objekt verschoben werden soll, wird die Differenz zur letzten
            // Mausposition ausgerechnet und das Objekt um diese verschoben.
            _movingGraphicObject.Move(e.X - _lastMouseLocation.X, e.Y - _lastMouseLocation.Y);
            _lastMouseLocation = e.Location;
            // Hier könnte man noch optimieren, indem man immer nur den Bereich
            // neuzeichnet, in dem das Objekt bewegt wurde.
            this.Invalidate();
        }
        if (_canvas.Contains(e.Location)) {
            _lblMouseLocation.Text = string.Format("x = {0}; y= {1}", e.X, e.Y);
        }
    }

    protected override void OnMouseUp(MouseEventArgs e) {
        base.OnMouseUp(e);
        _movingGraphicObject = null;
    }

    protected override void OnSizeChanged(EventArgs e) {
        base.OnSizeChanged(e);
        // Wenn sich die Größe des Fensters ändert, wird die Größe der Zeichenfläche angepasst.
        _canvas = new Rectangle(new Point(0, 0),
            new Size(this.ClientSize.Width, this.ClientSize.Height - 70));
        this.Invalidate();
    }

    void AddCircle(object sender, EventArgs e) {
        // Erstellt einen Kreis mit zufälligen Werten für Mittelpunkt, Radius 
        // (mindestens 10 Pixel) und Farbe, der komplett zu sehen ist.
        int x = _random.Next(0, _canvas.Width);
        int y = _random.Next(0, _canvas.Height);
        int radius = _random.Next(10, Math.Max(11, Math.Min(Math.Min(_canvas.Width - x, x),
            Math.Min(_canvas.Height - y, y))));
        Pen p = new Pen(Color.FromArgb(_random.Next(0, 256), _random.Next(0, 256), _random.Next(0, 256)), 1);
        _graphicObjects.Add(new MyCircle(p, new Point(x, y), radius));
        this.Invalidate();
    }

    void AddLine(object sender, EventArgs e) {
        // Erstellt eine Linie mit zufälligen Werten für Start-, Endpunkt und
        // Farbe, die komplett zu sehen ist.
        int x1 = _random.Next(0, _canvas.Width);
        int x2 = _random.Next(0, _canvas.Width);
        int y1 = _random.Next(0, _canvas.Height);
        int y2 = _random.Next(0, _canvas.Height);
        Pen p = new Pen(Color.FromArgb(_random.Next(0, 256), _random.Next(0, 256), _random.Next(0, 256)), 1);
        _graphicObjects.Add(new MyLine(p, new Point(x1, y1), new Point(x2, y2)));
        this.Invalidate();
    }

    void AddRectangle(object sender, EventArgs e) {
        // Erstellt ein Rechteck mit zufälligen Werten für Position, Breite/Höhe 
        // (mindestens 15 Pixel) und Farbe, das komplett zu sehen ist.
        int x = _random.Next(0, _canvas.Width);
        int y = _random.Next(0, _canvas.Height);
        int w = _random.Next(15, Math.Max(16, _canvas.Width - x));
        int h = _random.Next(15, Math.Max(16, _canvas.Height - x));
        Pen p = new Pen(Color.FromArgb(_random.Next(0, 256), _random.Next(0, 256), _random.Next(0, 256)), 1);
        _graphicObjects.Add(new MyRectangle(p, new Rectangle(x, y, w, h)));
        this.Invalidate();
    }

    void AddText(object sender, EventArgs e) {
        // Erstellt ein Text-Objekt mit dem eingegebenen Text und zufälligen 
        // Werten für Position und Farbe.
        if (string.IsNullOrEmpty(_txtText.Text)) return;
        int x = _random.Next(0, _canvas.Width - 30);
        int y = _random.Next(0, _canvas.Height - 30);
        int size = _random.Next(10, 75);
        Pen p = new Pen(Color.FromArgb(_random.Next(0, 256), _random.Next(0, 256), _random.Next(0, 256)), 1);
        _graphicObjects.Add(new MyText(p, _txtText.Text, FontFamily.GenericSerif, FontStyle.Regular, size, new Point(x, y)));
        this.Invalidate();
    }
}

static class Program {
    [STAThread]
    static void Main() {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(new MainForm());
    }
}

A wise man can learn more from a foolish question than a fool can learn from a wise answer!
Bruce Lee

Populanten von Domizilen mit fragiler, transparenter Außenstruktur sollten sich von der Translation von gegen Deformierung resistenter Materie distanzieren!
Wer im Glashaus sitzt, sollte nicht mit Steinen werfen.