Menu e shortcut in Delphi

Tra le piccole insidie della programmazione in Delphi, la gestione di menu e shortcut è forse una delle meno note. Trascurarne alcuni aspetti può portare a comportamenti errati o incompleti, ma la soluzione è spesso molto semplice.

Gestire voci di menu e shortcut in Delphi è un’operazione piuttosto semplice. La procedura più consueta prevede di disaccoppiare l’interfaccia grafica dal codice vero e proprio; per esempio, i componenti che gestiscono i menù sono collocati in una form, mentre in un datamodule troveranno posto le azioni, con proprietà ed eventi che saranno validi per tutti i componenti associati ad esse.

A volte, però, capita che il baco si nasconda nei dettagli e renda difficile trovare la chiave giusta per scrivere codice altamente riutilizzabile.

Consideriamo il caso di una voce di menù che può agire su un numero arbitrario di controlli, senza limitazioni rispetto al loro tipo. Un esempio sono le classiche voci di copia e incolla, cui tradizionalmente si associano gli shortcut CTRL+C e CTRL+V, che devono operare su tipi TEdit, TCombo, e così via. Il codice che implementa le operazioni di copia o incolla devono sapere quale componente contiene i dati da leggere o da modificare; saperlo a priori, nella maggior parte dei casi, è impossibile. Ogni volta che la voce di menù è usata dall’utente, deve determinare su quale controllo dovrà operare.

In generale, possiamo dire che, se la voce appartiene al menu principale, le operazioni andranno svolte sul controllo attivo nella form; ma come individuare il controllo giusto quando, invece, l’azione è invocata da un menu popup?

Vediamo un piccolo esempio. Attraverso una voce di menù, copieremo in una label il nome dei componenti sui quali la voce stessa agisce. Saranno proposte tre soluzioni, l’ultima delle quali sembra essere quella più completa. Il codice completo è disponibile nel nostro spazio di GitHub.

Form del programma dimostrativo.
La form del programma dimostrativo.

La soluzione di base

Quando si scrive codice altamente generico, come per esempio BindAPI, non si possono conoscere gli oggetti sui quali la procedura dovrà lavorare. L’Object Pascal, così come il FreePascal, permette di sbrigare questa incombenza con la consueta eleganza:

procedure TForm1.Test1_Click(Sender: TObject);
var
  lParentMenu: TMenu;
  lComponent: TComponent;
begin
  lParentMenu := TMenuItem(Sender).GetParentMenu ;
  lComponent := TPopupMenu(lParentMenu).PopupComponent ;
  Label1.Caption := lComponent.name;
end;

Per provare questo piccolo blocco di codice, mettiamo in una form due componenti TEdit, un componente TLabel e un componente TPopupMenu con una sola voce. Questo menu popup sarà associato a tutti i componenti della form. “Test” e CRTL+M saranno caption del menu e shortcut associato.

Ora compiliamo ed eseguiamo il programma. Spostiamo il mouse sul primo controllo TEdit, schiacciamo il tasto destro e facciamo click sulla voce di menù: nella label vedremo il nome “Edit1”. Ora posizioniamo il mouse sulla label e ripetiamo l’operazione: la sua caption tornerà ad essere Label1. Collochiamo il mouse sul secondo controllo TEdit e ripetiamo l’operazione: il testo diventerà “Edit2”.

Il baco nascosto nello shortcut

Sembra funzionare tutto bene. Ora torniamo con il mouse su Edit1, gli diamo il focus premendo il tasto sinistro, e infine premiamo i tasti CTRL+M: questa è la combinazione che abbiamo scelto nell’editor delle proprietà.

Editor delle proprietà di Delphi.
L’editor delle proprietà per la voce di menù.

Ci aspetteremmo che il testo nell’etichetta torni ad essere “Edit1”, invece non cambia. Questo accade perché la proprietà PopupComponent conserva la memoria dell’ultimo controllo che ha richiamato il menu popup attraverso il mouse, anche se nel frattempo ha perso il focus.

Capire se la voce di menu è stata aperta con un click o richiamata da uno shortcut è senz’altro possibile, ma richiede di mettere mano a non poco codice; per questo, alcuni raccomandano di far agire le procedure legate a voci di menù sul controllo attivo:

procedure TForm1.Test2_Click(Sender: TObject);
var
  lComponent: TComponent;
begin
  lComponent := Screen.ActiveControl;
  Label1.Caption := lComponent.name;
end;

Il problema, però, è che se ora andate sull’etichetta, aprite il menu e fate click sulla sua voce, la procedura scriverà il nome del componente TEdit attivo, non quello della label. In generale, questo sistema fallirà per tutti i componenti che non possono essere selezionati.

La soluzione più semplice

Il giusto compromesso fra semplicità e funzionalità, che può essere poi personalizzato secondo le esigenze, ci pare qualcosa di questo tipo:

function TForm1.FindComponentFromMenu(Sender: TObject): TComponent;
var
  lParentMenu: TMenu;
  lControl: TComponent;
begin
  lControl := nil;
  if Sender is TMenuItem then
  begin
    lParentMenu := TMenuItem(Sender).GetParentMenu;
    if lParentMenu is TPopupMenu then
    begin
      lControl := TPopupMenu(lParentMenu).PopupComponent;
      TPopupMenu(lParentMenu).PopupComponent := nil;
    end;
  end;
  if lControl is TGraphicControl then
    Result := lControl
  else
    Result := Screen.ActiveControl;
end;

La logica è assai semplice. Per prima cosa si verifica se il Sender è di tipo TMenuItem e si trova in un componente TPopupMenu. Se è così, ricava il componente puntato da PopupComponent e se discende da TGraphicControl, lo restituisce come risultato; altrimenti, restituisce il controllo attivo. Notate che dopo aver memorizzato il controllo nella variabile lControl, dobbiamo resettare il valore di PopupComponent. Se non lo facessimo, ogni volta che con il tasto destro apriamo il menù su Label1 e richiamiamo la procedura, la proprietà PopupComponent punterà ancora a Label1 fino a quando non si cambia esplicitamente il focus. E’ ovvio che ciò creerebbe problemi se usassimo subito lo shortcut CTRL+M.

L’evento OnClick associato alla voce di menu cambierà di conseguenza:

procedure TForm1.Test3_Click(Sender: TObject);
var
  lComponent: TComponent;
begin
  lComponent := FindComponentFromMenu(Sender);
  if Assigned(lComponent) then
    Label1.Caption := lComponent.Name;
end ;

Ora tutti gli elementi della form ai quali si associa il menu permetteranno una corretta gestione delle informazioni. Da questa struttura estremamente semplice si potranno derivare controlli più sofisticati sul corretto componente da usare.

Conclusioni

Questo semplice esempio su voci di menu e shortcut in Delphi ci ricorda che spesso i problemi di programmazione più insidiosi si nascondono nei dettagli. Anche se può sembrare scontato che il funzionamento della procedura richiamata da mouse o da shortcut sia uguale, nella realtà questo risultato si ottiene solo con una buona conoscenza dei meccanismi che lo regolano. Nel prossimo post vedremo che cosa succede agli shortcut quando sono associati alle action che si trovano in un datamodule.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *