用WPF實現列印及列印預覽
應該說,WPF極大地簡化了我們的列印輸出工作,想過去使用VC++做開發的時候,列印及預覽可是一件極麻煩的事情,而現在我不會再使用C++來做Windows的桌面應用了——價效比實在太低。
WPF的列印功能是很強大而簡便的,它甚至能夠直接列印介面上的內容,包括各種控制元件的顯示內容,例如你在介面上擺放了一個datagrid控制元件,畫了一個五角星,或寫了一段文字,都可以直接打印出來,這裡有一篇文章很簡單明瞭地說明了這個功能:
http://www.cnblogs.com/gnielee/archive/2010/07/02/wpf-print-sample.html
這種做法是非常直截了當的,但恐怕不是很適合我們一般的應用,我們更多的時候需要自適應紙張,表格輸出,自動分頁,還有分頁預覽……
自己設計分頁是非常麻煩的事情,沒做過的人恐怕沒法理解為什麼,我這裡插點題外話提一下,為什麼分頁難做?那是因為:你不真正把文件打印出來,你就不知道到底要在什麼地方分頁。舉個最簡單的例子,就一大段文字,給你列印,你認為到第幾個字要另開一頁?你估算了一下:一行20個字,我的列印紙一共20行,所以到第400個字的時候分頁。——Too simple,你沒考慮一行的字數根本就是不確定的(字元非等寬),也沒考慮回車換行所產生的空行,更沒考慮字型大小,行間距等影響因素,另外還有單詞自適應因素,最後還有紙張大小……Oh,my god,這簡直沒法做,是的,自己很難做的,一個比較笨但有效的方法是“模擬列印”,用二分法找到開始分頁的那個點,我以前做過的一個手機看書軟體就是這麼幹的,而真實的分頁演算法是很複雜的,所幸的是這次不需要我們來做了。下面是我寫的一個demo。
這是列印預覽效果:
程式碼並不多。設計的思路就是:文件模版(xaml)+資料(.net物件)=列印輸出
文件模版可以單獨建立,右擊你的WPF工程,Add - New Item - Flow Document(WPF),Visual Studio並沒有提供這個xaml的預覽,這點不得不說是個缺陷,微軟的理由是這種Flow Document的顯示需要一個容器,單獨的Flow Document(流文件)是沒法預覽的,你必須把它放在一個容器中才可以,流文件的容器有FlowDocumentScrollViewer,FlowDocumentPageViewer,FlowDocumentReader,另外還有DocumentViewer,這個只支援固定流文件(只讀)。關於流文件及其列印方面的技術在《WPF程式設計寶典》一書中都有具體講述,建議大家要詳細瞭解的話先去閱讀一下此書,下面主要是一些書中沒有的內容。
列印預覽,我們這次選擇了DocumentViewer,因為它直接就帶有很好的分頁功能,我們只需要生成固定文件(XPS),然後交給它,它就能很好的將內容預覽出來——太棒了。
現在我們大致看看這個流文件模版的內容:
<Table FontSize="16"> <Table.Columns> <TableColumn Width="200"></TableColumn> <TableColumn Width="600"></TableColumn> </Table.Columns> <TableRowGroup> <TableRow> <TableCell> <Paragraph> 訂單號 </Paragraph> </TableCell> <TableCell> <Paragraph> <Run Text="{Binding OrderNo}"></Run> </Paragraph> </TableCell> </TableRow> <TableRow> <TableCell> <Paragraph> 客戶名稱 </Paragraph> </TableCell> <TableCell> <Paragraph> <Run Text="{Binding CustomerName}"></Run> </Paragraph> </TableCell> </TableRow>
<!-- 省略一大段 -->
</TableRowGroup> </Table>
我把多餘的內容去掉了,現在注意看“<Run Text="{Binding OrderNo}"></Run>”這個地方,我將這個Run的Text屬性繫結到DataContext的OrderNo去了,也就是說,它會根據資料的內容,渲染出不同的結果。
這裡一切OK,但最大的問題來了:流文件的Table卻不能跟UIElement的DataGrid控制元件那樣能動態地根據資料的條目數渲染出相應的行!也就是說Table的行數是固定的,流文件上的物件是靜態的,所以我們只能用後臺程式碼來手工改變它了,這是相當不方便的地方……我定義了這麼一個介面來做這種工作:
public interface IDocumentRenderer { void Render(FlowDocument doc, Object data); }
建立一個物件,實現這個介面,然後根據data的內容,往doc裡對應的地方插入行。
另外還需要特別說明的是程式碼中使用了一些BeginInvoke,也許大家不太瞭解那是什麼意思,為什麼需要這麼麻煩?其實,那是因為你給Document的DataContext賦值的時候,Document的內容並不是馬上改變的,不信你可以把我寫的這些BeginInvoke改為直接呼叫,然後看看列印預覽的文件內容,是不是哪些binding的地方還是空白的?所以需要一個“延後”呼叫。關於BeginInvoke的內容可以看我這篇blog:
http://www.cnblogs.com/guogangj/archive/2013/01/22/2870590.html
最後,主介面上的“直接列印”為了防止使用者連續點選,需要在點了一下之後把它變灰,然後過幾秒鐘之後再把它變亮。
最後的最後:完整程式碼下載