Listenfeld: Reihenfolge mehrerer Einträge ändern

Wir haben bereits in mehreren Beiträgen beschrieben, wie Sie die individuelle Reihenfolge von Elementen einer Tabelle über den Inhalt eines Feldes etwa namens „ReihenfolgeID“ einstellen können – zum Beispiel in Listenfeldern oder Unterformularen in der Datenblattansicht. Dort haben wir die Reihenfolge dann durch Markieren der Einträge und anschließendes Betätigen etwa von Schaltflächen mit Beschriftungen wie „Ganz nach oben“, „Nach oben“, „Nach unten“ oder „Ganz nach unten“ geändert. Im vorliegenden Beitrag schauen wir uns nun an, wie wir im Listenfeld die Reihenfolge für mehrere Einträge gleichzeitig ändern können.

Mehrfach-Reihenfolge im Listenfeld

Listenfelder haben gegenüber der Datenblattansicht den großen Vorteil, dass Sie damit nicht nur zusammenhängende Einträge markieren können, sondern sogar Einträge, die beliebig im Listenfeld verteilt sind. Die einzige Voraussetzung dafür ist, dass Sie für die Eigenschaft Mehrfachauswahl des Listenfeldes den Wert Einzeln oder Erweitert auswählen. Welchen Sie nutzen, hängt von den Anforderungen und den Gewohnheiten der Benutzer ab – für die hier vorgestellte Lösung funktionieren beide gleich gut.

Entwurf des Beispielformulars

Das Beispielformular bauen wir wie in Bild 1 auf. Wir fügen ein Listenfeld namens lstKategorien ein sowie vier Schaltflächen namens cmdTop, cmdUp, cmdDown und cmdBottom.

Entwurf des Formulars frmMehrreihenfolgeListBox

Bild 1: Entwurf des Formulars frmMehrreihenfolgeListBox

Außerdem hinterlegen wir als Datensatzherkunft für das Listenfeld die Tabelle tblKategorien, deren Entwurf wie in Bild 2 aussieht. Genau genommen verwenden wir sogar eine Abfrage auf Basis dieser Tabelle, welche die Datensätze nach dem Feld ReihenfolgeID sortiert:

Entwurf der Tabelle tblKategorien

Bild 2: Entwurf der Tabelle tblKategorien

SELECT tblKategorien.KategorieID, tblKategorien.Kategoriename, tblKategorien.ReihenfolgeID FROM tblKategorien ORDER BY tblKategorien.ReihenfolgeID;

Die Eigenschaft Spaltenanzahl des Listenfeldes stellen wir auf 3 ein, die Eigenschaft Spaltenbreiten auf 0cm;5cm. Dadurch erscheinen nur das zweite und das dritte Feld der Datensatzherkunft im Listenfeld, also die Felder Kategoriename und ReihenfolgeID.

Definition des Änderns der Reihenfolge

Bevor wir uns an die Programmierung begeben, müssen wir noch herausfinden, wie die Reihenfolge überhaupt geändert werden soll. Bei einem einzelnen Element, dessen Reihenfolge verschoben werden soll, ist das kein Problem: Hier ermitteln wir die ReihenfolgeID des zu verschiebenden Elements und den des Zielelements und nehmen entsprechende Änderungen an den Inhalten dieses Felds für die betroffenen Datensätze vor.

Bei mehreren gleichzeitig zu verschiebenden Datensätzen, die darüberhinaus noch nicht einmal zusammenhängen müssen, ist allerdings zuvor zu definieren, welcher Datensatz wo landet. Wenn Sie beispielsweise den zweiten und den dritten Eintrag markieren und die Schaltfläche Ganz nach oben betätigen, ist die Aufgabe klar: Dann sollen der zweite und der dritte Datensatz die erste und die zweite Position einnehmen und der erste auf die dritte Position verschoben werden:

Vorher:      Nachher:
Eintrag 1    Eintrag 2
Eintrag 2    Eintrag 3
Eintrag 3    Eintrag 1
...          ...

Gleiches gilt, wenn diese beiden Datensätze etwa mit der Schaltfläche Nach unten um eine Position nach unten verschoben werden sollen: Dann landen der zweite und der dritte Eintrag auf der dritten und vierten Position und der vierte Eintrag wird nach oben auf die zweite Position verschoben:

Vorher:      Nachher:
Eintrag 1    Eintrag 1
Eintrag 2    Eintrag 4
Eintrag 3    Eintrag 2
Eintrag 4    Eintrag 3
...          ...

Was aber, wenn der Benutzer etwa den zweiten und den vierten Eintrag verschiebt Dann gibt es zwei Möglichkeiten:

  • Die Einträge werden in dem Raster, in dem sie sich gerade befinden, nach oben verschoben.
  • Die zu verschiebenden Einträge werden zu einem Block zusammengefasst und an die Zielposition verschoben.

Beides lässt sich wieder besser am Beispiel erläutern. Angenommen, wir wollen den zweiten und den vierten Eintrag nach der ersten Methode um eine Position nach oben verschieben, sieht das so aus:

Vorher:      Nachher:
Eintrag 1    Eintrag 2
Eintrag 2    Eintrag 1
Eintrag 3    Eintrag 4
Eintrag 4    Eintrag 3

Die Einträge werden also nach oben verschoben, behalten aber die Abstände zueinander bei. Oder wir verschieben einfach alle markierten Elemente an die Zielposition und alle verdrängten Elemente nach unten:

Vorher:      Nachher:
Eintrag 1    Eintrag 2
Eintrag 2    Eintrag 4
Eintrag 3    Eintrag 1
Eintrag 4    Eintrag 3

Da die zweite Methode einfacher zu programmieren ist, werden wir diese verwenden. Auch, weil uns kein Anwendungsfall einfällt, bei dem man nach der erstgenannten Methode verfahren sollte.

Code für Formular und Listenfeld

Im Klassenmodul des Formulars legen wir zunächst drei Konstanten an, die wichtige Daten aufnehmen, die an vielen Stellen verwendet werden – zum Beispiel den Namen der Quelltabelle, des Primärschlüsselfeldes dieser Tabelle und des Reihenfolge-Feldes.

Auf diese Weise brauchen Sie diese Bezeichnungen nur an einer Stelle zu ändern, wenn Sie die Reihenfolge-Funktionen einmal in ein anderes Formular übernehmen wollen:

Const m_Table As String = "tblKategorien"
Const m_PKField As String = "KategorieID"
Const m_Orderfield As String = "ReihenfolgeID"

Außerdem deklarieren wir eine Variable namens m_ListBox, in der wie einen Verweis auf das Listenfeld speichern, das die Datensätze in der Reihenfolge anzeigen soll:

Dim m_ListBox As ListBox

Diese Variable füllen wir gleich beim Laden des Formulars in der folgenden Prozedur mit einem Verweis auf das gewünschte Listenfeld:

Private Sub Form_Load()
     Set m_ListBox = Me!lstKategorien
     FillOrderID
End Sub

Hier rufen wir auch gleich die Prozedur FillOrderID auf. Diese sorgt dafür, dass die Werte des Feldes ReihenfolgeID aktualisiert werden, so dass diese durchnummeriert sind.

ReihenfolgeID aktualisieren

Die Prozedur FillOrder sieht wie in Listing 1 aus. Diese Prozedur greift die Inhalte der Konstanten m_PKField, m_Orderfield und m_Table auf und stellt damit ein Recordset zusammen, das die Datensätze der angegebenen Tabelle, hier tblKategorien, mit den interessanten Feldern KategorieID und ReihenfolgeID enthält. Dieses Recordset durchläuft die Prozedur in einer Do While-Schleife und aktualisiert dort den Wert des Feldes aus m_Orderfield, hier ReihenfolgeID, mit dem Wert der Eigenschaft AbsolutePosition des Recordsets und addiert noch den Wert 1 hinzu. Dadurch werden die Datensätze der Tabelle mit Werten für das Feld ReihenfolgeID durchnummeriert.

