jueves, 22 de diciembre de 2011

Personalizar DataGridView (II.1) - Bloquear columnas de solo lectura

Bien, continuando con la personalización del control DataGridView y como ya hemos visto en los artículos anteriores.

Personalizar DataGridView (III) - Cambiar Diseñador.

Continuaremos con el articulo II, en ocasiones se nos presenta la necesidad de saltar "x" columna de este control sin necesidad de que esta este marcada como solo lectura, por el simple hecho de que el usuario no quiere pasar sobre esta columna porque no necesita modificar su contenido, salvo en aquellos casos que si se requiera, generalmente no se quiere atrasar pasando sobre estas columnas.

Para estos casos lo que yo he hecho es agregarle un evento al control al cual he llamado "BeforeFocusColumn" el cual se ejecuta antes de cambiar la columna activa, con la idea de validar la columna que recibirá el foco, validar y en caso de ser necesario cambiar esta columna por la que nosotros necesitemos que reciba el foco.

ejemplo:
 
private void view_BeforeFocusColumn(object sender, BeforeFocusColumnEventArgs e)
{
    if (e.NewColumnIndex == this.colDescripcion.Index)
    {
        if (e.OldColumnIndex == this.colPrecioVenta.Index)
        {
            e.NewColumnIndex = this.colCodigo.Index;
        }
        else
        {
            // descomente esta linea para provocar una excepción al intentar
            // asignar a CurrentCell una celda no visible.
            //e.NewColumnIndex = this.colMarca.Index;
            e.NewColumnIndex = this.colPrecioVenta.Index;
        }
    }
}
 
Como podemos ver en el código de ejemplo, el evento recibe dos parámetros:
object sender: que viene a ser el control que invoca el evento.
BeforeFocusColumnEventArgs e: un nuevo argumento de evento que he creado para pasar la información necesaria al evento.

