程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> 關於C# >> C#發現之旅第八講 ASP.NET圖形開發帶超鏈接的餅圖

C#發現之旅第八講 ASP.NET圖形開發帶超鏈接的餅圖

編輯:關於C#

為了讓大家更深入的了解和使用C#,我們將開始這一系列的主題為“C#發現之旅 ”的技術講座。考慮到各位大多是進行WEB數據庫開發的,而所謂發現就是發現我們所 不熟悉的領域,因此本系列講座內容將是C#在WEB數據庫開發以外的應用。目前規劃的主要內 容是圖形開發和XML開發,並計劃編排了多個課程。在未來的C#發現之旅中,我們按照由淺入 深,循序漸進的步驟,一起探索和發現C#的其他未知的領域,更深入的理解和掌握使用C#進 行軟件開發,拓寬我們的視野,增強我們的軟件開發綜合能力。

本系列課程配套的演示代碼。

課程說明

經過以前的課程,大家都掌握了一些圖形編程技術,現在我們可以 將把圖形編程技術投入到實際應用中去。在本課程中,我們一起研究使用圖形編程在ASP.NET 中實現一個帶超鏈接的餅圖。

功能需求

現客戶提出如下需求,

在 ASP.NET的程序中顯示一個二維的橢圓形的餅圖界面。

其中每一個餅塊具有提示文本 和一個超鏈接。

當用戶鼠標移動到某個餅塊時浏覽器會顯示一些提示文本。

當用戶點擊該餅塊則鏈接到其他頁面使得頁面能顯示和該餅塊相關的詳細信息。

該軟件的用戶界面如圖所示

運行軟件

根據軟件功能需求,我已經開發出了這個軟件,首先演示運行一下這個軟件,給大家 一個直觀的了解。

演示程序中有一個pie.aspx的頁面。該頁面就包含了一個帶超鏈接 的餅圖對象,在浏覽器中打開這個頁面,可以看到如下的用戶界面

這個頁面中 顯示一個餅圖,每一個扇型都顯示數據庫中一個客戶的總訂單金額,總共顯示了10個客戶。 我們鼠標移動到一個餅圖項目上,可以顯示出這個客戶的名稱和訂單金額。當鼠標點擊餅圖 項目時,就會鏈接到另外一個頁面。該頁面也顯示一個餅圖,其中的項目用於顯示單個訂單 的金額。鼠標放在某個項目上可以看到訂單的時間,地點,訂單人姓名和訂單金額。

在這裡,第一個頁面為主頁面,第二個頁面為明細頁面。我們察看主頁面的HTML代 碼,可以看到代碼為

<img src='pieimage.aspx? name=pie_customers' usemap='#dea88afea25748bda1ae28aa3e260d1a'
border='0' />
<map name="dea88afea25748bda1ae28aa3e260d1a">
<area shape="poly" coords="200,150,399,145,400,150," href="pie_orders.aspx?customerid=ALFKI"
title="客戶名稱:三川 實業有限公司訂單金額:518.7 點擊察看該客戶訂單的詳細情況" />
<area shape="poly" coords="200,150,397,125,399,135,399,145," href="pie_orders.aspx?customerid=ANATR"
title="客戶名稱:東南 實業訂單金額:2581.95 點擊察看該客戶訂單的詳細情況" />
<area shape="poly" coords="200,150,372,73,376,79,380,85,384,92,387,98,390,105,393,111,395,118,3 97,125,"
href="pie_orders.aspx?customerid=ANTON" title=" 客戶名稱:坦森行貿易訂單金額:7023.97 點擊察看該客戶訂單的詳細情況" />
<area shape="poly" coords="200,150,269,9,285,14,300,20,314,27,328,34,340,43,352,52,362,62,372,7 3,"
href="pie_orders.aspx?customerid=AROUT" title="客戶 名稱:國頂有限公司訂單金額:12899.15 點擊察看該客戶訂單的詳細情況" />
</map>

我們使用圖片來顯示餅圖圖形,使用map元素來在圖片上設置 提示文本和超鏈接,使用多邊形來模擬扇形的超鏈接區域。明細頁面也類似。

軟件設 計

文檔對象模型

餅圖是一種很常見的展示數據的方式,使用文檔對象模型的 軟件設計思想來分析這個餅圖形狀,可以很容易的知道,餅圖可以抽象為餅圖對象本身和餅 圖項目。

餅圖對象可以定義整體餅圖的位置和大小,它的邊界就是包含整個餅圖的橢 圓的外切矩形,而且餅圖對象是個容器,可以容納若干個餅圖項目。

餅圖項目定義單 個扇形區域,包括餅圖項目對應的數值,提示文本,超鏈接地址,扇形區域的起始角度和終 止角度。

程序結構設計

該軟件應用到ASP.NET中,在桌面程序和WEB程序中應 用圖形編程具有很大的差別,WEB程序沒有視圖控件的概念,通常是采用服務器端生成包含圖 形的圖片文檔來顯示圖形,由於WEB程序底層的HTTP傳輸協議是無狀態的,因此程序結構比較 復雜,其結構如圖所示

在這裡,采 用服務器端生成包含圖形的圖片來顯示圖形。由於HTML頁面是純文本的文檔,不能包含圖片 文檔等二進制數據,因此必須采用至少兩個頁面來配合顯示包含圖文的頁面,其中主頁面來 顯示HTML文檔,而圖片服務頁面則專門來生成圖片文檔。

在這裡說明一下程序的流程 ,首先客戶端浏覽器向主頁面發出請求,該頁面中需要顯示圖形,於是生成一個圖形文檔對 象,向這個圖形文檔對象填充數據,然後根據圖形文檔對象和圖片服務頁面的接口來生成專 門顯示圖片的HTML代碼,同時還得講圖片文檔對象放入到一個臨時數據容器中,最常用的就 是session容器。

主頁面處理完成後將生成的HTML文檔發送到客戶端浏覽器,浏覽器 解析HTML 文檔,並根據其引用的圖片URL向服務器端的圖片服務頁面發送請求,最常見的就 是IMG元素的SRC屬性。圖片服務頁面根據URL中的參數從臨時數據容器中獲得圖形文檔對象, 從中獲得圖像數據,然後轉發到浏覽器,浏覽器獲得圖像數據後才能完整的展示主頁面生成 的HTML文檔。

整個過程比較復雜,需要對HTTP和HTML有比較深的了解。

當然 我們可以去掉圖片服務頁面,在主頁面中生成圖片,然後將圖片保存到一個臨時文件中,這 樣做不甚合理,需要開放WEB程序訪問服務器文件系統的權限,而且圖片文件是比較大的,占 磁盤空間,需要及時刪除,而如何刪除這些臨時文件也是一個問題。而采用圖片服務頁面是 在內存中生成圖像文檔的,不用寫臨時文件,這樣做方便網站的維護。

軟件代碼說明

根據軟件設計,使用VS.NET2003開發出這樣的程序,現對該程序代碼進行詳細說明。 首先說明一下餅圖文檔對象模型。

餅圖項目對象 PieShapeItem

本類型定義 了餅圖中的一個項目,代碼如下

/// <summary>
/// 單個餅圖形 狀項目
/// </summary>
/// <remarks>該類型是PieShape列表的成 員類型</remarks>
[System.Serializable()]
public class PieShapeItem
{
  private double dblValue = 0 ;
  /// <summary>
  /// 數值
  /// </summary>
  public double Value
  {
    get{ return dblValue ;}
    set{ dblValue = value;}
  }
  private string strText = null;
   /// <summary>
  /// 對象文本
  /// </summary>
   public string Text
  {
    get{ return strText ;}
     set{ strText = value;}
  }
  private string strLink = null;
   /// <summary>
  /// 項目鏈接地址
  /// </summary>
  public string Link
  {
    get{ return strLink ;}
     set{ strLink = value;}
  }
  private Color intColor = Color.Black ;
  /// <summary>
  /// 項目顏色
  /// </summary>
  public Color Color
  {
    get{ return intColor ;}
    set{ intColor = value;}
  }
  /// <summary>
  /// 開始角度
  /// </summary>
   internal float StartAngle = 0 ;
  /// <summary>
  /// 結束角 度
  /// </summary>
  internal float EndAngle = 0 ;
}//public class PieShapeItem

這個代碼很簡單,也就是定義了一個餅圖 項目的一些屬性。

餅圖文檔對象 PieShape

本對象定義了一個完整的餅圖, 是本程序的核心模塊。本對象首先是PieShapeItem的一個強類型列表,它是從 System.Collections.CollectionBase上面派生的,通過使用對象的Add或Remove方法就能往 這個對象添加或刪除餅圖項目對象PieShapeItem。當向列表添加項目時未指定項目顏色時會 創建一個默認顏色,本類型定義了一個StdColors的靜態的顏色數組,新餅圖元素的顏色就根 據這個顏色數組進行分配。

對象定義了一個RefreshState方法用來計算各個餅圖元素 對應的扇形區域的起始角度和終止角度。

/// <summary>
/// 刷 新對象狀態
/// </summary>
/// <remarks>
/// 本函數中反 向遍歷所有的餅圖項目,
/// 計算各個餅圖項目的起始和終止角度 </remarks>
public void RefreshState()
{
  double TotalValue = 0 ;
  foreach( PieShapeItem item in this )
  {
     TotalValue += item.Value ;
  }
  float AngleCount = 0 ;
  for( int iCount = this.Count - 1 ; iCount >= 0 ; iCount -- )
  {
    PieShapeItem item = this[ iCount ] ;
    float angle = ( float ) ( 360.0 * item.Value / TotalValue ) ;
    item.StartAngle = ( float ) Math.Round( AngleCount , 3 ) ;
    item.EndAngle = ( float ) Math.Round( AngleCount + angle , 3 ) ;
    AngleCount += angle ;
     item.StartAngle = ( float ) Math.Round( FixAngle( item.StartAngle ) , 3 );
    item.EndAngle = ( float ) Math.Round( FixAngle( item.EndAngle ) , 3 ) ;
  }
}
/// <summary>
/// 根據橢圓形狀修正角度
/// </summary>
/// <param name="angle">原始角度 </param>
/// <returns>修正後的角度值</returns>
private float FixAngle( float angle )
{
  if( ( angle % 90.0 ) == 0 )
    return angle ;
  if( intWidth == intHeight )
     return angle ;
  double x = intWidth * Math.Cos( angle * Math.PI / 180 );
  double y = intHeight * Math.Sin( angle * Math.PI / 180 );
   float result = ( float ) ( Math.Atan2( y , x ) * 180 / Math.PI );
  if( result < 0 )
    result += 360 ;
  return result ;
}

在這個方法中,我們首先計算所有項目的數值和。然後遍歷所有的項目,計 算它的扇形區域的起始角度和終止角度。由於要顯示的是橢圓形餅圖,相對於正圓圖形是被 壓扁的,因此這個起始角度和終止角度需要調用FixAngle函數進行修正。

由於在計算 機屏幕上繪制扇形是順時針方向的,而我們察看一般習慣是順時針看的,因此這裡需要反向 遍歷餅圖項目。

計算各個餅圖項目的角度數據後,我們可以繪制圖形或者獲得圖形定 位信息。首先我們定義函數CreatePath函數來獲得指定餅圖項目的路徑對象。代碼如下。

/// <summary>
/// 為一個餅圖項目創建路徑對象
/// </summary>
/// <param name="item">餅圖項目 </param>
/// <returns>創建的路徑對象</returns>
public GraphicsPath CreatePath( PieShapeItem item )
{
  GraphicsPath path = new GraphicsPath();
  path.AddPie(
    intLeft ,
     intTop ,
    intWidth ,
    intHeight ,
     item.StartAngle ,
    item.EndAngle - item.StartAngle );
  return path ;
}

這個函數也很簡單,創建一個路徑對象,然後添加一個扇形區 域。

我們定義了一個Draw函數來繪制餅圖圖形。其代碼為

/// <summary>
/// 繪制餅圖圖形
/// </summary>
/// <param name="g">圖形繪制對象</param>
/// <param name="ClipRectangle">剪切矩形</param>
public void Draw( Graphics g , Rectangle ClipRectangle )
{
  foreach( PieShapeItem item in this )
  {
    using( GraphicsPath path = CreatePath( item ))
    {
      using( SolidBrush b = new SolidBrush( item.Color ))
      {
        g.FillPath( b , path );
         g.DrawPath( Pens.Black , path );
      }
    }
  }
}

這個方法也不復雜,也就是遍歷所有的餅圖項目,為每一個項 目創建路徑對象,然後使用餅圖項目的顏色填充路徑,並使用黑線繪制路徑的邊框。

由於本程序是ASP.NET程序,需要在服務器端生成圖片文檔,因此本對象也提供了 CreateBitmap函數來生成包含圖形的位圖對象。其代碼為

/// <summary>
/// 創建一個包含對象圖形位圖對象
/// </summary>
/// <returns>創建的位圖對象</returns>
public Bitmap CreateBitmap( )
{
  Bitmap bmp = new Bitmap( intWidth + 1 , intHeight + 1 ) ;
  using( Graphics g = Graphics.FromImage( bmp ))
  {
    g.Clear( Color.White );
     g.TranslateTransform( intLeft , intTop );
    g.SmoothingMode = SmoothingMode.HighQuality ;
    Draw( g , this.Bounds );
  }
  return bmp ;
}

這個函數裡面首先根據對象大小生成一個位圖對 象,然後在這個位圖對象上創建一個圖形繪制對象,填充白色背景,並進行一下坐標轉換, 然後調用Draw函數繪制對象圖形。然後函數就返回創建的位圖對象了。

這個對象還定 義了一個GetHtmlString的函數向主頁面提供一個顯示餅圖的HTML代碼。

/// <summary>
/// 創建用於顯示餅圖圖片和超鏈接的HTML代碼字符串
/// </summary>
/// <param name="imgsrc">圖片地址 </param>
/// <returns>創建的HTML字符串</returns>
/// <remarks>
/// 此處沒有簡單拼湊HTML字符串,而是利用XML和HTML的相似性
/// 使用一個XmlTextWriter來生成HTML字符串。
/// </remarks>
public string GetHtmlString( string imgsrc )
{
  if( this.Count == 0 )
    return "";
  // 生成唯一的 map 元素名稱
  string name = System.Guid.NewGuid().ToString("N");
  // 生成  XmlTextWriter 對象
  System.IO.StringWriter myStr = new System.IO.StringWriter();
  System.Xml.XmlTextWriter writer = new XmlTextWriter( myStr );
  writer.IndentChar = ' ' ;
   writer.Indentation = 3 ;
  writer.Formatting = System.Xml.Formatting.Indented ;
  // 開始輸出HTML
   writer.WriteStartDocument();
  // 輸出圖片元素
  writer.WriteRaw ("<img src='" + imgsrc + "' usemap='#" + name + "' border='0'/>");
  // 輸出 map 元素
   writer.WriteStartElement("map");
  writer.WriteAttributeString ("name" , name );
  foreach( PieShapeItem item in this )
   {
    // 輸出超鏈接區域
    Point[] ps = this.GetPoints( item );
    writer.WriteStartElement("area");
     writer.WriteAttributeString("shape" , "poly");
     writer.WriteStartAttribute("coords" , null );
    for( int iCount = 0 ; iCount < ps.Length ; iCount ++ )
    {
       writer.WriteString( ps[ iCount ].X.ToString() );
       writer.WriteString("," );
      writer.WriteString( ps[ iCount ].Y.ToString() );
      writer.WriteString("," );
    }
    writer.WriteEndAttribute();
    if( item.Link != null && item.Link.Length > 0 )
    {
       writer.WriteAttributeString("href" , item.Link );
    }
    writer.WriteAttributeString("title" , item.Text );
     writer.WriteEndElement();
  }
  writer.WriteEndElement();
   writer.WriteEndDocument();
  writer.Close();
  string html = myStr.ToString();
  // 修正輸出的HTML字符串
  int index = html.LastIndexOf("?>");
  if( index > 0 )
  {
    html = html.Substring( index + 2 );
  }
  return html ;
}
/// <summary>
/// 獲得包圍餅圖區域的點坐標數組
/// </summary>
/// <param name="item">餅圖項目 </param>
/// <returns>點坐標數組</returns>
private Point[] GetPoints( PieShapeItem item )
{
  GraphicsPath path = CreatePath( item );
  path.Flatten();
  PointF[] ps = path.PathPoints ;
  path.Dispose();
  Point[] ps2 = new Point[ ps.Length ] ;
  for( int iCount = 0 ; iCount < ps.Length ; iCount ++ )
  {
    ps2[ iCount ].X = ( int ) ( ps[ iCount ].X );
     ps2[ iCount ].Y = ( int ) ( ps[ iCount ].Y );
  }
  return ps2 ;
}

