Umfragen und Multiple-Choice-Tests - Teil 2

15.04.2006 von Helma  Spona
Im Gegensatz zu einfachen Umfragen sind Multiple-Choice-Tests etwas schwieriger anzuzeigen. Sie benötigen dazu etwas VBA-Unterstützung, um eine optimale Bedienung zu ermöglichen. Wie das aussehen kann, zeigt Ihnen dieser zweite Teil der Artikelfolge.

Zunächst einmal benötigen Sie ein Formular zum Verwalten von Fragen und Antworten. Das geht recht einfach mit Hilfe eines Unterformulars.

Tests verwalten und eingeben

Als Erstes erstellen Sie ein Formular für die Tabelle tabFragen, das alle Felder, zumindest aber die Felder Frage und Hilfe enthält (Bild 1). Darüber hinaus benötigen Sie ein Formular, das als Unterformular beginnt und in der Endlosansicht angezeigt wird. Hier benötigen Sie nur die Felder Antwort und Wert. Das zweite Formular fügen Sie dann als Unterformular in das erste ein und verknüpfen die beiden Formulare über die Felder tabFragen.ID und tabAntworten.Frage.

Bild 1: Aufbau des Formulars zur Verwaltung der Fragen und Antworten.

Damit ist das Formular prinzipiell fertig. Sie können natürlich noch beliebige Hinweise und Steuerelemente ergänzen. Wichtig bei der Eingabe ist, dass Sie nicht nur die Antworten eingeben, sondern auch das Kontrollkästchen für das Feld Wert aktivieren, wenn es sich um die richtige Antwort handelt.

Multiple-Choice-Tests anzeigen

Zum Anzeigen des Tests sind ähnliche Formulare notwendig. Sie müssen allerdings sicherstellen, dass

Sie benötigen dazu als Basis ein Formular, das an die Tabelle tabFragen gebunden ist. Darin zeigen Sie die Felder ID, Frage und Hilfe an, deaktivieren diese aber, indem Sie deren Eigenschaften Aktiviert auf Nein und Gesperrt auf Ja setzen (Abb. 2)

Bild 2: Deaktivieren der Eingabefelder für Frage und Hilfetext.

Außerdem müssen Sie in dieses Formular noch ein ungebundenes Eingabefeld einfügen, in dem der Benutzername eingegeben werden kann. Möchten Sie, dass automatisch der aktuelle Access-Benutzer angezeigt wird, geben Sie für die Eigenschaft Steuerelementinhalt die Access- Funktion =CurrentUser() an.

Damit der Benutzer nicht zur nächsten Frage navigieren kann, ohne dass die Antwort gespeichert wird, benötigen Sie innerhalb des Formulars eine Schaltfläche "Weiter", die Sie später noch mit entsprechendem VBA-Code ausstatten. Gleichzeitig sollten Sie dann die Navigationsschaltflächen, Trennlinien und Datensatzzeiger abschalten.

Die Steuerelemente für die Antwortenanzeigen

Zur Anzeige der Antworten benötigen Sie wieder ein Unterformular. Dabei sind Sie gleich mit einer Reihe von Schwierigkeiten konfrontiert:

Bei einem gebundenen Formular, das die Datensätze aus der Tabelle tabAntworten passend zum übergeordneten Formular anzeigt, werden natürlich auch die richtigen Antworten angezeigt. Dann braucht der Benutzer aber natürlich keinen Test mehr machen, wenn er die Antworten schon angezeigt bekommt. Sie müssten also zumindest das Kontrollkästchen als ungebundenes Steuerelement einfügen. In einem Endlosformular würden dann beim Aktivieren eines Kontrollkästchens das Kontrollkästchen jedes Datensatzes aktiviert werden. Zudem können Sie die Daten so nicht auswerten, weil Sie immer nur Zugriff auf den aktuellen Datensatz haben.

