Печать календаря в WinRT

77

Работая над большим проектом, я часто вешаю на стену распечатанные календари. Распечатка выглядит предельно просто - ничего, кроме большого количества свободных ячеек для записи того, что мне нужно сделать каждый день.

Программа PrintMonthlyPlanner печатает календари на месяцы из диапазона заданного пользователем. Главная страница выглядит так:

Приложение для печати календарей

Месяц и год выбираются при помощи элемента управления FlipView. Кнопка доступна только в том случае, если начальный месяц меньше либо равен конечному. Обработчик Click кнопки реализуется всего одной строкой кода:

await PrintManager.ShowPrintUIAsync();

Хотя обычно пользователь вызывает чудо-кнопки и панель "Устройства", программа может сделать то же самое. Как правило, этот вариант используется для программ, печатающих только в особых ситуациях. Интересно, что панель, вызываемая ShowPrintUIAsync, несколько отличается от панели чудо-кнопки "Устройства":

Всплывающая панель при щелчке по кнопке Устройства

Поскольку программа PrintMonthlyPlanner предназначена исключительно для печати, выводимые ей страницы не отображаются программой и видны на экране только на панели печати:

Печать календаря

Обратите внимание на выбор альбомной ориентации (Landscape). Программа задает это начальное значение в предположении, что страницы календаря лучше форматируются в альбомном режиме. Каждая страница печатается до самой границы печатаемой области листа.

Я создал пользовательский элемент управления для выбора месяца и года. Этот элемент управления называется MonthYearSelect, а его файл XAML состоит из двух шаблонных элементов управления FlipView, у которых в качестве ItemsPanel используется горизонтальная панель StackPanel:

<UserControl ...>
    
    <UserControl.Resources>
        <Style TargetType="FlipView">
            <Setter Property="ItemsPanel">
                <Setter.Value>
                    <ItemsPanelTemplate>
                        <StackPanel Orientation="Vertical" />
                    </ItemsPanelTemplate>
                </Setter.Value>
            </Setter>
            
            <Setter Property="ItemTemplate">
                <Setter.Value>
                    <DataTemplate>
                        <TextBlock Text="{Binding}" VerticalAlignment="Center" />
                    </DataTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </UserControl.Resources>
    
    <Grid>
        <StackPanel Orientation="Horizontal">
            <FlipView x:Name="monthFlipView" 
                      SelectionChanged="yearFlipView_SelectionChanged" />
            
            <TextBlock Text="&#x00A0;" />
            
            <FlipView x:Name="yearFlipView"
                      SelectionChanged="yearFlipView_SelectionChanged" />   
        </StackPanel>
    </Grid>
</UserControl>

Отчасти для того, чтобы использовать новые возможности Windows Runtime, я выбрал в качестве открытого интерфейса к этому классу объект Calendar (вместо традиционного объекта .NET DateTime). Я надеялся сделать программу подходящей для календарей любого типа, но класс Calendar недостаточно хорошо документирован для выхода за рамки обычного грегорианского календаря. Мне даже не удалось понять, обязательно ли неделя начинается с воскресенья (стандарт в большинстве западных стран), или же она может начинаться с понедельника (как, например, в России).

Также выяснилось, что Calendar представляет собой класс, а не структуру, и меня стало беспокоить создание новых объектов Calendar при каждом переключении FlipView. Я решил, что элемент управления будет создавать всего один объект Calendar, изменяя свойства Month и Year этого единственного объекта.

Но в этом случае предоставлять доступ к Calendar как к свойству зависимости не было смысла, поэтому свойство типа Calendar представляет собой обычное свойство с именем MonthYear, дополненное событием MonthYearChanged для обозначения новых значений Month и Year:

using System;
using Windows.Globalization;
using Windows.Globalization.DateTimeFormatting;
using Windows.UI.Xaml.Controls;

namespace PrintMonthlyPlanner
{
    public sealed partial class MonthYearSelect : UserControl
    {
        public event EventHandler MonthYearChanged;

