13. Implementieren von Menüs: Bearbeiten, Textgröße und andere, Beispiel

Dieses Kapitel zeigt an einem Beispiel, wie man Menüs implementiert, die mit dem Target zusammenarbeiten sollen. Das sind
  • Ein "Bearbeiten" Menü (Kopieren, Einfügen usw.)

  • Ein Menü "Größe" (Ändern der Schriftgröße)

  • Ein Menü "Font" (Ändern des Textfonts)
Dazu schreiben wir ein Programm, dass neben den genannten Menüs drei Textobjekte enthält, wobei zwei davon mit den Menüs zusammenarbeiten sollen, eins aber nicht.

Bild

Der komplette Sourcecode, inklusive der hier nicht explizit dargestellten Objekte, finden Sie bei den Beispielen unter "Objekte\Allgemeines\Edit Menü & mehr".

Die Grundidee ist folgende:

  • Klickt der Nutzer mit der Maus in ein Textobjekt so wird es zum Target. Das Objekt, das vorher das Target war verliert dabei diesen Status. Wir schreiben also einen OnTargetChanged Handler, der die Menüs updated, so dass sie den Zustand des neuen Targetobjekts reflektieren.

  • Ist ein Objekt das Target, das nicht mit den Menüs zusammenarbeiten soll, werden die Menüs komplett disabled.

  • Um die Buttons "Ausschneiden", "Kopieren" und "Löschen" zu verwalten benötigen wir außerdem einen OnSelectionChanged Handler, der gerufen wird, wenn der Nutzer in einem Textobjekt etwas selektiert oder deselektiert.

  • Um den Button "Einfügen" zu verwalten, benötigen wir einen OnClpChange Handler für das Applicationobjekt.

Die Textobjekte

Die ersten beiden Texte sollen mit den Menüs interagieren und haben deswegen sowohl einen OnTargetChanged als auch einen OnSelectionChanged Handler. Beachten Sie, dass die Handler für beide Textobjekte die gleichen sind. FontSize und FontID sind jedoch unterschiedlich. Der dritte Text verfügt über keinerlei Handler.
Memo Text1
  fontSize = 14
  fontID = FID_MONO
  text$ = "Ein Mops kam in die Küche"
  defaultFocus
  OnTargetChanged = HandleTarget
  OnSelectionChanged = HandleSelection
  fixedSize = 200, 150
End Object

Memo Text2
  fontSize = 24
  fontID = FID_SANS
  text$ = "und stahl dem Koch ein Ei."
  OnTargetChanged = HandleTarget
  OnSelectionChanged = HandleSelection
  fixedSize = 200, 150
End Object

Memo Text3
  text$ = "Ich interagiere nicht mit den Menüs!"
  ExpandWidth
End Object

Die Menüs

Das Edit-Menü ("Bearbeiten") enthält die Buttons für die entsprechenden Funktionen. Jeder Button hat seinen eigenen Actionhandler und einen Keyboard Shortcut gesetzt. Keyboard Shortcuts sind im Kapitel 3.1.4 des Objekthandbuchs beschrieben.
Menu EditMenu
  Caption$ = "Bearbeiten", 0
  Children = CutButton, CopyButton, PasteButton, DeleteButton
End OBJECT

Button CutButton
  Caption$ = "Ausschneiden", 0
  ActionHandler = DoCut           ' ButtonAction
  kbdShortcut = KSM_CTRL+ KSM_PHYSICAL+ ASC("x")
End OBJECT

Button CopyButton
  Caption$ = "Kopieren", 0
  ActionHandler = DoCopy          ' ButtonAction
  kbdShortcut = KSM_CTRL+ KSM_PHYSICAL+ ASC("c")
End OBJECT

Button PasteButton
  Caption$ = "Einfügen", 0
  ActionHandler = DoPaste         ' ButtonAction
  kbdShortcut = KSM_CTRL+ KSM_PHYSICAL+ ASC("v")
End OBJECT

Button DeleteButton
  Caption$ = "Löschen", 0
  ActionHandler = DoDelete        ' ButtonAction
  kbdShortcut = &hF9A             ' Siehe KeyCodes Library
End OBJECT


Die Menüs für Zeichengröße und Font sind prinzipiell gleich aufgebaut. Jedes Menü enthält eine RadioButtonGroup mit drei RadioButton-Objekten. Das besondere ist, dass der Identifier der RadioButton-Objekte gleichzeitig die Eigenschaft repräsentiert, die er einstellen soll. Die RadioButton-Objekte aus dem FontSelector sind Font-ID's und die aus dem SizeSelektor sind Punktgrößen.

Menu FontMenu
  Caption$ = "Font", 0
  Children = FontSelector
End OBJECT

RadioButtonGroup FontSelector
  Children = FontOption1, FontOption2, FontOption3
  ApplyHandler = ChangeFont           ' ListAction
  selection = FID_MONO
End OBJECT