Die Lösung dieses Problems besteht im Prinzip darin, die Steuerelemente erst zur Laufzeit zu erzeugen, und zwar in der benötigten Anzahl und im Kopf- oder Fußbereich des Formulars, da sie nicht für jeden Datensatz in der Tabelle tabAntworten wiederholt werden sollen. Dabei gibt es aber ein zweites Problem: Sie wissen nicht, wie viele Steuerelemente Sie benötigen, weil die maximale Anzahl Antworten nicht begrenzt ist. Zudem können Sie Steuerelemente in Access nur in der Entwurfsansicht erstellen. Wenn Sie das Formular aber als Unterformular anzeigen lassen, können Sie nicht bei jedem Datensatzwechsel im Hauptformular das Unterformular in der Entwurfsansicht öffnen, anpassen und wieder neu laden. Es gibt daher nur einen wirklich praktikablen Weg, der nachfolgend im Detail beschrieben wird:

Unterformular und Hilfsfunktionen erstellen

Als Unterformular erstellen Sie einfach ein ungebundenes Formular, das jedoch einen Kopfbereich haben muss. Um diesen zu erzeugen, blenden Sie ihn in der Entwurfsansicht des Formulars über Ansicht/Formularkopf/-fuß ein. Innerhalb des Formulars sollte sich kein Steuerelement befinden.

Außerdem benötigen Sie noch eine Funktion, die ermittelt, wie hoch die maximale Zahl von Antworten für die Fragen des Tests ist. Die Funktion verwendet dazu die Domänen-Aggregatfunktion dMax, die Sie auf eine Abfrage oder Tabelle anwenden können. Sie benötigen dazu eine Tabelle oder Abfrage, die die Anzahl von Antworten für die einzelnen Fragen zurückgibt. Da Sie diese Abfrage später noch mehrmals benötigen, lohnt es sich, sie als Abfrage in der Datenbank zu speichern (Bild 3). Sie können für die Abfrage den folgenden SQL-Code verwenden und auf Basis dieses Codes eine Abfrage abfMaxAntwortenerstellen:

Bild 3: Aufbau der Abfrage abfMaxAntworten.

SELECT Frage, Count(ID) AS Anzahl
FROM tabAntworten
GROUP BY Frage;

Nun benötigen Sie nur noch eine Funktion, die die Domänenaggregatfunktion aufruft und die maximale Anzahl Antworten zurückgibt (Listing 1).

Function getMaxAnzahl()
getMaxAnzahl = DMax("Anzahl", "abfMaxAntworten")
End Function

Steuerelemente im Unterformular erstellen

Innerhalb des Unterformulars benötigten Sie für jede mögliche Antwort

Das heißt also, Sie benötigen dreimal so viele Steuerelemente, wie es maximale Antworten gibt. Spätestens beim zweiten Aufruf der Prozedur begegnen Ihnen jedoch Fehlermeldungen, weil die Steuerelemente eventuell schon vorhanden sind. Daher müssen Sie zuerst alle Steuerelemente des Formulars löschen, bevor Sie die benötigten neu erzeugen. Beides übernimmt die Prozedur Steuerelemente_ erstellen (Listing 2).

Sub Steuerelemente_erzeugen()
Dim objContr As Control
Dim objLabel As Control
Dim strName As String
Dim lngOben As Long
Dim lngLinks As Long
Dim lngI As Long
Dim lngMax As Long
lngMax = getMaxAnzahl()
Const Abstand = 100
Application.Echo 0
'vorhandene Steuerelemente löschen
strName = "frmAntwortenAnzeigenUfo"
DoCmd.OpenForm strName, acDesign
Set objForm = Screen.ActiveForm
Do
For Each objContr In objForm
DeleteControl strName, objContr.Name
Next objContr
Loop Until objForm.Controls.Count = 0
lngLinks = Abstand
lngOben = 500
For lngI = 1 To lngMax
Set objContr = CreateControl(strName, acCheckBox, acHeader, , , lngLinks, _
lngOben, 300, 300)
objContr.Name = "Wert" & lngI
lngLinks = lngLinks + 500 + Abstand
Set objLabel = CreateControl(strName, acLabel, acHeader, "Aufschrift", , _
lngLinks, lngOben, 1900, 250)
objLabel.Caption = "Aufschrift"
objLabel.Name = "Antwort" & lngI
lngLinks = lngLinks + 1900 + Abstand
Set objContr = CreateControl(strName, acTextBox, acHeader, , , lngLinks,
lngOben, 300, 250)
objContr.Name = "ID" & lngI
objContr.Visible = False
'objContr.Value = lngI
lngOben = lngOben + 250 + Abstand
lngLinks = Abstand
Next lngI
'Formular schließen und speichern
DoCmd.Close acForm, strName, acSaveYes
Application.Echo 1
End Sub
Sub TestAnzeigen()
Steuerelemente_erzeugen
DoCmd.OpenForm "frmTestAnzeigen", acNormal
End Sub

