从应用打印

本主题介绍如何从 Windows 应用打印。

有关更高级的功能,请参阅 自定义打印预览 UI

注册打印

向应用添加打印的第一步是通过获取当前窗口的 PrintManager 对象来注册打印。 PrintManager 类负责协调应用的打印流。 若要使用此类,必须首先调用返回特定于当前活动窗口的 PrintManager 对象的方法。

你的应用必须在希望用户能够打印的每个屏幕上执行此操作。 只有向用户显示的屏幕才能注册打印。 如果应用的一个屏幕已注册打印,则必须在退出时取消注册打印。 如果它被另一个屏幕替换,则下一个屏幕必须在打开时注册打印。

提示

 如果需要支持从应用中的多个页面进行打印,可以将此打印代码放在一个常见的帮助程序类中,并使应用页面重复使用它。 有关如何执行此操作的示例,请参阅 PrintHelper UWP 打印示例中类。

用户启动打印后,可以使用 PrintDocument 准备要发送到打印机的页面。该 PrintDocument 类型位于 Microsoft.UI.Xaml.Printing 命名空间中,以及支持准备 XAML 内容进行打印的其他类型。

PrintDocument 类用于处理应用与 PrintManager 之间的大部分交互,但它公开了其自身的多个回调。 在注册期间,为其打印事件创建和注册处理程序的实例 PrintManagerPrintDocument 注册处理程序。

在此示例中,注册在方法中 RegisterForPrinting 执行,该方法是从页面的 加载 事件处理程序调用的。

using Microsoft.UI.Xaml.Printing;
using Windows.Graphics.Printing;

PrintDocument printDocument = null;
IPrintDocumentSource printDocumentSource = null;
List<UIElement> printPreviewPages = new List<UIElement>();

private void MainPage_Loaded(object sender, RoutedEventArgs e)
{
    RegisterForPrinting();
}

private void RegisterForPrinting()
{
    var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(App.MainWindow);
    PrintManager printManager = PrintManagerInterop.GetForWindow(hWnd);
    printManager.PrintTaskRequested += PrintTask_Requested;

    printDocument = new PrintDocument();
    printDocumentSource = printDocument.DocumentSource;
    printDocument.Paginate += PrintDocument_Paginate;
    printDocument.GetPreviewPage += PrintDocument_GetPreviewPage;
    printDocument.AddPages += PrintDocument_AddPages;
}

警告

在 UWP 打印示例中,建议注册从 OnNavigatedTo 方法重写进行打印。 在非 UWP 应用中,需要使用 PrintManagerInterop.GetForWindow 调用中的窗口句柄,因此应使用加载事件来确保窗口句柄不是 null,这可能是 OnNavigatedTo 中的情况。

此处,在方法中 UnregisterForPrinting 取消注册事件处理程序,该方法从 OnNavigatedFrom 方法调用。

protected override void OnNavigatedFrom(NavigationEventArgs e)
{
    base.OnNavigatedFrom(e);
    UnregisterForPrinting();
}


private void UnregisterForPrinting()
{
    if (printDocument == null)
    {
        return;
    }

    printDocument.Paginate -= PrintDocument_Paginate;
    printDocument.GetPreviewPage -= PrintDocument_GetPreviewPage;
    printDocument.AddPages -= PrintDocument_AddPages;

    var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(App.MainWindow);
    PrintManager printManager = PrintManagerInterop.GetForWindow(hWnd);
    printManager.PrintTaskRequested -= PrintTask_Requested;
}

注意

如果你有一个多页应用并且未断开打印连接,则当用户离开页然后返回到该页时会引发异常。

创建打印按钮

将打印按钮添加到应用屏幕中,你希望显示该按钮。 请确保它不会干扰要打印的内容。

<Button x:Name="InvokePrintingButton"
        Content="Print"
        Click="InvokePrintingButton_Click"/>

在 Button 的 Click 事件处理程序中,向用户显示 Windows 打印 UI。

var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(App.MainWindow);
await PrintManagerInterop.ShowPrintUIForWindowAsync(hWnd);

此方法是显示相应打印窗口的异步方法,因此需要将异步关键字添加到 Click 处理程序。 我们建议先调用 IsSupported 方法,以检查应用是否在支持打印的设备上运行(并处理不打印的情况)。 如果当时无法出于任何其他原因执行打印,该方法将引发异常。 我们建议捕获这些异常,并让用户知道打印何时无法继续。

