Interfaz de Usuario Basica

Resulta indudable que una parte esencial de un programa de ajedrez es su interfaz gráfica. Bien es cierto que, en los últimos tiempos, los desarrolladores de este tipo de software han obviado un poco esta parte del desarrollo y se han apoyado en frontends del estilo de WinBoard/XBoard o más recientemente Arena. Estos programas ofrecen una interfaz gráfica completa con una gran cantidad de opciones de configuración visuales y permiten que nuestro programa o motor de ajedrez se comunique con él mediante un cierto protocolo. De esta forma, durante el desarrollo de nuestro programa podemos "olvidarnos" (lo pongo entre comillas porque, como mínimo, siempre tendremos que preocuparnos de la comunicación con la interfaz, sea o no propia) de todas las funciones visuales del mismo y concentrarnos en la lógica específica del juego.

Esta web, sin embargo, no se centra únicamente en la programación de software de ajedrez, sino que fue pensada para incluir todo tipo de información sobre programación bajo la plataforma .NET y por ello vamos a incluir también en esta serie de artículos el desarrollo de una interfaz gráfica relativamente elaborada para nuestro programa. En este primer artículo sobre el tema, vamos a construir un control personalizado para representar el tablero de ajedrez y la posición de las piezas.

Alternativas de representación

En nuestro caso concreto, se nos presentan dos alternativas claras a la hora de representar los elementos visuales de nuestro programa, entre ellos, el tablero y las piezas del juego.

  • Dibujar directamente todos los elementos visuales sobre la ventana principal de la aplicación.
  • Crear un control personalizado con todos los elementos visuales y añadirlo a la ventana principal de la aplicación.

La primera opción, aun siendo más sencilla de implementar dado que sólo habría que modificar la lógica de dibujo de la ventana en la que queramos incluir el tablero, presenta varios inconvenientes claros. Por un lado, estaríamos mezclando en el código del programa toda la lógica encargada de la presentación de los elementos visuales con la lógica específica del juego (lo que, entre otras cosas, nos haría más complicado el mantenimiento de la aplicación) y por otro lado no podríamos utilizar esa misma representación en otras ventanas del programa (por ejemplo, un caso frecuente en algunos programas de ajedrez es mostrar previsualizaciones de partidas guardadas en tableros más pequeños) sin repetir gran parte del código o al menos sin tener que adaptarlo a las nuevas situaciones.

El desarrollo de un control de usuario encargado de la representación visual del tablero, que se preocupe de todas las funciones de dibujo básicas como el escalado o el refresco del control, e incluso de operaciones como la traducción de los clicks del ratón sobre el control a coordenadas de tablero (fila y columna), nos hará la vida mucho más fácil durante el resto de la implementación del programa y nos evitará algunos inconvenientes como los ya comentados.

Tipos de controles personalizados en .NET

La plataforma .NET introduce numerosas posibilidades a la hora de crear componentes de usuario totalmente personalizados, y entre ellos, controles visuales para aplicaciones de escritorio, como es nuestro caso. Así, además de todos los controles ya existentes que se proporcionan con la propia plataforma, vamos a tener la posibilidad de crear nuevos controles que se adapten a nuestras necesidades específicas y que nos proporcionen la funcionalidad necesaria para nuestra aplicación.

Las tres alternativas principales para el desarrollo de un control personalizado son las siguientes:

  • Crear un nuevo control como composición de varios controles ya existentes que interactúen entre sí de una forma determinada. De esta forma, el control resultante no será más que un contenedor de varios controles que implementan una lógica concreta. Así, por ejemplo, podríamos crear un control Calculadora compuesto por varios botones ( Button ) y un cuadro de texto ( TextBox ) estándar dispuestos de una forma determinada y asociarles la lógica de una calculadora, tal y como si desarrolláramos una aplicación de escritorio tradicional. Para crear un control de este tipo heredaremos nuestra clase de UserControl .

  • Heredar el nuevo control de uno ya existente, de forma que podamos extender o modificar su funcionalidad. Así, podríamos heredar, por ejemplo, de la clase Button para crear un botón con alguna funcionalidad específica. La ventaja principal de este método es que podremos utilizar toda la funcionalidad del control del que hemos heredado y sólo tendremos que añadir las funciones extra específicas de nuestro control.

  • Crear un control completamente personalizado, encargándonos de implementar toda la lógica necesaria, incluido el dibujo completo del control, la gestión de eventos, etc. Para crear un control de este tipo heredaremos nuestra clase directamente de de la clase Control .

En nuestro caso, queremos crear un control que represente en pantalla un tablero de ajedrez con sus piezas de juego. Esta representación no se puede conseguir obviamente como agrupación de controles estándar ni tiene demasiada funcionalidad común con ningún control proporcionado por la plataforma, por lo que se ha elegido el tercer método comentado para el desarrollo de nuestro tablero de ajedrez. A continuación se comentarán los pasos básico para crear un control de este tipo.

Creación de un control personalizado

Como hemos dicho, un control completamente personalizado heredará de la clase Control proporcionada por .NET. De esta clase se hereda tan sólo la funcionalidad básica de un control, como por ejemplo la posición y tamaño del control o la gestión de los eventos principales de ratón y teclado. Sin embargo, no se proporciona ninguna lógica para el dibujo del control en la ventana, por lo que deberemos implementarla nosotros como paso principal en el desarrollo de un control de este tipo.

Veamos los pasos para crear un control personalizado desde Visual Studio .NET:

  1. Crear un nuevo proyecto de tipo Biblioteca de controles de Windows.
  2. Desde el menú proyecto seleccionar la opción Agregar componente… y después elegir la plantilla Control personalizado desde la ventana Agregar nuevo elemento.
  3. Indicar un nombre para la nueva clase y pulsar aceptar.
  4. Pulsar en el enlace “Haga clic aquí para cambar a la vista de códigos” para acceder al código del control. De esta forma podremos añadir la información necesaria a la clase e implementar los métodos necesarios para nuestro control.
  5. Como ya hemos dicho, además de toda la funcionalidad específica del control, tendremos que implementar también la lógica de dibujo del mismo. Esto se hará dentro del método OnPaint() se ha creado automáticamente al añadir el control. Este método recibe como argumento un objeto de tipo PaintEventArgs que, entre otros, posee una propiedad llamada Graphics que utilizaremos para llamar a todas las funciones gráficas disponibles para el dibujo del control. De esta forma, por ejemplo, para dibujar un rectángulo procederíamos de la siguiente forma:
protected override void OnPaint(PaintEventArgs pe) 
{
    //Dibuja un rectangulo 

    pe.Graphics.DrawRectangle(...); 

    // Llamando a la clase base OnPaint 
    base .OnPaint(pe); 
}

Control NChessBoard

A continuación detallaremos la implementación del control de usuario desarrollado para la aplicación NChess, llamado NChessBoard.

Atributos y propiedades

Toda la información necesaria para la representación de nuestro tablero de ajedrez se incluirá como atributos NChessBoard.

tablero

Queremos que nuestro tablero de ajedrez tenga un aspecto similar al que aparece en la imagen superior, pero siempre dando la posibilidad al usuario de personalizarlo todo lo posible. Por tanto, hay ciertos datos referentes a la apariencia visual del tablero que deberemos almacenar en nuestra clase, entre ellos:

Dato Variable Tipo Descripción
Margen margen float Ancho del margen. Espacio entre el borde del control y el borde del tablero.
Casillas blancas colorBlancas Color Color de las casillas blancas.
Casillas negras colorNegras Color Color de las casillas negras.
Fuente leyenda fuente Font Tipo de letra para las marcas de fila y columna.

Debemos almacenar las imágenes de las piezas para su posterior dibujo sobre el tablero:

Dato Variable Tipo Descripción
Piezas blancas imgBlancas Image[] Array de imágenes para las piezas blancas.
Piezas negras imgNegras Image[] Array de imágenes para las piezas negras.

Aparte de la información estrictamente visual, el elemento más importante de esta clase será el array donde se almacenará la posición de las piezas. Cada pieza se representará mediante un número que identifique su tipo y color. En nuestro caso, la representación elegida será la siguiente:

Piezas Blancas Código   Piezas Negras Código
Peón
1
  Peón
-1
Torre
2
  Torre
-2
Caballo
3
  Caballo
-3
Alfil
4

 

Alfil
-4
Dama
5
  Dama