這個函數參數是圖片URL地址,該地址有主頁面的程序提供,首先我 們使用GUID類型來創建一個唯一的名稱,我們沒有直接使用字符串拼湊來生成HTML字符串, 而是使用XmlTextWriter來書寫HTML字符串。

首先是輸出img標簽,然後輸出一個map 標簽。

由於餅圖項目可以帶有超鏈接,這裡就能生成map/area元素來在圖片中定義熱 點,在HTML語法中,圖片熱點沒有扇形區域種類,因此只好使用使用多邊形來模擬扇形。也 就是使用HTML標記”<area shape=poly coords=’多邊形的頂點坐標’ />”來模擬扇形熱點。

我們遍歷所與的餅圖項目,調用GetPoints函數獲得 模擬這個項目對應的扇形區域的點坐標,將這些點坐標數據填寫到area元素的coord屬性中, 然後還填上title屬性來顯示提示文本,設置href屬性來設置這個圖片熱點的超鏈接地址。

HTML文檔輸出完畢後我們獲得這個XML字符串,去掉前面的XML聲明頭,然後就把這個 XML字符串當作HTML字符串返回給主程序了。

GetPoints函數就是獲得模擬扇形區域的 點坐標,首先是根據餅圖項目創建一個路徑,調用路徑的Flatten函數進行線段模擬運算,然 後獲得它的PathPoints屬性,這個屬性值是PointF類型的數組,需要轉換為Point數組。

主頁面 pie.aspx

這個頁面是演示程序的主頁面,界面上放置一個名為 lblResult的標簽,察看它的C#代碼也不復雜,主要是Page_Load方法,其代碼為

private void Page_Load(object sender, System.EventArgs e)
{
// 連接數據庫
using( OleDbConnection conn = new OleDbConnection())
{
conn.ConnectionString = @"Provider=Microsoft.Jet.OLEDB.4.0;Data Source="
+ this.Server.MapPath("demomdb.mdb");
conn.Open();
// 查詢數據庫
using( OleDbCommand cmd = conn.CreateCommand())
{
cmd.CommandText = @"
select top 10
customers.customerid ,
customers.companyname as 客戶名稱,
( select sum( round( orderdetails.unitprice
* orderdetails.quantity
* ( 1.0 - orderdetails.discount) , 2 ) )
from orderdetails , orders
where orderdetails.orderid = orders.orderid
and orders.customerid = customers.customerid
) as 訂單總金額
from customers";
OleDbDataReader reader = cmd.ExecuteReader();
// 創建餅圖對象
PieShape pie = new PieShape();
pie.Width = 400 ;
pie.Height = 300 ;
System.IO.StringWriter writer = new System.IO.StringWriter();
while( reader.Read())
{
string id = Convert.ToString( reader.GetValue( 0 ));
double Value = Convert.ToDouble( reader.GetValue( 2 ));
string Text = "客戶名稱:" + Convert.ToString( reader.GetValue( 1 ))
+ "\r\n訂單金額:" + Convert.ToString( reader.GetValue( 2 ))
+ "\r\n點擊察看該客戶訂單的詳細情況" ;
string Link = "pie_orders.aspx?customerid=" + id ;
pie.Add( Value , Text , Link );
}//while
reader.Close();
pie.RefreshState();
this.Session["pie_customers"] = pie ;
this.lblResult.Text = pie.GetHtmlString("pieimage.aspx?name=pie_customers");
this.DataGrid1.DataSource = pie ;
this.DataGrid1.DataBind();
}
}
}

這個函數中首先是連接數據庫,並執行一個 SQL查詢,查詢結果是10個客戶編號,名稱及其名下所有的訂單總金額。

創建一個餅 圖文檔對象,然後遍歷查詢結果,對每一條記錄都調用餅圖文檔對象的Add方法向餅圖文檔中 添加一個餅圖項目。

