El Control PropertyGridEntrega 2 de 230 de Abril 2003
|
.
|
Representación gráfica de propiedades
La entrega anterior de esta serie de dos artículos la terminamos desplegando y agrupando en varias filas las propiedades de un objeto personalizado, a la vez que la cabecera de grupo mostraba una representación textual del valor de la propiedad desplegada. Sólo faltaba un pequeño detalle para alcanzar la perfección: así como la propiedad Color muestra un rectangulito pintado del color que el valor de la propiedad representa, también nuestro ColorOjosEx quería uno y se nos quedó muy triste sin él. Vamos a dárselo.
Todo lo que tenemos que hacer es añadir a la clase editora EditorColorOjosEx los dos procedimientos siguientes:
Public Overloads Overrides Function GetPaintValueSupported(ByVal context As ITypeDescriptorContext) As Boolean Return True' True si utilizamos el rectangulito, False si no End Function Public Overloads Overrides Sub PaintValue(ByVal e As PaintValueEventArgs) e.Graphics.FillRectangle(New SolidBrush(CType(e.Context.Instance, MiAmigo).ColorOjosEx.Color), e.Bounds) End SubGetPaintValueSupported determina si utilizamos la pequeña área gráfica o no. PaintValue es quien pinta. Su argumento e contiene toda la información necesaria:
- e.Value contiene el valor de la propiedad
- e.Context.Instance contiene el objeto que muestra el PropertyGrid
- e.Graphics representa la superficie del rectangulito sobre la que se puede pintar
- e.Bounds representa las medidas del rectangulito.
Así pues, todo consiste en obtener el objeto Graphics (e.Graphics) sobre el que pintaremos, obtener el color leyendo la propiedad ColorOjosEx.Color del objeto actual MiAmigo que nos proporciona e.context.Instance, (CType(e.Context.Instance, MiAmigo).ColorOjosEx.Color) y derramarlo hasta los límites marcados por e.Bounds. Aquí tenemos el resultado:
El rectangulito pintado Igual que pintamos el rectangulito podemos mostrar imágenes en él. Es más, podemos desplegar un combo cuyos ítems incluyan una imagen. Todo lo que tenemos que hacer es, igual que con cualquier propiedad que se edite vía combo, escribir una clase heredera de StringConverter que genere la lista de valores, y, para mostrar las imágenes, escribir un editor derivado de UITypeEditor que incluya la función GetPaintValueSupported = True y el método PaintValue. Es en este método donde asignaremos a cada item del Combo obtenido de e.Value una imagen.
Vamos a clasificar nuestros grados de amistad administrados por la propiedad "Escala" en tres grupos, y a cada grupo le asignaremos una imagen. La única variación que introduciremos en nuestra clase "MiAmigo" será el tipo de la propiedad "Escala": no será una enumeración, sino un tipo "básico" como "String" para poder crear un Combo mediante una clase heredera de Stringconverter:
Public Class EditorGradoAmistad : Inherits UITypeEditor Public Overloads Overrides Function GetPaintValueSupported(ByVal context As ITypeDescriptorContext) As Boolean Return True End Function Public Overloads Overrides Sub PaintValue(ByVal e As PaintValueEventArgs) Dim bms As String Select Case e.Value 'Asignamos imágenes a grupos de valores Case Amistades(0), Amistades(1), Amistades(2) bms = "best2" Case Amistades(3), Amistades(4), Amistades(5) bms = "ok" Case Amistades(6), Amistades(7) bms = "bad2" Case Else : bms = "ok" End Select Dim bmp As Bitmap = New Bitmap(Application.StartupPath & "\" & bms & ".bmp") e.Graphics.DrawImage(bmp, e.Bounds) 'Dibujamos la imagen bmp.Dispose() End Sub End Class Public Class ComboAmistades : Inherits StringConverter Public Sub New() MyBase.New() End Sub Public Overloads Overrides Function GetStandardValuesSupported(ByVal context As ITypeDescriptorContext) As Boolean 'True = Se despliega la lista 'False = No se despliega la lista y el ususario debe escribir un valor Return True End Function Public Overloads Overrides Function GetStandardValues(ByVal context As ITypeDescriptorContext) As StandardValuesCollection Return New StandardValuesCollection(Amistades) End Function Public Overloads Overrides Function GetStandardValuesExclusive(ByVal context As ITypeDescriptorContext) As Boolean 'True = el combo no admite más items que los de la lista 'False = el combo admite un item que no esté en la lista Return True End Function End Class Public Module AmistadesArray Public Amistades As Array = New String() _ {"Bueno", "Gran Amigo", "Para Siempre", "Amigote", "Cercano", "Conocido", "Interesado", "Enemigo"} End Module <Category("Amistad"), Description("Nivel de amistad"), _ Editor(GetType(EditorGradoAmistad), GetType(UITypeEditor)), _ TypeConverter(GetType(ComboAmistades))> _ Public Property Escala() As String Get Return _Escala End Get Set(ByVal Value As String) _Escala = Value End Set End Property
Dibujitos en los items del combo
Edición de colecciones
Si el lector ha leído la primera entrega y lo que llevamos de ésta, le alegrará saber que implementar la edición de una colección no requiere código que no hayamos aprendido ya a elaborar.
El editor de colecciones de VSNET se compone de un ListBox y un PropertyGrid. El ListBox almacena los elementos del array (empleo los términos "colección" y "array" indistintamente, en realidad me refiero a cualquier tipo que implemente la intefaz IEnumerable) y el PropertyGrid muestra y permite editar las propiedades del elemento seleccionado en el ListBox. Es decir: la edición sigue teniendo lugar en un PropertyGrid. Recomiendo, por tanto, la estrategia siguiente: implementar primero la edición de la propiedad sin los dos paréntesis (corchetes en C#) en su tipo, y, cuando hayamos conseguido la personalización deseada, añadírselos. VSNET se encargará del resto.
A modo de ejemplo crearemos un muy simple álbum de fotos. Escribiremos una clase que contenga la imagen, el pie de foto, el lugar y la fecha; instanciaremos y editaremos objetos de esta clase y los guardaremos en un array que hará de álbum.
Esta es la propiedad Fotos de nuestra clase MiAmigo:
'Es conveniente inicializar el array aunque sea con un objeto vacío. Private _Fotos() As Fotografía = {New Fotografía()} <Category("Rasgos físicos"), Description("Elige una foto")> _ Public Property Fotos() As Fotografía() Get Return _Fotos End Get Set(ByVal Value As Fotografía()) _Fotos = Value End Set End PropertyÉsta la nueva clase Fotografía:
<TypeConverter(GetType(FotografíaConverter)), _ DefaultProperty("Instantánea")> _ Public Class Fotografía Private im As Image Private pie, lu, na As String Private da As Date Public Property Instantánea() As Image Get Return im End Get Set(ByVal Value As Image) im = Value End Set End Property Public Property PieDeFoto() As String Get Return pie End Get Set(ByVal Value As String) pie = Value End Set End Property Public Property Fecha() As Date Get Return da End Get Set(ByVal Value As Date) da = Value End Set End Property Public Property Lugar() As String Get Return lu End Get Set(ByVal Value As String) lu = Value End Set End Property <Editor(GetType(BuscadorArchivo), GetType(UITypeEditor))> _ Public Property Narración() As String Get Return na End Get Set(ByVal Value As String) na = Value End Set End Property End ClassY ésta su clase convertidora:
Public Class FotografíaConverter : Inherits StringConverter Public Overloads Overrides Function CanConvertFrom(ByVal context As ITypeDescriptorContext, _ ByVal sourceType As Type) As Boolean If sourceType Is GetType(String) Then Return True End If Return MyBase.CanConvertFrom(context, sourceType) End Function Public Overloads Overrides Function CanConvertTo(ByVal context As ITypeDescriptorContext, _ ByVal destinationType As Type) As Boolean If destinationType Is GetType(Fotografía) Then Return True End If Return MyBase.CanConvertFrom(context, destinationType) End Function Public Overloads Overrides Function ConvertTo(ByVal context As ITypeDescriptorContext, _ ByVal culture As CultureInfo, ByVal value As Object, ByVal destinationType As Type) As Object If destinationType Is GetType(String) AndAlso TypeOf value Is Fotografía Then 'Esta función obtiene el objeto Fotografía 'editado en el PropertyGrid del editor de colecciones 'Lo almacenamos en una variable pública Bild = value 'Y escribimos en la línea del ListBox el pie de foto para distinguirlo Return Bild.PieDeFoto End If Return MyBase.ConvertTo(context, culture, value, destinationType) End Function Public Overloads Overrides Function ConvertFrom(ByVal context As ITypeDescriptorContext, _ ByVal culture As CultureInfo, ByVal value As Object) As Object If TypeOf value Is String Then 'Esta función se ejecuta cuando seleccionamos un objeto en el ListBox 'y lo muestra el PropertyGrid del editor de colecciones 'Simplemente recuperamos el objeto 'que antes habíamos metido en la variable pública Return Bild End If Return MyBase.ConvertFrom(context, culture, value) End Function End Class Public Module Bilder Public Bild As Fotografía End ModuleEn definitiva, nada nuevo salvo que, si el tipo de la propiedad es una colección de tipos "básicos" como por ejemplo "Image" o "Point", entonces no es necesario, al menos para que el editor de colecciones funcione, ningún convertidor; en cambio, si se trata de un tipo complejo o personalizado, como es nuestro caso, entonces sí es necesaria una clase convertidora y además el atributo no se aplica a la propiedad sino al tipo: el atributo TypeConverter que contiene como parámetro FotografíaConverter no se aplica a la propiedad Fotos, sino a la clase Fotografía.
Es recomendable inicializar la propiedad cuyo tipo es una colección aunque sea con un objeto nuevo y sin editar, como hemos hecho nosotros. Si no el editor de colecciones puede no funcionar correctamente.
Al probar este código hemos descubierto que por cada item añadido a la colección el PropertyGrid ha añadido una fila identificada con el índice del item, e inmediatamente nos ha dominado un malsano deseo: ¿y si desplegáramos en el mismo PropertyGrid las propiedades de cada ítem de la colección? ¿y si, completamente enlodados en la lujuria, exigiéramos poder editar las propiedades de los ítems ahí mismo, sin tener que llamar al editor de colecciones? ¿y si, respondo yo, tuviéramos todo esto en nuestra pantalla con sólo modificar una línea de código? Pues todo se reduciría a derivar la clase convertidora no de StringConverter, sino de ExpandableObjectConverter:
Public Class FotografíaConverter : Inherits ExpandableObjectConverter 'Todo lo demás permanece igualHágase la luz:
Una colección totalmente desplegada Saciemos nuestra ambición: podemos modificar los ítems en el mismo PropertyGrid pero no podemos añadir ni eliminar ítems sin el editor de colecciones, así que añadiremos dos opciones al menú: Añadir Foto y Eliminar Foto, e implementaremos la adición y eliminación de fotos:
Private Sub Añadir_Foto_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MenuItem13.Click If Not TypeOf PropertyGrid1.SelectedObject Is MiAmigo Then Exit Sub On Error GoTo ExitSub '"Sacamos" el objeto del PropertyGrid Dim Tronco As MiAmigo = PropertyGrid1.SelectedObject '"Sacamos" el array de fotos del objeto Dim Retratos As Fotografía() = Tronco.Fotos 'Hacemos hueco para un ítem más ReDim Preserve Retratos(Retratos.GetUpperBound(0) + 1) 'Le añadimos un ítem nuevo Retratos(Retratos.GetUpperBound(0)) = New Fotografía() '"Metemos" el array en el objeto Tronco.Fotos = Retratos '"Metemos" el objeto en el PropertyGrid PropertyGrid1.SelectedObject = Tronco ExitSub: End Sub Private Sub Elimina_Foto_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MenuItem14.Click If Not TypeOf PropertyGrid1.SelectedObject Is MiAmigo Then Exit Sub On Error GoTo ExitSub Dim i, j As Short '"Sacamos" el objeto del PropertyGrid Dim Tronco As MiAmigo = PropertyGrid1.SelectedObject '"Sacamos" el array de fotos del objeto Dim Retratos As Fotografía() = Tronco.Fotos 'Metemos todos los ítems del array en una fila 'excepto el que queremos eliminar 'que es el seleccionado por el usuario Dim q As New Queue() For i = 0 To Retratos.GetUpperBound(0) If Not (PropertyGrid1.SelectedGridItem.Label = String.Format("[{0}]", i) _ OrElse PropertyGrid1.SelectedGridItem.Parent.Label = String.Format("[{0}]", i)) _ Then q.Enqueue(Retratos(i)) Next 'Convertimos la fila en un array tipo Object Dim Nuevos As Object() = q.ToArray 'Convertimos el array tipo Object en tipo Fotografía Dim SinEse(Nuevos.GetUpperBound(0)) As Fotografía For i = 0 To Nuevos.GetUpperBound(0) SinEse(i) = CType(Nuevos(i), Fotografía) Next 'Metemos el nuevo array en el objeto Tronco.Fotos = SinEse 'Metemos el objeto en el PropertyGrid PropertyGrid1.SelectedObject = Tronco ExitSub: End SubSólo hay una cosa que se puede hacer en el editor de colecciones y no (todavía) en el mismo PropertyGrid: Cambiar el orden de los items. Cedo al lector el reto de su implementación.
Otros editores personalizados
Cuadros de diálogo comunes de Windows
A continuación copio la clase que genera el editor de la propiedad "Narración". Su razón de ser es el empleo de un cuadro de diálogo OpenFileDialog para editar un tipo String. Esta clase es igualita a la que utilizamos para mostrar el cuadro de diálogo hecho por nosotros que editaba el color de ojos, solo que esta vez la invocación va dirigida a un cuadro de diálogo común de Windows:
Public Class BuscadorArchivo : Inherits UITypeEditor Public Overloads Overrides Function EditValue(ByVal context As _ System.ComponentModel.ITypeDescriptorContext, _ ByVal provider As System.IServiceProvider, ByVal value As Object) As Object 'Este procedimiento llama al cuadro de diálogo 'OpenFileDialog y devuelve la ruta del archivo seleccionado 'respetando siempre el tipo String de la propiedad Dim openf As Windows.Forms.OpenFileDialog = New Windows.Forms.OpenFileDialog() With openf .Filter = "Archivos de texto¦*.txt;*.rtf;*.doc" .ShowReadOnly = False .CheckFileExists = True End With Dim r As Windows.Forms.DialogResult = openf.ShowDialog If r = DialogResult.OK Then Return openf.FileName End Function Public Overloads Overrides Function GetEditStyle(ByVal context As _ System.ComponentModel.ITypeDescriptorContext) As _ System.Drawing.Design.UITypeEditorEditStyle Return UITypeEditorEditStyle.Modal End Function Public Overridable Overloads Function GetPaintValueSupported() As Boolean Return True End Function End Class
Más PropertyGrids
El mejor editor de una propiedad cuyo tipo es una clase creada por nosotros es ... otro PropertyGrid!. que a la vez puede recurrir, para editar una propiedad de tipo complejo, a otro PropertyGrid y así sucesivamente y sin menoscabo de ninguna de las potencialidades que acabamos de aprender. No hay, por tanto, tipo complejo que se le resista a un PropertyGrid. El lector ya se imaginará dónde está el truco: invocación al cuadro de diálogo que contiene el PropertyGrid dentro del método EditValue de la clase derivada de UITypeEditor. En el programa "Amigo" encontrará un ejemplo.
Recategorización y ocultación de propiedades: PropertyTabs
Un PropertyTab es un CheckBox con apariencia de botón añadido al PropertyGrid que permite seleccionar las propiedades que serán visibles (ocultación) y redefinir el valor de su atributo Category(Attribute) (recategorización), de forma que esta ocultación y recategoriación ocurren sólo mientras se mantene pulsado. Si añadimos varios PropertyTabs facilitaremos al usuario la búsqueda y edición de propiedades por "supergrupos" o le permitiremos ordenarlas por criterios de nuestra elección.
Para cada botón que añadamos hemos de escribir una clase derivada de PropertyTab. La función básica de esta clase será crear un array que contendrá los descriptores de las propiedades o PropertyDescriptor que se mostrarán al pulsar el botón. Un PropertyDescriptor no es una propiedad, sino un objeto que contiene información acerca de una propiedad. Podemos crear un PropertyDescriptor nuevo a partir de una determinada propiedad entregando a la función compartida TypeDescriptor.CreateProperty los siguientes argumentos:
- El tipo del objeto que muestra el PropertyGrid
- Un PropertyDescriptor de la propiedad de la cual copia toda la información
- Una lista de atributos opcional. Si el PropertyDescriptor del punto anterior ya contiene algún atributo de la lista, entonces los valores se actualizan.
La sintaxis es la siguiente:
Dim PropertyDescriptorNuevo As PropertyDescriptor = TypeDescriptor.CreateProperty(Tipo del objeto del PropertyGrid, PropertyDescriptor de la propiedad "vieja", ParamArray de atributos)
A continuación copio una clase que ocultará todas las propiedades que no pertenezcan a la categoría "Rasgos físicos":
Public Class RasgosSelector : Inherits PropertyTab Private Const Atributo As String = "Rasgos físicos" Public Overloads Overrides Function GetProperties(ByVal component As Object, ByVal attributes() As System.Attribute) _ As System.ComponentModel.PropertyDescriptorCollection 'component contiene el SelectedObject del PropertyGrid 'attributes es un ParamArray de atributos 'que actúa como un filtro de propiedades Dim ProsMiAmigo As PropertyDescriptorCollection If attributes Is Nothing Then 'Mete en el array un PropertyDescriptor por cada propiedad ProsMiAmigo = TypeDescriptor.GetProperties(component) Else 'Mete en el array un PropertyDescriptor por cada propiedad 'que esté marcada con los atributos contenidos en "attributes" ProsMiAmigo = TypeDescriptor.GetProperties(component, attributes) End If Dim ProsArray As New ArrayList() Dim i As Integer For i = 0 To ProsMiAmigo.Count - 1 If ProsMiAmigo(i).Category = Atributo Then 'Seleccionamos los descriptores cuyas propiedades 'estén marcadas con el atributo "Rasgos físicos" 'Entregamos: 'el tipo metido en el PropertyGrid GetType(MiAmigo) = ProsMiAmigo(i).ComponentType 'El descripor de la propiedad (ProsMiAmigo(i)) 'para que el nuevo descriptor copie toda la información que no modificamos 'Y un nuevo atributo, que solo redefine el atributo especificado 'o sea, en este caso, CategoryAttribute 'los demás atributos, si los hubiere, no varían 'Metemos cada PropertyDescriptor nuevo en el ArrayList ProsArray.Add(TypeDescriptor.CreateProperty(ProsMiAmigo(i).ComponentType, _ ProsMiAmigo(i), New CategoryAttribute(Atributo))) End If Next 'Convertimos el ArrayList en un Array de tipo PropertyDescriptor Dim ReturnArray() As PropertyDescriptor = ProsArray.ToArray(GetType(PropertyDescriptor)) 'No devolvemos exactamente un Array sino una PropertyDescriptorCollection Return New PropertyDescriptorCollection(ReturnArray) End Function Public Overloads Overrides Function GetProperties(ByVal component As Object) As System.ComponentModel.PropertyDescriptorCollection Return Me.GetProperties(component, Nothing) 'Sin filtro End Function Public Overrides ReadOnly Property TabName() As String Get Return Atributo 'Este es el tooltip End Get End Property Public Overrides ReadOnly Property Bitmap() As System.Drawing.Bitmap Get 'La imagen del botón Return New Bitmap(Application.StartupPath & "\CLOUD-01.bmp") End Get End Property End ClassSólo queda entregarle nuestro nuevo PropertyTab al PropertyGrid:
PropertyGrid1.PropertyTabs.AddTabType(GetType(RasgosSelector), _ System.ComponentModel.PropertyTabScope.Component)
Podemos asociar un tipo a un PropertyTab marcándolo con el atributo PropertyTab(Attribute) y entregándole el tipo de una clase derivada de PropertyTab como la que acabamos de crear:
<DefaultProperty("Nombre"), PropertyTab(GetType(RasgosSelector))> Public Class MiAmigoUn tipo admite sólo un atributo PropertyTab, lo cual no quiere decir que no puedan aparecer varios ProeprtyTabs a la vez en el PropertyGrid. La gestión de la visibilidad de los PropertyTabs se encarga de ello.
La enumeración PropertyTabScope determina el ámbito o visibilidad del PropertyTab. Sus valores son los siguientes:
- Component El PropertyTab sólo aparece cuando la clase generadora del SelectedObject del PropertyGrid está marcada con el atributo PropertyTab(Attribute) y el parámetro de este atributo es el mismo PropertyTab (opción por defecto).
- Document Esta opción tiene sentido para entornos como el IDE de VSNET donde se editan varios documentos a la vez y los objetos que muestra el PropertyGrid varían según el documento editado. Si no hay más que un documento, como en nuestro programa de ejemplo "Amigo", entonces aparece siempre.
- Global El PropertyTab aparece siempre pero puede ser eliminado.
- Static El PropertyTab aparece siempre y no puede ser eliminado.
Así pues, un PropertyTab asignado a un determinado tipo, es decir, visible a nivel de Component, puede convivir con otros PropertyTabs con visibilidad más amplia, como, por ejemplo, Document.
El código y la imagen siguientes muestran las propiedades categorizadas por tipo (sólo copio la función GetProperties porque los demás miembros, salvo la ruta de la imagen, no varían):
Public Overloads Overrides Function GetProperties(ByVal component As Object, ByVal attributes() As System.Attribute) _ As System.ComponentModel.PropertyDescriptorCollection Dim ProsMiAmigo As PropertyDescriptorCollection If attributes Is Nothing Then ProsMiAmigo = TypeDescriptor.GetProperties(component) Else ProsMiAmigo = TypeDescriptor.GetProperties(component, attributes) End If Dim ProsArray(ProsMiAmigo.Count - 1) As PropertyDescriptor Dim i As Integer For i = 0 To ProsArray.GetUpperBound(0) ProsArray(i) = TypeDescriptor.CreateProperty(ProsMiAmigo(i).ComponentType, _ ProsMiAmigo(i), New CategoryAttribute(ProsMiAmigo(i).PropertyType.ToString)) Next Return New PropertyDescriptorCollection(ProsArray) End Function
Propiedades agrupadas según su tipo Los + y - de este PropertyGrid son engañosos. Al menos a mí no se me despliegan las propiedades tras recategorizarlas. Si alguien sabe cómo se hace le agradecería que me lo dijera.
Conclusión
Todavía queda mucho que decir: el despliegue de propiedades recategorizadas que espero que alguien me lo enseñe algún día, el enlace a datos, la apariencia del control, los eventos ... y temas más avanzados como la creación dinámica de tipos por medio de CodeDom y su edición en el PropertyGrid ... sin embargo creo que con lo que yo he contado nos hemos hecho el lector y yo con una buena base para explorar las grandes posibilidades de este control que tan elegante es y tanto trabajo nos ahorra.
Adjunto dos programas de ejemplo:
El programa "Amigo" contiene todo el código explicado en esta entrega, y el Programa "Controles" simula el editor de propiedades del IDE de VBNET, con combo y todo.
Amigo2 Amigo2.zip 504 KB Controles2 Controles2.zip 41.5 KB