在一个商业网站中,会经常用到一些复杂图表,用于显示一些股票市场的大盘信息或者市场的销售报告、供应需求信息。这些图表蕴含的数据库数据是动态的,而图表显示的结果是静态的,我们怎么实现这些动态图表网页的显示呢? |
首先应该指出,指望和假定浏览器的客户端能够安装特别的图表组件(如MSCHART组件)是不现实的,任何访问者都不会因为要浏览你的网页去刻意安装一个图表组件。在网页中插入一个ActiveX图表组件的方法也是不明智的,客户端一般情况下都会禁止这些可能不安全的ActiveX组件,而禁止这些ActiveX组件(或Java Applet)的下载和运行,则会使你的采用ActiveX控件(或者Java Applet)网页变得一踏糊涂。而手工把这些图表制成图片(GIF、JPG)也不可取,没有人愿意作这件吃力不讨好的事情。 |
解决图表显示最好的方案是采用ASP COM组件,通过它,你可以把数据库中的数据转换成一张图表,再转化浏览器能够识别的图像格式,通过网络服务器下载到浏览器客户端。在搜索引擎中,通过搜索ASP、CHART、Component关键字的确可以得到大量的类似组件,遗憾的是,这些组件不是免费的,既不提供源码,还有时间限制,这对一个安全至上的网络服务器来说,采用这些Demo版本图表组件显然是不可取的。自己编写一个图表的显示组件,其庞大的工作量,又会让人望而却步。 |
Office 2000/XP提供了一个功能强大WEB Chart组件,这个组件能够根据数据库产生的记录集或用户提交的信息产生各种复杂图表,这个组件同时能够把生成的图表输出为网上常用的各种图像格式,我们可以通过编写一个简单的COM组件,将输出的图像文件以一种二进制数据的格式发送到浏览器端,你也可以根据需要删除这个临时的输出文件。因为图表是作为图片格式被下载到客户端的,所以并不需要客户端下载任何COM组件,也不需要客户端安装Microsoft Office系统即可浏览这些图表。而且每个客户存取的是一个二进制的数据流,输出的图像文件只是一个临时的过渡品,每个用户都可以根据提交的信息获得一个属于自己的图表图像,各个客户互不影响,不存在彼此之间的图形文件交叉覆盖。由于整个图像的生成和发送过程大部分都是通过进程内服务的COM组件实现,其服务器资源的开销要比加载一个Excel自动化服务进程小的多。相比之下,这种方法也不失是一种不错的替代方案。 |
基本原理: |
利用内存中的Office Web Component(组件)借助于脚本可以产生一个图表(chart),也就是调用Server.CreateObject("OWC.Chart"),图表包含了所有各种需要系列和格式,ASP脚本能够调用ChartSpace的ExportPicture方法产生一个临时的GIF图表文件。接着就可以通过我们编写的COM组件通过调用Reponse.BinWrite作为二进制数据流发送到客户端。 |
我编写的COM组件提供了一个IgetFileStream接口,它共有五个输出方法,GetFileBytes、KillFile、SendBinFile。 |
Function GetFileBytes(strFileName As String) |
Function KileFile(strFileName As String) As Long |
Sub SendBinFile(FileName As String, ContentType As String, bDelete As Long, bExpires As Long, bClear As Long, bBuffer As Long, bEnd As Long) |
Sub OnStartPage(piUnk As Unknown) |
Sub OnEndPage() |
其中GetFileBytes接受一个二进制文件名,它能够返回一个指向二进制数据流的指针变量,函数的返回值可以作为参数通过Reponse.BinWrite把数据流发送到客户端。 |
KillFile主要用于删除产生的临时文件,这个函数调用了DeleteFile Win32 API函数删除文件。 |
SendBinFile方法能够同时实现发送和临时文件的删除过程。 |
其中ContentType相当于设置Reponse的ContentType属性 |
bDelete 为TRUE标识需要删除临时文件 |
bExpires为true时,将设置Reponse的Expires属性为0值 |
bClear为True时,将调用Response的Clear方法 |
bBuffer为true,将设置Reponse的Buffer属性为真 |
bEnd为True时,调用Reponse的End方法,否则将调用Flush方法 |
OnStartPage和OnEndPage是向组件的生成类中添加Active Page Component对象时生成的两个方法,通过前者获得ASP的几个内置对象,后者负责内存回收。 |
组件使用前需要用RegSvr32注册,然后利用IIS建立一个虚拟目录,使其指向你存放ASP文件的路径。如果你的IIS的站点目录为c:\Inetpub\wwwroot,你也可以在这个目录下建立一个GifFile目录,把目标文件拷贝到这个目录,只要你的IIS设置安装正确,而且机器上安装了Microsoft Office 2000/XP,只要你在浏览器地址栏键入Http:/LocalHost/GifFile/即可显示出上述的界面。 |
附ChartGIf.Asp源码: |
<%@ Language=VBScript %> |
<% |
Option Explicit |
'Response.Expires = -1 |
'Response.ContentType = "image/gif" |
'页面级对象 |
Dim m_cn '链接对象 |
Dim m_rs '记录集对象 |
Dim m_cspace 'OWC.ChartSpace object |
Dim m_fso '文件系统对象 |
Dim m_objBinaryFile '二进值文件流对象 |
Dim m_sYear '需要查询的年份 |
Dim sSQL '执行的SQL 语句 |
Dim c 'OWC 常量对象 |
Dim cht '图表临时参考变量 |
Dim ax '数轴的临时参考变量 |
Dim fnt 'OWCFont字体的临时参考变量 |
Dim sFullFileName '输出GIF文件的临时参考路径 |
'获得查询年份,缺省为2002 |
m_sYear = Request.QueryString("year") |
if len(m_sYear) = 0 then m_sYear = "2002" |
'创建一个ADO链接和记录集对象 |
set m_cn = Server.CreateObject("ADODB.Connection") |
set m_rs = Server.CreateObject("ADODB.Recordset") |
'链接到Access数据库,使用客户端的游标引擎打开记录集 |
m_cn.Open "provider=microsoft.jet.oledb.4.0;data source=" & Server.MapPath(".") & "\data.mdb" |
sSQL = "SELECT Month([OrderDate]) AS [Month], Invoices.Salesperson, " & _ |
"Sum(Invoices.ExtendedPrice) AS Sales FROM Invoices " & _ |
"WHERE Invoices.OrderDate between #1/1/" & m_sYear & "# and #12/31/" & m_sYear & "# " & _ |
"GROUP BY Month([OrderDate]), Invoices.Salesperson" |
m_rs.CursorLocation = 3 'adUseClient |
m_rs.Open sSQL, m_cn, 3 'adOpenStatic |
'增加一个带有图例的簇列图表 |
set m_cspace = server.CreateObject("OWC.Chart") |
set cht = m_cspace.Charts.Add() |
set c = m_cspace.Constants |
cht.Type = c.chChartTypeColumnClustered |
cht.HasLegend = True |
'设置图表的数据源,把销售人添加到序列名称,销售月份添加到分类组,销售金额作为图表中各项取值 |
set m_cspace.DataSource = m_rs |
cht.SetData c.chDimSeriesNames, 0, "Salesperson" |
cht.SetData c.chDimCategories, 0, "Month" |
cht.SetData c.chDimValues, 0, "Sales" |
'增加一个图表标题,并格式化标题 |
cht.HasTitle = True |
cht.Title.Caption = m_sYear & "的销售金额 " |
set fnt = cht.Title.Font |
fnt.Name = "宋体" |
fnt.Size = 10 |
fnt.Bold = True |
'对分类轴添加标题,并格式化标题 |
set ax = cht.Axes(c.chAxisPositionBottom) |
ax.HasTitle = True |
ax.Title.Caption = "月份" |
set fnt = ax.Title.Font |
fnt.Name = "宋体" |
fnt.Size = 8 |
fnt.Bold = True |
'对数值轴添加标题,并格式化标题 |
set ax = cht.Axes(c.chAxisPositionLeft) |
ax.NumberFormat = "Currency" |
ax.HasTitle = True |
ax.Title.Caption = "元" |
set fnt = ax.Title.Font |
fnt.Name = "宋体" |
fnt.Size = 8 |
fnt.Bold = True |
'用一个临时变量名保存当前文件,这个文件名是唯一的 |
set m_fso = CreateObject("Scripting.FileSystemObject") |
sFullFileName = Server.MapPath(".") & "\" & m_fso.GetTempName() |
m_cspace.ExportPicture sFullFileName, "GIF", 800, 400 |
'使用 On Error Resume Next语句是为了确保我们删除了临时文件,即使一些函数调用失败 |
on error resume next |
'GIF文件已经输出,我们可以通过COM组件把它的内容发送到客户端 |
set m_objBinaryFile = server.CreateObject("BinFileWrite.GetFileStream") |
'Response.BinaryWrite m_objBinaryFile.GetFileBytes(CStr(sFullFileName)) |
m_objBinaryFile.SendBinFile CStr(sFullFileName),"image/GIF",TRUE,FALSE,TRUE,TRUE,TRUE |
'GIF文件已经不需要了可以删除了 |
'm_objBinaryFile.DeleteFile CStr(sFullFileName) |
%> |
下面是COM组件SendBinFile方法的实现源码: |
int BufferSize = 0; |
UCHAR *BufferAddress; |
VARIANT ImgInMemory; |
STDMETHODIMP CGetFileStream::SendBinFile(BSTR FileName, BSTR ContentType, BOOL bDelete, BOOL bExpires, BOOL bClear, BOOL bBuffer, BOOL bEnd) |
{ |
USES_CONVERSION; |
HANDLE hFile=CreateFile(OLE2T(FileName),GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL); |
if(hFile==INVALID_HANDLE_VALUE){return S_FALSE;} |
DWORD dwFileSizeHigh; |
DWORD dwFileSizeLow=GetFileSize(hFile,&dwFileSizeHigh); |
HANDLE hFileMapping = CreateFileMapping(hFile, |
NULL, |
PAGE_READONLY, |
0,0,NULL); |
if (hFileMapping == 0){ |
CloseHandle(hFile); |
return S_OK; |
} |
LPBYTE pFileData = (LPBYTE)MapViewOfFile(hFileMapping, |
FILE_MAP_READ, |
0,0,0); |
if (pFileData == 0){ |
CloseHandle(hFile); |
CloseHandle(hFileMapping ); |
return S_OK; |
} |
SAFEARRAYBOUND rgsabound[1]; |
BufferSize=dwFileSizeLow; |
VariantInit(&ImgInMemory); |
ImgInMemory.vt = VT_ARRAY | VT_UI1; |
rgsabound[0].cElements = dwFileSizeLow; |
rgsabound[0].lLbound = 0; |
ImgInMemory.parray = SafeArrayCreate(VT_UI1,1,rgsabound); |
if(ImgInMemory.parray != NULL) { |
void * pArrayData = NULL; |
//Get a pointer to beginning of data area in ImgInMemory |
SafeArrayAccessData(ImgInMemory.parray,&pArrayData); |
memcpy(pArrayData,pFileData,dwFileSizeLow); |
SafeArrayUnaccessData(ImgInMemory.parray); |
UnmapViewOfFile(pFileData ); |
CloseHandle(hFile); |
CloseHandle(hFileMapping ); |
if(bClear)m_piResponse->Clear(); |
m_piResponse->put_ContentType(ContentType); |
if(bExpires)m_piResponse->put_Expires(0); |
if(bDelete)DeleteFile(FileName); |
if(bBuffer)m_piResponse->put_Buffer(TRUE); |
m_piResponse->BinaryWrite(ImgInMemory); |
if(bEnd)m_piResponse->End(); |
else{ |
m_piResponse->Flush(); |
} |
if(BufferSize>0){ VariantClear(&ImgInMemory); |
} |
return S_OK; |
} |
UnmapViewOfFile(pFileData ); |
CloseHandle(hFile); |
CloseHandle(hFileMapping ); |
if(BufferSize>0){ VariantClear(&ImgInMemory); |
} |
return S_FALSE; |
} |
本程序在Windows 2000/IIS 5.0/Microsoft Office 2000环境下用Visual C++6调试通过。 |
点击这里下载相关程序。 |