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

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