El Control PropertyGridEntrega 1 de 219 Abril 2003
|
.
|
Introducción
El control PropertyGrid (cuadro o "rejilla" de propiedades) es un editor de propiedades de objetos. Es exactamente el mismo control que utiliza el IDE para editar las propiedades de los controles y demás elementos que constituyen una aplicación. VSNET, gracias a su potente capacidad de reflexión, nos permite incorporarlo a nuestras aplicaciones para que el usuario edite los objetos generados por ella.
El control PropertyGrid se compone de dos columnas y, como mínimo, tantas filas como propiedades editables contiene el objeto. La columna de la izquierda identifica la propiedad por su nombre, y la de la derecha ofrece un espacio para ver y/o editar la propiedad. La edición de las propiedades puede realizarse directamente, es decir, escribiendo el valor en las celdas correspondientes, o mediante editores apropiados. Una de las múltiples virtudes de este control es que los editores forman parte de sí mismo, lo cual supone para el programador un considerable ahorro de código, al tiempo que ofrece asistencia al usuario en la edición de objetos, lo cual, sin duda, añade elegancia y valor a la aplicación.
Otra de las virtudes de este control son sus grandes posibilidades de personalización. Si bien el editor que ofrece el PropertyGrid para editar una determinada propiedad depende del tipo de ésta, el programador, lejos de estar constreñido a ese editor, puede elegir cualquier otro, si lo considera más adecuado, o incluso puede crear su propio editor. O, si lo prefiere, puede dejar que el usuario edite las propiedades del objeto prácticamente sin darse cuenta de ello, simplemente interactuando con la aplicación. Todas estas posibilidades y otras más las veremos a continuación.
Edición de controles
Comenzaremos la exploración de todo el potencial del control PropertyGrid examinando su uso más sencillo: la edición de controles. El lector puede verlo en acción ejecutando el programa de ejemplo "Controles". El formulario de este programa no contiene más que un PropertyGrid y un panel, aparte del menú y una barra de herramientas que permiten seleccionar un control y lanzarlo a una posición aleatoria del panel. Inmediatamente tras el lanzamiento el PropertyGrid se llena con las propiedades del control. Si el usuario edita los valores en el PropertyGrid, el control se modifica en consecuencia. Además, podemos cambiar el control que se refleja en el PropertyGrid con sólo pulsar el ratón sobre él.
La selección del objeto cuyas propiedades llenarán el PropertyGrid se realiza mediante la propiedad .SelectedObject del propertyGrid. Supongamos que nuestro PropertyGrid se llama PropertyGrid1 y queremos editar el botón Button1. Entonces:
PropertyGrid1.SelectedObject = Button1
Si queremos cambiar de objeto, basta con repetir la misma línea asignándole el nuevo objeto:
PropertyGrid1.SelectedObject = Button2
Y si queremos vaciar el PropertyGrid:
PropertyGrid1.SelectedObject = Nothing
Basta con una línea que asigna el control al PropertyGrid para que los valores que el usuario modifica en el PropertyGrid afecten automáticamente al control. Es como si el PropertyGrid se hubiera "conectado" al control. Inversamente, si la modificación del valor de una propiedad se origina en el mismo control, el PropertyGrid asume el nuevo valor. El programa "Controles" muestra un ejemplo: arrastre el lector sobre el panel cualquier control lanzado en él y comprobará que los valores de la propiedad "Location" se actualizan en consecuencia.
Edición de objetos
Button1 no es, en realidad, sino una instancia de la clase Button. Podemos crear nuestras propias clases y enviar objetos instanciados a partir de ellas al PropertyGrid para que el usuario de nuestra aplicación los edite. Vamos a crear una clase llamada "MiAmigo" y aprenderemos a editar sus propiedades mediante el PropertyGrid del modo más personalizado posible. Empezaremos con los siguientes miembros:
Imports System.ComponentModel Public Class MiAmigo Public Enum GradoAmistad Enemigo Interesado Conocido Cercano Amigote Bueno GranAmigo ParaSiempre End Enum Private _Cumpleaños As Date Private _Escala As GradoAmistad Private _Nombre As String Private _ColorOjos As Color Private _Transparente As Boolean Private _Foto As Image <Category("Identificación"), Description("Nombre y apellidos")> _ Public Property Nombre() As String Get Return _Nombre End Get Set(ByVal Value As String) _Nombre = Value End Set End Property <Category("Rasgos físicos"), Description("Color de los ojos")> _ Public Property ColorOjos() As Color Get Return _ColorOjos End Get Set(ByVal Value As Color) _ColorOjos = Value End Set End Property <Category("Personalidad"), Description("¿Es transparente y sincero o falso y opaco?")> _ Public Property Transparente() As Boolean Get Return _Transparente End Get Set(ByVal Value As Boolean) _Transparente = Value End Set End Property <Category("Identificación"), Description("Día de su cumpleaños")> _ Public Property Cumpleaños() As Date Get Return _Cumpleaños End Get Set(ByVal Value As Date) _Cumpleaños = Value End Set End Property <Category("Amistad"), Description("Nivel de amistad")> _ Public Property Escala() As GradoAmistad Get Return _Escala End Get Set(ByVal Value As GradoAmistad) _Escala = Value End Set End Property <Category("Rasgos físicos"), Description("Elige una foto")> _ Public Property Foto() As Image Get Return _Foto End Get Set(ByVal Value As Image) _Foto = Value End Set End Property End ClassLa asignación de una instancia de nuestra clase al PropertyGrid es idéntica a la asignación de un control:
PropertyGrid1.SelectedObject = New MiAmigo
Atributos de las propiedades
Podemos controlar ciertos aspectos de las propiedades de las clases cuyos objetos enviamos a un control PropertyGrid mediante atributos opcionales incluidos en el espacio System.ComponentModel:
Category(String) Clasifica las propiedades en grupos.
DescriptionAttribute(String) Texto que aparece cuando se selecciona la propiedad.
BrowsableAttribute(Boolean) Indica si la propiedad se muestra o no el el control
ReadOnlyAttribute(Boolean) Permite o prohibe al usuario editar la propiedad. No confunda el lector este atributo con la marca ReadOnly de una propiedad que sólo implementa Get. Una propiedad mostrada en un PropertyGrid puede ser de sólo lectura para el usuario, pero implementar tanto Set como Get y ser, por consiguiente, tanto de lectura como de escritura para su código cliente.
DefaultValueAttribute(Object) Establece el valor por defecto de la propiedad
DefaultPropertyAttribute(String) Este atributo no se aplica a una propiedad sino a la clase. Establece la propiedad que aparece seleccionada cuando se entrega el objeto al control.
Edición de propiedades mediante listas desplegables
Hay dos maneras de introducir un ComboBox en el área de edición de una propiedad en un PropertyGrid. Una de ellas, la más sencilla, es mediante una enumeración. Si el tipo de una propiedad es una enumeración, entonces el PropertyGrid despliega en un ComboBox la representación textual de cada miembro de la enumeración para que el usuario seleccione uno de ellos. Si, una vez seleccionado el miembro, necesitamos no el valor numérico que representa, sino su expresión textual, entonces hemos de invocar ToString:
Dim Tú As MiAmigo = PropertyGrid1.SelectedObject Dim TuAmistad As String = Tú.Escala.ToStringEsta extremada sencillez presenta, sin embargo, dos inconvenientes: la representación textual de los miembros de las enumeraciones no puede contener espacios en blanco, lo cual, si necesitamos más bien expresiones que palabras, obliga a presentarlas juntando las palabras o recurriendo a guiones; y, lo que puede ser más grave, los miembros de las enumeraciones, una vez escritos por el programador, son inmutables. Necesitamos Combos cuyos elementos puedan variar en base a decisiones tomadas por el usuario y cuya representación textual carezca de trabas.
Vamos a añadir una nueva propiedad a la clase MiAmigo: EquipoPreferido, y desplegaremos un combo para que el usuario seleccione su equipo de fútbol. Para ello escribiremos la siguiente clase heredera de System.ComponentModel.Stringconverter:
Imports System.ComponentModel Public Class ComboEquipos Inherits StringConverter Public Sub New() MyBase.New() End Sub Private Equipos() As String = {"Barcelona", "Real Madrid", "Atlético de Madrid", "Depor", "Real Sociedad"} 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(Equipos) 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 False End Function End ClassEsta clase reimplementa tres funciones:
GetStandardValuesSupported permite alternar dinámicamente entre el ComboBox, donde el usuario debe elegir entre las opciones disponibles, y el TextBox, donde el usuario introduce libremente un valor.
GetStandardValuesExclusive determina si el usuario debe limitarse a los valores de la lista o si, aun disponiendo de ella, puede introducir un valor nuevo.
GetStandardValues entrega la lista de miembros del Combobox.
Ya sólo resta asignar esta clase a la propiedad mediante el atributo TypeConverter, que toma como parámetro el tipo recién definido:
Private _EquipoPreferido As String <Category("Gustos y aficiones"), TypeConverter(GetType(ComboEquipos)), _ Description("Seleccione un equipo de fútbol o introduzca uno nuevo")> _ Public Property EquipoPreferido() As String Get Return _EquipoPreferido End Get Set(ByVal Value As String) _EquipoPreferido = Value End Set End PropertyNo podemos conformarnos con tan limitado número de opciones, y menos en tema tan delicado como las aficiones futbolísticas. Procedamos rápidamente a flexibilizar nuestro Combo para dar cabida a cualquier equipo de fútbol.
Antes que nada ampliaremos la accesibilidad del array trasladándolo a un módulo público:
End Class Public Module Fútbol Public Equipos() As String = {"Barcelona", "Real Madrid", "Atlético de Madrid", "Depor", "Real Sociedad"} End ModuleLa clave está en el evento PropertyValueChanged, que se dispara cuando se modifica el valor de una propiedad en el PropertyGrid. Su parámetro e contiene dos propiedades: ChangedItem y OldValue. ChangedItem devuelve un objeto GridItem que representa la propiedad recién editada. De este objeto nos interesan ahora dos propiedades: Label y Value. Label devuelve el nombre de la propiedad que viene expresado en la etiqueta que la identifica, y Value nos entrega encapsulado en una variable de tipo Object el nuevo valor que ha introducido o seleccionado el usuario. OldValue, como su nombre indica, devuelve que valor de la propiedad antes de ser editada.
Con toda esta información podemos implementar el evento PropertyValueChanged de forma que, si detecta un cambio en el valor de la propiedad EquipoPreferido, entonces añade ese valor al array Equipos, previa comprobación, claro está, de que no se trata de un ítem del mismo array. Puesto que hemos trasladado el array a un módulo público, no perderá sus nuevos valores y los veremos en el Combo cuando entreguemos al PropertyGrid un nuevo objeto MiAmigo. He aquí el código:
Private Sub PropertyGrid1_PropertyValueChanged(ByVal sender As Object, _ ByVal e As System.Windows.Forms.PropertyValueChangedEventArgs) _ Handles PropertyGrid1.PropertyValueChanged 'Detectamos un cambio en la propiedad que nos interesa 'comprobando mediante .Label el nombre de la propiedad If e.ChangedItem.Label = "EquipoPreferido" Then 'Introducimos el valor nuevo en una variable por comodidad Dim valor As String = e.ChangedItem.Value 'Comprobamos que el valor no era uno de los items del array If Array.IndexOf(Equipos, valor) < 0 Then 'Redimensionamos el array para dar cabida al nuevo valor ReDim Preserve Equipos(Equipos.GetUpperBound(0) + 1) 'Añadimos el nuevo valor al array Equipos.SetValue(valor, Equipos.GetUpperBound(0)) End If End If End SubEl único problema que se nos presenta ahora es cómo conservar los nuevos valores tras cerrar el programa para que aparezcan al abrirlo de nuevo. Una buena solución es serializar el array. Más adelante, cuando almacenemos nuestros amigos en disco, mencionaré la serialización.
Edición de propiedades mediante editores personalizados
Generación del editor
Fijémonos ahora en la propiedad ColorOjos. El PropertyGrid nos ofrece un completísimo cuadro de diálogo donde podemos seleccionar y personalizar cualquiera de los (xxxxxx) colores disponibles. Eso es, precisamente, lo que no nos interesa, pues los colores de los ojos humanos se sitúan en un rango más bien reducido, y sería más fácil para el usuario que le presentáramos sólo ese rango de colores. Tenemos que construirnos nuestro propio cuadro de diálogo y asignárselo a la propiedad.
En la figura siguiente podemos ver el editor predeterminado para las propiedades de tipo Color y el que he creado yo específico para el color de los ojos (El degradado de colores lo he generado, dicho sea de paso, con el programa "Dibujo" que incluimos mi hermano Manuel y yo en nuestro tutorial sobre GDI+ que te invitamos a leer si pulsas aquí)
El editor de colores predeterminado
Nuestro editor personalizado
Como se puede apreciar, mi editor personalizado despliega la gama de los colores que esperamos ver en los ojos humanos: castaño-gris-azul-verde-negro, pero también puede mostrar, en el caso de que a alguien le gustan las lentillas de colores chillones, el cuadro de diálogo común "ColorDialog" para que el usuario, si lo desea, no eche de menos ningún color. No me detendré en los detalles de implementación de mi "EyesColorPicker" porque alargaría en exceso este artículo. El lector puede, si lo desea, consultar el código en el programa "Amigo".
Sí voy a mencionar, sin embargo, algunos extremos acerca de la invocación al cuadro de diálogo personalizado, puesto que, aunque estrictamente hablando caen fuera del tema de este artículo, su conocimiento es imprescindible para poder traer al "EyesColorPicker" a escena.
Si el formulario que alberga nuestro cuadro de diálogo personalizado está incluido en el mismo proyecto donde se encuentra el PropertyGrid, entonces su invocación no difiere de la de cualquier cuadro de diálogo. Ahora bien, si, como sucede en nuestro programa de ejemplo "Amigo", el cuadro de diálogo personalizado está dentro de otro proyecto "servidor" que es llamado desde el proyecto "cliente", entonces, para que el proyecto cliente pueda invocarlo, debemos convertir el servidor en una DLL siiguiendo los siguiente pasos:
Accedemos a las propiedades del proyecto servidor (lo seleccionamos en el explorador de proyectos y pulsamos el menú Project -> Properties) y seleccionamos "Class Library" en el combo etiquetado "Output Type".
Creamos en el proyecto servidor una clase pública que contenga un miembro también público que funcionará como punto de entrada a la DLL. Este miembro se encargará de invocar el cuadro de diálogo, entregarle los parámetros de arranque y devolver al código cliente los valores generados por la interacción del usuario.
Seleccionamos el proyecto servidor en el explorador de proyectos, pulsamos el botón derecho del botón y en el menú contextual pulsamos "Build". Esta acción generará una DLL en la carpeta "bin".
Ya podemos añadir en el proyecto cliente una referencia al proyecto servidor seleccionando el proyecto cliente y pulsando en Project -> Add Reference.... -> Projects. Aparecerá el nombre del proyecto servidor, lo seleccionamos, pulsamos Select y aceptamos.
El explorador de proyectos debe parecerse a éste:
'Punto de entrada Public Class EntryPoint Public Function Run(ByVal sol As Color) As Object Dim Eye As EyeColorPicker = New EyeColorPicker() Eye.Color = sol 'Parámetro de arranque 'Llamada al cuadro de diálogo Dim r As DialogResult = Eye.ShowDialog If r = DialogResult.OK Then Return Eye.Color End Function End ClassPunto de entrada de la DLL
'Invocación desde el código cliente Dim Ojo As New EyesColorWindowsApplication.EntryPoint() Dim sol As Object = Ojo.Run(value) If Not sol Is Nothing Then Return CType(sol, Color)Explorador de proyectos
Invocación desde el código cliente
El código "Punto de Entrada" es el contenido en la clase "EntryPoint" del proyecto servidor. Este será el punto de entrada (de ahí el nombre que le he puesto) de la DLL, y puede invocarse desde el código cliente instanciando un objeto de tipo [Nombre del proyecto servidor].[Nombre de la clase que contiene el punto de entrada].
Salvado este posible escollo, la cuestión ahora es dónde colocar la invocación al proyecto servidor. La solución es una clase heredera de System.Drawing.Design.UITypeEditor. Aquí está:
Imports System.Drawing.Design Imports System.Globalization Public Class EditorColorOjos : Inherits UITypeEditor Public Overloads Overrides Function EditValue(ByVal context As _ System.ComponentModel.ITypeDescriptorContext, _ ByVal provider As System.IServiceProvider, ByVal value As Object) As Object Dim Ojo As New EyesColorWindowsApplication.EntryPoint() Dim sol As Object = Ojo.Run(value) If Not sol Is Nothing Then Return CType(sol, Color) End Function Public Overloads Overrides Function GetEditStyle(ByVal context As _ System.ComponentModel.ITypeDescriptorContext) As _ System.Drawing.Design.UITypeEditorEditStyle Return UITypeEditorEditStyle.Modal End Function Public Overloads Function GetPaintValueSupported() As Boolean Return True End Function End ClassLa clave está en la función EditValue. Su argumento value contiene el valor leído en el PropertyGrid. Es dentro de esta función donde llamamos al editor personalizado entregándole un valor de arranque (Object = Ojo.Run(value)), esperamos que el usuario edite o seleccione el valor deseado, obtenemos ese valor (Object = Ojo.Run(value)) y se lo entregamos al PropertyGrid (Return CType(sol, Color)). Recomiendo, para evitar errores de conversión (Cast) entregar a y recoger del cuadro de diálogo los valores encapsulados en una variable de tipo Object. Al PropertyGrid, sin embargo, debemos entregarle el valor desencapsulado (en nuestro caso, extraemos de la caja Object un Color (estoy traduciendo literalmente "unboxing" = sacar de la caja. Lo mío me costó comprender que "boxing" y "unboxing" no tenían nada que ver con el boxeo...))
La función GetEditStyle permite decidir si el cuadro de diálogo lo presentamos con modo (.Modal), es decir, como cualqier cuadro de diálogo que no permite acceder a ninguna otra ventana de la aplicación hasta que no lo cerramos, o también con modo pero colocado bajo la celda de la propiedad, como si fuera la lista de items de un ComboBox (DropDown). También existe la opción None, que simplemente no permite que se muestre el cuadro de diálogo.
Por último, la función GetPaintValueSupported debe devolver siempre True
Sólo queda asignar esta clase a la propiedad mediante el atributo Editor:
<Editor(GetType(EditorColorOjos), GetType(UITypeEditor)), _ Category("Rasgos físicos"), Description("Color de los ojos")> _ Public Property ColorOjos() As Color Get Return _ColorOjos End Get Set(ByVal Value As Color) _ColorOjos = Value End Set End PropertyEl atributo Editor(Attribute) toma dos parámetros: el tipo que llama al cuadro de diálogo y su tipo base UITypeEditor. Ya puede el lector saborear el fruto de su trabajo.
Conversión de tipos
Mi paciente lector se merece una explicación. Ha seguido mis indicaciones al pie de la letra, ha revisado repetidas veces el código y no ha encontrado errores. ¿Por qué, entonces, no sale nuestro cuadro de diálogo? ¿por qué no funciona nuestra creación?
Nuestra obra de arte no funciona porque todavía le falta implementación, y yo lo sabía y me lo he callado. Estoy, pese a ello, seguro de que mi lector me va a perdonar porque, gracias a esta broma, ha aprendido algo nuevo: no se ha mostrado nuestro editor del color de ojos porque le falta código, tampoco ha aparecido el editor de colores predeterminado porque, al marcar el atributo EditorAttribute con nuestra clase EditorColorOjos, le hemos cortado el paso, sin embargo, nos hemos encontrado con un Combo repleto de miembros de la estructura Color. La explicación es sencilla: simplemente, a falta de editor, el PropertyGrid ha tratado la propiedad según su tipo: los miembros de las estructuras los despliega en un Combo, igual que los de las enumeraciones. Esto significa que la edición de propiedades cuyos tipos son estructuras sean o no personalizadas se lleva a cabo vía combo sin que tengamos que escribir una sola línea de código.
El código que falta tiene que ver con el tipo de datos utilizado para representar la propiedad en la celda del PropertyGrid. Aunque el tipo de nuestra propiedad es Color, internamente el PropertyGrid opera con su representación numérica, por tanto necesita que le convirtamos el tipo Color a Integer. Para ello hemos de implementar una clase heredera de System.ComponentModel.TypeConverter o de alguna de las siguientes clases herederas de TypeConverter: StringConverter, Int16/32/64Converter, DoubleConverter, DecimalConverter, CollectionConverter, DateTimeConverter, BooleanConverter, EnumConverter, GuidConverter, SingleConverter u otra de nombre [nombre de tipo]Converter. Aquí está la nuestra:
Public Class ColorConverter : Inherits Int32Converter Public Overloads Overrides Function CanConvertFrom(ByVal context As ITypeDescriptorContext, _ ByVal sourceType As Type) As Boolean If sourceType Is GetType(Integer) Then Return True End If Return MyBase.CanConvertFrom(context, sourceType) End Function Public Overloads Overrides Function ConvertFrom(ByVal context As ITypeDescriptorContext, _ ByVal culture As CultureInfo, ByVal value As Object) As Object 'Del Grid al diálogo If TypeOf value Is Integer Then Return Color.FromArgb(value) End If Return MyBase.ConvertFrom(context, culture, value) End Function Public Overloads Overrides Function CanConvertTo(ByVal context As ITypeDescriptorContext, _ ByVal destinationType As Type) As Boolean If destinationType Is GetType(Integer) Then Return True End If Return MyBase.CanConvertTo(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(Integer) AndAlso TypeOf value Is Color Then Return CType(value, Color).ToArgb End If Return MyBase.ConvertTo(context, culture, value, destinationType) End Function End ClassEl objetivo es convertir el valor de tipo Color de la propiedad a un tipo Integer y entregárselo al PropertyGrid, y recibir del PropertyGrid un valor de tipo Integer y convertirlo al tipo Color de la propiedad y, de paso, entregárselo al editor. Eso es exactamente lo que hace esta clase.
En primer lugar debemos acordar que permitimos la conversión tanto en un sentido como en otro: desde el tipo del PropertyGrid al de la propiedad (CanConvertFrom = True) y desde el de la propiedad al del PropertyGrid (CanConvertTo = True). Después debemos comprobar que los tipos son correctos. He aquí una tabla donde especifico el tipo correcto para las variables de cada miembro de la clase convertidora, incluido el valor de salida. Con "PropertyGrid" me refiero al tipo que espera el PropertyGrid (en nuestro caso, Integer) y con "Propiedad" el propio de la propiedad, valga la redundancia; en nuestro caso, Color.
Procedimientos
Variables
value
sourceType
destinationType
Return
CanConvertFrom
PropertyGrid
Boolean
ConvertFrom
PropertyGrid
Propiedad
CanConvertTo
Propiedad
Boolean
ConvertTo
Propiedad
PropertyGrid
PropertyGrid
Una vez efectuadas las oportunas comprobaciones, procederemos a la conversión en sí.
La función ConvertFrom nos entrega en su parámetro value el valor procedente del PropertyGrid que hemos de convertir al valor que espera la propiedad. Implementaremos, por tanto, la conversión, y el resultado será el valor de salida de la función. En nuestro caso value contiene el tipo Integer que entrega el PropertyGrid, lo convertimos a un tipo Color y el resultado es el valor de salida de la función.
La función ConvertTo, por contra, nos entrega en su parámetro value el tipo de la propiedad que hemos de convertir al valor que espera el PropertyGrid. Implementamos la conversión y el resultado lo colocamos en la puerta de salida de la función. En nuestro caso, value contiene el tipo Color que hemos convertido al tipo Integer que espera el PropertyGrid.
Sólo resta, como de constumbre, asignar a la propiedad la clase convertidora mediante un atributo. El atributo se llama, como era de esperar, TypeConverter(Attribute):
<Editor(GetType(EditorColorOjos), GetType(UITypeEditor)), _ TypeConverter(GetType(ColorConverter)), _ Category("Rasgos físicos"), Description("Color de los ojos")> _ Public Property ColorOjos() As Color Get Return _ColorOjos End Get Set(ByVal Value As Color) _ColorOjos = Value End Set End PropertyAhora sí que funciona.
Edición de propiedades en varias filas
No cante el lector victoria. La mayoría de los usuarios de nuestra aplicación seleccionará el color visualmente, pero seguro que a un pequeño porcentaje de ellos le da por jugar con los números a ver qué pasa. Tras seleccionar un determinado color, en la celda de la propiedad nos ha aparecido su representación textual. Deberíamos poder seleccionar un color simplemente editando los valores numéricos que lo representan. Es más, sería mucho más cómodo y elegante que cada uno de esos valores tuviera su propia fila en el PropertyGrid, y que todas las filas estuvieran agrupadas bajo la de la su propiedad "ColorOjos".
La clase que distribuye los miembros de un tipo en varias filas hereda de ExpandableObjectConverter, que a su vez hereda de System.ComponentModel.TypeConverter, lo cual implica que es incompatible con la clase Int32Converter que hemos utilizado en nuestro ejemplo y con cualquier otra heredera de TypeConverter. O aplicamos una clase o aplicamos la otra, pero las dos a la vez es imposible. Además, una vez implementada la heredera de ExpandableObjectConverter, debemos incluirla en un atributo Editor(Attribute) que se aplica no a la propiedad, sino a su tipo. No podemos aplicar atributos al tipo Color, así que tenemos que construirnoslo nosotros.
Para más inri, el tipo Color no es una clase, sino una estructura, ergo no lo podemos heredar. Tenemos que construir nuestra clase desde los cimientos. He aquí un ejemplo:
Imports System.ComponentModel Imports System.Drawing.Design Imports System.Globalization <TypeConverter(GetType(ColorConverterEx))> _ Public Class ColorEx Private _A As Byte = 255 Private _R As Byte = 255 Private _G As Byte = 255 Private _B As Byte = 255 Public Sub New() End Sub Public Sub New(ByVal value As Color) _A = value.A _R = value.R _G = value.G _B = value.B End Sub Public Sub New(ByVal Transparencia As Byte, ByVal Rojo As Byte, ByVal Verde As Byte, ByVal azul As Byte) _A = Transparencia _R = Rojo _G = Verde _B = azul End Sub <DefaultValue(255)> _ Public Property Transparencia() As Byte Get Return _A End Get Set(ByVal Value As Byte) _A = Value End Set End Property <DefaultValue(255)> _ Public Property Rojo() As Byte Get Return _R End Get Set(ByVal Value As Byte) _R = Value End Set End Property <DefaultValue(255)> _ Public Property Verde() As Byte Get Return _G End Get Set(ByVal Value As Byte) _G = Value End Set End Property <DefaultValue(255)> _ Public Property Azul() As Byte Get Return _B End Get Set(ByVal Value As Byte) _B = Value End Set End Property Public Property Color() As Color Get Return Color.FromArgb(_A, _R, _G, _B) End Get Set(ByVal Value As Color) Transparencia = Value.A Rojo = Value.R Verde = Value.G Azul = Value.B End Set End Property Public ReadOnly Property Matiz() As Single Get Return Color.FromArgb(_A, _R, _G, _B).GetHue End Get End Property Public ReadOnly Property Brillo() As Single Get Return Color.FromArgb(_A, _R, _G, _B).GetBrightness End Get End Property Public ReadOnly Property Saturación() As Single Get Return Color.FromArgb(_A, _R, _G, _B).GetSaturation End Get End Property End ClassPara no borrar lo que tanto esfuerzo nos ha costado, añadiremos a la clase MiAmigo una propiedad ColorOjosEx de tipo ColorEx:
<Editor(GetType(EditorColorOjosEx), GetType(UITypeEditor)), _ Category("Rasgos físicos"), Description("Color de los ojos")> _ Public Property ColorOjosEx() As ColorEx Get Return _colorOjosEx End Get Set(ByVal Value As ColorEx) _colorOjosEx = Value End Set End Property Private _ColorOjosEx As ColorExDebemos, lógicamente, introudcir modificaciones en nuestro editor de color de ojos para adaptarlo al nuevo tipo, y tenemos que implementar el convertidor ColorConverterEx encargado de distribuir los miembros del tipo ColorEx en varias filas.
El nuevo editor sólo tiene que modificar la función EditValue porque los tipos Color que nuestro selector de color de ojos personalizado recibe y entrega han de ser convertidos a tipos ColorEx, de modo que no merece la pena reescribirlo entero: creamos una clase EditorColorOjosEx que herede de EditorColorOjos y reimplementamos EditValue (para ello hemos marcado esta función en EditColorOjos Overridable):
Public Class EditorColorOjosEx : Inherits EditorColorOjos Public Overloads Overrides Function EditValue(ByVal context As _ System.ComponentModel.ITypeDescriptorContext, _ ByVal provider As System.IServiceProvider, ByVal value As Object) As Object 'value contiene un tipo ColorEx If value Is Nothing Then value = New ColorEx(Color.White) Dim Ojo As New EyesColorWindowsApplication.EntryPoint() 'Entregamos un tipo Color obtenido convirtiendo el tipo ColorEx de value Dim sol As Object = _ Ojo.Run(Color.FromArgb(value.Transparencia, value.Rojo, value.Verde, value.azul)) 'Recibimos un tipo Color, lo convertimos en ColorEx y le damos salida. If Not sol Is Nothing Then Return New ColorEx(sol.A, sol.R, sol.G, sol.B) End Function End ClassLos miembros reimplementados de la clase convertidora no hace, a fin de cuentas, sino convertir un objeto ColorEx (es decir, el tipo de la propiedad) en su representacón textual y viceversa. En realidad, la distribución de las proiedades en filas es responsabilidad de la clase base. Por lo demás, esta clase es muy similar a cualquier otra derivada de TypeConverter:
Public Class ColorConverterEx : Inherits ExpandableObjectConverter 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(ColorEx) 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 ColorEx Then 'Este procedimiento obtiene un tipo ColorEx de value 'y lo convierte a un tipo String con el formato: 'Color [Transparencia=255, Rojo=10, Verde=248, azul=243] 'que será la cadena de texto que aparecerá 'en la "celda madre" de la propiedad 'Esta cadena de texto no se puede editar directamente en el PropertyGrid 'sino que se actualiza ella sola cuando el usuario modifica 'los valores de las propiedades desplegadas en filas 'del tipo ColorEx de la propiedad ColorOjosEx 'Concluyendo: este procedimiento lee el tipo ColorEx de la propiedad 'y lo escribe transformado en String en el PropertyGrid Dim cX As ColorEx If value Is Nothing Then cX = New ColorEx(255, 255, 255, 255) Else cX = CType(value, ColorEx) End If Dim s As String s = String.Format("Transparencia={0}, ", cX.Transparencia) s &= String.Format("Rojo={0}, ", cX.Rojo) s &= String.Format("Verde={0}, ", cX.Verde) s &= String.Format("azul={0}", cX.Azul) s = String.Format("Color [{0}]", s) Return s 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 'Este procedimiento obtiene un tipo String de value con el formato: 'Color [Transparencia=255, Rojo=10, Verde=248, azul=243] 'y lo convierte a un tipo ColorEx 'Al actuar el usuaro sobre los valores de las propiedades 'del tipo ColorEx de la propiedad ColorOjosEx 'los valores representados en la mencionada cadena varían en consonancia. 'De hecho, esta cadena contiene información suficiente 'para generar un objeto ColorEx que contenga los valores 'editados por el usuario. 'En resumidas cuentas, este procedimiento lee la susodicha cadena de tipo String 'y con esa información genera un objeto ColorEx 'y se lo entrega a la propiedad ColorOjosEx If TypeOf value Is String Then Dim i As Short, s(), t(), u(3) As String Dim cx As ColorEx = New ColorEx() s = Split(value, ",") For i = 0 To 3 t = Split(s(i), "=") u(i) = Trim(t(1)) Next cx.Transparencia = Byte.Parse(u(0)) cx.Rojo = Byte.Parse(u(1)) cx.Verde = Byte.Parse(u(2)) cx.Azul = Byte.Parse(u(3)) Return cx End If Return MyBase.ConvertFrom(context, culture, value) End Function End ClassAquí tenemos nuestra propiedad repartida en varias filas:
Quizá el lector se pregunte porqué cuando modifica el valor de la propiedad Color toda la propiedad ColorOjosEx asume el cambio, mientras que, si modifica el valor de las propiedades Transparencia, Rojo, Azul o Verde, nada sucede y el valor de ColorOjosEx no se altera.
Observe el miembro Color de la Clase ColorEx. Cuando el código cliente escribe un valor en la propiedad Color, es decir, asigna un valor a value dentro de Set, el código de la propiedad escribe a su vez en las propiedades Transparencia, Rojo, Azul y Verde, con lo que el objeto de tipo ColorEx queda modificado. Esta modificación la lee la clase conversora y genera la representación textual del nuevo valor, que deposita en la celda derecha de la propiedad "madre" ColorOjosEx.
Cuando el código cliente asigna un nuevo valor a la propiedad Transparencia, Rojo, Azul o Verde, simplemente, y aunque suene tautológico, varía el valor de una variable privada sin prácticamente repercusión inmediata en las demás propiedades ni en el objeto en sí, de ahí que los demás miembros no se alteren. Para solventar este pequeño escollo recurriremos otra vez al evento PropertyValueChanged más arriba descrito. Si detectamos que el cambio de valor ocurre en las propiedades Transparencia, Rojo, Azul o Verde, entonces nos valdremos del siguiente truco para que el cambio afecte a todo el objeto:
Private Sub PropertyGrid1_PropertyValueChanged(ByVal sender As Object, _ ByVal e As System.Windows.Forms.PropertyValueChangedEventArgs) _ Handles PropertyGrid1.PropertyValueChanged 'Detectamos un cambio en la propiedad que nos interesa 'comprobando mediante .Label el nombre de la propiedad Select Case e.ChangedItem.Label Case "Transparencia", "Rojo", "Verde", "Azul" '"Extraemos" el objeto del PropertyGrid Dim o As MiAmigo = PropertyGrid1.SelectedObject 'Le asignamos nuevos valores a las propiedades que nos interesen 'o simplemente creamos un objeto nuevo y se lo asignamos, 'que es precisamente lo que hacemos aquí 'recuperando los valores de las variables privadas 'por medio de las propiedades del objeto extraído o.ColorOjosEx = New ColorEx(o.ColorOjosEx.Transparencia, _ o.ColorOjosEx.Rojo, o.ColorOjosEx.Verde, o.ColorOjosEx.Azul) 'y volvemos a "meter" el objeto en el propertyGrid PropertyGrid1.SelectedObject = o End Select End SubQuizá el el lector también se pregunte si es imprescindible transformar el tipo de la propiedad en su reperesentación textual y viceversa. Si se trata sólo de cuatro valores, como en este caso, aporta elegancia, pero si se trata de un tipo muy complejo, expresar todas sus propiedades en una línea puede resultar muy engorroso y no tan elegante. Efectivamente, el lector tiene toda la razón. La solución, además, es muy sencilla: es el viejo y eficaz truco de la variable pública. En vez de codificar y decodificar una línea de texto, creamos una variable pública en un módulo público del mismo tipo que la propiedad y almacenamos en ella el objeto editado por el usuario. En vez de la representación textual del objeto, escribimos en la celda una frase informativa o un mensaje para el usuario o lo que se nos ocurra. La implementación de la clase convertidora es más sencilla porque, en realidad, no hay nada que convertir:
Public Class ColorConverterEx : Inherits ExpandableObjectConverter 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(ColorEx) 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 ColorEx Then Dim cx As ColorEx If value Is Nothing Then cx = New ColorEx(255, 255, 255, 255) Else cx = CType(value, ColorEx) End If 'En vez de convertirlo a tipo texto lo almacenamos en la variable ColorExVar ColorExVar = cx 'Escribimos en la celda un texto informativo o representativo del objeto Return "Frío intenso. Muy por debajo de cero grados." 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 'En vez de decodificar una línea de texto 'recuperamos el objeto almacenado en ColorExVar Return ColorExVar End If Return MyBase.ConvertFrom(context, culture, value) End Function End Class Public Module ColorExMOdule Public ColorExVar As ColorEx End Module
Código de ejemplo del Control PropertyGrid
1-Controles.zip (34.7 KB)
Controles lanza diferentes controles al formulario y los conecta con el PropertyGrid permitiéndole al usuario editar todas sus propiedades. Además los controles pueden arrastrarse y así comprobar que los valores que indican su posición se actualizan automáticamente en el PropertyGrid.
2-Amigo.zip (138 KB)
Amigo crea una clase, la conecta con el PropertyGrid y añade un editor y otras personalizaciones de edición de las propiedades, todas ellas explicadas en esta entrega.