Поиск на сайте: Расширенный поиск


Новые программы oszone.net Читать ленту новостей RSS
CheckBootSpeed - это диагностический пакет на основе скриптов PowerShell, создающий отчет о скорости загрузки Windows 7 ...
Вы когда-нибудь хотели создать установочный диск Windows, который бы автоматически установил систему, не задавая вопросо...
Если после установки Windows XP у вас перестала загружаться Windows Vista или Windows 7, вам необходимо восстановить заг...
Программа подготовки документов и ведения учетных и отчетных данных по командировкам. Используются формы, утвержденные п...
Red Button – это мощная утилита для оптимизации и очистки всех актуальных клиентских версий операционной системы Windows...

Основы печати в Silverlight

Текущий рейтинг: 5 (проголосовало 2)
 Посетителей: 1315 | Просмотров: 2082 (сегодня 0)  Шрифт: - +

В Silverlight 4 появилась поддержка печати, и я хочу сразу взять быка за рога и показать вам крошечную программу под названием PrintEllipse — это все, что она делает. В XAML-файле для MainPage содержится Button, а все содержимое файла отделенного кода для MainPage приведено на рис. 1.

Рис. 1. Код MainPage для PrintEllipse

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Printing;
using System.Windows.Shapes;

namespace PrintEllipse
{
  public partial class MainPage : UserControl
  {
    public MainPage()
    {
      InitializeComponent();
    }
    void OnButtonClick(object sender, RoutedEventArgs args)
    {
      PrintDocument printDoc = new PrintDocument();
      printDoc.PrintPage += OnPrintPage;
      printDoc.Print("Print Ellipse");
    }
    void OnPrintPage(object sender, PrintPageEventArgs args)
    {
      Ellipse ellipse = new Ellipse
      {
        Fill = new SolidColorBrush(Color.FromArgb(255, 255, 192, 192)),
        Stroke = new SolidColorBrush(Color.FromArgb(255, 192, 192, 255)),
        StrokeThickness = 24    // 1/4 inch
      };
      args.PageVisual = ellipse;
    }
  }
}

Обратите внимание на директиву using дляSystem.Windows.Printing. Когда вы щелкаете кнопку, программа создает объект типа PrintDocument и назначает обработчик для события PrintPage. А когда программа вызывает метод Print, появляется стандартное диалоговое окно печати. В этот момент пользователь получает возможность указать нужный принтер и задать различные свойства печати, например выбрать книжную или ландшафтную ориентацию.

При нажатии кнопки Print в диалоге печати программа получает вызов обработчика события PrintPage. Конкретно эта программа реагирует на этот вызов созданием элемента Ellipse и его присваиванием свойству PageVisual аргументов события. (Я умышленно выбрал светлые пастельные цвета, чтобы программа не заставляла ваш принтер расходовать слишком много чернил.) И через короткое время из принтера появляется страница с гигантским эллипсом.

Вы можете запустить эту программу с моего веб-сайта по ссылке bit.ly/dU9B7k и самостоятельно опробовать ее. Конечно, весь исходный код для этой статьи можно скачать.

Если ваш принтер подобен большинству других, то его аппаратное обеспечение запрещает печать по самым краям бумаги. У принтеров обычно есть внутренние ограничения на размер полей, за границами которых он ничего не печатает; в итоге область печати оказывается меньше полного размера страницы.

Вы заметите в этой программе, что эллипс занимает все область печати на странице, и очевидно, что это осуществляется с минимальными усилиями со стороны программы. Область печати страницы во многом похожа на элемент-контейнер на экране: изображение обрезается, когда какой-то элемент выходит за границы области вывода. Некоторые, гораздо более изощренные графические среды вроде Windows Presentation Foundation (WPF) ведут себя несколько иначе (но, конечно, WPF предлагает куда больший контроль над печатью и ее гибкость, чем Silverlight).

PrintDocument и события

Помимо события PrintPage, в PrintDocument также определены события BeginPrint и EndPrint, но они не столь важны, как PrintPage. Событие BeginPrint сообщает о начале задания на печать. Оно срабатывает, когда пользователь выходит из стандартного диалога печати, нажав кнопку Print, и дает программе возможность выполнения необходимой инициализации. За вызовом обработчика BeginPrint следует первый вызов обработчика PrintPage.

Программе, которой требуется напечатать более одной страницы в рамках конкретного задания на печать, может легко делать это. В каждом вызове обработчика PrintPage свойство HasMorePages объекта PrintPageEventArgs изначально выставляется в false. Когда обработчик заканчивает печать страницы, он просто устанавливает это свойство в true, сообщая тем самым, что нужно напечатать минимум еще одну страницу. Затем вновь вызывается PrintPage. Объект PrintDocument поддерживает свойство PrintedPageCount, значение которого увеличивается на 1 после каждого вызова обработчика PrintPage.

Когда обработчик PrintPage возвращает управление со свойством HasMorePages, установленным в значение по умолчанию (false), задание на печать считается выполненным и срабатывает событие EndPrint, позволяющее программе выполнить необходимую очистку. Событие EndPrint также срабатывает, когда в процессе печати возникает ошибка; свойство Error объекта EndPrintEventArgs имеет тип Exception.

Координаты на принтере

Код, показанный на рис. 1, записывает в свойство StrokeThickness объекта Ellipse значение 24, и, если вы измерите результат после печати, вы обнаружите, что линии имеют толщину в четверть дюйма. Как вы знаете, программа Silverlight обычно оперирует размерами графических объектов и элементов управления полностью в пикселях. Однако, когда вывод отправляется на принтер, координаты и размеры измеряются в аппаратно-независимых единицах по 1/96 дюйма. Независимо от реального разрешения принтера из программы Silverlight он всегда виден как устройство с разрешением 96 DPI.

Эта координатная система (96 точек на дюйм) широко используется в WPF, нде такие единицы иногда называют аппаратно-независимыми пикселями. Значение 96 DPI выбрано не случайно: по умолчанию Windows предполагает, что ваш дисплей имеет разрешение 96 точек на дюйм, поэтому во многих случаях WPF-программа на самом деле рисует в пикселях. В спецификации CSS предполагается, что дисплей имеет разрешение 96 DPI, и это значение используется для преобразований между пикселями, дюймами и миллиметрами. Значение 96 также является удобным числом для преобразования размеров шрифтов, которые обычно указываются в точках (1/72 дюйма). Точка составляет три четверти аппаратно-независимого пикселя.

У PrintPageEventArgs есть два удобных свойства только для чтения, которые сообщают размеры в единицах по 1/96 дюйма: PrintableArea типа Size (размеры области печати на страницы) и PageMargins типа Thickness (ширина левого, верхнего, правого и нижнего полей непечатаемых краев). Сложите их значения (правильным образом) и вы получите полный размер бумаги.

Мой принтер (когда в него загружаются листы бумаги в стандарте 8,5 × 11 дюйма с портретной ориентацией) сообщает о PrintableArea размером 791 × 993. Четыре значения свойства PageMargins равны 12 (слева), 6 (сверху), 12 (справа) и 56 (снизу). Если вы просуммируете значения по горизонтали (791, 12 и 12), вы получите 815. Значения по вертикали равны 994, 6 и56, сумма которых дает 1055. Я не знаю, почему между этими значениями и значениями 816 и 1056, получаемыми перемножением размера страницы в дюймах на 96, существует разница в одну единицу.

Когда принтер переведен в режим ландшафтной печати, размеры по горизонтали и вертикали, сообщаемые свойствами PrintableArea и PageMargins, меняются местами. На практике анализ свойства PrintableArea является единственным способом в программах Silverlight определить, в каком режиме печати находится принтер — с портретной или ландшафтной ориентацией. Все, что печатается программой, автоматически выравнивается и поворачивается в зависимости от установленного режима.

Зачастую, когда вы что-то печатаете в реальных программах, вы определяете поля, которые несколько превышают непечатаемые поля. Как это сделать в Silverlight? Поначалу я думал, что для этого достаточно задать свойство Margin печатаемого элемента. Значение Margin вычислялось бы как общий желательный размер полей (в единицах по 1/96 дюйма), из которого вычитаются значения свойства PageMargins в PrintPageEventArgs. Этот подход работал неважнецки, но правильное решение оказалось почти таким же простым. Программа PrintEllipseWithMargins (ее можно запустить с bit.ly/fCBs3X) идентична первой программе с тем исключением, что свойство Margin устанавливается в Ellipse, а затем Ellipse присваивается Border как дочерний объект, и этот Borderзаполняет всю область печати. В качестве альтернативы можно задать свойство Padding элемента Border. На рис. 2 показан новый методOnPrintPage.

Рис. 2. Метод OnPrintPage для вычисления полей

void OnPrintPage(object sender, PrintPageEventArgs args)
{
  Thickness margin = new Thickness
  {
    Left = Math.Max(0, 96 - args.PageMargins.Left),
    Top = Math.Max(0, 96 - args.PageMargins.Top),
    Right = Math.Max(0, 96 - args.PageMargins.Right),
    Bottom = Math.Max(0, 96 - args.PageMargins.Bottom)
  };
  Ellipse ellipse = new Ellipse
  {
    Fill = new SolidColorBrush(Color.FromArgb(255, 255, 192, 192)),
    Stroke = new SolidColorBrush(Color.FromArgb(255, 192, 192, 255)),
    StrokeThickness = 24,   // 1/4 inch
    Margin = margin
  };
  Border border = new Border();
  border.Child = ellipse;
  args.PageVisual = border;
}

Объект PageVisual

Специальных графических методов или классов, связанных непосредственно с принтером, нет. Вы «рисуете» на принтере так же, как и на дисплее, что осуществляется сборкой визуального дерева объектов, производных от FrameworkElement. Это дерево может включать элементы Panel, в том числе Canvas. Чтобы распечатать это визуальное дерево, присвойте самый верхний элемент свойству PageVisual объекта PrintPageEventArgs. (PageVisual определено как UIElement, который является родительским классом для FrameworkElement, но на практике все, что вы будете присваивать PageVisual, будет производным от FrameworkElement.)

Почти в каждом классе, производном от FrameworkElement, есть нетривиальные реализации методов MeasureOverride и ArrangeOverride для целей разметки. В методе MeasureOverride элемент определяет свой желательный размер, который иногда определяется желательными размерами его дочерних элементов, для чего вызываются их методы Measure. В методе ArrangeOverride элемент размещает свои дочерние элементы относительно себя, вызывая их методы Arrange.

Когда вы присваиваете какой-либо элемент свойству PageVisual объекта PrintPageEventArgs, подсистема печати в Silverlight вызывает Measure в самом верхнем элементе с передачей размера PrintableArea. Вот как в моем примере размеры Ellipse или Border автоматически подгоняются под размеры области печати на странице.

Однако вы также можете это свойство PageVisual элементу, который уже является частью визуального дерева, отображаемого в окне программы. В таком случае подсистема печати не вызывает Measure этого элемента, а использует размеры и разметку, уже определенные для дисплея. Это позволяет печатать что-то из окна вашей программы с приемлемой достоверностью, но какая-то часть может оказаться обрезанной по размерам страницы.

Конечно, вы можете явно задать свойства Width и Height печатаемых элементов и использовать размер PrintableArea.

Масштабирование и поворачивание

Следующая программа оказалась потруднее, чем я думал поначалу. В этой программе ставилась цель разрешить пользователю печатать любой файл изображения, поддерживаемый Silverlight, а именно PNG- и JPEG-файлы, которые хранятся локально на компьютере пользователя. Для загрузки файлов используется класс OpenFileDialog. По соображениям безопасности OpenFileDialog возвращает только объект FileInfo, который позволяет программе открыть выбранный файл. Имя файла и каталог не предоставляются.

Я хотел, чтобы эта программа печатала битовую карту настолько большой на странице, насколько это возможно (исключая ограничивающие поля принтера), не влияя на соотношение сторон растрового изображения (аспект). Обычно это несложно: по умолчанию режим Stretch элемента Image устанавливается в Uniform, т. е. битовая карта максимально растягивается без искажений аспекта.

Но я решил, что пользователь не должен специально выставлять портретную или ландшафтную ориентацию на принтере; изображение должно автоматически поворачиваться в зависимости от конкретного изображения. Если принтер был в портретном режиме и ширина изображение превышает его высоту, то изображение должно печататься в этом режиме но с поворотом на 90 градусов. Эта небольшая функция сразу же резко усложнила программу.

Если бы я писал аналогичную WPF-программу, та могла бы самостоятельно переключать принтер в портретный или ландшафтный режим. Но в Silverlight это невозможно. Интерфейс принтера определен так, что лишь пользователь может менять подобные настройки.

Опять же, если бы я писал WPF-программу, то в качестве альтернативы мог бы установить свойство LayoutTransform элемента Image так, чтобы изображение поворачивалось на 90 градусов. Повернутый элемент Image можно было бы потом отмасштабировать под размеры страницы, а сама битовая карта была бы подогнана под размер этого элемента.

Но Silverlight не поддерживает LayoutTransform. Silverlight поддерживает только RenderTransform, поэтому, если элемент Image нужно повернуть для имитации ландшафтного режима в портретном режиме, то размеры Image должны быть вручную подогнаны под размеры страницы в ландшафтной ориентации.

Вы можете опробовать результат моей первой попытки с bit.ly/eMHOsB. Метод OnPrintPage создает элемент Image и задает свойство Stretch равным None, т. е. элемент Image отображает битовую карту в ее размерах в пикселях, а на принтере каждый пиксель считается равным 1/96 дюйма. Затем программа поворачивает, масштабирует и транслирует этот элемент Image, вычисляя преобразование, которое применяется к свойству RenderTransform элемента Image.

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

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

Что здесь происходит? Ну, нечто похожее я уже наблюдал раньше на дисплеях. Учитывайте, что RenderTransform влияет только на то, как элемент отображается, но никак не сказывается на том, как он появляется в системе разметки. С точки зрения системы разметки, я отображаю битовую карту в элементе Image со свойством Stretch, установленным в None, т. е. размер элемента Image соответствует размеру самой битовой карты. Если битовая карта больше страницы в принтере, тогда часть элемента Image визуализировать не требуется, и, по сути, он обрезается независимо от того, что RenderTransform соответственно ужимает элемент Image.

При второй попытке, результаты которой вы можете опробовать на bit.ly/g4HJ1C, я сменил стратегию. Метод OnPrintPage показан на рис. 3. Элементу Image задаются явные значения Width и Height, которые точно соответствуют вычисленной области отображения. Поскольку все это укладывается в область печати на странице, ничего не обрезается. Режим Stretch установлен в Fill, т. е. битовая карта заполняет элемент Image независимо от аспекта. Если элемент Image не будет поворачиваться, одна сторона уже отмасштабирована правильно, а вторая должна быть уменьшена умножением на коэффициент масштабирования. Если элемент Image нужно еще и повернуть, то коэффициенты масштабирования должны учитывать другой аспект (соотношение сторон) в повернутом элементе Image.

Рис. 3. Печать изображения в PrintImage

void OnPrintPage(object sender, PrintPageEventArgs args)
{
  // Find the full size of the page
  Size pageSize =
    new Size(args.PrintableArea.Width
    + args.PageMargins.Left + args.PageMargins.Right,
    args.PrintableArea.Height
    + args.PageMargins.Top + args.PageMargins.Bottom);

  // Get additional margins to bring the total to MARGIN (= 96)
  Thickness additionalMargin = new Thickness
  {
    Left = Math.Max(0, MARGIN - args.PageMargins.Left),
    Top = Math.Max(0, MARGIN - args.PageMargins.Top),
    Right = Math.Max(0, MARGIN - args.PageMargins.Right),
    Bottom = Math.Max(0, MARGIN - args.PageMargins.Bottom)
  };

  // Find the area for display purposes
  Size displayArea =
    new Size(args.PrintableArea.Width
    - additionalMargin.Left - additionalMargin.Right,
    args.PrintableArea.Height
    - additionalMargin.Top - additionalMargin.Bottom);

  bool pageIsLandscape = displayArea.Width > displayArea.Height;
  bool imageIsLandscape = bitmap.PixelWidth > bitmap.PixelHeight;

  double displayAspectRatio = displayArea.Width / displayArea.Height;
  double imageAspectRatio = (double)bitmap.PixelWidth / bitmap.PixelHeight;

  double scaleX = Math.Min(1, imageAspectRatio / displayAspectRatio);
  double scaleY = Math.Min(1, displayAspectRatio / imageAspectRatio);

  // Calculate the transform matrix
  MatrixTransform transform = new MatrixTransform();

  if (pageIsLandscape == imageIsLandscape)
  {
    // Pure scaling
    transform.Matrix = new Matrix(scaleX, 0, 0, scaleY, 0, 0);
  }
  else
  {
    // Scaling with rotation
    scaleX *= pageIsLandscape ? displayAspectRatio : 1 /
      displayAspectRatio;
    scaleY *= pageIsLandscape ? displayAspectRatio : 1 /
      displayAspectRatio;
    transform.Matrix = new Matrix(0, scaleX, -scaleY, 0, 0, 0);
  }

  Image image = new Image
  {
    Source = bitmap,
    Stretch = Stretch.Fill,
    Width = displayArea.Width,
    Height = displayArea.Height,
    RenderTransform = transform,
    RenderTransformOrigin = new Point(0.5, 0.5),
    HorizontalAlignment = HorizontalAlignment.Center,
    VerticalAlignment = VerticalAlignment.Center,
    Margin = additionalMargin,
  };

  Border border = new Border
  {
    Child = image,
  };

  args.PageVisual = border;
}

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

Другой подход — вращение самих битовых карт, а не элемента Image. Создайте WriteableBitmap из загруженного объекта BitmapImage, а затем второй WritableBitmap, у которого переставлены местами горизонтальный и вертикальный размеры. Потом скопируйте все пиксели из первого WriteableBitmap во второй, меняя местами строки и столбцы.

Печать многостраничного календаря

Наследование от UserControl — чрезвычайно популярный прием в программировании для Silverlight, когда нужно создать повторно используемый элемент управления без лишних хлопот. Большая часть UserControl — это визуальное дерево, определенное в XAML.

Так вот, чтобы определить визуальное дерево для печати, вы также можете наследовать от UserControl! Этот прием иллюстрируется в программе PrintCalendar, которую вы можете опробовать на bit.ly/dIwSsn. Введите начальный и конечный месяцы, и программа напечатает календарь на все месяцы в этом диапазоне — по одному на страницу.

Благодаря урокам, извлеченным при создании программы PrintImage, я не стал возиться с полями или ориентацией; вместо этого я включил элемент Button, который возлагает ответственность на пользователя, как показано на рис. 4.

*

Рис. 4. Кнопка PrintCalendar

UserControl, который определяет страницу календаря, называется CalendarPage; XAML-файл показан на рис. 5. TextBlock рядом с верхней границей отображает месяц и год. Под ним размещен Grid с семью столбцами для дней недели и шестью строками для шести недель (неполных) в месяце.

Рис. 5. РазметкаCalendarPage

<UserControl x:Class="PrintCalendar.CalendarPage"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  FontSize="36">
  <Grid x:Name="LayoutRoot" Background="White">
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto" />
      <RowDefinition Height="*" />
    </Grid.RowDefinitions>
    <TextBlock Name="monthYearText"
      Grid.Row="0"
       FontSize="48"
       HorizontalAlignment="Center" />
    <Grid Name="dayGrid"
      Grid.Row="1">
      <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
      </Grid.ColumnDefinitions>
      <Grid.RowDefinitions>
        <RowDefinition Height="*" />
        <RowDefinition Height="*" />
        <RowDefinition Height="*" />
        <RowDefinition Height="*" />
        <RowDefinition Height="*" />
        <RowDefinition Height="*" />
      </Grid.RowDefinitions>
    </Grid>
  </Grid>
</UserControl>

CalendarPage — в отличие от большинства производных от UserControl — определяет конструктор с параметром, как показано на рис. 6.

Рис. 6. КонструкторCalendarPage в отделенном коде

public CalendarPage(DateTime date)
{
  InitializeComponent();
  monthYearText.Text = date.ToString("MMMM yyyy");
  int row = 0;
  int col = (int)new DateTime(date.Year, date.Month, 1).DayOfWeek;
  for (int day = 0; day < DateTime.DaysInMonth(date.Year, date.Month); day++)
  {
    TextBlock txtblk = new TextBlock
    {
      Text = (day + 1).ToString(),
      HorizontalAlignment = HorizontalAlignment.Left,
      VerticalAlignment = VerticalAlignment.Top
    };
    Border border = new Border
    {
      BorderBrush = blackBrush,
      BorderThickness = new Thickness(2),
      Child = txtblk
    };
    Grid.SetRow(border, row);
    Grid.SetColumn(border, col);
    dayGrid.Children.Add(border);
    if (++col == 7)
    {
      col = 0;
      row++;
    }
  }
  if (col == 0)
    row--;
  if (row < 5)
    dayGrid.RowDefinitions.RemoveAt(0);
  if (row < 4)
    dayGrid.RowDefinitions.RemoveAt(0);
}

Параметром является DateTime, и конструктор использует свойства Month и Year для создания Border, содержащего TextBlock для каждого дня месяца. Каждый из них присваивается подключенным свойствам Grid.Row и Grid.Column, а затем добавляется в Grid. Как вы знаете, месяцы часто охватывают только пять недель, а в феврале вообще лишь четыре недели, поэтому объекты RowDefinition на самом деле удаляются из Grid, если они не нужны.

Производные UserControl обычно не имеют конструкторов с параметрами, так как чаще всего они образуют части более крупных визуальных деревьев. НоCalendarPage используется иначе. Вместо этого обработчик PrintPage просто присваивает новый экземпляр CalendarPage свойству PageVisual объекта PrintPageEventArgs. Вот полное тело обработчика, четко демонстрирующее, сколько работы выполняется CalendarPage:

args.PageVisual = new CalendarPage(dateTime);
args.HasMorePages = dateTime < dateTimeEnd;
dateTime = dateTime.AddMonths(1);

Добавление функции печати в программу часто рассматривается как устрашающая работа, требующая написания уймы кода. Возможность определения большей части печатаемой страницы в XAML-файле значительно упрощает эту задачу.

Автор: Чарльз Петцольд  •  Иcточник: MSDN Magazine  •  Опубликована: 08.12.2011
Нашли ошибку в тексте? Сообщите о ней автору: выделите мышкой и нажмите CTRL + ENTER


Оценить статью:
Вверх
Комментарии посетителей
Комментарии отключены. С вопросами по статьям обращайтесь в форум.