Laden...

(nicht ganz) alles erschlagendes DragnDrop

Erstellt von ErfinderDesRades vor 16 Jahren Letzter Beitrag vor 13 Jahren 10.997 Views
ErfinderDesRades Themenstarter:in
5.299 Beiträge seit 2008
vor 16 Jahren
(nicht ganz) alles erschlagendes DragnDrop

DragnDrop ist ein hervorragend userfreundliches Eingabe-Instrument - intuitiv, Wysiwyg, schreibfehler-sicher, flexibel.
Es ist allerdings auch nicht ganz trivial zu implementieren, was auch mit seiner Mächtigkeit zusammenhängt.

**3 Grundelemente müssen immer zusammenwirken:**1.Das Origin-Control muß die Events Origin_MouseDown(), _MouseMove() verarbeiten, um den User-Input "Dieses soll gezogen werden" zu identifizieren, und Origin.DodragDrop() aufzurufen (unter Angabe der zulässigen DropEffekts (Kombinationen möglich aus "Move", "Copy", "Link", "Scroll")). 1.Das Target-Control muß das Target_DragOver()-Event verarbeiten, und ggfs. unter Berücksichtigung der Modifizierer-Tasten den speziellen DragDropEffekt festlegen. Geschieht dieses nicht, bleibt er auf DropEffekts.None, und der Drag-Vorgang entwickelt sich in einer wenig ergiebigen Ausprägung ;o). 1.Im Target_DragDrop()-Event ist die Eingabe beendet, und die daraus folgende Aktion umzusetzen.

Letzteres kann recht komplex werden, wenn von mehren Controls gedragt werden soll, und wenn verschiedene DropEffects unterstützt werden sollen.


[B]Außerordentlich ärgerliche Eigenart:[/B]
[U]Die IDE fängt keine Fehler!![/U]
Tritt im _DragOver() ein Fehler auf, kehrt die Code-Ausführung einfach zum Aufrufer zurück, und der DropEffekt bleibt auf .None festgesetzt.
Noch schlimmer: Auch im _DragDrop() bekommt der Programmierer kein Feedback, wenn er Mist verzapft hat (was an dieser Stelle leicht passieren kann). Das Dropping wird einfach ignoriert - ebensogut hätte der User [Esc] drücken können.
Ich weiß nicht, ob das ein Bug der IDE ist, ein internes Threading-Problem, oder sich aus Erfordernissen anwendungsübergreifender Kommunikation ergibt. Für mich sind es jedenfalls die übelsten mir je unterlaufenen Fehler, wenn das Programm sich nicht verhält, wie es soll, dabei aber einfach weiterläuft, als sei nichts gewesen.

Das DataObject
Dem Drag-Vorgang kann ein DataObject mitgegeben werden. Da kann man beliebige Daten hineintun, zusammen mit einer Information, was für Daten nun drin ist.
Auf diese Weise kann der Drag-Empfänger checken, ob er für die Verarbeitung des Drop-Events vorgesehen ist.
Das alles ist etwas umständlich und auch unsicher.
Erstmal muß man die zu draggenden Daten ins DataObject stopfen, dazu die Zusatz-Info. Und beim DragOver des Empfängers muß der die Zusatz-Info auswerten. Und beim DragDrop müssen die Daten wieder ausgepackt werden, und in typisierte Form transferiert, denn im DataObject ist nur Typ Object drin.
Die Gefahr besteht darin, daß die Zusatz-Info zu ungenau ist. Als Beispiel das Beispiel zu DoDragDrop der MSDN: Dort wird ein String von einer Listbox in die andere gedraggt. Die Zusatz-Info im DataObject lautet: "Hier ist ein String drin."
Ja, jetzt kannich im Notepad einen Text markieren, und ebenso in die Listbox draggen, weil, wenn Notepad ein Text-Dragging startet, stattet er das DataObject ebenfalls mit der ZusatzInfo "Hier String" aus. Aber ich glaube nicht, daß das das beabsichtigte Verhalten des MSDN-Beispiels ist, sieht jedenfalls recht komisch aus 😉.