Wichtig ist, dass Sie die Steuerelemente so benennen, dass Sie später auf einen bestimmten Datensatz zugreifen können. Daher werden die Kontrollkästchen "Wert", die Labelfelder "Antwort" und die Textfelder "ID" gefolgt von einer fortlaufenden Nummer genannt. Diese ist für alle Steuerelemente, die einen Datensatz bilden, gleich und resultiert aus dem Wert der Schleifenvariablen lngI.

Die Prozedur Steuerelemente_erzeugen rufen Sie auf, bevor das Hauptformular angezeigt wird. Dafür sorgt die Prozedur TestAnzeigen.

Daten in das Unterformular laden

Wenn Sie nun die Prozedur TestAnzeigen ausführen und das Formular aufrufen (Bild 4), werden nur die erzeugten Steuerelemente ohne gültige Inhalte angezeigt. Damit die passenden Antworten zu der Frage im Hauptformular angezeigt und die überflüssigen Steuerelemente ausgeblendet werden, müssen Sie einer Ereignisprozedur für das Current-Ereignis des Hauptformulars sorgen.

Bild 4: Vorläufige Anzeige des Haupt- und Unterformulars.

Zunächst prüfen Sie, ob noch ein Datensatz im Hauptformular angezeigt wird. Das ist nicht mehr der Fall, wenn das ID-Feld den Wert Null hat. In diesem Fall geben Sie einfach eine Meldung aus und sorgen dafür, dass das Formular geschlossen wird. Das geht allerdings nicht direkt innerhalb der Ereignisprozedur, sondern erst nach Abschluss der Prozedur. Damit das Formular dann trotzdem geschlossen wird, setzen Sie einfach eine auf Modulebene deklarierte Variable boolSchliessen auf true und initialisieren den Timer des Formulars. In der Timer-Ereignisprozedur prüfen Sie dann den Wert der Variablen und schließen gegebenenfalls das Formular.

Falls noch ein gültiger Datensatz im Hauptformular angezeigt wird, rufen Sie zunächst mit Hilfe einer SQL-Anweisung ein Recordset mit den Antworten zur aktuellen Frage ab und durchlaufen es in einer Schleife. Für jeden Datensatz geben Sie dann die entsprechende Labelaufschrift und den Feldwert des Feldes ID aus. Anschließend blenden Sie in einer weiteren For-Schleife die nicht benötigten Steuerelemente aus.

