Stücklisten, Teil II

Was lange währt, wird endlich gut: Hier kommt der zweite Teil zu der vor einigen Ausgaben begonnenen Reihe zum Thema Stücklisten. Nachdem das TreeView-Steuerelement zur Anzeige und zum Bearbeiten von Baugruppen und Teilen steht, erweitern wir die Lösung nun um Kontextmenü-Einträge, mit denen Sie Baugruppen und Elemente hinzufügen, bearbeiten und entfernen können.

Kontextmenüs für die Stückliste

Im ersten Teil der Beitragsreihe haben wir bereits ein erstes Kontextmenü mit Untermenüs zur Anwendung hinzugefügt. Dieses wird angezeigt, wenn der Benutzer mit der rechten Maustaste auf den Root-Eintrag des TreeViews mit der Beschriftung Produkte klickt. Es erscheint dann ein Kontextmenü mit den beiden Einträgen Baugruppen und Einzelteile, die wiederum Untermenüs mit den vorhandenen Elementen anzeigen (s. Bild 1).

Kontextmenü zum Anlegen von Produkten

Bild 1: Kontextmenü zum Anlegen von Produkten

Nach dem Anklicken einer der Baugruppen beziehungsweise eines der Einzelteile wird dieses direkt unterhalb des Elements Produkte angelegt.

Es fehlen allerdings noch einige weitere Kontextmenüs – beispielsweise, um Baugruppen weitere Baugruppen oder Elemente hinzuzufügen, um Baugruppen und Teile zu entfernen oder auch um Baugruppen oder Teile zu markieren, zu kopieren und an anderen Stellen wieder einzufügen.

Produkte, Baugruppen und Einzelteile

Zur Erinnerung an den ersten Teil der Beitragsreihe: Wir unterscheiden prinzipiell zwischen Baugruppen (Tabelle tblBaugruppen) und Einzelteilen (tblEinzelteile) sowie zwischen Produkten. Letztere sind Baugruppen oder Einzelteile, die im Datensatz ihrer Tabelle mit dem Wert True im Feld IstProdukt gekennzeichnet sind.

Produkte werden immer direkt unterhalb des Root-Elements Produkte angezeigt, alle anderen dort, wo sie zugeordnet wurden. Es kann natürlich auch Baugruppen oder Einzelteile geben, die nur in der Tabelle verfügbar sind, aber noch keinem Produkt oder keiner Baugruppe untergeordnet sind und dementsprechend nicht im TreeView erscheinen.

Baugruppen oder Teile per Kontextmenü entfernen

Die wichtigste Prozedur beim Anzeigen von Kontextmenüs ist die, welche beim Herunterdrücken der rechten Maustaste ausgelöst wird. In unserem Fall sieht diese wie in Listing 1 aus.

Private Sub objTreeView_MouseDown(ByVal Button As Integer, ByVal Shift As Integer, _
         ByVal x As stdole.OLE_XPOS_PIXELS, ByVal y As stdole.OLE_YPOS_PIXELS)
     Dim objNode As MSComctlLib.Node
     Dim strTyp As String
     Dim lngID As Long
     Dim strKey As String
     Dim strKeyItem As String
     Select Case Button
         Case acRightButton
             Set objNode = objTreeView.HitTest(x, y)
             If Not objNode Is Nothing Then
                 strKey = objNode.Key
                 strKeyItem = Mid(objNode.Key, InStrRev(objNode.Key, "|") + 1)
                 strTyp = Left(strKeyItem, 1)
                 lngID = Mid(strKeyItem, 2)
                 Select Case strTyp
                     Case "r"
                         KontextmenueRoot strKey
                     Case "b"
                         KontextmenueBaugruppe lngID, strKey
                     Case "e"
                         KontextmenueEinzelteil lngID, strKey
                 End Select
             End If
     End Select
End Sub

Listing 1: Aufrufen eines Kontextmenü-Eintrags