Vereinfachter Anwendungsfall "Draggen innerhalb einer Anwendung"
Hierbei ist das DataObject überflüssig. Es müssen nicht beliebige Daten transportiert werden, sondern folgende (typisierte und immer gleichartige) Daten decken alle Erfordernisse ab:*Das StartControl *Die Position der Maus überm StartControl, zum Zeitpunkt des DragStarts

Anhand dieser Infos kann der Empfänger checken, ob er für den Empfang von diesem StartControl vorgesehen ist, und die Mausposition ermöglicht bei Treeview, Listview, Listbox, DatagridView auch die Identifikation des "gemeinten" Items, welches gedragt werden soll.
Also statt *Daten anhand des StartControls ermitteln *Daten in DataObject stopfen *Daten wieder herausholen *Daten in typisierte Objekte überführen *Daten verarbeiten

nur noch*StartControl und Mausposition übermitteln *Daten anhand dieser Info ermitteln *Daten verarbeiten

Der erste Punkt "StartControl und Mausposition übermitteln" ist freundlicherweise bei jedem Dragvorgang derselbe, sodaß man codemäßig stark vereinfachen kann.
Anfangs habich einfach von DataObject abgeleitet, und die Properties "StartControl" und "Mausposition" hinzugefügt. Ging schomal sehr gut.

Inzwischen habe ich ein (nicht ganz) alles erschlagendes DragnDrop geproggt, also einen Satz Klassen, der die schematisierbaren Implementations-Elemente weitestgehend wegkapselt.
Der Code-User kann einen "DragDropper" mit einem "TargetControl" initialisieren, und dann "DragJobs" adden, also Informationen, von welchem Control mit welchen DragDropEffects auf dieses Target gezogen werden darf.
Dann musser nur noch im DragDropper.Drop-Event die mit dem Zieh-Vorgang gemeinte Aktion umsetzen. Dabei stehen ihm folgende Informationen der DragDropper.DropEventArgs zur Verfügung:*e.Origin.Control - Das Control, von dem gezogen wurde *e.Origin.Mouse - Die Mausposition überm Origin-Control, zum Zeitpunkt des DragStarts *e.Origin.Index - Bei Listen-Controls (Listbox, ListView, DataGridView) der Index des Items unter der Maus *e.Target.Control - Das Control, auf das gedropt wurde *e.Target.Mouse - Die Mausposition überm Target-Control, zum Zeitpunkt des Droppens *e.Target.Index - Bei Listen-Controls der Index des Items unter der Maus *Effect - Der DragDrop-Effect, wie er durch den Abgleich der Modifier-Tasten mit den im DragJob erlaubten DragDropEffect sich ergibt

Optional werden auch noch 2 Validate-Events geboten:* ValidateDrag - ob vom Origin-Control gezogen werden kann

  • ValidateDrop - ob aufs Target-Control gedropt werden kann

Wie gesagt: optional - für ein Wald-und-Wiesen-Dragging reicht die interne Validierung vollkommen.
Die überprüft nämlich bei TreeView, Listbox, ListView, DataGridView, ob sich unter der Maus ein Item befindet.
Und beim Droppen werden unzulässige Drop-Targets unterdrückt (etwa einen Treenode in sich selbst ablegen, mit DropEffect.Move ).
Dann hamwa noch den Highlighter, der die Drop-Targets beim DragOver highlightet.
Die außerordentlich ärgerlichen Eigenart ist ausgebootet, indem ich DragDropper.Drop-Event nicht innerhalb des (gekapselten) Control_DragDrop-Events auslöse, sondern erst nach dem (ebenfalls gekapselten) Aufruf von Control.DoDragDrop(). Da DoDragDrop() eine blockierende Funktion ist, ist somit der eigentliche DragVorgang mit seiner ärgerlichen Eigenart komplett durchgelaufen, bevor die eigentliche (und fehleranfällige) Verarbeitung der Daten beginnt.

