As I write this, Vista is out and promises new and cool things, and Windows Presentation Foundation (WPF) is closing the gap between rich Windows clients and rich web clients. So, a huge number of developers and businesses out there are interested in rich web clients. For them, I wrote this article.
This article uses a trivial example to demonstrate how to dynamically create GDI+ graphics in web pages. The example, a ticking clock, has plumbing that would support any kind of continuously updating, dynamically rendered graphic.
Rendering Graphics with GDI+
Although web forms do not have a canvas (or device context, also called DC) and you therefore cannot ask a Web form for its Graphics object, you can simulate this behavior by rendering graphics in ASP.NET. In summary, to render images with GDI+ you need:
- One user control that will act as your device context or drawing canvas
- One web page to contain the user control
- One web page to contain an image control (The first web page will actually play the role of the image for this web page.)
Figure 1 depicts the relationship between the user control and two web pages.
Figure 1: The Relationship Between the Controls that Play the Role of Dynamic Canvas
One page is actually the viewable page, and the second page and user control play the role of dynamic canvas. Take a look at how this works by building it from the inside out.
Defining the UserControl
The UserControl is as close to the Graphics object (canvas or device context, if you prefer) as you are going to get. The basic idea of rendering the image is to create an image, get a Graphics object from that image, draw something on it, and then save that image on the HttpResponse.OutputStream (this code is show in Listing 1).
Listing 1: The Code That Orchestrates Rendering the Clock
Private Sub DrawClock() Dim b As Bitmap = New Bitmap(FClockWidth, FClockHeight) Dim g As Graphics = Graphics.FromImage(b) Dim p As Pen = New Pen(Brushes.Black, 1) g.SmoothingMode = Drawing2D.SmoothingMode.AntiAlias DrawClockFace(g, p) DrawClockTicks(g, p) DrawClockHands(g, p) Response.ContentType = "image/jpeg" b.Save(Response.OutputStream, ImageFormat.Jpeg) Response.End() End Sub
The reason I used an inner UserControl is that writing to the output stream is destructive to things already on the stream. So, other HTML would be obliterated by writing the image. When you save the image to the output response stream, that is all that will be there. The code that renders the clock is not that important technically, but Listing 2 shows it for fun.
Listing 2: The ClockControl (The ClockControl.ascx Page Is an Empty UserControl.)
Imports System.Drawing Imports System.Drawing.Imaging Imports System.Runtime.InteropServices Partial Class ClockControl Inherits System.Web.UI.UserControl Private FClockWidth As Integer = 100 Private FClockHeight As Integer = 100 Private Sub DrawClock() Dim b As Bitmap = New Bitmap(FClockWidth, FClockHeight) Dim g As Graphics = Graphics.FromImage(b) Dim p As Pen = New Pen(Brushes.Black, 1) g.SmoothingMode = Drawing2D.SmoothingMode.AntiAlias DrawClockFace(g, p) DrawClockTicks(g, p) DrawClockHands(g, p) Response.ContentType = "image/jpeg" b.Save(Response.OutputStream, ImageFormat.Jpeg) Response.End() End Sub Private Sub DrawClockFace(ByVal g As Graphics, ByVal p As Pen) g.FillRectangle(Brushes.White, 0, 0, FClockWidth, FClockHeight) g.DrawEllipse(p, New Rectangle(1, 1, 98, 98)) End Sub Private Sub DrawClockTicks(ByVal g As Graphics, ByVal p As Pen) ' draw the clock ticks ' Borrowed from Mike Gold's article: ' http://www.vbdotnetheaven.com/UploadFile '/mgold/VirtualClockinVBdotNET04212005032826AM/ 'VirtualClockinVBdotNET.aspx Dim count As Integer = 1 Dim hour As Integer = 1 Dim angle As Double For count = 0 To 330 Step 30 angle = (count - 1) * Math.PI / 180 'For angle = 0 To (2 * Math.PI) - (2.0 * Math.PI / 14) 'Step 2.0 * Math.PI / 12 Dim x As Double = (FClockWidth - 20) / 2 *_ Math.Cos((angle - Math.PI / 3)) + _ (FClockWidth - 20) / 2 + 5 Dim y As Double = (FClockWidth - 20) / 2 * _ Math.Sin((angle - Math.PI / 3)) + _ (FClockWidth - 20) / 2 + 4 Dim font As Font = New Font("Times New Roman", 8) g.DrawString(Convert.ToString(hour), font, Brushes.Black, _ CSng(x), CSng(y), _ New StringFormat) 'count += 1 hour += 1 Next count ' angle End Sub Private Sub DrawClockHands(ByVal g As Graphics, ByVal p As Pen) Dim d As DateTime = DateTime.Now ' draw hour hand Dim h As Integer = d.Hour DrawHour(g, h) ' draw minute hand Dim m As Integer = d.Minute DrawMinute(g, m) ' draw second hand Dim s As Integer = d.Second DrawSecond(g, s) End Sub Private Sub DrawHour(ByVal g As Graphics, ByVal hour As Integer) Const OFFSET As Integer = 30 Dim p As Pen = New Pen(Color.Black, 3) ' Figure out the Angle in radians Dim angle As Double = ((hour - 1) Mod 12) * 30 * Math.PI / 180 Dim x, y As Double x = (FClockWidth - OFFSET) / 2 * _ Math.Cos((angle - Math.PI / 3)) + _ (FClockWidth - OFFSET) / 2 + 10 y = (FClockWidth - OFFSET) / 2 * _ Math.Sin((angle - Math.PI / 3)) + _ (FClockWidth - OFFSET) / 2 + 8 g.DrawLine(p, CSng(FClockWidth / 2), CSng(FClockHeight / 2), _ CSng(x), CSng(y)) End Sub Private Sub DrawMinute(ByVal g As Graphics, _ ByVal Minute As Integer) Dim p As Pen = New Pen(Color.Black, 2) ' Figure out the Angle in radians Dim angle As Double = (Minute - 6) * 6 * Math.PI / 180 Dim x, y As Double x = (FClockWidth - 20) / 2 * Math.Cos((angle - Math.PI / 3)) + _ (FClockWidth - 20) / 2 + 10 y = (FClockWidth - 20) / 2 * Math.Sin((angle - Math.PI / 3)) + _ (FClockWidth - 20) / 2 + 8 g.DrawLine(p, CSng(FClockWidth / 2), CSng(FClockHeight / 2), _ CSng(x), CSng(y)) End Sub Private Sub DrawSecond(ByVal g As Graphics, _ ByVal Second As Integer) Dim p As Pen = New Pen(Color.Red, 1) ' Figure out the Angle in radians Dim angle As Double = (Second - 6) * 6 * Math.PI / 180 Dim x, y As Double x = (FClockWidth - 20) / 2 * Math.Cos((angle - Math.PI / 3)) + _ (FClockWidth - 20) / 2 + 10 y = (FClockWidth - 20) / 2 * Math.Sin((angle - Math.PI / 3)) + _ (FClockWidth - 20) / 2 + 8 g.DrawLine(p, CSng(FClockWidth / 2), CSng(FClockHeight / 2), _ CSng(x), CSng(y)) End Sub Protected Sub Page_Load(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles Me.Load DrawClock() End Sub End Class
The last three lines of code in DrawClock move the dynamic image to the output stream. You will need to use System.Drawing and System.Drawing.Image to support the ClockControl behavior.