RadioButton FontOption1
  Caption$ = "Mono" : identifier = FID_MONO
End OBJECT

RadioButton FontOption2
  Caption$ = "Sans" : identifier = FID_SANS
End OBJECT

RadioButton FontOption3
  Caption$ = "Symbol" : identifier = FID_SYMBOLPS
End OBJECT


Das Menü "SizeMenu" ist prinzipiell genauso aufgebaut.

Menu SizeMenu
  Caption$ = "Größe", 0
  Children = SizeSelector
End OBJECT

RadioButtonGroup SizeSelector
Children = SizeOption1, SizeOption2, SizeOption3
orientChildren = ORIENT_VERTICALLY   ' ORIENT_HORIZONTALLY
ApplyHandler = ChangeSize ' ListAction
selection = 14
  End OBJECT
RadioButton SizeOption1
Caption$ = "14 pt" : identifier = 14
  End OBJECT
RadioButton SizeOption2
Caption$ = "18 pt" : identifier = 18
  End OBJECT
RadioButton SizeOption3
Caption$ = "24 pt" : identifier = 24
  End OBJECT

Das Application Objekt

Das Application Objekt muss einen OnClpChange Handler haben, damit das Clipboard überwacht werden kann.
Application DemoApplication
    Children = DemoPrimary
    OnClpChange = MonitorClipboard
END Object

Überwachen des Clipboard

Der OnClpChange Handler wird jedes Mal gerufen, wenn irgendeine Applikation Änderungen am Clipboard vornimmt. Das schließt unser eigenes Programm mit ein. Wir müssen daher nur nachschauen, ob ein Text im Clipboard ist und den "Einfügen" Button enablen oder disablen. Dazu verwenden wir die BASIC Routine ClipboardTest. Das Format der Daten, die sich im Clipboard befinden, wird durch eine eindeutige Kombination aus ManufacturerID (manufID) und Format-Nummer (formatNo) gekennzeichnet. Für Texte gilt: manufID = 0 (GeoWorks), formatNo. = 0 (TEXT). Mehr dazu finden Sie im Kapitel 5 "Arbeit mit der Zwischenablage".
SYSTEMACTION MonitorClipboard
  IF ClipboardTest(0, 0) then
    PasteButton.enabled = TRUE
  else
    PasteButton.enabled = FALSE
  End IF
END ACTION   ' MonitorClipboard
Sollte gerade ein Objekt das Target sein, dass nicht mit den Menüs zusammenarbeiten soll oder kann, so stört uns das hier nicht, da wir an anderer Stelle sicherstellen, dass das Menu-Objekt dann nicht enabled ist. Der PasteButton ist dann niemals aktiv, auch wenn er enabled ist.

Die anderen Buttons des Bearbeiten-Menüs

Alle anderen Buttons des Bearbeiten-Menüs müssen enabled werden wenn der Nutzer Text selektiert hat, andernfalls müssen sie disabled werden. Da diese Abfrage mehrfach gebraucht wird lagern wir sie in eine SUB aus. Der Parameter textObj bezeichnet das aktuelle Targetobjekt, die Instancevariable selectionLen enthält die Anzahl der selektierten Zeichen.
SUB UpdateEditMenu (textObj as OBJECT)
  IF textObj.selectionLen THEN  ' d.h. selectionLen <> 0
    CopyButton.enabled = TRUE
    CutButton.enabled = TRUE
    DeleteButton.enabled = TRUE
  ELSE
    CopyButton.enabled = FALSE
    CutButton.enabled = FALSE
    DeleteButton.enabled = FALSE
  END IF
END SUB                         'UpdateEditMenu

Die Textobjekt- Handler

Jetzt kümmern wir uns darum, was passiert, wenn der Nutzer ein Textobjekt anklickt. Das entsprechende Objekt wird dann zum Target. Vorher - und das ist sehr wichtig für uns - verliert das Objekt, das bis dahin Target war, jedoch seine Target-Status. Der OnTargetChanged Handler wird also zweimal gerufen: zuerst von dem Objekt das bis dahin Target war (mit dem Parameter hasTarget = FALSE) und danach von dem Objekt das jetzt Target wird (mit dem Parameter hasTarget = TRUE).

Wird er Handler also wegen einem Targetverlust gerufen disablen wir einfach alle Menüs, andernfalls enablen wir sie und updaten die UI.

Das hat einen weiteren Vorteil: Klickt der User auf ein Objekt, dass nicht mit den Menüs zusammenarbeiten kann oder soll (in unserem Fall Text3), so wird der OnTargetChanged Handler nur einmal gerufen (mit hasTarget = FALSE) und die Menüs bleiben so lange disabled, bis der Nutzer wieder in eins der Objekte Text1 oder Text2 klickt.

