banner de las colaboraciones

 

El Control PropertyGrid

Entrega 1 de 2

19 Abril 2003
Cipriano Valdezate Sayalero

Link a la entrega 2

 

Indice

 

.

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 Class

La 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:


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.ToString

Esta 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 Class

Esta 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 Property

No 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 Module

La 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 Sub

El ú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í)

Editor de colores predeterminado
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:

  1. 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".

  2. 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.

  3. 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".

  4. 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 Class
Punto 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 Class

La 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 Property

El 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 Class

El 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 Property

Ahora 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 Class

Para 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 ColorEx

Debemos, 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 Class

Los 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 Class

Aquí 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 Sub

Quizá 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.


la Luna del Guille o... el Guille que está en la Luna... tanto monta...