Achtung: Möglicherweise kommt Ihnen diese Prozedur bekannt vor, da wir diese im ersten Teil schon einmal vorgestellt haben. Bei der Weiterentwicklung haben wir jedoch ein paar kleine Optimierungen vorgenommen, welche das Anlegen und Entfernen von Elementen wesentlich erleichtern. Dazu gehört unter anderem, dass wir bei den Aufrufen der Prozeduren KontextmenueEinzelteil und KontextmenueBaugruppe nun immer auch den Key-Wert des aktuellen Elements mitgeben – mehr dazu weiter unten.

Die Prozedur prüft anhand des Parameters Button, welche Maustaste der Benutzer heruntergedrückt hat. Handelt es sich um die rechte Maustaste (acRightButton), soll das Kontextmenü eingeblendet werden. Unsere Prozedur dient dabei als eine Art Verteiler, die prüfen soll, auf welcher Elementart die rechte Maustaste betätigt wurde, und dann eine entsprechende Prozedur zum Anzeigen des Kontextmenüs aufruft. Bevor dies geschieht, ermittelt sie mit der HitTest-Methode des TreeView-Elements auf welchem Element die rechte Maustaste betätigt wurde. Das Ergebnis landet in der MSComCtlLib.Node-Variablen objNode. Sollte diese anschließend nicht den Wert Nothing enthalten, hat der Benutzer tatsächlich ein Element angeklickt.

In diesem Fall ermitteln wir aus der Key-Variablen des Elements, der beispielsweise den Wert r0, r0|b18 oder r0|b18|e50 enthalten kann, den Buchstaben des hintersten Elements, hier also r, b oder e, und speichern diesen in der Variablen strTyp. Die Zahl hinter diesem Element, die dem Primärschlüsselwert des entsprechenden Eintrags einer der Tabellen tblBaugruppen oder tblEinzelteile entspricht, landet hingegen in der Variablen lngID. Der Inhalt von strTyp wird in der folgenden Select Case-Bedingung geprüft. Abhängig vom Wert rufen wir dann eine der Prozeduren KontextmenueRoot (r, Root-Element), KontextmenueEinzelteil (p, Produkt-Element), KontextmenueBaugruppe (b, Bauteil-Element) oder KontextmenueEinzelteil (e, Einzelteil-Element) auf. Dieser übergeben wir den Primärschlüssel des aktuellen Elements sowie den Key-Wert (zum Beispiel r0, r0|b18 oder r0|b18|e50).

Schauen wir uns erst das Root-Kontextmenü an. Die entsprechende Prozedur heißt KontextmenueRoot und sieht wie in Listing 2 aus. Die Prozedur löscht zunächst ein eventuell bereits vorhandenes Kontextmenü namens TreeView_Root – vorsichtshalber bei deaktivierter Fehlerbehandlung, da der Versuch, ein nicht vorhandenes Kontextmenü zu löschen, einen Fehler auslösen würde. Danach legen wir dieses direkt neu an, und zwar mit der dafür vorgesehenen Methode Add. Diese erwartet als Parameter den Namen des Menüs, die Art (da die Add-Methode früher auch für das Anlegen von Menüleisten und Symbolleisten verantwortlich war – hier msoBarPopup für Kontextmenü) sowie die Angabe, ob es sich um ein temporäres Menü handelt, das mit dem Schließen der Anwendung wieder entsorgt werden kann.

Private Sub KontextmenueRoot(Optional strKey As String)
     Dim cbr As CommandBar
     On Error Resume Next
     CommandBars("TreeView_Root").Delete
     On Error GoTo 0
     Set cbr = CommandBars.Add("TreeView_Root", msoBarPopup, False, True)
     With cbr
         KontextmenueBaugruppen cbr, 0, "r0"
         KontextmenueEinzelteile cbr, 0, "r0"
         .ShowPopup
     End With
End Sub

Listing 2: Kontextmenü für das Root-Element einblenden

Danach folgt der Aufruf zweier weiterer Routinen, nämlich KontextmenueBaugruppen und KontextmenueEinzelteile:

KontextmenueBaugruppen cbr, 0, "r0"
KontextmenueEinzelteile cbr, 0, "r0"

