lunes, 24 de noviembre de 2008

Insertando una ProgressBar en un DataGridView


Después de unos cuantos posts orientados hacia el terreno de la metodologías y la gestión de proyectos, con este artículo vamos a volver al campo del desarrollo puro y duro.

El objetivo de este post es mostrar cómo poder insertar un control de tipo ProgressBar en una columna del control DataGridView. Este problema, a priori complicado de solucionar ya que el DataGridView no permite la inserción directa de controles en sus columnas, es relativamente sencillo de abordar si se comprende el modelo de objetos que utiliza el control DataGridView de .Net.

No vamos a entrar en detalle sobre el citado modelo de objetos, únicamente explicaremos que para poder crear nuevas columnas (cómo la nuestra de ProgressBar) necesitamos crear una nueva clase que represente el tipo de columna, y una nueva que represente el tipo de celdas que pertenecen a dicha columna. Para ello basta con crear clases que deriven de DataGridViewColumn (o una de sus clases hijas) y DataGridViewCell (o una de sus clases hijas)

Para crear una columna que permita añadir objetos ProgressBar vamos a crear una clase que derive de DataGridViewImageColumn (después veremos porque concretamente de esta y no de la clase base o de alguna de las otras subclases)


public class DataGridViewProgressColumn : DataGridViewImageColumn
{
public DataGridViewProgressColumn()
{
CellTemplate = new DataGridViewProgressCell();
}
}

En esta clase vemos que en el constructor se asigna una propiedad llamada CellTemplate. Esta propiedad se encarga de definir el tipo de celda que corresponde a la columna, de manera que cuando se tengan que crear nuevas celdas el DataGridView sepa cómo crearla. Cómo podemos ver se esta asignado a la propiedad una nueva instancia del tipo DataGridViewProgressCell, que es una clase que también tenemos que crear. Ahora vamos a ver cómo construir la clase que representará a nuestro nuevo tipo de celda:

class DataGridViewProgressCell : DataGridViewImageCell
{
}

Lo primero es derivar del tipo de celda correspondiente (DataGridViewImageCell, que es el tipo de celda que corresponde a la columna de la que habíamos derivado nuestra ProgressColumn)

Ahora vamos a explicar como funcionará esta clase para poder entender todo el código que va a ir dentro de ella:

La celda mostrará una barra de progreso (en el ejemplo la estándar de Windows, pero ya veréis que será muy fácil modificarla al gusto de cada uno) , que irá actualizándose en base al valor que nosotros le pasemos a la celda. Es decir nosotros asignaremos valores enteros entre 0 y 100 y la barra de progreso se irá actualizando. Parece lógico pero ya veréis que si no de deja claro ahora puede ser complicado entender el siguiente código:

Añadimos los siguientes constructores a la clase:

static Image emptyImage;

static DataGridViewProgressCell()
{
emptyImage = new Bitmap(1, 1, System.Drawing.Imaging.PixelFormat.Format32bppArgb);

}

public DataGridViewProgressCell()

{

this.ValueType = typeof(int);
}


En el constructor estático (o de clase) se inicializa una variable de tipo Image mientras que en el contructor público se indica el tipo de datos de la columna, que será un entero. Este valor será el que se utilizará para recoger el porcentaje que se debe mostrar en la barra de progreso y ahora veremos que utilidad tiene la imagen del cosntructor estático.

El siguiente paso es sobreescribir el método GetFormattedValue. Este método sirve para transformar el valor de entrada de la celda (que en este caso es un entero) en el objeto que espera el DataGridView, que al estar derivando de DataGridViewImageCell es una imagen (Ahora se entiende el porqué de la imagen del constructor estático)


protected override object GetFormattedValue(object value, int rowIndex, ref DataGridViewCellStyle cellStyle, TypeConverter valueTypeConverter, TypeConverter formattedValueTypeConverter,
DataGridViewDataErrorContexts context)
{
return emptyImage;
}



Cómo veis, en lugar de formatear el valor de entrada y devolver la imagen correspondiente que parecía la opción más lógica según lo expuesto arriba en este método devolvemos directamente la imagen vacía, ya que en nuestro caso no se va a utilizar este método para representar el valor del ProgressBar, pero se debe devolver una imagen por consistencia con el tipo de datos de la celda madre (DataGridViewImageCell).

Y finalmente vamos ver cómo tenemos que a sobreescribir el método Paint de la celda. En este método será donde representemos realmente el valor del progreso. Con esto completaremos la clase:

protected override void Paint(System.Drawing.Graphics g, System.Drawing.Rectangle clipBounds, System.Drawing.Rectangle cellBounds, int rowIndex, DataGridViewElementStates cellState, object value, object formattedValue, string errorText, DataGridViewCellStyle cellStyle, DataGridViewAdvancedBorderStyle advancedBorderStyle, DataGridViewPaintParts paintParts)
{
int progressVal;
if (value != null && value.GetType() == typeof(Int32))
progressVal = (int)value;
else
progressVal = 1;

float percentage = ((float)progressVal / 100.0f);

Brush backColorBrush = new SolidBrush(cellStyle.BackColor);

Brush foreColorBrush = new SolidBrush(cellStyle.ForeColor);

base.Paint(g, clipBounds, cellBounds, rowIndex, cellState, value, formattedValue, errorText, cellStyle, advancedBorderStyle, (paintParts & ~DataGridViewPaintParts.ContentForeground));

const int margin = 4;

ProgressBar pb = new ProgressBar();
pb.Height = cellBounds.Bottom - cellBounds.Top - (margin * 2);
pb.Width = cellBounds.Right - cellBounds.Left - (margin * 2);
pb.Value = progressVal;

Bitmap bmp = new Bitmap(pb.Width, pb.Height);

pb.DrawToBitmap(bmp, pb.ClientRectangle);

g.DrawImage(bmp, new Point(cellBounds.X + margin, cellBounds.Y + margin));
}