在此示例中,按钮单击的事件处理程序中会显示打印窗口。 如果方法引发异常(因为当时无法执行打印), ContentDialog 控件会通知用户情况。

private async void InvokePrintingButton_Click(object sender, RoutedEventArgs e)
{
    if (PrintManager.IsSupported())
    {
        try
        {
            // Show system print UI.
            var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(App.MainWindow);
            await Windows.Graphics.Printing.PrintManagerInterop.ShowPrintUIForWindowAsync(hWnd);
        }
        catch
        {
            // Printing cannot proceed at this time.
            ContentDialog noPrintingDialog = new ContentDialog()
            {
                Title = "Printing error",
                Content = "\nSorry, printing can' t proceed at this time.",
                PrimaryButtonText = "OK"
            };
            await noPrintingDialog.ShowAsync();
        }
    }
    else
    {
        // Printing is not supported on this device.
        ContentDialog noPrintingDialog = new ContentDialog()
        {
            Title = "Printing not supported",
            Content = "\nSorry, printing is not supported on this device.",
            PrimaryButtonText = "OK"
        };
        await noPrintingDialog.ShowAsync();
    }
}

设置应用内容的格式

调用时 ShowPrintUIForWindowAsync将引发 PrintTaskRequested 事件。 在事件处理程序中PrintTaskRequested,通过调用 PrintTaskRequest.CreatePrintTask 方法创建 PrintTask。 传递打印页的标题和 PrintTaskSourceRequestedHandler 委托的名称。 标题显示在打印预览 UI 中。 PrintTask PrintDocument提供PrintTaskSourceRequestedHandler内容的链接。

在此示例中,还定义了一个完成处理程序来捕获错误。 处理完成事件是个好主意,因为应用可以让用户知道是否发生了错误并提供可能的解决方案。 同样,你的应用可以使用完成事件来指示用户在打印作业成功后要执行的后续步骤。

private void PrintTask_Requested(PrintManager sender, PrintTaskRequestedEventArgs args)
{
    // Create the PrintTask.
    // Defines the title and delegate for PrintTaskSourceRequested.
    PrintTask printTask = args.Request.CreatePrintTask("WinUI 3 Printing example", PrintTaskSourceRequested);

    // Handle PrintTask.Completed to catch failed print jobs.
    printTask.Completed += PrintTask_Completed;

    DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Normal, () =>
    {
        InvokePrintingButton.IsEnabled = false;
    });
}

private void PrintTaskSourceRequested(PrintTaskSourceRequestedArgs args)
{
    // Set the document source.
    args.SetSource(printDocumentSource);
}

private void PrintTask_Completed(PrintTask sender, PrintTaskCompletedEventArgs args)
{
    string StatusBlockText = string.Empty;

    // Notify the user if the print operation fails.
    if (args.Completion == PrintTaskCompletion.Failed)
    {
        StatusBlockText = "Failed to print.";
    }
    else if (args.Completion == PrintTaskCompletion.Canceled)
    {
        StatusBlockText = "Printing canceled.";
    }
    else
    {
        StatusBlockText = "Printing completed.";
    }


    DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Normal, () =>
    {
        StatusBlock.Text = StatusBlockText;
        InvokePrintingButton.IsEnabled = true;
    });
}

