Silverlight 打印基礎知識

來源: MSDN  發布時間: 2011-09-04 17:40  閱讀: 6689 次  推薦: 1   原文鏈接   [收藏]  

  Silverlight 4 在 Silverlight 功能列表中添加了打印,我想通過向您介紹令我欣慰的小程序來探討這一點。

  該程序稱為 PrintEllipse,名稱就是它要執行的所有操作。 MainPage 的 XAML 文件包含一個按鈕,圖 1 中完整地顯示了 MainPage 代碼隱藏文件。

  圖 1 PrintEllipse 的 MainPage 代碼

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Printing;
using System.Windows.Shapes;

namespace PrintEllipse
{

publicpartialclass MainPage : UserControl
{

public MainPage()
{
InitializeComponent();
}

void OnButtonClick(object sender, RoutedEventArgs args)
{
PrintDocument printDoc
=new PrintDocument();
printDoc.PrintPage
+= OnPrintPage;
printDoc.Print(
"Print Ellipse");
}

void OnPrintPage(object sender, PrintPageEventArgs args)
{
Ellipse ellipse
=new Ellipse
{
Fill
=new SolidColorBrush(Color.FromArgb(255, 255, 192, 192)),
Stroke
=new SolidColorBrush(Color.FromArgb(255, 192, 192, 255)),
StrokeThickness
=24// 1/4 inch
};
args.PageVisual
= ellipse;
}
}
}
  

  請注意 System.Windows.Printing 的 using 指令。 在單擊此按鈕時,該程序將創建一個類型為 PrintDocument 的對象,并為 PrintPage 事件分配一個處理程序。 當程序調用 Print 方法時,將顯示標準打印對話框。 用戶可借此機會設置要使用的打印機,并設置各種打印屬性,例如縱向或橫向模式。

  當用戶單擊打印對話框中的“打印”時,該程序將接收到對 PrintPage 事件處理程序的調用。 此特殊程序會通過創建 Ellipse 元素并將該元素設置為事件參數的 PageVisual 屬性來進行響應。 (我故意選擇淡彩色以便程序不會使用太多油墨。)很快,將從打印機中出來一頁,且該頁中填充了一個非常大的橢圓。

  您可以從網站 bit.ly/dU9B7k 運行此程序并親自檢驗。 當然,本文中的所有源代碼也是可下載的。

  如果您的打印機與大多數打印機一樣,則內部硬件將禁止打印機打印到紙張的每個邊緣。 打印機通常具有固有的內置邊距,不會在邊距內打印任何內容;打印內容限制在小于頁面全部大小的“可打印區域”上。

  關于此程序,您將注意到的是:橢圓整體顯示在頁面的可打印區域中,很顯然,程序可以輕松達到此目的。 頁面可打印區域的行為方式與屏幕上的容器元素非常類似:它僅在元素大小超出此區域時才對子項進行剪輯。 一些更復雜的圖形環境(例如 Windows Presentation Foundation (WPF))未必有如此好的表現(當然,與 Silverlight 相比,WPF 可提供更多打印控制和靈活性)。

  PrintDocument 和事件

  除了 PrintPage 事件,PrintDocument 還定義了 BeginPrint 和 EndPrint 事件,但這些事件并非與 PrintPage 一樣重要。 BeginPrint 事件表明打印作業的開始。 當用戶通過按“打印”按鈕退出標準打印對話框并給程序機會執行初始化時,將觸發該事件。 調用 BeginPrint 處理程序之后,將對 PrintPage 處理程序進行首次調用。

  要在特殊打印作業中打印多頁的程序將這樣操作。 在對 PrintPage 處理程序的每次調用中,PrintPageEventArgs 的 HasMorePages 屬性初始將設置為 false。 當處理程序完成一頁后,它只需將該屬性設置為 true 即可表明至少必須再打印一頁。 然后再次調用 PrintPage。 PrintDocument 對象維護 PrintedPageCount 屬性,該屬性在每次對 PrintPage 處理程序執行調用后遞增。

  如果 PrintPage 處理程序退出時 HasMorePages 設置為其默認值 false,打印作業將結束并觸發 EndPrint 事件,這樣,程序將有機會執行清理任務。 當打印過程中出現錯誤時也會觸發 EndPrint 事件;EndPrintEventArgs 的 Error 屬性的類型為 Exception。

  打印機坐標

  圖 1 中顯示的代碼將 Ellipse 的 StrokeThickness 設置為 24,如果您度量打印結果,您將發現這是四分之一英寸寬。 如您所知,Silverlight 程序通常以像素為單位從整體上調整圖形對象和控件的大小。 但是,涉及打印機時,坐標和大小都采用與設備無關的單位,即 1/96 英寸。 不論打印機的實際分辨率如何,在 Silverlight 程序中,打印機始終顯示為 96 DPI 設備。

  您可能知道,在整個 WPF 中都使用這種 96 個單位為一英寸的坐標系,其中,單位有時稱為“與設備無關的像素”。此 96 DPI 值不是隨意選擇的:默認情況下,Windows 假定您的視頻顯示器的一英寸具有 96 個點,因此,在很多情況下,WPF 程序實際上以像素為單位進行繪制。 CSS 規范假定視頻顯示器的分辨率為 96 DPI,該值用于在像素、英寸和毫米之間進行轉換。 值 96 還是一個便于轉換字體大小的數字,字體大小通常用磅(即 1/72 英寸)來指定。 一磅是一個與設備無關像素的四分之三。

  PrintPageEventArgs 具有兩個有用的只讀屬性,這兩個屬性也以 1/96 英寸為單位報告大小:類型為 Size 的 PrintableArea 提供頁面的可打印區域的尺寸,類型為 Thickness 的 PageMargins 是位于左側、頂部、右側和底部的不可打印邊緣的寬度。 以正確的方式將這兩個屬性加到一起,您就會得到紙張的完整大小。

  我的打印機裝載的是標準 8.5 x 11 英寸的紙張并設置為縱向模式,報告 PrintableArea 為 791 x 993。 PageMargins 屬性的四個值為 12(左側)、6(頂部)、12(右側)和 56(底部)。 如果將水平方向的值 791、12 和 12 相加,將得到 815。 垂直方向的值為 994、6 和 56,加起來是 1,055。 我不確定為什么這些值與將頁面大小(以英寸為單位)與 96 相乘所得的值 816 和 1,056 之間存在一個單位的差異。

  當打印機設置為橫向模式時,PrintableArea 和 PageMargins 報告的水平尺寸和垂直尺寸值將交換。 實際上,查看 PrintableArea 屬性是 Silverlight 程序確定打印機是縱向模式還是橫向模式的唯一方式。 該程序打印的任何內容將根據此模式自動對齊和旋轉。

  通常,當您打印現實生活中的內容時,定義的邊距將比不可打印邊距稍大些。 在 Silverlight 中如何做到這一點呢? 首先,這與設置要打印元素的 Margin 屬性一樣容易。 此 Margin 是這樣計算的:從所需總邊距(以 1/96 英寸為單位)中減去 PrintPageEventArgs 中提供的 PageMargins 屬性的值。 該方法的效果不是很好,但正確的解決方案幾乎一樣簡單。 PrintEllipseWithMargins 程序(可以在 bit.ly/fCBs3X 上運行)與第一個程序相同,只不過對 Ellipse 設置了 Margin 屬性,然后將 Ellipse 設置為將填充可打印區域的 Border 的子項。 或者,您也可以對 Border 設置 Padding 屬性。 圖 2 顯示新的 OnPrintPage 方法。

  圖 2 用于計算邊距的 OnPrintPage 方法

void OnPrintPage(object sender, PrintPageEventArgs args)
{
Thickness margin
=new Thickness
{
Left
= Math.Max(0, 96- args.PageMargins.Left),
Top
= Math.Max(0, 96- args.PageMargins.Top),
Right
= Math.Max(0, 96- args.PageMargins.Right),
Bottom
= Math.Max(0, 96- args.PageMargins.Bottom)
};
Ellipse ellipse
=new Ellipse
{
Fill
=new SolidColorBrush(Color.FromArgb(255, 255, 192, 192)),
Stroke
=new SolidColorBrush(Color.FromArgb(255, 192, 192, 255)),
StrokeThickness
=24, // 1/4 inch
Margin = margin
};
Border border
=new Border();
border.Child
= ellipse;
args.PageVisual
= border;
}
   

  PageVisual 對象

  沒有與打印機相關聯的特殊圖形方法或圖形類。 您可以按照在視頻顯示器上“繪制”對象的相同方式在打印機頁面上“繪制”對象,方法是組合從 FrameworkElement 派生的對象可視樹。 此樹可以包含面板元素,其中包括畫布。 若要打印該可視樹,請將最上面的元素設置為 PrintPageEventArgs 的 PageVisual 屬性。 (PageVisual 定義為 UIElement,該元素是 FrameworkElement 的父類,但實際上,將設置為 PageVisual 的一切對象都將從 FrameworkElement 派生。)

  出于布局的目的,從 FrameworkElement 派生的幾乎所有類都包含 MeasureOverride 和 ArrangeOverride 方法的重要實現。 在類的 MeasureOverride 方法中,有一個元素決定類的所需大小,有時通過調用子項的 Measure 方法來確定子項的所需大小。 在 ArrangeOverride 方法中,有一個元素通過調用子項的 Arrange 方法來排列子項相對于類本身的位置。

  將某個元素設置為 PrintPageEventArgs 的 PageVisual 屬性時,Silverlight 打印系統將使用 PrintableArea 大小在該最上面的元素上調用 Measure。 這就是(舉例來說)Ellipse 或 Border 的大小自動調整為頁面的可打印區域的方式。

  但是,您也可以將該 PageVisual 屬性設置為已屬于程序窗口中所顯示的可視樹的元素。 這種情況下,打印系統不會對該元素調用 Measure,而是使用已為視頻顯示器確定的度量和布局。 這可使您在從程序窗口打印內容時保持合理的保真度,還意味著所打印的內容可能裁剪為頁面大小。

  當然,您可以對打印的元素設置明確的 Width 和 Height 屬性,并且可以使用 PrintableArea 大小來幫助解決問題。

  縮放和旋轉

  我要探討的下一個程序比我預期更具挑戰性。 目標是存儲在用戶本地計算機上允許用戶打印 Silverlight 支持的任何圖像文件(即 PNG 和 JPEG 文件)的程序。 此程序使用 OpenFileDialog 類加載這些文件。 出于安全考慮,OpenFileDialog 僅返回讓程序打開文件的 FileInfo 對象。 不提供文件名或目錄。

  我希望此程序在頁面(不包括預設邊距)上盡可能大地打印位圖,而不改變位圖的長寬比。 通常情況下,這非常簡單:Image 元素的默認 Stretch 模式為 Uniform,這意味著位圖將被盡可能大地拉伸而不會扭曲。

  但是,我決定不需要用戶在相應打印機上針對特殊圖像專門設置縱向或橫向模式。 如果打印機設置為縱向模式,且圖像的寬度大于高度,則我希望圖像在縱向頁面上橫著打印。 這個小功能立即會使程序更復雜。

  如果我編寫一個實現此功能的 WPF 程序,程序本身可將打印機切換為縱向或橫向模式。 但是這在 Silverlight 中無法實現。 打印機接口的定義使只有用戶可以更改此類設置。

  同樣,如果我編寫 WPF 程序,則可以對 Image 元素設置 LayoutTransform 以將其旋轉 90 度。 隨后將調整旋轉后的 Image 元素的大小以適合頁面,而且位圖本身也會調整大小以適合該 Image 元素。

  但是 Silverlight 不支持 LayoutTransform。 Silverlight 僅支持 RenderTransform,因此如果必須旋轉 Image 元素以適合在縱向模式下打印的橫向圖像,還必須將 Image 元素的大小手動調整為橫向頁面的尺寸。

  您可以在 bit.ly/eMHOsB 上試驗我最初的嘗試。 OnPrintPage 方法創建一個 Image 元素并將 Stretch 屬性設置為 None,這意味著 Image 元素按位圖的像素大小顯示位圖,這在打印機上意味著每個像素假定為 1/96 英寸。 程序然后將旋轉該 Image 元素、調整其大小,并通過計算適用于該 Image 元素的 RenderTransform 屬性的變換來轉換該元素。

  此類代碼的難點當然是數學計算,因此,看到該程序可以在打印機設置為縱向和橫向模式的情況下處理縱向和橫向圖像,將是一件非常高興的事。

  但是,如果由于圖像太大而導致程序失敗,又將是非常不愉快的事。 您可以親自試驗尺寸(除以 96 時)稍大于頁面大小(以英寸為單位)的圖像。 圖像將按正確大小顯示,但顯示不完整。

  這行代碼起什么作用呢? 哦,我以前在視頻顯示器上看到過該代碼。 請記住,RenderTransform 僅影響元素的顯示方式,不影響元素在布局系統上的外觀。 對于布局系統,我在 Stretch 設置為 None 的 Image 元素中顯示了一個位圖,意味著 Image 元素與位圖本身一樣大。 如果位圖大于打印機頁面,則無法呈現 Image 元素的某些部分,實際上將剪輯該元素,而與相應縮小 Image 元素的 RenderTransform 無關。

  我的第二個嘗試(您可在 bit.ly/g4HJ1C 上試用)采取了不同的策略。 圖 3 中顯示了 OnPrintPage 方法。 為 Image 元素提供了顯式 Width 和 Height 設置,使該元素正好符合計算的顯示區域的大小。 由于該元素全部位于頁面的可打印區域中,因此不會剪輯任何內容。 Stretch 模式設置為 Fill,這意味著不論長寬比如何,位圖都將填充該 Image 元素。 如果不旋轉 Image 元素,正確調整了一個尺寸的大小,另一個尺寸必須應用可減小大小的比例因子。 如果還必須旋轉 Image 元素,則這些比例因子必須適合旋轉后 Image 元素的不同長寬比。

  圖 3 在 PrintImage 中打印圖像

