Un controllo con interfaccia simile al ComboBox - 2
a cura di Stefano Castelli (requisiti: nessuno)Premessa
In questo articolo costruiremo un controllo che erediti da quello appositamente sviluppato nella prima parte e che rimpiazzi il DateTimePicker, con in più diverse caratteristiche, in particolare quella di accettare date nulle.Prima analisi
In un drammatico sforzo di fantasia ho deciso di chiamare tale controllo DatePick.Partial Class DatePick Inherits ComboUIUna volta ereditato da ComboUI, potrei creare un oggetto MonthCalendar all'interno di DatePick con l'idea di mostrarlo/nasconderlo quando l'utente clicca sul bottone di ComboUI, ma purtroppo non funzionerebbe.
Inanzitutto, per poter visualizzare il calendario dovremmo aumentare le proprietà Height e Width del controllo in modo da includere il calendario. Ma questo non sarebbe di aiuto perché anche i controlli interni _TBox e _Btn sarebbero ridimensionati e il calendario rimarrebbe comunque non visibile.
Ma molto più grave sarebbe il fatto che il controllo verrebbe in parte nascosto, qualora le sue dimensioni fossero tali da andare oltre quelle del contenitore (sia esso un form, un Groupbox ecc.). Per esempio, nella figura seguente, un oggetto ListBox è all'interno di un contenitore GroupBox. Le dimensioni della ListBox sono tali che parte di essa rimane nascosta dal contenitore.
![]()
Questo è un comportamento che voglio evitare. Come? Non sto a farla lunga e vado subito alla soluzione. Useremo un Form, che creeremo all'interno di DatePick e sul quale piazzeremo un controllo MonthCalendar. Naturalmente tale form non avrà bordi e barra del titolo. Ma anche dopo avergli assegnato (o tolto) queste caratteristiche dovremo stare attenti perché il form sarà indipendente dal controllo. Per esempio, se viene spostato il form dove si trova il controllo, il form che alloggia il MonthCalendar non si muove. Per cui dovremo fare in modo che non appena il form che contiene il calendario viene disattivato, esso venga anche chiuso.
Inizio sviluppo
Allora, iniziamo con il dichiarare un oggetto di tipo Form ed un oggetto di tipo MonthCalendar nel nostro componente.Private WithEvents _Form As Form Private WithEvents _Cal As MonthCalendar Dim _Visible As BooleanLa variabile _Visible naturalmente ci dirà se il form è visibile o no. La preparazione e visualizzazione del form è delegata alla procedura dtButtonClick, richiamata quando l'utente clicca sul pulsante dell'oggetto base ComboUI. Quando questo avviene, viene generato un evento ButtonClick, e la suddetta procedura viene quindi eseguita:
Private Sub BtnClick(ByVal e As System.EventArgs) Handles Me.ButtonClick dtButtonClick() End SubIn questa procedura, il form viene preparato, posizionato e visualizzato.
Private Sub dtButtonClick() Dim Point As Point If _Visible = False Then _Form = New Form _Cal = New MonthCalendar With _Form .FormBorderStyle = FormBorderStyle.None .ShowInTaskbar = False .KeyPreview = True ' to catch the ESC key .StartPosition = FormStartPosition.Manual .ShowIcon = False .TopMost = True 'Trova le coordinate dell'angolo basso a sinistra 'rispetto al sistema di coordinate dello schermo Point = Me.PointToScreen(New Point(0, Me.Height)) 'posiziona _Form nella posizione dello schermo 'equivalente al punto sopra menzionato .Top = Point.Y .Left = Point.X .Height = _Cal.Height .Width = _Cal.Width .Controls.Add(_Cal) If _OpenCalOnSetDate Then If IsDate(Me.Text) AndAlso CDate(Me.Text) <> NULL_DATE Then _Cal.SetDate(CDate(Me.Text)) End If End If .Show() End With End If End SubVi sono poi le procedure che gestiscono la visibilità/invisibilità del form. La più importante è FrmDeactivate, eseguita quando il form viene deattivato, dove esso viene rimosso.
Private Sub BtnGotFocus(ByVal e As System.EventArgs) Handles Me.ButtonGotFocus Try If _Form.Visible Then _Visible = True Else _Visible = False End If Catch ex As Exception _Visible = False End Try End Sub Private Sub BtnMouseUp(ByVal e As System.Windows.Forms.MouseEventArgs) Handles Me.ButtonMouseUp _Visible = Not _Visible End Sub Private Sub FrmKeyPress(ByVal sender As Object, _ ByVal e As System.Windows.Forms.KeyPressEventArgs) _ Handles _Form.KeyPress If e.KeyChar = Chr(Keys.Escape) Then _Form.Dispose() Application.DoEvents() _Visible = False End If End Sub Private Sub FrmDeactivate(ByVal sender As Object, _ ByVal e As System.EventArgs) _ Handles _Form.Deactivate _Visible = False _Form.Dispose() Application.DoEvents() End SubProprietà Specifiche
Per rendere il controllo più flessibile e facilitarne l'uso, si aggiungono alcune proprietà che determineranno alcuni aspetti del comportamento del controllo:
- Text (String)
Questa non è una nuova proprietà ma la voglio menzionare perché è la proprietà che ci permette di impostare una data nel controllo.- Value (Date)
Restituisce il valore della data del controllo in formato Date.- TransferOnDblClick (Boolean)
Se impostata a True, la data viene trasferita dal calendario al controllo quando l'utente fa doppio click sulla data desiderata. Altrimenti il trasferimento avviene quando l'utente fa un singolo click.- CloseCalendarAfterTransfer (Boolean)
Se impostata a True, il calendario viene chiuso dopo che la data è stata trasferita, altrimenti rimane aperto fino a che l'utente non clicca sul button di ComboUI- OpenCalOnSetDate (Boolean)
Se impostata a True, il calendario viene aperto alla data contenuta nella proprietà Text. Questa operazione avviene quando il form viene visualizzato, nella procedura dtButtonClick, nella quale viene quindi aggiunto il seguente codice:With _Form '... If _OpenCalOnSetDate Then If IsDate(Me.Text) AndAlso CDate(Me.Text) <> NULL_DATE Then _Cal.SetDate(CDate(Me.Text)) End If End If .Show() End WithNULL_DATE è una costante che contiene il valore da considerare nullo per le date, cioè il valore minimo (DateTime.MinValue):
Private Const NULL_DATE As DateTime = #12:00:00 AM# 'DateTime.MinValueTrasferimento della data selezionata.
Quando l'utente seleziona una data dal MonthCalendar, questa viene trasferita nella proprietà Text di ComboUI. Per determinare quando la data viene selezionata, esiste il comodo evento MonthCalendar.DateSelected, che viene generato ogni volta che l'utente clicca su un numero che rappresenta uno dei giorni.![]()
E questo andrebbe benissimo se volessimo che la data venga trasferita con un singolo click del mouse. Ma se vogliamo che il trasferimento avvenga quando l'utente fa doppio click dobbiamo usare una tecnica un po' più raffinata, perché sfortunatamente l'oggetto MonthCalendar è sprovvisto dell'evento MouseDoubleClick.
Dobbiamo quindi emulare l'evento MouseDoubleClick, e dobbiamo farlo quando l'utente clicca su uno dei numeri.Iniziamo allora a vedere come determinare in quale zona l'utente ha cliccato.
L'oggetto MonthCalendar ha un metodo chiamato HitTest che (cito MSDN) "restituisce una classe MonthCalendar.HitTestInfo contenente informazioni sulla parte di un controllo di calendario mensile che si trova nella coordinata xy specificata."
La classe HitTestInfo a sua volta restituisce la proprietà HitArea (di tipo enumerativo HitArea) che contiene informazioni su quale parte del calendario è stata cliccata (di nuovo, cito MSDN su HitArea):
Nome membro Descrizione CalendarBackground Il punto specificato fa parte dello sfondo del calendario. Date Il punto specificato si trova su una data all'interno del calendario. La proprietà Time dell'oggetto MonthCalendar.HitTestInfo è impostata sulla data in corrispondenza del punto specificato. DayOfWeek Il punto specificato si trova sull'abbreviazione di un giorno, ad esempio "Fri". La proprietà Time dell'oggetto MonthCalendar.HitTestInfo è impostata su 1° gennaio 0001. NextMonthButton Il punto specificato si trova sul pulsante presente nell'angolo superiore destro del controllo. Se l'utente fa clic su questo punto, viene eseguito lo scorrimento del calendario mensile, in modo da visualizzare il mese o l'insieme di mesi successivo. NextMonthDate Il punto specificato si trova su una data del mese successivo, visualizzato parzialmente all'inizio del mese corrente. Se l'utente fa clic su questo punto, viene eseguito lo scorrimento del calendario mensile, in modo da visualizzare il mese o l'insieme di mesi successivo. Nowhere Il punto specificato non si trova sul controllo calendario mensile o si trova su una parte inattiva del controllo. PrevMonthButton Il punto specificato si trova sul pulsante presente nell'angolo superiore sinistro del controllo. Se l'utente fa clic su questo punto, viene eseguito lo scorrimento del calendario mensile, in modo da visualizzare il mese o l'insieme di mesi precedente. PrevMonthDate Il punto specificato si trova su una data del mese precedente, visualizzato parzialmente all'inizio del mese corrente. Se l'utente fa clic su questo punto, viene eseguito lo scorrimento del calendario mensile, in modo da visualizzare il mese o l'insieme di mesi precedente. TitleBackground Il punto specificato si trova sullo sfondo del titolo di un mese. TitleMonth Il punto specificato si trova nella barra del titolo di un mese, sul nome di un mese. TitleYear Il punto specificato si trova nella barra del titolo di un mese, sul valore dell'anno. TodayLink Il punto specificato si trova sul collegamento alla data odierna, nella parte inferiore del controllo calendario mensile. WeekNumbers Il punto specificato si trova su un numero di settimana. Questa situazione si verifica solo se la proprietà ShowWeekNumbers dell'oggetto MonthCalendar è attivata. La proprietà Time dell'oggetto MonthCalendar.HitTestInfo è impostata sulla data corrispondente nella prima colonna a sinistra. Creeremo allora una variabile globale di tipo HitArea il cui valore verrà assegnato nell'evento Mousedown del calendario.
Private _HitArea As MonthCalendar.HitArea Private Sub CalMouseDown(ByVal sender As Object, _ ByVal e As System.Windows.Forms.MouseEventArgs) _ Handles _Cal.MouseDown Dim hti As MonthCalendar.HitTestInfo = _Cal.HitTest(e.X, e.Y) _HitArea = hti.HitArea End SubUseremo poi la variabile _HitArea nell'evento MouseUp, perché è lì che controlleremo se si è verificato un DoubleClick.
L'idea per questa verifica è semplice: si intercetta l'evento MouseUp e se si riscontra che esso accade entro un certo intervallo da un precedente evento MouseUp significa che l'utente ha fatto DoubleClick.Per determinare l'intervallo di tempo, useremo la proprietà Tick dell'oggetto Now. Tick restituisce il numero di intervalli della durata di 10 microsecondi trascorsi dal 1 Gennaio 0001. Ci sono 10000 Tick in un millisecondo, quindi si tratta di una misura di tempo molto precisa.
Il numero massimo di millisecondi che possono trascorrere tra due click per poter essere considerati come un doubleclick è ritrovabile nella proprietà DoubleClickTime dell'oggetto SystemInformation.
Basterà allora controllare che questo numero moltiplicato per 10000 sia maggiore del numero di Tick trascorsi tra due click consecutivi. Detto ciò, il seguente codice dovrebbe essere facilmente comprensibile:Private Sub CalMouseUp(ByVal sender As Object, _ ByVal e As System.Windows.Forms.MouseEventArgs) _ Handles _Cal.MouseUp If _HitArea = MonthCalendar.HitArea.Date Then If e.Button = MouseButtons.Left Then If Not _LastClickRaisedDoubleClick _ AndAlso Now.Ticks - _LastClickTime <= SystemInformation.DoubleClickTime * 10000 Then If _TransferOnDblClick Then TransferData() End If _LastClickRaisedDoubleClick = True Else _LastClickRaisedDoubleClick = False End If _LastClickTime = Now.Ticks End If If Not _TransferOnDblClick Then TransferData() End If End If End SubUn'ultima nota sulla Sub TransferData. La data selezionata viene visualizzata usando il formato "dd-MMM-yyyy". La ragione per questa scelta è dovuta al fatto che devo fare contente persone abituate ad avere date in formato mm/dd/yyyy e persone abituate ad avere date in formato dd/mm/yyyy. Con il formato "dd-MMM-yyyy" sembra che siano tutte contente.
Private Sub TransferData() Try Me.Text = _Cal.SelectionEnd.ToString("dd-MMM-yyyy") Catch ex As Exception End Try If _CloseCalendarAfterTransfer Then _Form.Dispose() Application.DoEvents() _Visible = Not _Visible End If End SubVerifica della data nulla
Nel form che uso per testare il controllo, intercetto l'evento TextChanged per verificare se la data restituita dal DatePick è nulla:Private Sub DatePick1_TextChanged(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles DatePick1.TextChanged If DatePick1.Value = DateTime.MinValue Then TextBox1.Text = "data nulla" Else TextBox1.Text = "data cambiata" End If End SubConclusione
Adesso ho un DateTimePicker in cui l'utente può svuotare la casella e ottenere quindi una data nulla.
Il codice sorgente allegato a questo articolo è scaricabile dall'area download.
Per chiarimenti, critiche, suggerimenti, scrivete all'autore.