前言
当.NET时代到来时,我们在高兴激动的时候,把自己的系统升级到.NET或是用它开发新的系统。
打印--管理信息系统永恒的话题。于是,我们在开发业务系统的时候不得不再专门做一个打印组件或是购买一个中间件完成单据合同、清单、文档、工资单、报表等等的打印。
在开始之前,如果您还想了解更多关于打印的信息,可以查看我曾经写的一篇技术文档:
NET环境下有关打印页面设置、打印机设置、打印预览对话框的实现及应用,二次封装了.NET类库的打印页面设置、打印机设置、打印预览对话框。对于想了解VB.NET与C#打印分页原理与实现的朋友有一些益处,应用封装后的dll开发的示例实现了一个完整的文档打印。
此文曾经被CSDN主页收录为头条精华,从本文的最后部分的相关资源中可以找到它的链接及源码。
历史的天空
当你还在兴奋为自己在网上找到了一个打印程序完成DataGrid的打印的时候,当你还在高高兴兴的跟着微软走的时候,2005年12月2日的这一天,MS在北京奥体中心以“Ready To Rock 撼动未来”为主题发布了SQLServer2005、Visual Studio2005和BizTalkServer2006三个重量级产品。
MS的Fans热烈欢呼庆幸,快快欢迎新时代的到来吧。
当你打安装了VS2005后,当你打开IDE的时候,当你找ToolBox的时候,你会发现,DataGrid悄然的离开了我们的视线。不要奇怪,经典的ToolBar都被取代了,人家都是Window元老了,DataGrid才几年时间?该退下来就退下来,让年轻有为的上去嘛。
DataGrid光荣的退居二线了,只是为了兼容,要不然,估计就得养老了,还要它继续以前它完成的任务呢。MS建议使用DataGridView,不要惊诧,DataGridView无论是在界面上还是在功能上还是在控件的设计时支持上,确实比DataGrid强多了。你大可不必再对那个双击啊、单击啊、选择多个单元格等等一系列烦恼了。单元格操作选择上很像EXCEL,还有了固定列(冻结列),有了这句话,估计您这下放心了吧。
打印思想:以不变应万变
很多打印程序,都是针对特定的网格控件如DataGrid打印,这一下,当VS2005隆重登录的时候,当DataGridView登上宝座的时候,我们的打印程序还能用吗?
回答是肯定的,只要您使用的打印核心是针对二维数据的打印。无论它是VB6的MSHFlexGrid,还是VB5的DBGrid...,还是VS.NET2002/3/5的DataGrid,还是现在新登场DataGridView,打印它们,还不是件简单的事情?
还有人说,我想打印ListView?没关系,写一个把ListView导出到二维数组的程序就搞定了,一切照样,即使是DataTable、Asp.NET的DataGrid、GridView,一样轻松搞定它。
总之,实现网格、报表的打印,你要把握这个思想:
打印单元格文本,加上绘制线,就组成了网格,多个网格就组成了报表、单据等。
也许您还要问上一句:
那么打印文本呢?哈哈,还是网格,只是一行一列,不用绘制网格线。
也许有人要还要问:
我的报表或单据合同之类的太复杂了,能实现吗?回答是肯定的,我们可以通过组合多个网格,合并网格的单元格进行实现。
总之,四个字:二维打印,也就是,无论你是往前走还是往后看,以不变应万变。往前走怎么说?也就是即使你还留念VB6的MSHFlexGrid,没关系,添加它的引用,照样在.NET窗口中可以使用它,它也是二维的,应用上面的方法,照打不误。往后看怎么解?也就是说如果你还在使用VS2002、2003,那么以后用VS2005也可以打印DataGridView啊?再进一步,哪一天MS把DataGridView用新的网格取代了或者你自己开发一个或三方网格取代了它,还是那句话,照打不误,以不变应万变。
打印精髓
二维数据及网格的绘制,无非就是绘制单元格文本及网格线,接下来我将给出具体的实现,你可以自己封装并修改它。至于成型的架构,从本文的最后部分的相关资源中可以找到它的链接及源码。下载源码
核心是DrawGrid网格,亮点是合并打印算法,因此把源码列出来!
#region 画标准横坚网格线核心
/// <summary>
/// 画网格线,标准备的横竖线交叉的线
/// </summary>
/// <param name="g">绘图表面</param>
/// <param name="p_rec">绘图区</param>
/// <param name="p_pen">绘图线的笔,可以定义颜色与线宽</param>
/// <param name="p_rows">行数</param>
/// <param name="p_cols">列数</param>
/// <param name="p_rowHeight">行高</param>
/// <param name="p_arrColsWidth">列宽</param>
/// <param name="p_gridLineFlag">网格线类型</param>
/// <param name="p_gridBorderFlag">边框类型</param>
/// <param name="p_scaleXY">水平与垂直方向缩放量</param>
/// <remarks>
/// 作 者:周方勇[长江支流]
/// 修改日期:2004-08-07
/// </remarks>
protected void DrawGridLine(Graphics g,Rectangle p_rec,Pen p_pen,int p_rows,int p_cols,int p_rowHeight,int[] p_arrColsWidth,GridLineFlag p_gridLineFlag,GridBorderFlag p_gridBorderFlag,PointF p_scaleXY)
{
//缩放矩阵,用于绘图
Rectangle rec = new Rectangle(p_rec.X,p_rec.Y,p_rec.Width,p_rec.Height);
//缩放程序
this.TransGrid(g,rec,p_scaleXY);
#region 有网格线才画
if (p_gridLineFlag != GridLineFlag.None)
{
int lngRows = p_rows; //arrStrGrid.GetLength(0); //行数,也可由二维数组算出
int lngCols = p_cols; //arrStrGrid.GetLength(1); //列数
int lngRowIndex; //当前行
int lngColIndex; //当前列
//起止坐标
int X1, X2,Y1, Y2;
int lngLineLen; //线长
int lngLineHei; //线高
//计算坐标、线长、线高
lngLineLen = rec.Width;
lngLineHei = rec.Height;
#region 包括横线就画
if (p_gridLineFlag == GridLineFlag.Horizontal || p_gridLineFlag == GridLineFlag.Both)
{
//******先画横线******
X1 = rec.X;
Y1 = rec.Y;
X2 = X1 + lngLineLen;
//最上边与最下边的线不画
for(lngRowIndex = 1 ; lngRowIndex < lngRows ; lngRowIndex++)
{
Y1 += p_rowHeight; //这里可以换成行高数组
//Y1 += p_arrRowsWidth[lngRowIndex - 1];//这里可以换成行高数组
Y2 = Y1;
g.DrawLine(p_pen,X1,Y1,X2,Y2);
}
}
#endregion
#region 包括竖线就画
if (p_gridLineFlag == GridLineFlag.Vertical || p_gridLineFlag == GridLineFlag.Both)
{
//******再画竖线******
//列宽
int[] mArrColWidth = new int[lngCols];
mArrColWidth = p_arrColsWidth;
//Y不变
X1 = rec.X;
Y1 = rec.Y;
Y2 = Y1 + lngLineHei;
//最左边与右边的线不画
for(lngColIndex = 0 ; lngColIndex < lngCols-1 ; lngColIndex++)
{
X1 += mArrColWidth[lngColIndex];
X2 = X1;
g.DrawLine(p_pen,X1,Y1,X2,Y2);
}
}
#endregion
}//End If
#endregion
//******边框******
if (p_gridBorderFlag != GridBorderFlag.None)
{
this.DrawGridBorder(g,rec,p_pen,p_gridBorderFlag);
}
//重置,不再变换
this.ResetTransGrid();
}
#endregion
#region 画合并线的核心
/// <summary>
/// 画网格线,根据合并方式判断相邻单元格内容一格一格的画
/// </summary>
/// <param name="g">绘图表面</param>
/// <param name="p_rec">绘图区</param>
/// <param name="p_pen">绘图线的笔,可以定义颜色与线宽</param>
/// <param name="arrStrGrid">二维数组</param>
/// <param name="p_rowHeight">行高</param>
/// <param name="p_arrColsWidth">列宽</param>
/// <param name="p_gridLineFlag">网格线类型</param>
/// <param name="p_gridBorderFlag">边框类型</param>
/// <param name="p_scaleXY">水平与垂直方向缩放量</param>
/// <param name="gridMergeFlag">网格单元格合并方式</param>
/// <remarks>
/// 作 者:周方勇[长江支流]
/// 修改日期:2004-08-07
/// </remarks>
protected void DrawGridMergeLine(Graphics g,Rectangle p_rec,Pen p_pen,string[,] arrStrGrid,int p_rowHeight,int[] p_arrColsWidth,GridLineFlag p_gridLineFlag,GridBorderFlag p_gridBorderFlag,PointF p_scaleXY,GridMergeFlag gridMergeFlag)
{
//缩放矩阵,用于绘图
Rectangle rec = new Rectangle(p_rec.X,p_rec.Y,p_rec.Width,p_rec.Height);
int lngRows = arrStrGrid.GetLength(0); //行数
int lngCols = arrStrGrid.GetLength(1); //列数
//网格不合并直接画标准网格线,否则一个单元格一个单元格的画
if (gridMergeFlag == GridMergeFlag.None)
{
this.DrawGridLine(g,rec,p_pen,lngRows,lngCols,p_rowHeight,p_arrColsWidth,p_gridLineFlag,p_gridBorderFlag,p_scaleXY);
return;
}
else
{
#region 有网格线才画
if (p_gridLineFlag != GridLineFlag.None)
{
//变换
this.TransGrid(g,rec,p_scaleXY);
//起止坐标
int X1, X2,Y1, Y2;
//列宽
int[] mArrColWidth = new int[lngCols];
mArrColWidth = p_arrColsWidth;
#region 画单元格线
//边界不画
for(int i = 0 ; i < lngRows ; i++)
{
X1 = rec.X;
Y1 = rec.Y;
for(int j = 0 ; j < lngCols ; j++)
{
//-----水平线-----
X2 = X1 + mArrColWidth[j];
Y1 = rec.Y + p_rowHeight * i; //****可用行高数组
Y2 = Y1;
//画第二行开始及以下的横线,当前行与上一行文本不同
if (i > 0)
{
//任意合并,只要相邻单元格内容不同就画线,即只要相邻单元格内容相同就合并
if (gridMergeFlag == GridMergeFlag.Any)
{
//画线(条件:此列不合并 || 文本空 || 当前行与上一行文本不同)
if(arrStrGrid[i,j] == "" || arrStrGrid[i,j] != arrStrGrid[i-1,j])
{
g.DrawLine(p_pen,X1,Y1,X2,Y2);
}
}
}
//-----'竖线-----
//画第二列以后的竖线,当前列与上一列比较
if (j > 0)
{
Y2 = Y2 + p_rowHeight; //****可用行高数组
X2 = X1;
//任意合并,只要相邻单元格内容不同就画线,即只要相邻单元格内容相同就合并
if (gridMergeFlag == GridMergeFlag.Any)
{
//画线(条件:此行不合并 || 文本空 || 当前列与上一列文本不同)
if(arrStrGrid[i,j] == "" || arrStrGrid[i,j] != arrStrGrid[i,j-1])
{
g.DrawLine(p_pen,X1,Y1,X2,Y2);
}
}
}
//下一列,宽加上
X1 += mArrColWidth[j];
}//End For 列
}//End For 行
#endregion
//******边框******
if (p_gridBorderFlag != GridBorderFlag.None)
{
this.DrawGridBorder(g,rec,p_pen,p_gridBorderFlag);
}
//重置,不再变换
this.ResetTransGrid();
}//End If
#endregion
}//End If
}//End Function
#endregion
#region 标准不合并网格的文本
/// <summary>
/// 绘制网格文本,标准的行与列单元格,无合并
/// </summary>
/// <param name="g">绘图表面</param>
/// <param name="p_rec">绘图区</param>
/// <param name="p_brush">绘图文本的画刷,可以定义颜色</param>
/// <param name="arrStrGrid">二维字符数组(网格)</param>
/// <param name="p_rowHeight">固定行高</param>
/// <param name="p_arrColsWidth">列宽数组,为null时则平均列宽</param>
/// <param name="alignment">由Left,Center,Right对齐方式第一个字母组成的串</param>
/// <param name="p_scaleXY">指定X与Y向缩放比例值</param>
/// <remarks>
/// 作 者:周方勇[长江支流]
/// 修改日期:2004-08-07
/// </remarks>
protected void DrawGridText(Graphics g,Rectangle p_rec,Brush p_brush,string[,] arrStrGrid,int p_rowHeight,int[] p_arrColsWidth,string alignment,Font p_font,PointF p_scaleXY)
{
try
{
//缩放矩阵,用于绘图
Rectangle rec = new Rectangle(p_rec.X,p_rec.Y,p_rec.Width,p_rec.Height);
Font font = p_font;
if (font == null)
{
font = new Font("宋体",12.0F);
}
int lngRows = arrStrGrid.GetLength(0); //行数
int lngCols = arrStrGrid.GetLength(1); //列数
//列宽
int[] mArrColWidth = new int[lngCols];
mArrColWidth = p_arrColsWidth;
//列对齐方式
AlignFlag[] arrAlign;
arrAlign = this.GetColsAlign(alignment);
//变换
this.TransGrid(g,rec,p_scaleXY);
//起止坐标
int X1,Y1,width;
#region 画单元格文本
StringFormat sf = new StringFormat(); //字符格式
sf.LineAlignment = StringAlignment.Center; //垂直居中
sf.FormatFlags = StringFormatFlags.LineLimit | StringFormatFlags.NoWrap;
for(int i = 0 ; i < lngRows ; i++)
{
X1 = rec.X;
Y1 = rec.Y + p_rowHeight*i; //****可用行数组
for(int j = 0 ; j < lngCols ; j++)
{
width = mArrColWidth[j];
Rectangle recCell = new Rectangle(X1,Y1,width,p_rowHeight + 4); //实际上居中会稍微偏上,因为字体有预留边距
sf.Alignment = StringAlignment.Near; //默认左对齐
if(arrAlign.Length > j)
{
if (arrAlign[j] == AlignFlag.Center)
{
sf.Alignment = StringAlignment.Center; //居中
}
else if (arrAlign[j] == AlignFlag.Right)
{
sf.Alignment = StringAlignment.Far ; //居右
}
}
g.DrawString(arrStrGrid[i,j],font,p_brush,recCell,sf);
X1 += width;
}//End For 列
}//End For 行
#endregion
//重置,不再变换
this.ResetTransGrid();
// font.Dispose();
}
catch(Exception e)
{
System.Windows.Forms.MessageBox.Show(e.Message);
}
finally
{
}
}//End Function
#endregion
#region 合并方式下的网格文本
/// <summary>
/// 绘制网格文本,标准的行与列单元格,有合并
/// </summary>
/// <param name="g">绘图表面</param>
/// <param name="p_rec">绘图区</param>
/// <param name="p_brush">绘图文本的画刷,可以定义颜色</param>
/// <param name="arrStrGrid">二维字符数组(网格)</param>
/// <param name="p_rowHeight">固定行高</param>
/// <param name="p_arrColsWidth">列宽数组,为null时则平均列宽</param>
/// <param name="alignment">由Left,Center,Right对齐方式第一个字母组成的串</param>
/// <param name="p_scaleXY">指定X与Y向缩放比例值</param>
/// <remarks>
/// 作 者:周方勇[长江支流]
/// 修改日期:2004-08-07
/// </remarks>
protected void DrawGridMergeText(Graphics g,Rectangle p_rec,Brush p_brush,string[,] arrStrGrid,int p_rowHeight,int[] p_arrColsWidth,string alignment,Font p_font,PointF p_scaleXY,GridMergeFlag gridMergeFlag)
{
if (gridMergeFlag == GridMergeFlag.None)
{
DrawGridText(g,p_rec,p_brush,arrStrGrid,p_rowHeight,p_arrColsWidth,alignment,p_font,p_scaleXY);
return;
}
try
{
//缩放矩阵,用于绘图
Rectangle rec = new Rectangle(p_rec.X,p_rec.Y,p_rec.Width,p_rec.Height);
Font font = p_font;
if (font == null)
{
font = new Font("宋体",12.0F);
}
int lngRows = arrStrGrid.GetLength(0); //行数
int lngCols = arrStrGrid.GetLength(1); //列数
//列宽
int[] mArrColWidth = new int[lngCols];
mArrColWidth = p_arrColsWidth;
//列对齐方式
AlignFlag[] arrAlign;
arrAlign = this.GetColsAlign(alignment);
//变换
this.TransGrid(g,rec,p_scaleXY);
#region 画单元格文本
StringFormat sf = new StringFormat(); //字符格式
sf.LineAlignment = StringAlignment.Center; //垂直居中
sf.FormatFlags = StringFormatFlags.LineLimit | StringFormatFlags.NoWrap;
CellRectangle cell = new CellRectangle(rec.X,rec.Y,0,p_rowHeight); //单元格
for(int i = 0 ; i < lngRows ; i++)
{
for(int j = 0 ; j < lngCols ; j++)
{
//.....
cell = this.GetMergeCell(new Point(rec.X,rec.Y),arrStrGrid,p_rowHeight,mArrColWidth,i,j);
Rectangle recCell = new Rectangle(cell.Left,cell.Top,cell.Width,cell.Height + 4); //实际上居中会稍微偏上,因为字体有预留边距
sf.Alignment = StringAlignment.Near; //默认左对齐
if(arrAlign.Length > j)
{
if (arrAlign[j] == AlignFlag.Center)
{
sf.Alignment = StringAlignment.Center; //居中
}
else if (arrAlign[j] == AlignFlag.Right)
{
sf.Alignment = StringAlignment.Far ; //居右
}
}
g.DrawString(arrStrGrid[i,j],font,p_brush,recCell,sf);
}//End For 列
}//End For 行
#endregion
//重置,不再变换
this.ResetTransGrid();
//font.Dispose();
}
catch(Exception e)
{
System.Windows.Forms.MessageBox.Show(e.Message);
}
finally
{
}
}//End Function
#endregion
以上方法中用到的GetColWidth函数,算法比较简单,就是把总共的列宽除以列数,让每列平均。为了十分的精确,其实是让除最后一列的列的列宽为平均值,最后一列为总列宽减去前面列宽和。 另外,GridLineFlag枚举定义了Horizontal、Vertical和GridLineFlag.Both,表示水平、垂直及两者都有的网格线。
其它支持函数,请下载源码!
DataGridView转换为二维数组
/// <summary>
/// 将VS.Net 2005 DataGridView控件的数据导出到二维数组。
/// </summary>
/// <param name="dataGridView">VS.Net 2005 DataGridView控件。</param>
/// <param name="includeColumnText">是否要把列标题文本也导到数组中。</param>
/// <作者>长江支流</作者>
/// <日期></日期>
public string[,] ToStringArray(DataGridView dataGridView, bool includeColumnText)
{
#region 实现...
string[,] arrReturn = null;
int rowsCount = dataGridView.Rows.Count;
int colsCount = dataGridView.Columns.Count;
if (rowsCount > 0)
{
//最后一行是供输入的行时,不用读数据。
if (dataGridView.Rows[rowsCount - 1].IsNewRow)
{
rowsCount--;
}
}
int i = 0;
//包括列标题
if (includeColumnText)
{
rowsCount++;
arrReturn = new string[rowsCount, colsCount];
for (i = 0; i < colsCount; i++)
{
arrReturn[0, i] = dataGridView.Columns[i].HeaderText;
}
i = 1;
}
else
{
arrReturn = new string[rowsCount, colsCount];
}
//读取单元格数据
int rowIndex = 0;
for (; i < rowsCount; i++, rowIndex++)
{
for (int j = 0; j < colsCount; j++)
{
arrReturn[i, j] = dataGridView.Rows[rowIndex].Cells[j].Value.ToString();
}
}
return arrReturn;
#endregion 实现
}
ListView转换为二维数组
/// <summary>
/// 将ListView的数据导出到二维数组。
/// </summary>
/// <param name="listView">二维数据视图</param>
/// <param name="includeColumnText">是否要把列标题文本也导到数组中。</param>
/// <remarks>
/// <作者>长江支流</作者>
/// <日期>2005-08-21</日期>
/// </remarks>
/// <returns>二维数组。</returns>
public string[,] ToStringArray(ListView listView,bool includeColumnText)
{
ListView lvw = listView;
int rowsCount = lvw.Items.Count;
int colsCount = lvw.Columns.Count;
//包括列标题
if (includeColumnText)
{
rowsCount++;
}
string[,] arrReturn = null;
arrReturn = new string[rowsCount,colsCount];
int i = 0;
if (includeColumnText)
{
//写标题
for(i = 0 ; i < colsCount; i++)
{
arrReturn[0,i] = lvw.Columns[i].Text;
}
i = 1;
}
//写数据行Items
int rowIndex = 0;
for(; i < rowsCount; i++,rowIndex++)
{
for (int j = 0; j < colsCount; j++)
{
arrReturn[i,j] = lvw.Items[rowIndex].SubItems[j].Text;
}
}
return arrReturn;
}
打印ListView:
private void btnPrintEasy_Click(object sender, System.EventArgs e)
{
MisGoldPrinter webmis = new MisGoldPrinter(); //打印组件
webmis.Title = "MIS金质打印通/nWWW.WebMIS.COM.CN"; //网格标题
webmis.DataSource = ToStringArray(listView,true); //任意二维的数据通通打印
webmis.Preview(); //打印预览
}
VB.Net:
Private Sub btnPrintEasy_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnPrintEasy.Click
Dim webmis As MisGoldPrinter '打印组件
webmis = New MisGoldPrinter
webmis.Title = "MIS金质打印通"+vbCrLf+"WWW.WebMIS.COM.CN" '网格标题
webmis.DataSource = ToStringArray(listView,true) '任意二维的数据通通打印
webmis.Preview();
webmis.Dispose();
End Sub
打印DataGridView:
不用说,相信您已掌握到打印精髓,不就是一个二维数组么?
是的,就是一个二维数组。为了效率,你可以使用一个DataTable,分页将数据写到二维数组中打印。下面,就以DataGridView打印为例,再把网格的字体和列宽也都设置一下。
private void btnPrintEasy_Click(object sender, System.EventArgs e)
{
GoldPrinter.MisGoldPrinter webmis = new GoldPrinter.MisGoldPrinter(); //打印组件
webmis.Title = "MIS金质打印通/nWWW.WebMIS.COM.CN"; //标题,还可设置子标题
(webmis.Title as GoldPrinter.Title).Font = new System.Drawing.Font("宋体", 12, System.Drawing.FontStyle.Bold);
//下面这一句就可以打印DataGridView
//(webmis.Body as GoldPrinter.Body).DataSource = ToStringArray(dataGridView1, true);
//为人特性化,自定义表体,可以设置字体、列宽、列对齐方式
GoldPrinter.Body gridBody = new GoldPrinter.Body();
//任意二维的数据通通打印,或者是设置GridText属性
gridBody.DataSource = ToStringArray(dataGridView1, true);
gridBody.Font = dataGridView1.Font;
gridBody.ColsWidth = GetColsWidth(dataGridView1);
webmis.Body = gridBody;
webmis.Preview();
webmis.Dispose();
}
有了ListView的打印示例,VB.NET的代码我想也不用写了吧,上面一段照搬,新增的部分把//改成',把;去掉就OK了。
这里还用到提取ListView/Vs2005DataGridView列宽的方法int[] GetColsWidth(ListView listView)、int[] GetColsWidth(DataGridView dataGridView)。
为方便读者ListView与Vs2005DataGridView打印,我加了一个类,请见金质打印通工程 GoldPrint/DataGridViewListViewHelper.cs,提供将ListView、VS2005DataGridView转换成二维数组的方法并提取列宽。 下载源码
相关内容:
相关源码下载信息请访问:
http://blog.csdn.net/flygoldfish
打印核心架构及源码(建议学习者下载,然后再对照全部源码看看应用设计模式及优化。):
http://www.webmis.com.cn/DownLoad/MisGoldPrinterOpenSource.rar
打印全部源码(软件公司、工作室、个人,无需再购三方打印控件,拿来就用,还可以再加工定制自己需要的打印。):
http://www.webmis.com.cn/DownLoad/MisGoldPrinter.rar
NET环境下有关打印页面设置、打印机设置、打印预览对话框的实现及应用: http://blog.csdn.net/flygoldfish/archive/2004/08/17/77208.aspx
http://blog.csdn.net/flygoldfish/archive/2004/09/06/95685.aspx