Nach dem Anlegen der Kontextmenü-Elemente für die Baugruppen und die Einzelteile wird das Menü mit der Popup-Methode des CommandBar-Objekts eingeblendet.

Menüeinträge für die Baugruppen

Die Prozedur KontextmenueBaugruppen (s. Listing 3) erwartet drei Parameter – einen Verweis auf das Kontextmenü (cbr), die ID des Elements, von dem aus das Kontextmenü aufgerufen wurde, sowie den Wert der Key-Eigenschaft dieses Elements.

Public Sub KontextmenueBaugruppen(cbr As CommandBar, Optional lngBaugruppeParentID As Long, Optional ByVal strKey As String)
     Dim db As DAO.Database
     Dim rst As DAO.Recordset
     Dim cbp As CommandBarPopup
     Dim cbc As CommandBarButton
     Dim strIN As String
     Set db = CurrentDb
     Set cbp = cbr.Controls.Add(msoControlPopup)
     cbp.Caption = "Baugruppen"
     Set cbc = cbp.Controls.Add(msoControlButton)
     With cbc
         .Caption = "Neue Baugruppe"
         .OnAction = "=NeueBaugruppe(0)"
     End With
     If lngBaugruppeParentID = 0 Then
         strWHERE = " WHERE IstProdukt = FALSE"
     Else
         strWHERE = AuszuschliessendeBaugruppen(lngBaugruppeParentID)
         If Len(strWHERE) > 0 Then
             strWHERE = " WHERE BaugruppeID NOT IN (" & strIN & ")"
         End If
     End If
     Set rst = db.OpenRecordset("SELECT BaugruppeID, Baugruppe FROM tblBaugruppen " & strWHERE _
         & " ORDER BY Baugruppe", dbOpenDynaset)
     If Not Len(strKey) = 0 Then
         strKey = ", ''" & strKey & "''"
     End If
     Do While Not rst.EOF
         Set cbc = cbp.Controls.Add(msoControlButton)
         With cbc
             .Caption = rst!Baugruppe
             .OnAction = "=BaugruppeHinzufuegen(" & rst!BaugruppeID & ", " & lngBaugruppeParentID & strKey & ")"
         End With
         rst.MoveNext
     Loop
End Sub

Listing 3: Untermenü für die Baugruppen hinzufügen

Die Prozedur legt mit der Variablen cbp ein Steuerelement des Typs msoControlPopup an, also ein Untermenü, und weist diesem den Text Baugruppen hinzu. Das Untermenü soll einen Eintrag mit der Bezeichnung Neue Baugruppe sowie eine Auflistung der bisher verfügbaren Baugruppen erhalten. Also legen wir zunächst eine einzelne Schaltfläche an (Typ msoControlButton), speichern diese in der Variablen cbc und weisen den Text Neue Baugruppe sowie für die OnAction-Eigenschaft den Wert =NeueBaugruppe(0) hinzu. Der Wert 0 für den einzigen Parameter kennzeichnet, dass die Baugruppe in der obersten Ebene, also als Produkt angelegt werden soll – dazu später mehr.

Danach ermittelt die Prozedur die anzuzeigenden Baugruppen. Wenn die Funktion vom Root-Element aus aufgerufen wurde, sollen alle Elemente erscheinen, die noch nicht über das Feld IstProdukte als Produkt gekennzeichnet sind und ohnehin schon als Produkt-Elemente angezeigt werden.

Wurde das Kontextmenü von einer Baugruppe aus aufgerufen, erzeugt die Prozedur mit Hilfe der Funktion AuszuschliessendeBaugruppen eine Liste der Baugruppen, die nicht angelegt werden können, da dies einen Zirkelbezug erzeugen würde. Mehr dazu weiter unten. Dies ergibt eine Liste wie 1, 2, 3, die dann innerhalb der folgenden If…Then-Bedingung zu einer WHERE-Bedingung hinzugefügt wird. Diese sieht anschließend etwa wie folgt aus:

WHERE BaugruppeID NOT IN (1, 2, 3)

