袁永福 2008-5-15
系列课程说明
为了让大家更深入的了解和使用C#,我们将开始这一系列的主题为“C#发现之旅”的技术讲座。考虑到各位大多是进行WEB数据库开发的,而所谓发现就是发现我们所不熟悉的领域,因此本系列讲座内容将是C#在WEB数据库开发以外的应用。目前规划的主要内容是图形开发和XML开发,并计划编排了多个课程。在未来的C#发现之旅中,我们按照由浅入深,循序渐进的步骤,一起探索和发现C#的其他未知的领域,更深入的理解和掌握使用C#进行软件开发,拓宽我们的视野,增强我们的软件开发综合能力。
本系列课程配套的演示代码下载地址为 http://files.cnblogs.com/xdesigner/cs_discovery.zip 。
本系列课程已发布的文章有
C#发现之旅第一讲 C#-XML开发
C#发现之旅第二讲 C#-XSLT开发
C#发现之旅第三讲 使用C#开发基于XSLT的代码生成器
C#发现之旅第四讲 Windows图形开发入门
C#发现之旅第五讲 图形开发基础篇
C#发现之旅第六讲 C#图形开发中级篇
C#发现之旅第七讲 C#图形开发高级篇
C#发现之旅第八讲 ASP.NET图形开发带超链接的饼图
C#发现之旅第九讲 ASP.NET验证码技术
C#发现之旅第十讲 文档对象模型
经过以前的课程,大家都掌握了一些图形编程技术,现在我们可以将把图形编程技术投入到实际应用中去。在本课程中,我们一起研究使用图形编程在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,397,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,73," 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 = "时间:" + reader.GetValue( 0 ) + "\r\n人员:" + reader.GetValue( 1 ) + "\r\n地点:" + reader.GetValue( 2 ) + "\r\n金额:" + reader.GetValue( 3 ); string Link = "#" ; pie.Add( Value , Text , Link ); }//while reader.Close(); // 刷新饼图状态 pie.RefreshState(); this.Session["customerid"] = pie ; this.lblResult.Text = pie.GetHtmlString("pieimage.aspx?name=customerid"); this.DataGrid1.DataSource = pie ; this.DataGrid1.DataBind(); }//using }//using } |
本页面中,首先从页面参数中获得客户编号,然后连接数据库,进行SQL查询,获得指定客户编号的所有订单记录。然后将查询所得数据填充到一个饼图文档对象,然后将该文档对象使用名称customerid保存到Session中,这样未来运行的图片服务页面就从Session中获得名称为cusotmerid的饼图文档对象来显示饼图图片了。
小结在本次课程中,我们研究在在ASP.NET中使用图形编程来显示一个带超链接的饼图图形,可以看出在ASP.NET中由于WEB程序的机构特点,其图形编程比在桌面开发中要复杂,涉及了很多知识,其实是HTML和图形编程的混合。