Hoy toca la composición y como no teclear código con WPF.Digo esto porque no pretendo explicar las diferencias entre composición y agregación, lo que está claro es que la composición quiere decir que se compone, un coche se compone de motor, ruedas, chasis, etc y el coche no es nada sin estas piezas y estas piezas, podrían serlo por su cuenta, pero fueron diseñadas para funcionar en el coche, por tanto aplicándolo al cartón de bingo (que no tiene nada que ver con el coche en cuanto a fisonomía pero si a composición), este se compone de líneas y cada línea de números. Los números tienen aplicación en la vida, pero un cartón sin estos poco valdría y por supuesto un cartón sin líneas, ¿que cantaríamos? números! no, el bingo tiene sus reglas y ahí estamos los programadores para aplicar las reglas al software y llevarlas a cabo.
Si realizamos un diagrama de clases (simplificado) de un cartón de bingo, sería algo como esto
La línea que une las clases o la asociación con el rombo relleno de negro, indica eso, que es una composición.
Sabiendo esto, vamos a diseñar un cartón de bingo más en serio y crearemos
Esto es lo que se me ha ocurrido (ahora solo el diagrama), un objeto Number
, el cual tiene dos propiedades, una el valor y otra si el número está marcado, un objeto Row
(Línea) que contiene una colección de números y un objeto Card
que contiene una colección de líneas. Por partes.
La clase Number
, como ya he dicho tiene dos propiedades y además tiene dos constructores y un solo método; los constructores uno sin parámetros y otro para asignarle el número. Como la clase implementa la interfaz Icomparable<Number>
, obligatoriamente el método CompareTo
debemos crearlo para que posteriormente pueda ordenarse la lista de números como nosotros queremos. Y visto esto, aquí el código
public class Number:IComparable<Number> { #region Constructor /// <summary> /// Representa un número de cartón de bingo /// </summary> public Number() { } /// <summary> /// Representa un número de cartón de bingo /// </summary> /// <param name="number">Número que se asigna como valor</param> public Number(int number) { this.Value = number; } #endregion #region Fields, properties and constants /// <summary> /// Valor del número /// </summary> public int Value { get; set; } /// <summary> /// Indica si el número está marcado o no /// </summary> public bool IsChecked { get; set; } = false; #endregion #region Public Methods /// <summary> /// Compara esta instancia con un objeto específico y retorna un entero que indica /// </summary> /// <param name="other">Objeto a comparar</param> /// <returns>Retorna un entero con signo. Negativo si es menor, un 0 si es igual y un entero positivo si es mayor.</returns> public int CompareTo(Number other) { int result = 0; if (this.Value > other.Value) { result = 1; } else if (this.Value < other.Value) { result = -1; } return result; } #endregion }
la clase Row
o la clase de la línea le he incluido dos constructores, uno que que genera una línea en base a una lista de números disponibles, esto es porque a la línea le interesa saber que números hay que escoger para que no se repitan y el otro al que le pasamos los números en un array como parámetro.
Tiene una constante que indica la cantidad de números que tiene una línea y una colección ¿de que?, pues de la clase Number
. El motor de la clase o los métodos privados, son dos, uno para obtener una lista de decenas y otro para crear la línea con sus números, los cuales no deben repetir decena ni repetir números de otras líneas. Para esto creo una lista de las decenas, 0, 10, 20, … 70 y 80, instancio la clase Random
con una semilla sobre la suma de los milisegundos del tiempo actual y las decenas pendientes, esto provoca una semilla distinta en cada iteración y por tanto algo más parecido a la aleatoriedad, además le incluyo un número impar de milisegundos de retardo. Esto también nos va a generar cartones más reales y con una distribución más normal, imaginad un cartón con los 15 números agrupados desde las unidades a la columna de 40 y el resto de columnas (50, 60, 70 y 80) vacías, esos cartones son válidos, pero poca gente los querría, es como si nos dieran un billete de lotería con el 11111, está en el bombo, pero poca gente lo quiere. Después de esto,
- Sobre la lista de decenas escojo una al azar,
- Filtro los números disponibles para acotarlos en esa decena y vuelvo a escoger uno aleatorio
- Elimino el número escogido de la lista de números disponibles (para que en el siguiente número no lo esté)
- Añado el número a la colección de la línea
- Ordeno la línea.
con esto, cada vez que genero una línea, me aseguro que no se repitan las decenas y los números por línea.
Los métodos públicos, son:
ToString()
. Para devolver una cadena con los números de la línea, sustituyendo los espacios por asteriscos (este lo he ppuesto por si alguien lo quiere obtener por consola)GetRowNumbersToString()
. Este igual que el anterior, pero con la diferencia de que solo aparecen los números separados por tabulaciones (Cabe destacar el uso deForEach
en una sola línea)GetRowNumber()
. Que devuelve una lista de objetos Number con todos los números de la línea.
El código…
public class Row { #region Constructor /// <summary> /// Representa una línea de cartón de bingo /// </summary> /// <param name="_availableNumbers">Lista de números disponibles desde la cual se generará los objetos Number</param> public Row(List<int> _availableNumbers) { CreateRow(_availableNumbers); } /// <summary> /// Representa una línea de cartón de bingo /// </summary> /// <param name="args">Array con los números que se usarán para generar los objetos Number</param> public Row(int[] args) { if (args.Length > numbersCount) throw new OverflowException(String.Format("El número máximo de números que acepta una línea es de {0}", numbersCount)); foreach (var item in args) { Number number = new Number(); number.Value = item; rowNumbers.Add(number); } rowNumbers.Sort(); } #endregion #region Fields, properties and constants /// <summary> /// Constante que indica la cantida de números de una linea /// </summary> private const int numbersCount = 5; /// <summary> /// Lista de objetos Number /// </summary> public List<Number> rowNumbers = new List<Number>(); #endregion #region Public Methods /// <summary> /// Retorna una cadena formateada por decenas con los valores de los objetos Number contenidos en esta instancia /// </summary> /// <returns>Cadena de texto con los valores de los objetos Number</returns> public override string ToString() { string textToString = ""; for (int i = 0; i < 9; i++) { int decena = i * 10; int incremento = i == 8 ? decena + 11 : decena + 10; var query = this.rowNumbers.Where(k => k.Value > decena && k.Value < incremento); if (query.Count() > 0) { textToString += query.First().Value + "\t"; } else { textToString += "*\t"; } } return textToString; } /// <summary> /// Retorna una cadena con los valores de los objetos Number contenidos en esta instancia /// </summary> /// <returns>Cadena de texto con los valores de los objetos Number</returns> public string GetRowNumbersToString() { string textToString = ""; this.rowNumbers.ForEach(k => textToString += k.Value + "\t"); return textToString; } public List<Number> GetRowNumber() { List<Number> numbers = new List<Number>(); for (int i = 0; i < 9; i++) { int decena = i * 10; int incremento = i == 8 ? decena + 11 : decena + 10; var query = this.rowNumbers.Where(k => k.Value > decena && k.Value < incremento); if (query.Count() > 0) { numbers.Add(query.First()); } else { numbers.Add(new Number(-1)); } } return numbers; } #endregion #region Private Methods /// <summary> /// Inicia la instancia actual con objetos Number a partir de una lista de números disponibles /// </summary> /// <param name="_availableNumbers">Lista de números disponibles desde la cual se generará los objetos Number</param> private void CreateRow(List<int> _availableNumbers) { List<int> decenas = GetTen(); int number = 0; Random rnd=new Random(DateTime.Now.Millisecond + _availableNumbers.Count); for (int i = 0; i < numbersCount; i++) { System.Threading.Thread.Sleep(3); int decena = decenas[rnd.Next(0, decenas.Count)]; // Eliminar la decena de la lista para no solicitarla en la próxima petición decenas.Remove(decena); int incremento = decena == 80 ? decena + 11 : decena + 10; var temp = _availableNumbers.Where(k => k > decena && k < incremento); number = temp.ElementAt(rnd.Next(0, temp.Count())); _availableNumbers.Remove(number); rowNumbers.Add(new Number(number)); } this.rowNumbers.Sort(); } /// <summary> /// Retorna una lista con las decenas desde 0 al 80 /// </summary> /// <returns></returns> private List<int> GetTen() { List<int> decenas = new List<int>(); for (int i = 0; i < 9; i++) { decenas.Add(i*10); } return decenas; } #endregion }
Por último, la clase Card
(Cartón, no ibas a pensar que algo tan sencillo como un cartón iba a tener tanta complejidad). Esta realiza lo siguiente:
- Rellena la lista de números disponibles, los números del 1 al 90.
- Crea un nuevo cartón, el cual instancia tres veces la clase Row,envíandole la lista de números disponibles, si en la primera línea se han añadido el 7, 23, 35,67,77 y 89, estos números una vez que se ha creado la fila, se eliminan de la lista de disponibilidad, esto lo hago para seleccionar solo los números que quedan y aumentar el rendimiento; os imagináis escoger un número del 1 al 90 y solo queda el número 43, hasta que acierte la aplicación puede pasar un tiempo no computado, mientras que si vamos extrayendo números, el tiempo siempre será el mismo
- Llena una lista de cadenas de texto con los números del cartón con el fin de usarlos para la visualización.
Además, he implementado la interfaz IEquatable<Card>
para en el futuro por si hubiera que comprobar un cartón, poder hacerlo comparando el cartón ganador. el código de la clase lo muestro a continuación (si teneís alguna duda con el código, no dudéis en preguntame y añadir un comentario, lo responderé gustosamente)
public class Card: IEquatable<Card>,INotifyPropertyChanged { #region Constructor /// <summary> /// Representa un cartón de bingo con 3 líneas y 5 números por línea /// </summary> public Card() { FillAvailableNumbers(); CreateNewCard(); cardNumbers = new List<string>(); foreach (var item in GetRowNumber().Select(k => k.Value)) { cardNumbers.Add(item==-1?"":item.ToString()); } } #endregion #region Fields, properties and constants /// <summary> /// Número de líneas que alberga un cartón /// </summary> private const int rowCount = 3; /// <summary> /// Lista de Líneas del cartón /// </summary> List<Row> cardRows = new List<Row>(); /// <summary> /// Lista de números disponibles que se pueden agregar a un cartón /// </summary> List<int> availableNumbers = new List<int>(); /// <summary> /// Listado de número ordenados por filas para visualización /// </summary> public List<string> cardNumbers { get; set; } public event PropertyChangedEventHandler PropertyChanged; public int CardNumber { get; set; } public int SerialNumber { get; set; } #endregion #region Public Methods /// <summary> /// Retorna una cadena con los datos del cartón formateados /// </summary> /// <returns>Cadena detexto con los datos del cartón</returns> public override string ToString() { string textToString = ""; foreach (var row in this.cardRows) { textToString += row.ToString() + "\n"; } textToString += "\n"; foreach (var row in this.cardRows) { textToString += row.GetRowNumbersToString() + "\n"; } return textToString; } /// <summary> /// Retorna un valor que indica que esta instancia es igual a un objeto específico /// </summary> /// <param name="other">Un objeto Card a comparar con esta instancia</param> /// <returns>True si tiene el mismo valor que esta instancia; en caso contrario false</returns> public bool Equals(Card other) { bool isEquals = true; List<int> cardNumbersThis = GetCardNumbers(this); List<int> cardNumbersOther = GetCardNumbers(other); cardNumbersThis.ForEach(k => cardNumbersOther.Remove(k)); if (cardNumbersOther.Count > 0) isEquals = false; return isEquals; } /// <summary> /// Retorna una lista con los números ordenados de cada fila y asignados los huecos con el valor -1 /// </summary> /// <returns></returns> public List<Number> GetRowNumber() { List<Number> numbers = new List<Number>(); foreach (var row in this.cardRows) { numbers.AddRange(row.GetRowNumber()); } return numbers; } #endregion #region Private Methods /// <summary> /// Retorna una lista de todos los números del cartón, indicando los elementos vacíos con -1 /// </summary> /// <param name="card">Cartón de bingo del que se extraen los datos</param> /// <returns>Lista de enteros con todos los números del cartón. A los elementos vacíos, se les asigna -1</returns> private List<int> GetCardNumbers(Card card) { List<int> cardNumbers = new List<int>(); foreach (var row in card.cardRows) { cardNumbers.AddRange(row.rowNumbers.Select(k => k.Value)); } return cardNumbers; } /// <summary> /// crea las tres líneas y las asigna al cartón /// </summary> private void CreateNewCard() { for (int i = 0; i < rowCount; i++) { Row row = new Row(availableNumbers); foreach (var item in row.rowNumbers) { availableNumbers.Remove(item.Value); } cardRows.Add(row); } } /// <summary> /// Rellena los números disponibles para generar cartones /// </summary> private void FillAvailableNumbers() { for (int i = 1; i < 91; i++) { availableNumbers.Add(i); } } #endregion }
Y ahora vamos a diseñar el cartón con WPF sin escribir código C#, XAML si, pero este se encarga de los errores y la robustez.
Creo un control de usuario y le añado lo siguiente
<UserControl x:Class="Bingo.CardForm" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:Pascal" mc:Ignorable="d" Height="250" Width="650"> <Grid x:Name="CardGrid"> <Grid.Resources> <Style x:Key="baseStyle" TargetType="TextBlock"> <Setter Property="TextAlignment" Value="Center"/> <Setter Property="VerticalAlignment" Value="Stretch"/> <Setter Property="HorizontalAlignment" Value="Stretch"/> <Setter Property="Margin" Value="3"/> <Setter Property="FontSize" Value="14"/> </Style> <Style x:Key="numberStyle" TargetType="TextBlock" BasedOn="{StaticResource baseStyle}"> <Setter Property="FontSize" Value="40"/> <Setter Property="Foreground" Value="DarkGray"/> <Setter Property="Background" Value="LightGray"/> </Style> </Grid.Resources> <Grid.ColumnDefinitions> <ColumnDefinition Width="10"/> <ColumnDefinition Width="70"/> <ColumnDefinition Width="70"/> <ColumnDefinition Width="70"/> <ColumnDefinition Width="70"/> <ColumnDefinition Width="70"/> <ColumnDefinition Width="70"/> <ColumnDefinition Width="70"/> <ColumnDefinition Width="70"/> <ColumnDefinition Width="70"/> <ColumnDefinition Width="10"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="30"/> <RowDefinition Height="70"/> <RowDefinition Height="70"/> <RowDefinition Height="70"/> <RowDefinition Height="10"/> </Grid.RowDefinitions> <TextBlock Text="Cartón Nº:" Grid.Column="1" Grid.Row="0" HorizontalAlignment="Right" Style="{StaticResource baseStyle}"/> <TextBlock x:Name="CardNumber" Text="{Binding CardNumber}" Grid.Column="2" Grid.Row="0" Style="{StaticResource baseStyle}"/> <TextBlock Text="Serie:" Grid.Column="4" Grid.Row="0" HorizontalAlignment="Right" Style="{StaticResource baseStyle}"/> <TextBlock x:Name="SerialNumber" Text="{Binding SerialNumber}" Grid.Column="5" Grid.Row="0" Style="{StaticResource baseStyle}"/> <TextBlock x:Name="Row11" Text="{Binding cardNumbers[0]}" Grid.Column="1" Grid.Row="1" Style="{StaticResource numberStyle}"/> <TextBlock x:Name="Row12" Text="{Binding cardNumbers[1]}" Grid.Column="2" Grid.Row="1" Style="{StaticResource numberStyle}"/> <TextBlock x:Name="Row13" Text="{Binding cardNumbers[2]}" Grid.Column="3" Grid.Row="1" Style="{StaticResource numberStyle}"/> <TextBlock x:Name="Row14" Text="{Binding cardNumbers[3]}" Grid.Column="4" Grid.Row="1" Style="{StaticResource numberStyle}"/> <TextBlock x:Name="Row15" Text="{Binding cardNumbers[4]}" Grid.Column="5" Grid.Row="1" Style="{StaticResource numberStyle}"/> <TextBlock x:Name="Row16" Text="{Binding cardNumbers[5]}" Grid.Column="6" Grid.Row="1" Style="{StaticResource numberStyle}"/> <TextBlock x:Name="Row17" Text="{Binding cardNumbers[6]}" Grid.Column="7" Grid.Row="1" Style="{StaticResource numberStyle}"/> <TextBlock x:Name="Row18" Text="{Binding cardNumbers[7]}" Grid.Column="8" Grid.Row="1" Style="{StaticResource numberStyle}"/> <TextBlock x:Name="Row19" Text="{Binding cardNumbers[8]}" Grid.Column="9" Grid.Row="1" Style="{StaticResource numberStyle}"/> <TextBlock x:Name="Row21" Text="{Binding cardNumbers[9]}" Grid.Column="1" Grid.Row="2" Style="{StaticResource numberStyle}"/> <TextBlock x:Name="Row22" Text="{Binding cardNumbers[10]}" Grid.Column="2" Grid.Row="2" Style="{StaticResource numberStyle}"/> <TextBlock x:Name="Row23" Text="{Binding cardNumbers[11]}" Grid.Column="3" Grid.Row="2" Style="{StaticResource numberStyle}"/> <TextBlock x:Name="Row24" Text="{Binding cardNumbers[12]}" Grid.Column="4" Grid.Row="2" Style="{StaticResource numberStyle}"/> <TextBlock x:Name="Row25" Text="{Binding cardNumbers[13]}" Grid.Column="5" Grid.Row="2" Style="{StaticResource numberStyle}"/> <TextBlock x:Name="Row26" Text="{Binding cardNumbers[14]}" Grid.Column="6" Grid.Row="2" Style="{StaticResource numberStyle}"/> <TextBlock x:Name="Row27" Text="{Binding cardNumbers[15]}" Grid.Column="7" Grid.Row="2" Style="{StaticResource numberStyle}"/> <TextBlock x:Name="Row28" Text="{Binding cardNumbers[16]}" Grid.Column="8" Grid.Row="2" Style="{StaticResource numberStyle}"/> <TextBlock x:Name="Row29" Text="{Binding cardNumbers[17]}" Grid.Column="9" Grid.Row="2" Style="{StaticResource numberStyle}"/> <TextBlock x:Name="Row31" Text="{Binding cardNumbers[18]}" Grid.Column="1" Grid.Row="3" Style="{StaticResource numberStyle}"/> <TextBlock x:Name="Row32" Text="{Binding cardNumbers[19]}" Grid.Column="2" Grid.Row="3" Style="{StaticResource numberStyle}"/> <TextBlock x:Name="Row33" Text="{Binding cardNumbers[20]}" Grid.Column="3" Grid.Row="3" Style="{StaticResource numberStyle}"/> <TextBlock x:Name="Row34" Text="{Binding cardNumbers[21]}" Grid.Column="4" Grid.Row="3" Style="{StaticResource numberStyle}"/> <TextBlock x:Name="Row35" Text="{Binding cardNumbers[22]}" Grid.Column="5" Grid.Row="3" Style="{StaticResource numberStyle}"/> <TextBlock x:Name="Row36" Text="{Binding cardNumbers[23]}" Grid.Column="6" Grid.Row="3" Style="{StaticResource numberStyle}"/> <TextBlock x:Name="Row37" Text="{Binding cardNumbers[24]}" Grid.Column="7" Grid.Row="3" Style="{StaticResource numberStyle}"/> <TextBlock x:Name="Row38" Text="{Binding cardNumbers[25]}" Grid.Column="8" Grid.Row="3" Style="{StaticResource numberStyle}"/> <TextBlock x:Name="Row39" Text="{Binding cardNumbers[26]}" Grid.Column="9" Grid.Row="3" Style="{StaticResource numberStyle}"/> </Grid> </UserControl>
El código XAML del formulario principal
<Window x:Class="Bingo.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:Bingo" mc:Ignorable="d" Title="MainWindow" Height="401.567" Width="812.461"> <Grid> <local:CardForm x:Name="cardform"/> <Button x:Name="button" Content="Generar" HorizontalAlignment="Left" Height="38" Margin="591,306,0,0" VerticalAlignment="Top" Width="136" Click="button_Click"/> </Grid> </Window>
en este bloque de código, debemos fijarnos en como estamos declarando el control de usuario que hemos creado y que ahora estamos insertando en el formulario principal; primero declaramos el espacio clr-namespace:Bingo
con la key local
(línea 6) y luego llamamos al control desde esta key que hace referencia al espacio de nombres donde se encuentra el control, si no hacemos esto, jamás podremos declarar el control de usuario dentro de la ventana principal, porque no lo encontraría.
y el code behind de este form, solo esto, el resto lo hace WPF. En este bloque cabe destacar que los único que hacemos desde la ventana principal es instanciar un objeto Card
(Cartón) y asignar al DataContext
del grid del control de usuario, el objeto instanciado card
. El botón, lo único que hace es llamar al método CartonNuevo
mostrando cada vez que se pulse, un nuevo cartón aleatorio.
namespace Bingo { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } void CartonNuevo() { Card card = new Card(); card.CardNumber = 1729; card.SerialNumber = 1; cardform.CardGrid.DataContext = card; } private void button_Click(object sender, RoutedEventArgs e) { CartonNuevo(); } } }
Volvemos al código XAML del control de usuario, en el control grid hay varias cosas que ver
- En el bloque
Resources
delGrid
, creo unStyle
para losTextblock
llamado baseStyle - Luego creo otro estilo llamado numberStyle que hereda de baseStyle y modifica algunas propiedades (Está subrayado desde la línea 9 a la 22)
- En las propiedades
Text
de los controlesTextblock
, hacemos unBinding
al elemento concreto de la colección List (Text="{Binding cardNumbers[0]}"
) del objetoCard
, al número de cartón y a la serie, de modo que sin ningún código en C#, el formulario nos mostrará el cartón, solo debemos decirle desde el formulario principal, que elDataContext
es el objeto instanciadocard
. (Dejo subrayado como ejemplo el primerTextBlock
de la línea 48)
He aquí como quedaría el resultado
como siempre, si tenéis alguna duda o pregunta, comentario al canto
Saludos