**Zur Beispiel-Solution:***Ein Form, wo man Labels, Listbox-Items, Treenodes recht freizügig herumdraggen kann. *Auch intern kann man den Treeview bzw. die Listbox per DragnDrop umräumen. *Wo sinnvoll, ist sowohl DragDropEffects.Move als auch .Copy erlaubt. *ValidateDragStart-Demo: Für die ersten beiden Treenodes wird nur DropEffect.Copy zugelassen. *Ein 2. Form zugefügt, um zu demonstrieren, wie man von einer auf die andere Form draggt.

Ich zeige mal den Form-Code (Ur-Fassung, jetzt nicht mehr ganz korrekt) :


using System;
using System.Drawing;
using System.Windows.Forms;

namespace DragDrop {
   public partial class Form1 : Form {

      private DragDropper _TreeViewDropper;
      private DragDropper _ListboxDropper;

      public Form1() {
         InitializeComponent();
         TreeView1.Sorted = true;
         TreeView1.ExpandAll();
         TreeView1.NodeMouseClick += TreeView1_NodeMouseClick;
         _TreeViewDropper = new DragDropper(this.TreeView1);
         _TreeViewDropper.AddJob(lbError, DragDropEffects.Move);
         _TreeViewDropper.AddJob(Label1, DragDropEffects.Copy);
         _TreeViewDropper.AddJob(Listbox1, DragDropEffects.Move | DragDropEffects.Copy);
         _TreeViewDropper.AddJob(TreeView1, DragDropEffects.Move | DragDropEffects.Copy);
         _TreeViewDropper.Drop += _TreeViewDropper_Drop;
         _ListboxDropper = new DragDropper(this.Listbox1);
         _ListboxDropper.AddJob(Label1, DragDropEffects.Copy);
         _ListboxDropper.AddJob(Listbox1, DragDropEffects.Move | DragDropEffects.Copy);
         _ListboxDropper.AddJob(TreeView1, DragDropEffects.Move | DragDropEffects.Copy);
         _ListboxDropper.Drop += _ListboxDropper_Drop;
      }

      private void _ListboxDropper_Drop(object Sender, DragDropper.DropEventArgs e) {
         if (e.Origin.Control == Listbox1) {
            int Indx = e.Origin.Index;
            Listbox1.Items.Insert(e.Target.Index, Listbox1.Items[Indx]);
            if (e.Effect == DragDropEffects.Move) {
               if (e.Target.Index < Indx) Indx++;
               Listbox1.Items.RemoveAt(Indx);
            }
         }
         else if (e.Origin.Control == Label1) {
            Listbox1.Items.Insert(e.Target.Index, Label1.Name);
         }
         else if (e.Origin.Control == TreeView1) {
            TreeNode ndOrigin = TreeView1.GetNodeAt(e.Origin.Mouse);
            Listbox1.Items.Insert(e.Target.Index, ndOrigin.Text);
            if (e.Effect == DragDropEffects.Move) {
               Helpers.NodeCutOut(ndOrigin);
            }
         }
      }

      private void _TreeViewDropper_Drop(object Sender, DragDropper.DropEventArgs e) {
         TreeNode ndTarget = TreeView1.GetNodeAt(e.Target.Mouse);
         TreeNodeCollection Nodes = ndTarget == null ? TreeView1.Nodes : ndTarget.Nodes;
         if (e.Origin.Control == Listbox1) {
            int Indx = e.Origin.Index;
            Nodes.Add(Listbox1.Items[Indx].ToString());
            if (e.Effect == DragDropEffects.Move) Listbox1.Items.RemoveAt(Indx);
         }
         else if (e.Origin.Control == Label1) { Nodes.Add(Label1.Name); }
         else if (e.Origin.Control == lbError) { Nodes.Add(((object)lbError) as TreeNode); }
         else if (e.Origin.Control == TreeView1) {
            TreeNode ndOrigin = TreeView1.GetNodeAt(e.Origin.Mouse);
            if (e.Effect == DragDropEffects.Move) {
               ndOrigin.Remove();
               Nodes.Add(ndOrigin);
            }
            else { Nodes.Add(Helpers.NodeCloneByText(ndOrigin)); }
         }
         if (ndTarget != null) ndTarget.Expand();
      }


