Pro ASP.NET 2.0 In CSharp 2005 (2005) [eng]
.pdf
1028 C H A P T E R 3 0 ■ DY N A M I C G R A P H I C S A N D G D I +
Figure 30-7. Testing gradient styles
Embedding Dynamic Graphics in a Web Page
The Image.Save() approach has one problem that has been used in all the examples so far. When you save an image to the response stream, you overwrite whatever information ASP.NET would otherwise use. If you have a web page that includes other static content and controls, this content won’t appear at all in the final web page. Instead, the dynamically rendered graphics will replace it.
Fortunately, a simple solution exists. You can link to a dynamically generated image using the HTML <img> tag or the Image web control. But instead of linking your image to a static image file, link it to the .aspx file that generates the picture.
For example, consider the graphic shown earlier in Figure 30-1. It’s stored in a file named SimpleDrawing.aspx, and it writes a dynamically generated image to the response stream. In another page, you could show the dynamic image by adding an Image web control and setting the ImageUrl property to SimpleDrawing.aspx. You could then add other controls or even multiple Image controls that link to the same content.
Figure 30-8 shows an example that uses two <img> tags that point to SimpleDrawing.aspx, along with additional ASP.NET web controls in between.
C H A P T E R 3 0 ■ DY N A M I C G R A P H I C S A N D G D I + |
1029 |
Figure 30-8. Mixing dynamically drawn content and ordinary web controls
■Tip Remember that creating a GDI+ drawing is usually an order of magnitude slower than serving a static image. As a result, it’s probably not a good idea to implement graphical buttons and other elements that you’ll repeat multiple times on a page using GDI+. (If you do, consider caching or saving the image file once you’ve generated it to increase performance.)
Using the PNG Format
PNG is an all-purpose format that always provides high quality by combining the lossless compression of GIFs with the rich color support of JPEGs. However, browsers such as Internet Explorer often don’t handle it correctly when you return PNG content directly from a page. Instead of seeing the picture content, you’ll receive a message prompting you to download the picture content and open it in another program. However, the <img> tag approach effectively sidesteps this problem.
You need to be aware of two more quirks when using PNG. First, some older browsers (including Netscape 4.x) don’t support PNG. Second, you can’t use the Bitmap.Save() method shown in earlier examples.
Technically speaking, the problem is that you can’t use the Save() method with a nonseekable stream. Response.OutputStream is a nonseekable stream, which means data must be written from beginning to end. Unfortunately, to create a PNG file, .NET needs to be able to move back and forth in a file, which means it requires a seekable stream. The solution is fairly simple. Instead of saving directly to Response.OutputStream, you can create a System.IO.MemoryStream object, which represents an in-memory buffer of data. The MemoryStream is always seekable, so you can save the image to this object. Once you’ve performed this step, you can easily copy the data from the MemoryStream to the Response.OutputStream. The only disadvantage is that this technique requires more memory because the whole graphic needs to be held in memory at once. However, the graphics you use in web pages generally aren’t that large, so you probably won’t observe any reduction in performance.
1030 C H A P T E R 3 0 ■ DY N A M I C G R A P H I C S A N D G D I +
Here’s the code you need to implement this solution, assuming you’ve imported the System.IO namespace:
Response.ContentType = "image/png";
//Create the PNG in memory. MemoryStream mem = new MemoryStream();
image.Save(mem, System.Drawing.Imaging.ImageFormat.Png);
//Write the MemoryStream data to the output stream. mem.WriteTo(Response.OutputStream);
//Clean up.
g.Dispose();
image.Dispose();
Passing Information to Dynamic Images
When you use this technique to embed dynamic graphics in web pages, you also need to think about how the web page can send information to the code that generates the dynamic graphic. For example, what if you don’t want to show a fixed piece of text but you want to generate a dynamic label that incorporates the name of the current user? (In fact, if you do want to show a static piece of text, it’s probably better to create the graphic ahead of time and store it in a file, rather than generating it using GDI+ code each time the user requests the page.) One solution is to pass the information using the query string. The page that renders the graphic can then check for the query string information it needs.
The following example uses this technique to create a data-bound list that shows a thumbnail of every bitmap in a given directory. Figure 30-9 shows the final result.
This page needs to be designed in two parts: the page that contains the GridView and the page that dynamically renders a single thumbnail. The GridView page will call the thumbnail page multiple times (using <img> tags) to fill the list.
It makes sense to design the page that creates the thumbnail first. To make this component as generic as possible, you shouldn’t hard-code any information about the directory to use or the size of a thumbnail. Instead, this information will be retrieved through three query string arguments.
The first step that you need to perform is to check that all this information is supplied when the page first loads, as shown here:
protected void Page_Load(object sender, System.EventArgs e)
{
if ((Request.QueryString["X"] == null) || (Request.QueryString["Y"] == null) || (Request.QueryString["FilePath"] == null))
{
//There is missing data, so don't display anything.
//Other options include choosing reasonable defaults
//or returning an image with some static error text.
}
else
{
int x = Int32.Parse(Request.QueryString["X"]); int y = Int32.Parse(Request.QueryString["Y"]);
string file = Server.UrlDecode(Request.QueryString["FilePath"]);
...
C H A P T E R 3 0 ■ DY N A M I C G R A P H I C S A N D G D I + |
1031 |
Figure 30-9. A data-bound thumbnail list
Once you have the basic set of data, you can create your Bitmap and Graphics objects as always. In this case, the Bitmap dimensions should correspond to the size of the thumbnail, because you don’t want to add any additional content:
...
// Create the in-memory bitmap where you will draw the image.
Bitmap image = new Bitmap(x, y);
Graphics g = Graphics.FromImage(image);
...
Creating the thumbnail is easy. All you need to do is load the image (using the static Image.FromFile() method) and then draw it on the drawing surface. When you draw the image, you specify the starting point, (0, 0), and the height and width. The height and width correspond to the size of the Bitmap object. The Graphics class will automatically scale your image to fit these dimensions, using antialiasing to create a high-quality thumbnail:
...
// Load the file data. System.Drawing.Image thumbnail = System.Drawing.Image.FromFile(file);
1032 C H A P T E R 3 0 ■ DY N A M I C G R A P H I C S A N D G D I +
// Draw the thumbnail. g.DrawImage(thumbnail, 0, 0, x, y);
...
Lastly, you can render the image and clean up, as follows:
...
// Render the image. image.Save(Response.OutputStream, ImageFormat.Jpeg); g.Dispose();
image.Dispose();
}
}
The next step is to use this page (named ThumbnailViewer.aspx) in the page that contains the GridView. The basic idea is that the user will enter a directory path and click the submit button. At this point, your code can perform a little work with the System.IO classes. First, you need to create a DirectoryInfo object that represents the user’s choice. Second, you need to retrieve a collection of FileInfo objects that represent files in that directory using the DirectoryInfo.GetFiles() method. To narrow the selection down so that it includes only bitmaps, you use the search expression *.bmp. Finally, the code binds the array of FileInfo objects to a GridView, as shown here:
protected void cmdShow_Click(object sender, System.EventArgs e)
{
//Get a string array with all the image files. DirectoryInfo dir = new DirectoryInfo(txtDir.Text); gridThumbs.DataSource = dir.GetFiles("*.bmp");
//Bind the string array.
gridThumbs.DataBind();
}
It’s up to the GridView template to determine how the bound FileInfo objects are displayed. In this example, you need to show two pieces of information—the short name of the file and the corresponding thumbnail. Showing the short name is straightforward. You simply need to bind to the FileInfo.Name property. Showing the thumbnail requires using an <img> tag to invoke the ThumbnailViewer.aspx page. However, constructing the right URL can be a little tricky, so the best solution is to hand the work off to a method in the web-page class called GetImageUrl().
Here’s the complete GridView declaration with the template:
<asp:GridView ID="gridThumbs" runat="server" AutoGenerateColumns="False" Font-Names="Verdana" Font-Size="X-Small" GridLines="None">
<Columns>
<asp:TemplateField>
<ItemTemplate>
<img src='<%# GetImageUrl(Eval("FullName")) %>' /> <%# Eval("Name") %>
<hr/>
</ItemTemplate>
</asp:TemplateField>
</Columns>
</asp:GridView>
The GetImageUrl() method examines the full file path, encodes it, and adds it to the query string so ThumbnailViewer.aspx can find the required file. At the same time, the GetImageUrl() method also chooses a thumbnail size of 50 by 50 pixels. Note that the file path is URL-encoded. That’s because filenames commonly include characters that aren’t allowed in URLs, like the space:
1034 C H A P T E R 3 0 ■ DY N A M I C G R A P H I C S A N D G D I +
■Tip If you’re worried about confusing your real web pages with the web pages you use to supply GDI+ drawing, consider using a custom HTTP handler to generate the image. With an HTTP handler, your image generators can have a custom extension and use essentially the same code in the ProcessRequest() method. HTTP handlers were first demonstrated in Chapter 5.
The Custom Control Class
The first step is to create the control class. This class (named GradientLabel) derives from Control rather than WebControl. That’s because it won’t be able to support the rich set of style properties because it renders a dynamic graphic, not an HTML tag.
public class GradientLabel : Control { ... }
The GradientLabel class provides five properties, which allow the user to specify the text, the font size, and the colors that are used for the gradient and text, as follows:
public string Text
{
get { return (string)ViewState["Text"]; } set { ViewState["Text"] = value; }
}
public int TextSize
{
get { return (int)ViewState["TextSize"]; } set { ViewState["TextSize"] = value; }
}
public Color GradientColorA
{
get { return (Color)ViewState["ColorA"]; } set { ViewState["ColorA"] = value; }
}
public Color GradientColorB
{
get { return (Color)ViewState["ColorB"]; } set { ViewState["ColorB"] = value; }
}
public Color TextColor
{
get { return (Color)ViewState["TextColor"]; } set { ViewState["TextColor"] = value; }
}
The properties are set to some sensible defaults in the GradientLabel constructor, as shown here:
public GradientLabel()
{
Text = "";
TextColor = Color.White; GradientColorA = Color.Blue;
1036 C H A P T E R 3 0 ■ DY N A M I C G R A P H I C S A N D G D I +
Here’s the portion of the drawing code that retrieves the query string information and measures the text:
...
// Define the font.
Font font = new Font("Tahoma", textSize, FontStyle.Bold);
//Use a test image to measure the text. Bitmap image = new Bitmap(1, 1); Graphics g = Graphics.FromImage(image); SizeF size = g.MeasureString(text, font); g.Dispose();
image.Dispose();
//Using these measurements, try to choose a reasonable bitmap size.
//Even if the text is large, cap the size at some maximum to
//prevent causing a serious server slowdown!
int width = (int)Math.Min(size.Width + 20, 800); int height = (int)Math.Min(size.Height + 20, 800); image = new Bitmap(width, height);
g = Graphics.FromImage(image);
...
You’ll see that in addition to the size needed for the text, an extra 20 pixels are added to each dimension. This allows for a padding of 10 pixels on each side.
Finally, you can create the LinearGradientBrush, paint the drawing surface, and then add the text, as follows:
...
LinearGradientBrush brush = new LinearGradientBrush( new Rectangle(new Point(0,0), image.Size),
gradientColorA, gradientColorB, LinearGradientMode.ForwardDiagonal);
//Draw the gradient background. g.FillRectangle(brush, 0, 0, 300, 300);
//Draw the label text.
g.DrawString(text, font, new SolidBrush(textColor), 10, 10);
// Render the image to the output stream. image.Save(Response.OutputStream, System.Drawing.Imaging.ImageFormat.Jpeg);
g.Dispose();
image.Dispose();
}
To test the label, you can create a control tag like this:
<cc1:gradientlabel id="GradientLabel1" runat="server" Text="Test String" GradientColorA="MediumSpringGreen" GradientColorB="RoyalBlue"></cc1:gradientlabel>
Figure 30-11 shows the rendered result.
C H A P T E R 3 0 ■ DY N A M I C G R A P H I C S A N D G D I + |
1037 |
Figure 30-11. A GDI+ label custom control
This control still has many shortcomings. Notably, it can’t size the drawing surface or wrap its text dynamically, and it doesn’t allow the user to set the text font or the spacing between the text and the border. To complete the control, you would need to find a way to pass this extra information in the query string. Clearly, if you want to create a practical web control using GDI+, you have a significant amount of work to do.
Charting with GDI+
When the query approach works, it’s a great, logical way to solve the problem of sending information from an ordinary page to a page that creates a dynamic graphic. However, it won’t always work. One of the problems with the query string is that it’s limited to a relatively small amount of string data. If you need to send something more complex, such as an object or a block of binary data, you need to find another technique.
One realistic solution is to use the Session collection. This has more overhead, because everything you put in the Session collection uses server memory, but it allows you to transmit any serializable type of data, including custom objects. To get a feel for why you might want to use
the Session collection, it helps to consider a more advanced example.
The next example uses GDI+ to create a graphical pie chart. Because the pie chart is drawn dynamically, your code can build it according to information in a database or information supplied by the user. In this example, the user adds each slice of the pie using the web page, and the image is redrawn automatically. The slices are sent to the dynamic image page through the Session collection as special PieSlice objects.
To create this example, the first step is to create the PieSlice object. Each PieSlice includes a text label and a numeric value, as shown here:
public class PieSlice
{
private float dataValue; public float DataValue
{
get {return dataValue;} set {dataValue = value;}
}
private string caption; public string Caption