Public Sub FillOrderID()
     Dim db As DAO.Database
     Dim rst As DAO.Recordset
     Set db = CodeDb
     Set rst = db.OpenRecordset("SELECT " & m_PKField & ", " & m_Orderfield & " FROM " & m_Table & " ORDER BY " _
         & m_Orderfield, dbOpenDynaset)
     Do While Not rst.EOF
         rst.Edit
         rst(m_Orderfield) = rst.AbsolutePosition + 1
         rst.Update
         rst.MoveNext
     Loop
     rst.Close
     Set rst = Nothing
     Set db = Nothing
End Sub

Listing 1: Prozedur zum Wiederherstellen der Werte des Feldes ReihenfolgeID

Das Ergebnis sieht dann wie in Bild 3 aus.

Die Tabelle tblKategorien mit Werten im Feld ReihenfolgeID

Bild 3: Die Tabelle tblKategorien mit Werten im Feld ReihenfolgeID

Schaltflächen mit Code versehen

Die vier Schaltflächen versehen wir mit jeweils einer Prozedur, welche die Reihenfolge der aktuell markierten Einträge aktualisiert. Die Prozedur für die oberste Schaltfläche cmdTop lautet wie folgt:

Private Sub cmdTop_Click()
     Dim lngTarget As Long
     Dim lngMove() As Long
     lngMove = GetMove
     lngTarget = DMin(m_Orderfield, m_Table)
     InterchangeOrder lngTarget, lngMove
     RequeryControls lngMove
End Sub 

Hier rufen wir als Erstes die Funktion GetMove auf. Diese ist dafür verantwortlich, ein Array zusammenzustellen, dass alle Primärschlüsselwerte der zu verschiebenden Einträge des Listenfeldes aufnimmt (siehe Listing 2). Die Funktion durchläuft dazu eine For Each-Schleife über alle ausgewählten Elemente des Listenfeldes (ItemsSelected), das wir mit m_ListBox referenziert haben. Der 0-basierte Index des jeweils durchlaufenen Elements wird in der Variablen var gespeichert. Darin wird zuerst das Array lngMove auf eine Anzahl von Einträgen entsprechend dem Wert der Variablen i erweitert. Dann weisen wir dem Element mit dem Index i den Wert der ersten Spalte des Listenfeldes zu (ItemData(var)). Schließlich erhöhen wir den Wert der Variablen i um eins, bevor wir den nächsten ausgewählten Eintrag untersuchen.

Private Function GetMove() As Long()
     Dim lngMove() As Long
     Dim i As Integer
     Dim var As Variant
     For Each var In m_ListBox.ItemsSelected
         ReDim Preserve lngMove(i)
         lngMove(i) = m_ListBox.ItemData(var)
         i = i + 1
     Next var
     GetMove = lngMove
End Function

Listing 2: Die Prozedur GetMove

Die nächste Anweisung ermittelt mit der DMin-Funktion den kleinsten Wert des Feldes aus m_Orderfield (hier ReihenfolgeID) der Tabelle aus m_Table (hier tblKategorien) und speichert diesen in der Variablen lngTarget. Das ist in diesem Fall das Ziel beim Verschieben des Elements. Danach folgt der Aufruf der Prozedur InterchangeOrder mit lngTarget und dem Array lngMove als Parameter. Diese schauen wir uns weiter unten an. Die Prozedur stellt die ReihenfolgeID-Werte für die betroffenen Elemente neu ein.

Schließlich ruft die Prozedur noch eine weitere Routine namens RequeryControls auf. Diese aktualisiert zuerst den Inhalt des Listenfeldes, wodurch die Elemente nach dem Ändern der Werte des Feldes ReihenfolgeID durch die vorgegebene Sortierung gegebenenfalls neu angeordnet werden. Dann ruft sie die Prozedur SelectEntries auf, die dafür sorgt, dass die vor dem Verschieben markierten Einträge erneut markiert werden. Durch das Requery gehen die Markierungen nämlich verloren. Schließlich sorgt die Prozedur ActivateControls dafür, dass die Schaltflächen entsprechend der aktuell ausgewählten Elemente aktiviert oder deaktiviert werden. Wenn nämlich das erste Element markiert ist, sollen die Schaltflächen Ganz nach oben und Nach oben deaktiviert werden. Wenn das letzte Element markiert ist, sollen die Schaltflächen Nach unten und Ganz nach unten deaktiviert sein:

Private Sub RequeryControls(lngMove() As Long)
     m_ListBox.Requery
     SelectEntries lngMove()
     ActivateControls
End Sub

Schaltfläche „Nach oben“

Bevor wir uns die hier erwähnten Prozeduren ansehen, werfen wir noch einen Blick auf den Code der übrigen Schaltflächen. Zunächst die Schaltfläche, mit der die aktuell markierten Einträge um eine Position nach oben verschoben werden sollen:

Private Sub cmdUp_Click()
     Dim lngTarget As Long, lngTargetMove As Long
     Dim lngMove() As Long
     lngMove = GetMove
     lngTargetMove = DLookup(m_Orderfield, m_Table, _
         m_PKField & "=" & lngMove(0))
     lngTarget = DMax(m_Orderfield, m_Table, m_Orderfield _
         & "<" & lngTargetMove)
     InterchangeOrder lngTarget, lngMove
     RequeryControls lngMove
End Sub

Der Unterschied ist, dass wir den Wert für lngTarget, also für die Zielposition, auf etwas aufwendigere Weise ermitteln, und zwar mit zwei Domänenfunktionen. Die erste DLookup-Funktion ermittelt den Wert des Feldes ReihenfolgeID für den Datensatz der Tabelle tblKategorien, der dem ersten markierten Datensatz entspricht. Wenn also die Datensätze mit den Primärschlüsselwerten 2 und 3 markiert sind, die in diesem Fall auch die Positionen 2 und 3 innehaben, dann ist der mit dem Primärschlüsselwert 2 der aus lngMove(0), also der erste Wert des Arrays. Für diesen ermittelt die DLookup-Funktion dann den Wert des Feldes ReihenfolgeID, hier auch 2, und schreibt ihn in die Variable lngTargetMove. Die folgende DMax-Funktion ermittelt dann den größten Wert des Feldes ReihenfolgeID, der kleiner als der Wert aus lngTargetMove ist. Damit ist dann die Ziel-Reihenfolgenposition ermittelt und in der Variablen lngTarget gespeichert. Mit lngTarget und lngMove() wird dann wiederum die Prozedur InterchangeOrder aufgerufen und dann die Prozedur RequeryControls.

Schaltfläche „Ganz nach unten“

Die durch diese Schaltfläche ausgelöste Prozedur ist wieder genauso einfach wie die für die Schaltfläche Ganz nach oben. Hier verwenden wir lediglich für die Variable lngTarget den maximalen Wert des Feldes ReihenfolgeID der Tabelle tblKategorien:

Private Sub cmdBottom_Click()
     Dim lngTarget As Long
     Dim lngMove() As Long
     lngMove = GetMove
     lngTarget = DMax(m_Orderfield, m_Table)
     InterchangeOrder lngTarget, lngMove
     RequeryControls lngMove
End Sub

Schaltfläche „Nach unten“

Etwas komplizierter ist dagegen die Schaltfläche mit der Beschriftung Nach unten (siehe Listing 3). Hier ermitteln wir wieder das Array mit den Primärschlüsselwerten der markierten Elemente. Daraus berechnen wir dann über die Funktion UBound den größten Index-Wert und speichern ihn in lngMax. Diesen nutzen wir dann in einer DLookup-Funktion als Vergleichswert für das Kriterium, mit der wir den ReihenfolgeID-Wert für den untersten markierten Eintrag ermitteln. Die folgende DMin-Funktion liest dann den kleinsten ReihenfolgeID-Wert der Tabelle tblKategorien ein, der größer ist als der ReihenfolgeID-Wert aus lngTargetMove. Der Rest entspricht der Vorgehensweise der vorherigen Prozeduren.

Ende des frei verfügbaren Teil. Wenn Du mehr lesen möchtest, hole Dir ...

den kompletten Artikel im PDF-Format mit Beispieldatenbank

diesen und alle anderen Artikel mit dem Jahresabo

Schreibe einen Kommentar