Formularanwendung "Matrixoperationen"
mit Hinzufügen und Löschen von Steuerelementen zur Laufzeit
Dieses Beispiel ist im Hinblick auf folgende Punkte interessant:
-
Als Benutzeroberfläche dient ausschließlich ein Formular. Alle Ein- und
Ausgaben werden auf diesem Formular gemacht. Die Benutzersteuerung geschieht auf der Basis von geplanten Dialogzuständen mit Hilfe von Zustandsdiagrammen.
- Von den Steuerelementen des Formulars werden die meisten erst zur Laufzeit des Programms angelegt. Das hat einen einfachen Grund: wie
viele Elemente benötigt werden und in welcher Anordnung, hängt von der Benutzereingabe ab.
- Die Benutzereingabe wird nur auf simple Art überprüft (validiert). Zum Abfangen der restlichen Fehler wird die (alte) VBA-Fehlerbehandlung benutzt.
- Zwei von den drei zur Verfügung stehenden Matrixoperationen (Inverse und Transponierte) werden mit Hilfe von Worksheet-Funktionen gelöst, nur die dritte (Sortierung) erfordert eigene Programmierung in nennenswertem Umfang. Hierbei wird das Prinzip der Delegation angewandt.
Die Aufgabenstellung
Das Programm soll wahlweise eine von drei Matrixoperationen durchführen. Das folgende Bild zeigt den Kopf des Formulars kurz nach dem Start. Der untere, nicht abgebildete Teil des Formulars ist noch leer. Er wird später den Rest der Eingaben und die Ergebnisse aufnehmen.

Die Art der Benutzung sei kurz verbal beschrieben:
Nach Eingabe einer Zeilen- und einer Spaltenanzahl kann der Benutzer die Schaltfläche <Eingabe vorbereiten> drücken. Es wird dann auf dem Formular die Eingabemöglichkeit für eine Matrix in der gewünschten Größe eröffnet.
Mit der Schaltfläche <Operation durchführen> wird die gewünschte Operation ausgeführt; das Ergebnis wird auf dem Formular unterhalb der eingegebenen Matrix angezeigt.
Die Schaltfläche <Operation zurücksetzen> kann nach Durchführung einer Operation benutzt werden, um die Ergebnisse aus dem Formular zu entfernen. Die Eingaben bleiben dabei erhalten. Der Benutzer kann nun mit denselben Daten eine andere der drei Operationen durchführen. Er kann aber auch die Daten verändern und dann eine beliebige der drei Operationen anstoßen.
Die Schaltfläche <Alles zurücksetzen> entfernt die Ergebnisse und die Eingaben aus dem Formular. Sie stellt also den Anfangszustand wieder her.
Die Schaltfläche <Beenden> schließt das Formular und beendet die Programmdurchführung. Sie ist stets verfügbar. Das Programm kann also in jeder Phase beendet werden. Eingegebene Daten werden allerdings nicht gespeichert.
Planung der Dialogzustände
Bei dieser Planung (s. Skript) werden zunächst Zustände gefunden, welche der Dialog zwischen Benutzer und Programm annehmen kann. Solche Zustände sind gekennzeichnet durch den Bearbeitungsstand und, davon abhängig, die Möglichkeiten der Aktion, welche der Benutzer in diesem Augenblick hat. Durch Aktionen des Benutzers, insbesondere das Anklicken von Schaltflächen, wechselt das Programm den Zustand.
Die Planung dieser Zustände geschieht im Wesentlichen in einem Diagramm, dem Zustandsdiagramm, und einer ergänzenden Tabelle (s. unten). Die Tabelle definiert den Zustand. Sie gibt an, welche der Steuerelemente im betreffenden Zustand aktiviert sind (A) und welche nicht (N).
Wie das Diagramm zeigt, werden drei Zustände unterschieden. Mit Hilfe der Pfeile werden die Übergänge zwischen den Zuständen angezeigt. Klickt der Benutzer z.B. im Zustand „Eingabe vorbereitet“ auf die Schaltfläche <Operation durchführen>, so geht das Programm in den Zustand „Operation durchführen“ über.
Was geschieht, wenn Fehler auftreten, wird in diesem Diagramm vernachlässigt. Nichtsdestotrotz muss natürlich eine Fehlerbehandlung stattfinden. In diesem Falle wird das Programm nach Ausgabe einer Fehlermeldung in den Zustand „Eingabe vorbereitet“ zurückkehren. Der Benutzer kann dann seine Eingaben korrigieren und danach einen weiteren Versuch machen, die Operation anzustoßen.

Das UI im Detail
Die folgenden Bilder illustrieren die obigen Ausführungen zum UI. Im ersten Bild befindet sich das Formular noch im Zustand „nicht vorbereitet“. Der Benutzer kann nun die Größe der Ursprungsmatrix eingeben und die gewünschte Operation markieren. Dann muss er die Schaltfläche <Eingabe vorbereiten> drücken.

Das folgende Bild zeigt das Formular im Zustand „Eingabe vorbereitet“. Die Eingabefelder für die Matrix stehen bereit, aber der Benutzer hat noch nichts eingegeben.

Auch das nächste Bild beruht noch auf dem Zustand „Eingabe vorbereitet“. Der Benutzer hat nunmehr die Matrix ausgefüllt. Er hat aber noch nicht die Schaltfläche <Operation durchführen> gedrückt. Noch könnte er den Inhalt der Matrix oder die gewünschte Operation verändern.

Indem der Benutzer die Schaltfläche <Operation durchführen> drückt, gelangt er in den Zustand „Operation durchgeführt“ (nächstes Bild). Im unteren Teil des Formulars wird jetzt die Ergebnismatrix angezeigt.

Der Benutzer kann nun die Schaltfläche <Operation zurücksetzen> anklicken, um mit den Daten noch eine andere Operation durchzuführen zu können. Er kommt dann in den Zustand „Eingabe vorbereitet“ zurück.
Er könnte jetzt nach Belieben den Inhalt der Matrix belassen oder verändern und die gewünschte Operation wählen. Wir wollen annehmen, dass er die Matrix unverändert lässt, aber als Operation jetzt das Sortieren auswählt (Bild unten).

Klickt der Benutzer auf die Schaltfläche <Operation durchführen>, so wird im unteren Teil des Formulars die sortierte Matrix angezeigt. Der Dialog befindet sich wieder im Zustand „Operation durchgeführt“ (nächstes Bild).

Das Programm hat eine zweischichtige Architektur, welche eine Trennung von Benutzerführung (UI) und Programmlogik realisiert. Die obere Schicht besteht aus dem Formular bzw. dem Formularmodul (MatrixForm). Die untere Schicht ist zweigeteilt und umfasst ein Modul MatrixSortierung, welches eine Sortierfunktion bereitstellt und ein zweites Modul ValHlp mit Hilfsfunktionen zur Validierung von Eingaben.

Auffällig und etwas untypisch ist, dass der Code für das Formularmodul umfangreicher ist als der für die Programmlogik. Hierfür gibt es zwei Gründe:
- Nur ein kleiner Teil des Programmkerns wird selbst programmiert. Für die inverse Matrix und die transponierte Matrix werden Worksheet-Funktionen herangezogen. Besonders die Programmierung der Inversen hätte viel Aufwand erfordert.
- Im Formularmodul ist auch der Code für das Anlegen und Entfernen der Ein- und Ausgabefelder enthalten.
Der Code des Formularmoduls
Die Formularklasse besitzt eine Reihe von Instanzenvariablen, die alle die Felder für die Matrizen betreffen. Die Variablen m, h und v nehmen die Textboxes der einzugebenden Matrix auf, die Variablen r, u und n die der auszugebenden Ergebnismatrix.
Beachten Sie, dass den Textfeldern bei ihrem Anlegen in den Prozeduren AnlegenBtn_Click und insertErgTBxs Namen zugewiesen werden (zweiter Parameter der Methode Controls.Add). Dies ist notwendig, weil die Felder beim Löschen mit der Methode Controls.Remove mit ihren Namen angesprochen werden müssen.
Die Prozeduren, welche die Dialogzustände herstellen, befinden sich am Ende des Programmtexts. Das Anlegen und das Löschen der Felder für die Matrizen wurde nicht diesen Prozeduren einverleibt, sondern in gesonderten Prozeduren realisiert, weil diese Vorgänge nicht nur vom Dialogzustand, sondern auch von der gewünschten Größe der Eingangsmatrix und der ausgewählten Operation abhängen.
Eine explizite Eingabevalidierung findet nur in Bezug auf die Eingabe der Matrixgröße statt (Prozedur AnlegenBtn_Click()). Von dort aus wird die Funktion ValidierungOK aufgerufen, welche ihrerseits auf allgemein verwendbare Prüffunktionen im Modul ValHlp zurückgreift. Die vom Benutzer eingegebene Matrix wird nicht validiert. Dies wäre natürlich möglich, aber aufwendig. Stattdessen wird in der Prozedur DurchfuehrenBtn_Click() die klassische VBA-Fehlerbehandlung mit On Error GoTo verwendet, welche alle bei der Realisierung der Matrixoperation auftretenden Fehler abfangen kann. Der Preis für diese pauschale Fehlerbehandlung ist allerdings, dass dem Benutzer nur sehr allgemeine Hinweise auf die Fehlerursache gegeben werden können.
Option Explicit
Private m() As Control 'Eingabematrix
Private h() As Control 'Beschriftung der Spalten der Eingabematrix
Private v() As Control 'Beschriftung der Zeilen der Eingabematrix
Private r() As Control 'Ergebnismatrix
Private u() As Control 'Beschriftung der Spalten der Ergebnismatrix
Private n() As Control 'Beschriftung der Zeilen der Ergebnismatrix
Private rVorhanden As Boolean 'verhindert Löschversuch, wenn nicht vorhanden
Private mVorhanden As Boolean
'Aktionen nach Klicken von <Eingabe vorbereiten>
Private Sub AnlegenBtn_Click()
If Not ValidierungOK Then Exit Sub
Dim i As Integer, j As Integer
Dim z As Integer, s As Integer
z = CInt(Me.ZeilenzahlTBx.Text)
s = CInt(Me.SpaltenzahlTBx.Text)
'die Eingabefelder werden angelegt
ReDim m(1 To z, 1 To s)
ReDim h(1 To s)
ReDim v(1 To z)
For j = 1 To s 'Beschriftung der Spalten
Set h(j) = Me.Controls.Add("Forms.TextBox.1", "h" & j)
h(j).Height = 24
h(j).Width = 50
h(j).Left = 45 + (j - 1) * 50
h(j).Top = 120
h(j).Text = "" & j
h(j).Locked = True
Next j
For i = 1 To z 'Beschriftung der Zeilen
Set v(i) = Me.Controls.Add("Forms.TextBox.1", "v" & i)
v(i).Height = 24
v(i).Width = 50
v(i).Left = 15
v(i).Top = 139 + (i - 1) * 25
v(i).Text = "" & i
v(i).Locked = True
Next i
For i = 1 To z
For j = 1 To s
Set m(i, j) = Me.Controls.Add("Forms.TextBox.1", "m" & i & j)
m(i, j).Height = 24
m(i, j).Width = 50
m(i, j).Left = 45 + (j - 1) * 50
m(i, j).Top = 139 + (i - 1) * 25
Next j
Next i
mVorhanden = True
ZustandEingabeVorbereitet
End Sub
'Aktionen nach Klicken des Buttons <Beenden>
Private Sub BeendenBtn_Click()
Unload Me
End Sub
'Aktionen nach Klicken des Buttons <Alles zurücksetzen>
Private Sub CancelBtn_Click()
Me.ZeilenzahlTBx.Text = ""
Me.SpaltenzahlTBx.Text = ""
Me.TransponierteOBtn.Value = True
entferneEingabeTBxs
entferneErgebnisTBxs
ZustandNichtVorbereitet
End Sub
'Aktionen nach Klicken des Buttons <Operation durchführen>
Private Sub DurchfuehrenBtn_Click()
On Error GoTo errorhandler1
If Me.TransponierteOBtn.Value = True Then
transponierteMatrix
ElseIf Me.InverseOBtn.Value = True Then
invertierteMatrix
Else
sortierteMatrix
End If
ZustandOperationDurchgefuehrt
Exit Sub
errorhandler1:
MsgBox "Es ist ein Fehler aufgetreten. Stellen Sie sicher, " & _
"dass in allen Eingabefeldern zulässige Werte stehen."
ZustandEingabeVorbereitet
End Sub
'Ermittlung und Ausgabe der Transponierten
Private Sub transponierteMatrix()
Dim e() As Double, t As Variant
e = inputMatrix 'Daten aus den Eingabefeldern holen
t = Application.WorksheetFunction.Transpose(e)
insertErgTBxs CInt(Me.SpaltenzahlTBx.Text), CInt(Me.ZeilenzahlTBx.Text)
outputErg t 'Ausgabe in die Ergebnisfelder
End Sub
'Ermittlung und Ausgabe der Inversen
Private Sub invertierteMatrix()
Dim e() As Double, t As Variant
e = inputMatrix 'Daten aus den Eingabefeldern holen
t = Application.WorksheetFunction.MInverse(e)
insertErgTBxs CInt(Me.ZeilenzahlTBx.Text), CInt(Me.SpaltenzahlTBx.Text)
outputErg t 'Ausgabe in die Ergebnisfelder
End Sub
'Ermittlung und Ausgabe der sortierten Matrix
Private Sub sortierteMatrix()
Dim e() As Double, t() As Double
e = inputMatrix 'Daten aus den Eingabefeldern holen
t = Matrixsortierung.sortMatrix(e)
insertErgTBxs CInt(Me.ZeilenzahlTBx.Text), CInt(Me.SpaltenzahlTBx.Text)
outputErg t 'Ausgabe in die Ergebnisfelder
End Sub
'speichert die Daten der Eingabe zur Weiterverarbeitung in einem Array
Public Function inputMatrix() As Double()
Dim i As Integer, j As Integer, rows As Integer, cols As Integer
rows = CInt(Me.ZeilenzahlTBx.Text)
cols = CInt(Me.SpaltenzahlTBx)
Dim p() As Double
ReDim p(1 To rows, 1 To cols)
For i = 1 To rows
For j = 1 To cols
p(i, j) = CDbl(m(i, j).Text)
Next j
Next i
inputMatrix = p
End Function
'schreibt die Ergebnisdaten in die Ausgabe-Textboxes
'braucht Variant-Input für Ergebnise aus WorksheetFunctions
Public Sub outputErg(a As Variant)
Dim i As Integer, j As Integer
For i = 1 To UBound(a, 1)
For j = 1 To UBound(a, 2)
r(i, j).Text = CStr(a(i, j))
Next j
Next i
End Sub
'Aktionen nach Klicken des Buttons <Operation zurücksetzen>
Private Sub OpZurueckBtn_Click()
entferneErgebnisTBxs
ZustandEingabeVorbereitet
End Sub
'Aktionen beim Laden der Maske
Private Sub UserForm_Initialize()
Me.TransponierteOBtn.Value = True
ZustandNichtVorbereitet
End Sub
'überprüft die Eingaben der Zeilen- und der Spaltenzahl
Public Function ValidierungOK() As Boolean
If Not (ValHlp.istImGanzzBereich(Me.ZeilenzahlTBx.Text, 1, 10) And _
ValHlp.istImGanzzBereich(Me.SpaltenzahlTBx.Text, 1, 10)) Then
ValidierungOK = False
MsgBox "Es sind nur Zeilen- und Spaltenzahlen von 1 bis 10 zugelassen"
Exit Function
End If
If Me.InverseOBtn.Value = True And Not CInt(Me.ZeilenzahlTBx.Text) = _
CInt(Me.SpaltenzahlTBx.Text) Then
ValidierungOK = False
MsgBox "Die Inverse kann nur von einer quadratischen Matrix ermittelt werden"
Exit Function
End If
ValidierungOK = True
End Function
'legt die Textboxes zur Ergebnisausgabe auf dem Formular an
Private Sub insertErgTBxs(ByVal z As Integer, ByVal s As Integer)
Dim i As Integer, j As Integer, shift As Integer
ReDim r(1 To z, 1 To s)
ReDim u(1 To s)
ReDim n(1 To z)
shift = IIf(s > z, s, z)
For j = 1 To s 'Beschriftung der Spalten
Set u(j) = Me.Controls.Add("Forms.TextBox.1", "u" & j)
u(j).Height = 24
u(j).Width = 50
u(j).Left = 45 + (j - 1) * 50
u(j).Top = 120 + shift * 25 + 50
u(j).Text = "" & j
u(j).Locked = True
Next j
For i = 1 To z 'Beschriftung der Zeilen
Set n(i) = Me.Controls.Add("Forms.TextBox.1", "n" & i)
n(i).Height = 24
n(i).Width = 50
n(i).Left = 15
n(i).Top = 139 + (i - 1) * 25 + shift * 25 + 50
n(i).Text = "" & i
n(i).Locked = True
Next i
For i = 1 To z 'für die Matrix selbst
For j = 1 To s
Set r(i, j) = Me.Controls.Add("Forms.TextBox.1", "r" & i & j)
r(i, j).Height = 24
r(i, j).Width = 50
r(i, j).Left = 45 + (j - 1) * 50
r(i, j).Top = 139 + (i - 1) * 25 + shift * 25 + 50
Next j
Next i
rVorhanden = True
End Sub
'löscht die Textboxes für die Eingabe
Private Sub entferneEingabeTBxs()
If Not mVorhanden Then Exit Sub
Dim i As Integer, j As Integer
For j = 1 To UBound(h) 'Beschriftung der Spalten entfernen
Me.Controls.Remove "h" & j
Next j
For i = 1 To UBound(v) 'Beschriftung der Zeilen entfernen
Me.Controls.Remove "v" & i
Next i
For i = 1 To UBound(m, 1) 'Matrix entfernen
For j = 1 To UBound(m, 2)
Me.Controls.Remove "m" & i & j
Next j
Next i
mVorhanden = False
End Sub
'löscht die Textboxes für die Ergebnisausgabe
Private Sub entferneErgebnisTBxs()
If Not rVorhanden Then Exit Sub
Dim i As Integer, j As Integer
For j = 1 To UBound(u) 'Beschriftung der Spalten entfernen
Me.Controls.Remove "u" & j
Next j
For i = 1 To UBound(n) 'Beschriftung der Zeilen entfernen
Me.Controls.Remove "n" & i
Next i
For i = 1 To UBound(r, 1) 'Matrix entfernen
For j = 1 To UBound(r, 2)
Me.Controls.Remove "r" & i & j
Next j
Next i
rVorhanden = False
End Sub
'=================================================
'Dialogzustände
'=================================================
Private Sub ZustandNichtVorbereitet()
Me.ZeilenzahlTBx.Enabled = True
Me.SpaltenzahlTBx.Enabled = True
Me.TransponierteOBtn.Enabled = True
Me.InverseOBtn.Enabled = True
Me.SortierenOBtn.Enabled = True
Me.AnlegenBtn.Enabled = True
Me.DurchfuehrenBtn.Enabled = False
Me.CancelBtn.Enabled = True
Me.OpZurueckBtn.Enabled = False
End Sub
Private Sub ZustandEingabeVorbereitet()
Me.ZeilenzahlTBx.Enabled = False
Me.SpaltenzahlTBx.Enabled = False
Me.TransponierteOBtn.Enabled = True
Me.InverseOBtn.Enabled = True
Me.SortierenOBtn.Enabled = True
Me.AnlegenBtn.Enabled = False
Me.DurchfuehrenBtn.Enabled = True
Me.CancelBtn.Enabled = True
Me.OpZurueckBtn.Enabled = False
End Sub
Private Sub ZustandOperationDurchgefuehrt()
Me.ZeilenzahlTBx.Enabled = False
Me.SpaltenzahlTBx.Enabled = False
Me.TransponierteOBtn.Enabled = False
Me.InverseOBtn.Enabled = False
Me.SortierenOBtn = False
Me.AnlegenBtn.Enabled = False
Me.DurchfuehrenBtn.Enabled = False
Me.CancelBtn.Enabled = True
Me.OpZurueckBtn.Enabled = True
End Sub
Der Code des Moduls Matrixsortierung
In diesem Modul wird das Prinzip der Delegation angewandt (s. Skript). Die Hauptfunktion ist sortMatrix, eine sehr kurze Funktion, deren Rumpf nur eine Zeile umfasst. Die Funktion lässt zunächst durch Aufruf von MatrixToList die zu sortierende Matrix in ein eindimensionales Array verwandeln, übergibt dieses Array dann der Funktion InsertionSort zum Sortieren und verwandelt anschließend das von InsertionSort gelieferte eindimensionale Array durch einen Aufruf der Funktion ListToMatrix wieder in eine Matrix der ursprünglichen Dimensionierung.
'wandelt eine Matrix in ein eindimensionales Array um
Public Function MatrixToList(ByRef a() As Double) As Double()
Dim t() As Double
Dim m As Integer, n As Integer, i As Integer, j As Integer
m = UBound(a, 1) 'Anzahl der Zeilen
n = UBound(a, 2) 'Anzahl der Spalten
ReDim t(1 To m * n)
For i = 1 To m 'übertrage alle Elemente
For j = 1 To n
t((i - 1) * n + j) = a(i, j)
Next j
Next i
MatrixToList = t
End Function
'wandelt ein eindimensionales Array in eine Matrix um. m ist die Zeilenzahl
'der Matrix. Die Anzahl der Elemente in a ist durch m ohne Rest teilbar
Public Function ListToMatrix(ByRef a() As Double, ByVal m As Integer) As Double()
Dim n As Integer, i As Integer
n = UBound(a) \ m 'Anzahl der Spalten in der Zielmatrix
Dim r() As Double 'Zielmatrix
ReDim r(1 To m, 1 To n)
For i = 1 To UBound(a) 'übertrage alle Elemente
r((i - 1) \ n + 1, i - ((i - 1) \ n) * n) = a(i)
Next i
ListToMatrix = r
End Function
'sortiert ein eindimensionales Array in aufsteigender Ordnung
'ohne Nebeneffekte, weil keine Sortierung "in place"
Public Function InsertionSort(ByRef a() As Double) As Double()
Dim s() As Double
ReDim s(1 To UBound(a))
Dim i As Integer, j As Integer, found As Boolean
s(1) = a(1)
For j = 2 To UBound(a)
i = j - 1
found = False
Do While i > 0 And Not found
If s(i) > a(j) Then
s(i + 1) = s(i)
i = i - 1
Else
found = True
End If
Loop
s(i + 1) = a(j)
Next j
InsertionSort = s
End Function
'sortiert die Elemente einer 1-basierten Matrix in aufsteigender Ordnung;
'benutzt MatrixToList, InsertionSort und ListToMatrix
Public Function sortMatrix(ByRef m() As Double) As Double()
sortMatrix = ListToMatrix(InsertionSort(MatrixToList(m)), UBound(m, 1))
End Function
Der Code des Moduls ValHlp
Bei diesem Modul handelt es sich um eine Sammlung allgemein verwendbarer Funktionen, welche Eingabewerte auf ihre Zulässigkeit überprüfen. Hier sollen nur die Funktionen des Moduls aufgelistet werden, welche in unserer aktuellen Anwendung benutzt werden. Es sind:
'ermittelt für den Wert z, ob er einer Ganzzahl entspricht;
'z kann auch ein String sein
Public Function istGanzzahl(ByVal z As Variant) As Boolean
If Not IsNumeric(z) Then
istGanzzahl = False
Else
If CLng(z) - Int(z) = 0 Then
istGanzzahl = True
Else
istGanzzahl = False
End If
End If
End Function
'ermittelt für den Wert z, ob er einer Ganzzahl entspricht und
'im Bereich [min, max] liegt; z kann auch ein String sein
Public Function istImGanzzBereich(ByVal z As Variant, _
ByVal min As Long, _
ByVal max As Long) As Boolean
If Not istGanzzahl(z) Then
istImGanzzBereich = False
Else
If CLng(z) >= min And CLng(z) <= max Then
istImGanzzBereich = True
Else
istImGanzzBereich = False
End If
End If
End Function