Página principal
Artículos y trucos
Catálogo de productos
Ejemplos y descargas
Mis libros
Cursos de formación
Investigación y desarrollo
Libros recomendados
Mis páginas favoritas
Acerca del autor
 
En colaboración con Amazon
 
Intuitive Sight

LINQ y la tabla de colores

Con la aparición de LINQ, han proliferado los trucos y artículos que utilizan este recurso hasta para apagar la tele desde el sofá. El caso extremo podría ser un ray tracer programado como una gigantesca consulta LINQ. Se trata, por supuesto y como el propio autor del ray tracer aclara, de una demostración de la potencia de LINQ más que de una forma recomendable de programación.

... pero al final, era inevitable que un servidor cayese también en la tentación. La verdad es que LINQ for Objects simplifica muchos algoritmos, y siempre que no haga falta el 100% de eficiencia, es preferible un algoritmo pequeño y declarativo que uno rápido, pero ilegible.

En este caso, quería crear un menú que permitiese insertar un nombre de color, de los reconocidos en .NET, en una descripción de escena de XSight RT. El problema: que hay demasiados nombres de colores (AliceBlue, AntiqueWhite, Aqua, etc) para que cupiesen cómodamente dentro de un mismo submenú. La idea: agruparlos en submenús de acuerdo a su tono. En un submenú irían los tonos azules, en otros los verdes, y así sucesivamente. Una primera solución sería crear manualmente estos menús, clasificando manualmente los colores. Pero, ¿por qué pasar por un infierno, cuando se puede automatizar la tarea?

CLASIFICACION DE COLORES

El primer paso hacia la meta consiste en obtener una descripción textual del tono de un color determinado. El tono de un color está razonablemente bien representado por el valor del componente hue cuando el color se representa en el espacio HSV (hue-saturation-value). De manera que creé una sencilla rutina que obtenía el hue a partir de un color, y de acuerdo al valor obtenido, seleccionaba la clasificación más apropiada:

private string GetColorClass(Color c)
{
    int min = Math.Min(Math.Min(c.R, c.G), c.B);
    int max = Math.Max(Math.Max(c.R, c.G), c.B);
    int delta = max - min;
    // Si todos los colores primarios tienen la misma intensidad,
    // se trata de un tono de gris.
    if (delta == 0)
        return "Gray";
    // Clasificamos el tinte según el color dominante.
    int hue;
    if (c.R == max)
        hue = 60 * (c.G - c.B) / delta;
    else if (c.G == max)
        hue = 120 + 60 * (c.B - c.R) / delta;
    else
        hue = 240 + 60 * (c.R - c.G) / delta;
    if (hue < 0)
        hue += 360;
    // Usamos el tinte para clasificar el color.
    if (hue < 16)       return "Red";
    else if (hue < 39)  return "Orange";
    else if (hue < 80)  return "Yellow";
    else if (hue < 159) return "Green";
    else if (hue < 195) return "Cyan";
    else if (hue < 259) return "Blue";
    else if (hue < 348) return "Violet";
    else                return "Red";
}

En realidad, la mayor parte de GetColorClass nos sobra: aunque .NET v1.1 no lo ofrecía, a partir de la v2.0, la estructura Color tiene un método GetHue que devuelve el tono de un color dado como un valor entre 0 y 360. Una vez obtenido el tono, hay que seleccionar qué rango de valores resultan en tonos rojizos, azulados y así sucesivamente. Cada cual, supongo, tendrá su propia idea sobre este tema.

Hay un caso especial a tratar: los grises. En un tono grisáceo, los tres componentes RGB deben ser iguales, y la saturación, según la escala HSV, debe ser cero. Nuestra rutina comienza por descartar dicho caso. Si, por el contrario, usted decide usar GetHue, deberá también llamar a GetSaturation para descartar antes los grises, el blanco y el negro. Tampoco sería mala idea considerar como grises aquellos colores que no lo son matemáticamente, pero que se aproximan lo suficiente.

UNA CONSULTA CON FILTROS Y GRUPOS

Ya estamos listos para escribir una expresión LINQ que nos permita enumerar todos los colores con nombres de .NET, agrupados de acuerdo a su tono. Olvidémonos primero de los grupos, y veamos cómo podemos obtener la lista de colores con nombres, a secas. Para ello utilizaremos reflexión, y el tipo enumerativo KnownColor:

  1. Vamos a sacar los nombres de colores de la lista de constantes definidas para el tipo enumerativo KnownColor.
  2. KnownColor, por desgracia, tiene también colores "relativos" como ActiveCaption. Deberíamos eliminar estas constantes.
  3. Pero podemos aprovechar el hecho de que, mientras existe una propiedad Color.Blue, no existe un Color.ActiveCaption. Por lo tanto, sólo aceptaremos aquellos nombres extraídos de KnownColor que sean, al mismo tiempo, el nombre de una propiedad de la clase Color.
  4. Podemos obtener un color a partir de una de las constantes de KnownColor mediante el método estático FromKnownColor, de la clase Color.

La siguiente expresión nos permite, por lo tanto, obtener la lista de todos los colores con nombres:

var q = from KnownColor kc in Enum.GetValues(typeof(KnownColor))
        where typeof(Color).GetProperty(kc.ToString()) != null
        select Color.FromKnownColor(kc)