创建打印任务后, PrintManager 会通过引发 分页 事件请求打印页面集合以在打印预览 UI 中显示。 (这与 Paginate 接口的方法 IPrintPreviewPageCollection 相对应。此时会调用注册期间创建的事件处理程序。

重要

 如果用户更改打印设置,将再次调用分页事件处理程序,以便重新排列内容。 为获得最佳用户体验,建议在重新排列内容之前检查设置,避免在不需要时重新初始化分页内容。

分页 事件处理程序中,创建页面以在打印预览 UI 中显示并发送到打印机。 用于准备应用内容以供打印的代码特定于你的应用和打印的内容。

此示例演示了创建单个打印页面的基本步骤,该页面从屏幕上显示的页面打印图像和标题。

  • 创建一个列表来保存要打印的 UI 元素(页面)。
  • 清除预览页列表,以便每次进行分页时不会复制页面。
  • 使用 PrintPageDescription 获取打印机页的大小。
  • 设置 XAML 内容的格式以适应打印机页面。 要打印的每个页面都是一个 XAML UI 元素(通常是包含其他内容的容器元素)。 在此示例中,元素是在代码中创建的,并使用与屏幕上显示的元素相同的数据。
  • 根据需要将内容流到其他页面上。 本基本示例中未显示多个页面,但将内容划分为页面是分页事件的重要组成部分。
  • 将每页添加到要打印的页面列表中。
  • 设置 PrintDocument 上的预览页计数。
List<UIElement> printPreviewPages = new List<UIElement>();

private void PrintDocument_Paginate(object sender, PaginateEventArgs e)
{
    // Clear the cache of preview pages.
    printPreviewPages.Clear();

    // Get the PrintTaskOptions.
    PrintTaskOptions printingOptions = ((PrintTaskOptions)e.PrintTaskOptions);
    // Get the page description to determine the size of the print page.
    PrintPageDescription pageDescription = printingOptions.GetPageDescription(0);

    // Create the print layout.
    StackPanel printLayout = new StackPanel();
    printLayout.Width = pageDescription.PageSize.Width;
    printLayout.Height = pageDescription.PageSize.Height;
    printLayout.BorderBrush = new Microsoft.UI.Xaml.Media.SolidColorBrush(Microsoft.UI.Colors.Black);
    printLayout.BorderThickness = new Thickness(48);

    Image printImage = new Image();
    printImage.Source = printContent.Source;

    printImage.Width = pageDescription.PageSize.Width / 2;
    printImage.Height = pageDescription.PageSize.Height / 2;

    TextBlock imageDescriptionText = new TextBlock();
    imageDescriptionText.Text = imageDescription.Text;
    imageDescriptionText.FontSize = 24;
    imageDescriptionText.HorizontalAlignment = HorizontalAlignment.Center;
    imageDescriptionText.Width = pageDescription.PageSize.Width / 2;
    imageDescriptionText.TextWrapping = TextWrapping.WrapWholeWords;

    printLayout.Children.Add(printImage);
    printLayout.Children.Add(imageDescriptionText);

    // Add the print layout to the list of preview pages.
    printPreviewPages.Add(printLayout);

    // Report the number of preview pages created.
    PrintDocument printDocument = (PrintDocument)sender;
    printDocument.SetPreviewPageCount(printPreviewPages.Count,
                                          PreviewPageCountType.Intermediate);
}

下面是应用 UI 的屏幕截图,以及内容在打印预览 UI 中的显示方式。

系统打印预览 UI 旁边的应用 UI 的屏幕截图,其中显示了要打印的图像和标题。

当特定页面显示在打印预览窗口中时, PrintManager引发 GetPreviewPage 事件。 这与 MakePage 接口的方法 IPrintPreviewPageCollection 相对应。 此时会调用注册期间创建的事件处理程序。

GetPreviewPage 事件处理程序中,在打印文档中设置相应的页面。

private void PrintDocument_GetPreviewPage(object sender, GetPreviewPageEventArgs e)
{
    PrintDocument printDocument = (PrintDocument)sender;
    printDocument.SetPreviewPage(e.PageNumber, printPreviewPages[e.PageNumber - 1]);
}

最后,用户单击打印按钮后, PrintManager 会通过调用 MakeDocument 接口的方法 IDocumentPageSource 请求要发送到打印机的页面的最终集合。 在 XAML 中,这会引发 AddPages 事件。 此时会调用注册期间创建的事件处理程序。

AddPages 事件处理程序中,将页面集合中的页面添加到 要发送到打印机的 PrintDocument 对象。 如果用户指定要打印的特定页面或页面范围,请使用此处的信息仅添加实际发送到打印机的页面。

private void PrintDocument_AddPages(object sender, AddPagesEventArgs e)
{
    PrintDocument printDocument = (PrintDocument)sender;

    // Loop over all of the preview pages and add each one to be printed.
    for (int i = 0; i < printPreviewPages.Count; i++)
    {
        printDocument.AddPage(printPreviewPages[i]);
    }

    // Indicate that all of the print pages have been provided.
    printDocument.AddPagesComplete();
}