Windows 10 UWP 11 of N: Printing API with ItemsCollection control

  • 627
  • 0
  • UAP
  • 2021-04-30

實作Print列印功能在Universal Windows App!

今天要介紹的是Windows 10的列印文件(Print API)在UWP上如何實作!

從8.1開始就已經支援的Print API到底有甚麼變化呢?在8.1上只能在平板或是PC上使用Print,但在Windows 10的UWP上也延伸到了Mobile!

那...到底要怎樣實作Print API呢?先看以下的圖片展示Print的框架

這次範例使用的是Print API銜接 XAML的Render部分!

Windows Mobile的列印功能又與Desktop版本不同!需要採用的是Network的連線方式,因此Printer Driver的實際驅動安裝是採用Install on demand!

先說明一下目前8.1以及10的Print差異,之前在使用8.1的Store的流程會從Charms Bar呼叫Device(裝置)接者按下Print(列印)才會顯示印表的預覽畫面。

第二點就是PrintManagerShowPrintUIAsync需要放在Try-Catch的程式碼區塊內!如果裝置安裝Driver或是不支援的狀況導致無法只用至少在Print的Dialog會優雅的關閉!(Graceful fail)

接者開始實作Print的Code啦,這邊我採用的一樣是MVVM的方式來進行APP的開發。

先是實作Model的方式

public class SampleModel : INotifyPropertyChanged
    {
        private string _Title;

        public string Title
        {
            get { return _Title; }
            set { _Title = value; NotifyChange(); }
        }

        private string _Description;

        public string Description
        {
            get { return _Description; }
            set { _Description = value; }
        }

        private string _ImageSrcUri;

        public string ImageSrcUri
        {
            get { return _ImageSrcUri; }
            set { _ImageSrcUri = value; NotifyChange(); }
        }


        private ImageSource _ImageSrc;

        public ImageSource ImageSrc
        {
            get { return _ImageSrc; }
            set { _ImageSrc = value; NotifyChange(); }
        }

        public async Task LoadImageAsync()
        {
            if(!string.IsNullOrEmpty(ImageSrcUri) && !string.IsNullOrWhiteSpace(ImageSrcUri))
            {
                var storageFile = await StorageFile.GetFileFromApplicationUriAsync(new Uri(ImageSrcUri));
                using (var stream = await storageFile.OpenReadAsync())
                {
                    var bitmapImg = new BitmapImage();
                    await bitmapImg.SetSourceAsync(stream);
                    _ImageSrc = bitmapImg;
                }
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
        protected void NotifyChange([CallerMemberName]string propertyName = "")
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }

中間大家可以看到有個LoadImage的Method,後續的Code會解釋為什麼要在Model做這個功能!

接者是實作ViewModel的部分,請看以下程式碼

namespace PrintUAP.ViewModel
{
    public class MainPageViewModel
    {
        private ObservableCollection<SampleModel> _SampleList;
        public ObservableCollection<SampleModel> SampleList
        {
            get { return _SampleList; }
        }

        private PrintDocument printDoc;
        private PrintManager printMgr;

        public MainPageViewModel()
        {
            _SampleList = new ObservableCollection<SampleModel>(Enumerable.Range(0, 10).Select(i => new SampleModel()
            {
                Title = string.Format("Title - {0}", i),
                Description = string.Format("ABCDEFGHIJKLMNOPQRSTUVWXYZ,This is description of page {0}", i),
                ImageSrcUri = string.Format("ms-appx:///Assets/CKONE.jpg")
            }));
        }

        public void RegistrationPrintManager()
        {
            printMgr = PrintManager.GetForCurrentView();
            printMgr.PrintTaskRequested += MainPage_PrintTaskRequested;
        }

        private void MainPage_PrintTaskRequested(PrintManager sender, PrintTaskRequestedEventArgs args)
        {
            var deferral = args.Request.GetDeferral();
            var printTask = args.Request.CreatePrintTask("SamplePrintTitle", new PrintTaskSourceRequestedHandler(PrintTaskHandler));
            deferral.Complete();
        }

        private async void PrintTaskHandler(PrintTaskSourceRequestedArgs args)
        {
            var deferral = args.GetDeferral();
            await Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
            {
                printDoc = new PrintDocument();
                printDoc.AddPages += PrintDoc_AddPages;
                printDoc.Paginate += PrintDoc_Paginate;
                printDoc.GetPreviewPage += PrintDoc_GetPreviewPage;
                args.SetSource(printDoc.DocumentSource);
            });
            deferral.Complete();
        }

        private async void PrintDoc_GetPreviewPage(object sender, GetPreviewPageEventArgs e)
        {
            await Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, async () =>
            {
                printDoc.SetPreviewPage(e.PageNumber, await BuildVisual(e.PageNumber));
            });
        }

        private void PrintDoc_Paginate(object sender, PaginateEventArgs e)
        {
            printDoc.SetPreviewPageCount(SampleList.Count, PreviewPageCountType.Final);
        }

        private async void PrintDoc_AddPages(object sender, AddPagesEventArgs e)
        {
            await Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, async () =>
            {
                printDoc.AddPage(await BuildVisual(0));
                printDoc.AddPagesComplete();
            });
        }

        private async Task<UIElement> BuildVisual(int pageNumber)
        {
            var pageVisual = new Grid();
            var mainPage = (Window.Current.Content as Frame).Content as MainPage;
            var contentGrid = mainPage.Content as Grid;
            var richTb = contentGrid.Children.FirstOrDefault() as RichTextBlock;
            var UIContainer = (richTb.Blocks.FirstOrDefault() as Paragraph).Inlines.FirstOrDefault() as InlineUIContainer;
            var renderCtrl = UIContainer.Child as ItemsControl;
            if ((pageNumber - 1) >= 0)
            {
                var dataContext = renderCtrl.Items[pageNumber - 1] as SampleModel;
                await dataContext.LoadImageAsync();
                var elementItem = renderCtrl.ContainerFromItem(dataContext) as ListViewItem;
                var presenter = new ContentPresenter();
                presenter.ContentTemplate = elementItem.ContentTemplate;
                var uiElement = presenter.ContentTemplate.LoadContent() as UIElement;
                (uiElement as FrameworkElement).DataContext = dataContext;
                ((uiElement as Grid).Children.FirstOrDefault() as Image).Source = dataContext.ImageSrc;
                pageVisual.Children.Add(uiElement);
            }
            return pageVisual;
        }

        public void UnRegistrationPrintManager()
        {
            if (printMgr != null)
            {
                printMgr.PrintTaskRequested -= MainPage_PrintTaskRequested;
            }
        }
    }
}

這邊就是把SampleList建立起來,然後藉由Registration和UnRegistration將Printer的Pipeline的TaskRequest指派給EventHandler或是沒有。

然後CreatePrintTask設定顯示Print task的Name以及後續處裡的Handler,然後再PrintTaskHandler裡面開始建立PrintDocument!這個物件就是建立Print的內容(輸出給印表機的主要物件),並且委派三個重要的Event(AddPages,Paginate,GetPreviewPage)

AddPages->是加入多個Page在列印的文件中,每次的列印流程都是有一個PrintDocument但該Document可以含有多個Page!

Paginate->是設定預覽畫面的數量並且顯示在預覽視窗的UI上。

GetPreviewPage->顯示單一預覽頁面時候會觸發的事件。

以上三個事件除了Paginate以外都需要用到UI Thread!但由於採用的是MVVM的開發方式所以要用MainView.CoreWindow.Dispatcher來取代在Page.cs可以呼叫的Dispatcher。

接者到App.xaml上面先定義MainPageViewModel的Resource來Binding MainPage的DataContext。