Lo que estamos haciendo aquí es que cada vez que se llama al evento Paint de la celda se actualiza el estado de la variable ProgressBar interna de la misma y se "pinta" el control dentro de la celda (mediante GDI). Ahora vemos cómo a partir de un entero de entrada tenemos el control ProgressBar en pantalla. El utilizar GDI para "pintar" el control aporta mucha flexibilidad ya que permite darle la apariencia que se quiera: Por ejemplo en lugar de utilizar un control ProgressBar interno y "ver su imagen" podríamos dibujar el resultado del progreso de la manera que mejor nos pareciera, con los colores que mas nos gustasen, con efectos, con texto, etc...

Ahora que ya tenemos las clases preparadas vamos a ver cómo utilizarlas en un DataGridView de manera que podamos visualizar el control en las diferentes filas que se añadan al mismo.

En el ejemplo vamos a suponer que creamos el DataGridView en tiempo de diseño (aunque se podría hacer en tiempo de ejecución). Lo primero es arrastrar el control DataGridView a nuestro formulario y visualizar la pantalla de propiedades del control.


Arrastramos el DataGridView al formulario


Pantalla de propiedades



En esta pantalla definimos las columnas que tendrá nuestro DataGridView. Pulsando el botón Add aparece una pantalla donde se indica entre otras cosas el tipo de la columna y aquí ya podemos ver que en la lista aparece un nuevo tipo, DataGridViewProgressBarColumn, que será el que seleccionaremos para la columna que tiene que mostrar la barra de progreso (Para que el nuevo tipo de columna aparezca hay que tener acceso al espacio de nombres donde hemos definido la clase desde el formulario).



Añadiendo la columna de progreso

Cuando hayamos añadido las columnas necesarias (en el ejemplo añadiremos únicamente dos) volvemos al formulario y creamos un objeto BindingSource arrastrándolo hacia el mismo. Con este objeto implementaremos el enlace entre los datos y el DataGridView.

En la pantalla deberíamos tener algo parecido a esto:


DataGridView con las dos columnas y el objeto BindingSource

El siguiente paso es crear un DataSet que será el que contenga los datos. Este DataSet podríamos extraerlo de una base de datos, de un servicio Web o de cualquier otro origen de datos, en nuestro ejemplo lo construiremos y lo llenaremos programáticamente.

private void InitializeDataSet()
{
//Creamos el DataSet y definimos las columnas.
customDataSet = new DataSet();
customDataSet.Tables.Add(new DataTable("TablaEjemplo"));
customDataSet.Tables["TablaEjemplo"].Columns.Add(new DataColumn("DSNombre", typeof(string)));
customDataSet.Tables["TablaEjemplo"].Columns.Add(new DataColumn("DSProgreso", typeof(int)));

DataRow row1 = customDataSet.Tables[0].NewRow();
row1["DSNombre"] = "Fila 1";
row1["DSProgreso"] = 25;
customDataSet.Tables[0].Rows.Add(row1);

DataRow row2 = customDataSet.Tables[0].NewRow();
row2["DSNombre"] = "Fila 2";
row2["DSProgreso"] = 35;
customDataSet.Tables[0].Rows.Add(row2);

//Ponemos a false el valor AutoGenerateColumns para utilizar las columnas definidas en vista de diseño
dataGridView1.AutoGenerateColumns = false;

//Enlazamos el Dataset con el DataGrid
bindingSource1.DataSource = customDataSet.Tables[0];
dataGridView1.DataSource = bindingSource1.DataSource;
}


Volvemos a la vista de diseño del Grid e indicamos la relación entre las columnas del DataGrid y las del DataSet (opción Columnas de las propiedades del control)


En la opción DataPropertyName de cada columna ponemos el nombre de la columna del DataSet que enlazaremos. Fijaros que son las columnas definidas en customDataSet

Si ahora ejecutamos el proyecto vemos que aparecen las dos filas creadas y que la barra de progreso de cada una corresponde a los valores dados a las columnas del DataSet. Si quisíeramos modificar ese valor sólo tendríamos que ir cambiando el valor de la celda DSProgreso del DataSet y automáticamente se vería reflejado en el Grid y en la barra de progreso.



Gracias al potente modelo de objetos que .Net define para el DataGridView podemos de manera similar al ejemplo mostrado definir columnas para hospedar casi cualquier tipo de control que necesitemos. En ocasiones habrá que derivar de la columna DataGridViewImageCell cómo en el ejemplo, pero podemos cualquiera de las otras clases base según la funcionalidad que necesitemos insertar en nuestra nueva columna.

Os esperamos con vuestras opiniones, sugerencias sobre el tema o críticas al artículo en los comentarios!

Un saludo!!



3 comentarios:

Genderson dijo...

gracias amigo justo lo q buscada desde hace 1 par d meses. Felicidades x tu blog sigue asi.

Anónimo dijo...

Tio no funciona

Anónimo dijo...

Não funcionou. Faltam detalhes.