-5
Rey
6
  Rey
-6

Todos estos valores se declararán como constantes de la clase. Por otro lado, dado que sólo necesitamos 6 valores para cada color de pieza utilizaremos un array de bytes con signo (sbyte ) para almacenar la posición de las mismas. La declaración de atributos privados de la clase quedará por tanto de la siguiente forma:

//Ancho del margen exterior del tablero (Donde se dibujaran las etiquetas)
private float margen = 30.0F;

//Color cuadros blancos
private Color colorBlancas = Color.LightCyan;


//Color cuadros negros
private Color colorNegras = Color.DarkCyan;

//Imagenes piezas blancas
private Image[] imgBlancas; 

//Imagenes piezas negras
private Image[] imgNegras;

//Posicion de las piezas en el tablero

private sbyte[,] tab;


//Constantes
public const sbyte PB = 1; //Peon Blanco
public const sbyte TB = 2; //Torre Blanca

//...
public const sbyte DN = -5; //Dama Negra
public const sbyte RN = -6; //Rey Negro

Algunos de estos atributos privados se expondrán públicamente como propiedades para que se pueda acceder a ellos en tiempo de diseño desde la paleta de propiedades de Visual Studio .NET o en tiempo de ejecución para modificar su valor. Un ejemplo será el color de las casillas:

[Description("Obtiene o establece el color de las casillas blancas")]
public Color ColorBlancas 
{
    get 
    {
        return colorBlancas; 
    }
    set 
    {
        colorBlancas = value; 
        Invalidate();
    }
}

El atributo Description que aparece antes de la declaración de la propiedad indica la descripción que se mostrará en la paleta de propiedades del diseñador de Visual Studio:

propiedades

Otro detalle a destacar es la llamada a la función Invalidate() después de modificar el color de las casillas. Esto hará que el control vuelva a dibujarse cada vez que se modifique este dato, reflejando así el cambio en pantalla.

Dibujo del control

Una vez que nuestra clase ya dispone de toda la información necesaria para el dibujo del control podemos proceder a la implementación del método OnPaint (), que será llamado automáticamente cada vez que el control necesite ser dibujado.

El dibujo del tablero implica una serie de pasos que se enumeran a continuación:

  1. Cálculo de las dimensiones de una casilla.
  2. Dibujo del marco exterior del control.
  3. Dibujo de las casillas.
  4. Dibujo del marco del tablero.
  5. Dibujo de las piezas.
  6. Dibujo de las casillas seleccionadas.
  7. Dibujo de la numeración de filas y columnas.

Vayamos por partes:

1. Cálculo de las dimensiones de una casilla

El ancho de una casilla individual del tablero lo basaremos siempre en la longitud más corta entre ancho y alto del control.De esta forma nos aseguramos de que la representación del control nunca sobrepasa los límites del mismo. Así, por ejemplo, si el control es más ancho que alto, las dimensiones de una casilla se calculará como el alto del control menos los márgenes, dividido entre ocho (el número de casillas de un tablero de ajedrez). Por tanto, el tablero se ajustaría en este caso al alto del control.

2. Dibujo del marco exterior del control

Este elemento no presenta ninguna dificultad y consiste en dibujar un rectángulo que ocupe todo el espacio del control mediante el método DrawRectangle() .

3. Dibujo de las casillas

Las casillas se dibujarán una a una comenzando por la casilla superior izquierda y avanzando por filas hasta terminar por la inferior derecha, y su color (blanco o negro) se calculará dependiendo de si el número de casilla es par o impar. De esta forma nos aseguramos que se alternan las casillas de ambos colores. Para el dibujo se usa el método FillRectangle() , que dibuja rectángulos rellenos de un color determinado.

4. Dibujo del marco del tablero

Este elemento tampoco presenta ningún problema, y sólo habrá que tener en cuenta que su dimensión, al igual que en el caso de las casillas, depende del lado mayor del control (ancho o alto).

5. Dibujo de las piezas