      /// <summary>
      /// bei Rechtsklick auf einen Treenode diesen entfernen, Children in den Parent hängen
      /// </summary>
      private void TreeView1_NodeMouseClick(object sender, TreeNodeMouseClickEventArgs e) {
         if (e.Button == MouseButtons.Right) { Helpers.NodeCutOut(e.Node); }
      }

      private void btToggleDragEnable_CheckedChanged(object sender, EventArgs e) {
         if (btToggleDragEnable.Checked) {
            _TreeViewDropper.AddJob(Listbox1, DragDropEffects.Move | DragDropEffects.Copy);
         }
         else { _TreeViewDropper.RemoveJob(Listbox1); }
      }
   }
}

Ich denke, herkömmlich geproggt wäre man schnell mit mehreren hundert Zeilen Code dabei, bei den ganzen MouseMoves, MouseDowns, DragOvers, DragLeaves, DragDrops, die zu proggen wären.

Ich empfehle auch, das CodeBeispiel der Hilfe zur DoDragDrop()-Funktion mal in einem Form zu implementieren.
Mein Ansatz ist ja schon recht eigenwillig, und der DragDropper-Code, ich denk, sehr schwer verständlich, und man darf ruhig mal gesehen haben, wies eigentlich gedacht ist.

**Also nochmal die Features in Kürze:***Minimaler Aufwand, einen "DragJob" zu konfigurieren *Gewährleistung, daß nur vom (konfigurierten) Origin-Control aufs DragTarget-Control gezogen werden kann. *Basis-Validierung (z.B. Item-Dragging ohne Item unter der Maus wird abgewiesen, keine Items in sich selbst ablegen) *sicheres Debuggen der folgenden Datenverarbeitung - kein Verschlucken von Exceptions *Target-Item-Highlighter *zusätzliche Events "ValidateDragStart" und "ValidateDrop"

Schlagwörter: Drag, Drop, DragnDrop, Drag and Drop, Dragging

Der frühe Apfel fängt den Wurm.

ErfinderDesRades Themenstarter:in
5.299 Beiträge seit 2008
vor 14 Jahren

Hier nochmal ein ganz einfaches Sample, um das Grundprinzip zu zeigen:
Vom Ziel-Control wird ein DragDropper erstellt, und per .AddJob() wird ihm mitgeteilt, von welche(m/n) Control(s) drauf gezogen werden kann:

using System;
using System.Drawing;
using System.Windows.Forms;

namespace DragDrop {
   public partial class Form1 : Form {
      private DragDropper _Dropper;
      public Form1() {
         InitializeComponent();
         _Dropper = new DragDropper(pictureBox1);
         _Dropper.AddJob(treeView1, DragDropEffects.Move);
         _Dropper.Drop += new EventHandler<DragDropper.DropEventArgs>(_Dropper_Drop);
      }

      void _Dropper_Drop(object sender, DragDropper.DropEventArgs e) {
         TreeView src = (TreeView)e.Origin.Control;
         MessageBox.Show("dropped from: " + src.Name + ": " + src.GetNodeAt(e.Origin.Mouse).Text);
      }
   }
}

Der frühe Apfel fängt den Wurm.

1.820 Beiträge seit 2005
vor 13 Jahren

Hallo!

Sehr schöner Artikel.

Allerdings hat die Komponente mit der expliziten Angabe der Quell- und Ziel-Controls einen Schönheitsfehler.
Kann eine Anwendung über AddIns so erweitert werden, dass auch zusätzliche UI-Elemente dazu kommen, welche u.U. ebenfalls Daten draggen könnten, müsste jedes AddIn die entsprechende Dropper-Instanz suchen, sich dort eintragen und ggf. beim Entladen wieder austragen.
Evtl. könnte man EINEN Dropper instanziieren, welcher dann für sämtliche Drag'n'Drop-Vorgänge zuständig wäre (dann müssten bei AddJob 3 Parameter angegeben werden: Quell-Control, Ziel-Control, Effect) und man könnte diese Instanz in einen DI-Container packen.

Nobody is perfect. I'm sad, i'm not nobody 🙁