開源:VS.NET打印思想與2003/5DataGrid、DataGridView及二維數據如ListView等終極打印實現

前言

  當.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轉換成二維數組的方法並提取列寬。下載源碼 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章