Dim boolSchliessen As Boolean
Private Sub Form_Current()
If IsNull(Me.ID.Value) Then
MsgBox "Test abgeschlossen!", vbInformation
boolSchliessen = True
Me.TimerInterval = 500
Else
'Daten in das UnterFormular laden
Dim objUForm As Form
Dim strSQL As String
Dim objRS As Recordset
Dim lngMax As Long
Dim lngDS As Long
Dim lngI As Long
Set objUForm = Me.frmAntwortenUfo.Form
'Daten ermitteln
strSQL = "SELECT * FROM tabAntworten WHERE Frage=" & Me.ID.Value
Set objRS = Application.CurrentDb.OpenRecordset(strSQL)
'Recordset durchlaufen und Steuerelemente initialisieren
If objRS.RecordCount > 0 Then
objRS.MoveFirst
lngDS = 1
Do While objRS.EOF = False
objUForm.Controls("Antwort" & lngDS).Caption = _
objRS.Fields("Antwort").Value
objUForm.Controls("ID" & lngDS).Value = objRS.Fields("ID").Value
objUForm.Controls("Wert" & lngDS).Visible = True
objUForm.Controls("Wert" & lngDS).Value = False
objUForm.Controls("Antwort" & lngDS).Visible = True
objUForm.Controls("ID" & lngDS).Visible = False
objRS.MoveNext
lngDS = lngDS + 1
Loop
End If
objRS.Close
'Nicht benötigte Steuerelemente ausblenden
lngMax = CLng(objUForm.Controls.Count / 3)
For lngI = lngDS To lngMax
objUForm.Controls("Wert" & lngI).Visible = False
objUForm.Controls("Antwort" & lngI).Visible = False
objUForm.Controls("ID" & lngI).Visible = False
Next lngI
End If
End Sub
Private Sub Form_Timer()
If boolSchliessen = True Then
DoCmd.Close acForm, Me.Name, acSaveYes
End If
End Sub

Bild 5: Korrekte Anzeige des Multiple-Choice-Tests.

Damit wird der Test nun korrekt angezeigt, und zu jeder Frage werden die möglichen, definierten Antworten gezeigt (Bild 5).

Antworten speichern

Was nun noch fehlt ist die Speicherung der Daten in der Tabelle tabErgebnisse. Dazu erstellen Sie zunächst eine Ereignisprozedur für den Speichern- Button, indem Sie eine Hilfsfunktion Speichern aufrufen. Dieser übergeben Sie ein Array mit den Daten aus dem Unterformular und die Daten aus dem übergeordneten Formular, also die ID der Frage und den Benutzer (Listing 4).

Private Sub bttWeiter_Click()
On Error GoTo Err_bttWeiter_Click
'Ermitteln der Daten im Unterformular
Dim lngMax As Long
Dim objUForm As Form
Dim arrDaten As Variant
Dim lngI As Long
Dim lngZeilen As Long
Set objUForm = Me.frmAntwortenUfo.Form
lngMax = objUForm.Controls.Count \ 3
lngZeilen = 0
For lngI = 0 To lngMax - 1
If (objUForm.Controls("Wert" & lngI + 1).Visible = True) _
And (objUForm.Controls("Wert" & lngI + 1).Value = True) Then
If lngZeilen = 0 Then
ReDim arrDaten(1, 0)
Else
ReDim Preserve arrDaten(1, lngZeilen)
End If
arrDaten(0, lngZeilen) = objUForm.Controls("Wert" & lngI + 1).Value
arrDaten(1, lngZeilen) = objUForm.Controls("ID" & lngI + 1).Value
lngZeilen = lngZeilen + 1
End If
Next lngI
'Speichern der Daten
Speichern Me.ID.Value, Me.txtBenutzer.Value, arrDaten
'Nächste Frage anzeigen
DoCmd.GoToRecord , , acNext
Exit_bttWeiter_Click:
Exit Sub
Err_bttWeiter_Click:
MsgBox Err.Description
Resume Exit_bttWeiter_Click
End Sub
Sub Speichern(lngID As Long, strBenutzer As String, arrDaten As Variant)
Dim strSQLDEL As String
Dim strSQLINS As String
Dim strSQLTemp As String
Dim lngI As Long
Dim lngMin As Long
Dim lngMax As Long
'Alle alten Antworten des Benutzers zu dieser Frage löschen
strSQLDEL = "DELETE FROM tabErgebnisse WHERE Benutzer='" & _
strBenutzer & "' AND Frage=" & lngID & ";"
Application.CurrentDb.Execute strSQLDEL
If Not (IsArray(arrDaten)) Then
Exit Sub
End If
strSQLINS = "INSERT INTO tabErgebnisse (Frage,Antwort,Benutzer) VALUES ( "
lngMin = LBound(arrDaten, 2)
lngMax = UBound(arrDaten, 2)
'Array durchlaufen und SQL-Anweisung zusammensetzen
For lngI = lngMin To lngMax
strSQLTemp = strSQLINS & lngID & "," & arrDaten(1, lngI) & ",'" & _
strBenutzer & "');"
'SQL-Anweisung ausführen
Application.CurrentDb.Execute strSQLTemp
Next lngI
End Sub

Zunächst müssen Sie dazu aber das Array erzeugen und mit den benötigten Daten füllen. Dazu speichern Sie zunächst einen Verweis auf das Unterformular in der Variablen objUForm und ermitteln dann die Anzahl der darin enthaltenen Steuerelemente. Geteilt durch 3 ergibt das die maximale Anzahl Datensätze, die dargestellt werden können. Diese Zahl benötigen Sie für den Zugriff auf die Steuerelemente des Formulars.

Erweitern des dynamischen Array

In einer Schleife, die von 0 bis zur ermittelten Zahl lngMax -1 läuft, füllen Sie dann das Array arr- Daten. Die Daten müssen Sie allerdings nur dann speichern, wenn der Benutzer die Antwort ausgewählt hat. Das geht nur, wenn das Kontrollkästchen überhaupt sichtbar ist. Sie müssen daher innerhalb der For-Schleife prüfen, ob das Kontrollkästchen sichtbar ist und den Wert true hat.

Nur dann erweitern Sie das dynamische Array um eine weitere Zeile und speichern dort den Eintrag. Problematisch ist das allerdings, wenn der Benutzer keine Antwort ankreuzt. Dann würde bei einer Variablendeklaration mit Dim arrDaten() As Variant nur ein undimensioniertes Array entstehen, das beim Zugriff auf die Dimensionsgrenzen mit UBound und LBound oder den Array- Inhalt zu einer Fehlermeldung führen würde. Daher wurde hier eine einfache Variant-Variable deklariert.

Zum Schluss übergeben Sie das Array an die Prozedur Speichern. Nach Aufruf der ProzedurSpeichern rufen Sie mit der Methode GoToRecord des DoCmd-Objekts den nächsten Datensatz auf. Darauf wird wieder die Current-Ereignisprozedur ausgeführt, die die verfügbaren Antworten aus der Tabelle lädt und das Unterformular anpasst.

In der Prozedur Speichern löschen Sie zunächst mit einer entsprechenden SQL-DELETEAnweisung alle vorherigen Antworten zu dieser Frage des aktuellen Benutzers. Damit verhindern Sie, dass Sie zunächst prüfen müssen, ob es schon Antworten zu dieser Frage gibt, die Sie dann entweder ändern oder neu einfügen müssen. Bevor Sie die Daten des Arrays in die Datenbanktabelle einfügen, sollten Sie nun jedoch prüfen, ob überhaupt ein Array als Parameter arrDaten übergeben wurde. Das können Sie mit der IsArray-Funktion tun. Wurde kein Array übergeben, verlassen Sie die Prozedur einfach mit Exit Sub. Eine Speicherung von Daten ist dann nicht notwendig, weil der Benutzer keine Antwort ausgewählt hat.

Anschließend durchlaufen Sie das Array in einer Schleife und setzen für jede Zeile die SQLAnweisung zusammen und führen Sie aus.

Da nur die aktivierten Antworten gespeichert werden, hält sich die Datenmenge pro Test in Grenzen. Zudem können Sie später bei der Auswertung der Ergebnisse recht schnell vergleichen, ob die richtigen Antworten gegeben wurden.

Wie geht es weiter?

Ein wesentlicher Teil von Multiple-Choice-Tests ist nun realisiert: die Anzeige des Tests. Ähnlich komplex wird allerdings die Auswertung, da Sie die definierten Ergebnisse und die vom Benutzer gegebenen Antworten vergleichen müssen.

Bild 6: Die gespeicherten Testergebnisse.

Wenn Sie dem Benutzer außerdem noch eine Liste der falsch beantworteten Fragen anzeigenm möchten, wird es noch komplizierter. Dennoch ist eine komfortable Auswertung des Tests natürlich möglich. Wie das aussehen kann, zeigt der letzte Teil der Artikelfolge im Detail.