Destrocemos la expresión para analizar sus partes. Primero veamos de dónde obtendremos la lista original:

from KnownColor kc in Enum.GetValues(typeof(KnownColor))
...

El método Enum.GetValues nos da todas las constantes de un tipo enumerativo en un resultado de tipo Array. Nos gustaría obtener directamente, por supuesto, un valor de tipo KnownColor[]. Podríamos hacer directamente una conversión del tipo de retorno... o podemos especificar explícitamente, como hemos hecho, el tipo de cada valor obtenido al recorrer la lista de valores. Casi seguramente, la primera técnica sería más eficiente, pues sólo se realizaría una conversión, pero la técnica utilizada es más elegante y general.

...
where typeof(Color).GetProperty(kc.ToString()) != null
...

Ahora, dado un valor kc de tipo KnownColor, intentamos comprobar si existe una propiedad con el mismo nombre definida por el tipo Color. El método GetProperty nos sirve para obtener información sobre una propiedad de la que conocemos el nombre: si no existe dicha propiedad, el método devuelve un puntero vacío. Nos quedamos, por supuesto, con aquellos kc para los que obtenemos referencias no nulas.

...
select Color.FromKnownColor(kc)

Finalmente, con los kc que sobreviven a la prueba, obtenemos el correspondiente color: no olvide que KnownColor es un tipo enumerativo, y que lo que realmente necesitamos son valores de tipo Color. La expresión la asignamos a una variable temporal q, cuyo tipo, deducido estáticamente por el compilador, es IEnumerable<Color>: una secuencia de colores, en otras palabras.

Si no necesitásemos más, no sería necesario usar LINQ para resolver esta tarea: nos bastaría un simple bucle con una condición en su interior. Pero necesitamos agrupar los valores de la lista anterior de acuerdo al tono predominante de cada color. Transformamos, por lo tanto, la consulta inicial en esta otra:

var q = from KnownColor kc in Enum.GetValues(typeof(KnownColor))
        where typeof(Color).GetProperty(kc.ToString()) != null
        let c = Color.FromKnownColor(kc)
        group c by GetColorClass(c);

Observe que la cláusula let, que define una variable de rango llamada c, es indispensable porque hacemos referencia dos veces a c en la cláusula de agrupamiento. Note también que la cláusula group sustituye a la cláusula select, y la hace innecesaria.

RECORRIENDO EL RESULTADO

¿De qué tipo es ahora la variable q? Cuando agrupamos los valores de una secuencia, esperamos obtener una secuencia de secuencias. En este caso, el tipo exacto inferido por el compilador es el siguiente:

IEnumerable<IGrouping<string, Color>>

Con toda seguridad, usted ya conoce IEnumerable, pero es menos probable que IGrouping le resulte familiar. Este segundo tipo es simplemente una interfaz derivada de IEnumerable. Esto es: IGrouping es simplemente un tipo de secuencia. Lo que la diferencia de un IEnumerable a secas es que tiene una propiedad adicional, llamada Key. Es decir, un IGrouping es una secuencia con un elemento adicional de cabecera. Nuestra expresión, entonces, devuelve una lista de listas con cabeceras:

Si iteramos directamente sobre q

foreach (var grupo in q)
{
    string tonoColor = grupo.Key;
    // ...
}

obtendremos en cada paso una segunda lista, con el nombre del correspondiente tono de color en la propiedad Key de la lista anidada. Para recorrer los colores de cada grupo necesitamos un bucle anidado:

foreach (var grupo in q)
{
    Console.WriteLine("Tono: " + grupo.Key);
    foreach (var c in grupo)
        Console.WriteLine("      " + c.Name);
}

Es fácil transformar el doble bucle anterior, junto con la correspondiente consulta LINQ, para generar un documento HTML que muestre los colores, con sus nombres, agrupados por tonalidades:

private void Form1_Load(object sender, EventArgs e)
{
    var sb = new StringBuilder("<html>").AppendLine().
        AppendLine("<body>").
        AppendLine("<table><tr><th>Color name</th><th width=\"120\">Color</th></tr>");
    // Recorremos cada grupo de colores.
    foreach (var group in from KnownColor kc in Enum.GetValues(typeof(KnownColor))
                          where typeof(Color).GetProperty(kc.ToString()) != null
                          let c = Color.FromKnownColor(kc)
                          group c by GetColorClass(c))
    {
        sb.Append("<tr><th colspan=\"2\" bgcolor=\"").
            Append(group.Key).Append("\">").
            Append(group.Key).AppendLine("</th></tr>");
        // Recorremos los colores de cada grupo.
        foreach (var color in group)
            sb.Append("<tr><td>").Append(color.Name).
                Append("</td><td bgcolor=\"rgb(").
                Append(color.R).Append(",").
                Append(color.G).Append(",").
                Append(color.B).
                AppendLine(")\">&nbsp;</td></tr>");
    }
    sb.AppendLine("</table>").
        AppendLine("</body>").
        AppendLine("</html>");
    webBrowser1.DocumentText = sb.ToString();
}

POR SU CUENTA Y RIESGO...

Y si quiere una tarea más interesante, piense en una manera eficiente de averiguar, dado un color arbitrario, cual de los colores predefinidos por .NET es el más cercano. Por supuesto, no estoy pensando en usar la fuerza bruta. ¿Qué estructura de datos es la más adecuada para este algoritmo?