标签归档:c#

用FASTREPORT实现WEB应用中自定义报表

开发WEB应用系统通常都会遇到报表打印问题。简单应用可利用IE的页面打印功能,利用HTML标签控制格式来实现。但复杂的业务型应用系统,报表不仅是组成应用的重要部分,还常常是相当复杂的。现在很多应用系统都要求提供自定义报表的功能——即客户可以自行设计、修改报表。

在C/S结构系统中,报表问题有很多成熟的解决方法。如DELPHI开发工具不仅自带有报表控件,还可以利用第三方控件来实现快速灵活的报表制作和打印,其中有名的控件是FR-Software & A.Tzyganenko 的FastReport。FastReport提供了能与DELPHI无缝集成的从设计到打印的完整控件包,提供的设计界面友好灵活,对于开发可让用户自定义报表的C/S应用来说,是一种很好的解决方式。

在B/S结构应用中,Crystal Report是一种大型报表系统常用和推荐的解决方案。但Crystal Report目前价格昂贵,而且该系统相当庞大。它的可定制性及精确控制打印效果方面尚不够完善。当然,在目前市场上,它仍是一种首选的WEB应用的报表解决方案。

如果能将C/S应用中成熟的报表解决方案搬到B/S应用中,相信对于大部分开发人员来说,都是非常欢迎的。本文将讲述一个在JAVA环境中利用FastReport实现B/S应用中用户可自定义的报表解决方案。因为笔者近段时间正用DELPHI、JAVA做一些项目,所以样例代码就以DELPHI、JAVA编写。

本解决方案样例的基本环境是:WINDOWS 2000 SERVER+SQL SERVER 2000+TOMCAT 4.0。开发工具:IntelliJ IDEA 3.0,DELPHI 5.0。客户端为IE 5.0浏览器。

方案共要求用DELPHI编写两个程序,一个是将被包含在网页中并在浏览器中运行的ACTIVEX(.ocx),一个是运行在服务器端的报表处理程序,中间通过JAVA程序连接——或任何其他WEB语言都可以,如ASP、PHP等。方案的基本原理图如下:

报表设计过程

 

 

报表打印过程

 

 

REPORT SERVER:可以做成一个普通的WINDOWS程序,也可以做成一个COM程序(Automation Object)。在报表设计过程中,用户端(ACTIVEX)向WEB SERVER发送报表设计请求,请求中包含要设计报表的名称;WEB SERVER 收到该请求后,调用REPORT SERVER请求设计的报表文件;REPORT SERVER收到请求后,先装载报表的数据环境,然后将报表设计文件(.frf)和该报表的数据环境文件压缩成一个包文件(.zip),将该包文件的完整路径名返回给WEB SERVER调用程序;WEB SERVER将包文件回送给用户端(ACTIVEX),用户端将接收到的包文件保存到本地硬盘上,并解压缩,从数据环境文件中恢复数据环境,通过FASTREPORT的相应控件打开报表文件给用户提供可视化设计。用户在ACTIVEX中设计报表时,虽然不能和数据库连接,但因数据环境已存在,所以用户仿如在通常的C/S应用结构下设计报表,能正常地看到报表的数据字典信息。在报表打印过程中,用户端(ACTIVEX)向WEB SERVER发送报表打印/预览请求,请求中包含要打印/预览的报表名称;WEB SERVER 收到该请求后,调用REPORT SERVER请求打印或预览的报表文件;REPORT SERVER收到请求后,先装载报表的数据环境,然后装载报表文件(.frf),接着在无界面状态下运行报表,最后将生成的已准备的报表文件(.frp)压缩成一个包文件(.zip),将该包文件的完整路径名返回给WEB SERVER调用程序;WEB SERVER将包文件回送给用户端(ACTIVEX),用户端将接收到的包文件保存到本地硬盘上,并解压缩,通过FASTREPORT的相应控件打开报表文件(.frp)给用户预览或打印或重新调整格式或输出为其他格式文件。用户在ACTIVEX中预览报表,仿如在通常的C/S应用结构下预览报表。

WEB SERVER:提供通常的WEB服务功能。

ACTIVEX:ActiveX是Microsoft提出的一组使用COM(Component Object Model,部件对象模型)使得软件部件可在网络环境中进行交互的技术集。它与具体的编程语言无关。作为针对Internet应用开发的技术,ActiveX被广泛应用于WEB服务器以及客户端的各个方面。本方案中的ACTIVEX控件主要做两方面的事情,一是利用FASTREPORT控件进行报表处理,包括报表设计(.frf文件)和报表打印(.frp文件)。一是与WEB SERVER进行通信,请求和接收包文件。本文样例的ACTIVEX采用DELPHI 5.0编写。

下面分述各部分的一例具体实现(因为仅为说明方案的实现,所以很多代码细节都进行了简省)。

一、             REPORT SERVER

REPORT SERVER既可以做成一个普通的WINDOWS程序,也可以做成一个COM程序(Automation Object)。本例中为简化见,采用普通的WINDOWS程序实现。

在DELPHI中NEW一个应用程序。在FORM中加入TfrReport、TfrDBDataSet、ADOConnection、TADOQuery等控件——为了使用FASTREPORT的控件,需要安装该控件包,可从站点http://www.fast-report.com 下载,国内很多软件站点都提供该控件包的下载。其中TfrDBDataSet、TADOQuery控件视应用需要可加入多个,另外为了压缩文件,还要加入一个压缩控件,本例使用VCLZip。在Form1中加入三个函数:preDesignReport(rpFileName:String),prePrintReport(rpFileName:String),zipReportFiles(rpFileName:String),分别用于准备报表设计文件、准备报表打印文件、压缩报表文件 。Form1.Create方法为:

procedure TForm1.FormCreate(Sender: TObject);

var

rpFileName,mode:String;

begin

if paramCount>1 then

begin

mode:=paramStr(1);

rpFileName:=paramStr(2);

if mode=’d’ then   //设计报表

if preDesignReport(rpFileName) then

zipReportFiles(rpFileName);

if mode=’r’ then   //打印报表

if prePrintReport(rpFileName) then

zipReportFiles(rpFileName);

end;

Application.Terminate;

end;

程序根据调用参数判断是准备报表设计文件还是准备报表打印文件,接着调用相应的过程来实现。最后的Application.Terminate 是让程序执行功能后即退出——因为这是服务端程序,是不能与用户交互的。

preDesignReport(rpFileName:String)方法:

function TForm1.preDesignReport(rpFileName:String):boolean;

var

……   //其他变量

dtfFileName:String;

begin

……

dtfFileName:=StringReplace(rpFileName, ExtractFileExt(rpFileName),’.dtf’,

[rfReplaceAll, rfIgnoreCase]);

try

rpAdoquery.SQL.Add(‘…’);

rpAdoquery.open;//打开报表的数据环境

rpAdoquery.FieldList.SaveToFile(dtfFileName);

result:=true;

except

on Exception do

result:=false;

end;

end;

函数preDesignReport的作用是准备报表设计文件。报表中可以引用多个DataSet,本例假设报表只引用一个名为rpAdoquery的DataSet。rpFileName 为报表文件名(.frf),DtfFileName为保存数据环境的文件名(.dtf)。因为用户端不能连接数据库,所以将DataSet中的Fileds通过rpAdoquery.FieldList.SaveToFile(dtfFileName)保存到文件,和报表文件一起传送给用户端的ACTIVEX,ACTIVEX利用.dtf文件复现报表的数据环境。

prePrintReport(rpFileName:String)方法:

function TForm1.prePrintReport(rpFileName:String):boolean;

var

……

repFileName:String;

begin

……

repFileName:=StringReplace(rpFileName, ExtractFileExt(rpFileName),’.frp’,

[rfReplaceAll, rfIgnoreCase]);

try

rpAdoquery.SQL.Add(‘…’);

rpAdoquery.open;//打开报表的数据环境

frReport1.ShowProgress:=False;

frReport1.Clear;

frReport1.LoadFromFile(rpFileName);

frDBDataSet1.DataSet :=rpAdoquery;

frReport1.Dataset :=frDBDataSet1;

frReport1.PrepareReport;

frReport1.SavePreparedReport(repFileName);

result:=true;

except

on Exception do

result:=false;

end;

end;

函数prePrintReport的作用是准备打印的报表文件,即先在服务器端装载报表并运行,将运行好的报表保存为文件,用于传送到用户端进行预览或打印。RepFileName是已准备好的报表文件名(.frp)。同样假设报表只引用一个名为rpAdoquery的DataSet。frReport1.ShowProgress:=False 使报表运行过程中不显示进度窗口(服务器端不能显示与用户交互的界面);接下来frReport1.Clear;…装载报表文件及设置相关数据属性;frReport1.PrepareReport 是在不显示预览窗口的情况下运行报表;frReport1.SavePreparedReport(repFileName) 将运行好的报表保存到文件,该文件传送给用户端的ACTIVEX,ACTIVEX可以直接预览或显示该报表。

zipReportFiles(rpFileName:String)方法:

function TForm1.zipReportFiles(rpFileName:String):boolean;

var

……

zipFileName,fileName:String;

zipCount:Integer;

begin

……

zipFileName:=StringReplace(rpFileName, ExtractFileExt(rpFileName),’.zip’,

[rfReplaceAll, rfIgnoreCase]);

fileName:= ExtractFileName(rpFileName);

fileName:= ChangeFileExt(fileName,’.*’);

try

VCLZip1.ZipName:= zipFileName;

VCLZip1.RootDir:= ‘./’;

VCLZip1.FilesList.Add(fileName);

zipCount:= VCLZip1.Zip;

if zipCount = 0 then

result:=false

else

result:=true;

except

on Exception do

result:=false;

end;

end;

函数zipReportFiles的作用是把要传送给用户端的报表文件压缩为一个.zip文件,简化文件传送过程,而且压缩了数据量。ACTIVEX接收到.zip文件后,先解压出包中文件,再进行处理。

 

 

二、             WEB SERVER

方案中WEB SERVER的作用主要是根据ACTIVEX的请求调用REPORT SERVER,并将REPORT SERVER生成的.zip文件发送给ACTIVEX。样例通过一个report.jsp文件来处理:ACTIVEX通过get请求report.jsp文件,report.jsp文件调用REPORT SERVER处理后,将.zip文件发送给ACTIVEX。

Report.jsp文件:

<%@ page import=”…”%>

<%@page contentType=” APPLICATION/OCTET-STREAM” %>

<%

try

{

String reqFileName = request.getParameter(“rpFileName”);

String reqMode = request.getParameter(“mode”);//d为设计报表,r为打印报表

String rpFileName = xxxx.getRpFileName(reqFileName); //根据请求的报表名获得实际的报表文件名,如请求订单报表,而订单报表实际对应的报表文件为order.frf。

String      l_cmd=”reportserver.exe “+reqMode+” “+ reqFileName;

Process l_ps=java.lang.Runtime.getRuntime().exec(l_cmd,null);

byte[] l_b=new byte[100];

while(l_ps.getInputStream().read(l_b,0,100)!=-1){

;

}

 

//发送文件

String zipFileName = xxxx.getZipFileName(reqFileName); //获得压缩文件名

response.setHeader(“Content-Disposition”,”attachment; filename=/”” +

zipFileName + “/””);

java.io.FileInputStream fileInputStream =

new java.io.FileInputStream(zipFileName);

int i;

while ((i=fileInputStream.read()) != -1) {

out.write(i);

}

fileInputStream.close();

out.close();

}

catch(Exception e)

{

……

}

%>

String l_cmd=”reportserver.exe “+reqMode+” “+ reqFileName; 组成调用REPORT SERVER的命令串。while(l_ps.getInputStream().read(l_b,0,100)!=-1){ ; } 等待REPORT SERVER执行完成,否则,程序在启动REPORT SERVER后即执行下一行语句。发送文件的方式有多种,比如也可以由ACTIVEX通过ftp方式取得。

 

 

 

三、ACTIVEX

方案中的ACTIVEX控件主要做两方面的事情,一是报表利用FASTREPORT控件进行报表处理,包括报表设计(.frf文件)和报表打印(.frp文件)。一是与WEB SERVER进行通信,请求和接收包文件。

在DELPHI中NEW一个ActiveForm 应用,取名为reportAForm。在form中加入Combox、button、edit、label等与用户交互的控件;为了处理报表,加入FASTREPORT的多个frSpeedButton用于处理报表事件,如设计、预览、打印、翻页、保存等;加入frReport、frDBDataSet、frDesigner等用于在运行时设计报表;如果设计报表时要使用图形、复选框等内容,也要加入相应的控件;加入frPreview、frTextExport、frRTFExport等控件使可以预览报表并可以将报表输出为text、rtf等格式文件;加入ADOQuery(根据实际需要可加入多个)为报表设计提供数据环境,ADOQuery不OPEN,不与数据库连接;加入NMHTTP用于与WEB SERVER联系。加入四个函数:DesignReport(rpFileName:String),PrintReport(rpFileName:String),unzipReportFiles(rpFileName:String),getReportFile(rpFileName,mode:String)分别用于设计报表、打印报表、解压缩报表和向WEB SERVER发送请求以取得报表文件 。

getReportFile(rpFileName,mode:String)方法:

function TreportAForm.getReportFile(rpFileName,mode:String):boolean;

var

……

zipFileName:String;

begin

……

zipFileName:=StringReplace(rpFileName, ExtractFileExt(rpFileName),’.zip’,

[rfReplaceAll, rfIgnoreCase]);

try

NMHTTP1.inputFileMode := TRUE;

NMHTTP1.body:=’./ ‘+ zipFileName;

NMHTTP1.Get(‘http://www…./../report.jsp?rpFileName=’+

rpFileName+’&mode=’+mode);

Result:=true;

except

on Exception do

Result:=false;

end;

end;

函数getReportFile的作用是向WEB SERVER发送报表请求(通过NMHTTP的Get方法),并将返回的压缩包文件保存到本地硬盘(zipFileName)。

unzipReportFiles(rpFileName:String)方法:

function TreportAForm.unzipReportFiles(rpFileName:String) :boolean;

var

……

zipFileName,fileName:String;

zipCount:Integer;

begin

……

zipFileName:=StringReplace(rpFileName, ExtractFileExt(rpFileName),’.zip’,

[rfReplaceAll, rfIgnoreCase]);

fileName:= ExtractFileName(rpFileName);

fileName:= ChangeFileExt(fileName,’.*’);

try

VCLUnZip1.ZipName:= ‘./’+ zipFileName;

VCLUnZip1.DestDir:= ‘./’;

VCLUnZip1.OverwriteMode:= Always;

VCLUnZip1.ReadZip;

VCLUnZip1.FilesList.Add(fileName);

zipCount:= VCLUnZip1.UnZip;

if zipCount = 0 then

result:=false

else

result:=true;

except

on Exception do

result:=false;

end;

end;

函数unzipReportFiles的作用是将压缩包中的文件解压出来,供ACTIVEX使用。它与REPORT SERVER程序中的zipReportFiles刚好是个相反的过程。

DesignReport(rpFileName:String)方法:

function TreportAForm. DesignReport (rpFileName:String) :boolean;

var

dtfFileName,rpFileName:String;

fldlist:TStringList;

T: TStringField;

i:Integer;

begin

……

dtfFileName:=StringReplace(rpFileName, ExtractFileExt(rpFileName),’.dtf’,

[rfReplaceAll, rfIgnoreCase]);//获得数据环境文件名

fldlist:=TStringList.Create;

fldlist.LoadFromFile(dtfFileName);

rpAdoquery.Fields.Clear;

for i := 0 to fldlist.Count – 1 do

begin

T := TStringField.Create(nil);

T.FieldName := fldlist[i];

T.Name := rpAdoquery.Name + T.FieldName;

rpAdoquery.Fields.add(T);

end;

FrReport1.LoadFromFile(rpFileName);

FrReport1.DesignReport;

end;

函数DesignReport先从.dtf(由REPORT SERVER生成)文件中恢复报表的数据环境,接着使用FASTREPORT的FrReport控件设计报表。在FASTREPORT中,对DataSet中的Field只关心名称(全部通过Variant类型处理),而并不关心数据类型,所以恢复报表的数据环境时,所有字段都当作String类型加入。样例假设报表只有一个名为rpAdoquery的DataSet。报表设计运行时窗口在ACTIVEX进程空间运行。

用户端设计好报表并保存后,需要将保存的报表文件(.frf)回送给服务器存储。文件上传对于大部分开发人员来说应该都是熟悉而简单的,该部分程序本文就省略了。

PrintReport(rpFileName:String)方法:

function TreportAForm. PrintReport (rpFileName:String) :boolean;

var

repFileName:String;

begin

……

repFileName:=StringReplace(rpFileName, ExtractFileExt(rpFileName),’.frp’,

[rfReplaceAll, rfIgnoreCase]);//获得已准备的报表文件名

try

frPreview1.clear;

FrReport1.Preview:=nil;

FrReport1.clear;

FrReport1.LoadPreparedReport(repFileName);

FrReport1.Preview :=frPreview1;

FrReport1.ShowPreparedReport;

result:=true;

except

on Exception do

result:=false;

end;

end;

函数PrintReport装入由REPORT SERVER运行好的报表.frp文件,通过调用FrReport的ShowPreparedReport方法在ACTIVEX端预览和打印。

方案实现方法的介绍结束。本方案具有的优点为:保持应用的结构形式不变(B/S),将C/S应用结构下已非常成熟的报表方案移植过来,使得在WEB应用中也可实现任意复杂的报表设计和打印,以及对打印效果进行精确控制。

C#实现软件注册码算法

Microsoft.NET的应用程序的代码文件,与Java生成的文件类似,它们都没有本地代码,而是一种类似于汇编的代码。这样,只要有合适的工具,就可以完整的把别人写出来的程序反编译成自己需要的程序文件。

我所知道的.Net下的反汇编程序是Salamander 和 Refelector两个工具,他们都可以对.Net的程序集反编译成你需要的语言。

那么,我们写的程序,做的项目,如何进行正版的许可证管理,有许多方法。

最好的方案,是几个方法的综合。下面我说一下单独的许可验证方法。

最简单的方法,就是使用许可存储。方法是用户输入正版的注册码,通过程序中专门的算法程序进行验算,得出的结果与事先保存在程序中的结果比对,比对一致表示输入正确。然后把结果保存在存储中,如注册表或者专门的许可文件中,程序许可通过。

这个方法使用的人/公司最多,但是缺点也是最多的,只要使用上面的工具把验算注册码的算法给弄清楚,就可以自己写一个生成序列号的注册机,这个注册方法就形同虚设了。

还有一个比较好的方法,就是仿照WindowsXP的激活机制,客户的程序自动访问互联网的一个专门设定的服务器,通过Tcp/Ip或者WebService远程访问服务器上的许可程序,许可后把结果保存在客户端计算机上。这个方法的好处是许可验证代码保存在开发者控制的计算机上,客户端无法获取验证算法,而且可以通过数据库管理用户,非常方便。

但是这个方法也有缺点,首先是可靠的Internet连接。如果要防止用户使用盗版,则必须在客户端的程序中添加一个随机访问远程许可服务器验证的功能,这样不但需要一个24小时的Internet连接,而且经常进行验证也会干扰程序的正常运行。还有就是如果有人通过研究客户端的接收返回信息的代码,弄一个虚拟的验证服务器,这个功能也会完蛋。

那么,所有的焦点都聚集在客户端的验证算法上,只要这个客户端的验证算法被人弄清楚了,整个程序的许可可以说就不存在了,所以许多开发者/开发公司费好大的力气,弄一个足够复杂的验证算法出来,用算法的复杂度来抵抗破解。但是再复杂的算法,只要有人写得出来,就有人能破解得出来,这个道理我想大家都明白。

那是否有加密算法与解密算法不同的办法呢?有。而且.Net自带的类库里面就有这个算法。
这个算法的原理是不对称加密的原理。不对称加密原理大家基本上都了解。加密的密码(密钥)分为两个部分,公钥和私钥。通过私钥加密的密文只能通过公钥解密。根据这个特性,我们可以发现只要开发者保存好私钥,即使算法代码被客户端破解,因客户端不知道保存在开发者处的私钥,也无法生成注册码。

