Le avventure in VB.Net di un principiante ex-VB6 - 20 bis
a cura di Marcello Biglioli (requisiti: conoscenza elementare della programmazione a oggetti)Premessa
La ventesima puntata della serie da cui prende il titolo questo articolo ha attirato la mia attenzione, dato che sto iniziando (finalmente) a lavorare con .NET, anche se ho scelto C# invece del buon vecchio VB (spiegherò questa scelta, per i pochi a cui interessa, in appendice; per ora diciamo solo che si tratta di considerazioni assolutamente personali e "di gusto", dato che le potenzialità dei linguaggi sono assolutamente identiche).L'articolo, in sostanza, descriveva come "piegare" il controllo TabControl in dotazione al framework, per cambiarne l'aspetto. La soluzione di Diego e Oscar (uso della proprietà DrawMode) mi era piaciuta, e siccome sugggerivano di usare uno Snippet per inserire tutta una routine, avevo chiesto perché non derivare direttamente il controllo, in modo da averlo a disposizione già "aggiustato". Mi ha risposto Diego di essere "alle prime armi" (seee!).
(n.d.r.: In realtà, 'è alle prime armi' soprattutto il target di lettori della serie: non si voleva mettere troppa carne al fuoco - le 'note del revisore' come questa sono di Diego, che ha anche revisionato il codice VB).
Ok, mi sono detto, per uno come me che non ne sa nulla (ma che è curioso come una scimmia), quale migliore occasione per imparare e nel frattempo ricavare qualcosa di utile per il futuro? Ed ecco, in figura, il risultato: l'euTabControl. (eu sta per "EUSoft", la mia società). Già applicato "in produzione".
![]()
Le schede "Stampi" e "Ricerca Stampi" sono disabilitate (e cliccandoci sopra si ottiene solo un beep, non di censura, ma un suono di avviso che non si può abilitarle).
Introduzione
Uno degli aspetti più "speciali" della programmazione a oggetti sta nel fatto che è veramente facile modificare il comportamento di un oggetto (control, form, ma anche oggetti di tipo business e via dicendo) semplicemente ereditando da un altro e aggiungendo funzionalità o cambiandone il comportamento. Senza più dover ridefinire ogni proprietà, metodo e evento, come ho fatto per esempio in VB6 per "personalizzare" l'oggetto "Connection". Una vera faticaccia. In .NET, invece, è veramente semplice.Progettazione
Per prima cosa, mi sono detto, occorre PROGETTARE. Smettiamo di improvvisare soluzioni raffazzonate.
Cosa NON ha TabControl, che a me serve?
Soprattutto la possibilità di disabilitare le singole schede, magari impedendo di "accedere" a quelle disabilitate. E, ciliegina sulla torta, visualizzare in "grigino" il testo della scheda disabilitata. Per la verità ho in testa altre cose, ma non mettiamo troppa carne al fuoco. Dovrò aggiungere le proprietà "NormalColor", "DisabledColor" e i metodi "EnableTab" e "IsTabEnabled". Occorre spiegare a cosa servono? Spero di no, visto che ho scelto nomi il più possibile autoesplicativi.Implementazione
Dopo aver attentamente studiato, riprodotto e testato il codice di Diego e Oscar, mi sono messo di buzzo buono, e ho iniziato a scrivere le variabili private che mi mancavano, la proprietà, e via dicendo. Per iniziare scrivo://C# using System; using System.Collections.Generic; using System.Text; using System.Windows.Forms; using System.Drawing; namespace euControls { /// <summary> /// Estensione di TabControl per aggiungere /// BackColor /// Prop. TabVisible (add. TabPageCollection, sposta) /// OwnerDraw delle linguette da completare (icone, etc.) /// </summary> public class euTabControl : TabControl {'VB Option Strict On Imports System.Windows.Forms Imports System.Drawing Namespace euControls Public Class euTabControl Inherits TabControl(n.d.r.: In C# devono essere indicate esplicitamente tutte le direttive di importazione, mentre in Visual Basic alcune di esse sono già implicitamente impostate.
Option Strict è una facilitazione esclusiva di Visual Basic).Al di là di queste prime differenze, ciò che conta è che la mia classe/controllo EuTabControl, inserita nel mio Namespace EuControls, eredita dal controllo TabControl standard. Ovvero, in partenza, si comporta come la "madre", ha gli stessi membri, espone gli stessi eventi. Al limite, si potrebbe semplicemente non aggiungere nient'altro (a parte chiudere namespace e classe) e "creare" un control funzionante esattamente come il control di partenza. A cosa serve fare questa operazione? Per esempio, a utilizzare controlli "personalizzati" che verranno implementati in seguito, senza più modificare le form che li "usano".
//C# private List<string> m_DisabledPagesList = new List<string>(); private StringFormat m_Fmt = new StringFormat();'VB #Region "Campi" Private m_DisabledPagesList As New List(Of String)() Private m_Fmt As New StringFormat() #End RegionI campi sono costituiti dalla lista delle schede disabilitate e dal formato della stringa per "stampare" la proprietà "Text" della TabPage, che verrà definito nel costruttore.
//C# private Brush m_NormalColor = SystemBrushes.ControlText; public Brush NormalColor { get { return m_NormalColor; } set { m_NormalColor = value; } } private Brush m_DisabledColor = SystemBrushes.GrayText; public Brush DisabledColor { get { return m_DisabledColor; } set {m_DisabledColor = value;}}'VB #Region "Proprietà" Private m_NormalColor As Brush = SystemBrushes.ControlText Public Property NormalColor() As Brush Get Return m_NormalColor End Get Set(ByVal value As Brush) m_NormalColor = value End Set End Property Private m_DisabledColor As Brush = SystemBrushes.GrayText Public Property DisabledColor() As Brush Get Return m_DisabledColor End Get Set(ByVal value As Brush) m_DisabledColor = value End Set End Property #End Region(n.d.r.: in VB 2010 si evita questa verbosità di VB con le Property dichiarate su una riga sola, senza nemmeno la necessità del membro privato).
Le proprietà implementate sono i colori dei pennelli che da usare per scrivere le intestazioni di scheda. Vengono loro assegnati i valori di default, ma queste proprietà mi danno il modo di cambiare questo comportamento.
//C# public euTabControl() { m_Fmt.Alignment = StringAlignment.Center; m_Fmt.LineAlignment = StringAlignment.Center; base.DrawMode = TabDrawMode.OwnerDrawFixed; }'VB #Region "Costruttore" Public Sub New() m_Fmt.Alignment = StringAlignment.Center m_Fmt.LineAlignment = StringAlignment.Center Me.DrawMode = TabDrawMode.OwnerDrawFixed End Sub #End RegionNel costruttore imposto le proprietà del formato di stampa dei Text delle TabPages (centrati in entrambi i versi). Inoltre, dato che devo intervenire sulle proprietà del controllo base, uso la parola chiave base (MyBase in VB) per impostare la modalità OwnerDrawFixed come default per la proprietà DrawMode. (Fare riferimento alla 'base' per sfruttare il 'già fatto' è tipico del codice delle classi che ereditano).
(n.d.r.: Questo è un punto che potrebbe sembrare di difficile comprensione: la classe derivata imposta una proprietà della classe da cui deriva, onde imporre a se stessa tale proprietà, in conseguenza della derivazione. Infatti, dovrebbe funzionare ugualmente un Me.DrawMode (o un this.DrawMode) - e sarebbe più chiaramente comprensibile. Però quella di Marcello è una impostazione mentale influenzata dallo studio del C++: per lui è più chiara la versione che per me è più contorta).
//C# public bool IsEnabledTab(string tbName) { if (!TabPages.ContainsKey(tbName)) { MessageBox.Show("Tab non presente", "ERRORE", MessageBoxButtons.OK, MessageBoxIcon.Error); return false; } return !m_DisabledPagesList.Contains(tbName); }'VB Public Function IsEnabledTab(ByVal tbName As String) As Boolean If Not TabPages.ContainsKey(tbName) Then MessageBox.Show("Tab non presente", "ERRORE", MessageBoxButtons.OK, MessageBoxIcon.Error) Return False End If Return Not m_DisabledPagesList.Contains(tbName) End FunctionNel primo dei due metodi aggiunti verifico che la scheda passata sia abilitata. Innanzitutto verifico che il nome di scheda passato come parametro sia esistente, altrimenti espongo un messaggio di errore. In realtà dovrei sollevare un'eccezione, ma non avevo tempo e voglia di implementare questo pezzo di codice (però me lo sono segnato nelle cose da fare :o)).
Quindi restituisco la non-presenza di tale scheda nell'elenco di schede disabilitate.
Questo sistema è migliore dell'uso della proprietà Tag delle singole TabPage, perché mi lascia libera tale proprietà per altri usi, più inerenti al lato business, eventualmente, che al lato interfaccia del controllo. Inoltre, se me ne dimenticassi, potrei avere un domani delle "sorprese" e perdere tempo a capire perché quanto scrivo nella Tag "scompare" a mia insaputa (manco fossi un ministro!)//C# public bool EnableTab(string tbName, bool bEnabled) { if (!TabPages.ContainsKey(tbName)) { MessageBox.Show("Tab non presente", "ERRORE", MessageBoxButtons.OK, MessageBoxIcon.Error); return false; } if (bEnabled) { if (m_DisabledPagesList.Contains(tbName)) m_DisabledPagesList.Remove(tbName); } else { if (!m_DisabledPagesList.Contains(tbName)) m_DisabledPagesList.Add(tbName); } Refresh(); return true; }'VB Public Function EnableTab(ByVal tbName As String, ByVal bEnabled As Boolean) As Boolean If Not TabPages.ContainsKey(tbName) Then MessageBox.Show("Tab non presente", "ERRORE", MessageBoxButtons.OK, MessageBoxIcon.Error) Return False End If If bEnabled Then If m_DisabledPagesList.Contains(tbName) Then m_DisabledPagesList.Remove(tbName) Else If Not m_DisabledPagesList.Contains(tbName) Then m_DisabledPagesList.Add(tbName) End If Refresh() Return True End FunctionIl secondo metodo aggiunto permette all'utente (o al programma) di abilitare o disabilitare le singole schede.
Dopo il controllo sull'esistenza della scheda, controllo in base al secondo parametro l'elenco delle schede disabilitate, per vedere se devo rimuoverla o aggiungerla.
Tutto semplice, ma ho la "sensazione" di aver dimenticato qualcosa, un dettaglio... Vedremo poi!Per disegnare le schede, invece di usare l'evento sollevato dal controllo, sovrascrivo il metodo OnDrawItem della classe base che scatena l'evento:
//C# protected override void OnDrawItem(DrawItemEventArgs e) { base.OnDrawItem(e); //ora "correggo" il disegno RectangleF bnds = e.Bounds; bnds.Width += 3; bnds.X -= 1; Brush brsh = (IsEnabledTab(TabPages[e.Index].Name) ? m_NormalColor : m_DisabledColor); e.Graphics.DrawString(TabPages[e.Index].Text, base.Font, brsh , bnds, m_Fmt); }'VB Protected Overloads Overrides Sub OnDrawItem(ByVal e As DrawItemEventArgs) MyBase.OnDrawItem(e) 'ora "correggo" il disegno Dim bnds As RectangleF = e.Bounds bnds.Width += 3 bnds.X -= 1 Dim brsh As Brush = DirectCast(IIf(IsEnabledTab(TabPages(e.Index).Name), _ m_NormalColor, m_DisabledColor), _ Brush) e.Graphics.DrawString(TabPages(e.Index).Text, MyBase.Font, brsh, bnds, m_Fmt) End Sub(n.d.r.: In Visual Basic, la funzione IIF restituisce un Object generico, non un Brush. Per rispettare Option Strict On, quindi, bisogna effettuare una tipizzazione esplicita, il che richiede, per una miglior leggibilità del codice, l'uso di una variabile a parte).
Si noti che, per verificare se la scheda è disabilitata o meno, uso il metodo pubblico IsEnabledTab, perché è sempre meglio evitare di ripetere codice (con la possibilità di inserire errori a ripetizione, rendendo difficile una futura manutenzione).Infine, voglio fare in modo che la selezione di una scheda disabilitata non abbia effetto. Per far questo, sovrascrivo il metodo di evento OnSelecting, che viene scatenato proprio quando si cerca di selezionare una scheda, sia da codice che con mouse o tastiera.
//C# protected override void OnSelecting(TabControlCancelEventArgs e) { if (!IsEnabledTab(e.TabPage.Name)) { e.Cancel = true; System.Media.SystemSounds.Beep.Play(); return; } base.OnSelecting(e); }'VB Protected Overloads Overrides Sub OnSelecting(ByVal e As TabControlCancelEventArgs) If Not IsEnabledTab(e.TabPage.Name) Then e.Cancel = True System.Media.SystemSounds.Beep.Play() Return End If MyBase.OnSelecting(e) End SubIl metodo semplicemente verifica se la scheda "richiesta" è disabilitata, nel qual caso emette un beep dopo aver impostato la "cancellazione" della richiesta. Altrimenti, si lascia continuare con la normale esecuzione della classe base.
Correzione
Pensavo di aver finito, ma mi è venuta in mente una eventualità "bizzarra" (in realtà neanche tanto!)
Supponiamo che l'utente voglia disabilitare proprio la scheda correntemente selezionata. Cosa accadrebbe? Che l'aspetto della scheda cambierebbe, ma si potrebbe ancora "lavorarci", poiché rimane attiva e abilitata (ovviamente). Quindi modifico in questo modo il metodo "EnableTab"://C# public bool EnableTab(string tbName, bool bEnabled) { if (!TabPages.ContainsKey(tbName)) { MessageBox.Show("Tab non presente", "ERRORE", MessageBoxButtons.OK, MessageBoxIcon.Error); return false; } if (bEnabled) { if (m_DisabledPagesList.Contains(tbName)) m_DisabledPagesList.Remove(tbName); } else { if (!m_DisabledPagesList.Contains(tbName)) m_DisabledPagesList.Add(tbName); } // se la tabpage disabilitata è quella attiva, // cerco la tab abilitata più vicina e la abilito if (SelectedTab.Name == tbName) { bool Moved = false; // prima cerco in avanti for (int i = TabPages.IndexOfKey(tbName) + 1; i < TabPages.Count; i++) { if (IsEnabledTab(TabPages[i].Name)) { Moved = true; SelectedTab = TabPages[i]; break; } } // poi cerco indietro if (!Moved) { for (int i = TabPages.IndexOfKey(tbName) - 1; i >= 0; i--) { if (IsEnabledTab(TabPages[i].Name)) { Moved = true; SelectedTab = TabPages[i]; break; } } } if (!Moved) SelectedTab = null; } Refresh(); return true; }'VB Public Function EnableTab(ByVal tbName As String, ByVal bEnabled As Boolean) As Boolean If Not TabPages.ContainsKey(tbName) Then MessageBox.Show("Tab non presente", "ERRORE", MessageBoxButtons.OK, MessageBoxIcon.Error) Return False End If If bEnabled Then If m_DisabledPagesList.Contains(tbName) Then m_DisabledPagesList.Remove(tbName) Else If Not m_DisabledPagesList.Contains(tbName) Then m_DisabledPagesList.Add(tbName) End If ' se la tabpage disabilitata è quella attiva, cerco la tab abilitata più vicina e la abilito If SelectedTab.Name = tbName Then Dim moved As Boolean = False For i As Integer = TabPages.IndexOfKey(tbName) + 1 To TabPages.Count - 1 ' prima cerco in avanti If IsEnabledTab(TabPages(i).Name) Then moved = True SelectedTab = TabPages(i) Exit For End If Next ' poi cerco indietro If Not moved Then For i As Integer = TabPages.IndexOfKey(tbName) - 1 To 0 Step -1 If IsEnabledTab(TabPages(i).Name) Then moved = True SelectedTab = TabPages(i) Exit For End If Next End If If Not moved Then SelectedTab = Nothing End If End If Refresh() Return True End FunctionIn pratica, se è disabilitata la scheda correntemente attiva, faccio in modo che diventi attiva la prima scheda non disabilitata che trovo, scorrendo l'insieme di schede prima in avanti e poi all'indietro (eventualmente).
Se sono tutte disabilitate, non viene impostato il flag moved e quindi viene svuotata la proprietà SelectedTab.Quest'ultima istruzione provocherebbe però un errore nel metodo "OnSelecting", che quindi devo trasformare così:
//C# protected override void OnSelecting(TabControlCancelEventArgs e) { if (e.TabPage != null && !IsEnabledTab(e.TabPage.Name)) { e.Cancel = true; System.Media.SystemSounds.Beep.Play(); return; } base.OnSelecting(e); }'VB Protected Overloads Overrides Sub OnSelecting(ByVal e As TabControlCancelEventArgs) If e.TabPage IsNot Nothing AndAlso Not IsEnabledTab(e.TabPage.Name) Then e.Cancel = True System.Media.SystemSounds.Beep.Play() Return End If MyBase.OnSelecting(e) End SubNella nuova implementazione, si verifica prima che la scheda selezionata non sia nulla (come impostato dall'istruzione "finale" della routine precedente).
Conclusione
Le modifiche fatte al controllo sono semplici e limitate. Ma si potrebbe anche impostare il colore delle schede, piuttosto che permettere al TabControl di "nascondere" delle schede (oggi si deve rimuovere le schede che si vuol nascondere, per poi eventualmente reinserirle: perché non aggiungere una collezione di TabPage "nascoste" con un metodo "HideTab" per poi poterle reintrodurre tramite un metodo "ShowTab"?). E che dire della possibilità di memorizzare se l'utente vuol nascondere alcune schede perché non gli servono? Insomma, una volta iniziato, si può continuare a modificare i controlli a proprio piacimento. Senza dover intervenire sulle funzionalità già soddisfacenti. E senza dover riempire le nostre form di codice ripetitivo (tratto da snippet, per esempio :o)).
Abbiamo visto come sia davvero semplice farlo, in .Net.Il codice a corredo di questo articolo è scaricabile dall'area download del sito.
Per commenti e quant'altro di vostro interesse, potete contattarmi al mio indirizzo.Appendice
La scelta del linguaggio (C# invece che VB) che ho operato dipende dalla mia pigrizia, anzitutto.
Nel VB (di un paio d'anni fa, soprattutto) rimanevano alcune cose, alcuni automatismi, alcune scelte architetturali che "nascondevano" parte del codice, facilitando magari il programmatore, ma evitandogli di dover capire appieno e completamente le novità intrinseche del nuovo ambiente. E una volta scelto VB come linguaggio, la mia pigrizia mi avrebbe portato ad approfittare di questi automatismi, come di alcune parole chiave (o funzioni) arrivate dal vecchio VB6 ma non native. E che non era possibile rimuovere nemmeno dereferenziando VisualBasic (o come si chiamava il namespace da disabilitare). Insomma, piazza pulita e nessun aiuto. Non si impara a nuotare col salvagente. Ma è una MIA scelta che non vuol alimentare in alcun modo la rivalità inutile (e un po' buffa) tra "sciarpisti" e "VBisti".