void OnPrintPage(object sender, PrintPageEventArgs args)
{

// Find the full size of the page
Size pageSize =
new Size(args.PrintableArea.Width
+ args.PageMargins.Left + args.PageMargins.Right,
args.PrintableArea.Height

+ args.PageMargins.Top + args.PageMargins.Bottom);

// Get additional margins to bring the total to MARGIN (= 96)
Thickness additionalMargin =new Thickness
{
Left
= Math.Max(0, MARGIN - args.PageMargins.Left),
Top
= Math.Max(0, MARGIN - args.PageMargins.Top),
Right
= Math.Max(0, MARGIN - args.PageMargins.Right),
Bottom
= Math.Max(0, MARGIN - args.PageMargins.Bottom)
};


// Find the area for display purposes
Size displayArea =
new Size(args.PrintableArea.Width
- additionalMargin.Left - additionalMargin.Right,
args.PrintableArea.Height

- additionalMargin.Top - additionalMargin.Bottom);

bool pageIsLandscape = displayArea.Width > displayArea.Height;
bool imageIsLandscape = bitmap.PixelWidth > bitmap.PixelHeight;

double displayAspectRatio = displayArea.Width / displayArea.Height;
double imageAspectRatio = (double)bitmap.PixelWidth / bitmap.PixelHeight;

double scaleX = Math.Min(1, imageAspectRatio / displayAspectRatio);
double scaleY = Math.Min(1, displayAspectRatio / imageAspectRatio);

// Calculate the transform matrix
MatrixTransform transform =new MatrixTransform();

if (pageIsLandscape == imageIsLandscape)
{

// Pure scaling
transform.Matrix =new Matrix(scaleX, 0, 0, scaleY, 0, 0);
}

else
{
// Scaling with rotation
scaleX *= pageIsLandscape ?
displayAspectRatio : 1/
displayAspectRatio;
scaleY
*= pageIsLandscape ?
displayAspectRatio : 1/
displayAspectRatio;
transform.Matrix
=new Matrix(0, scaleX, -scaleY, 0, 0, 0);
}

Image image
=new Image
{
Source
= bitmap,
Stretch
= Stretch.Fill,
Width
= displayArea.Width,
Height
= displayArea.Height,
RenderTransform
= transform,
RenderTransformOrigin
=new Point(0.5, 0.5),
HorizontalAlignment
= HorizontalAlignment.Center,
VerticalAlignment
= VerticalAlignment.Center,
Margin
= additionalMargin,
};

Border border
=new Border
{
Child
= image,
};

args.PageVisual
= border;
}
  

  代碼實在是太雜亂了(我懷疑可能有一些簡化,但對我來說不是很明顯),但它適合所有大小的位圖。

  另一種方法是旋轉位圖本身而不是 Image 元素。 從加載的 BitmapImage 對象創建一個 WriteableBitmap,并使用交換的水平尺寸和垂直尺寸創建另一個 WritableBitmap。 然后將第一個 WriteableBitmap 中的所有像素復制到具有交換的行和列的第二個 WriteableBitmap。

  多個日歷頁面

  在 Silverlight 編程中,從 UserControl 派生是一項相當常用的技術,可用來創建可重用控件而不會增加很多麻煩。 UserControl 的大部分是在 XAML 中定義的可視樹。

  還可以通過從 UserControl 派生來定義用于打印的可視樹! PrintCalendar 程序中闡釋了此技術,您可以在 bit.ly/dIwSsn 上進行試驗。 輸入開始月份和結束月份后,程序將打印該范圍中的所有月份,一個月份打印一頁。 您可以將頁面裝訂為掛歷并進行標記,就好像真實的日歷掛歷一樣。

  體驗 PrintImage 程序后,我不想為邊距或方向而費心;我增加了一個按鈕,通過它將此職責交給了用戶,如圖 4 所示。

