WPF/MVVM 打印或创建应用程序屏幕图像





5.00/5 (3投票s)
本文将介绍一种简单的方法,为您的应用程序中的每个屏幕(窗口)或 FrameworkElement(用户控件)添加打印或另存为图像的功能。
应用程序屏幕的示例打印输出
引言
我当时在一个项目中工作,发现自己需要将屏幕上的数据复制到报告中供用户使用。经过一番思考,我决定研究打印屏幕信息的方法。我希望生成一份专业的报告,而不仅仅是用户需要手动处理的简单屏幕截图或剪切图像。
以下代码提供了一种简单的方法来实现打印,并可在页面顶部添加标题或描述。它还允许程序捕获您的程序屏幕作为图像,可用于文档、电子邮件、故障排除、帮助文件,甚至用作工具提示!
带有屏幕图像工具提示的示例屏幕图像
代码构成
代码主要由 2 个类(和 2 个对应的接口类)组成。一个用于创建屏幕文件图像,另一个用于创建屏幕打印件。
以下每个主题
- 将屏幕(
FrameworkElement
)保存为图像文件 - 将屏幕(
FrameworkElement
)作为报告打印
将在下方进行解释。
此代码可以编译成类库并添加到您的项目中,或者添加到您的 MVVM 框架中,以便您可以将此功能添加到所有 MVVM 项目中。在详细解释代码后,我将在一个小型的 MVVM 应用程序中使用这两者来演示如何将其集成到您的程序中。
1. 将屏幕保存为图像文件
带有打印或创建页面图像菜单选项的应用程序屏幕
用于创建屏幕/FrameworkElement
图像文件的代码从一个 Enum
开始,该 Enum
枚举了可用于保存图像的各种图像类型。下面列出了 5 种可用的图像类型。
ImageType 枚举
Private Enum GWSImageType
BMPImage
GIFImage
JPGImage
PNGImage
TIFImage
End Enum
这些 Enum
在后台用于区分每次方法调用。BMPImage
将创建一个“.bmp”位图文件。JPGImage
将创建一个“.jpg”JPEG 图形文件,依此类推...
每次方法调用都使用相同的签名(一个用于创建图像的框架元素,一个可选的双精度值,用于缩放图像大小)。
默认缩放值“1
”表示以实际大小创建图像,不进行缩放。值为“2
”将创建两倍大的图像,值为“.5
”将创建一半大小的图像。
我为 public
方法创建了一个接口,以便使用服务提供程序来查找可用的方法,稍后将详细介绍(MVVM 内容)。
创建 BitmapSource
文件的函数调用也被设为 public
,以便此函数可用于获取屏幕/FrameworkElement
的 BitmapSource
。然后,您可以使用此 BitmapSource
在图像控件中显示它。后面将展示一个使用此函数创建屏幕图像并将其用于工具提示的示例。该屏幕的示例出现在上方。
SaveImage 接口
Public Interface IGWSSaveImage
Sub CreateImageFileBMP(ByVal objControl As FrameworkElement, Optional ByVal Scale As Double = 1)
Sub CreateImageFileGIF(ByVal objControl As FrameworkElement, Optional ByVal Scale As Double = 1)
...
Function CreateBitmapSource(ByVal source As FrameworkElement, _
Optional ByVal scale As Double = 1) As BitmapSource
End Interface
每种图像类型的代码都相同,只是用于保存位图图像的编码器和文件名扩展名不同。每个方法都调用相同的过程,“XXX
”被替换为图像类型。
CreateImageFileXXX
Public Sub CreateImageFileXXX(ByVal source As FrameworkElement, Optional ByVal scale As Double = 1)
Dim _bm As BitmapSource = CreateBitmapSource(source, scale)
Dim _bmArray As Byte() = CreateImageByteArray(_bm, GWSImageType.XXXImage)
SaveImageFile(_bmArray, GWSImageType.XXXImage)
End Sub
上述方法中的三个步骤执行以下功能:
- 将
FrameworkElement
转换为BitmapSource
- 将
BitmapSource
转换为编码的字节数组 - 将字节数组保存到文件
1. 将 FrameworkElement 转换为 BitmapSource
将框架元素转换为位图源的基本代码直接来自 Microsoft 文档。我添加了缩放功能,以获得更小或更大的图像。
UIElement.Rendersize.Width
与 FrameworkElement.ActualWidth
(以及高度)相同,因此如果您想使用 UIElement
作为源对象,代码仍然有效。ActualWidth
和 ActualHeight
仅在 FrameworkElements
上找到,而在 UIElements
上找不到。
代码使用 VisualBrush
将源“绘制”到一个 DrawingContext Rectangle
中。
CreateBitmapSource 函数
Public Function CreateBitmapSource(ByVal source As FrameworkElement, _
Optional ByVal scale As Double = 1) As BitmapSource Implements IGWSSaveImage.CreateBitmapSource
Dim actualWidth As Double = _
source.RenderSize.Width 'same as source.actualwidth - rendersize also works with UIElements
Dim actualHeight As Double = _
source.RenderSize.Heigt 'same as source.actualHeight- rendersize also works with UIElements
Dim renderWidth As Double = actualWidth * scale
Dim renderHeight As Double = actualHeight * scale
Dim renderTarget As New RenderTargetBitmap(CInt(renderWidth), _
CInt(renderHeight), 96, 96, PixelFormats.[Default])
Dim sourceBrush As New VisualBrush(source)
Dim drawingVisual As New System.Windows.Media.DrawingVisual()
Dim drawingContext As DrawingContext = drawingVisual.RenderOpen()
Using drawingContext
drawingContext.PushTransform(New ScaleTransform(scale, scale))
drawingContext.DrawRectangle(sourceBrush, Nothing, _
New Rect(New Point(0, 0), New Point(actualWidth, actualHeight)))
End Using
renderTarget.Render(drawingVisual)
Return renderTarget
End Function
2. 将 BitmapSource 转换为编码的字节数组
一旦我们有了 BitmapSource
对象,我们就可以使用适当的编码器将其转换为字节数组,具体取决于我们想要接收的文件类型。
select case
语句根据函数调用的 ImageType
参数选择合适的编码器。一旦我们有了合适的编码器,我们就可以创建字节数组文件。
CreateImageByteArray 函数
Private Shared Function CreateImageByteArray_
(ByRef source As BitmapSource, ByVal imageType As GWSImageType) As Byte()
Dim _imageArray As Byte() = Nothing
Dim ImageEncoder As BitmapEncoder = Nothing
Select Case imageType
Case GWSImageType.BMPImage
ImageEncoder = New BmpBitmapEncoder()
...
ImageEncoder.Frames.Add(BitmapFrame.Create(source))
Using outputStream As New MemoryStream()
ImageEncoder.Save(outputStream)
_imageArray = outputStream.ToArray()
End Using
Return _imageArray
End Function
3. 将字节数组保存到文件
现在我们已经为所需的文件类型编码了字节数组,下一步就是将文件保存到磁盘。下面的代码使用 SaveFileDialog
来获取保存文件的路径和文件名。SaveFileDialog
通常是您的框架辅助方法的一部分,但为完整起见,我将其包含在此类中。
保存图像文件流
Private Shared Sub SaveImageFile(ByVal byteData As Byte(), ByRef imageType As GWSImageType)
Dim fileName As String
fileName = GetSaveImageDialog(imageType)
If String.IsNullOrEmpty(fileName) OrElse fileName.Trim() = String.Empty Then
Else
Try
Dim oFileStream As System.IO.FileStream
oFileStream = New System.IO.FileStream(fileName, System.IO.FileMode.Create)
oFileStream.Write(byteData, 0, byteData.Length)
oFileStream.Close()
MessageBox.Show("Window Image Saved. " & fileName, "Save Data", _
MessageBoxButton.OK, MessageBoxImage.Information)
Catch ex As Exception
MessageBox.Show("Error Saving Image to disk.", "Disk Error", _
MessageBoxButton.OK, MessageBoxImage.Stop)
End Try
End If
End Sub
现在我们已经将屏幕的图像保存为所选图像类型的文件。稍后,我们将演示如何在 MVVM 类型应用程序中使用此代码。
2. 将屏幕作为报告打印
带有打印页面菜单选项的应用程序屏幕
创建屏幕报告的代码与创建屏幕图像的代码类似。打印代码将屏幕图像作为视觉画笔(如上所述)并将其放入 Viewbox
中,然后对 Viewbox
进行操作和打印。该代码允许您选择报告的标题,以及设置打印选项,如纸张大小、方向、字体大小和拉伸报告大小的功能。
同样,我创建了一个接口来访问此类,以便使用服务提供程序来查找可用的方法,再次稍后将详细介绍(MVVM 内容)。
此外,PrintControl
类包含可以设置的 public
属性来格式化您的打印报告。这是接口代码和可以设置的属性。
PrintControl 接口
Public Interface IGWSPrintControl
Sub PrintUIElement(ByVal objPrint As UIElement)
Property PrintDescription As String
Property PrintTitle As String
Property PrintMargin As Double
Property PrintFontSize As Double
Property PrintFontWeight As FontWeight
Property PrintForeground As Media.Color
Property PrintStretch As Windows.Media.Stretch
End Interface
这些属性的目的不言自明。PrintControl
类首先为所有 public
属性设置默认值。PrintDescription
是打印队列中打印时显示的报告描述。
PrintTitle
是作为报告一部分打印的标题。它最初设置为空 string
,表示不显示标题。字体大小、字体粗细和媒体颜色仅适用于打印标题,而打印边距和媒体拉伸适用于整个报告。
默认公共属性值
Private _printDescription As String = "GWS Framework Print"
Private _printTitle As String = String.Empty
Private _printMargin As Double = 100
Private _printFontSize As Double = 36
Private _printFontWeight As FontWeight = FontWeights.Bold
Private _printForeground As Media.Color = Colors.Maroon
Private _printStretch As Windows.Media.Stretch = Stretch.Uniform
在 Print Control 类的主要方法调用中,我将传入一个 UIElement
而不是 Framework Element,以表明两者都可以使用。
主方法首先声明一个 PrintDialog
,它将弹出打印对话框窗口并允许您选择打印机、纸张大小、方向等。选择后,PrintDialog
将包含设置报告属性所需的属性。
以下是打印报告的步骤概述:
- 创建一个
PrintDialog
对象。 - 创建一个主网格并将其大小设置为
PrintableArea
。 - 向主网格添加一个
Viewbox
并设置其对齐方式。 - 向
Viewbox
添加一个新网格,包含 2 行。 - 将您的屏幕作为
VisualBrush
添加到底部行。 - 将您的标题文本添加到顶部行并设置其属性。
- 打印主网格。
首先,您可以看到声明了一个 PrintDialog
对象,并且 PrtDialog.ShowDialog
将弹出对话框供用户使用。如果用户选择打印按钮,ShowDialog
将返回 true
,PrintDialog
对象将填充用户的选择。我们可以从 PrintDialog
中获取 PrintableAreaWidth
和 PrintableAreaHeight
,并将其分配给我们的主网格(PageGrid
)的大小。
接下来,我们创建一个 Viewbox
并将其添加到我们的网格中。Viewbox
的关键在于拉伸设置,它允许您更改屏幕报告的大小。稍后在 MVVM 示例程序中,我将添加一个菜单项,用户可以在其中更改拉伸参数,然后再进行打印。
CreateUIElement 设置和参数
Private Sub CreateUIElement(ByVal objPrint As UIElement)
Dim PrtDialog As New PrintDialog
If PrtDialog.ShowDialog = True Then
Dim PageGrid As New Grid
PageGrid.Width = PrtDialog.PrintableAreaWidth
PageGrid.Height = PrtDialog.PrintableAreaHeight
Dim PageViewBox As New Viewbox
PageGrid.Children.Add(PageViewBox)
With PageViewBox
.HorizontalAlignment = Windows.HorizontalAlignment.Center
.VerticalAlignment = Windows.VerticalAlignment.Center
.Margin = New Thickness(Me.PrintMargin)
.Stretch = Me.PrintStretch
End With
...
我们创建一个矩形并将其设置为我们的屏幕对象的大小。我们将把这个矩形放入一个 Viewbox
中,以便在需要时进行调整。创建一个带有 2 行的网格。底部行的高度设置为“*”,以便它能占用显示 VisualBrush
图像所需的所有空间。
创建网格并分配 VisualBrush
...
Dim PrintRectangle As New Rectangle
PrintRectangle.Height = objPrint.RenderSize.Width
PrintRectangle.Width = objPrint.RenderSize.Width
Dim PrintGrid As New Grid
Dim PrintRow1 As New RowDefinition
Dim PrintRow2 As New RowDefinition
PrintRow1.Height = GridLength.Auto
PrintRow2.Height = New GridLength(1.0, GridUnitType.Star)
PrintGrid.RowDefinitions.Add(PrintRow1)
PrintGrid.RowDefinitions.Add(PrintRow2)
Dim PrintVisualBrush As New VisualBrush(objPrint)
PrintRectangle.Fill = PrintVisualBrush
...
我们创建一个 TextBlock
来保存报告标题,并根据此类 public
属性设置其属性。您可以在打印报告之前轻松更改每个属性的值。这将在下面的示例 MVVM 应用中进行演示。
创建标题文本
...
Dim PrintTextBlock As New TextBlock
With PrintTextBlock
.Text = Me.PrintTitle
.FontSize = Me.PrintFontSize
.FontWeight = Me.PrintFontWeight
.Foreground = New SolidColorBrush(Me.PrintForeground)
.VerticalAlignment = VerticalAlignment.Top
.HorizontalAlignment = HorizontalAlignment.Center
End With
Grid.SetRow(PrintTextBlock, 0)
Grid.SetRow(PrintRectangle, 1)
PrintGrid.Children.Add(PrintRectangle)
If Me.PrintTitle <> String.Empty Then
PrintGrid.Children.Add(PrintTextBlock)
End If
PageViewBox.Child = PrintGrid
...
必须在我们的网格上调用 Measure
和 Arrange
,否则报告将打印出空白页。完成这些操作后,只需将我们的网格发送到 PrintDialog.PrintVisual
方法即可。这就是我们使用报告的打印描述的地方。
测量、排列和打印
...
PageGrid.Measure(New Size(Double.PositiveInfinity, Double.PositiveInfinity))
PageGrid.Arrange(New Rect(0, 0, PrtDialog.PrintableAreaWidth, PrtDialog.PrintableAreaHeight))
Try
PrtDialog.PrintVisual(PageGrid, Me.PrintDescription)
Catch ex As Exception
Throw ex
End Try
...
就是这样。您现在可以打印或创建屏幕图像了。
为了使本文完整,我现在将把这些代码整合到一个示例 MVVM 项目中,以说明实现此代码的一种方法。
MVVM 示例项目
示例 WPF/MVVM 屏幕
概述
为了让您看到代码的实际效果,我创建了一个小型的 MVVM 风格项目,以展示如何将这两种实用工具集成到您的程序中。这不是对 WPF 或 MVVM 的解释。我只会解释与实现屏幕打印/图像捕获相关的代码。如果您想查看所有功能,完整的源代码包含在随附的下载文件中。
如上图所示,打印菜单允许用户在打印前选择拉伸设置。您也可以添加一个菜单选项让用户输入报告标题。我使用了 ViewModel
切换模式来导航程序中的屏幕。我在 Code Project 的上一篇文章中详细解释了 ViewModel
切换。
打下基础
这是与我上一篇文章中显示的相同简单的 MVVM 框架。我在这里重复它是为了完整性,并感谢编写原始代码的人。
首先 - 基础架构
如果您正在创建任何类型的 WPF 应用程序(从企业级解决方案到小型独立应用程序),使用 Model-View-ViewModel 模式是明智的。这不是一篇解释 MVVM 的文章,Code Project 上有很多可以参考的文章。使用 MVVM 时,使用框架来管理程序基础设施是值得的。市面上也有许多可供选择的框架。
我创建了一个简单的 MVVM 框架模块,为应用程序提供基本“基础架构”。该框架包含以下项目:
- RelayCommand.vb
- Messenger.vb
- ServiceContainer.vb
- IMessageBoxService.vb
- MessageBoxService.vb
- ServiceInjector.vb
- IScreen.vb
- BaseViewModel.vb
- IShowDialogService.vb
- ShowDialogService.vb
中继命令、消息传递器、IScreen
和服务提供程序来自 **Josh Smith** 的文章,而包含 INotifyPropertyChanged
逻辑的 BaseViewModel
类则来自 **Karl Shifflett** 在 Code Project 上的文章。此框架的所有代码都包含在项目源代码下载文件中。
整合
通过几个简单的步骤,就可以将 `Printing` 和 `ImageSave` 添加到我程序的所有屏幕中。由于我在所有 WPF/MVVM 程序中使用 `ViewModel` 切换,因此我的主窗口包含一个内容控件来显示当前 `ViewModel`。首先,我只需要给 `ContentControl`(`x:name = "MyView"`)命名,以便我可以将其传递给 `Print` 和 `SaveImage` 方法。
MainWindow XAML ContentControl
<ContentControl Content="{Binding Path=CurrentPageViewModel}"
Grid.Row="1"
x:Name="MyView"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" />
然后,我在 MainWindow
上创建两个菜单项。我将每个菜单项链接到一个命令,并将 ContentControl
作为命令参数传递。
MainWindow Xaml 菜单项
<MenuItem Header="_Print Page"
Command="{Binding Path=PrintCommand}"
CommandParameter="{Binding ElementName=MyView}">
...
<MenuItem Header="_Create Page Image"
Command="{Binding Path=SaveImageCommand}"
CommandParameter="{Binding ElementName=MyView}">
然后,在应用程序文件中,我使用 MVVM 框架中的 ServiceContainer
并为每个方法设置引用。这就是为什么我为两个类都创建了接口,如上文详细解释的。
Application.Xaml.VB 文件
Public Sub New()
...
ServiceContainer.Instance.AddService(Of IGWSPrintControl)(New GWSPrintControl)
ServiceContainer.Instance.AddService(Of IGWSSaveImage)(New GWSSaveImage)
...
End Sub
然后,每个菜单项将调用 ViewModel
上的 ICommand
。我将此代码放在每个 ViewModel
继承的 DisplayViewModel
中。每个 ICommand
都使用 Code Project 上 Josh Smith 文章中的标准 RelayCommand
。
每个命令都使用 MVVM 框架 ServiceContainer
来查找适当的方法。每个方法都将 ContentControl
作为参数传递,并使用默认属性进行打印或保存。
DisplayViewModel ICommands
Public Overridable ReadOnly Property PrintCommand() As ICommand
Get
Return New RelayCommand(Of UIElement)(AddressOf Me.Print)
End Get
End Property
Protected Overridable Sub Print(ByVal obj As UIElement)
Dim GWSPrintControl = MyBase.GetService(Of IGWSPrintControl)()
GWSPrintControl.PrintUIElement(obj)
End Sub
Public Overridable ReadOnly Property SaveImageCommand() As ICommand
Get
Return New RelayCommand(Of FrameworkElement)(AddressOf Me.ImageExecute)
End Get
End Property
Protected Overridable Sub ImageExecute(ByVal obj As FrameworkElement)
Dim GWSSaveImage = MyBase.GetService(Of IGWSSaveImage)()
GWSSaveImage.CreateImageFileJPG(obj)
End Sub
就是这样!该程序现在具有 `Print` 或 `SaveImage` 程序中每个屏幕的功能。
现在是一些锦上添花的功能
让我们更改 BoxGridViewModel 屏幕报告的标题、边距和拉伸。
对于每个 ViewModel,您可以覆盖 ICommand 并为该 ViewModel 设置属性。这将允许您更改每个屏幕的描述,并更改字体大小、颜色和边距。
由于 DisplayViewModel
上的每个 ICommand
都标记为 Overridable
,我们可以简单地在 CurrentPageViewModel
中 Override
ICommand
。然后,我们创建一个 PrintControl
对象,并为该 ViewModel
设置我们想要的任何属性。
带有覆盖的 BoxGridViewModel
Public Overrides ReadOnly Property PrintCommand() As ICommand
Get
Return New RelayCommand(Of UIElement)(AddressOf Me.Print)
End Get
End Property
Protected Overrides Sub Print(ByVal obj As UIElement)
Dim GWSPrintControl = MyBase.GetService(Of IGWSPrintControl)()
With GWSPrintControl
.PrintDescription = "Print Demo Description"
.PrintTitle = My.Application.Info.Description
.PrintStretch = Stretch.Fill
.PrintMargin = 50
End With
GWSPrintControl.PrintUIElement(obj)
End Sub
让我们更改 SaveImage 的文件类型并隐藏编辑屏幕上的 4 个按钮。.
屏幕上的一些信息您可能不希望显示在屏幕图像或打印件中。您可以在创建图像之前以任何方式更改屏幕,然后再将其恢复。
首先,我们在应用程序文件中声明 MVVM 框架的消息传递器,并设置两个常量 string
用作我们的消息。
Application.Xaml.vb 声明
Shared ReadOnly _messenger As New GwsMvvmFramework.Messenger
...
Friend Const MSG_CLEANUP_COMMAND As String = "Clean up"
Friend Const MSG_CLEANUP_RESET_COMMAND As String = "Clean up Reset"
...
Friend Shared ReadOnly Property Messenger() As Messenger
Get
Return _messenger
End Get
End Property
现在,当我们进行覆盖时,我们可以提前发送一条消息,告诉“监听者”执行某些操作(隐藏按钮)。然后,我们可以调用 CreateImageFilePNG
方法来生成 .png 输出文件。然后,我们发送另一条消息,告诉“监听者”执行另一个操作(取消隐藏按钮)。
EditRoundViewModel 覆盖
Protected Overrides Sub ImageExecute(ByVal obj As FrameworkElement)
Application.Messenger.NotifyColleagues(Application.MSG_CLEANUP_COMMAND)
Dim GWSSaveImage = MyBase.GetService(Of IGWSSaveImage)()
GWSSaveImage.CreateImageFilePNG(obj)
Application.Messenger.NotifyColleagues(Application.MSG_CLEANUP_RESET_COMMAND)
End Sub
在 EditRoundView
的代码隐藏文件中,我们注册接收两条消息,并为每个消息分配一个要执行的操作。第一条消息将隐藏按钮,第二条消息将恢复它们。按钮必须在 XAML 中具有 name 属性,以便可以从代码隐藏访问它们。所有这些代码隐藏代码都与 View 相关,并由消息传递器调用。
EditRoundView.Xaml.VB 代码隐藏文件
Partial Public Class EditRoundView
Public Sub New()
InitializeComponent()
Application.Messenger.Register_
(Application.MSG_CLEANUP_COMMAND, New Action(AddressOf Me.CleanUpScreenImage))
Application.Messenger.Register_
(Application.MSG_CLEANUP_RESET_COMMAND, New Action(AddressOf Me.CleanUpResetScreenImage))
End Sub
Private Sub CleanUpScreenImage()
AcceptButton.Visibility = Windows.Visibility.Hidden
CancelButton.Visibility = Windows.Visibility.Hidden
ApplyButton.Visibility = Windows.Visibility.Hidden
GetRnd.Visibility = Windows.Visibility.Hidden
End Sub
Private Sub CleanUpResetScreenImage()
AcceptButton.Visibility = Windows.Visibility.Visible
CancelButton.Visibility = Windows.Visibility.Visible
ApplyButton.Visibility = Windows.Visibility.Visible
GetRnd.Visibility = Windows.Visibility.Visible
End Sub
End Class
让我们创建屏幕的位图图像并将其用于工具提示。
这可能看起来不是最有用的功能,但它演示了如何获取屏幕的位图并将其用于您的程序。
在我们的 EditRoundViewModel
上,我们需要添加属性 get/set 来保存位图源。
EditRoundViewModel 属性
Public Property BitmapScreen As BitmapSource
Get
Return _BitmapScreen
End Get
Set(value As BitmapSource)
MyBase.SetPropertyValue("BitmapScreen", _BitmapScreen, value)
End Set
End Property
回想文章开头,我们将 CreateBitmapSource
方法声明为 public
,以便以后可以访问它。现在是时候了。将创建的位图源分配给我们的新 BitmapScreen
属性,现在我们的 ViewModel
中就有一个屏幕图像了。您可以在创建图像之前隐藏任何控件。请注意,我在 CreateBitmapSource
中将缩放设置为 0.35
以创建小的图像。
EditRoundViewModel ImageExecute 覆盖
Protected Overrides Sub ImageExecute(ByVal obj As FrameworkElement)
Application.Messenger.NotifyColleagues(Application.MSG_CLEANUP_COMMAND)
Dim GWSSaveImage = MyBase.GetService(Of IGWSSaveImage)()
GWSSaveImage.CreateImageFilePNG(obj)
BitmapScreen = GWSSaveImage.CreateBitmapSource(obj, 0.35)
Application.Messenger.NotifyColleagues(Application.MSG_CLEANUP_RESET_COMMAND)
End Sub
现在,我们只需在 XAML 文件中创建工具提示,并将其源设置为 ViewModel
中保存屏幕图像的 BitmapScreen
属性。然后,将工具提示分配给样式,并将其用于任何您想要的地方。
EditRoundViewModel XAML 工具提示
<Grid.Resources>
<ToolTip x:Key="tt">
<StackPanel Orientation="Vertical">
<TextBlock Text="Tool Tip with Image"></TextBlock>
<Image Source="{Binding BitmapScreen}"/>
</StackPanel>
</ToolTip>
...
<Style x:Key="TextBoxStyle1" TargetType="TextBox">
<Setter Property="TextBox.Margin" Value="5" />
<Setter Property="TextBox.TextAlignment" Value="Right" />
<Setter Property="ToolTip" Value="{StaticResource tt}"/>
</Style>
...
让我们允许用户从菜单项中选择打印拉伸设置。
这将展示如何让用户选择属性值来调整屏幕图像。虽然这将解释如何让用户选择拉伸值,但可以使用相同的逻辑来让用户选择其他属性。(字体大小、颜色等)
在 DisplayViewModel
上,我们需要设置一个 ICommand
来链接到我们将在稍后创建的 Stretch 菜单项。我们还需要一个 PrintStretch
属性来保存用户选择的拉伸值。我们需要确保 PrintControl
对象指向 Print Stretch 属性来获取其值。
DisplayViewModel 拉伸 ICommand 和属性
...
Public ReadOnly Property PrintStretchCommand() As ICommand
Get
Return New RelayCommand(Of Stretch)(AddressOf Me.PrintStretch)
End Get
End Property
Private Sub PrintStretch(ByVal obj As Stretch)
Print_Stretch = obj
End Sub
Public Property Print_Stretch() As Stretch
Get
Return _Print_Stretch
End Get
Set(value As Stretch)
_Print_Stretch = value
End Set
End Property
...
Protected Overridable Sub Print(ByVal obj As UIElement)
Dim GWSPrintControl = MyBase.GetService(Of IGWSPrintControl)()
With GWSPrintControl
.PrintDescription = "Print Demo Description"
.PrintTitle = "Control Printout"
.PrintStretch = Me.Print_Stretch
.PrintMargin = 50
End With
GWSPrintControl.PrintUIElement(obj)
End Sub
...
Stretch
是 Microsoft 提供的 Enum
。有 4 个可用值(Fill
、None
、Uniform
、UniformtoFill
)。由于它是一个 Enum
,我们可以用它来自动在 XAML 中创建我们的菜单项。我们需要确保在 XAML 中设置对 Mscorlib
和 PresentationCore
程序集的引用。然后,我们可以设置一个 ObjectDataProvider
来提供拉伸值。
MainWindowView XAML ObjectDataProvider
<Window x:Class="MainWindowView" Name="MyWin"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:PrintSaveDemo"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:prt="clr-namespace:System.Windows.Media;assembly=PresentationCore"
...
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/MainResources.xaml" />
</ResourceDictionary.MergedDictionaries>
<ObjectDataProvider x:Key="dataFromEnum1"
MethodName="GetValues"
ObjectType="{x:Type sys:Enum}">
<ObjectDataProvider.MethodParameters>
<x:Type TypeName="prt:Stretch" />
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
...
现在,我们可以为 Stretch
值创建菜单项,并将 ItemSource
设置为 ObjectDataProvider (x:Key=dataFromEnum1)
。要从菜单项中选择值,我使用的是一组互斥的单选按钮。StyleSelector
用于选择一个样式来显示一个已选中或未选中的 RadioButton
。
MainWindowView XAML 拉伸菜单和样式
<MenuItem.Resources>
<RadioButton x:Key="RadioMenu"
GroupName="RadioEnum"
x:Shared="False"
HorizontalAlignment="Center"
IsHitTestVisible="False"/>
<RadioButton x:Key="RadioMenuChecked"
GroupName="RadioEnum"
x:Shared="False"
HorizontalAlignment="Center"
IsHitTestVisible="False"
IsChecked="True"/>
<Style TargetType="MenuItem" x:Key="RadioUnCheckedStyle">
<Setter Property="Command"
Value="{Binding Path=DataContext.PrintStretchCommand,
RelativeSource={RelativeSource AncestorType={x:Type Window}}}" />
<Setter Property="CommandParameter" Value="{Binding}" />
<EventSetter Event="Click"
Handler="RadioEnum_Click" />
<Setter Property="Icon"
Value="{StaticResource RadioMenu}"/>
<Setter Property="StaysOpenOnClick" Value="True"/>
</Style>
<Style TargetType="MenuItem" x:Key="RadioCheckedStyle"
BasedOn="{StaticResource RadioUnCheckedStyle}">
<Setter Property="Icon"
Value="{StaticResource RadioMenuChecked}"/>
</Style>
</MenuItem.Resources>
<MenuItem Header="Print S_tretch"
Name="StretchEnum"
DataContext="{Binding}"
ItemsSource="
{Binding Source={StaticResource dataFromEnum1}}" >
...
<MenuItem.ItemContainerStyleSelector>
<local:RadioStyleSelector UnCheckedStyle="{StaticResource RadioUnCheckedStyle}"
CheckedStyle="{StaticResource RadioCheckedStyle}" />
</MenuItem.ItemContainerStyleSelector>
</MenuItem>
为了确保您可以单击 MenuItem
而不是仅单击 RadioButton
,需要在代码隐藏文件中添加以下代码。同样,这都是 View 相关的代码。
MainWindow.Xaml.Vb 点击事件
Partial Public Class MainWindowView
Public Sub New()
InitializeComponent()
End Sub
...
#Region "Methods"
Private Sub RadioEnum_Click(sender As Object, e As System.Windows.RoutedEventArgs)
'This solution is from a stackoverflow answer: http://stackoverflow.com/a/11497189/375727
Dim mitem As MenuItem = TryCast(sender, MenuItem)
If mitem IsNot Nothing Then
Dim rbutton As RadioButton = TryCast(mitem.Icon, RadioButton)
If rbutton IsNot Nothing Then
rbutton.IsChecked = True
End If
End If
End Sub
#End Region
End Class
好了。一个功能齐全的 WPF/MVVM 程序,能够打印或捕获每个屏幕。
*卡通图像来自 Xamalot.com 免费剪贴画。