調用餅圖文檔對象的RefreshState方法刷新餅圖的內部狀態,然 後將餅圖對象保存到Session中拱將來的圖片服務頁面使用。然後設置lblResult標簽的文本 為餅圖對象生成的HTML字符串。

這裡還有一個DataGrid只是用來簡單的顯示餅圖文檔 的內容。

圖片服務頁面 pieimage.aspx

本頁面用來生成餅圖圖形的圖像文檔 ,該頁面沒有任何HTML代碼,其C#代碼也很簡單,只有一個Page_Load函數,其代碼為

private void Page_Load(object sender, System.EventArgs e)
{
  // 獲得參數
  string name = this.Request.QueryString ["name"] ;
  if( name == null )
  {
    return ;
  }
  // 獲得餅圖對象
  PieShape pie = this.Session[ name ] as PieShape ;
  if( pie == null )
  {
    return ;
  }
  using( Bitmap bmp = pie.CreateBitmap())
  {
     this.Response.ContentType = "image/png";
     System.IO.MemoryStream ms = new System.IO.MemoryStream();
    bmp.Save( ms , System.Drawing.Imaging.ImageFormat.Png );
    ms.WriteTo( this.Response.OutputStream );
    ms.Close();
  }
}

該頁面試圖從session中加載頁面參數指定的名稱的餅圖文檔對象,並利用該 文檔對象的CreateBmp函數獲得一個位圖對象,然後輸出PNG格式的圖像數據。

這個圖 片服務頁面要正常工作,需要事先將餅圖文檔對象保存到Session中,然後使用正確的參數來 調用這個頁面。

主頁面 pie_customers.aspx中,已經將生成的文檔對象使用名稱 pie_customers保存到Session中,同時它生成的HTML代碼中使用的圖片地址就是 pieimage.aspx?name=pie_custoemrs ,主頁面為了圖片服務頁面正常工作已經做好了充分的 准備。由於主頁面和圖片服務頁面密切配合工作,客戶端浏覽器中才能完整的顯示餅圖圖形 。

在這裡使用了位圖對象的Save函數來輸出圖片文檔的二進制數據。這裡沒有直接輸 出到頁面的輸出流中,因為那樣做是會報錯的,這裡創建了一個臨時的內存流對象,將圖片 數據輸出到這個內存流中,然後將這個內存流中的數據輸出到頁面輸出流中。

二級主 頁面 pie_orders.aspx

在主頁面pie_custoemrs.aspx中新增餅圖元素時還設置了餅 圖項目的超鏈接“pie_orders.aspx?customerid=客戶編號”,這樣用戶點擊餅圖 圖片中的熱點時就能跳轉到二級主頁面來顯示指定客戶的訂單詳細信息。

二級主頁面 和主頁面有點類似,都用一個餅圖來顯示數據。其頁面結構和處理過程也差不多,它的 Page_Load方法代碼為

private void Page_Load(object sender, System.EventArgs e)
{
string customerid = this.Request.QueryString["customerid"] ;
if( customerid == null || customerid.Length == 0 )
return ;
// 連接數據庫
using( OleDbConnection conn = new OleDbConnection())
{
conn.ConnectionString = @"Provider=Microsoft.Jet.OLEDB.4.0;Data Source="
+ this.Server.MapPath("demomdb.mdb");
conn.Open();
// 查詢數據庫
using( OleDbCommand cmd = conn.CreateCommand())
{
cmd.CommandText = @"
SELECT OrderDate AS 訂購時間,
shipname AS 運輸人,
shipaddress AS 地點,
( select
sum( round( unitprice * quantity * ( 1 - discount) , 3 ) )
from orderdetails
where orderdetails.orderid = orders.orderid
) AS 總金額
FROM orders
WHERE customerid ='" + customerid + "'" ;
OleDbDataReader reader = cmd.ExecuteReader();
// 創建餅圖對象
PieShape pie = new PieShape();
pie.Width = 400 ;
pie.Height = 300 ;
System.IO.StringWriter writer = new System.IO.StringWriter();
while( reader.Read())
{
double Value = Convert.ToDouble( reader.GetValue( 3 ));
string Text = "
  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved