Печать календаря в WinRT
77Разработка под Windows 10 --- Печать календаря
Работая над большим проектом, я часто вешаю на стену распечатанные календари. Распечатка выглядит предельно просто - ничего, кроме большого количества свободных ячеек для записи того, что мне нужно сделать каждый день.
Программа 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=" " />
<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=" - "
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();
}
}
}