Danach erstellen wir ein Recordset auf Basis der Tabelle tblBaugruppen, in der wir alle Baugruppen erfassen, die dem aktuellen Element untergeordnet werden können, ohne einen Zirkelbezug auszulösen. Falls der Prozedur mit dem Parameter strKey ein Wert wie etwa r0|b1|b2 übermittelt wurde, soll dieser auch in der Funktion als Parameter enthalten sein, die durch einen der nun anzulegenden Kontextmenübefehle aufgerufen wird. Dazu wandeln wir die Zeichenkette wie r0|b1|b2 in die Zeichenkette , ”r0|b1|b2” um, damit wir diese in der folgenden Schleife leicht als Parameter zu der Funktion hinzufügen können.

In einer Do While-Schleife durchlaufen wir nun alle Datensätze dieses Recordsets und legen für jeden Datensatz einen neuen Eintrag im Kontextmenü an. Die Einträge erhalten jeweils den Namen der Baugruppe als Beschriftung und einen Ausdruck für die Eigenschaft OnAction, der die Funktion BaugruppeHinzufuegen mit dem Primärschlüsselwert des aktuellen Datensatzes als Parameter enthält – also beispielsweise folgenden Wert, wenn strKey leer war:

=BaugruppeHinzufuegen(123, 0)

Enthielt strKey hingegen einen Wert, lautet der Funktionsaufruf wie folgt:

=BaugruppeHinzufuegen(123, 0, ''r0|b1|b2'')

Das Ergebnis sieht dann etwa wie in Bild 2 aus.

Anzeigen der Baugruppen im Kontextmenü für das Root-Element

Bild 2: Anzeigen der Baugruppen im Kontextmenü für das Root-Element

Menüeinträge für die Einzelteile

Die zweite von der Prozedur KontextmenueRoot aufgerufene Prozedur heißt KontextmenueEinzelteile und sieht wie in Listing 4 aus. Diese Prozedur erwartet die gleichen drei Parameter wie die Prozedur KontextmenueBaugruppen.

Public Sub KontextmenueEinzelteile(cbr As CommandBar, Optional lngBaugruppeParentID As Long, Optional ByVal _
         strKey As String)
     Dim db As DAO.Database
     Dim rst As DAO.Recordset
     Dim cbp As CommandBarPopup
     Dim cbc As CommandBarButton
     Set db = CurrentDb
     Set cbp = cbr.Controls.Add(msoControlPopup)
     cbp.Caption = "Einzelteile"
     Set cbc = cbp.Controls.Add(msoControlButton)
     With cbc
         .Caption = "Neues Einzelteil"
         .OnAction = "=NeuesEinzelteil(0)"
     End With
     Set rst = db.OpenRecordset("SELECT EinzelteilID, Einzelteil FROM tblEinzelteile ORDER BY Einzelteil", _
         dbOpenDynaset)
     If Not Len(strKey) = 0 Then
         strKey = ", ''" & strKey & "''"
     End If
     Do While Not rst.EOF
         Set cbc = cbp.Controls.Add(msoControlButton)
         With cbc
             .Caption = rst!Einzelteil
             .OnAction = "=EinzelteilHinzufuegen(" & rst!EinzelteilID & ", " & lngBaugruppeParentID & strKey & ")"
         End With
         rst.MoveNext
     Loop
End Sub

Listing 4: Untermenü für die Einzelteile hinzufügen

Sie erstellt ein Untermenü mit der Beschriftung Einzelteile und speichert dieses in der Variablen cbp. Diesem fügt sie nun eine einzelne Schaltfläche hinzu, welche den Text Neues Einzelteil und die Funktion =NeuesEinzelteil(0) besitzt. Danach erstellt die Prozedur ein Recordset auf Basis der Tabelle tblEinzelteile und durchläuft diese Datensätze genau wie bei den Bauteilen.

