圖形、GDI + 和圖表(在網頁上嵌入動態圖形)

weixin_34279184發表於2012-12-11

       使用 Image.Save()方法將一個影像儲存到一個響應流時,會覆蓋所有 ASP.NET 要用到的控制元件。這有一個解決方案,可以使用 HTML 的 <img> 標籤或者 Image Web 控制元件來連結到一個生成動態影像的 .aspx 檔案。

       建立 GDI+ 影像通常比提供一個靜態影像慢一個數量級,因此,使用 GDI+ 多次重複繪製圖形按鈕或其他元素絕不是一個好主意。(如果真要這麼做,要考慮快取或者儲存生成的影像檔案以提高效能。)

 

使用 PNG 格式

       PNG 是一種通用格式,這種格式將 GIF 的無失真壓縮和 JPEG 的豐富色彩結合起來以支援高質量影像。

       動態生成的 PNG 影像有一個問題,即不能使用 BitMap.Save()方法。Response.OutputStream 是一個線性流,你只能從頭到尾順序寫入資料。要建立一個 PNG 檔案,.NET 需要能夠在一個檔案裡來回前後的定位,需要一個可定位的流。

       解決方案也很簡單,可以建立一個 System.IO.MemoryStream 物件(記憶體裡的一個緩衝區),將影像儲存到這個物件後,就能很容易的從 MemoryStream 複製資料到 Response.OutputStream 了。

Response.ContentType = "image/png";
 
MemoryStream mem = new MemoryStream();
image.Save(mem,ImageFormat.Png);
 
mem.WriteTo(Response.OutputStream);
 
g.Dispose();
image.Dispose();

 

 

傳遞資訊給動態影像

       當使用這個技術在網頁裡動態生成圖形時,可以在網頁中向動態生成圖形的程式碼傳遞資訊。

 

       下面的例子,使用這一技巧建立一個資料繫結列表,顯示一個給定目錄中每個點陣圖的縮圖。

       這個頁面需要設計為兩部分:包含 GridView 的頁面(多次呼叫縮圖頁面來填充列表)和動態呈現一個單一縮圖的頁面(多次被呼叫)。

       首先,設計建立縮圖的頁面,為了儘可能通用,不應該硬編碼任何關於要使用的目錄和縮圖大小的資訊,應從呼叫者處獲取這些資訊:

protected void Page_Load(object sender, EventArgs e)
{
    if (string.IsNullOrEmpty(Request.QueryString["X"]) ||
        string.IsNullOrEmpty(Request.QueryString["Y"]) ||
        string.IsNullOrEmpty(Request.QueryString["FilePath"]))
    {
        // There is missing data, so don't display anything.
        // Or return 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"]));
        ...

 

       一旦獲得了這些基本資料,就可以建立 BitMap 和 Graphics 物件了。本例中,BitMap 大小應該與縮圖大小相對應:

Bitmap image = new Bitmap(x, y);
Graphics g = Graphics.FromImage(image);

 

       Graphics 類會自動伸縮你的影像來適應這些寬窄,使用反鋸齒技術來建立一個高質量的縮圖

Bitmap image = new Bitmap(x, y);
Graphics g = Graphics.FromImage(image);
 
System.Drawing.Image thumbnail = System.Drawing.Image.FromFile(file);
g.DrawImage(thumbnail, 0, 0, x, y);
 
image.Save(Response.OutputStream, ImageFormat.Jpeg);
g.Dispose();
image.Dispose();
    }
}

 

       接著,是在包含 GridView 的頁面中使用這個頁面。基本思路是使用者輸入目錄路徑並單擊“提交”按鈕,此時,你的程式碼可以使用 System.IO 類來做點事

protected void btnShow_Click(object sender, EventArgs e)
{
    DirectoryInfo dir = new DirectoryInfo(txtDir.Text);
    gridThumbs.DataSource = dir.GetFiles();
    gridThumbs.DataBind();
}

 

       繫結了 FileInfo 陣列之後,如何顯示完全由 GridView 模板決定。本例中,需要顯示兩段資訊,即檔案的短名和對應的縮圖。短名只需要繫結到 FileInfo.Name 屬性即可。縮圖需要使用 <img> 標籤來呼叫動態圖片頁面,不過,構建正確的 URL 有一點點麻煩,因此將該工作交給網頁類中的 GetImageUrl()方法來完成。

protected string GetImageUrl(object path)
{
    return "ThumbnailViewer.aspx?x=50&y=50&FilePath="
        + Server.UrlEncode((string)path);
}
<div>
    Directory:<asp:TextBox runat="server" ID="txtDir"></asp:TextBox>
    <br />
    <br />
    <asp:Button ID="btnShow" runat="server" OnClick="btnShow_Click" Text="Show Thumbnails" />
    <br />
    <br />
    <asp:GridView ID="gridThumbs" runat="server" AutoGenerateColumns="False" Font-Names="Verdana"
        Font-Size="X-Small" GridLines="None">
        <Columns>
            <asp:TemplateField>
                <ItemTemplate>
                    <img alt="" src='<%# GetImageUrl(Eval("FullName")) %>' />
                    <%
# Eval("Name") 
%>
                    <hr />
                </ItemTemplate>
            </asp:TemplateField>
        </Columns>
    </asp:GridView>
</div>

image

      檔案路徑以 URL 形式編碼,這是因為檔名通常包含 URL 禁止的字元,比如空格。因此這一小小改變非常重要。

 

使用 GDI+ 的自定義控制元件

       你可能已經想急於使用 GDI+ 來建立封裝的很好的自定義控制元件,遺憾的是,ASP.NET 並沒有為你在網頁裡嵌入 GDI+ 影像變得簡單。

       如你所見,使用 GDI+ 需要建立一個獨立的網頁輸出影像,然後使用 <img> 標籤將這個頁面的內容嵌入進來。因此,你不能簡單的直接在一個網頁上拖放一個使用了 GDI+ 的自定義控制元件。

       你可以做的是建立一個包裝了 <img> 標籤的自定義控制元件。這個控制元件提供一個方便的程式設計介面、包括屬性、方法和事件。不過,該控制元件實際上並不能生成影像,它可以從它的屬性獲取資料,構建 URL 的查詢字串部分,然後將自己呈現為一個 <img> 標籤,該標籤指向真正完成影像生成的頁面。

       這個自定義控制元件提供了更高階別的包裝,這個包裝抽象了傳遞資訊到你的 GDI+ 頁面的過程!

       也可以考慮使用 HTTP 處理程式來生成影像。你的影像生成器可以具有一個自定義的副檔名。

 

1. 自定義控制元件類

       與任何自定義控制元件類一樣,可以放在網站的 App_Code 資料夾中,或是放在一個單獨的類庫專案中。

       本例中的自定義控制元件類從 Control 類派生,而不從 WebControl 類派生。因為它不能支援富樣式屬性集,因為它僅僅呈現動態圖形,而不是一個 HTML 標籤

public class GradientLabel : Control
{
    public string Text
    {
        get { return (string)ViewState["Text"]; }
        set { ViewState["Text"] = value; }
    }
 
    public int TextSize
    {
        get { return (int)ViewState["TextSize"]; }
        set { ViewState["TextSize"] = value; }
    }
 
    /// <summary>
    /// 漸變的起始顏色
    /// </summary>
    public Color GradientColorStart
    {
        get { return (Color)ViewState["GradientColorStart"]; }
        set { ViewState["GradientColorStart"] = value; }
    }
 
    /// <summary>
    /// 漸變的結束顏色
    /// </summary>
    public Color GradientColorEnd
    {
        get { return (Color)ViewState["GradientColorEnd"]; }
        set { ViewState["GradientColorEnd"] = value; }
    }
 
    public Color TextColor
    {
        get { return (Color)ViewState["TextColor"]; }
        set { ViewState["TextColor"] = value; }
    }
 
    // 建構函式將屬性設定成一些合理的預設值
    public GradientLabel()
    {
        Text = "";
        TextColor = Color.White;
        GradientColorStart = Color.Blue;
        GradientColorEnd = Color.DarkBlue;
        TextSize = 14;
    }
 
    protected override void Render(HtmlTextWriter writer)
    {
        HttpContext context = HttpContext.Current;
        writer.Write(string.Format(@"<img src='GradientLabel.aspx?Text={0}&TextSize={1}
            &TextColor={2}&GradientColorStart={3}&GradientColorEnd={4}' />",
            context.Server.UrlEncode(Text), TextSize.ToString(), TextColor.ToArgb(), 
            GradientColorStart.ToArgb(), GradientColorEnd.ToArgb()));
    }
}

 

2. GDI+ 影像呈現頁面

       這個呈現頁面有一個有趣的挑戰:即文字和字型大小是動態提供的!因此,不可能使用一個固定大小的點陣圖,太小會截斷文字,太大會浪費過多的記憶體且傳遞頁面太費時。

       解決方案是建立所需的 Font 物件,然後呼叫 Graphics.MeasureString 引數來判斷要顯示你的文字需要多少畫素。唯一需要注意的是,要避免點陣圖過大。為避免這個風險,呈現程式碼強制 800 畫素高和寬的限制。(還可使用 DrawString()方法的另一個版本,即接受一個放置文字的矩形,有多行空間的話,這個方法會自動包裝文字,允許顯示超過幾行的大量文字。)

protected void Page_Load(object sender, EventArgs e)
{
    string text = Server.UrlDecode(Request.QueryString["Text"]);
    int textSize = int.Parse(Request.QueryString["TextSize"]);
    Color textColor = Color.FromArgb(int.Parse(Request.QueryString["TextColor"]));
    Color gradientColorStart = Color.FromArgb(int.Parse(Request.QueryString["GradientColorStart"]));
    Color gradientColorEnd = Color.FromArgb(int.Parse(Request.QueryString["GradientColorEnd"]));
 
    // 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.
    // 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);
 
    // 使用線性漸變封裝 System.Drawing.Brush
    LinearGradientBrush brush = new LinearGradientBrush(
        new Rectangle(new Point(0, 0), image.Size), gradientColorStart, 
        gradientColorEnd, LinearGradientMode.ForwardDiagonal);
 
    // Draw the gradient background.
    g.FillRectangle(brush, 0, 0, width, height);
 
    // Draw the label text.
    g.DrawString(text, font, new SolidBrush(textColor), 10, 10);
 
    // Render the image to the output stream.
    image.Save(Response.OutputStream,ImageFormat.Jpeg);
 
    g.Dispose();
    image.Dispose();
}

       除了文字的大小外,每邊都增加了 20 畫素,這允許每一位度都增加了 10 個畫素。

 

3. 測試效果

<cc1:GradientLabel ID="GradientLabel1" runat="server" Text="Test String"
 GradientColorStart="MediumSpringGreen" GradientColorEnd="RoyalBlue">
</cc1:GradientLabel>

image 

 

       把資訊從一個頁面傳送到另一個頁面是 GDI+ 另一個不錯的應用,但是對查詢字串大小的限制意味著它能處理的資料量較有限。對於較大的資料量,可以採用 Session 傳遞,Session 能傳遞任何可序列化的資料,不過也需要顧及到伺服器的開銷。

相關文章