El dibujo de las piezas sí presenta algún detalle interesante. Las imágenes de las piezas tienen inevitablemente un color de fondo. Sin embargo, nosotros no queremos que se muestre este color sino el que hayamos elegido para nuestro tablero, y además dependerá de la casilla en la que se encuentre la pieza. En otras palabras, nosotros necesitamos que el fondo de las piezas sea transparente. Para ello haremos en primer lugar que todas las imágenes de las piezas tengan el mismo color, en nuestro caso el azul puro, y a la hora de dibujarlas sobre el tablero indicaremos a las funciones de dibujo que el color de fondo elegido no se represente. Esto se consigue mediante el uso de atributos de imagen (clase ImageAttribute) y definiendo sobre éstos un rango de colores que se harán transparentes mediante el método SetColorKey(). En nuestro caso, la implementación queda como sigue:

//Establece el ColorKey (color transparente)
Color lowerColor = Color.FromArgb(0,0,1);
Color upperColor = Color.FromArgb(0,0,255);
ImageAttributes imageAttr = new ImageAttributes();

imageAttr.SetColorKey(lowerColor,upperColor,ColorAdjustType.Default);

//Dibujo de las piezas
for(int i=0; i<8; i++)

for(int j=0; j<8; j++)
dibujaPieza(pe,imageAttr,ladoCasilla,i,j);

//...

//Dibuja una pieza en el tablero
private void dibujaPieza(System.Windows.Forms.PaintEventArgs e, 
ImageAttributes imageAttr, float ladoCasilla, 

int fila, int columna)
{
    GraphicsUnit unidades = GraphicsUnit.Pixel;

    //Se define el rectangulo de destino de la imagen mediante tres puntos
    PointF sup_izq = new PointF(margen+ladoCasilla*columna, margen+ladoCasilla*fila);
    PointF sup_der = new PointF(margen+ladoCasilla*columna+ladoCasilla, margen+ladoCasilla*fila);
    PointF inf_izq = new PointF(margen+ladoCasilla*columna, margen+ladoCasilla*fila+ladoCasilla);
    PointF[] rectDest = {sup_izq, sup_der, inf_izq};

    if(tab[fila,columna]>0) //Pieza blanca

    {
        RectangleF rectOrig = 
        imgBlancas[tab[fila,columna]-1].GetBounds(ref unidades);

        e.Graphics.DrawImage(imgBlancas[tab[fila,columna]-1],
        rectDest,
        rectOrig,
        unidades,
        imageAttr);
    }
    else if(tab[fila,columna]<0) //Pieza negra
    {
        RectangleF rectOrig = 
        imgNegras[-tab[fila,columna]-1].GetBounds(ref unidades);

        e.Graphics.DrawImage(imgNegras[-tab[fila,columna]-1],
        rectDest,
        rectOrig,
        unidades,
        imageAttr);
    }
}

Por su parte, el escalado y representación de las imágenes de las piezas se realiza utilizando una de las sobrecargas del método DrawImage() que recibe, entre otras cosas, la zona de la imagen que se quiere representar (variable rectOrig, que en nuestro caso equivale a la imagen completa obtenida mediante GetBounds()) y la zona del control a la que se quiere ajustar la imagen (variable rectDest, que se calcula según la casilla en la que se quiera representar la imagen de la pieza).

6. Dibujo de las casillas seleccionadas

En nuestro tablero de ajedrez queremos tener la posibilidad de resaltar las casillas seleccionadas cuando hagamos clic sobre el tablero para hacer un movimiento concreto. Podrá haber hasta dos casillas seleccionadas (origen y destino del movimiento), pero los estados posibles del tablero en un momento determinado de juego pueden ser tres:

  • Ninguna casilla seleccionada
  • Una casilla seleccionada (sólo se ha seleccionado la casilla de origen de un movimiento)
  • Dos casillas seleccionadas (ya se han seleccionado las casillas de origen y destino de un movimiento)

Para controlar este comportamiento definiremos un nuevo atributo privado llamado estadoSel que indique el estado actual de selección en que se encuentra el tablero y que podamos consultar a la hora de dibujar las casillas seleccionadas. A parte de esto, el dibujo de las casillas seleccionadas no presenta ningún otro inconveniente, dado que indicaremos la selección simplemente con un rectángulo alrededor de la casilla correspondiente.

7. Dibujo de la numeración de filas y columnas

La numeración de las casillas la dibujaremos mediante la función de dibujo de cadenas proporcionada por GDI, llamada DrawString(). El dibujo de una cadena lo realizaremos normalmente indicando la cadena a dibujar, un rectángulo donde hacerlo y un formato determinado para la cadena, donde podremos especificar la alineación, vertical y horizontal, dentro del rectángulo de destino indicado. El formato se define mediante un objeto de tipo StringFormat. Esta clase tiene dos propiedades para definir la alineación de una cadena dentro de un rectángulo determinado, Alignmentpara la alineación horizontal y LineAlignment para la alineación vertical. Ambas propiedades pueden tomar los valores Center , Far y Near, que harán que la cadena se centre en el rectángulo, o se ajuste al inicio o final del rectángulo, según se haya definido éste. En nuestro caso, y como ejemplo, la numeración horizontal de las casillas tiene el siguiente aspecto:

//Formato de cadena
StringFormat sf = new StringFormat();

//Formato de cadena para las etiquetas horizontales
sf.Alignment = StringAlignment.Center;
sf.LineAlignment = StringAlignment.Near;

//Letras (Horizontal)
for(int i=0; i<8; i++)
{
    g.DrawString(Chr(65+i),
    fuente,
    numeracionBrush, 
    new RectangleF(margen + ladoCasilla*i + ladoCasilla/2 - 10, 
    margen + ladoCasilla*8 + 5, 20, 20), sf);
}

Con los pasos anteriores ya tendríamos terminada la representación completa del control y podríamos añadirlo a la ventana principal de nuestra aplicación. Sin embargo, aún es posible añadir un pequeño detalle más para evitar el parpadeo originado al refrescar el dibujo del tablero. Para ello activaremos lo se denomina doble buffer, que provocará un refresco mucho más suave del control, e inapreciable en la mayoría de los casos.

Activar el doble buffer de dibujo

Para activar esta característica estableceremos un estilo de dibujo diferente al predeterminado mediante el método SetStyle() , al que llamaremos desde el constructor del control. Este método puede recibir bastantes opciones de representación, de las que usaremos las siguientes:

  • DobleBuffer: Activa el doble buffer de pantalla.
  • UserPaint: Indica que es el propio control, y no el sistema operativo, el que se dibuja a sí mismo. Es necesario activar este flag para establecer completamente el doble buffer.
  • AllPaintingInWmPaint: hace que el control pase por alto el mensaje de ventana WM _ERASEBKGND , es decir, el repintado del fondo, para reducir aún más el parpadeo. Debe activarse junto con UserPaint y DobleBuffer para activar completamente la opción de doble buffer.
  • ResizeRedraw: Hace que el control se redibuje cada vez que se modifica su tamaño.

Por último, además de establecer el nuevo estilo de dibujo, habrá que actualizarlo en el control mediante UpdateStyles() . De esta forma, la activación del doble buffer y el repintado al cambiar de tamaño quedaría de la siguiente forma:

//Para evitar parpadeo al repintar la ventana
this.SetStyle(
ControlStyles.DoubleBuffer | 
ControlStyles.UserPaint | 
ControlStyles.AllPaintingInWmPaint |
ControlStyles.ResizeRedraw,
true);

//Se actualiza el estilo de dibujo del control
this.UpdateStyles();

Eventos de tablero

Otra característica que vamos a añadir a nuestro tablero de ajedrez es la posibilidad de comunicar el evento del clic del ratón sobre una casilla determinada. La plataforma .NET ofrece la posibilidad de hacer esto fácilmente mediante los siguientes pasos:

1. Definición de la clase que almacenará la información del evento

En nuestro caso, esta clase será muy sencilla debido a que tan sólo debe almacenar la fila y la columna correspondiente a la casilla sobre la que se ha pulsado, proporcionar propiedades públicas para ambos datos y escribir un constructor que los reciba como parámetro. Esta clase la llamaremos ClickBoardEventArgs y se muestra su definición más abajo.

2. Declaración de un delegado para el evento

Mediante los delegados indicamos de cierta forma el tipo de llamada que se recibirá cuando se produzca el evento creado, incluyendo los parámetros utilizados. La declaración de este elemento se muestra junto a la definición de la clase ClickBoardEventArgs.

//Clase para almacenar los datos del evento ClickBoardEvent
public class ClickBoardEventArgs : EventArgs
{
    //Atributos privados
    private byte fila, columna;

    //Propiedades publicas

    public byte Fila
    {
        get
        {
            return fila;
        }
    }

    public byte Columna
    {
        get

        {
            return columna;
        }
    }

    //Constructor
    public ClickBoardEventArgs(byte fila, byte columna)
    {
        this.fila = fila;
        this.columna = columna;
    }
}



//Delegado para el evento ClickBoardEvent
public delegate void ClickBoardEventHandler(object sender, ClickBoardEventArgs e);

3. Declaración del evento como atributo de la clase

Como atributo privado de la clase que lanzará el evento, NChessBoard, se declarará el evento a lanzar de la siguiente forma:

//Evento ClickBoardEvent 

public event ClickBoardEventHandler ClickBoard;

4. Lanzar el evento

Por último, redefiniremos el evento OnMouseDown() de nuestro control, y en caso de detectar que se ha hecho clic dentro de los límites del tablero (sin contar los márgenes del control) traduciremos las coordenadas de control a coordenadas de tablero (mediante la función propia toCasilla() ) y lanzaremos el evento ClickBoard pasándole como parámetros la fila y columna calculadas.

protected override void OnMouseDown(MouseEventArgs e)
{
    //Si se ha hecho click dentro de los limites del tablero sin contar el margen 
    //se lanza el evento ClickBoard
    if(clickEnTablero(e.X,e.Y))
    if(ClickBoard != null)
    ClickBoard(this,New ClickBoardEventArgs(
    (byte)toCasilla(e.X,e.Y).X,
    (byte)toCasilla(e.X,e.Y).Y));

    base.OnMouseDown(e);
}

Métodos públicos del control

Además de toda la funcionalidad de dibujo y gestión de eventos, se proporcionarán con el control una serie de métodos públicos para inicializar y manipular el control y la posición de las piezas, que son las funciones que nos serán necesarias cuando desarrollemos nuestra aplicación principal. Entre estas funciones se encuentran algunas como tableroVacio() o posiciónInicial() que nos ayudarán a limpiar de piezas el tablero o a establecer las piezas en su posición inicial; movimiento() , que realizará el movimiento de una pieza de una casilla a otra y refrescará el tablero; seleccionarCasilla() para seleccionar una casilla determinada por su fila y columna, etc. La implementación de estas funciones es bastante sencilla y puede consultarse directamente en el código fuente del control.

Aplicación de ejemplo

Junto con el control se proporciona una aplicación de ejemplo donde se puede experimentar con las opciones disponibles y que puede servir de base para construir la ventana principal de nuestro programa de ajedrez. Esta aplicación de ejemplo es bastante sencilla y lo único que ofrece es la posibilidad de establecer las piezas en su posición inicial, colocar piezas en casillas determinadas o seleccionar mediante el ratón las casillas del tablero. Esto puede dar una idea básica de cómo usar el control desde una aplicación base y cómo manipularlo para adaptarlo a nuestras necesidades.

Descarga

El control desarrollado en este artículo y su programa de prueba puede ser descargado pulsando aquí.

Enlaces Relacionados

MSDN - GDI+
http://msdn.microsoft.com/library/spa/default.asp?url=/library/SPA/cpguide/html/cpconDrawingEditingImages.asp

MSDN – Controles WinForms
http://msdn.microsoft.com/library/spa/default.asp?url=/library/SPA/cpguide/html/cpconCreatingWinFormsControls.asp

Building Windows Forms Controls and Components with Rich Design-Time Features
Parte 1: http://msdn.microsoft.com/msdnmag/issues/03/04/Design-TimeControls/default.aspx
Parte 2: http://msdn.microsoft.com/msdnmag/issues/03/05/design-timecontrols/default.aspx

Working with Images in the .NET Framework
http://msdn.microsoft.com/msdnmag/issues/03/07/CuttingEdge/default.aspx

Painting techniques using Windows Forms for the Microsoft .NET Framework
http://windowsforms.net/articles/windowsformspainting.aspx

Actualizado por Sgoliver el 30/12/2005

Salvo indicación expresa, todo el contenido de esta web (incluido texto y descargas) están bajo una licencia de Creative Commons

Licencia de Creative Commons