UITypeEditory jsou editory, jež k editaci vlastnosti v PropertyGridu používají ovládací prvek. Needituje je tedy přímo PropertyGrid (po vizuální stránce).
Jsou dva typy UITypeEditorů. Oba vycházejí ze stejného základu, liší se pouze grafickým zobrazením.
DropDownEditory zobrazují editační ovládací prvek jako nabídku pod vlastností.
Modalní editory zobrazují editační ovládací prvek v dialogovém okně.
N.B.: Vlastnost může rozšiřovat TypeConverter i UITypeEditor zároveň. Ukázkou budiž vlastnost Font jakéhokoliv ovládacího prvku. Vidíme, že je možno ji expandovat a stejně tak i otevřít dialog pro výběr písma.
Teď už víme, k čemu editory slouží. Nudná teorie je za námi, teď si prakticky ukážeme, jak je využít. Budeme vycházet z komponenty Person. Stavme tedy na projektu z minulého dílu.
Zdrojové kódy
Ke komponentě přidáme vlastnost Mood (DisplayName: Aktuální nálada). Ta nabývá hodnoty vlastního datového typu Mood. Přidejte si její deklaraci níže do našeho projektu ke komponentě Person.
Private _mood As Mood
<Category("Actual"), DisplayName("Aktuální nálada"), Editor(GetType(MoodEditor), GetType(UITypeEditor))> _
Public Property Mood() As Mood
Get
Return _mood
End Get
Set(ByVal value As Mood)
_mood = value
End Set
End Property
Visual Studio nám podtrhne několik věcí. Jako první to bude MoodEditor v atributu Editor. Tento atribut udává, který UITypeEditor se má použít k editaci komponenty. Tím je právě MoodEditor. Popíšeme si jej dále. Druhá podrtžená věc bude Mood, jakožto datový typ vlastnosti Mood. Vytvoříme si do projektu novou třídu jménem Mood (Pravý klik na jméno projektu v Solution Exploreru > Add > New Item > Class > Mood.vb). Vytvořenou kostru třídy upravíme do této podoby:
Public Class Mood
Private _mood As String = String.Empty
Private _hint As String = String.Empty
Public Sub New(ByVal mood As String, ByVal hint As String)
_mood = mood
_hint = hint
End Sub
<RefreshProperties(RefreshProperties.Repaint)> _
Public Property Mood() As String
Get
Return _mood
End Get
Set(ByVal value As String)
_mood = value
End Set
End Property
<RefreshProperties(RefreshProperties.Repaint)> _
Public Property Hint() As String
Get
Return _hint
End Get
Set(ByVal value As String)
_hint = value
End Set
End Property
End Class
Nyní máme vytvořenou třídu Mood. Zakomentujte nebo smažte podtržené atributy a spusťte projekt. V PropertyGridu nelze vlastnost editovat. S tím jsme se setkali i u vlastnosti Song v minulém díle. Vytvoříme si tedy pro Mood TypeConverter. (Umístíme do stejného souboru)
Nezapomeneme k třídě Mood přidat atribut TypeConverter.
<TypeConverter(GetType(MoodClassConverter))> _
Nyní už samotný konverter:
Public Class MoodClassConverter
Inherits ExpandableObjectConverter
Public Overrides Function CanConvertFrom(ByVal context As System.ComponentModel.ITypeDescriptorContext, ByVal sourceType As System.Type) As Boolean
Return sourceType Is GetType(String) OrElse MyBase.CanConvertFrom(context, sourceType)
End Function
Public Overrides Function CanConvertTo(ByVal context As System.ComponentModel.ITypeDescriptorContext, ByVal destinationType As System.Type) As Boolean
Return destinationType Is GetType(String) OrElse MyBase.CanConvertTo(context, destinationType)
End Function
Než budeme overridovat další metody, zastavíme se. Funkce ConvertFrom zajišťuje převod z řetězce na objekt, zde na třídu Mood. Proč se nad tím ale pozastavuji? Třída Mood má dvě vlastnosti: Mood a Hint. Druhá vlastnost je prostá - libovolný text rozšiřující a popisující aktuální náladu. První vlastnost je nálada samotá. Ta nemůže to být libovolný řetězec. Musí se jednat o jeden z několika předem daných. Ve funkci ConvertFrom musí tedy proběhnout kontrola, zda nálada skutečně odpovídá nějakému ze sady daných řetězců. K těm se bude přistupovat poměrně z velkého počtu dalších míst v kódu. Umístíme je tedy do samotné třídy Mood. Přidáme do ní:
Shared ReadOnly Property Moods() As StandardValuesCollection
Get
Return New StandardValuesCollection(New String() {"Euphoria", "Happy", "Glad", "Normal", "Sad", "Confused", "Depression", "Angry"})
End Get
End Property
Shared určuje, že se k vlastnosti dá přistupovat bez vytvoření instance třídy. Občas totiž budeme potřebovat zjistit jen zmíněnou kolekci a žádnou další funkcionalitu třídy. Možná se podivíte nad typem kolekce - StandardValuesCollection. Ten takový musí být. Dále si řekneme, proč.
Public Overrides Function ConvertFrom(ByVal context As System.ComponentModel.ITypeDescriptorContext, ByVal culture As System.Globalization.CultureInfo, ByVal value As Object) As Object
If TypeOf (value) Is String Then
Dim str As String = DirectCast(value, String)
Dim rex As String = "^(?[\w]+)(:(?.+))?$" ' Masce vyhoví výrazy "Nálada:Popis" i "Nálada"
With Regex.Match(str, rex)
If .Success Then
' Pokud převáděný text vyhoví, zpracujeme jej.
' Mood.Moods je přístup ke kolekci možných nálad bez vytvoření instance třídy Mood - díky Shared.
Dim matches = (From m As String In Mood.Moods Where m.ToLower() = .Groups("mood").Value.ToLower() Select m)
' Příkaz v závorkách je LINQ příkaz. Říká, že se má projít kolekce Mood.Moods a položku, která odpovídá náladě zadané uživatelem, připojit k výsledku.
' Výsledek je kolekce matches. Její počet položek bude 0, pokud je zadaná nesprávná hodnota, nebo 1, pak se funkce ukončí vrácením nové třídy Mood.
If matches.Count > 0 Then Return New Mood(matches(0), .Groups("hint").Value)
ElseIf String.IsNullOrEmpty(str) Then
' Pokud se jedná o prázdný řetězec, vrátíme Nothing - vlastnost nemá žádnou hodnotu.
Return Nothing
End If
End With
' Pokud řetězec není prázdný, ale nevyhověl, není ve správném formátu.
Throw New ArgumentException("Entered values must match mask 'Mood:Hint' and Mood must be one of allowed values (see Mood.Moods).")
Else
Return MyBase.ConvertFrom(context, culture, value)
End If
End Function
Public Overrides Function ConvertTo(ByVal context As System.ComponentModel.ITypeDescriptorContext, ByVal culture As System.Globalization.CultureInfo, ByVal value As Object, ByVal destinationType As System.Type) As Object
If destinationType Is GetType(String) Then
Dim mood As Mood = DirectCast(value, Mood)
If mood IsNot Nothing Then
' Vrátí formát "Nálada:Popis", pokud popis není prázdný, a pokud je, vrátí "Nálada".
Return String.Format("{0}{1}{2}", mood.Mood.ToString(), IIf(String.IsNullOrEmpty(mood.Hint), String.Empty, ":"), mood.Hint)
Else
Return String.Empty
End If
Else
Return MyBase.ConvertTo(context, culture, value, destinationType)
End If
End Function
End Class
Nyní jsme dosáhli toho, co bychom měli umět již z minulého dílu. Vlastnost Mood je možné rozkliknout. Zkusíme tak, že spustíme a do Mood napíšeme například "Normal". Tím se vlastnost naplní nově vytvořenou třídou Mood s vlastností Mood="Normal" a Hint prázdnou. TypeConverted funguje. Pokud máte položku stále zašedlou, ujistěte se, že jste opravdu přidali třídě Mood atribut TypeConverter.
Ještě než se pustíme do psaní UITypeEditoru pro třídu Mood, ukážeme si, jak zajistit, aby bylo v PropertyGridu možné zadávat do vlastnosti Mood u třídy Mood jen řetězce, které jsou povolené. Použijeme TypeConverter. Konkrétně StringConverter.
K vlastnost Mood u třídy Mood přidáme atribut TypeConverter.
<RefreshProperties(RefreshProperties.Repaint), TypeConverter(GetType(MoodPropertyConverter))> _
Teď se pustíme do psaní konverteru. Bude to poněkud netradiční konverter. Bude overridovat jiné metody, než na které jsme zvyklí.
Public Class MoodPropertyConverter
Inherits StringConverter
Public Overrides Function GetStandardValuesSupported(ByVal context As System.ComponentModel.ITypeDescriptorContext) As Boolean
Return True ' Říká, že máme vlastní kolekci hodnot, které chceme používat
End Function
Public Overrides Function GetStandardValuesExclusive(ByVal context As System.ComponentModel.ITypeDescriptorContext) As Boolean
Return True ' A zakazuje, aby uživatel mohl do pole psát. Musí si jednoduše jednu z možností vybrat.
End Function
Public Overrides Function GetStandardValues(ByVal context As System.ComponentModel.ITypeDescriptorContext) As System.ComponentModel.TypeConverter.StandardValuesCollection
Return Mood.Moods ' Vrací kolekci řetězců. Správně by měla být deklarována zde, v konverteru (proto ten krkolomný datový typ), ale pro přehlednost a praktičnost jsme jí umístil do třídy Mood.
End Function
End Class
Nyní se dostáváme k hlavnímu tématu našeho článku. K UITypeEditorům. Konkrétně k DropDownEditoru, který si vytvoříme k editaci datového typu Mood.
Jako první musíme mít nějakou komponentu, která se bude zobrazovat. Přidejte do projektu UserControl jménem MoodView. Vypadat by měl asi takto:
Doporučuji přepsat Designer Vaší komponenty, abychom pracovali všichni na stejném základě.
MoodView.Designer.vb: (pokud tento soubor ve své Solution hledáte marně, klikněte na ikonu s popisem 'Show All Files' v Solution Exploreru).
<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()> _
Partial Class MoodView
Inherits System.Windows.Forms.UserControl
'UserControl overrides dispose to clean up the component list.
<System.Diagnostics.DebuggerNonUserCode()> _
Protected Overrides Sub Dispose(ByVal disposing As Boolean)
Try
If disposing AndAlso components IsNot Nothing Then
components.Dispose()
End If
Finally
MyBase.Dispose(disposing)
End Try
End Sub
'Required by the Windows Form Designer
Private components As System.ComponentModel.IContainer
'NOTE: The following procedure is required by the Windows Form Designer
'It can be modified using the Windows Form Designer.
'Do not modify it using the code editor.
_
Private Sub InitializeComponent()
Me.components = New System.ComponentModel.Container
Dim resources As System.ComponentModel.ComponentResourceManager = New System.ComponentModel.ComponentResourceManager(GetType(MoodView))
Me.TrackBar1 = New System.Windows.Forms.TrackBar
Me.PictureBox1 = New System.Windows.Forms.PictureBox
Me.Label1 = New System.Windows.Forms.Label
Me.TextBox1 = New System.Windows.Forms.TextBox
Me.ImageList1 = New System.Windows.Forms.ImageList(Me.components)
CType(Me.TrackBar1, System.ComponentModel.ISupportInitialize).BeginInit()
CType(Me.PictureBox1, System.ComponentModel.ISupportInitialize).BeginInit()
Me.SuspendLayout()
'
'TrackBar1
'
Me.TrackBar1.Anchor = CType(((System.Windows.Forms.AnchorStyles.Top Or System.Windows.Forms.AnchorStyles.Left) _
Or System.Windows.Forms.AnchorStyles.Right), System.Windows.Forms.AnchorStyles)
Me.TrackBar1.AutoSize = False
Me.TrackBar1.Location = New System.Drawing.Point(3, 3)
Me.TrackBar1.Maximum = 7
Me.TrackBar1.Name = "TrackBar1"
Me.TrackBar1.Size = New System.Drawing.Size(144, 30)
Me.TrackBar1.TabIndex = 0
'
'PictureBox1
'
Me.PictureBox1.Location = New System.Drawing.Point(3, 39)
Me.PictureBox1.Name = "PictureBox1"
Me.PictureBox1.Size = New System.Drawing.Size(51, 27)
Me.PictureBox1.TabIndex = 1
Me.PictureBox1.TabStop = False
'
'Label1
'
Me.Label1.Anchor = CType(((System.Windows.Forms.AnchorStyles.Top Or System.Windows.Forms.AnchorStyles.Left) _
Or System.Windows.Forms.AnchorStyles.Right), System.Windows.Forms.AnchorStyles)
Me.Label1.Font = New System.Drawing.Font("Microsoft Sans Serif", 8.25!, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, CType(238, Byte))
Me.Label1.Location = New System.Drawing.Point(60, 39)
Me.Label1.Name = "Label1"
Me.Label1.Size = New System.Drawing.Size(87, 27)
Me.Label1.TabIndex = 2
Me.Label1.Text = "Normal"
Me.Label1.TextAlign = System.Drawing.ContentAlignment.MiddleLeft
'
'TextBox1
'
Me.TextBox1.Anchor = CType((((System.Windows.Forms.AnchorStyles.Top Or System.Windows.Forms.AnchorStyles.Bottom) _
Or System.Windows.Forms.AnchorStyles.Left) _
Or System.Windows.Forms.AnchorStyles.Right), System.Windows.Forms.AnchorStyles)
Me.TextBox1.Location = New System.Drawing.Point(3, 69)
Me.TextBox1.Multiline = True
Me.TextBox1.Name = "TextBox1"
Me.TextBox1.Size = New System.Drawing.Size(144, 40)
Me.TextBox1.TabIndex = 3
'
'ImageList1
'
'
'MoodView
'
Me.AutoScaleDimensions = New System.Drawing.SizeF(6.0!, 13.0!)
Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font
Me.Controls.Add(Me.TextBox1)
Me.Controls.Add(Me.Label1)
Me.Controls.Add(Me.PictureBox1)
Me.Controls.Add(Me.TrackBar1)
Me.Name = "MoodView"
Me.Size = New System.Drawing.Size(150, 112)
CType(Me.TrackBar1, System.ComponentModel.ISupportInitialize).EndInit()
CType(Me.PictureBox1, System.ComponentModel.ISupportInitialize).EndInit()
Me.ResumeLayout(False)
Me.PerformLayout()
End Sub
Friend WithEvents TrackBar1 As System.Windows.Forms.TrackBar
Friend WithEvents PictureBox1 As System.Windows.Forms.PictureBox
Friend WithEvents Label1 As System.Windows.Forms.Label
Friend WithEvents TextBox1 As System.Windows.Forms.TextBox
Friend WithEvents ImageList1 As System.Windows.Forms.ImageList
End Class
Otevřete Designer komponenty a ve vlastnostech komponenty ImageList1 zvolte vlastnost Images. Nahrajte následující obrázky (ve správném pořadí!):
Obrazové podklady
Dále by bylo vhodné nastavit u ImageList1 vlastnosti ColorDepth na 32 bitů a ImageSize na [51; 27]. Teď je naše komponenta graficky připravena. Přeneseme se do kódu a zajistíme funkcionalitu.
Public Class MoodView
' Do konstruktoru předáme editovaný objekt.
Public Sub New(ByVal mood As Mood)
InitializeComponent() ' Připravíme všechny komponenty.
If mood IsNot Nothing Then ' Pokud editovaný objekt není prázdný, přizpůsobíme komponentu podle jeho vlastností.
TrackBar1.Value = mood.Index ' Ohlásí chybu, jak ji napravit si ukážeme dále.
TextBox1.Text = mood.Hint
TrackBar1_Scroll()
Else ' V opačném případě nastavíme komponentu do výchozího stavu.
TrackBar1.Value = 3
TrackBar1_Scroll()
End If
End Sub
' Přes tuto vlastnost se pak budeme dotazovat na změněný objekt.
Public ReadOnly Property Mood() As Mood
Get
Return New Mood(Mood.Moods(Me.TrackBar1.Value), Me.TextBox1.Text)
End Get
End Property
' Tato vlastnost vrátí obrázek náležící k dané vlastnosti.
Shared ReadOnly Property Icon(ByVal index As Integer) As Image
Get
Return New MoodView(Nothing).ImageList1.Images(index)
End Get
End Property
Private Sub TrackBar1_Scroll() Handles TrackBar1.Scroll
PictureBox1.Image = ImageList1.Images(Me.TrackBar1.Value)
Label1.Text = Mood.Moods(Me.TrackBar1.Value)
End Sub
End Class
Komponenta je hotová, ale ještě jí nemůžeme vyzkoušet. Musíme nejdřív vytvořit Editor, který ji bude hostovat.
Vrátíme se do souboru Mood.vb a přidáme novou třídu:
Public Class MoodEditor
Inherits UITypeEditor
Public Overrides Function GetEditStyle(ByVal context As System.ComponentModel.ITypeDescriptorContext) As System.Drawing.Design.UITypeEditorEditStyle
Return UITypeEditorEditStyle.DropDown ' Určuje, že se má zobrazit nabídka, nikoliv dialog.
End Function
Public Overrides Function EditValue(ByVal context As System.ComponentModel.ITypeDescriptorContext, ByVal provider As System.IServiceProvider, ByVal value As Object) As Object
Using editor As MoodView = New MoodView(DirectCast(value, Mood)) ' Vytvoříme instanci ovládacího prvku MoodView a předáme jí editovaný objekt.
' Vytvoříme providera, který ovládací prvek zobrazí a bude čekat, dokud se nad ovládacím prvkem nestiskneme Enter.
DirectCast(provider.GetService(GetType(IWindowsFormsEditorService)), IWindowsFormsEditorService).DropDownControl(editor)
Return editor.Mood ' Pomocí vlastnosti Mood získáme zpět pozměnený objekt a o zbytek (dosazení do vlastnosti) se postará UITypeEditor.
End Using
End Function
Public Overrides Function GetPaintValueSupported(ByVal context As System.ComponentModel.ITypeDescriptorContext) As Boolean
Return True ' Tato vlastnost určuje, že se má u vlastnosti vykreslit malý obdélníček s grafickou reprezentací její hodnoty (jako například u ForeColor.)
End Function
Opět se zastavím uprostřed kódu. Následující metoda se stará o vykreslení obdélníčku. My do ní vykreslím emotikon, který vykreslujeme do PictureBoxu v MoodView. K obrázku se dostaneme přes vlastnost Icon u MoodView. Zjištění indexu zajistíme novou vlastností do třídy Mood.
<Browsable(False)> _
Public ReadOnly Property Index() As Integer
Get
For i As Integer = 0 To Moods.Count - 1
If Moods(i) = _mood Then
Return i
End If
Next
End Get
End Property
Upravili jsme třídu Mood a pokračujeme v psaní editoru:
Public Overrides Sub PaintValue(ByVal e As System.Drawing.Design.PaintValueEventArgs)
If e.Value Is Nothing Then ' Pokud je hodnota vlastnosti nulová, vykreslíme červený kříž
e.Graphics.DrawLine(Pens.Red, e.Bounds.Left, e.Bounds.Top, e.Bounds.Right - 1, e.Bounds.Bottom - 1)
e.Graphics.DrawLine(Pens.Red, e.Bounds.Right - 1, e.Bounds.Top, e.Bounds.Left, e.Bounds.Bottom - 1)
Else
' Pokud ne, přes vlastnost Icon ovládacího prvku MoodView získáme obrázek na indexu, který odpovídá indexu nálady v kolekci.
e.Graphics.DrawImage(MoodView.Icon(DirectCast(e.Value, Mood).Index), e.Bounds)
End If
End Sub
Public Overrides ReadOnly Property IsDropDownResizable() As Boolean
Get
Return False ' Nechceme, aby bylo možné ovládací prvek dimenzovat.
End Get
End Property
End Class
UITypeEditor je na světě. Spusťte aplikaci. Jak vidíte, nic se nezměnilo. Žádný DropDownEditor se neobjevil. Pokud jste četli pozorně, vzpomenete si, co jsme na začátku zakomentovali, aby nám to necházelo chyby. Ano, správně, některé atributy vlastnosti Mood třídy Person. Jak tedy vypadá kompletní sada jejich atributů?
<Category("Actual"), DisplayName("Aktuální nálada"), Editor(GetType(MoodEditor), GetType(UITypeEditor))> _
Public Property Mood() As Mood
...
Teď by mělo vše pracovat tak, jak má. Pokud tomu tak není, neváhejte se na mne obrátit v diskusi.
Zdrojové kódy
Závěrem
Pokud byste se chtěli dozvědět, jak zobrazit místo DropDownEditoru modální dialog, vězte, že je to velmi jednoduché. Stačí ve třídě MoodEditor, metodě GetEditStyle nastavit Modal a ve funkci EditValue volat na provideru metodu ShowDialog. Samozřejmě místo ovládacího prvku si musíme vytvořit formulář.
To je pro dnešní díl vše. Příště si povíme něco o tom, jak vylepšit práci s ovládacími prvky přímo v Designeru.