<Application
    x:Class="PrintUAP.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:PrintUAP"
    xmlns:vm="using:PrintUAP.ViewModel">

    <Application.Resources>
        <vm:MainPageViewModel x:Key="MainPageVM"/>
    </Application.Resources>
    
</Application>

接者在MainPage.xaml將變更DataContext以及Content,如下Code。當然如果要使用Compile binding一樣可以將Binding換掉,請參考前幾篇的UWP文章

<Page
    x:Class="PrintUAP.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:PrintUAP"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    DataContext="{Binding Source={StaticResource MainPageVM}}">

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <RichTextBlock Margin="20">
            <Paragraph>
                <InlineUIContainer>
                    <ListView ItemsSource="{Binding SampleList}" SelectionMode="None" IsItemClickEnabled="False">
                        <ListView.ItemTemplate>
                            <DataTemplate>
                                <Grid>
                                    <Grid.RowDefinitions>
                                        <RowDefinition Height="Auto"/>
                                        <RowDefinition Height="Auto"/>
                                    </Grid.RowDefinitions>
                                    <Grid.ColumnDefinitions>
                                        <ColumnDefinition Width="200"/>
                                        <ColumnDefinition Width="*"/>
                                    </Grid.ColumnDefinitions>
                                    <Image Grid.Row="0" Grid.Column="0" Source="{Binding ImageSrcUri}"/>
                                    <TextBlock Grid.Row="0" Grid.Column="1" Style="{ThemeResource HeaderTextBlockStyle}" Text="{Binding Title}" Foreground="Green" TextTrimming="WordEllipsis" TextWrapping="Wrap"/>
                                    <TextBlock Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" Style="{ThemeResource SubheaderTextBlockStyle}" Text="{Binding Description}" Foreground="Red" TextTrimming="WordEllipsis" TextWrapping="Wrap"/>
                                </Grid>
                            </DataTemplate>
                        </ListView.ItemTemplate>
                        <ListView.ItemContainerStyle>
                            <Style TargetType="ListViewItem">
                                <Setter Property="Template">
                                    <Setter.Value>
                                        <ControlTemplate TargetType="ListViewItem">
                                            <ListViewItemPresenter SelectionCheckMarkVisualEnabled="False"/>
                                        </ControlTemplate>
                                    </Setter.Value>
                                </Setter>
                            </Style>
                        </ListView.ItemContainerStyle>
                    </ListView>
                </InlineUIContainer>
            </Paragraph>
        </RichTextBlock>
    </Grid>

    <Page.BottomAppBar>
        <CommandBar>
            <CommandBar.PrimaryCommands>
                <AppBarButton Icon="Add" Label="print" Click="AppBarButton_Click"/>
            </CommandBar.PrimaryCommands>
        </CommandBar>
    </Page.BottomAppBar>
</Page>

這邊主要是將ListView的UI長出來,並且使用AppBarButton呼叫PrintManager 顯示列印UI的部分。

接下來在MainPage.xaml.cs加入以下的Code

public MainPageViewModel MainPageVM;
        public MainPage()
        {
            this.InitializeComponent();
            MainPageVM = this.DataContext as MainPageViewModel;
            this.Loaded += MainPage_Loaded;
            this.Unloaded += MainPage_Unloaded;
        }

        private void MainPage_Loaded(object sender, RoutedEventArgs e)
        {
            MainPageVM.RegistrationPrintManager();
        }

        private void MainPage_Unloaded(object sender, RoutedEventArgs e)
        {
            MainPageVM.UnRegistrationPrintManager();
        }

        private async void AppBarButton_Click(object sender, RoutedEventArgs e)
        {
            await PrintManager.ShowPrintUIAsync();
        }

在Win8.1顯示Preview的畫面

Win10的顯示列印預覽模式

 


***以上Code以及說明都有可能隨著Windows 10 的版本以及Visual Studio 2015版本有所調整!***

參考資料 MSDN, Printing: Build 2015 Developing Apps That Print in Windows 10 ( 2-94 )

下次再分享Windows 10 的新技術拉~