圖 4 PrintCalendar 按鈕

  定義日歷頁面的 UserControl 稱為 CalendarPage,圖 5 中顯示了 XAML 文件。 頂部附近的 TextBlock 顯示月份和年份。 然后是另一個網格,其中包含七列(用于表示星期幾)和六行(用于表示一月中的最多六周或部分  周)。

  圖 5 CalendarPage 布局

<UserControl x:Class="PrintCalendar.CalendarPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
FontSize="36">
<Grid x:Name="LayoutRoot" Background="White">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Name="monthYearText"
Grid.Row="0"
FontSize="48"
HorizontalAlignment="Center"/>
<Grid Name="dayGrid"
Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
</Grid>
</Grid>
</UserControl>

  與大多數 UserControl 派生項不同,CalendarPage 定義了一個包含參數的構造函數,如圖 6 所示。

  圖 6 CalendarPage 代碼隱藏構造函數

public CalendarPage(DateTime date)
{
InitializeComponent();
monthYearText.Text
= date.ToString("MMMM yyyy");
int row =0;
int col = (int)new DateTime(date.Year, date.Month, 1).DayOfWeek;
for (int day =0; day < DateTime.DaysInMonth(date.Year, date.Month); day++)
{
TextBlock txtblk
=new TextBlock
{
Text
= (day +1).ToString(),
HorizontalAlignment
= HorizontalAlignment.Left,
VerticalAlignment
= VerticalAlignment.Top
};
Border border
=new Border
{
BorderBrush
= blackBrush,
BorderThickness
=new Thickness(2),
Child
= txtblk
};
Grid.SetRow(border, row);
Grid.SetColumn(border, col);
dayGrid.Children.Add(border);

if (++col ==7)
{
col
=0;
row
++;
}
}

if (col ==0)
row
--;
if (row <5)
dayGrid.RowDefinitions.RemoveAt(
0);
if (row <4)
dayGrid.RowDefinitions.RemoveAt(
0);
}

  該參數是 DateTime 類型,構造函數使用 Month 和 Year 屬性創建一個邊框,其中包含月份中每一天的 TextBlock。 每個 TextBlock 都被分配了一個 Grid.Row 和 Grid.Column 附加屬性,然后添加到網格中。 如您所知,月份通常跨越五周,有時候二月僅有四周,因此,如果不需要 RowDefinition 對象,實際上會將它們從網格中刪除。

  UserControl 派生項通常不具有包含參數的構造函數,因為它們通常構成大型可視樹的部分。 但是,CalendarPage 的使用并非如此。 實際上,PrintPage 處理程序只是將 CalendarPage 的新實例分配給 PrintPageEventArgs 的 PageVisual 屬性。 下面是該處理程序的完整主體,清晰地闡釋了 CalendarPage 執行的工作量:

args.PageVisual =new CalendarPage(dateTime);
args.HasMorePages
= dateTime < dateTimeEnd;
dateTime
= dateTime.AddMonths(1);

  因此,向程序中添加打印選項經常被視為涉及大量代碼的令人精疲力盡的工作。 能夠在 XAML 文件中定義大部分打印頁面使整個工作變得不那么可怕。

1
0
 
標簽:Silverlight
 
 

文章列表

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()