TARGETACTION HandleTarget

  if hasTarget = FALSE THEN
    Editmenu.enabled = FALSE
    Fontmenu.enabled = FALSE
    Sizemenu.enabled = FALSE
    return
  end if

  EditMenu.enabled = TRUE
  UpdateEditMenu (sender)

  FontMenu.enabled = TRUE
  FontSelector.selection = sender.fontID

  SizeMenu.enabled = TRUE
  SizeSelector.selection = sender.fontSize

END ACTION     ' HandleTarget


Außerdem müssen wir noch das Edit-Menü informieren, falls der Nutzer die Textselektion ändert.

TEXTACTION HandleSelection
  UpdateEditMenu (sender)
END ACTION

Handler des Bearbeiten-Menüs

Nun müssen wir die ActionHandler der Buttons aus dem Bearbeiten-Menü implementieren. Da wir nicht wissen können, ob das aktuelle Target das Objekt Text1 oder Text2 ist verwenden wir als Ziel die globale Variable Target. Wenn die Handler der Menüs aufgerufen werden kann dies nur eines der beiden genannten Objekte sein, da wir vorne sichergestellt haben das die Menüs nur aktiv sind, wenn eines dieser Objekte das Target ist.

Die Handler an sich sind relativ einfach. Wir verwenden die für alle GenericClass Objekte definierten Clipboard-Methoden ClpCopy und ClpPaste. Ausschneiden entspricht einem Kopieren in das Clipboard mit anschließendem Löschen. Für das Löschen verwenden wir die Textobjekt Methode DeleteSelection. Außerdem müssen wir noch das Edit-Menü updaten wenn wir etwas gelöscht haben. Dazu greifen wir wieder auf die globale Variable Target zurück

BUTTONACTION DoCut
  Target.ClpCopy
  Target.DeleteSelection
  UpdateEditMenu (Target)
END ACTION

BUTTONACTION DoCopy
  Target.ClpCopy
END ACTION

BUTTONACTION DoPaste
  Target.ClpPaste
END ACTION

BUTTONACTION DoDelete
  Target.DeleteSelection
  UpdateEditMenu (Target)
END ACTION

Die anderen Menü-Handler

Die Menüs - und damit die RadioButtonGroups - sind nur aktiv, wenn eines der Objekte Text1 oder Text2 das Target ist. Da die Identifier der RadioButton-Objekte ihre Funktion (eine FontID oder eine Schriftgröße) widerspiegeln werden die Actionhandler der RadioButtonGroups sehr einfach.
LISTACTION ChangeFont
  Target.fontID = selection
END ACTION' ChangeFont

LISTACTION ChangeSize
  Target.fontsize = selection
END ACTION' ChangeSize

Abschließende Überlegungen

Besondere Aufmerksamkeit verdient die Frage, ob die Menüs am Programmstart wirklich immer die korrekte Situation widerspiegeln. In unserem Fall müssen die Menüs die Eigenschaften des Objekts Text1 widerspiegeln, weil dieses Objekt den Hint defaultFocus gesetzt hat. Hier ist manchmal etwas Handarbeit (setzen der richtigen Startwerte) angesagt.

Der eleganteste und sicherste Weg um eventuell verbleibende Probleme zu umgehen ist, einen OnStartup Handler für das Applicationobjekt zu schreiben. Dort können Sie die UI-Objekte Ihren Vorstellungen nach anpassen.

Application DemoApplication
  Children = DemoPrimary
  OnStartup = StartupCode
  ...
END Object
SYSTEMACTION StartupCode
  UpdateEditMenu (Text1)
  ...
END ACTION  ' StartupCode
Für den Anfänger ist es oft sehr schwer, die Zusammenhänge zu überblicken. In unserem Fall stellt sich die Situation für einen erfahrenen Programmierer so dar: Für den Zustand der Menüs sind genau zwei Routinen zuständig: der Actionhandler MonitorClipboard und die SUB UpdateEditMenu. MonitorClipboard wird am Programmstart automatisch gerufen. Da das Objekt Text1 den Hint defaultFocus gesetzt hat, wird es am Programmstart auch gleichzeitig zum Target. Damit wird der Handler HandleTarget mit dem Parameter hasTarget = TRUE am Programmstart gerufen, was den Aufruf von UpdateEditMenu zur Folge hat. Damit sind die Menüs auf dem aktuellen Stand. Wie gesagt, der Einsteiger überblickt so etwas nicht.

Deswegen die folgenden Tipps:

  • Prüfen Sie den Zustand der Menüpunkte am Programmstart bewusst nach.

  • Versuchen Sie, falls möglich, absichtlich verschiedene Zustände herzustellen um die Schwachstellen zu finden. Starten Sie das Programm zum Beispiel bewusst einmal mit und einmal ohne dass sich Text im Clipboard befindet.

  • Disablen Sie die Menüs per default (enabled = FALSE), wenn Sie nicht sicher wissen, dass sie aktiv sein dürfen. Fälschlicher Weise disablete Menüs fallen bei einer Sichtprüfung eher auf als fälschlicher Weise enablete Menüs.

^

Weiter...