|
» myCSharp.de Diskussionsforum |
|
|
|
Autor
 |
|
|
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:
C#-Code: |
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:
C#-Code: |
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.
C#-Code: |
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:
C#-Code: |
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:
C#-Code: |
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:
C#-Code: |
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:
C#-Code: |
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:
C#-Code: |
Point _lastMouseLocation;
MyGraphicObject _movingGraphicObject;
protected override void OnMouseDown(MouseEventArgs e) {
base.OnMouseDown(e);
if (!_canvas.Contains(e.Location)) return;
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) {
_movingGraphicObject.Move(e.X - _lastMouseLocation.X, e.Y - _lastMouseLocation.Y);
_lastMouseLocation = e.Location;
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.
C#-Code: |
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:
C#-Code: |
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:
- Linie wie bei einem Bildbearbeitungsprogramm
- DrawLine -> Enden verschieben
|
|
03.12.2006 15:14
|
E-Mail |
Beiträge des Benutzers |
zu Buddylist hinzufügen
|
|
|
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!
C#-Code: |
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
public abstract class MyGraphicObject {
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; }
}
public virtual bool Hit(Point pt) {
return _path.IsOutlineVisible(pt, HitTestPen);
}
public virtual bool Contains(Point pt) {
return _path.IsVisible(pt);
}
public virtual void Draw(Graphics g) {
g.DrawPath(_pen, _path);
}
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();
_lblMouseLocation.AutoSize = true;
_lblMouseLocation.Location = new Point(10, 195);
_lblMouseLocation.Anchor = AnchorStyles.Left | AnchorStyles.Bottom;
_btnAddCircle.Text = "Add Circle";
_btnAddCircle.Width = 100;
_btnAddCircle.Location = new Point(10, 210);
_btnAddCircle.Anchor = AnchorStyles.Left | AnchorStyles.Bottom;
_btnAddCircle.Click += new EventHandler(AddCircle);
_btnAddLine.Text = "Add Line";
_btnAddLine.Width = 100;
_btnAddLine.Location = new Point(120, 210);
_btnAddLine.Anchor = AnchorStyles.Left | AnchorStyles.Bottom;
_btnAddLine.Click += new EventHandler(AddLine);
_btnAddRectangle.Text = "Add Rectangle";
_btnAddRectangle.Width = 100;
_btnAddRectangle.Location = new Point(230, 210);
_btnAddRectangle.Anchor = AnchorStyles.Left | AnchorStyles.Bottom;
_btnAddRectangle.Click += new EventHandler(AddRectangle);
_btnAddText.Text = "Add Text";
_btnAddText.Width = 100;
_btnAddText.Location = new Point(120, 235);
_btnAddText.Anchor = AnchorStyles.Left | AnchorStyles.Bottom;
_btnAddText.Click += new EventHandler(AddText);
_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);
this.DoubleBuffered = true;
}
protected override void OnPaint(PaintEventArgs e) {
base.OnPaint(e);
e.Graphics.SmoothingMode = SmoothingMode.HighQuality;
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);
if (!_canvas.Contains(e.Location)) return;
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) {
_movingGraphicObject.Move(e.X - _lastMouseLocation.X, e.Y - _lastMouseLocation.Y);
_lastMouseLocation = e.Location;
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);
_canvas = new Rectangle(new Point(0, 0),
new Size(this.ClientSize.Width, this.ClientSize.Height - 70));
this.Invalidate();
}
void AddCircle(object sender, EventArgs e) {
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) {
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) {
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) {
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());
}
}
|
|
|
03.12.2006 15:14
|
E-Mail |
Beiträge des Benutzers |
zu Buddylist hinzufügen
|
|
|
|