        public MonthYearSelect()
        {
            this.InitializeComponent();

            // Создание объекта Calendar с текущей датой
            Calendar calendar = new Calendar();
            calendar.SetToNow();

            // Заполнение первого элемента управления FlipView
            // сокращенными названиями месяцев
            DateTimeFormatter monthFormatter = 
                new DateTimeFormatter(YearFormat.None, MonthFormat.Abbreviated, 
                                      DayFormat.None, DayOfWeekFormat.None);

            for (int month = 1; month <= 12; month++)
            {
                string strMonth = monthFormatter.Format(
                                    new DateTimeOffset(2000, month, 15, 0, 0, 0, TimeSpan.Zero));
                monthFlipView.Items.Add(strMonth);
            }

            // Заполнение второго элемента управления FlipView
            // годами (5 лет до текущего года, 25 после)
            for (int year = calendar.Year - 5; year <= calendar.Year + 25; year++)
            {
                yearFlipView.Items.Add(year);
            }

            // Инициализация элементов управления FlipView
            monthFlipView.SelectedIndex = calendar.Month - 1;
            yearFlipView.SelectedItem = calendar.Year;
            this.MonthYear = calendar;
        }

        public Calendar MonthYear { private set; get; }

        private void yearFlipView_SelectionChanged(object sender, 
            SelectionChangedEventArgs e)
        {
            if (this.MonthYear == null)
                return;
            
            if (monthFlipView.SelectedIndex != -1)
                this.MonthYear.Month = (int)monthFlipView.SelectedIndex + 1;

            if (yearFlipView.SelectedIndex != -1)
                this.MonthYear.Year = (int)yearFlipView.SelectedItem;

            // Инициирование события
            if (MonthYearChanged != null)
                MonthYearChanged(this, EventArgs.Empty);
        }
    }
}

Файл MainPage.xaml создает экземпляры двух элементов управления MonthYearSelect:

<Page FontSize="48" ... >

    <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
        <Grid HorizontalAlignment="Center"
              VerticalAlignment="Center">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="Auto" />
            </Grid.ColumnDefinitions>
            
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
            
            <local:MonthYearSelect x:Name="monthYearSelect1"
                                   Grid.Row="0" Grid.Column="0"
                                   Height="144"
                                   VerticalAlignment="Center"
                                   MonthYearChanged="OnMonthYearChanged" />
            
            <TextBlock Text=" -&#x00A0;" 
                       Grid.Row="0" Grid.Column="1"
                       VerticalAlignment="Center" />

            <Button Name="printButton"
                    Content="Распечатать 1 месяц"
                    Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3"
                    FontSize="24"
                    HorizontalAlignment="Center"
                    Margin="0 24"
                    Click="OnPrintButtonClick" />

            <local:MonthYearSelect x:Name="monthYearSelect2"
                                   Grid.Row="0" Grid.Column="2"
                                   Height="144"
                                   VerticalAlignment="Center"
                                   MonthYearChanged="OnMonthYearChanged" />
        </Grid>
    </Grid>
</Page>

Для того, чтобы календарь отображался на русском языке, в файле конфигурации App.xaml.cs необходимо явно задать культуру для всего приложения как "ru-RU":

namespace WinRTTestApp
{
    sealed partial class App : Application
    {
        public App()
        {
            this.InitializeComponent();
            this.Suspending += OnSuspending;

            Windows.Globalization.ApplicationLanguages.PrimaryLanguageOverride = "ru-RU";
        }
        
        // ...
    }
}

Эта программа несколько отличается от других программ ранее тем, что печать в ней не остается включенной во время работы приложения. Печать включается только при вводе в двух элементах управления MonthYearSelect диапазона месяцев. С каждым изменением этих двух элементов управления программа должна сгенерировать новую надпись для кнопки; определить, должна ли кнопка быть заблокированной или разблокированной; и определить, нужно ли присоединить или отсоединить обработчик PrintTaskRequested. Большая часть этой логики сосредоточена в начальной секции класса MainPage:

using System;
using System.Collections.Generic;
using Windows.Globalization;
using Windows.Graphics.Printing;
using Windows.Graphics.Printing.OptionDetails;
using Windows.UI;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Printing;

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        PrintDocument printDocument;
        IPrintDocumentSource printDocumentSource;
        List<UIElement> calendarPages = new List<UIElement>();
        bool printingEnabled;

        public MainPage()
        {
            this.InitializeComponent();

            // Создание объекта PrintDocument и назначение обработчиков
            printDocument = new PrintDocument();
            printDocumentSource = printDocument.DocumentSource;
            printDocument.Paginate += OnPrintDocumentPaginate;
            printDocument.GetPreviewPage += OnPrintDocumentGetPreviewPage;
            printDocument.AddPages += OnPrintDocumentAddPages;
        }

        private void OnMonthYearChanged(object sender, EventArgs e)
        {
            // Вычисление количества месяцев и проверка его неотрицательности
            int printableMonths = GetPrintableMonthCount();
            printButton.Content = String.Format("Распечатать {0} месяц", printableMonths,
                printableMonths <= 1 ? "" : printableMonths < 5 ? "а" : "ев");
            printButton.IsEnabled = printableMonths > 0;

            // Назначение и отсоединение обработчика PrintManager
            if (printingEnabled != printableMonths > 0)
            {
                PrintManager printManager = PrintManager.GetForCurrentView();

                if (printableMonths > 0)
                    printManager.PrintTaskRequested += OnPrintManagerPrintTaskRequested;
                else
                    printManager.PrintTaskRequested -= OnPrintManagerPrintTaskRequested;

                printingEnabled = printableMonths > 0;
            }
        }

        private int GetPrintableMonthCount()
        {
            Calendar cal1 = monthYearSelect1.MonthYear;
            Calendar cal2 = monthYearSelect2.MonthYear;
            return cal2.Month - cal1.Month + 1 + 12 * (cal2.Year - cal1.Year);
        }

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

        private void OnPrintManagerPrintTaskRequested(PrintManager sender,
            PrintTaskRequestedEventArgs e)
        {
            // Создание PrintTask
            PrintTask printTask = e.Request.CreatePrintTask("Monthly Planner",
                                                               OnPrintTaskSourceRequested);

            // Выбор альбомной ориентации
            PrintTaskOptionDetails optionDetails =
                PrintTaskOptionDetails.GetFromPrintTaskOptions(printTask.Options);

            PrintOrientationOptionDetails orientation =
                optionDetails.Options[StandardPrintTaskOptions.Orientation] as
                                                                    PrintOrientationOptionDetails;

            orientation.TrySetValue(PrintOrientation.Landscape);
        }

        private void OnPrintTaskSourceRequested(PrintTaskSourceRequestedArgs e)
        {
            e.SetSource(printDocumentSource);
        }

        // ...
    }
}

Также обратите внимание на то, что обработчик PrintTaskRequested обращается к параметру Orientation и инициализирует его значением Landscape. Это будет происходить каждый раз, когда пользователь открывает панель принтера. А если пользователь решительно не хочет печатать календари в альбомном режиме? Возможно, вам стоит отслеживать используемый режим и выбирать его при следующем вызове панели принтера - и даже сохранить предпочтения пользователя до следующего запуска программы.

За создание страниц отвечает обработчик Paginate, который сохраняет их в поле для обработчиков GetPreviewPage и AddPages. Страницы строятся на базе панели Grid с семью столбцами для семи дней недели, а количество строк определяется количеством недель в конкретном месяце (от четырех в феврале до шести в другие месяцы), плюс еще одна строка для заголовка с месяцем и годом:

// ...
namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        // ...

        private void OnPrintDocumentPaginate(object sender, PaginateEventArgs e)
        {
            // Подготовка к генерированию страниц
            uint pageNumber = 0;
            calendarPages.Clear();
            Calendar calendar = monthYearSelect1.MonthYear.Clone();
            calendar.Day = 1;
            Brush black = new SolidColorBrush(Colors.Black);

            // Для каждого месяца
            do
            {
                PrintPageDescription printPageDescription =
                                        e.PrintTaskOptions.GetPageDescription(pageNumber);

                // Задание отступов для внешнего элемента Border
                double left = printPageDescription.ImageableRect.Left;
                double top = printPageDescription.ImageableRect.Top;
                double right = printPageDescription.PageSize.Width
                                        - left - printPageDescription.ImageableRect.Width;
                double bottom = printPageDescription.PageSize.Height
                                        - top - printPageDescription.ImageableRect.Height;
                Border border = new Border { Padding = new Thickness(left, top, right, bottom) };

                // Для ячеек календаря используется панель Grid
                Grid grid = new Grid();
                border.Child = grid;
                int numberOfWeeks = (6 + (int)calendar.DayOfWeek + calendar.LastDayInThisMonth) / 7;

                for (int row = 0; row < numberOfWeeks + 1; row++)
                    grid.RowDefinitions.Add(new RowDefinition
                    {
                        Height = new GridLength(1, GridUnitType.Star)
                    });

                for (int col = 0; col < 7; col++)
                    grid.ColumnDefinitions.Add(new ColumnDefinition
                    {
                        Width = new GridLength(1, GridUnitType.Star)
                    });

                // Наверху выводится месяц и год
                Viewbox viewbox = new Viewbox
                {
                    Child = new TextBlock
                    {
                        Text = calendar.MonthAsSoloString() + " " + calendar.YearAsString(),
                        Foreground = black,
                        FontSize = 96,
                        HorizontalAlignment = HorizontalAlignment.Center
                    }
                };
                Grid.SetRow(viewbox, 0);
                Grid.SetColumn(viewbox, 0);
                Grid.SetColumnSpan(viewbox, 7);
                grid.Children.Add(viewbox);

                // Перебор дней месяца
                for (int day = 1, row = 1, col = (int)calendar.DayOfWeek;
                     day <= calendar.LastDayInThisMonth; day++)
                {
                    Border dayBorder = new Border
                    {
                        BorderBrush = black,

                        // Чтобы избежать двойной прорисовки линий
                        BorderThickness = new Thickness
                        {
                            Left = day == 1 || col == 0 ? 1 : 0,
                            Top = day - 7 < 1 ? 1 : 0,
                            Right = 1,
                            Bottom = 1
                        },

                        // День месяца в верхнем левом углу
                        Child = new TextBlock
                        {
                            Text = day.ToString(),
                            Foreground = black,
                            FontSize = 24,
                            HorizontalAlignment = HorizontalAlignment.Left,
                            VerticalAlignment = VerticalAlignment.Top
                        }
                    };
                    Grid.SetRow(dayBorder, row);
                    Grid.SetColumn(dayBorder, col);
                    grid.Children.Add(dayBorder);

                    if (0 == (col = (col + 1) % 7))
                        row += 1;
                }
                calendarPages.Add(border);
                calendar.AddMonths(1);
                pageNumber += 1;
            }
            while (calendar.Year < monthYearSelect2.MonthYear.Year ||
                   calendar.Month <= monthYearSelect2.MonthYear.Month);

            printDocument.SetPreviewPageCount(calendarPages.Count, PreviewPageCountType.Final);
        }

        private void OnPrintDocumentGetPreviewPage(object sender, GetPreviewPageEventArgs e)
        {
            printDocument.SetPreviewPage(e.PageNumber, calendarPages[e.PageNumber - 1]);
        }

        private void OnPrintDocumentAddPages(object sender, AddPagesEventArgs e)
        {
            foreach (UIElement calendarPage in calendarPages)
                printDocument.AddPage(calendarPage);

            printDocument.AddPagesComplete();
        }
    }
}
Пройди тесты
Лучший чат для C# программистов