[C#]
 
public class BeforeFocusColumnEventArgs : EventArgs
{
    private MEPDataGridView Owner = null;
    private int vNewColumnIndex = -1;
    public int OldColumnIndex { get; private set; }
    public int NewColumnIndex 
    {
        get { return vNewColumnIndex; }
        set
        {
            if (this.Owner != null && this.Owner.IsHandleCreated)
            {
                if (value < 0 || value > (this.Owner.Columns.Count - 1))
                    throw new Exception("Invalid current cell index");
                else if (!this.Owner.Columns[value].Visible)
                    throw new Exception("Current cell cannot be set to an invisible cell.");
                this.vNewColumnIndex = value;
            }
        }
    }

    public BeforeFocusColumnEventArgs(int currentColumnIndex, int focusColumnIndex, MEPDataGridView owner)
    {
        this.Owner = owner;
        this.OldColumnIndex = currentColumnIndex;
        this.NewColumnIndex = focusColumnIndex;
    }
}
 
como podemos observar en el constructor de la clase la cual hereda de "EventArgs",  también recibe como parámetro el control propietario, esto con la idea de; hay que validar la columna que el programador indique que recibirá el foco en sustitución de la columna que el control nos indica que recibirá el foco.

esta validación la puede haber hecho desde el evento que ejecuta el evento pero!!!!... al hacer la validación y provocar la excepción la linea donde se marcara la excepción sera dentro de la clase "Program" en el caso de [C#], en la linea "Application.Run(new Form1());" y aquí sera difícil saber donde se ocasiono la excepción; así que, para que se marque la linea que provoca la excepción es mejor provocar esta en la propiedad "NewColumnIndex" cuando su valor es cambiado.

Este nuevo evento "BeforeFocusColumn" se ejecuta o dispara desde del evento "SetCurrentCellAddressCore".

[C#]
 
protected override bool SetCurrentCellAddressCore(int columnIndex, int rowIndex, bool setAnchorCellAddress, bool validateCurrentCell, bool throughMouseClick)
{
    if (!throughMouseClick)
    {
        int currentCellIndex = -1;
        if (this.CurrentCell != null)
            currentCellIndex = this.CurrentCell.ColumnIndex;

        BeforeFocusColumnEventArgs ea = new BeforeFocusColumnEventArgs(currentCellIndex, columnIndex, this);
        this.OnBeforeFocusColumn(ea);
        if (ea.NewColumnIndex != columnIndex)
        {
            // unselect old column index
            this.SetSelectedCellCore(columnIndex, rowIndex, false);
            // select new column index
            this.SetSelectedCellCore(ea.NewColumnIndex, rowIndex, true);
            columnIndex = ea.NewColumnIndex;
        }
    }

    return base.SetCurrentCellAddressCore(columnIndex, rowIndex, setAnchorCellAddress, validateCurrentCell, throughMouseClick);
}
 
Ademas, como podemos ver en el código, nuestro nuevo evento es ejecutado solo si la celda es cambiada por el teclado, en caso de ser cambiada por el ratón (mouse) no se ejecutara permitiendo que la celda reciba el foco, ya que la idea es esta, que el usuario final pueda modificar el contenido de la celda, posicionándose en la celda usando el ratón (mouse) pero no el teclado.



Como siempre, espero les sea de utilidad y no olviden dejar sus comentarios.


Salu2,

miércoles, 21 de diciembre de 2011

Personalizar DataGridView - Actualizaciones


Revisión 12 Diciembre 2011.

  • Mejora en los métodos auxiliares para saltar las columnas marcadas como solo lectura, considerando las columnas marcadas como no visibles, como también si el control no es el primero en recibir el foco al mostrarse su formulario o contenedor.
  • Se cambio la forma de procesar la tecla "ENTER" para avanzar a la siguiente columna, sobre escribiendo el método ProcessCmdKey.
[C#]
 
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
    if (keyData == Keys.Enter && this.EnterMoveNextColumn)
    {
        msg.WParam = (IntPtr)NativeMethods.VK_TAB;
        return base.ProcessCmdKey(ref msg, Keys.Tab);
    }
    return base.ProcessCmdKey(ref msg, keyData);
}
 
Descargar:
Codigo Fuente [C#]

lunes, 12 de diciembre de 2011

Personalizar DataGridView (II) - Bloquear columnas de solo lectura

Personalizar DataGridView - Actualizaciones
Personalizar DataGridView (II.1) - Bloquear columnas de solo lectura.
Personalizar DataGridView (III) - Cambiar Diseñador.

Bien, continuando con el articulo "Personalizar DataGridView (I) - Pintar área vacía", ahora lo que haré es darle al control la funcionalidad de bloquear las columnas cuya propiedad "ReadOnly" se establezca en "true", entiéndase por "Bloquear" el evitar que las columnas cuya propiedad "ReadOnly=true" puedan recibir el foco, ya sea por el teclado o por el ratón (mouse).

Para tal objetivo agregare una nueva propiedad al control la cual llamare "AllowFocusReadOnlyColumns" cuyo valor predeterminado sera "true", en caso de ser "false" las columnas marcadas como solo lectura no recibirán el foco.

También le daré la funcionalidad de poder avanzar a la siguiente columna al presionar la tecla "ENTER" agregando otra propiedad que llamare "EnterMoveNextColumn" cuyo valor predeterminado sera "true"

[C#]
 
private bool vAllowFocusReadOnlyColumns;
private bool vEnterMoveNextColumn;

[DefaultValue(true)]
public bool AllowFocusReadOnlyColumns
{
    get { return vAllowFocusReadOnlyColumns; }
    set { vAllowFocusReadOnlyColumns = value; }
}

[DefaultValue(true)]
public bool EnterMoveNextColumn
{
    get { return vEnterMoveNextColumn; }
    set { vEnterMoveNextColumns = value; }
}
 
[VB.NET]
 
Private vAllowFocusReadOnlyColumns As Boolean
Private vEnterMoveNextColumn As Boolean

<DefaultValue(True)> _
Public Property AllowFocusReadOnlyColumns() As Boolean
    Get 
        Return vAllowFocusReadOnlyColumns
    End Get
    Set (ByVal value As Boolean)
        vAllowFocusReadOnlyColumns = value
    End Set
End Property

<DefaultValue(True)> _
Public Property EnterMoveNextColumn() As Boolean
    Get
        Return vEnterMoveNextColumn
    End Get
    Set (ByVal value As Boolean)
        vEnterMoveNextColumn = value
    End Set
End Property
 
Para agregarle esta funcionalidad al control sobre escribiré el método "WndProc" para escuchar el mensaje del teclado "WM_KEYDOWN" y los mensajes del ratón (mouse) "WM_LBUTTONDBLCLK", "WM_LBUTTONDOWN" y el mensaje "WM_SETFOCUS", en caso de no permitir que las columnas marcadas como solo lectura reciban el foco, estos mensajes no serán procesados por el método base del control. 

[C#]
 
[PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
protected override void WndProc(ref Message m)
{
    switch (m.Msg)
    {
        case NativeMethods.WM_SETFOCUS:
            this.WmSetFocus(ref m);
            break;
        case NativeMethods.WM_LBUTTONDBLCLK:
            this.WmLButtonDblClk(ref m);
            return;
        case NativeMethods.WM_LBUTTONDOWN:
            this.WmLButtonDown(ref m);
            return;
        case NativeMethods.WM_KEYDOWN:
            this.WmKeyDown(ref m);
            return;
    }
    base.WndProc(ref m);
}
 
[VB.NET]
 
<PermissionSet(SecurityAction.Demand, Name:="FullTrust")> _
Protected Overrides Sub WndProc(ByRef m As System.Windows.Forms.Message)
    Select Case m.Msg
        Case NativeMethods.WM_SETFOCUS
            Me.WmSetFocus(m)
        Case NativeMethods.WM_LBUTTONDBLCLK
            Me.WmLButtonDblClk(m)
            Return
        Case NativeMethods.WM_LBUTTONDOWN
            Me.WmLButtonDown(m)
            Return
        Case NativeMethods.WM_KEYDOWN
            Me.WmKeyDown(m)
            Return
    End Select

    MyBase.WndProc(m)
End Sub
 
Y a continuación los proyectos para que los descarguen.

Código fuente [C#]
Código fuente [VB.NET]

En el próximo articulo lo que haré sera agregar un evento al control para poder indicar si la columna puede recibir el foco sin necesidad de que la propiedad "ReadOnly" este establecida en "true", ya que el algunos casos no necesitamos que la celda reciba el foco pero si que pueda ser editada.

Espero sea de utilidad este articulo y no olviden dejar sus comentarios.

Salu2,

domingo, 11 de diciembre de 2011

Personalizar DataGridView (I) - Pintar área vacía

Personalizar DataGridView (II) - Bloquear columnas de solo lectura.
Personalizar DataGridView - Actualizaciones.
Personalizar DataGridView (II.1) - Bloquear columnas de solo lectura.
Personalizar DataGridView (III) - Cambiar Diseñador.

Bien, como lo dice el titulo vamos a personalizar el control DataGridView y lo haremos en varios artículos, en este lo que hare es pintar el área del control que queda vacía cuando tenemos un origen de datos con pocos registros, a esta área la llamare "Empty Area", así que en lugar de que se vea de esta manera.


haré que se vea de esta otra manera.


para esto crearemos nuestro propio control que herede de DataGridView al cual llamare MEPDataGridView, sobre escribiremos los métodos OnCellPainting y OnPaint del control, creare 5 métodos auxiliares "PaintColumnHeader", "PaintIndicatorHeader", "PaintRowIndicator", "PaintCell" y "PaintEmptyArea" y un Argumento "PaintingEventArgs" que hereda de EventArgs que sera pasado a algunos de los métodos auxiliares. 

[C#]
 
public class MEPDataGridView : DataGridView
{        
    
    public MEPDataGridView()        
    {            
        this.SetStyle(ControlStyles.AllPaintingInWmPaint, true);
        this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
        this.SetStyle(ControlStyles.ResizeRedraw, true);
        this.DoubleBuffered = true;        
    }

}
 
[VB.NET]
 
Public Class MEPDataGridView    
    Inherits DataGridView    
 
    Public Sub New()        
        Me.SetStyle(ControlStyles.AllPaintingInWmPaint, True)
        Me.SetStyle(ControlStyles.OptimizedDoubleBuffer, True)
        Me.SetStyle(ControlStyles.ResizeRedraw, True)
        Me.DoubleBuffered = True    
    End Sub

End Class
 

como bien podemos observar en el constructor he activado el DoubleBuffer para reducir el parpadeo (flicker) al dibujar el control cuando este cambie de dimensiones, para mayor referencia ControlStyles.

desde este articulo dibujare los encabezados de las columnas para mantener uniformidad en los colores de estas ya que al tener habilitados los estilos visuales para la aplicación "Application.EnableVisualStyles", es dificil obtener los colores predeterminados con que se pintan los encabezados, y en este caso necesito hacerlo así para mantener uniformidad al pintar la columna indicador en el "Empty Area".

Ahora agregaremos una nueva propiedad al control para indicarle cuando queremos que se pinten las filas en el "Empty Area", ha esta propiedad la llamare "FillEmptyArea" cuyo tipo sera "Boolean" true/false, cuyo valor predeterminado sera "false".

[C#]
 
private bool vFillEmptyArea;    

[DefaultValue(false)]
public bool FillEmptyArea
{
    get
    {
        return this.vFillEmptyArea;
    }
    set
    {
        this.vFillEmptyArea = value;
        if (this.IsHandleCreated)
            this.Invalidate(true);
    }
}
 
[VB.NET]
 
Private vFillEmptyArea As Boolean

<DefaultValue(False)> _
Public Property FillEmptyArea() As Boolean
    Get
        Return Me.vFillEmptyArea
    End Get
    Set(ByVal value As Boolean)
        Me.vFillEmptyArea = value
        If Me.IsHandleCreated Then
            Me.Invalidate(True)
        End If
    End Set
End Property
 

Ademas hare visible la propiedad “AutoGenerateColumns” para poder cambiar esta desde las propiedades del control en tiempo de diseño.

[C#]
 
[Browsable(true), DefaultValue(true)]
public new virtual bool AutoGenerateColumns
{
    get
    {
        return vAutoGenerateColumns;
    }
    set
    {
        vAutoGenerateColumns = value;
        base.AutoGenerateColumns = value;
    }
}
 
[VB.NET]
 
<Browsable(True), DefaultValue(True)> _
Public Overloads Property AutoGenerateColumns() As Boolean
    Get
        Return Me.vAutoGenerateColumns
    End Get
    Set(ByVal value As Boolean)
        Me.vAutoGenerateColumns = value
        MyBase.AutoGenerateColumns = value
    End Set
End Property
 

Ahora sobre escribiremos el evento "OnCellPainting" del control para dibujar los encabezados de las columnas y dibujar el indicador de la fila de cada registro del origen de datos.

[C#]
 
protected override void OnCellPainting(DataGridViewCellPaintingEventArgs e)
{
    base.OnCellPainting(e);
    if (e.RowIndex == -1 && e.ColumnIndex == -1)
    {
        // Método auxiliar para pintar el encabezado de la columna indicador.
        this.PaintIndicatorHeader(e);
    }
    else if (e.RowIndex == -1 && e.ColumnIndex > -1)
    {
        // Método auxiliar para pinta los encabezados de las columnas
        this.PaintColumnHeader(new PaintingEventArgs(e.Graphics, e.CellBounds, e.RowIndex, e.ColumnIndex));
        e.PaintContent(e.ClipBounds);
        e.Handled = true;
     }
     else if (e.ColumnIndex == -1)
     {
        // Método auxiliar para pinta la columna indicador.
        this.PaintRowIndicator(new PaintingEventArgs(e.Graphics, e.CellBounds, e.RowIndex, e.ColumnIndex));
        e.PaintContent(e.ClipBounds);
        e.Handled = true;
     }
}
 
[VB.NET]
 
Protected Overrides Sub OnCellPainting(ByVal e As DataGridViewCellPaintingEventArgs)
    MyBase.OnCellPainting(e)

    If e.RowIndex = -1 And e.ColumnIndex = -1 Then
        ' Método auxiliar para pintar el encabezado de la columna indicador.
        Me.PaintIndicatorHeader(e)
    ElseIf e.RowIndex = -1 And e.ColumnIndex > -1 Then
        ' Método auxiliar para pinta los encabezados de las columnas
        Me.PaintColumnHeader(New PaintingEventArgs(e.Graphics, e.CellBounds, e.RowIndex, e.ColumnIndex))
        e.PaintContent(e.ClipBounds)
        e.Handled = True
    ElseIf e.ColumnIndex = -1 Then
        ' Método auxiliar para pinta la columna indicador.
        Me.PaintRowIndicator(New PaintingEventArgs(e.Graphics, e.CellBounds, e.RowIndex, e.ColumnIndex))
        e.PaintContent(e.ClipBounds)
        e.Handled = True
    End If

End Sub
 
Luego sobre escribimos el evento "OnPaint" para dibujar las filas o lineas para llenar el "Empty Area"

[C#]
 
protected override void OnPaint(PaintEventArgs e)
{
    base.OnPaint(e);
    if (this.FillEmptyArea)
        // Método auxiliar para pintar el área vacía
        this.PaintEmptyArea(e);
}
 
[VB.NET]
 
Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs)
    MyBase.OnPaint(e)
    If Me.FillEmptyArea Then _
        Me.PaintEmptyArea(e) ' Método auxiliar para pintar el área vacía
End Sub
 


y aquí les dejos los proyectos para que los descarguen.

Código fuente [C#]
Código fuente [VB.NET]


En el próximo artículo veremos como evitar que el usuario pueda colocarse sobre las columnas cuya propiedad "ReadOnly" este establecida en "true".


Espero que sea de utilidad este artículo y no olviden dejar sus comentarios.

Salu2,