Dieser Artikel beschränkt sich auf Draggen innerhalb einer Anwendung. Hier gelten spezifische Bedingungen, und wenn man dem Rechnung trägt, kann man Drag&Drop einfacher und sicherer implementieren, als wenn man sich von der Framework-Unterstützung für Drag&Drop leiten läßt. Die Framework-Unterstützung ist nämlich auf anwendungsübergreifendes Drag&Drop ausgelegt. |
übliche Vorgehensweise:- Das Quell-Control verarbeitet die Events Quell_MouseDown() und Quell_MouseMove(), um den User-Input "Dieses soll gezogen werden" zu identifizieren.
Ist ein solcher Input identifiziert, wird ein DataObject mit den zu ziehenden Daten befüllt, und mit Quell.DodragDrop() wird der DragVorgang dann gestartet (unter Angabe der zulässigen DropEffekts (Kombinationen möglich aus "Move", "Copy", "Link", "Scroll")).
- Das Ziel-Control muß das Ziel_DragOver()-Event verarbeiten, und ggfs. unter Berücksichtigung der Modifizierer-Tasten den speziellen DragDropEffekt festlegen. Geschieht dieses nicht, bleibt er auf DropEffekts.None, und auf dem Ziel kann nicht gedropt werden.
Alternativ kann das Ziel auch _DragEnter() verarbeiten, aber _DragOver() hat den Vorzug, dass während des Ziehens noch mittels Modifizier-Tasten (Shift, Strg, Alt) der gewünschte DropEffekt modifiziert werden kann.
- Im Ziel_DragDrop()-Event ist die Eingabe beendet, und die vom User gemeinte Aktion wird umgesetzt.
Zwei erhebliche Mängel dieser Vorgehensweise:- Innerhalb eines Drag-Vorganges kann die IDE keine Fehler fangen!
Ursache dieser Eigenheit ist die anwendungsübergreifende Ausrichtung: Der Drag-Vorgang wird an einen OLE-Mechanismus weitergegeben, und die Events kommen dann vom System. Der OLE-Mechanismus unterdrückt alle Exceptions, da sonst auch DragOver-Fehler von eigentlich unbeteiligten Dritt-Anwendungen einen Drag-Vorgang stören würden, wenn zufällig über die Dritt-Anwendung gezogen wird.
Für das _DragOver()-Event folgt daraus: Den Code möglichst einfach halten. Da das Programm einfach weiter läuft, verursachen selbst einfachste Fehler stundenlanges Suchen.
Noch ungünstiger wirkt sich diese Eigenheit im _DragDrop()-Event aus, wenn die gemeinte Aktion umgesetzt werden soll. Hier kann die gesamte Anwendung betroffen sein, und ein Fehler auch Datenbestände beschädigen. Auch werden Methoden aufgerufen, die evtl. später noch weiterentwickelt werden, sodaß ein ursprünglich funktionierendes Dragging sich auf einmal fehlerhaft verhält (wie gesagt: ohne jede Rückmeldung!). Hier ohne Debug-Unterstützung der IDE zu programmieren ist ziemlich riskant.
Edit März 2012: Die haben den Bug behoben! VisualStudio2010 und die .Net4-Runtime behandeln nun auch Fehler innerhalb von Drag&Drop-Events korrekt, also mit CodeStop und pipapo, und ermöglichen somit eine gezielte Fehlersuche.
- Das DataObject ist überflüssig und verleitet zu unsicheren Implementationen, bei denen unzulässige Daten aus anderen Anwendungen auf die eigene gezogen werden können.
Lösung: Draggen ohne DataObject und _DragDrop-Event :)
Das ist sogar einfacher, weil ein Event entfällt, und der Umgang mit dem DataObject. Ein DataObject kann mit beliebigen Daten befüllt werden, dementsprechend umständlich und fehleranfällig ist seine Handhabung. Dabei sind statt amorpher Daten genau 4 typsichere Informationen erforderlich, um den Zustand jedes DragVorgangs fehlerfrei zu bewerten:
- das Quell-Control
- die Position der Maus über dem Quell-Control, zum Zeitpunkt des DragStarts
- das Ziel-Control
- die Position der Maus über dem Ziel-Control, zum Zeitpunkt des DragOvers/DragDrops
Diese Informationen mussten auch bei der herkömmlichen Vorgehensweise ermittelt werden: Die Quell-Control-Infos brauchte man, um das Quell-Item zu ermitteln, anhand dessen das DataObject befüllt wurde, die Ziel-Control-Infos, um festzulegen, wo eingefügt wird.
Das _DragDrop-Event kann einfach ausgelassen werden - der endgültige DropEffekt wird ebensogut von
Control.DoDragDrop() zurückgegeben. Dadurch verlagert sich die Umsetzung der gemeinten Aktion in das
_MouseMove() des Quell-Controls, was recht praktisch ist, weil so hat man die Quell-Control-Infos gleich zugreifbar.
Vor allem aber hat man die Debug-Unterstützung wieder, bei der Entwicklung der umzusetzenden Aktion.
Benutzer-freundlichkeit
Dazu gehört m.E. wesentlich eine Markierung des Drop-Zieles, schon im DragOver. Um dem User Feedback zu geben, wo sein Drag-Item droppen wird.
Wird hier nicht weiter besprochen, ist im Sample-Code dabei.
Im Download enthaltener Code enthält auch eine recht vollständige Lösung für Treeview, ListView, Listbox und DatagridView, aber in der Praxis mag man es evtl. lieber kleiner und überschaubarer haben:
C#-Code: |
using System;
using System.Drawing;
using System.Windows.Forms;
namespace BaseDrag {
public partial class frmBaseDrag : Form {
private static DataObject _dumData = new DataObject();
private static DragDropEffects _allEffects =
DragDropEffects.Move | DragDropEffects.Copy | DragDropEffects.Link | DragDropEffects.Scroll;
private ListBox _src;
private ListBox _dest;
public frmBaseDrag() {
InitializeComponent();
this.Location = Screen.PrimaryScreen.WorkingArea.Location;
foreach (var lb in new ListBox[] { listBox1, listBox2, listBox3 }) {
lb.MouseMove += ListBox_MouseMove;
lb.DragOver += ListBox_DragOver;
lb.DragLeave += (s, e) => Highlighter.Off();
}
}
private void ListBox_MouseMove(object sender, MouseEventArgs e) {
if (e.Button != MouseButtons.Left) return;
_src = (ListBox)sender;
if (_src.SelectedIndex < 0) return;
try {
var effect = _src.DoDragDrop(_dumData, _allEffects);
if (effect == DragDropEffects.None) return;
var iDest = _dest.IndexFromPoint(_dest.PointToClient(Control.MousePosition));
if (iDest < 0) iDest = _dest.Items.Count;
switch (effect) {
case DragDropEffects.Move:
_dest.Items.Insert(iDest, _src.SelectedItem);
_src.Items.RemoveAt(_src.SelectedIndex);
break;
case DragDropEffects.Copy:
_dest.Items.Insert(iDest, _src.SelectedItem);
break;
case DragDropEffects.Link:
_dest.Items.Insert(iDest, string.Concat("linked with: ", _src.SelectedItem));
break;
}
} finally {
Highlighter.Off();
_src = null;
}
}
private void ListBox_DragOver(object sender, DragEventArgs e) {
if (_src == null) return;
try {
_dest = (ListBox)sender;
var pt = new Point(e.X, e.Y);
var iDest = _dest.IndexFromPoint(_dest.PointToClient(pt));
if (iDest < 0) iDest = _dest.Items.Count;
switch (Control.ModifierKeys) {
case Keys.None:
e.Effect = DragDropEffects.Move;
if (_src == _dest) {
var diff = iDest - _src.SelectedIndex;
if (diff == 0 || diff == 1) e.Effect = DragDropEffects.None;
}
break;
case Keys.Shift:
e.Effect = DragDropEffects.Copy;
break;
case Keys.Control:
e.Effect = DragDropEffects.Link;
break;
default:
e.Effect = DragDropEffects.None;
break;
}
if (e.Effect == DragDropEffects.None) return;
if (_dest.Items.Count == 0) {
Highlighter.FullWidth(_dest, 0, _dest.ItemHeight);
} else if (iDest == _dest.Items.Count) {
Highlighter.After(_dest, _dest.GetItemRectangle(_dest.Items.Count - 1));
} else {
Highlighter.Before(_dest, _dest.GetItemRectangle(iDest));
}
} catch (Exception ex) {
var sError = ex.ToString();
System.Diagnostics.Debugger.Break();
}
}
}
}
|
Einfacher geht es leider nicht, will man mehrere DropEffects unterstützen, Debug-Unterstützung der IDE, und die Ziel-Item highlighten.
Draggen mit anderen Controls ist prinzipiell gleich, nur das Ermitteln des Items unter der Maus ist bei Treeview, ListView, Listbox und DatagridView je spezifisch.
Und bei Treeview ist die
"SelfDrag mit Effect.Move"-Regel anders: Ein Node darf weder in sich selbst, seine ChildNodes noch seinen Parent abgelegt werden.
Straight-forward dagegen die Lösung mit Unterstützung der (beiliegenden) DragDropper-Klasse:
C#-Code: |
using System;
using System.Windows.Forms;
using DragDrop;
namespace BaseDrag {
public partial class frmDragDropper : Form {
public frmDragDropper() {
InitializeComponent();
var boxes = new ListBox[] { listBox1, listBox2, listBox3 };
foreach (var b in boxes) {
var dropper = new DragDropper(b);
foreach (var bx in boxes) {
dropper.AddJob(bx, DragDropEffects.Move, DragDropEffects.Copy, DragDropEffects.Link);
}
dropper.Drop += dropper_Drop;
}
}
void dropper_Drop(object sender, DragDropper.DropEventArgs e) {
var src = (ListBox)e.Origin.Control;
var srcItem = src.Items[e.Origin.Index].ToString();
var dest = (ListBox)e.Target.Control;
switch (e.Effect) {
case DragDropEffects.Copy:
dest.Items.Insert(e.Target.Index, srcItem);
break;
case DragDropEffects.Move:
dest.Items.Insert(e.Target.Index, srcItem);
src.Items.RemoveAt(e.Origin.Index);
break;
case DragDropEffects.Link:
dest.Items.Insert(e.Target.Index, "linked with: " + srcItem);
break;
}
}
}
}
|
Hier ist DragOver für die verschiedenen Controls schon implementiert und weggekapselt, sodaß man sich ganz auf die Umsetzung der gemeinten Aktion konzentrieren kann.
(Entsprechend aufwändig aber auch die
DragDropper-Klasse)
Absicherung bei anwendungsübergreifendem Drag & Drop
Hier macht die Verwendung des DataObjects natürlich Sinn. Es besteht aber ebenfalls das Problem der fehlenden IDE-Debug-Unterstützung.
Man kann im
Drop()-Event eine Verzögerung einbauen, sodaß der eigentliche Code erst ausgeführt wird, nachdem das Drop-Event abgelaufen ist. Hierzu kann das
Application.Idle() - Event verwendet werden - Variante mit anonymen Methoden:
C#-Code: |
void listBox1_DragDrop(object sender, DragEventArgs e) {
Action ExecDrop = () => {
var leftCut = Application.StartupPath.Length;
foreach (string s in ((DataObject)e.Data).GetFileDropList()) {
listBox1.Items.Add(e.Effect.ToString() + " " + s.Substring(leftCut));
}
};
AppDelay(ExecDrop);
}
public void AppDelay(Action action) {
EventHandler idle = null;
idle = (s, e2) => {
Application.Idle -= idle;
action();
};
Application.Idle += idle;
}
|
Zum Schluß noch ein
Dankeschön! an die PowerUser, die mich mit Gegenlesen und Kommentaren ein Stück weiter gebracht haben.