Pintando sobre un bitmap en WPF

Una de las cosas que más llaman la atención de la aplicación que desarrollamos en el Web Day (Mi Bolsa) es la gráfica de cotizaciones.

La gráfica se genera en tiempo real en servidor y toma como parámetro el símbolo de la empresa y la fecha final. Aquí lo tenéis en un ejemplo en vivo, podéis poner cualquier símbolo de una empresa (ej. MSFT, AABC, MSN...) y una fecha. Al pinchar en el botón la gráfica se actualiza en servidor (por cierto los valores mostrados son aleatorios...):

Símbolo: Fecha: mibolsa

 

La técnica utilizada para hacerlo es bastante antigua, pero con un toquecillo de WPF. En la página web sólo hay un elemento IMG cuya propiedad SRC se establece a una dirección de una página ASP.NET de servidor. Esta página toma como parámetros el ancho y alto, la empresa y la fecha y genera dinámicamente una imagen en el servidor en formato JPEG.

Los principales trucos para hacer que esto funcione:

  • Cambiar el MIME type de la página ASP.NET para que el browser la identifique correctamente como una imagen JPEG y no como una página HTML. Para hacer esto hay que establecer la propiedad ContentType de la respuesta. Si tienes el código fuente de Mi Bolsa puedes verlo en el archivo TickerChart.aspx, dentro de la función Page_Load:

Response.ContentType = "image/jpeg"

  • Generar la imagen en el servidor. Para darle vidilla hemos hecho la imagen con WPF. El concepto es muy parecido a usar GDI, se trata de generar un stream de bytes con la codificación de la imagen en JPEG. Una vez tengamos esta codificación la devolvemos en la página ASP.NET con un Response.Outputstream.Write.
    Otra historia es generar esa imagen desde WPF. La buena noticia es que cualquier renderización de WPF puede realizarse sobre una imagen en memoria en vez de sobre la pantalla. Para hacerlo tenemos que crear un objeto RenderTargetBitmap y llamar a su método Render con las primitivas que queramos pintar. El código que realiza esta operación está en el archivo ChartDrawer.cs:

public byte[] DrawChart()
{
PixelFormat pf = PixelFormats.Bgr32;
MemoryStream memStream = new MemoryStream();

      DrawingVisual drawingVisual = new DrawingVisual();
DrawingContext drawingContext = drawingVisual.RenderOpen();

      DrawBackground(drawingContext);
DrawAxis(drawingContext);
DrawGrid(drawingContext);
for(int i=0;i<_values.Count;i++)
DrawValues(drawingContext, i);
DrawAxisText(drawingContext);

      drawingContext.Close();

      RenderTargetBitmap renderBitmap = new RenderTargetBitmap
(_width, _height, 96, 96, PixelFormats.Pbgra32);
renderBitmap.Render(drawingVisual);

      JpegBitmapEncoder encoder = new JpegBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(renderBitmap));
encoder.Save(memStream);

      return memStream.GetBuffer();
}

Comments

  • Anonymous
    April 18, 2007
    Hey David ... interesante approach !!! mas q nada porq el modelo de objetos de WPF es muy pero muy simple :D y crear este tipo de "imagenes on the fly" se puede hacer muy facilmente. Claro ... todo esto en comparación con GDI, que tiene sus cosillas ;) Saludos

  • Anonymous
    April 30, 2007
    Genial! solo verlo me dispuse a migrar mi sistema (GDI+) a WPF ¡pero que veo! Vaya, vaya.. y parecía que estaba usando System.Windows.Media.Effects.DropShadowBitmapEffect... pero no, lo que has hecho es pintar dos veces con una ShadowBrush. Lo diseñé probando en WindowsFroms y cuando lo migro a web... crash! Excepción al aplicar el efecto DropShadowBitmapEffect {System.InvalidOperationException: The calling thread must be STA, because many UI components require this.} La cuestión es que quitando los efectos funciona bien en MTAThread, pero es una pena dar por perdidos los Effects para la generación dinámica de estadísticas, básicamente se nos queda en un gdi con brochas guays(gradientes, sombras etc..) No se si tu encontraste el mismo problema y por eso lo hiciste así, espero que no y que sepas la respuesta. He estado buscando bastante por la web y no he encontrado nada de como solucionarlo Gracias ¿Alguna idea?

  • Anonymous
    May 03, 2007
    ¿Has probado a marcar la página con el atributo ASPCOMPAT? <%@Page aspcompat=true ... %>