Dabei erstellt sie für jeden Eintrag eine neue Schaltfläche im Kontextuntermenü und weist diesem als Bezeichnung den Wert des Feldes Einzelteil zu. Auch der Wert der Eigenschaft OnAction mit der Funktion, die beim Betätigen dieses Kontextmenü-Eintrags ausgelöst werden soll, ist prinzipiell mit den von den Baugruppen bekannten Funktionen identisch – allein der Name der Funktion heißt EinzelteilHinzufuegen und nicht BaugruppeHinzufuegen. Ein Beispiel für einen solchen Funktionsaufruf sieht so aus:

=EinzelteilHinzufuegen(65, 47, ''r0|b46|b47'')

Das resultierende Kontextmenü sieht wie in Bild 3 aus.

Kontextmenü zum Hinzufügen eines Einzelteils zum Root-Element

Bild 3: Kontextmenü zum Hinzufügen eines Einzelteils zum Root-Element

Kontextmenü für eine Baugruppe

Eine Baugruppe kann weitere Baugruppen und Einzelteile enthalten. Das Kontextmenü soll also auch die beiden Einträge zum Hinzufügen von Baugruppen und Einzelteilen enthalten. Außerdem benötigen wir einen Eintrag, mit dem wir die aktuelle Baugruppe aus dem Produkt beziehungsweise der übergeordneten Baugruppe entfernen können.

Es wird schon hier offensichtlich: Wir benötigen Teile des zuvor definierten Kontextmenüs nochmal. Da wir komplett gleiche Code-Segmente nicht mehrfach in verschiedene Prozeduren schreiben wollen, um die Wartbarkeit zu erhöhen, haben wir die mehrfach verwendeten Teile einfach in eigene Routinen ausgegliedert – also beispielsweise in die Prozeduren KontextmenueBaugruppen und KontextmenueEinzelteile, die wir dann von den Prozeduren, in denen diese eigentlich zum Einsatz kommen, aufrufen.

Kontextmenü für Baugruppen

Soeben haben wir uns angesehen, wie das Kontextmenü für das Root-Element gefüllt wird. Das Kontextmenü für eine Baugruppe sieht nicht wesentlich anders aus. Es soll allerdings einen Eintrag zum Entfernen einer Baugruppe enthalten.

Daher wollen wir nun auf ähnliche Weise die Prozedur KontextmenueBaugruppe füllen. Diese soll auch die beiden Untermenüs Baugruppen und Einzelteile enthalten sowie auch noch einen Unterpunkt, mit dem die aktuelle Baugruppe aus dem Projekt beziehungsweise aus der übergeordneten Baugruppe entfernen werden kann (s. Bild 4).

Kontextmenü zum Entfernen einer Baugruppe

Bild 4: Kontextmenü zum Entfernen einer Baugruppe

Die Prozedur KontextmenueBaugruppe, die von der Prozedur objTreeView_MouseDown aufgerufen wird, wenn der Key des angeklickten Elements im letzten Element den Buchstaben b enthält (also zum Beispiel für r0|e123), sieht wie folgt aus:

Private Sub KontextmenueBaugruppe(lngID As Long, _
         Optional strKey As String)
     Dim cbr As CommandBar
     Dim cbc As CommandBarButton
     On Error Resume Next
     CommandBars("TreeView_Baugruppe").Delete
     On Error GoTo 0
     Set cbr = CommandBars.Add("TreeView_Baugruppe", _
         msoBarPopup, False, True)
     With cbr
         Set cbc = cbr.Controls.Add(msoControlButton)
         With cbc
             .Caption = "Baugruppe entfernen"
             .OnAction = "=BaugruppeEntfernen(" _
                 & lngID & ", ''" & strKey & "'')"
         End With
         KontextmenueBaugruppen cbr, lngID, strKey
         KontextmenueEinzelteile cbr, lngID, strKey
         .ShowPopup
     End With
End Sub

Zusätzlich zum Aufruf der beiden Prozeduren KontextmenueBaugruppen und KontextmenueEinzelteile enthält es zuvor noch ein paar Anweisungen, welche dem Kontextmenü im Untermenü noch einen Eintrag mit der Beschriftung Baugruppe entfernen zuweist.

Anlegen von Baugruppen

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

Ein Kommentar

Schreibe einen Kommentar