El control PropertyGrid, eventos y editores personalizados
En el anterior artículo de la serie vimos cómo utilizar las funcionalidades básicas del control PropertyGrid para editar las propiedades de un determinado objeto o colección de objetos. En este artículo os voy a mostrar los eventos más importantes de este control, así como la implementación de editores personalizados que permiten la edición de propiedades de tipos para los que no existe un editor por defecto, como pueden ser las clases desarrolladas por nosotros mismos en nuestro programa.
SI preferís empezar la serie por el principio, este es el enlace al artículo sobre los conceptos básicos del control PropertyGrid.
En este otro enlace os podéis descargar el código fuente del proyecto con los ejemplos de editores de propiedades personalizados, escrito en CSharp con Visual Studio 2013.
La clase Shape
Para empezar, vamos a añadir al proyecto básico una clase que representa distintas formas geométricas. Tenemos la clase abstracta base Shape, que tiene una propiedad Size que indica el ancho y la altura de la figura, otra propiedad de tipo Color que indica el color con el que vamos a dibujar la figura, y un método abstracto DrawShape para dibujarla:
public abstract class Shape
{
protected string _name;
public Shape()
{
ShapeSize = new Size(16, 16);
ShapeColor = Color.White;
}
public Shape(Size sz, Color c)
{
ShapeSize = sz;
ShapeColor = c;
}
[DisplayNameGlobal("NAME_SHAPESIZE")]
[DescriptionGlobal("DESC_SHAPESIZE")]
public Size ShapeSize { get; set; }
[DisplayNameGlobal("NAME_SHAPECOLOR")]
[DescriptionGlobal("DESC_SHAPECOLOR")]
public Color ShapeColor { get; set; }
public abstract void DrawShape(Graphics gr, int x, int y);
public override string ToString()
{
return _name;
}
}
En esta clase también hemos decorado las propiedades con los atributos de globalización para traducirlas y proporcionar ayuda al usuario, ya que también aparecerán en el PropertyGrid para ser editadas directamente.
He reemplazado el método ToString para que en el editor de propiedades aparezca un identificador personalizado de la clase. Si no hacemos esto, por defecto aparecerá el nombre del tipo de la clase.
Ahora derivamos tres clases para distintas figuras geométricas, un círculo, un rectángulo y un triángulo. Por ejemplo, para el círculo:
public class Circle : Shape
{
public Circle()
: base()
{
_name = Resources.SHAPE_CIRCLE;
}
public Circle(Size sz, Color c)
: base(sz, c)
{
_name = Resources.SHAPE_CIRCLE;
}
public override void DrawShape(Graphics gr, int x, int y)
{
Pen p = new Pen(ShapeColor);
gr.DrawEllipse(p,
new Rectangle(x, y, ShapeSize.Width, ShapeSize.Height));
p.Dispose();
}
}
Como en el proyecto básico hemos utilizado un archivo de recursos para establecer varios idiomas para la aplicación, lo aprovechamos también para ofrecer un nombre para la clase traducido al idioma actual.
Controles de usuario para editar las propiedades
A continuación, vamos a crear unos controles especializados en la edición de objetos de tipo Shape. El primero de ellos será una ListBox para mostrar los diferentes tipos de figuras disponibles. Empezamos creando una ListBox de propósito general apropiada para utilizar con un editor de propiedades:
public class ListBoxEditor : ListBox
{
protected object m_oSelection = null;
protected IWindowsFormsEditorService m_iwsService = null;
public ListBoxEditor(object selection, IWindowsFormsEditorService edsvc)
{
m_iwsService = edsvc;
SelectionMode = SelectionMode.One;
BorderStyle = BorderStyle.None;
m_oSelection = selection;
}
public object Selection
{
get
{
return m_oSelection;
}
}
protected override void OnSelectedIndexChanged(EventArgs e)
{
base.OnSelectedIndexChanged(e);
if (SelectedItem != null)
{
m_oSelection = SelectedItem;
}
}
protected override void OnClick(EventArgs e)
{
base.OnClick(e);
if (m_iwsService != null)
{
m_iwsService.CloseDropDown();
}
}
}
El objeto m_oSelection lo necesitamos para guardar la selección de la lista una vez que esta ha sido destruida, y el IWindowsFormsEditorService nos lo proporcionará el editor de la propiedad, y lo utilizaremos para indicar cuándo se puede dar por terminada la selección del usuario, al pulsar éste sobre alguno de los elementos.
A partir de esta lista genérica, podemos derivar nuestra lista especializada en objetos de tipo Shape:
public class ShapeListBoxEditor : ListBoxEditor
{
public ShapeListBoxEditor(object selection,
IWindowsFormsEditorService edsvc)
: base(selection, edsvc)
{
DrawMode = DrawMode.OwnerDrawFixed;
}
protected override void OnDrawItem(DrawItemEventArgs e)
{
base.OnDrawItem(e);
if (e.Index >= 0)
{
Shape shape = Items[e.Index] as Shape;
if (shape != null)
{
Rectangle rect = e.Bounds;
rect.Inflate(-2, -2);
shape.ShapeSize = new Size(rect.Height, rect.Height);
shape.DrawShape(e.Graphics, rect.X, rect.Y);
Font f = new Font("Arial", 9);
e.Graphics.DrawString(shape.ToString(), f,
SystemBrushes.WindowText, rect.Height + 4,
rect.Top +
((rect.Height -
e.Graphics.MeasureString(shape.ToString(),
f).Height) / 2));
f.Dispose();
}
}
}
}
Lo único que añade esta clase es el dibujo personalizado de los objetos Shape, mediante una llamada al método DrawShape.
El otro control que utilizaremos para editar objetos de tipo Shape será un cuadro de diálogo normal y corriente para editar su tamaño y color, implementado en el formulario de clase ShapeDialogBox.
Editores de propiedades personalizados
Con estos elementos ya podemos implementar los editores de propiedades personalizados. Para ello, debemos derivar una clase de UITypeEditor. El primer editor se encargará de seleccionar un elemento de la clase Shape, mostrando la lista desplegable con todas las opciones disponibles, en la clase ShapeTypeEditor.
Empezamos por reemplazar el método GetEditStyle, para indicar que el editor va a desplegar un control de usuario:
public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
{
return UITypeEditorEditStyle.DropDown;
}
También indicaremos que el control se encargará de pintar él mismo el valor seleccionado, reemplazando el método GetPaintValueSupported y devolviendo true:
public override bool GetPaintValueSupported(ITypeDescriptorContext context)
{
return true;
}
Para dibujar la figura, reemplazaremos el método PaintValue:
public override void PaintValue(PaintValueEventArgs e)
{
Shape shape = e.Value as Shape;
if (shape != null)
{
Rectangle rect = e.Bounds;
rect.Inflate(-2, -2);
shape.ShapeSize = new Size(rect.Height, rect.Height);
shape.DrawShape(e.Graphics, rect.X + 3, rect.Y);
}
}
Y para desplegar la lista con las distintas opciones y editar el valor de la propiedad, debemos reemplazar también el método EditValue:
public override object EditValue(ITypeDescriptorContext context,
IServiceProvider provider, object value)
{
IWindowsFormsEditorService edSvc =
(IWindowsFormsEditorService)provider.GetService
(typeof(IWindowsFormsEditorService));
Shape shape = value as Shape;
if (edSvc != null)
{
ShapeListBoxEditor dropdown = new ShapeListBoxEditor(shape, edSvc);
dropdown.Items.Add(_shapes[0]);
dropdown.Items.Add(_shapes[1]);
dropdown.Items.Add(_shapes[2]);
edSvc.DropDownControl(dropdown);
return dropdown.Selection;
}
return value;
}
El parámetro context proporciona información sobre el objeto y la propiedad que estamos editando. El objeto lo podemos obtener de la propiedad Instance, y el descriptor de la propiedad de PropertyDescriptor. El valor actual de la propiedad se pasa en el parámetro value. Este método debe devolver el nuevo valor de la propiedad seleccionado por el usuario. Obtenemos una instancia de IWindowsFormsEditorService que nos servirá para desplegar la lista de objetos Shape en el lugar apropiado del control y la desplegamos llamando al método DropDownControl, hasta que el control dé por terminada la edición.
Ahora ya podemos añadir al objeto DemoObject una propiedad de tipo Shape:
[CategoryGlobal("CAT_CUSTOMEDITORS")]
[DisplayNameGlobal("NAME_SHAPETYPEPROPERTY")]
[DescriptionGlobal("DESC_SHAPETYPEPROPERTY")]
[RefreshProperties(RefreshProperties.All)]
[Editor(typeof(ShapeTypeEditor), typeof(UITypeEditor))]
public Shape ShapeTypeProperty { get; set; }
Para indicar que esta propiedad tiene un editor personalizado, la decoraremos con el atributo Editor, en el cual indicaremos el tipo del editor y su clase base. Este es el aspecto que presenta nuestro editor en el control:
Ahora vamos a construir otro editor personalizado para editar un objeto de tipo Shape usando un cuadro de diálogo. Como en el caso anterior, derivaremos la clase ShapeEditor de UITypeEditor y reemplazaremos el método GetEditStyle para indicar que presentaremos un cuadro de diálogo modal al usuario:
public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext
context)
{
return UITypeEditorEditStyle.Modal;
}
El cuadro de diálogo lo mostraremos en el método reemplazado EditValue:
public override object EditValue(ITypeDescriptorContext context,
IServiceProvider provider, object value)
{
Shape shape = value as Shape;
if (shape != null)
{
ShapeDialogBox dlg = new ShapeDialogBox();
dlg.SelectedObject = shape;
dlg.ShowDialog();
return shape;
}
return value;
}
Y ya podemos añadir otra propiedad a la clase DemoObject indicando el editor personalizado con el atributo Editor, como antes:
[CategoryGlobal("CAT_CUSTOMEDITORS")]
[DisplayNameGlobal("NAME_SHAPEPROPERTY")]
[DescriptionGlobal("DESC_SHAPEPROPERTY")]
[Editor(typeof(ShapeEditor), typeof(UITypeEditor))]
public Shape ShapeProperty { get; set; }
Y este es el cuadro de diálogo que aparece cuando editamos la propiedad pulsando el botón de edición de la propiedad:
Eventos del control PropertyGrid
Para terminar, vamos a ver el evento PropertyValueChanged del control PropertyGrid, que se dispara cuando el usuario cambia el valor de alguna de las propiedades:
private void pgControl_PropertyValueChanged(object s,
PropertyValueChangedEventArgs e)
{
if (e.ChangedItem.PropertyDescriptor.Name == "ShapeTypeProperty")
{
Shape shape = e.ChangedItem.Value as Shape;
if (shape != null)
{
_object.ShapeProperty =
(Shape)shape.GetType().GetConstructor
(Type.EmptyTypes).Invoke(null);
}
}
}
Los datos de la propiedad modificada se encuentran en los argumentos del evento, en concreto en la propiedad ChangedItem. El nombre de la propiedad se encuentra en el miembro Name. Cuando el usuario modifique la propiedad ShapeTypeProperty, construiremos un objeto Shape del tipo seleccionado y se lo asignaremos a la propiedad ShapeProperty. Como ShapeTypeProperty está decorado con el atributo RefreshProperties, el valor se refrescará automáticamente en el control. Podemos conseguir el mismo efecto llamando al método Refresh del PropertyGrid.
En el próximo artículo mostraré cómo utilizar conversores de tipo con el control PropertyGrid.