这个算法就是 System.Security.Cryptography 名称空间的RSAPKCS1SignatureFormatter 类(用来生成注册码)和 RSAPKCS1SignatureDeformatter类(用来在客户端验证注册码)。验证过程如下:
首先,需要生成一个公钥和私钥对,当然,依靠人是无法生成的,我们可以通过 System.Security.Cryptography名称空间的RSACryptoServiceProvider 类来生成公钥/私钥对。

using(RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
{
// 公钥
stringpubkey =rsa.ToXmlString(false);
// 私钥
stringprikey =rsa.ToXmlString(true);
}

获取私钥以后,可以用 RSAPKCS1SignatureFormatter 类来生成注册码,代码如下(引用名称空间略)

using(RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
{
rsa.FromXmlString(prikey);;
//加密对象
RSAPKCS1SignatureFormatter f = new RSAPKCS1SignatureFormatter(rsa);
  f.SetHashAlgorithm("SHA1");
byte[] source =System.Text.ASCIIEncoding.ASCII.GetBytes(txtIn.Text);
SHA1Managed sha= new SHA1Managed();
byte[] result =sha.ComputeHash(source);
byte[] b =f.CreateSignature(result);
11 msg.Text =Convert.ToBase64String(b);
}

上面的代码是一个示例aspx页面的代码,页面包括一个id为msg的Label控件,一个ID为txtIn的TextBox控件,一个ID为btnOK的Button控件,上面的代码就是btnOK的事件处理程序的内容。大家可以非常清楚的看出处理流程,生成一个RsaCryptoServiceProvider类实例,然后把这个类实例的加密密钥指定为包含私钥的prikey字符串因为加密解密的公钥/私钥必须是对应的。然后获取txtIn输入的内容,生成密钥后在msg控件上显示。

下面是使用 RSAPKCS1SignatureDeformatter 类来验证输入: 

using(RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
{
  rsa.FromXmlString(pubkey);
  RSAPKCS1SignatureDeformatter f = new RSAPKCS1SignatureDeformatter(rsa);
  f.SetHashAlgorithm("SHA1");
  byte[] key =Convert.FromBase64String(txtKey.Text);
   SHA1Managed sha= new SHA1Managed();
  byte[] name =sha.ComputeHash(ASCIIEncoding.ASCII.GetBytes(txtIn.Text));
  if(f.VerifySignature(name,key))
   msg.Text ="验证成功";
  else
   msg.Text ="不成功";
}

上面的代码也很好理解,就是多了一个ID为txtKey的TextBox控件,他通过同时获取用户名/加密密钥来进行验证。重点是RSA类的FromXmlString()方法,注意上面的这个方法获取的是公钥,表示这段验证代码是保存在客户端的,客户端代码是没有私钥的,即使有人把程序集的代码反编译了也没有用。

上面两段代码需要注意的就是生成的公钥/私钥必须匹配,我使用RSA对象生成密钥对后保存成为字符串常量,就可以解决这个问题。

上面这个方法仍然无法解决客户使用ildasm反编译后暴力修改IL代码,只有靠可靠的强名称以及数字证书来保证程序集不被修改了。

异常处理之ThreadException、unhandledException及多线程异常处理

一:ThreadException和unhandledException的区别

处理未捕获的异常是每个应用程序起码有的功能,C#在AppDomain提供了UnhandledException 事件来接收未捕获到的异常的通知。常见的应用如下:

复制代码

代码

staticvoid Main(string[] args)
{
AppDomain.CurrentDomain.UnhandledException +=new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);
}

staticvoid CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
Exception error = (Exception)e.ExceptionObject;
Console.WriteLine(“MyHandler caught : “+ error.Message);
}

复制代码

未捕获的异常,通常就是运行时期的BUG,于是我们可以在UnhandledException 的注册事件方法CurrentDomain_UnhandledException中将未捕获异常的信息记录在日志中。值得注意的是,UnhandledException提供的机制并不能阻止应用程序终止,也就是说,CurrentDomain_UnhandledException方法执行后,应用程序就会被终止。

上面我们举的例子来自于控制台程序,UnhandledException可以在任何应用程序域中使用,在某些应用程序模型,如windows窗体程序,还存在ThreadException来处理 Windows 窗体线程中所发生的其未经处理的异常。即,在windows窗体程序中,使用 ThreadException 事件来处理 UI 线程异常,使用 UnhandledException 事件来处理非 UI 线程异常。ThreadException可以阻止应用程序终止。具体使用方法如下:

复制代码

代码

[STAThread]
staticvoid Main()
{
Application.ThreadException +=new ThreadExceptionEventHandler(UIThreadException);
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
AppDomain.CurrentDomain.UnhandledException +=
new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);
Application.Run(new ErrorHandlerForm());
}

privatestaticvoid UIThreadException(object sender, ThreadExceptionEventArgs t)
{
try
{
string errorMsg =”Windows窗体线程异常 : \n\n”;
MessageBox.Show(errorMsg + t.Exception.Message + Environment.NewLine + t.Exception.StackTrace);
}
catch
{
MessageBox.Show(“不可恢复的Windows窗体异常,应用程序将退出!”);
}
}

privatestaticvoid CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
try
{
Exception ex = (Exception)e.ExceptionObject;
string errorMsg =”非窗体线程异常 : \n\n”;
MessageBox.Show(errorMsg + ex.Message + Environment.NewLine + ex.StackTrace);
}
catch
{
MessageBox.Show(“不可恢复的非Windows窗体线程异常,应用程序将退出!”);
}
}

复制代码

除了Windows窗体程序,再来说一下WPF程序。WPF的UI线程和Windows的UI线程有点不一样。WPF的UI线程是交给一个叫做调度器的类:Dispatcher。代码如下:

复制代码

代码

public App()
{
this.DispatcherUnhandledException +=new DispatcherUnhandledExceptionEventHandler(Application_DispatcherUnhandledException);
AppDomain.CurrentDomain.UnhandledException +=new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);
}

void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
try
{
Exception ex = e.ExceptionObject as Exception;
string errorMsg =”非WPF窗体线程异常 : \n\n”;
MessageBox.Show(errorMsg + ex.Message + Environment.NewLine + ex.StackTrace);
}
catch
{
MessageBox.Show(“不可恢复的WPF窗体线程异常,应用程序将退出!”);
}
}

privatevoid Application_DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
{
try
{
Exception ex = e.Exception;
string errorMsg =”WPF窗体线程异常 : \n\n”;
MessageBox.Show(errorMsg + ex.Message + Environment.NewLine + ex.StackTrace);
}
catch
{
MessageBox.Show(“不可恢复的WPF窗体线程异常,应用程序将退出!”);
}
}

复制代码

无论是Windows窗体程序还是WPF程序,我们都看到捕获的异常当中分为”窗体线程异常”和”非窗体线程异常”。如在Windows窗体程序中,如果在窗体线程中,

thrownew Exception(“窗体线程异常”);

将会触发ThreadException事件。

Thread t =new Thread((ThreadStart)delegate
{
thrownew Exception(“非窗体线程异常”);
});
t.Start();

将会触发UnhandledException事件,然后整个应用程序会被终止。

 

二:多线程异常处理

 

多线程的异常处理,要采用特殊的做法。以下的处理方式会存在问题:

复制代码

代码

try
{
Thread t =new Thread((ThreadStart)delegate
{
thrownew Exception(“多线程异常”);
});
t.Start();
}
catch (Exception error)
{
MessageBox.Show(error.Message + Environment.NewLine + error.StackTrace);
}
复制代码

应用程序并不会在这里捕获线程t中的异常,而是会直接退出。从.NET2.0开始,任何线程上未处理的异常,都会导致应用程序的退出(先会触发AppDomain的UnhandledException)。上面代码中的try-catch实际上捕获的还是当前线程的异常,而t是属于新起的异常,所以,正确的做法应该是:

复制代码

代码

Thread t =new Thread((ThreadStart)delegate
{
try
{
thrownew Exception(“多线程异常”);
}
catch (Exception error)
{
MessageBox.Show(“工作线程异常:”+ error.Message + Environment.NewLine + error.StackTrace);
}
});
t.Start();
复制代码

也就是说,新起的线程中异常的捕获,可以将线程内部代码全部try起来。原则上来说,每个线程自己的异常应该在自己的内部处理完毕,不过仍旧有一个办法,可以将线程内部的异常传递到主线程。

在Windows窗体程序中,可以使用窗体的BeginInvoke方法来将异常传递给主窗体线程:

复制代码

代码

Thread t =new Thread((ThreadStart)delegate
{
try
{
thrownew Exception(“非窗体线程异常”);
}
catch (Exception ex)
{
this.BeginInvoke((Action)delegate
{
throw ex;
});
}
});
t.Start();
复制代码

上文的代码将最终引发主线程的Application.ThreadException。最终的结果看起来有点像:

 

在WPF窗体程序中,你可以采用如下的方法将工作线程的异常传递到主线程:

代码

Thread t =new Thread((ThreadStart)delegate
{
try
{
thrownew Exception(“非窗体线程异常”);
}
catch (Exception ex)
{
this.Dispatcher.Invoke((Action)delegate
{
throw ex;
});
}
});
t.Start();

WPF窗体程序的处理方式与Windows窗体程序比较,有两个很有意思的地方:

第一个是,在Windows窗体中,我们采用的是BeginInvoke方法。你会发现使用Invoke方法,并不能引发主线程的Application.ThreadException。而在WPF窗体程序中,无论是调度器的Invoke还是BeginInvoke方法都能将异常传递给主线程。

第二个地方就是InnerException。WPF的工作线程异常将会抛到主线程,变成主线程异常的InnerException,而Windows窗体程序的工作线程异常,将会被吃掉,直接变为null,只是在异常的Message信息中保存工作线程异常的Message。

 

三:ASP.NET异常处理

我们都知道ASP.NET的全局异常处理方法是Global中的Application_Error方法。我曾经查过ASP.NET的Appdomain.CurrentDomain.unhandledException,结果用反射得到的结果,unhandledException所注册的事件方法根本不是这个方法。联想到ASP.NET页面,包括这个全局处理类,都是交给aspnet_isapi.dll处理的,而aspnet_isapi.dll不是一个托管程序集。所以,应该理解为,ASP.NET的未捕获异常的处理,不同于托管异常(即CLR异常),而是交给aspnet_isapi.dll这个非托管DLL处理的。

使用C#执行MySQL的SQL文件

最近的一个项目,需要在初始化时建库及建表,因为建表的SQL语句是从MySQL中导出的,所以在建库之后,需要执行导出的Sql语句实现建表及插入数据。方法如下:

[csharp] view plain copy

  1. /// <summary> 
  2. /// 执行Sql文件 
  3. /// </summary> 
  4. /// <param name=”varFileName”>sql文件</param> 
  5. /// <param name=”Conn”>连接字符串</param> 
  6. /// <returns></returns> 
  7. private bool ExecuteSqlFile(string varFileName, String Conn)
  8. {
  9.     using (StreamReader reader = new StreamReader(varFileName, System.Text.Encoding.GetEncoding(“utf-8”)))
  10.     {
  11.         MySqlCommand command;
  12.         MySqlConnection Connection = new MySqlConnection(Conn);
  13.         Connection.Open();
  14.         try
  15.         {
  16.             string line = “”;
  17.             string l;
  18.             while (true)
  19.             {
  20.                 // 如果line被使用,则设为空
  21.                 if (line.EndsWith(“;”))
  22.                     line = “”;
  23.                 l = reader.ReadLine();
  24.                 // 如果到了最后一行,则退出循环
  25.                 if (l == nullbreak;
  26.                 // 去除空格
  27.                 l = l.TrimEnd();
  28.                 // 如果是空行,则跳出循环
  29.                 if (l == “”continue;
  30.                 // 如果是注释,则跳出循环
  31.                 if (l.StartsWith(“–“)) continue;
  32.                 // 行数加1 
  33.                 line += l;
  34.                 // 如果不是完整的一条语句,则继续读取
  35.                 if (!line.EndsWith(“;”)) continue;
  36.                 if (line.StartsWith(“/*!”))
  37.                 {
  38.                     continue;
  39.                 }
  40.                 //执行当前行
  41.                 command = new MySqlCommand(line, Connection);
  42.                 command.ExecuteNonQuery();
  43.             }
  44.         }
  45.         finally
  46.         {
  47.             Connection.Close();
  48.         }
  49.     }
  50.     return true;
  51. }
查看评论
5楼 chengjun163 2012-11-22 09:42发表 [回复]
weikai20007你那边可以执行成功吗?
Re: weikai20007 2012-12-06 19:31发表 [回复]
回复chengjun163:已经解决,链接:

http://www.cnblogs.com/easy5weikai/archive/2012/12/06/2805558.html

Re: 骏爷在此 2015-11-17 14:56发表 [回复]
回复weikai20007:这个方法遇到异常就停止执行了
Re: weikai20007 2012-12-06 13:10发表 [回复]
回复chengjun163:可以执行,我只是在楼主的基础上处理了DELIMITER,没处理其它特殊字符。
// 如果是空行,则跳出循环
if (temp == “” || temp == cmdEndFlag) continue;
// 如果是注释,则跳出循环
if (temp.StartsWith(“–“)) continue;

// 行数加1
lineToExecute += temp;
// 如果不是完整的一条语句,则继续读取
if (!lineToExecute.EndsWith(cmdEndFlag)) continue;//(!lineToExecute.EndsWith(“;”)) continue;
if (lineToExecute.StartsWith(“/*!”))
{
continue;
}

//执行当前行
command = new MySqlCommand(lineToExecute, Connection);
command.ExecuteNonQuery();
}
}
finally
{
Connection.Close();
}
}

return true;
}
}

// 如果line被使用,则设为空
if (lineToExecute.EndsWith(cmdEndFlag))//(lineToExecute.EndsWith(“;”))
lineToExecute = “”;

temp = reader.ReadLine();

if (temp != null && temp.Contains(“DELIMITER”))
{
temp = temp.Replace(“DELIMITER”,” “);
temp = temp.Trim();

cmdEndFlag = temp;
}

// 如果到了最后一行,则退出循环
if (temp == null) break;
// 去除空格
temp = temp.TrimEnd();

/// <summary>
/// 执行Sql文件
/// </summary>
/// <param name=”varFileName”>sql文件</param>
/// <param name=”Conn”>连接字符串</param>
/// <returns></returns>
public bool ExecuteSqlFile(string varFileName, String Conn)
{
//没处理lineToExecute.Contains(“DELIMITER ;;”)
string cmdEndFlag = “;”;

using (StreamReader reader = new StreamReader(varFileName, System.Text.Encoding.GetEncoding(“utf-8”)))
{
MySqlCommand command;
MySqlConnection Connection = new MySqlConnection(Conn);
Connection.Open();
try
{

楼主,你没有考虑脚本文件中存在 DELIMITER 的特殊情况,在此斗胆班门弄斧下,在你的基础上修改了下,借花献佛。

如何在.net应用中发现和避免内存和资源泄露

如何在.net应用中发现和避免内存和资源泄露

By Fabrice Marguerie

尽管很多人相信在.net应用中谈及内存及资源泄露是件很轻松的事情。但GC(垃圾回收器)并不是魔法师,并不能把你完全从小心翼翼处理内存与资源损耗中解放出来。

本文中我将解释缘何内存泄露依然存在以及如何避免其出现。别担心,本文不涉及GC内部工作机制及其它.net的资源及内存管理等高级特性中。

理解泄露本身及如何避免其出现很重要,尤其因为它无法轻松地自动检测到。单元测试在此方面无能为力。一旦产品中你的程序崩溃了,你需要马上找出解决方案。所以在一切都还不是太晚前,花些时间来学习一下本文吧。

Table of Content

·         介绍

·         泄露?资源?指什么?

·         如何检测泄露并找到泄露的资源

·         常见内存泄露原因

·         常见内存泄露原因演示

·         如何避免泄露

·         相关工具

·         结论

·         资源

介绍

近期,我参与了一个大的.net项目(暂叫它项目X吧),我在项目中负责追踪内存与资源泄露。大部分时间我都花在与GUI关联的泄露上,更准确地说是一个基于Composite UI Application Block (CAB).的windows窗体应用。接下来我要说的直接应用到winform上的内容,多数见解同样可以适用到其它.net应用中(像WPF,Silverlight,ASP.NET,Windows service,console application 等等)。

我不是个处理泄露方面的专家,所以我不得不深入钻研了一下应用程序,做一些清理工作。本文的目标是与你们分享在我解决问题过程中的所得所悟。希望能够帮助那些需要检测与解决内存、资源泄露问题的朋友。下面的概述部分首先会介绍什么是泄露,之后会看看如何检测到泄露和被泄露资源,以及如何解决与避免类似泄露,最后我会列出一个对此过程有帮助的工具列表及相关资源。

泄露?资源?指什么?

内存泄露

在进一步深入前,让我们先来定义下我所谓的“内存泄露”。简单引用在Wikipedia上找到的定义吧。该定义与我打算通过本文所帮助解决的问题完美的一致:

在计算机科学领域中,内存泄露是指一种特定的内存损耗,该损耗是由一个计算机程序未成功释放不需要的内存引起的。通常是程序中的BUG阻碍了不需要内存的释放。

仍然来自Wikipedia:”以下语言提供了自动的内存管理,但并不能避免内存泄露。像 Java,C#,VB.net或是LISP等。”

GC只回收那些不再使用的内存。而使用中的内存无法释放。在.net中,只要有一个引用指向的对象均不会被GC所释放。

句柄与资源

内存可不是唯一被视为资源的。当你的.net应用程序在Windows上运行时,消耗着一个完整的系统资源集。微软定义了系统三类对象:用户(user),图形设备接口(GUI),以及系统内核(kernel)。我不会在此给出完整的分类对象列表,只是指出一些重要的:

·         系统通过使用用户对象(User objects) 来支持windows管理。相关对象包括:提速缓冲表(Accelerator tables),Carets(补字号?),指针(Cursors),钩子(Hooks),图标(Icons),菜单(Menus)和窗体(Windows)。

·         GDI对象 支持图形绘制:位图(bitmaps),笔刷(Brushes),设备上下文(DC),字体(Fonts),内存设置上下文(Memory DCs),元文件(Metafiles),画笔(Pens),区域(Regions)等。

·         内核对象 支持内存管理,进程执行和进程间通讯(IPC):文件,进程,线程,信号(Semaphores),定时器(Timer),访问记号(Access tokens),套接字(Sockets)等。

所有系统对象的详细情况都可以在MSDN中找到。

系统对象之外,你还会碰到句柄(handles).据MSDN的陈述,应用程序不能直接访问对象数据或是对象所代表的系统资源。取而代之,应用程序一定都会获得一个对象句柄(Handle),可以使用它检查或是修改系统资源。在.net中无论如何,多数情况下系统资源的使用都是透明的,因为系统对象与句柄都由.net类直接或间接代表了。

非托管资源

像系统对象(System objects)这样的资源自身都不是个问题,但本文仍涵盖了它们,因为像Windows这样的操作系统对可同时打开的 套接字、文件等的数量都有限制。所以关注应用程序所使用系统对象的数量非常重要。

在特定时间段内一个进程所能使用的User与GDI对象数目也是有配额的。缺省值是10000个GDI对象和10000个User对象。如果想知道本机的相关设置值,可以使用如下的注册表键:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows: GDIProcessHandleQuota 和 USERProcessHandleQuota.

猜到了什么?确实没有这么简单,还有一些你会很快达到的其它限制。比如参照:我的一篇有关桌面堆的博客 所述。

假设这些值是可以自定义的,你也许认为一个解决方案就是打破默认值的限制—调高这些配额。但我认为这可不是个好主意,有如下原因:

1. 配额存在的原因:系统中不是只有你独自一个应用程序,所有运行在计算机中的其它进程与你的应用应该分享系统资源。

2.  如果你修改配额,使它不同于其它系统了。你不得不确认所有你的应用程序需要运行的机器都完成了这样的修改,而且这样的修改从系统管理员的角度来说是否会有问题也需要确认。

3.  大部分都采用了默认配额值。如果你发现配置值对你应用程序来说不够,那你可能确实有些清理工作要做了。

如何检测泄露及找到泄露的资源

泄露带来的实际问题在MSDN上的一篇文章中有着很好的描述:

哪怕在小的泄露只要它反复出现也会拖垮系统。

这与水的泄露异曲同工。一滴水的落下不是什么大问题。但是一滴一滴如此反复的泄露也会变为一个大问题。

像我稍后解释的,一个无意义的对象可以在内存中维持一整图的重量级对象。

仍然是同一篇文章,你会了解到:

通常三步根除泄露:

1.发现泄露

2.找到被泄露的资源

3.决定在源码中何时何处释放该资源

最直接“发现”泄露的方式是遭受泄露引发的问题

你或许没有见过内存不足。“内存不足”提示信息极少出现。因为操作系统运行中实际内存(RAM)不足时,它会使用硬盘空间来扩展内存。(称为虚拟内存)。

在你的图形应用程序中可能更多出现的是“句柄不足”的异常。准确的异常不是System.ComponentModel.Win32Exception 就是 System.OutOfMemoryException 均包含如下信息:”创建窗体句柄错误”。这两个异常多发于两个资源被同时使用的情况下,通常都因为该释放的对象没有被释放所致。

另外一种你会经常碰到的情况是你的应用程序或是整个系统变更得越来越慢。这种情况的发生是因为你的系统资源即将耗尽。

我来做个生硬的推断:大多数应用程序的泄露在多数时间里都不是个问题,因为由泄露导致出现的问题只在你的应用程序集中使用很长时间的情况下才会出现。

如果你怀疑有些对象在应该被释放后仍逗留在内存中,那需要做的第一件事就是找出这些对象都是什么。

这看起来很明显,但是找起来却不是这样。

建议通过内存工具找到非预期逗留在内存中的高级别对象或是根容器。在项目x中,这些对象可能是类似LayoutView实例一样的对象们(我们使用了MVP(Model View Presentation )模式)。在你的实际项目中,它可能依赖于你的根对象是什么。

下一步就是找出它们该消失却还在的原因。这才是调试器与工具能真正帮忙的。它们可以显示出这些对象是如何链接在一起的。通过查看那些指向“僵尸对象”(the zombie object)的引用你就可以找到引起问题的根本原因了。

你可以选择 ninja方式(译者:间谍方式?)(参照 工具介绍章节中有关 SOS.dll 和 WinDbg 的部分)。

我在项目X中用了JetBrains的dotTrace,本文中我将继续使用它来介绍。在后面的工具相关章节中我会向你更多的介绍该工具。

你的目标是找到最终引起问题的那个引用。不要停留在你找到的第一个目标上,但是也要问问自己为什么这个家伙还在内存中。

常见内存泄露的原因

上面提到的泄露情况在.net中较常见。好消息是造成这些泄露的原因并不多。这意味着当你尝试解决一个泄露问题时,不需要在大量可能的原因间搜寻。

我们来回顾一下这些常见的罪魁祸首,我把它们区别开来:

·         静态引用

·         未注销的事件绑定

·         未注销的静态事件绑定

·         未调用Dispose方法

·         Dispose方法未正常完成

除了上列典型的原因外,还有些其它情况也可能引发泄露:

·         Windows Forms:绑定源滥用

·         CAB:未移除对工作项的调用

我只列出了可能在你应用程序中出现的一些原因,但应该清楚你的应用程序依赖的其它.net代码、库实际使用中也可能引发泄露。

我们来举个例子。在项目x中,使用了一套第三方控件来构造界面。其中一个用来显示所有工具栏的控件,它管理着一个工具栏列表。这种方式没什么,但有一点,即使被管理的工具栏自身实现了IDisposable接口,管理类却永远也不会去调用它的Dispose方法。这是一个bug.幸运的是这发生在一个很容易发现的工作区:只能我们自身来调用所有工具样的Dispose方法了。不幸的是这还不够,工具栏类自身问题也不少:它并没有释放自身承载的控件(按钮,标签等等)。所以在解决方案中还要添加对每个工具栏中控件的释放,但是这次可就没那么简单了,因为工具栏中的每个子控件都不同。不管怎么样这只是一个特殊的例子,我要表达的观点是你应用程序中使用的任何第三方库、组件都可能引发泄漏。

最后,还有一种由.net framework造成的泄露,由一些不好的使用习惯引起。即使.net framework自身可能引发泄露,但这是你极少会遭遇到的情况。把责任推到.net身上很容易,但在我们把问题推到别人头上前,还是应该先从自身写的代码出发,看看里面有没有问题。

常见泄露演示

我已经列举出了泄露主要的来源,但我还不想仅限于此。如果每个泄露我都能举个鲜活的例子的话,我想本文会更实用些。好,我们先启动Vs 和 dotTrace , 然后看些示例代码。我会同时演示如何解决或是避免每个泄露情况。

项目X中使用了CAB和MVP模式,这意味着界面由工作空间、视图和呈现者组成。简单起见,我决定使用包含一组窗口的Winform应用。其中使用了与Jossef Goldberg的一篇关于“Wpf应用程序内存泄露”文章中相同的方法。甚至我会直接把相同的例子和事件处理函数应用到我的Winform App中。

当一个窗体被关闭及处置后,我们期待的结果是它同时也在内存中被释放了。对吧?但我下面要展示的是何种情况下该窗体未被释放。

下面的就是我创建的示例程序中的主窗口:

这个主窗口可以打开不同的子窗口;每个打开的子窗口都会分别引发不同的内存泄露。

本文相关示例代码在后面的“资源”一节中可以找到。

静态引用

我们先把显而易见的放在一边。如果一个对象被一个静态字段引用,那它永远也不会被释放。

像singletons模式中就是如此。每个Singletone对象通常都是一个静态对象,即使不是静态对象,那它至少也是会长期存在的对象。

这种情况很显而易见,但是记住不只是直接引用才危险。真正的危险往往来自间接引用。事实上,你一定要注意引用链。整个引用链中有多少个根。如果有静态对象作为根,那所有它的子节点将会始终存在着。

上图上如果Object1是静态的话,多数情况下会长时间存在,那所有它下面的引用将会一直在内存中保留着。危险就在于链条太长了以至于忽略了根节点是静态的。如果你只关注在一个深度级别上,考虑Object3和Object4一旦Object2离开内存那它们也就被释放了。这个没错,确定是,但你需要考虑到它们可能因为Object1一直存在而未被释放。

小心各种静态类型。如果可能尽量不用。非要使用的话,花些时间关注它们。

一个来自特定类型的风险—静态事件,我会在讲解常规事件相关内容时介绍它。

事件,或 “失效监听器”问题

一个子窗口订阅了主窗口中的一个事件,以便主窗口透明度变化时得到通知(包含在EventForm.cs文件中):

C#

mainForm.OpacityChanged += mainForm_OpacityChanged;

问题就是这个针对OpacityChanged事件的订阅创建了一个从主窗口到子窗口的引用。

下图显示了完成事件订阅后两个对象间是如何通讯的:

看看我这篇更多学习事件与引用的博文。下图就是该文章中体现事件观察与被观察者背后引用关系的:

下图所示就是你使用dotTrace搜索EventForm然后点击“最短路径”的结果:

应能看到主窗体(MainForm)保留着对事件窗体(EventForm)的引用。这种情况会出现了第一个你在应用中打开的事件窗口(EventForm)。这就意味着所有在应用中打开的事件窗口(EventForm)只要程序还未销毁,哪怕你已经不在使用它们了(包括关闭了它们)。

这些子窗口不光只是赖在内存中,它们还可能会引发异常,比如你改变了主窗口(MainForm)的透明度(opacity),那些已经被关闭的事件窗口(EventForm)就会引发异常,因为主窗口依然通过事件通知他们,但它们已经是“已处置(disposed)窗口”了。

最简单的解决方案就是通过在这些事件窗口(EventForm)被处置(dispose)时取消事件订阅,从而移除主窗口对事件窗口的引用:

C#

Disposed += delegate { mainForm.OpacityChanged -= mainForm_OpacityChanged; };

注意:我们这里有一个问题,MainForm对象在整个应用被关闭后依然在内存中存在。较短的生命周期内的对象相互引用可能不会引起内存问题。任何孤立的对象链都会被GC自动从内存中卸载掉。孤立的对象链由两个单向引用的对象或是一组没有外部引用的连接对象组成。

另一个解决方案是使用基于弱引用的弱委托。在我的那篇《事件与引用》的博文中有涉猎。网上也有几篇文章讲解了如何付诸实现。比如这篇:“弱引用事件”。找到的多数解决方案都是基于弱引用类。更多弱引用方面的学习可以参见MSDN

注意一下,一个以“弱事件模式”形成的解决方案已经在WPF中存在了。

现有的一些框架中如:CAB (Composite UI Application Block) 或 Prism (Composite Application Library) 均有一些其它的解决方案,像EventBroker 和 EventAggregator 。只要你想也可以使用自己实现的其它事件模式:broker/aggregator/mediator

订阅了静态对象或是生命周期较长的对象上的事件却没有适时取消订阅也会造成问题。另外一种问题来源于静态事件。

静态事件

来直接看个例子(StaticEventForm.cs中):

C#

SystemEvents.UserPreferenceChanged += SystemEvents_UserPreferenceChanged;

这次的例子与前面的很相似,不同的是这次我们订阅的是一个静态事件。因为是一个静态事件,所以对应的监听对象永远也不会被释放掉。

解决方案就是当我们的监听者完成所要做的事情后取消该静态事件订阅。

C#

SystemEvents.UserPreferenceChanged -= SystemEvents_UserPreferenceChanged;

Dispose方法没有被调用的情况

你是否已经开始注意事件和静态类型了呢?很好,但这还不够。你仍能发现一些游弋的未释放对象,即使你写了正确的清理代码。这种情况的发生有时仅仅只是因为这些清理的代码未被调用。。。

在Dispose方法或是Disposed事件中取消事件订阅和释放资源是很好的习惯,但如果未调用Dispose那也没有用。

再来看个有趣的例子。下面示例代码给一个窗体创建了一个上下文菜单(来自ContextMenuStripNotOKForm.cs):

C#

ContextMenuStrip menu = new ContextMenuStrip(); menu.Items.Add(“Item 1”); menu.Items.Add(“Item 2”); this.ContextMenuStrip = menu;

在窗口已经关闭且Dispose的情况下,下图是你可以通过dotTrace 看到的结果:

ContextMenuStrip仍在内存中!注意:想复现该问题,先通过右键鼠标显示上下文菜单,然后关闭窗口。

这就是一个由静态事件引发的泄露。通常解决方法就是下面的在Disposed事件处理程序中调用ContextMenuStrip的Dispose方法。

C#

Disposed += delegate { ContextMenuStrip.Dispose(); };

我猜你已经开始明白如果不多加小心,在.net中使用事件是很危险的。此处我想强调的是只此一行代码就轻易的引起了泄露。而当你创建一个Context-menu时是否考虑过潜在的内存泄露风险呢?

可能比你想象的还要糟。不只ContextMenuStrip未被释放,它使得整个窗体都还在内存中保留着!在下面截图中你可以看到ContextMenuStrip引用着窗体:

导致的结果就是只要ContextMenuStrip还在内存中,整个窗体也会被释放。哦,当然你也不要忘了,因为窗体还在,与其相关的一组对象都会持续保留在内存中 – 像窗体上的控件包含的组件,如下图所示:

这是我严重警告一定要对这种情况足够注视的原因。可能因为一个小对象而使内存中一个大的对象链不能被正常释放。我总能在项目X中看到这种情况的出现。水滴石穿,不要小看一滴水能带来的破坏。

因为单个控件不是指定它的父控件就是指向父控件的事件,所以这使得如果某个控件没有被调用dispose而致整个对象链都无法释放的潜在可能。这当然也包括上级容器包含的其它控件。所以示例中导致整个窗体还问题存在于内存中的情况也就可能出现(至少在整个应用程序完全终止前是这样的)。

在这个具体例子中,你可能正在考虑是否有ContextMenuStrip的地方总是出现这个问题呢。当然不会问题出现,使用设计器直接在窗体上直接创建,至少在此情形下,Vs自动生成的代码可以确保ContextMenuStrip相关组件可能被正确处置。

如果你对于设计器是如何处理的好奇,可以看看ContextMenuStripOKForm类及它的字段在ContextMenuStripOKForm.Designer.cs中是如何处理的。

我想指出另外一个在项目x 中看到的解决方法。由于某些原因,有些控件在源文件中没有与之相关的.Designer.cs文件。设计器代码生成的代码就在.cs文件中。别问我为什么。除了与众不同的代码结构(不推荐)外,问题在于这些代码是被完整拷贝过去的:但要吗Dispose方法就没有,要吗就是没有调用组件的Dispose方法。我想你能明白这种情况下出问题就不奇怪了。

不完整的Dispose方法

我想现在你已经领会了调用那些实现Dispose模式类的Dispose方法的重要性了,强调Dispose这是我要说的一件事情。在你的类中实现IDispose接口以及包含其它Dispose的调用这非常棒,对你的程序也非常有益,只要Dispose方法实现正确。

这段评论看起来好像有点儿傻,但是我这么做是因为我看到太多这种不完整实现Dispose的例子了。

你清楚它是怎么发生的。你创建好了自己的类;实现了IDisposable接口;你在Dispose方法中取消事件订阅和释放资源;并且你在所有需要调用Dispose的地方都调用了它。这很好,直到后来你的类中有一个订阅了新的事件或是消耗了新的资源。编码很容易,你热情洋溢的完成编码、测试。它运行起来很好,所以你很高兴。你提交代码,不错!但是。。。唔,你忘记了更新Dispose方法去释放新的事件或资源了。这种事情总在发生。

我就不举例了。这应该相当常见。

Windows窗体:绑定源误用

我们来解决一个Windows窗体上的问题。如果你用BindingSource组件,请确认你是按照它设计时给定的方式使用的。

通过静态引用,我已经看到了出现的BindingSource,而BindingSource的运转方式导致了内存泄露。使用BindingSource作为数据源的控件都保留着一个BindingSource的引用 ,即使这些控件被处置后。(Disposed)

下图所示的情况,就是在一个数据源是静态或长生命周期的BindingSource(如:BindingSourceForm.cs)的ComboBox被处置后 用doTrace查看的结果。

一个解决方案就是用一个BindlingList替代BindingSource.如你可以这样,拖放一个BindingSource到你的窗体上(指设计时),将BindingList分配给BindingSource作为它的数据源,同时把BindingSource分配给ComboBox作为它的数据源。这种方式下,你仍将使用一个BindingSource。

看BindingListForm.cs(例子源码中)中的这个处理。

这种方式并没有妨碍你使用BindingSource,但是应该在视图界面中创建它 (设计时窗体中,从而生成自动化代码。).总之这样做是有道理的:BindingSource是一个定义在System.WindowsForms命名空间中的表现层组件。BindingList比较而言,它只是一个集合,不隶属于可视化的组件。

注意:如果你并不是非用BindingSource不可,可以完全只用BindingList。

CAB:缺少从工作项(WorkItem)上移除

下面是一条对针对CAB应用程序的建议,但是你也可以应用到其它类型的应用中。

工作项(WorkItems)是构建CAB应用程序的中心。一个工作项(WorkItems)就是一个在上下文中保留相关对象轨迹的容器,并且执行依赖注入。通常一个视图(View)创建后就会增加一个工作项(WorkItems)与之对应。当视图关闭且被回收后,它应该从对应的工作项(WorkItems)上移除,否则工作项(WorkItems)就会使得它(视图)始终存于内存中,因为工作项(WorkItems)中维护着一个指向视图的引用。

如果你忘记了从对应工作项(WorkItems)上移除视图,那泄露由此产生。

在项目x中,我们使用了MVP设计模式(Model-View-Presenter)。下图显示了一个视图显示后不同元素间是如何连接的:

(译者:此处的插图有误,所以未添加)

注意WorkItem通过依赖注入得到presenter。而WorkItem多数时候又会顺便把presenter注入到视图(View)中。为了确保项目X中的所有内容都能适当的释放掉,我们使用了下图所示的一个职责链:

当一个视图(View)被处置(disposed)了(很可能就是因为它被关闭了),那它的Dispose方法就会被调用。该方法会依次调用presenter的dispose方法。Presenter认识WorkItem,在它的Dispose方法中会把自己和源头的视图从Worktem中移除掉。通过这种方式,所有内容都被适当的处置和释放了。

我们的应用程序框架中包括了实现了上面职责链条的基类,所以视图开发人员不需要重要实现这些类也不用每次都为此担心。即使不是CAB应用程序,我也鼓励在你的应用中实现这类模式。在你的对象中正确实现自动释放模式将助你避免那些由于疏忽引起的泄露。但要确保所有实现的方式一致,不要因为他们不知道适当的处理方式而出现每个开发人员实现都不同的情况,这也会导致泄露。

如何避免泄露

现在你对泄露本身以及它是如何产生的有了进一步的了解,此时我想强调几个重点并给出几个技巧。

我们先来探讨一条一般的规则。通常一个负责创建另外一个对象的对象也负责处置(disposing)它。当然不包括工厂类。

反过来:一个对象对于从别的对象上得到的对象没有处置(disposing)的职责。

事实上,这确实要依靠具体情形而定。无论如何,重要的是谨记对象属于谁(who created it)。

第二条原则:每一个+=(事件订阅)都是一个潜在的敌人!

根据我的个人经验,事件是.net中的主要泄露来源。它值得你反复确认甚至确认再三。每次你在代码中增加事件订阅,都应该考虑一下结果问问自己是否需要再增加一个-=来取消事件订阅。如果答案是需要,在你没忘记前马上加上。经常是加到Dispose方法中。

为了确保对象被成功回收,推荐的做法是有事件订阅的对象就需要对应的事件取消订阅。无论如何,当你绝对清楚一个事件源将不再发布事件通知了,而且你希望所有订阅它事件的其它对象能被释放,那么你可以强制移除所有该事件的订阅。我一篇博文中包含了如何做的代码事例。

马上给出一个技巧。通常当对象引用在一定数量的多个对象间共享时,问题就会出现了。因为这种情况下明了哪个对象引用了哪些引用就很困难了。有时在内存中克隆所需的对象要胜于引用现有对象,这样可以避免对象间反复缠绕。

最后,即使这已是.net中众所周知了,我还是想要再次强调调用Dispose的重要性。每次你分配了一个资源,一定要确保调用了Dispose或是将资源使用的代码写在一个using代码块中。如果你不是始终都这么做的话,你很快会被资源泄露搞死,通常情况下都是非托管资源引起的。

Tools

相关工具

几款工具可能助你追踪对象实例、也包括系统对象和句柄。我来列举几个。

Bear

Bear是一个可显示出所有Windows下运行进程信息的免费程序:

·         支持所有GDI对象的用法(hDC, hRegion, hBitmap, hPalette, hFont, hBrush)

·         支持所有用户对象的使用(hWnd,hMenu,hCursor,SetWindowsHookEx,SetTimer 和其它形式的对象)

·         句柄统计

GDIUsage

另外一个实用的工具是GDIUsage.这款工具也是免费的而且开源。

GDIUsage聚集在GDI对象上。通过它,你可以对当前GDI消耗情况拍摄快照,执行一个可能诱发泄露的行为,之后比较泄露前后资源的使用情况。这样可以大大的帮助我们,它能让我们看到操作期间增加了(或是释放了)哪些GDI对象。

此外,GDIUsage不光只是给出一个数字,还可以提供GDI对象的图形化显示。肉眼观察位图(bitmap)泄露的内容可以轻松的找出泄露的原因。

dotTrace

JetBrains出品的dotTrace是一个.net程序的内存与性能分析工具。

下图就是dotTrace的截屏。这也是项目X中我用的最多的工具。其它.net分析工具我也不太了解,但是dotTrace为我解决项目X中检测到的泄露提供了所需的信息。多达20个以上。。。我没说过这就是一个bug项目?

dotTrace允许你及时标识出特定时间下内存中的对象,它们如何存在(被哪些对象引用),以及它们是谁(谁被引用)。你还可以使用它提供的高级调试功能:追踪栈分配情况,查看销毁对象列表等。

下图展示的是两种内存状态间的差别

dotTrace也是一个性能分析工具:

使用的方法就是先启动dotTrace然后指定exe文件的路径,之后就可以请求它对你选择的应用程序进行分析了。

如果你想检查应用程序的内存使用情况,可以在程序运行时用dotTrace拍摄快照,然后让它显示出相应信息。你要做的第一件事大概就是让它显示出指定类在内存中有多少个实例,以及这些实例是如何存在的。

除了搜索托管实例外,你也可以搜索非托管资源。dotTrace没有对非托管资源追踪提供直接支持,但你可能搜索对应的.net包装对象。例如:你搜索位图、字体或是笔刷类的实例。如果你发现一个实例没有被释放,那么它在应用程序上分配的资源也仍然还在。

下一个我要介绍的工具就内置了对非托管资源的追踪。也就是说通过它你就能够直接探索HBITMAP, HFONT 或是 HBRUSH 句柄。

.net内存分析器

.net内存分析器是另一个有趣的工具。它提供了一些dotTrace不包括的实用特性:

·         查看哪些已经调用了处置方法,却还存在的对象

·         查看哪些已经被释放却没有调用处置方法的对象

·         非托管资源的追踪

·         附加到一个运行中的进程上

·         附加到一个进程的同时作为VS的调试器

·         自动内存分析(关于常见内存使用问题的提示和警告)

另外一些可用的内存分析器

上述几个工具只是一些工具所能帮助你的示例。dotTrace和.NET Memory Profiler是众多.net内存及性能分析工具中的两个。其它一些有名的包括:ANTS ProfilerYourKit ProfilerPurifyPlusAQtime 和 CLR Profiler.这些工具中的多数都提供了和dotTrace相同类型的功能。在SharpToolbox.com上你可以找到完整的专注于.net的分析工具集合

SOS.dll and WinDbg

另一个你可用的工具是SOS.dll. SOS.dll 是一个扩展调试的工具,帮助你在WinDbg.exe调试器和VS中调试托管程序,提供CLR资源有关的内部信息。可以用它来获取与GC相关的信息,与内存中对象、线程与锁、调用栈等相关的信息。

WinDbg 是你通常需要附加到产品中某个进程时用的较多的工具。关于SOS.dll 和 WinDBg 如果想更多了解可以看看 Rico Marian 的一篇博文,和Mike Taulty的两篇博文(SOS.dll 与 WinDbg 和 SOS.dll 与 Visual Studio) ,还可以参照Wikipedia

SOS.dll 和 WinDbg 作为Windows调试工具包中的一部分由微软免费提供。二者比之上述的其它工具的一大优势就是在保持强大功能的同时又有着较低的资源损耗。

下图是使用sos.dll和gcroot命令的示例输出:

WinDbg screenshot:

自定义工具

除去市面上的可用工具外,别忘了你也可以创建自己的工具。可以在多个应用程序中重用的独立工具。当然开发这种工具可能有点儿难度。

我们为项目X开发了一套完整工具,帮助我们保持实时的资源使用情况和潜在泄露情况进行跟踪。

这些工具之一在右侧显示一组存在与消亡的对象列表。它由一个CAB服务和一个CAB视图组成,可以用来检查我们期望被释放的对象是否真的被释放了。

下图就是该工具的截图:

如果保留应用中所有对象的痕迹,对于大的应用程序来说代价太高并且也违背产品设定的对象数量。事实上,我们无需关注所有的对象,只要关注那些应用中的高级别对象和根容器。这些对象我在解释如何检测泄露时已经提议过进行追踪。

创建该 工具使用的技术很简单。它使用了弱引用。弱引用类允许你引用一个同时可被GC回收的对象。此外,它还通过提供IsAlive属性提供你测试该引用是否已消亡的功能。

项目X中,我们还有一个提供GDI和用户对象使用概况的小部件。

当资源接近枯竭之时,这个小部件会有一个小的警告图标:

此外,该工具还会让用户关闭一些当前打开的窗口/选项卡/文档,并且阻止用户打开新的窗口等,直到资源使用情况重新低于临界级别。

为了读取到当前UI资源使用情况,我们使用了User32.dll中的GetGuiResources API。下面代码展示了如何在C#中引入该API:

C#

// uiFlags: 0 – Count of GDI objects

// uiFlags: 1 – Count of USER objects

// GDI objects: pens, brushes, fonts, palettes, regions, device contexts, bitmaps, etc.

// USER objects: accelerator tables, cursors, icons, menus, windows, etc.

[DllImport(“User32”)]

extern public static int GetGuiResources(IntPtr hProcess, int uiFlags);

public static int GetGuiResourcesGDICount(Process process)

{

  return GetGuiResources(process.Handle, 0);

}

public static int GetGuiResourcesUserCount(Process process)

{

  return GetGuiResources(process.Handle, 1);

}

通过Process.GetCurrentProcess()的WorkingSet64属性,获取内存使用情况。

结论

我希望本文为你改善应用程序和解决泄露方面提供了一个良好的基础。追踪泄露可以很有趣。。。如果你确实没什么其它更好的事情做的话:-)有时,你别无选择,因为对于你的应用程序来说解决泄露至关重要。

一旦解决了泄露,仍有工作要做。我强烈建议你在尽量降低资源消耗方面改善应用程序。不要损失功能。最后,我邀请你阅读我的另外一篇包含相关建议的博文

相关资源

演示程序源代码下载地址

假如你有意进一步钻研,下面是一些有趣的补充资源

·         Jossef Goldberg: Finding memory leaks in WPF applications

·         Tess Ferrandez has a series of posts about memory issues (ASP.NET, WinDbg, and more)

·         MSDN article by Christophe Nasarre: Resource Leaks: Detecting, Locating, and Repairing Your Leaky GDI Code

·         Article in French by Sami Jaber: Audit et analyse de fuites mémoire

·         My blog post about the Desktop Heap

·         My blog post about lapsed listeners

·         My blog post that shows how to force unsubscription from an eventc

C#学习之逆波兰公式简单实现

Stack<T>和Stack

Stack<T>和Stack则是后进先出的数据结构,通过Push和Pop法实现添加元素到队列的顶部和从队列的顶部移除元素。同样也提供了Peek方法、Count属性和ToArray方法。

栈的实践使用:逆波兰公式

表达式一般由操作数(Operand)、运算符(Operator)组成,例如算术表达式中,通常把运算符放在两个操作数的中间,
这称为中缀表达式(Infix Expression),如A+B。
波兰数学家Jan Lukasiewicz提出了另一种数学表示法,它有两种表示形式:
把运算符写在操作数之前,称为波兰表达式(Polish Expression)或前缀表达式(Prefix Expression),如+AB;
把运算符写在操作数之后,称为逆波兰表达式(Reverse Polish Expression)或后缀表达式(Suffix Expression),如AB+;
其中,逆波兰表达式在编译技术中有着普遍的应用。
算法:
一、 将中缀表达式转换成后缀表达式算法:
1、从左至右扫描一中缀表达式。
2、若读取的是操作数,则判断该操作数的类型,并将该操作数存入操作数堆栈
3、若读取的是运算符
  (1) 该运算符为左括号”(“,则直接存入运算符堆栈。
  (2) 该运算符为右括号”)”,则输出运算符堆栈中的运算符到操作数堆栈,直到遇到左括号为止。
  (3) 该运算符为非括号运算符:
      (a) 若运算符堆栈栈顶的运算符为括号,则直接存入运算符堆栈。
      (b) 若比运算符堆栈栈顶的运算符优先级高或相等,则直接存入运算符堆栈。
      (c) 若比运算符堆栈栈顶的运算符优先级低,则输出栈顶运算符到操作数堆栈,并将当前运算符压入运算符堆栈。
4、当表达式读取完成后运算符堆栈中尚有运算符时,则依序取出运算符到操作数堆栈,直到运算符堆栈为空。
二、逆波兰表达式求值算法:

对后缀表达式求值比直接对中缀表达式求值简单。在后缀表达式中,不需要括号,而且操作符的优先级也不再起作用了。您可以用如下算法对后缀表达式求值:

  1. 初始化一个空堆栈
  2. 从左到右读入后缀表达式
  3. 如果字符是一个操作数,把它压入堆栈。
  4. 如果字符是个操作符,弹出两个操作数,执行恰当操作,然后把结果压入堆栈。如果您不能够弹出两个操作数,后缀表达式的语法就不正确。
  5. 到后缀表达式末尾,从堆栈中弹出结果。若后缀表达式格式正确,那么堆栈应该为空。

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
using System;
using System.Collections.Generic;
using System.Diagnostics;
//System.Diagnostics命名空间 包含了能够与系统进程 事件日志 和性能计数器进行交互的类
//一般用于帮助诊断和调试应用程序
namespace ConsoleAppTest
{
    class Program
    {
        //是否为数字
        //判断是否为整数字符串
        static bool isNumber(string message)
        {
            //判断是否为整数字符串
            //是的话则将其转换为数字并将其设为out类型的输出值、返回true, 否则为false
            int result = -1;   //result 定义为out 用来输出值
            try
            {
                //当数字字符串的为是少于4时,以下三种都可以转换,任选一种
                //如果位数超过4的话,请选用Convert.ToInt32() 和int.Parse()
                //result = int.Parse(message);
                //result = Convert.ToInt16(message);
                result = Convert.ToInt32(message);
                return true;
            }
            catch
            {
                return false;
            }
        }
        //是否为操作符
        static bool isOpertor(string c)
        {
            return c == "+" || c == "-" || c == "*" || c == "/" || c == "(" || c == ")";
        }
        //判断操作符优先级大小
        static bool comparePriority(string op1, string op2)
        {
            return getPriorityValue(op1) > getPriorityValue(op2);
        }
        //获取操作符优先级
        static int getPriorityValue(string c)
        {
            int priority;
            switch (c)
            {
                case "+":
                    priority = 1;
                    break;
                case "-":
                    priority = 1;
                    break;
                case "*":
                    priority = 2;
                    break;
                case "/":
                    priority = 2;
                    break;
                default:
                    priority = 0;
                    break;
            }
            return priority;
        }
        //计算值
        static int getOperand(string op, int num1, int num2)
        {
            int result = -1;
            switch (op)
            {
                case "+":
                    result = num1 + num2;
                    break;
                case "-":
                    result = num1 - num2;
                    break;
                case "*":
                    result = num1 * num2;
                    break;
                case "/":
                    result = num1 / num2;
                    break;
            }
            return result;
        }
        static Stack<string> changeExpression(List<string> beforeExps)
        {
            Stack<string> operand = new Stack<string>();//操作数
            Stack<string> opertor = new Stack<string>();//操作符
            //遍历中序表示
            int length = beforeExps.Count;
            //判断是否为操作数 
            for (int i = 0; i < length; i++)
            {
                string c = beforeExps[i];
                if (isNumber(c))
                {
                    //操作数 存在操作数栈
                    operand.Push(c);
                }
                else
                {
                    //为运算符 
                    //若运算符为"("直接存入到运算符栈中
                    if (c == "(")
                    {
                        opertor.Push(c);
                    }
                    else if (c == ")")
                    {
                        //该运算符为右括号")",则输出运算符堆栈中的运算符到操作数堆栈,直到遇到左括号为止。 将"("出栈 
                        while (opertor.Peek() != "(")
                        {
                            string stringvalue = opertor.Pop();
                            operand.Push(stringvalue);
                        }
                        opertor.Pop();
                    }
                    else
                    {
                        // 该运算符为非括号运算符:
                        //考虑栈顶为空的情况
                        if (opertor.Count <= 0)
                        {
                            opertor.Push(c);
                            continue;
                        }
                        // (a) 若运算符堆栈栈顶的运算符为括号,则直接存入运算符堆栈。
                        ////符合为左括号 直接存入运算符
                        if (opertor.Peek() == "(")
                        {
                            opertor.Push(c);
                        }
                        else
                        {
                            //(b) 若比运算符堆栈栈顶的运算符优先级高或相等,则直接存入运算符堆栈。 
                            if (comparePriority(c, opertor.Peek()))
                            {
                                opertor.Push(c);
                            }
                            else
                            {
                                // (c) 若比运算符堆栈栈顶的运算符优先级低,则输出栈顶运算符到操作数堆栈,并将当前运算符压入运算符堆栈。
                                string stringvalue = opertor.Pop();
                                operand.Push(stringvalue);
                                opertor.Push(c);
                            }
                        }
                    }
                }
            }
            //4、当表达式读取完成后运算符堆栈中尚有运算符时,则依序取出运算符到操作数堆栈,直到运算符堆栈为空。
            while (opertor.Count > 0)
            {
                string stringvalue = opertor.Pop();
                operand.Push(stringvalue);
            }
            //反转operand 获取正常的会缀表达式
            Stack<string> resultSt = new Stack<string>();
            while (operand.Count > 0)
            {
                string stringvalue = operand.Pop();
                resultSt.Push(stringvalue);
            }
            return resultSt;
        }
        
        //转换string为list表
        static List<string> changeStrToList(string str)
        {
            List<string> resultSt = new List<string>();
            List<int> sortNum = new List<int>();
            bool isConNum = false;
            foreach(char c in str)
            {
                if (isOpertor(c.ToString()))
                {
                    if (isConNum && sortNum.Count > 0)
                    {
                        //添加数字
                        int num = 0;
                        for (int i = sortNum.Count -1 ; i >= 0; i--)
                        {
                            if (i == sortNum.Count - 1)
                            {
                                num = num + sortNum[i];
                            }else{
                                num = num + sortNum[i] * 10 * (sortNum.Count - 1 - i );
                            }
                           
                        }
                        resultSt.Add(num.ToString());
                        sortNum.Clear();
                    }
                    isConNum = false;
                    //如果是操作符直接添加
                    resultSt.Add(c.ToString());
                }else{
                    //如果是数字
                    isConNum = true;
                    sortNum.Add(int.Parse(c.ToString()));
                }
            }
            if (sortNum.Count > 0)
            {
                //添加数字
                int num = 0;
                for (int i = sortNum.Count - 1; i >= 0; i--)
                {
                    if (i == sortNum.Count - 1)
                    {
                        num = num + sortNum[i];
                    }
                    else
                    {
                        num = num + sortNum[i] * 10 * (sortNum.Count - 1 - i);
                    }
                }
                resultSt.Add(num.ToString());
                sortNum.Clear();
            }
            return resultSt;
        }
        //计算逆波兰公式
        static int calculateExpression(Stack<string> st)
        {
            //临时存储计算数据
            Stack<string> reslutSt = new Stack<string>();
            while(st.Count > 0)
            {
                string numStr = st.Peek();
                if (isNumber(numStr))
                {
                    //如果字符是一个操作数,把它压入堆栈。
                    reslutSt.Push(numStr);
                }
                else
                {
                    //如果字符是个操作符,弹出两个操作数,执行恰当操作,
                    //然后把结果压入堆栈。如果您不能够弹出两个操作数,后缀表达式的语法就不正确。
                    int number1 = int.Parse(reslutSt.Pop());
                    int number2 = int.Parse(reslutSt.Pop());
                    int value = getOperand(numStr, number2, number1);
                    reslutSt.Push(value.ToString());
                }
                st.Pop();
            }
            return int.Parse(reslutSt.Peek());
        }
        static void Main(string[] args)
        {
            Console.WriteLine("C# 逆波兰公式 输入您要计算的公式,按enter结束");
            //string inputStr = Console.ReadLine();
            string teststr = Console.ReadLine();
            List<string> inputStr = changeStrToList(teststr);
            Console.WriteLine("公式为:");
            foreach (string str in inputStr)
            {
                Console.Write("{0} ", str);
            }
            Console.Write(" ===> ");
            Stack<string> changeSt = changeExpression(inputStr);
            foreach (string str in changeSt)
            {
                Console.Write("{0} ", str);
            }
            Console.WriteLine("逆波兰公式计算:");
            //计算公式的值
            int resultValue = calculateExpression(changeSt);
            Console.WriteLine("{0} = {1} ", teststr ,resultValue);
            Console.ReadKey();
        }
    }
}

C# 性能优化最佳实践

1、显式注册的EvenHandler要显式注销以避免内存泄漏

将一个成员方法注册到某个对象的事件会造成后者持有前者的引用。在事件注销之前,前者不会被垃圾回收。

[csharp] view plain copy

  1. private void Form1_Load()
  2. {
  3.   ……
  4.   //注册事件
  5.   CommandRemotingContext.CmdChanged += new ReciverCmdStateChangedEventHandler(this.CommandRemotingContext_CmdChanged);
  6.   ……
  7. }
  8. private void Form1_FromClosed()
  9. {
  10.   ……
  11.   //关闭窗体时及时释放事件
  12.   CommandRemotingContext.CmdChanged -= new ReciverCmdStateChangedEventHandler(this.CommandRemotingContext_CmdChanged);
  13.   ……
  14. }

由事件引起的内存泄漏问题:

  • 对象A订阅了对象B中的事件
  • 对象A的生命周期远远大于对象B
  • 对象A没有取消订阅对象B的时间
  • 最终导致对象B无法释放

2、控件绑定的数据源批量操作应避免自动刷新

  • 客户端批量操作数据时,控件自带的刷新操作,会造成不必要的时间消耗
  • 当数据源(如DataTable、Array、List、ObservableCollection或其他IListSource等)被绑定到控件时,批量操作数据时应该断开绑定或挂起控件的刷新。
    [csharp] view plain copy

    1. this.gcBillList.DataSource = null;
    2. DataRowCollection rows = this.ds.Tables[0].Rows;
    3. foreach (DataRow row in rows)
    4. {
    5.     // DataRow数据操作
    6. }
    7. this.gcBillList.DataSource = this.ds.Tables[0].DefaultView;

3、减少客户端与服务端的通信次数

  • WebService调用并非越少越好,传输数据量较大的情况可考虑拆分为多次调用
  • 对于短WebService的调用,应尽量合并以减少交互次数
    [csharp] view plain copy

    1. //多次调用了相同的WS  
    2. txtCompanyName.Text=SCPubFunctionClient.PublicWSCal<string>(“ForTest”, “GetCompanyNameByID”,“0001”);  txtCompanyInnerName.Text=SCPubFunctionClient.PublicWSCal<string>(“ForTest”, “GetCompanyInnerNameByID”,“0001”);
    3. //合并相邻的WS  
    4. string[] result=SCPubFunctionClient.PublicWSCal<string>(“ForTest”, “GetCompanyNameAndInnerNameByID”,“0001”);
    5. txtCompanyName.Text=result[0];
    6. txtCompanyInnerName.Text= result[1];

4、减少客户端与服务端的通信次数

如非必要,应尽量避免在循环体内重复调用WebService

[csharp] view plain copy

  1. //循环调用了相同的WS  
  2. List<Person> persons;
  3. ……
  4. foreach(string personID in personIDs)
  5. {
  6.     person=HRPubWsClient.getPerson(personID);
  7.     persons.Add(person);
  8. }
  9. //合并WS  
  10. List<Person> persons;
  11. ……
  12. persons =HRPubWsClient.getPersonList(personIDs);

5、使用泛型来避免装箱、拆箱操作(减少垃圾回收压力)

  • 装箱操作会造成GC压力;如果发生在集合中,应该使用泛型集合避免。
  • 对于值类型的集合,使用List<T>来代替ArrayList,使用Dictionary<TKey, TValue> 来代替Hashtable。
    [csharp] view plain copy

    1. ArrayList h=new ArrayList();  //不建议
    2. h.Add(1);
    3. List<object> h = new List<object>();  //不建议
    4. h.Add(1);
    5. List<int> h = new List<int>();    //建议
    6. h.Add(1);

6、字符串操作:

 C# 字符串操作–减少垃圾回收压力
7、使用常量避免创建对象

  • 如下例,程序中存在大量 new decimal(0)的代码,这会导致小对象频繁创建及回收;正确的做法是使用 Decimal.Zero 常量。
    [csharp] view plain copy

    1. private string CurrencyCalc()
    2. {
    3.     if (firstValue == new decimal(0))  ……
    4.     if (secondValue == new decimal(0)) ……
    5.     if (thirdValue == new decimal(0)) ……
    6.     if (fourthValue == new decimal(0)) ……
    7.     ……
    8. }

8、避免不必要的抛出异常

C# 异常处理(Catch Throw)IL分析

9、使用RemoveAll而非RemoveAt进行删除多个元素

  • 使用RemoveAll方法对集合(如List)中的多个元素进行一次性删除时,只会对List的内部数组做一次resize 操作,效率明显高于循环调用RemoveAt。
    [csharp] view plain copy

    1. List<string>  lst = new List<string> {“1”“2”“3”“1”“2”“4”};
    2. //不建议:
    3. for (int i = lst.Count – 1; i >= 0; i–)
    4. {
    5.     if (lst[i] == “1” || lst[i] == “2”)
    6.     {
    7.         lst.RemoveAt(i);
    8.      }
    9. }
    10. //建议:
    11. lst.RemoveAll(s => s == “1” || s == “2”);

10、C# DataSet性能最佳实践

11、反射与动态绑定–减少CPU占用

  • 反射技术是将编译期间的静态绑定转换为延迟到运行期间的动态绑定。
  • C#主要支持 5 种动态创建对象的方式(时间消耗来自网络,与我实测的差距挺大,具体测试见下方):
动态创建对象的方式 与Direct Create
1.Type.InvokeMember 慢40倍以上
2.ContructorInfo.Invoke 慢40倍以上
3.Activator.CreateInstance(Type) 慢7倍
4.Activator.CreateInstance(assemblyName, typeName) 慢1000倍以上
5.Assembly.CreateInstance(typeName) 慢40倍以上
  • 应尽量避免使用反射和动态绑定;如必须使用,要遵循以下原则:

1. 使用接口调用方式将动态绑定改造为早期绑定(Direct Call)
2. 使用 Activator.CreateInstance(Type)方式动态创建对象

3. 使用typeof操作符代替GetType调用

小注:

通过循环创建实例记录时间如下:

加载程序集、获取类型在循环外部时间如下(这时不同创建方式消耗时间差距挺大):

代码如下:

[csharp] view plain copy

  1. public void TestCreateInstance()
  2.        {
  3.            Stopwatch watch1 = new Stopwatch();
  4.            var asmb = Assembly.LoadFrom(“ReflectiveClassLibrary.dll”);
  5.            Type type = asmb.GetType(“ReflectiveClassLibrary.TestClass”);
  6.            int num = 100000;
  7.            watch1.Start();
  8.            for (int i = 0; i < num; i++)
  9.            {
  10.                //var asmb = Assembly.LoadFrom(“ReflectiveClassLibrary.dll”);
  11.                //Type type = asmb.GetType(“ReflectiveClassLibrary.TestClass”);
  12.                Activator.CreateInstance(type);
  13.            }
  14.            watch1.Stop();
  15.            label1.Text = “Activator.CreateInstance(Type type)时间:” + watch1.ElapsedMilliseconds + “毫秒”;
  16.            watch1.Reset();
  17.            watch1.Start();
  18.            for (int i = 0; i < num; i++)
  19.            {
  20.                Activator.CreateInstance(“ReflectiveClassLibrary”“ReflectiveClassLibrary.TestClass”);
  21.            }
  22.            watch1.Stop();
  23.            label2.Text = “Activator.CreateInstance(string assemblyName,string typeName)时间:” + watch1.ElapsedMilliseconds + “毫秒”;
  24.            watch1.Reset();
  25.            watch1.Start();
  26.            for (int i = 0; i < num; i++)
  27.            {
  28.                //var asmb = Assembly.LoadFrom(“ReflectiveClassLibrary.dll”);
  29.                //加载程序集                
  30.                asmb.CreateInstance(“TestClass”);
  31.            }
  32.            watch1.Stop();
  33.            label3.Text = “assembly.CreateInstance(string typeName)时间:” + watch1.ElapsedMilliseconds + “毫秒”;
  34.            watch1.Reset();
  35.            watch1.Start();
  36.            for (int i = 0; i < num; i++)
  37.            {
  38.                //var asmb = Assembly.LoadFrom(“ReflectiveClassLibrary.dll”);
  39.                //Type type = asmb.GetType(“ReflectiveClassLibrary.TestClass”);
  40.                object obj = type.InvokeMember(null, BindingFlags.Public |
  41.               BindingFlags.Instance | BindingFlags.CreateInstance, nullnullnull);
  42.            }
  43.            watch1.Stop();
  44.            label4.Text = “Type.InvokeMember(string name, BindingFlags invokeAttr, Binder binder, object target, object[] args)时间:” + watch1.ElapsedMilliseconds + “毫秒”;
  45.            watch1.Reset();
  46.            watch1.Start();
  47.            for (int i = 0; i < num; i++)
  48.            {
  49.                //var asmb = Assembly.LoadFrom(“ReflectiveClassLibrary.dll”);
  50.                //Type type = asmb.GetType(“ReflectiveClassLibrary.TestClass”);
  51.                ConstructorInfo constructorInfo = type.GetConstructors()[0];
  52.                constructorInfo.Invoke(null);
  53.            }
  54.            watch1.Stop();
  55.            label5.Text = “ContructorInfo.Invoke(object[] parameters)时间:” + watch1.ElapsedMilliseconds + “毫秒”;
  56.        }

加载程序集、获取类型在循环内部时间如下(这时不同创建方式消耗时间差距比较小):

代码如下:

[csharp] view plain copy

  1. public void TestCreateInstance()
  2.        {
  3.            Stopwatch watch1 = new Stopwatch();
  4.            //var asmb = Assembly.LoadFrom(“ReflectiveClassLibrary.dll”);
  5.            //Type type = asmb.GetType(“ReflectiveClassLibrary.TestClass”);
  6.            int num = 100000;
  7.            watch1.Start();
  8.            for (int i = 0; i < num; i++)
  9.            {
  10.                var asmb = Assembly.LoadFrom(“ReflectiveClassLibrary.dll”);
  11.                Type type = asmb.GetType(“ReflectiveClassLibrary.TestClass”);
  12.                Activator.CreateInstance(type);
  13.            }
  14.            watch1.Stop();
  15.            label1.Text = “Activator.CreateInstance(Type type)时间:” + watch1.ElapsedMilliseconds + “毫秒”;
  16.            watch1.Reset();
  17.            watch1.Start();
  18.            for (int i = 0; i < num; i++)
  19.            {
  20.                Activator.CreateInstance(“ReflectiveClassLibrary”“ReflectiveClassLibrary.TestClass”);
  21.            }
  22.            watch1.Stop();
  23.            label2.Text = “Activator.CreateInstance(string assemblyName,string typeName)时间:” + watch1.ElapsedMilliseconds + “毫秒”;
  24.            watch1.Reset();
  25.            watch1.Start();
  26.            for (int i = 0; i < num; i++)
  27.            {
  28.                var asmb = Assembly.LoadFrom(“ReflectiveClassLibrary.dll”);
  29.                //加载程序集                
  30.                asmb.CreateInstance(“TestClass”);
  31.            }
  32.            watch1.Stop();
  33.            label3.Text = “assembly.CreateInstance(string typeName)时间:” + watch1.ElapsedMilliseconds + “毫秒”;
  34.            watch1.Reset();
  35.            watch1.Start();
  36.            for (int i = 0; i < num; i++)
  37.            {
  38.                var asmb = Assembly.LoadFrom(“ReflectiveClassLibrary.dll”);
  39.                Type type = asmb.GetType(“ReflectiveClassLibrary.TestClass”);
  40.                object obj = type.InvokeMember(null, BindingFlags.Public |
  41.               BindingFlags.Instance | BindingFlags.CreateInstance, nullnullnull);
  42.            }
  43.            watch1.Stop();
  44.            label4.Text = “Type.InvokeMember(string name, BindingFlags invokeAttr, Binder binder, object target, object[] args)时间:” + watch1.ElapsedMilliseconds + “毫秒”;
  45.            watch1.Reset();
  46.            watch1.Start();
  47.            for (int i = 0; i < num; i++)
  48.            {
  49.                var asmb = Assembly.LoadFrom(“ReflectiveClassLibrary.dll”);
  50.                Type type = asmb.GetType(“ReflectiveClassLibrary.TestClass”);
  51.                ConstructorInfo constructorInfo = type.GetConstructors()[0];
  52.                constructorInfo.Invoke(null);
  53.            }
  54.            watch1.Stop();
  55.            label5.Text = “ContructorInfo.Invoke(object[] parameters)时间:” + watch1.ElapsedMilliseconds + “毫秒”;
  56.        }

测试代码如下:c# 反射测试demo

12、序列化与反序列化

  • 相对于XML、二进制序列化方式,Protobuf效率较高,支持数据量较大
  • protobuf序列化后的大小是json的1/10,xml格式的1/20,是二进制序列化的1/10

C#  序列化

C# Protobuf-Net 序列化

13、数据压缩

在数据从客户端传输到服务端过程,为减少数据传输量,建议对数据进行压缩处理。

常用的数据压缩: C# 文件流压缩解压

14、

C# 使用PrintDocument 绘制表格 完成 打印预览 DataTable

经过不断的Google与baidu,最终整理出来的打印类

主要是根据两个参考的类组合而成,稍微修改了一下,参考代码及来源见最后(其中一份是VB语言的)

其中遇到的一些问题也已经得到了解决(分页,打印预览点击打印内容缺失)

——————————————————————————

相关知识

PrintDocument

定义一个可再次使用的对象,该对象将输出发送到打印机。

命名空间:System.Drawing.Printing
程序集:System.Drawing(在 system.drawing.dll 中)

通常可以创建 PrintDocument 类的实例,设置描述打印方式的属性,然后调用 Print 方法开始打印进程。通过使用 PrintPageEventArgs 中包含的 Graphics 来处理用于指定打印输出的 PrintPage 事

PrintDocument.PrintPage 事件

当需要为当前页打印的输出时发生。

PrintDocument.Print 方法

开始文档的打印进程。

PrintPageEventArgs.HasMorePages 

获取或设置一个值,该值指示是否打印附加页。

以上摘自:http://msdn.microsoft.com/zh-cn/library/system.drawing.printing.printdocument(v=VS.80).aspx

—————————

打印类

复制代码
using System.Drawing;
using System.Drawing.Printing;
using System.Windows.Forms;
using System.Data;
using System;

/// <summary>
/// 打印,打印预览
/// 唐小熊 2013-08-01
/// </summary>
public class ToPrint
{

    //以下用户可自定义
    //当前要打印文本的字体及字号
    private static Font TableFont = new Font("Verdana", 10, FontStyle.Regular);
    //表头字体
    private Font HeadFont = new Font("Verdana", 20, FontStyle.Bold);
    //表头文字
    private string HeadText = string.Empty;
    //表头高度
    private int HeadHeight = 40;
    //表的基本单位
    private int[] XUnit;
    private int YUnit = TableFont.Height * 2;
    //以下为模块内部使用
    private PrintDocument DataTablePrinter;
    private DataRow DataGridRow;
    private DataTable DataTablePrint;
    //当前要所要打印的记录行数,由计算得到
    private int PageRecordNumber;
    //正要打印的页号
    private int PrintingPageNumber = 1;
    //已经打印完的记录数
    private int PrintRecordComplete;
    private int PLeft;
    private int PTop;
    private int PRight;
    private int PBottom;
    private int PWidth;
    private int PHeigh;
    //当前画笔颜色
    private SolidBrush DrawBrush = new SolidBrush(Color.Black);
    //每页打印的记录条数
    private int PrintRecordNumber;
    //第一页打印的记录条数
    private int FirstPrintRecordNumber;
    //总共应该打印的页数
    private int TotalPage;
    //与列名无关的统计数据行的类目数(如,总计,小计......)
    public int TotalNum = 0;

    /// <summary>
    /// 打印
    /// </summary>
    /// <param name="dt">要打印的DataTable</param>
    /// <param name="Title">打印文件的标题</param>
    public void Print(DataTable dt, string Title)
    {
        try
        {
            CreatePrintDocument(dt, Title).Print();
        }
        catch (Exception ex)
        {
            MessageBox.Show("打印错误,请检查打印设置!");

        }
    }

    /// <summary>
    /// 打印预览
    /// </summary>
    /// <param name="dt">要打印的DataTable</param>
    /// <param name="Title">打印文件的标题</param>
    public void PrintPriview(DataTable dt, string Title)
    {
        try
        {
            PrintPreviewDialog PrintPriview = new PrintPreviewDialog();
            PrintPriview.Document = CreatePrintDocument(dt, Title);
            PrintPriview.WindowState = FormWindowState.Maximized;
            PrintPriview.ShowDialog();
        }
        catch (Exception ex)
        {
            MessageBox.Show("打印错误,请检查打印设置!");

        }
    }

    /// <summary>
    /// 创建打印文件
    /// </summary>
    private PrintDocument CreatePrintDocument(DataTable dt, string Title)
    {

        DataTablePrint = dt;
        HeadText = Title;
        DataTablePrinter = new PrintDocument();

        PageSetupDialog PageSetup = new PageSetupDialog();
        PageSetup.Document = DataTablePrinter;
        DataTablePrinter.DefaultPageSettings = PageSetup.PageSettings;
        DataTablePrinter.DefaultPageSettings.Landscape = true;//设置打印横向还是纵向
        //PLeft = 30; //DataTablePrinter.DefaultPageSettings.Margins.Left;
        PTop = DataTablePrinter.DefaultPageSettings.Margins.Top;
        //PRight = DataTablePrinter.DefaultPageSettings.Margins.Right;
        PBottom = DataTablePrinter.DefaultPageSettings.Margins.Bottom;
        PWidth = DataTablePrinter.DefaultPageSettings.Bounds.Width;
        PHeigh = DataTablePrinter.DefaultPageSettings.Bounds.Height;
        XUnit = new int[DataTablePrint.Columns.Count];
        PrintRecordNumber = Convert.ToInt32((PHeigh - PTop - PBottom - YUnit) / YUnit);
        FirstPrintRecordNumber = Convert.ToInt32((PHeigh - PTop - PBottom - HeadHeight - YUnit) / YUnit);

        if (DataTablePrint.Rows.Count > PrintRecordNumber)
        {
            if ((DataTablePrint.Rows.Count - FirstPrintRecordNumber) % PrintRecordNumber == 0)
            {
                TotalPage = (DataTablePrint.Rows.Count - FirstPrintRecordNumber) / PrintRecordNumber + 1;
            }
            else
            {
                TotalPage = (DataTablePrint.Rows.Count - FirstPrintRecordNumber) / PrintRecordNumber + 2;
            }
        }
        else
        {
            TotalPage = 1;
        }
 
        DataTablePrinter.PrintPage += new PrintPageEventHandler(DataTablePrinter_PrintPage);
        DataTablePrinter.DocumentName = HeadText;

        return DataTablePrinter;
    }

    /// <summary>
    /// 打印当前页
    /// </summary>
    private void DataTablePrinter_PrintPage(object sende, PrintPageEventArgs Ev)
    {


        int tableWith = 0;
        string ColumnText;

        StringFormat sf = new StringFormat();
        sf.Alignment = StringAlignment.Center;

        //打印表格线格式
        Pen Pen = new Pen(Brushes.Black, 1);

        #region 设置列宽

        foreach (DataRow dr in DataTablePrint.Rows)
        {
            for (int i = 0; i < DataTablePrint.Columns.Count; i++)
            {
                int colwidth = Convert.ToInt32(Ev.Graphics.MeasureString(dr[i].ToString().Trim(), TableFont).Width);
                if (colwidth > XUnit[i])
                {
                    XUnit[i] = colwidth;
                }
            }
        }

        if (PrintingPageNumber == 1)
        {
            for (int Cols = 0; Cols <= DataTablePrint.Columns.Count - 1; Cols++)
            {
                ColumnText = DataTablePrint.Columns[Cols].ColumnName.ToString().Trim();
                int colwidth = Convert.ToInt32(Ev.Graphics.MeasureString(ColumnText, TableFont).Width);
                if (colwidth > XUnit[Cols])
                {
                    XUnit[Cols] = colwidth;
                }
            }
        }
        for (int i = 0; i < XUnit.Length; i++)
        {
            tableWith += XUnit[i];
        }
        #endregion

        PLeft = (Ev.PageBounds.Width - tableWith) / 2;
        int x = PLeft;
        int y = PTop;
        int stringY = PTop + (YUnit - TableFont.Height) / 2;
        int rowOfTop = PTop;

        //第一页
        if (PrintingPageNumber == 1)
        {
            //打印表头
            Ev.Graphics.DrawString(HeadText, HeadFont, DrawBrush, new Point(Ev.PageBounds.Width / 2, PTop), sf);


            //设置为第一页时行数
            PageRecordNumber = FirstPrintRecordNumber;
            rowOfTop = y = PTop + HeadFont.Height + 10;
            stringY = PTop + HeadFont.Height + 10 + (YUnit - TableFont.Height) / 2;
        }
        else
        {
            //计算,余下的记录条数是否还可以在一页打印,不满一页时为假
            if (DataTablePrint.Rows.Count - PrintRecordComplete >= PrintRecordNumber)
            {
                PageRecordNumber = PrintRecordNumber;
            }
            else
            {
                PageRecordNumber = DataTablePrint.Rows.Count - PrintRecordComplete;
            }
        }

        #region 列名
        if (PrintingPageNumber == 1 || PageRecordNumber > TotalNum)//最后一页只打印统计行时不打印列名
        {
            //得到datatable的所有列名
            for (int Cols = 0; Cols <= DataTablePrint.Columns.Count - 1; Cols++)
            {
                ColumnText = DataTablePrint.Columns[Cols].ColumnName.ToString().Trim();

                int colwidth = Convert.ToInt32(Ev.Graphics.MeasureString(ColumnText, TableFont).Width);
                Ev.Graphics.DrawString(ColumnText, TableFont, DrawBrush, x, stringY);
                x += XUnit[Cols];
            }
        }
        #endregion



        Ev.Graphics.DrawLine(Pen, PLeft, rowOfTop, x, rowOfTop);
        stringY += YUnit;
        y += YUnit;
        Ev.Graphics.DrawLine(Pen, PLeft, y, x, y);

        //当前页面已经打印的记录行数
        int PrintingLine = 0;
        while (PrintingLine < PageRecordNumber)
        {
            x = PLeft;
            //确定要当前要打印的记录的行号
            DataGridRow = DataTablePrint.Rows[PrintRecordComplete];
            for (int Cols = 0; Cols <= DataTablePrint.Columns.Count - 1; Cols++)
            {
                Ev.Graphics.DrawString(DataGridRow[Cols].ToString().Trim(), TableFont, DrawBrush, x, stringY);
                x += XUnit[Cols];
            }
            stringY += YUnit;
            y += YUnit;
            Ev.Graphics.DrawLine(Pen, PLeft, y, x, y);

            PrintingLine += 1;
            PrintRecordComplete += 1;
            if (PrintRecordComplete >= DataTablePrint.Rows.Count)
            {
                Ev.HasMorePages = false;
                PrintRecordComplete = 0;
            }
        }

        Ev.Graphics.DrawLine(Pen, PLeft, rowOfTop, PLeft, y);
        x = PLeft;
        for (int Cols = 0; Cols < DataTablePrint.Columns.Count; Cols++)
        {
            x += XUnit[Cols];
            Ev.Graphics.DrawLine(Pen, x, rowOfTop, x, y);
        }



        PrintingPageNumber += 1;

        if (PrintingPageNumber > TotalPage)
        {
            Ev.HasMorePages = false;
            PrintingPageNumber = 1;
            PrintRecordComplete = 0;
        }
        else
        {
            Ev.HasMorePages = true;
        }


    }

}
复制代码

打印多页时 要根据剩余页数判断并设置 HasMorePages 是否继续执行 PrintPage

打印预览中点击打印按钮,实际上会再次执行 PrintPage 事件,所以再打印完文档后 应该对相关属性进行重置

复制代码
 if (PrintingPageNumber > TotalPage)
 {
    Ev.HasMorePages = false;
    PrintingPageNumber = 1;
    PrintRecordComplete = 0;
 }
 else
 {
    Ev.HasMorePages = true;
 }
复制代码

 

参考代码如下(或者说是复制的原本 ^_^)

版本一(VB)

来自 :http://bbs.csdn.net/topics/340132124 printdocument控件如何绘制表格?

复制代码
Imports System.Drawing
Imports System.Drawing.Pen
Imports System.Drawing.Font
Imports System.Drawing.PointF
Imports System.Drawing.Color
Imports System.Drawing.Printing
Imports System.Windows.Forms
Imports System.Windows.Forms.DataGrid

Public Class PrintDataTable
    '以下用户可自定义
    '当前要打印文本的字体及字号
    Private TableFont As New Font("宋体", 10)
    '表头字体
    Private HeadFont As New Font("宋体", 20, FontStyle.Bold)
    '副表头字体
    Private SubHeadFont As New Font("宋体", 10, FontStyle.Regular)
    '表头文字
    Private HeadText As String
    '副表头左文字
    Private SubHeadLeftText As String
    '副表头右文字
    Private SubHeadRightText As String
    '表头高度
    Private HeadHeight As Integer = 40
    '副表头高度
    Private SubHeadHeight As Integer = 20
    '表脚字体
    Private FootFont As New Font("宋体", 10, FontStyle.Regular)
    '副表脚字体
    Private SubFootFont As New Font("宋体", 10, FontStyle.Regular)
    '表脚文字
    Private FootText As String
    '副表脚左文字
    Private SubFootLeftText As String
    '副表脚右文字
    Private SubFootRightText As String
    '表脚高度
    Private FootHeight As Integer = 30
    '副表脚高度
    Private SubFootHeight As Integer = 20
    '表的基本单位
    Dim XUnit As Integer
    Dim YUnit As Integer = TableFont.Height * 2.5
    '以下为模块内部使用
    Private Ev As PrintPageEventArgs
    Private DataTablePrinter As PrintDocument
    Private DataGridColumn As DataColumn
    Private DataGridRow As DataRow
    Private DataTablePrint As DataTable
    '当前要打印的行
    Private Rows As Integer
    '当前DATAGRID共有多少列
    Private ColsCount As Integer
    '当前正要打印的行号
    Private PrintingLineNumber As Integer
    '当前要所要打印的记录行数,由计算得到
    Private PageRecordNumber As Integer
    '正要打印的页号
    Private PrintingPageNumber As Integer
    '共需要打印的页数
    Private PageNumber As Integer
    '当前还有多少页没有打印
    Private PrintRecordLeave As Integer
    '已经打印完的记录数
    Private PrintRecordComplete As Integer
    Private PLeft As Integer
    Private PTop As Integer
    Private PRight As Integer
    Private PBottom As Integer
    Private PWidth As Integer
    Private PHeigh As Integer
    '当前画笔颜色
    Private DrawBrush As New SolidBrush(System.Drawing.Color.Black)
    '每页打印的记录条数
    Private PrintRecordNumber As Integer
    '总共应该打印的页数
    Private TotalPage As Integer

    Sub New(ByVal TableSource As DataTable)
        DataTablePrint = New DataTable
        DataTablePrint = TableSource
        ColsCount = DataTablePrint.Columns.Count
    End Sub

    '用户自定义字体及字号
    Public WriteOnly Property SetTableFont() As System.Drawing.Font
        Set(ByVal Value As System.Drawing.Font)
            TableFont = Value
        End Set
    End Property

    Public WriteOnly Property SetHeadFont() As System.Drawing.Font
        Set(ByVal Value As System.Drawing.Font)
            HeadFont = Value
        End Set
    End Property

    Public WriteOnly Property SetSubHeadFont() As System.Drawing.Font
        Set(ByVal Value As System.Drawing.Font)
            SubHeadFont = Value
        End Set
    End Property

    Public WriteOnly Property SetFootFont() As System.Drawing.Font
        Set(ByVal Value As System.Drawing.Font)
            FootFont = Value
        End Set
    End Property

    Public WriteOnly Property SetSubFootFont() As System.Drawing.Font
        Set(ByVal Value As System.Drawing.Font)
            SubFootFont = Value
        End Set
    End Property

    Public WriteOnly Property SetHeadText() As String
        Set(ByVal Value As String)
            HeadText = Value
        End Set
    End Property

    Public WriteOnly Property SetSubHeadLeftText() As String
        Set(ByVal Value As String)
            SubHeadLeftText = Value
        End Set
    End Property

    Public WriteOnly Property SetSubHeadRightText() As String
        Set(ByVal Value As String)
            SubHeadRightText = Value
        End Set
    End Property

    Public WriteOnly Property SetFootText() As String
        Set(ByVal Value As String)
            FootText = Value
        End Set
    End Property

    Public WriteOnly Property SetSubFootLeftText() As String
        Set(ByVal Value As String)
            SubFootLeftText = Value
        End Set
    End Property

    Public WriteOnly Property SetSubFootRightText() As String
        Set(ByVal Value As String)
            SubFootRightText = Value
        End Set
    End Property

    Public WriteOnly Property SetHeadHeight() As Integer
        Set(ByVal Value As Integer)
            HeadHeight = Value
        End Set
    End Property

    Public WriteOnly Property SetSubHeadHeight() As Integer
        Set(ByVal Value As Integer)
            SubHeadHeight = Value
        End Set
    End Property

    Public WriteOnly Property SetFootHeight() As Integer
        Set(ByVal Value As Integer)
            FootHeight = Value
        End Set
    End Property

    Public WriteOnly Property SetSubFootHeight() As Integer
        Set(ByVal Value As Integer)
            SubFootHeight = Value
        End Set
    End Property

    Public WriteOnly Property SetCellHeight() As Integer
        Set(ByVal Value As Integer)
            YUnit = Value
        End Set
    End Property

    Public Sub Print()
        Try
            DataTablePrinter = New Printing.PrintDocument
            AddHandler DataTablePrinter.PrintPage, AddressOf DataTablePrinter_PrintPage
            Dim PageSetup As PageSetupDialog
            PageSetup = New PageSetupDialog
            PageSetup.Document = DataTablePrinter
            DataTablePrinter.DefaultPageSettings = PageSetup.PageSettings
            If PageSetup.ShowDialog() = DialogResult.Cancel Then
                Exit Sub
            End If
            PLeft = DataTablePrinter.DefaultPageSettings.Margins.Left
            PTop = DataTablePrinter.DefaultPageSettings.Margins.Top
            PRight = DataTablePrinter.DefaultPageSettings.Margins.Right
            PBottom = DataTablePrinter.DefaultPageSettings.Margins.Bottom
            PWidth = DataTablePrinter.DefaultPageSettings.Bounds.Width
            PHeigh = DataTablePrinter.DefaultPageSettings.Bounds.Height
            '将当前页分成基本的单元
            XUnit = (PWidth - PLeft - PRight) / DataTablePrint.Columns.Count - 1
            PrintRecordNumber = (PHeigh - PTop - PBottom - HeadHeight - SubHeadHeight - FootHeight - SubFootHeight - YUnit) \ YUnit
            If DataTablePrint.Rows.Count > PrintRecordNumber Then
                If DataTablePrint.Rows.Count Mod PrintRecordNumber = 0 Then
                    TotalPage = DataTablePrint.Rows.Count \ PrintRecordNumber
                Else
                    TotalPage = DataTablePrint.Rows.Count \ PrintRecordNumber + 1
                End If
            Else
                TotalPage = 1
            End If
            DataTablePrinter.DocumentName = TotalPage.ToString
            Dim PrintPriview As PrintPreviewDialog
            PrintPriview = New PrintPreviewDialog
            PrintPriview.Document = DataTablePrinter
            PrintPriview.WindowState = FormWindowState.Maximized
            PrintPriview.ShowDialog()
        Catch ex As Exception
            MsgBox("打印错误,请检查打印设置!", 16, "错误")
        End Try
    End Sub



    Private Sub DataTablePrinter_PrintPage(ByVal sender As Object, ByVal Ev As System.Drawing.Printing.PrintPageEventArgs)
        '还有多少条记录没有打印
        PrintRecordLeave = DataTablePrint.Rows.Count - PrintRecordComplete
        If PrintRecordLeave > 0 Then
            If PrintRecordLeave Mod PrintRecordNumber = 0 Then
                PageNumber = PrintRecordLeave \ PrintRecordNumber
            Else
                PageNumber = PrintRecordLeave \ PrintRecordNumber + 1
            End If
        Else
            PageNumber = 0
        End If
        '正在打印的页数,因为每打印一个新页都要计算还有多少页没有打印所以以打印的页数初始为0
        PrintingPageNumber = 0
        '计算,余下的记录条数是否还可以在一页打印,不满一页时为假
        If DataTablePrint.Rows.Count - PrintingPageNumber * PrintRecordNumber >= PrintRecordNumber Then
            PageRecordNumber = PrintRecordNumber
        Else
            PageRecordNumber = (DataTablePrint.Rows.Count - PrintingPageNumber * PrintRecordNumber) Mod PrintRecordNumber
        End If
        Dim fmt As New StringFormat
        '上下对齐
        fmt.LineAlignment = StringAlignment.Center
        '自动换行
        fmt.FormatFlags = StringFormatFlags.LineLimit
        '打印区域
        Dim Rect As New Rectangle
        '打印表格线格式
        Dim Pen As New Pen(Brushes.Black, 1)
        While PrintingPageNumber <= PageNumber
            '表头中间对齐
            fmt.Alignment = StringAlignment.Center
            '表头和副表头宽度等于设置区域宽度
            Rect.Width = PWidth - PLeft - PRight
            Rect.Height = HeadHeight
            Rect.X = PLeft
            Rect.Y = PTop
            '打印表头
            Ev.Graphics.DrawString(HeadText, HeadFont, Brushes.Black, RectangleF.op_Implicit(Rect), fmt)
            '副表头左对齐
            fmt.Alignment = StringAlignment.Near
            Rect.Width = (PWidth - PLeft - PRight) / 2 - 1
            Rect.Height = SubHeadHeight
            Rect.Y = PTop + HeadHeight
            '打印副表头左
            Ev.Graphics.DrawString(SubHeadLeftText, SubHeadFont, Brushes.Black, RectangleF.op_Implicit(Rect), fmt)
            '右副表头文字从右往左排列
            fmt.FormatFlags = StringFormatFlags.DirectionRightToLeft
            '右副表头右对齐
            fmt.Alignment = StringAlignment.Near
            Rect.X = PLeft + (PWidth - PLeft - PRight) / 2
            '打印副表头右
            Ev.Graphics.DrawString(SubHeadRightText, SubHeadFont, Brushes.Black, RectangleF.op_Implicit(Rect), fmt)
            fmt.Alignment = StringAlignment.Center
            Rect.X = PLeft
            Rect.Y = PTop + HeadHeight + SubHeadHeight + (PrintRecordNumber + 1) * (YUnit) + SubFootHeight
            Rect.Height = FootHeight
            Rect.Width = PWidth - PLeft - PRight
            '打印表脚
            Ev.Graphics.DrawString(FootText, FootFont, Brushes.Black, RectangleF.op_Implicit(Rect), fmt)
            '副表左左对齐
            fmt.Alignment = StringAlignment.Far
            Rect.X = PLeft
            Rect.Y = PTop + HeadHeight + SubHeadHeight + (PrintRecordNumber + 1) * (YUnit)
            Rect.Height = SubFootHeight
            Rect.Width = (PWidth - PLeft - PRight) / 2 - 1
            '打印左表脚
            Ev.Graphics.DrawString(SubFootLeftText, SubFootFont, Brushes.Black, RectangleF.op_Implicit(Rect), fmt)
            '副表头右对齐
            fmt.Alignment = StringAlignment.Near
            Rect.X = PLeft + (PWidth - PLeft - PRight) / 2
            If DataTablePrint.Rows.Count = 0 Then
                SubFootRightText = "第" & TotalPage & "页,共" & TotalPage & "页"
            Else
                SubFootRightText = "第" & TotalPage - PageNumber + 1 & "页,共" & TotalPage & "页"
            End If
            '打印右表脚
            Ev.Graphics.DrawString(SubFootRightText, SubFootFont, Brushes.Black, RectangleF.op_Implicit(Rect), fmt)
            '得到datatable的所有列名
            fmt.Alignment = StringAlignment.Center
            Dim ColumnText(DataTablePrint.Columns.Count) As String
            For Cols = 0 To DataTablePrint.Columns.Count - 1
                '得到当前所有的列名
                ColumnText(Cols) = DataTablePrint.Columns(Cols).ToString
                Rect.X = PLeft + XUnit * Cols
                Rect.Y = PTop + HeadHeight + SubHeadHeight
                Rect.Width = XUnit
                Rect.Height = YUnit
                Ev.Graphics.DrawString(ColumnText(Cols), New Font(TableFont, FontStyle.Bold), DrawBrush, RectangleF.op_Implicit(Rect), fmt)
                Ev.Graphics.DrawRectangle(Pen, Rect)
            Next
            '结束---------------------得到datatable的所有列名
            '当前页面已经打印的记录行数
            Dim PrintingLine As Integer = 0
            While PrintingLine < PageRecordNumber
                '确定要当前要打印的记录的行号
                DataGridRow = DataTablePrint.Rows(PrintRecordComplete)
                For Cols = 0 To DataTablePrint.Columns.Count - 1
                    Rect.X = PLeft + XUnit * Cols
                    Rect.Y = PTop + HeadHeight + SubHeadHeight + (PrintingLine + 1) * (YUnit)
                    Rect.Width = XUnit
                    Rect.Height = YUnit
                    If DataGridRow(ColumnText(Cols)) Is System.DBNull.Value = False Then
                        Ev.Graphics.DrawString(DataGridRow(ColumnText(Cols)), TableFont, DrawBrush, RectangleF.op_Implicit(Rect), fmt)
                    End If
                    Ev.Graphics.DrawRectangle(Pen, Rect)
                Next
                PrintingLine += 1
                PrintRecordComplete += 1
                If PrintRecordComplete >= DataTablePrint.Rows.Count Then
                    Ev.HasMorePages = False
                    PrintRecordComplete = 0
                    Exit Sub
                End If
            End While
            PrintingPageNumber += 1
            If PrintingPageNumber >= PageNumber Then
                Ev.HasMorePages = False
            Else
                Ev.HasMorePages = True
                Exit While
            End If
        End While
    End Sub

    '附转换函数
    'listview转为Datatable函数
    Public Function ListviewToDatatable(ByVal Listview1 As ListView)
        Dim Table1 As New DataTable
        Dim i As Integer
        Dim datacol As DataColumn
        For i = 0 To Listview1.Columns.Count - 1
            datacol = New DataColumn
            datacol.DataType = Type.GetType("System.Object")
            datacol.ColumnName = Listview1.Columns(i).Text.Trim
            Table1.Columns.Add(datacol)
        Next i
        Dim j As Integer
        Dim Datarow1 As DataRow
        For j = 0 To Listview1.Items.Count - 1
            Datarow1 = Table1.NewRow
            For i = 0 To Listview1.Columns.Count - 1
                Datarow1.Item(Listview1.Columns(i).Text.Trim) = Listview1.Items(j).SubItems(i).Text.ToString
            Next i
            Table1.Rows.Add(Datarow1)
        Next j
        Return Table1
    End Function

    '数据集转为Datatable函数
    Public Function DataBaseToDataTable(ByVal SqlDataAdapter1 As Data.SqlClient.SqlDataAdapter, ByVal TableName As String)
        Dim Table1 As New DataTable
        Dim DataSet1 As New DataSet
        DataSet1.Clear()
        SqlDataAdapter1.Fill(DataSet1, TableName)
        Table1 = DataSet1.Tables(TableName)
        Return Table1
    End Function
End Class



Public Class FormPrintDataTable

    Private Sub ButtonClickMe_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ButtonClickMe.Click
        Dim SqlConn As New OleDb.OleDbConnection
        Dim SqlCmd As New OleDb.OleDbCommand
        Dim SqlAda As New OleDb.OleDbDataAdapter
        Dim Dt As New DataTable
        SqlConn.ConnectionString = "provider=microsoft.jet.oledb.4.0;data source=" & Application.StartupPath & "\data\data.mdb"
        SqlCmd.Connection = SqlConn
        SqlCmd.CommandText = "select * from [CARD]"
        SqlConn.Open()
        SqlAda.SelectCommand = SqlCmd
        SqlAda.Fill(Dt)
        SqlCmd.Cancel()
        SqlConn.Close()
        Dim PDt As New PrintDataTable(Dt)
        PDt.SetHeadText = "XXXX煤矿报表测试"
        PDt.SetFootText = "矿长签字:             总工签字:             值班人员签字:"
        PDt.SetSubHeadLeftText = "数据时间:" & Now.Date
        PDt.SetSubHeadRightText = "打印时间:" & Now.Date
        PDt.Print()
    End Sub
End Class
复制代码

版本二 C# 打印DataTable 来自 http://blog.csdn.net/lee576/article/details/3352335

复制代码
public class PrintFunction
    {
        public String printName = String.Empty;
        public Font prtTextFont = new Font("Verdana", 10);
        public Font prtTitleFont = new Font("宋体", 10);
        private String[] titles = new String[0]; 
        public String[] Titles
        {
            set
            {
                titles = value as String[];
                if (null == titles)
                {
                    titles = new String[0];
                }
            }
            get
            {
                return titles;
            }
        }
        private Int32 left = 20;
        private Int32 top = 20;
        public Int32 Top
        {
            set
            {
                top = value;                
            }
            get
            {
                return top;
            }
        }
        public Int32 Left
        {
            set
            {
                left = value;
            }
            get
            {
                return left;
            }
        }
        private DataTable printedTable;
        private Int32 pheight;
        private Int32 pWidth;
        private Int32 pindex;
        private Int32 curdgi;
        private Int32[] width;
        private Int32 rowOfDownDistance = 3;
        private Int32 rowOfUpDistance = 2;
        public Boolean PrintDataTable(DataTable table)
        {
            PrintDocument prtDocument = new PrintDocument();
            try
            {
                if (printName != String.Empty)
                {
                    prtDocument.PrinterSettings.PrinterName = printName;
                }
                prtDocument.DefaultPageSettings.Landscape = true;
                prtDocument.OriginAtMargins = false;
                PrintDialog prtDialog = new PrintDialog();
                prtDialog.AllowSomePages = true;

                prtDialog.ShowHelp = false;
                prtDialog.Document = prtDocument;

                if (DialogResult.OK != prtDialog.ShowDialog())
                {
                    return false;
                }

                printedTable = table;
                pindex = 0;
                curdgi = 0;
                width = new Int32[printedTable.Columns.Count];
                pheight = prtDocument.PrinterSettings.DefaultPageSettings.Bounds.Bottom;
                pWidth = prtDocument.PrinterSettings.DefaultPageSettings.Bounds.Right;

                prtDocument.PrintPage += new PrintPageEventHandler(Document_PrintPage);

                prtDocument.Print();

            }
            catch( InvalidPrinterException ex )
            {
                MessageBox.Show("没有安装打印机");
            }
            catch (Exception ex)
            {
                MessageBox.Show("打印错误");
            }
            prtDocument.Dispose();
            return true;
        }

        Int32 startColumnControls = 0;
        Int32 endColumnControls = 0;
        private void Document_PrintPage(object sender, PrintPageEventArgs ev)
        {
            Int32 x = left;
            Int32 y = top;
            Int32 rowOfTop = top;
            Int32 i;
            Pen pen = new Pen(Brushes.Black, 1);
            if (0 == pindex)
            {
                for (i = 0; i < titles.Length; i++)
                {
                    ev.Graphics.DrawString(titles[i], prtTitleFont, Brushes.Black, left, y + rowOfUpDistance);
                    y += prtTextFont.Height + rowOfDownDistance;
                }
                rowOfTop = y;
                foreach(DataRow dr in printedTable.Rows)
                {
                    for (i = 0; i < printedTable.Columns.Count; i++)
                    {
                        Int32 colwidth = Convert.ToInt16(ev.Graphics.MeasureString(dr[i].ToString().Trim(), prtTextFont).Width);
                        if (colwidth>width[i])
                        {
                            width[i] = colwidth;
                        }
                    }
                }
            }
            for (i = endColumnControls; i < printedTable.Columns.Count; i++)
            {
                String headtext = printedTable.Columns[i].ColumnName.Trim();
                if (pindex == 0)
                {
                    int colwidth = Convert.ToInt16(ev.Graphics.MeasureString(headtext, prtTextFont).Width);
                    if (colwidth > width[i])
                    {
                        width[i] = colwidth;
                    }
                }
                if (x + width[i] > pheight)
                {
                    break;
                }
                ev.Graphics.DrawString(headtext, prtTextFont, Brushes.Black, x, y + rowOfUpDistance);
                x += width[i];
            }
            startColumnControls = endColumnControls;
            if (i < printedTable.Columns.Count)
            {
                endColumnControls = i;
                ev.HasMorePages = true;
            }
            else
            {   
                endColumnControls = printedTable.Columns.Count;
            }
            ev.Graphics.DrawLine(pen, left, rowOfTop, x, rowOfTop);
            y += rowOfUpDistance + prtTextFont.Height + rowOfDownDistance;
            ev.Graphics.DrawLine(pen, left, y, x, y);
            for (i = curdgi; i < printedTable.Rows.Count; i++)
            {
                x = left;
                for (Int32 j = startColumnControls; j < endColumnControls; j++)
                {
                    ev.Graphics.DrawString(printedTable.Rows[i][j].ToString().Trim(), prtTextFont, Brushes.Black, x, y + rowOfUpDistance);
                    x += width[j];
                }
                y += rowOfUpDistance + prtTextFont.Height + rowOfDownDistance;
                ev.Graphics.DrawLine(pen, left, y, x, y);
                if (y > pWidth - prtTextFont.Height - 30)
                {
                    break;
                }
            }
            ev.Graphics.DrawLine(pen, left, rowOfTop, left, y);
            x = left;
            for (Int32 k = startColumnControls; k < endColumnControls; k++)
            {
                x += width[k];
                ev.Graphics.DrawLine(pen, x, rowOfTop, x, y);
            }
            if (y > pWidth - prtTextFont.Height - 30)
            {
                pindex++;
                if (0 != startColumnControls)
                {
                    curdgi = i + 1;
                }
                if (!ev.HasMorePages)
                {
                    endColumnControls = 0;
                }
                ev.HasMorePages = true;
            }
        }
        public void PrintPreviewDataTable(DataTable prtTable)
        {
            PrintDocument prtDocument = new PrintDocument();
            try
            {
                if (printName != String.Empty)
                {
                    prtDocument.PrinterSettings.PrinterName = printName;

                }
                prtDocument.DefaultPageSettings.Landscape = true;
                prtDocument.OriginAtMargins = false;
                printedTable = prtTable;
                pindex = 0;
                curdgi = 0;
                width = new int[printedTable.Columns.Count];
                pheight = prtDocument.PrinterSettings.DefaultPageSettings.Bounds.Bottom;
                pWidth = prtDocument.PrinterSettings.DefaultPageSettings.Bounds.Right;
                prtDocument.PrintPage += new PrintPageEventHandler(Document_PrintPage);
                PrintPreviewDialog prtPreviewDialog = new PrintPreviewDialog();
                prtPreviewDialog.Document = prtDocument;
                prtPreviewDialog.ShowIcon = false;
                prtPreviewDialog.WindowState = FormWindowState.Maximized;
                prtPreviewDialog.ShowDialog();

            }
            catch (InvalidPrinterException ex)
            {
                MessageBox.Show("没有安装打印机");
            }
            catch (Exception ex)
            {
                MessageBox.Show("打印预览失败");
            }

        }
    }
复制代码

 

综合资料来源

http://msdn.microsoft.com/zh-cn/library/system.drawing.printing.printdocument(v=VS.80).aspx

http://blog.csdn.net/lee576/article/details/3352335

http://s.yanghao.org/program/viewdetail.php?i=273227 打印预览正常,打印缺少大部分

http://bbs.csdn.net/topics/340132124

InstallShield Limited Edition 打包软件安装包教程

从Visual Studio 2012开始,微软就把自家原来的安装与部署工具彻底废掉了,转而让大家去安装使用第三方的打包工具“InstallShield Limited Edition for Visual Studio”,注意这个版本是免费的,只需要邮件注册下,就会有要注册码。虽然不知道微软这是何用意,但是既然他叫我们用,那就去用吧。(其实,早在VS2010中,微软就把InstallShield Limited Edition for Visual Studio放在里面了,只是那个时候自家产品还在)。

如何下载注册: 

官网:https://www.flexerasoftware.com/producer/products/software-installation/installshield-software-installer/

用你的邮箱注册,会免费发注册码的,不需要破解

具体步骤:

InstallShield Limited Edition for Visual Studio 2013 图文教程(教你如何打包.NET程序)

1、新建了一个基本的三层项目,用来测试打包

InstallShield Limited Edition for Visual Studio 2013 图文教程(教你如何打包.NET程序)

2、根据实际情况,填写程序基本信息

InstallShield Limited Edition for Visual Studio 2013 图文教程(教你如何打包.NET程序)

3、这一步非常重要,看上图 :

1)设置为简体中文,否则安装路径有中文的话就会出问题
2)设置默认安装路径
3)修改默认字体
4)每次升级,重新打包,只需要点击这一行右侧的“…”按钮,就会重新生成Code,安装时就会自动覆盖老版本

InstallShield Limited Edition for Visual Studio 2013 图文教程(教你如何打包.NET程序)

4、选择我要的.NET Framework 4.0

InstallShield Limited Edition for Visual Studio 2013 图文教程(教你如何打包.NET程序)

5、这里什么也不做

InstallShield Limited Edition for Visual Studio 2013 图文教程(教你如何打包.NET程序)

6、添加要打包的文件、程序(建议选择Add Project Outputs,然后选程序主输出即可,这样会默认把安装需要的文件都包括进去)

InstallShield Limited Edition for Visual Studio 2013 图文教程(教你如何打包.NET程序)

7、右键——属性(Properties)

InstallShield Limited Edition for Visual Studio 2013 图文教程(教你如何打包.NET程序)

8、非常重要:

1)如果你的是.NET项目程序DLL、EXE,那就按照默认的设置,不要去改,否则出错
2)如果你的是OCX或者ActiveX等需要注册的DLL,那么选择“Self-registration”

InstallShield Limited Edition for Visual Studio 2013 图文教程(教你如何打包.NET程序)

9、设置快捷方式

InstallShield Limited Edition for Visual Studio 2013 图文教程(教你如何打包.NET程序)

10、注册表配置:

1)打开这个
2)现在这个测试项目,不需要写注册表信息,所以我什么都不改。如果你的项目要写注册表,那就自己填。

InstallShield Limited Edition for Visual Studio 2013 图文教程(教你如何打包.NET程序)

11、安装访谈(安装界面对话框配置):

1)打开这个
2)我就不修改了,大家根据自己的需求调整。
3)如果要修改,点击这个

InstallShield Limited Edition for Visual Studio 2013 图文教程(教你如何打包.NET程序)

12、修改安装步骤、对话框、背景等

InstallShield Limited Edition for Visual Studio 2013 图文教程(教你如何打包.NET程序)

13、到这这一步,就剩下最重要的问题:如何把.NET Framework一起打包进程序去:

1)解决方案,点击“Specify Application Data”–双击“Redistributables”–勾选“Microsoft .NET Framework 4.0 Full”–勾选之后,它会自动联网下载,下载速度是比较慢的,下完之后,右侧就会变成“Installed Locally”

A).NET 4.0:如果大家嫌慢,那就去网上下载:dotNetFx40_Full_x86_x64.exe,然后放到这个路径:
C:\Program Files (x86)\InstallShield\2013LE\SetupPrerequisites\Microsoft .net\4.0\Full
这样的话就能节省很多时间了,不过除了这个,它还需要下载其他东西的,只是你可以节省这部分时间

B).NET 3.5 SP1:下载dotnetfx35.exe,然后放到这个路径:
C:\Program Files (x86)\InstallShield\2013LE\SetupPrerequisites\Microsoft .net\3.5 SP1\Full

InstallShield Limited Edition for Visual Studio 2013 图文教程(教你如何打包.NET程序)

14、最后的设置:

1)解决方案,点击“Prepare for Release”–双击“Releases”–点击选中“SingleImage”–选项卡点击“Setup.exe”–找到“InstallShield Prerequisites Location”,把它设置为“Extract From Setup.exe”

InstallShield Limited Edition for Visual Studio 2013 图文教程(教你如何打包.NET程序)

15、打包完成了,运行效果如上图:
1)打包后的程序放在这个地方:XXX\Express\SingleImage\DiskImages\DISK1\setup.exe (70.9MB,比较大)

16、最后说一下安装,如果有配置文件的话,每次安装会覆盖,因此建议安装前先备份配置文件。

引自:http://www.wuleba.com/?p=23892

Visual Studio 2010 简体中文版上Windows项目包含本地系统必备组件发布失败处理方法

在尝试Visual Studio 2010 简体中文版时,我发现创建一个并发布Windows类型的项目时(如一个 C# windows 应用程序),如果在发布应用程序时选择使用“Microsoft .NET Framework 4 Client Profile (x86 and x64)”作为“系统必备”组件,同时选中“从与我的应用程序相同的位置下载系统必备组件”,会遇到如下错误:

“MSB3152: 系统必备组件的安装位置未设置为“组件供应商的网站”,并且无法在磁盘上找到项“Microsoft .NET Framework 4 Client Profile (x86 and x64)”中的文件“DotNetFX40Client\dotNetFx40LP_Client_x86_x64cs.exe”。有关详细信息,请参见帮助。”

 

然后我看了看VS2010的自述文件(http://download.microsoft.com/download/5/D/7/5D76B235-1073-477E-82F0-AEA4A20E4F71/VS2010RTM.htm#General),得到了如下的解决方案:

 

Visual Studio 2010 自述文件 Visual Studio 2010 自述文件

 

2.4.1.38 可再发行语言包的位置不正确导致 ClickOnce 发布失败。

如果在“系统必备”对话框中选中“从与我的应用程序相同的位置下载系统必备组件”选项,并选择以下任何组件作为系统必备组件,则当使用简体中文或繁体中文版本的 Visual Studio 2010 发布应用程序时,可能会显示生成错误:

 

  1. Microsoft .NET Framework 4(x86 和 x64)
  2. Microsoft .NET Framework 4 Client Profile(x86 和 x64)
  3. Microsoft Visual F# Runtime for .NET 2.0
  4. Microsoft Visual F# Runtime for .NET 4.0

 

对于“Microsoft .NET Framework 4 Client Profile(x86 和 x64)”,系统可能会显示下面的生成错误:

“MSB3152: 系统必备的安装位置未设置为‘组件供应商的网站’,无法在磁盘上找到项‘Microsoft .NET Framework 4 Client Profile (x86 和 x64)’中的文件‘DotNetFX40Client\dotNetFx40LP_Client_x86_x64cs.exe’。 有关详细信息,请参见‘帮助’。”

解决此问题的方法:

若要在简体中文版本中解决此问题,请按以下步骤操作:

  1. 导航到文件夹“%ProgramFiles%\Microsoft SDKs\Windows\v7.0A\Bootstrapper\Packages\DotNetFX40Client”。 对于 x64 操作系统,该路径位于 %ProgramFiles(x86)% 下。
  2. 将 zh-Hans 文件夹复制到名为 zh-chs 的新文件夹
  3. 导航到 zh-chs 文件夹。
  4. 在管理员模式下打开 Package.xml。
  5. 按如下方法将 >Culture< 的值更改为 zh-chs:<String Name=”Culture”>zh-chs</String>

若要在繁体中文版本中解决此问题,请按以下步骤操作:

  1. 导航到文件夹“%ProgramFiles%\Microsoft SDKs\Windows\v7.0A\Bootstrapper\Packages\DotNetFX40Client”。 对于 x64 操作系统,该路径位于 %ProgramFiles(x86)% 下。
  2. 将 zh-Hant 文件夹复制到名为 zh-cht 的新文件夹
  3. 导航到 zh-cht 文件夹。
  4. 在管理员模式下打开 Package.xml。
  5. 按如下方法将 >Culture< 的值更改为 zh-cht:<String Name=”Culture”>zh-cht</String>

2.4.1.39 ClickOnce 应用程序安装的可再发行语言包不正确。

如果在“系统必备”对话框中选中“从组件供应商的网站上下载系统必备组件”选项,并选择以下任何组件作为系统必备组件,则当使用简体中文或繁体中文版本的 Visual Studio 2010 发布应用程序时,可能会无法安装简体中文或繁体中文语言包:

 

  1. Microsoft .NET Framework 4(x86 和 x64)
  2. Microsoft .NET Framework 4 Client Profile(x86 和 x64)
  3. Microsoft Visual F# Runtime for .NET 2.0
  4. Microsoft Visual F# Runtime for .NET 4.0

 

解决此问题的方法:

若要在简体中文版本中解决此问题,请按以下步骤操作:

  1. 导航到文件夹“%ProgramFiles%\Microsoft SDKs\Windows\v7.0A\Bootstrapper\Packages\DotNetFX40Client”。 对于 x64 操作系统,该路径位于 %ProgramFiles(x86)% 下。
  2. 将 zh-Hans 文件夹复制到名为 zh-chs 的新文件夹
  3. 导航到 zh-chs 文件夹。
  4. 在管理员模式下打开 Package.xml。
  5. 按如下方法将 >Culture< 的值更改为 zh-chs:<String Name=”Culture”>zh-chs</String>

若要在繁体中文版本中解决此问题,请按以下步骤操作:

  1. 导航到文件夹“%ProgramFiles%\Microsoft SDKs\Windows\v7.0A\Bootstrapper\Packages\DotNetFX40Client”。 对于 x64 操作系统,该路径位于 %ProgramFiles(x86)% 下。
  2. 将 zh-Hant 文件夹复制到名为 zh-cht 的新文件夹
  3. 导航到 zh-cht 文件夹。
  4. 在管理员模式下打开 Package.xml。
  5. 按如下方法将 >Culture< 的值更改为 zh-cht:<String Name=”Culture”>zh-cht</String>

 

 

 

看来问题在于NetFramework4 更改了简体中文与繁体中文的language code (zh-Hans/zh-Hant),但vs2010在这里并没有正确的识别。以至于在发布clickonce时出现了错误。

不过我个人觉得文中给出的解决方案太过于繁琐了。其实有更简单的解决方案。

打开 “C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Bootstrapper\Packages\DotNetFX40Client” ,可以看到里面有很多不同语言的文件夹,其中zh-Hans就是简体中文文件所在的位置。直接把它前面的所有文件夹删掉,或者剪切走,然后重启VS应用程序,就可以了。

 

当然,如果需要以别的语言发布,那么还得按照文档的要求来做。我这里只是找了个省事的方法。

============================

C:\Program Files\Microsoft SDKs\Windows\v7.0A\Bootstrapper\Packages目录下的“一级文件夹”对应的exe文件为组件安装程序,“一级文件夹”下的zh-Hans目录下为简体汉化包程序.

例:发布所需要的组件位置:(直接把下面对应的组件复制出来打包安装即可)

 

DotNetFX40:(安装组件)

C:\Program Files\Microsoft SDKs\Windows\v7.0A\Bootstrapper\Packages\DotNetFX40\dotNetFx40_Full_x86_x64.exe

DotNetFX40:(安装组件简体语言包)

C:\Program Files\Microsoft SDKs\Windows\v7.0A\Bootstrapper\Packages\DotNetFX40\zh-Hans\dotNetFx40LP_Full_x86_x64zh-Hans.exe

DotNetFX40Client:(安装组件)

C:\Program Files\Microsoft SDKs\Windows\v7.0A\Bootstrapper\Packages\DotNetFX40Client\dotNetFx40_Client_x86_x64.exe

DotNetFX40Client:(安装组件简体语言包)

C:\Program Files\Microsoft SDKs\Windows\v7.0A\Bootstrapper\Packages\DotNetFX40Client\zh-Hans\dotNetFx40LP_Client_x86_x64zh-Hans.exe

WindowsInstaller4_5:(xp)

C:\Program Files\Microsoft SDKs\Windows\v7.0A\Bootstrapper\Packages\WindowsInstaller4_5\WindowsXP-KB942288-v3-x86.exe

WindowsInstaller3_1:

C:\Program Files\Microsoft SDKs\Windows\v7.0A\Bootstrapper\Packages\WindowsInstaller3_1\WindowsInstaller-KB893803-v2-x86.exe

实际情况可能是:

dotNetFx40LP_Full_x86_x64cs.exe 可以加在DotNetFX40/cs文件夹中。

VS 之 InstallShield Limited Edition for Visual Studio 2015 图文教程

从Visual Studio 2012开始,微软就把自家原来的安装与部署工具彻底废掉了,转而让大家去安装使用第三方的打包工具“InstallShield Limited Edition for Visual Studio”,注意这个版本是免费的,只需要邮件注册下,就会有要注册码。(其实,早在VS2010中,微软就把InstallShield Limited Edition for Visual Studio放在里面了,只是那个时候自家产品还在)。

如何下载注册: http://go.microsoft.com/fwlink/?LinkID=239620&lang=1033&ver=ult 。用你的邮箱注册,会免费发注册码的,不需要破解。

具体打包步骤如下:右键解决方案→添加→新建项目→其他项目类型→安装和部署→InstallShield Limited Edition Project,如下图:

1、第一步,点击“Application Information”选项,如下图,如实填写右侧内容;

2、第二步,点击“General Information”,根据实际情况,填写程序基本信息,如下图:

  1)设置为简体中文,否则安装路径有中文的话就会出问题;
  2)设置默认安装路径;
  3)修改默认字体;
  4)每次升级,重新打包,只需要点击这一行右侧的“…”按钮,就会重新生成Code,安装时就会自动覆盖旧版本。

3、点击“Installation Requirements”,根据实际情况选择安装要求,我的程序为.NET Framework 4.5开发。

4、点击“Application Files”,添加要打包的文件、程序,启动项目目录下“ bin\Release\ ”目录中的全部文件及文件夹。

5、选中OCX或者ActiveX等需要注册的DLL,右键→Properties;

  1)如果你的是.NET项目程序DLL、EXE,那就按照默认的设置,不要去改,否则出错
  2)如果你的是OCX或者ActiveX等需要注册的DLL,那么选择“Self-registration”

6、点击“ Application Shortcuts ”设置快捷方式,根据需要勾选选择;

7、点击“ Application Registry ”进行注册表配置,本项目无注册表配置;

8、点击“ Installation Interview ”进行安装问卷配置,下图中圈起的为常用设置;

9、“ Installation Interview ”面板左侧的“ Dialogs ”,选择安装步骤、对话框、背景等设置;

10、若要把.NET Framework一起打包进程序去,如下图:

11、解决方案,点击“Prepare for Release”–双击“Releases”–点击选中“SingleImage”–选项卡点击“Setup.exe”–找到“InstallShield Prerequisites Location”,把它设置为“Extract From Setup.exe”,打包完成;

12、打包完成后,解决方案→安装程序(FCSetup),右键生成,生成成功运行如下图:

  1)打包后的程序存在如下目录下:xxx\Express\SingleImage\DiskImages\DISK1\setup.exe

Install Shield Limited Edition 下载、注册、安装 VS打包安装包

因为现在VS部署安装包的功能被取消了,我们需要自己下载Install Shield Limited Edition 来实现这个功能,下面分享一下过程,给大家作参考。
首先下载地址:http://learn.flexerasoftware.com/content/IS-EVAL-InstallShield-Limited-Edition-Visual-Studio?lang=1033&ver=ult
能够连上外网的童鞋可以自己下载,很简单,把相关信息填写一下即可,如图:这里写图片描述
注:上面图片中几个地方有”Please Select“ 的地方是需要下拉选择的,如果你的这里不能下拉,肯定是网络问题,如果翻墙可以试一下下面地址中的代理,代理地址


如果实在不行,下面给你提供一个我已经下载过的,可以到这里下载,Install Shield Limited Edition 下载地址
如果没有密钥应该是5天试用期,你可以使用我的密钥:370FFQW-D18-E0709A1C9N

c#如何自动补齐字符串的长度

C#:

string fullstr= hh.ToString(“00000000”);  或             string fullstr= hh.ToString().PadLeft(长度, ‘0’);        sqlserver:       left(replicate(‘0’,8-Len(参数))+ lTrim(参数),8)        注:’0’为补齐时使用的字符,8为补齐之后的长度,‘8-Len(参数)’为打印字符的次数)     在数据库中,用户的户号为“00005555”,用户在输入查询条件的时候,不输入前面的‘0’即可查询,即输入“5555”,输出的结果为“00005555”

 

 

C#对于长度不定字符串补全零的问题

定义一个int变量:
int aa = 111;

转换成7位字符串,不够的补零,方法:
string aastr = aa.ToString(“0000000”);

string aastr = aa.ToString().PadLeft(7,’0′);

C# 汉字转拼音(全拼)

很多时候我们需要把汉字转换成拼音,比如姓名、城市名等。网上搜索了一把,把汉字转成拼音的代码很多,但大多都只是把汉字转成了拼音的首字母,比如把“深圳”转成了“sz”。那么如何把汉字转成全拼(“深圳”转成了“ShenZhen”。)呢?有的说汉字那么多根本做不到,有的说是要用字库。其实仔细分析一下,虽然汉字很多,但汉字的读音(忽略声调,因为我们不需要)却很有限。利用这个特点,我们把汉字转成拼音就不是一件很困难的事了!

代码:

    private void button1_Click(object sender, EventArgs e)
{
this.textBox2.Text = Hz2Py.Convert(this.textBox1.Text);
}

汉字转拼音类:

    /// <summary>
/// 汉字转拼音类
/// </summary>
public class Hz2Py
{
private static int[] pyValue = new int[]
{
-20319,-20317,-20304,-20295,-20292,-20283,-20265,-20257,-20242,-20230,-20051,-20036,
-20032,-20026,-20002,-19990,-19986,-19982,-19976,-19805,-19784,-19775,-19774,-19763,
-19756,-19751,-19746,-19741,-19739,-19728,-19725,-19715,-19540,-19531,-19525,-19515,
-19500,-19484,-19479,-19467,-19289,-19288,-19281,-19275,-19270,-19263,-19261,-19249,
-19243,-19242,-19238,-19235,-19227,-19224,-19218,-19212,-19038,-19023,-19018,-19006,
-19003,-18996,-18977,-18961,-18952,-18783,-18774,-18773,-18763,-18756,-18741,-18735,
-18731,-18722,-18710,-18697,-18696,-18526,-18518,-18501,-18490,-18478,-18463,-18448,
-18447,-18446,-18239,-18237,-18231,-18220,-18211,-18201,-18184,-18183, -18181,-18012,
-17997,-17988,-17970,-17964,-17961,-17950,-17947,-17931,-17928,-17922,-17759,-17752,
-17733,-17730,-17721,-17703,-17701,-17697,-17692,-17683,-17676,-17496,-17487,-17482,
-17468,-17454,-17433,-17427,-17417,-17202,-17185,-16983,-16970,-16942,-16915,-16733,
-16708,-16706,-16689,-16664,-16657,-16647,-16474,-16470,-16465,-16459,-16452,-16448,
-16433,-16429,-16427,-16423,-16419,-16412,-16407,-16403,-16401,-16393,-16220,-16216,
-16212,-16205,-16202,-16187,-16180,-16171,-16169,-16158,-16155,-15959,-15958,-15944,
-15933,-15920,-15915,-15903,-15889,-15878,-15707,-15701,-15681,-15667,-15661,-15659,
-15652,-15640,-15631,-15625,-15454,-15448,-15436,-15435,-15419,-15416,-15408,-15394,
-15385,-15377,-15375,-15369,-15363,-15362,-15183,-15180,-15165,-15158,-15153,-15150,
-15149,-15144,-15143,-15141,-15140,-15139,-15128,-15121,-15119,-15117,-15110,-15109,
-14941,-14937,-14933,-14930,-14929,-14928,-14926,-14922,-14921,-14914,-14908,-14902,
-14894,-14889,-14882,-14873,-14871,-14857,-14678,-14674,-14670,-14668,-14663,-14654,
-14645,-14630,-14594,-14429,-14407,-14399,-14384,-14379,-14368,-14355,-14353,-14345,
-14170,-14159,-14151,-14149,-14145,-14140,-14137,-14135,-14125,-14123,-14122,-14112,
-14109,-14099,-14097,-14094,-14092,-14090,-14087,-14083,-13917,-13914,-13910,-13907,
-13906,-13905,-13896,-13894,-13878,-13870,-13859,-13847,-13831,-13658,-13611,-13601,
-13406,-13404,-13400,-13398,-13395,-13391,-13387,-13383,-13367,-13359,-13356,-13343,
-13340,-13329,-13326,-13318,-13147,-13138,-13120,-13107,-13096,-13095,-13091,-13076,
-13068,-13063,-13060,-12888,-12875,-12871,-12860,-12858,-12852,-12849,-12838,-12831,
-12829,-12812,-12802,-12607,-12597,-12594,-12585,-12556,-12359,-12346,-12320,-12300,
-12120,-12099,-12089,-12074,-12067,-12058,-12039,-11867,-11861,-11847,-11831,-11798,
-11781,-11604,-11589,-11536,-11358,-11340,-11339,-11324,-11303,-11097,-11077,-11067,
-11055,-11052,-11045,-11041,-11038,-11024,-11020,-11019,-11018,-11014,-10838,-10832,
-10815,-10800,-10790,-10780,-10764,-10587,-10544,-10533,-10519,-10331,-10329,-10328,
-10322,-10315,-10309,-10307,-10296,-10281,-10274,-10270,-10262,-10260,-10256,-10254
};private static string[] pyName = new string[]
{
“A”,”Ai”,”An”,”Ang”,”Ao”,”Ba”,”Bai”,”Ban”,”Bang”,”Bao”,”Bei”,”Ben”,
“Beng”,”Bi”,”Bian”,”Biao”,”Bie”,”Bin”,”Bing”,”Bo”,”Bu”,”Ba”,”Cai”,”Can”,
“Cang”,”Cao”,”Ce”,”Ceng”,”Cha”,”Chai”,”Chan”,”Chang”,”Chao”,”Che”,”Chen”,”Cheng”,
“Chi”,”Chong”,”Chou”,”Chu”,”Chuai”,”Chuan”,”Chuang”,”Chui”,”Chun”,”Chuo”,”Ci”,”Cong”,
“Cou”,”Cu”,”Cuan”,”Cui”,”Cun”,”Cuo”,”Da”,”Dai”,”Dan”,”Dang”,”Dao”,”De”,
“Deng”,”Di”,”Dian”,”Diao”,”Die”,”Ding”,”Diu”,”Dong”,”Dou”,”Du”,”Duan”,”Dui”,
“Dun”,”Duo”,”E”,”En”,”Er”,”Fa”,”Fan”,”Fang”,”Fei”,”Fen”,”Feng”,”Fo”,
“Fou”,”Fu”,”Ga”,”Gai”,”Gan”,”Gang”,”Gao”,”Ge”,”Gei”,”Gen”,”Geng”,”Gong”,
“Gou”,”Gu”,”Gua”,”Guai”,”Guan”,”Guang”,”Gui”,”Gun”,”Guo”,”Ha”,”Hai”,”Han”,
“Hang”,”Hao”,”He”,”Hei”,”Hen”,”Heng”,”Hong”,”Hou”,”Hu”,”Hua”,”Huai”,”Huan”,
“Huang”,”Hui”,”Hun”,”Huo”,”Ji”,”Jia”,”Jian”,”Jiang”,”Jiao”,”Jie”,”Jin”,”Jing”,
“Jiong”,”Jiu”,”Ju”,”Juan”,”Jue”,”Jun”,”Ka”,”Kai”,”Kan”,”Kang”,”Kao”,”Ke”,
“Ken”,”Keng”,”Kong”,”Kou”,”Ku”,”Kua”,”Kuai”,”Kuan”,”Kuang”,”Kui”,”Kun”,”Kuo”,
“La”,”Lai”,”Lan”,”Lang”,”Lao”,”Le”,”Lei”,”Leng”,”Li”,”Lia”,”Lian”,”Liang”,
“Liao”,”Lie”,”Lin”,”Ling”,”Liu”,”Long”,”Lou”,”Lu”,”Lv”,”Luan”,”Lue”,”Lun”,
“Luo”,”Ma”,”Mai”,”Man”,”Mang”,”Mao”,”Me”,”Mei”,”Men”,”Meng”,”Mi”,”Mian”,
“Miao”,”Mie”,”Min”,”Ming”,”Miu”,”Mo”,”Mou”,”Mu”,”Na”,”Nai”,”Nan”,”Nang”,
“Nao”,”Ne”,”Nei”,”Nen”,”Neng”,”Ni”,”Nian”,”Niang”,”Niao”,”Nie”,”Nin”,”Ning”,
“Niu”,”Nong”,”Nu”,”Nv”,”Nuan”,”Nue”,”Nuo”,”O”,”Ou”,”Pa”,”Pai”,”Pan”,
“Pang”,”Pao”,”Pei”,”Pen”,”Peng”,”Pi”,”Pian”,”Piao”,”Pie”,”Pin”,”Ping”,”Po”,
“Pu”,”Qi”,”Qia”,”Qian”,”Qiang”,”Qiao”,”Qie”,”Qin”,”Qing”,”Qiong”,”Qiu”,”Qu”,
“Quan”,”Que”,”Qun”,”Ran”,”Rang”,”Rao”,”Re”,”Ren”,”Reng”,”Ri”,”Rong”,”Rou”,
“Ru”,”Ruan”,”Rui”,”Run”,”Ruo”,”Sa”,”Sai”,”San”,”Sang”,”Sao”,”Se”,”Sen”,
“Seng”,”Sha”,”Shai”,”Shan”,”Shang”,”Shao”,”She”,”Shen”,”Sheng”,”Shi”,”Shou”,”Shu”,
“Shua”,”Shuai”,”Shuan”,”Shuang”,”Shui”,”Shun”,”Shuo”,”Si”,”Song”,”Sou”,”Su”,”Suan”,
“Sui”,”Sun”,”Suo”,”Ta”,”Tai”,”Tan”,”Tang”,”Tao”,”Te”,”Teng”,”Ti”,”Tian”,
“Tiao”,”Tie”,”Ting”,”Tong”,”Tou”,”Tu”,”Tuan”,”Tui”,”Tun”,”Tuo”,”Wa”,”Wai”,
“Wan”,”Wang”,”Wei”,”Wen”,”Weng”,”Wo”,”Wu”,”Xi”,”Xia”,”Xian”,”Xiang”,”Xiao”,
“Xie”,”Xin”,”Xing”,”Xiong”,”Xiu”,”Xu”,”Xuan”,”Xue”,”Xun”,”Ya”,”Yan”,”Yang”,
“Yao”,”Ye”,”Yi”,”Yin”,”Ying”,”Yo”,”Yong”,”You”,”Yu”,”Yuan”,”Yue”,”Yun”,
“Za”, “Zai”,”Zan”,”Zang”,”Zao”,”Ze”,”Zei”,”Zen”,”Zeng”,”Zha”,”Zhai”,”Zhan”,
“Zhang”,”Zhao”,”Zhe”,”Zhen”,”Zheng”,”Zhi”,”Zhong”,”Zhou”,”Zhu”,”Zhua”,”Zhuai”,”Zhuan”,
“Zhuang”,”Zhui”,”Zhun”,”Zhuo”,”Zi”,”Zong”,”Zou”,”Zu”,”Zuan”,”Zui”,”Zun”,”Zuo”
};

/// <summary>
/// 把汉字转换成拼音(全拼)
/// </summary>
/// <param name=”hzString”>汉字字符串</param>
/// <returns>转换后的拼音(全拼)字符串</returns>
public static string Convert(string hzString)
{
// 匹配中文字符
Regex regex = new Regex(“^[\u4e00-\u9fa5]$”);
byte[] array = new byte[2];
string pyString = “”;
int chrAsc = 0;
int i1 = 0;
int i2 = 0;
char[] noWChar = hzString.ToCharArray();

for (int j = 0; j < noWChar.Length; j++)
{
// 中文字符
if (regex.IsMatch(noWChar[j].ToString()))
{
array = System.Text.Encoding.Default.GetBytes(noWChar[j].ToString());
i1 = (short)(array[0]);
i2 = (short)(array[1]);
chrAsc = i1 * 256 + i2 – 65536;
if (chrAsc > 0 && chrAsc < 160)
{
pyString += noWChar[j];
}
else
{
// 修正部分文字
if (chrAsc == -9254)  // 修正“圳”字
pyString += “Zhen”;
else
{
for (int i = (pyValue.Length – 1); i >= 0; i–)
{
if (pyValue[i] <= chrAsc)
{
pyString += pyName[i];
break;
}
}
}
}
}
// 非中文字符
else
{
pyString += noWChar[j].ToString();
}
}
return pyString;
}
}

2007年12月9日补充:一些生僻字转不了,比如:“睿”和“罡”字等。

 

汉字转拼音貌似一直是C#开发的一个难题,无论什么方案都有一定的bug,之前使用了两种方案。

1.Chinese2Spell.cs 一些不能识别的汉字全部转为Z

2.Microsoft Visual Studio International Feature Pack 1.0  连”广”、“区”都不能转,很让人失望。

 

这些都是2010年以前的方案,至少还有大侠在为汉字转拼音不断努力着,目前发现最完美的就是NPINYIN,在googlecode可以看到它的开源项目,http://code.google.com/p/npinyin/

不能识别的字很少,而且还在不断维护更新,日趋完美,推荐大家使用。

下载地址

dll:http://files.cnblogs.com/files/guohu/NPinyin-0.2.4588.20158-bin.zip

源码:http://files.cnblogs.com/files/guohu/NPinyin-0.2.x-source_code.zip

 

v0.2.x的变化

  • 1、增加对不同编码格式文本的支持,同时增加编码转换方法Pinyin.ConvertEncoding
  • 2、重构单字符拼音的获取,未找到拼音时返回字符本身.

汪思言 2012年7月23日晚

将中文转换成拼音全文和首字母的.net 组件。示例:

using System;
using System.Collections.Generic;
using System.Text;
using NPinyin;

namespace NPinyinTest
{
  class Program
  {
    static void Main(string[] args)
    {
      string[] maxims = new string[]{
        "事常与人违,事总在人为",
        "骏马是跑出来的,强兵是打出来的",
        "驾驭命运的舵是奋斗。不抱有一丝幻想,不放弃一点机会,不停止一日努力。 ",
        "如果惧怕前面跌宕的山岩,生命就永远只能是死水一潭", 
        "懦弱的人只会裹足不前,莽撞的人只能引为烧身,只有真正勇敢的人才能所向披靡"
      };

      string[] medicines = new string[] {
        "聚维酮碘溶液",
        "开塞露",
        "炉甘石洗剂",
        "苯扎氯铵贴",
        "鱼石脂软膏",
        "莫匹罗星软膏",
        "红霉素软膏",
        "氢化可的松软膏",
        "曲安奈德软膏",
        "丁苯羟酸乳膏",
        "双氯芬酸二乙胺乳膏",
        "冻疮膏",
        "克霉唑软膏",
        "特比奈芬软膏",
        "酞丁安软膏",
        "咪康唑软膏、栓剂",
        "甲硝唑栓",
        "复方莪术油栓"
      };

      Console.WriteLine("UTF8句子拼音:");
      foreach (string s in maxims)
      {
        Console.WriteLine("汉字:{0}\n拼音:{1}\n", s, Pinyin.GetPinyin(s));
      }

      Encoding gb2312 = Encoding.GetEncoding("GB2312");
      Console.WriteLine("GB2312拼音简码:");
      foreach (string m in medicines)
      {
        string s = Pinyin.ConvertEncoding(m, Encoding.UTF8, gb2312);
        Console.WriteLine("药品:{0}\n简码:{1}\n", s, Pinyin.GetInitials(s, gb2312));
      }

      Console.ReadKey();
    }
  }
}

运行结果

UTF8句子拼音: 汉字:事常与人违,事总在人为 拼音:shi chang yu ren wei , shi zong zai ren wei

汉字:骏马是跑出来的,强兵是打出来的 拼音:jun ma shi pao chu lai de , qiang bing shi da chu lai de

汉字:驾驭命运的舵是奋斗。不抱有一丝幻想,不放弃一点机会,不停止一日努力。 拼音:jia yu ming yun de duo shi fen dou 。 bu bao you yi si huan xiang , bu fa ng qi yi dian ji hui , bu ting zhi yi ri nu li 。

汉字:如果惧怕前面跌宕的山岩,生命就永远只能是死水一潭 拼音:ru guo ju pa qian mian die dang de shan yan , sheng ming jiu yong yuan zh i neng shi si shui yi tan

汉字:懦弱的人只会裹足不前,莽撞的人只能引为烧身,只有真正勇敢的人才能所向披靡 拼音:nuo ruo de ren zhi hui guo zu bu qian , mang zhuang de ren zhi neng yin w ei shao shen , zhi you zhen zheng yong gan de ren cai neng suo xiang pi mi

GB2312拼音简码: 药品:聚维酮碘溶液 简码:JWTDRY

药品:开塞露 简码:KSL

药品:炉甘石洗剂 简码:LGSXJ

药品:苯扎氯铵贴 简码:BZLAT

药品:鱼石脂软膏 简码:YSZRG

药品:莫匹罗星软膏 简码:MPLXRG

药品:红霉素软膏 简码:HMSRG

药品:氢化可的松软膏 简码:QHKDSRG

药品:曲安奈德软膏 简码:QANDRG

药品:丁苯羟酸乳膏 简码:DBQSRG

药品:双氯芬酸二乙胺乳膏 简码:SLFSEYARG

药品:冻疮膏 简码:DCG

药品:克霉唑软膏 简码:KMZRG

药品:特比奈芬软膏 简码:TBNFRG

药品:酞丁安软膏 简码:TDARG

药品:咪康唑软膏、栓剂 简码:MKZRG、SJ

药品:甲硝唑栓 简码:JXZS

药品:复方莪术油栓 简码:FFESYS

 

C#中串口通信编程 怎么改才能收发汉字

通常情况下,我们的程序代码是如下这样:
SerialPort serialPort = new SerialPort(); //这是串口通信对象
serialPort.WriteLine("Hello World"); //这是发送数据
string message = serialPort.ReadLine(); //这是接收数据
这没什么问题,收发都正常。但当你把英文"Hello World"换成中文"你好世界" 发送出去后,在另一边接收到的却是乱码。那么,怎样正确收发中文呢?正确的做法是,先将中文字符编码后再发送。接收时,将数据解码还原成中文。具体看下面:

//在发送数据时,使用这样的方法代码。
                    System.Text.UTF8Encoding utf8 = new System.Text.UTF8Encoding();
                    Byte[] writeBytes = utf8.GetBytes("你好世界");
                    serialPort.Write(writeBytes, 0, writeBytes.Length);

//在接收数据时,使用这样的方法代码。
                    System.Text.UTF8Encoding utf8 = new System.Text.UTF8Encoding();
                    Byte[] readBytes = new Byte[serialPort.BytesToRead];
                    serialPort.Read(readBytes, 0, readBytes.Length);
                    String decodedString = utf8.GetString(readBytes);
                    Console.WriteLine(decodedString); //控制台输入了中文:你好世界

这样就可以支持收发中文、英文、以其世界上常见的语种文字,毕竟UTF8编码支持大多数语言文字。如果,我们不使用UTF8编码,而使用GB2312编码,可以不可以呢?当然可以了。问题是死的,而头脑是灵活。呵呵~