Глава 15. Объекты печати


    Один из наиболее трудных аспектов программирования в Windows - это вывод на принтер. ObjectWindows путем использования ряда модулей и инкапсуляции поведения устройства печати и самой распечатки делает печать более простой.

    Данная глава описывает основы процесса печати, после чего описываются следующие задачи:

Почему печать представляет трудности?


    С одной стороны печать в Windows достаточно проста. Вы можете использовать для генерации распечатки те же функции GDI, что используются для вывода образов на экран. Для вывода текста используется функция TextOut, а для вычерчивания прямоугольника Rectangle.

    С другой стороны, процесс осложняется, так как Windows требует непосредственного "общения" с драйверами принтера через вызовы Escape или получения адреса DeviceMode или ExtDeviceMode. Это еще более осложняется требованием Windows, чтобы приложение считывало имя драйвера устройства из файла WIN.INI. Кроме того, устройства печати обладают большими возможностями проверки допустимости и возможностями разрешения, чем видеоустройства.

    ObjectWindows не может преодолеть все препятствия на этом пути, но делает процесс печати более простым и легким для понимания.

Печать в ObjectWindows


    Модуль ObjectWindows OPrinter предусматривает для упрощения печати два объекта - TPrinter и TPrintout. TPrinter инкапсулирует доступ к устройствам печати. Он предоставляет возможность конфигурирования принтера, выводя диалог, в котором пользователь может выбрать нужный принтер, а также установить параметры печати, такие как графическое разрешение или ориентация (горизонтальная и вертикальная) печати.

    TPtintout инкапсулирует задачу печати документа. К принтеру этот объект имеет такое же отношение, как TWindow - к экрану. Рисование на экране выполняется методом Paint объекта TWindow, а печать на принтере - методом PrintPage объекта TPrintout. Чтобы напечатать что-то на принтере, приложение должно передать методу Print объекта TPrinter экземпляр TPrintout.

Построение объекта принтера


    В большинстве случаев приложению требуется в каждый момент времени доступ только к одному принтеру. Простейшим способом реализации этого является задание в объекте основного окна поля с именем Printer (типа PPrinter), которые другие объекты в программе вызывают для целей печати. Чтобы сделать принтер доступным, поле Printer должно указывать на экземпляр TPrinter.

    В большинстве приложений это просто. Основное окно приложения инициализирует объект принтера, который использует заданный по умолчанию принтер, указанный в WIN.INI:

     constructor TSomeWindow.Init(AParent: PWindowsObject;
                                  ATitle: PChar);
     begin
        Inherited Init(AParent, ATitle);
         .
         .
         .
        Printer := New(PPrinter, Init);
     end;

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

    Хотя вы можете сомневаться насчет переопределения конструктора TPrinter для использования принтера, отличного от заданного в системе по умолчанию, рекомендуемой процедурой является использование конструктора по умолчанию, а затем смена связанного с объектом устройства. См. раздел "Выбор другого принтера".

Создание распечатки


    Единственной "хитрой" частью процесса печати в ObjectWindows являются создание распечатки. Этот процесс аналогичен написанию метода Paint для объекта окна: вы используете графические функции Windows для генерации в контексте устройства нужного графического образа. Контекст устройства оконного объекта обрабатывает ваши взаимодействия с устройством экрана; аналогичным образом контекст устройства распечатки изолирует вас от устройства печати.

    Примечание: Графические функции Windows поясняются в Главе 17.

    Чтобы создать объект распечатки, постройте новый тип, производный от TPtintout, который переопределяет PrintPage. В очень простых случаях это все, что требуется сделать. Если документ имеет размер более одной страницы, то вам нужно также переопределить HasNextPage для возврата True. Текущий номер страницы передается в качестве параметра PrintPage.

    Объект распечатки имеет поля, содержащие размер страницы и контекст устройства, который уже инициализирован для принтера. Объект принтера устанавливает значения, вызывая метод объекта распечатки SetPrintParams. Используйте контекст устройства объекта распечатки в любом вызове графических функций Windows.

    Модуль OPrinter включает в себя два специализированных объекта распечатки, которые показывают диапазон сложности распечаток. Объект TWindowPrintout, печатающий содержимое окна, очень прост. TEditPrintout, который печатает содержимое управляющего элемента редактирования, очень сложен, так как имеет множество возможностей.

Печать документа


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

    ObjectWindows предусматривает абстрактный объект распечатки TPrintout, из которого вы можете создать производные объекты распечатки. Вам нужно переопределить в TPrintout только несколько методов.

    Ваши объекты распечатки должны делать следующее:

    Остальная часть этой главы ссылается на пример программы PrnTest, записанной на ваших дистрибутивных дискетах под именем PRNTEST.PAS. PrnTest считывает текстовый файл в набор строк, а затем по команде печатает документ. Объект PrnTest описывается следующим образом:

     type
       PTextPrint = ^TTextPrint;
       TTextPrint = object(TPrintout);
         TextHeight, LinesPerPage, FirstOnPage, LastOnPage:
            Integer;
         TheLines; PCollection;
         constructor Init(ATitle: PChar;
                          TheText: PPCharCollection);
         function GetDialogInfo(var Pages: Intger): Boolean;
                          virtual;
         function HasNextPage(Page: Word): Boolean; virtual;
         procedure SetPrintParams(ADC: HDC; ASize: TPoint);
                          virtual;
         procedure PrintPage(Page: Word; var Rect: TRect;
                          Flags: Word); virtual;
     end;

Задание параметров печати


    Перед запросом распечатки документа объект принтера предоставляет вашему документу возможность разбивки на страницы. Для этого вызываются два метода объекта распечатки - SetPrintParams и GetDialogInfo.

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

    Если вы переопределяете SetPrintParams, убедитесь в вызове наследуемого метода, устанавливающего значения полей распечатки объекта.

Подсчет страниц


    После вызова SetPrintParams объект печати вызывает булевскую функцию GetDialogInfo. GetDialogInfo задает информацию, необходимую для вывода на экран диалогового блока, позволяющего пользователю выбрать перед печатью диапазон страниц. В подсчете страниц в GetDialogInfo и индикации вывода диалогового блока имеются два аспекта.

    Функция GetDialogInfo воспринимает единственный параметр-переменную Pages, которую она должна устанавливать в число страниц в документе или в 0, если она не может подсчитать страницы. Возвращаемое значение равно True, если вы хотите вывести диалоговый блок, и False для подавления его вывода.

    По умолчанию GetDialogInfo устанавливает Pages в 0 и возвращает True, что означает, что она не знает, сколько может получиться страниц, и что перед печатью будет выводиться диалоговый блок. GetDialogInfo обычно переопределяется для установки Pages в число страниц в документе и возвращает True.

    Например, PrnTest подсчитывает, сколько строк текста выбранного шрифта может поместиться в области печати в SetPrintParams, а затем использует это число для подсчета количества страниц, которые нужно напечатать в GetDialogInfo:

     procedure TTextPrint.SetPrintParams(ADC: HDC;
                                         ASize: TPoint);
     var TextMetrics: TTextMetric;
     begin
       inherited SetPrintParams(ADC, ASize);  { установить DC и
                                                размер Size }
       GetTextMetrics(DC, TextMetrics); { получить информацию о
                                          размере текста }
       TextHeigh := TextMetrics.tmHeight; { вычислить высоту
                                            строки }
       LinesPerPages := Size.Y div TextHeight; { и число строк
                                                 на странице }
     end;

     function TTextPtint.GetDialogInfo(var Pages: Integer):
                                       Boolean);
     begin
       Pages:= TheLines^.Count div LinesPerPage + 1;
       GetDialogInfo := True { вывод перед печатью диалогового
         блока }
     end;

Печать каждой страницы


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

     procedure TTextWindow.Paint(PaintDC: HDC;
        var PaintInfo: TPaintStruct);
     var
        Line: Integer;
        TextMetrics: TTextMetric;
        TheText: PChar;
        
        function TextVisible(ALine: Integer): Boolean;
        begin
          with Scroller^ do
             TextVisible := IsVisible(0, (ALine div YUnit) +
                YPos, 1, Attr.W div YUnit);
          end;

     begin
       GetTextMetrics(PaintDC, TextMetrics);
       Scroller^.SetUnits(TextMetrics.tmAveCharWidth,
           TextMetrics.tmHeight);
       Line := 0;
       while (Line < FileLines^.Count) and TextVisible(Line) do
       begin
         TheText := PChar(FileLines^.At(Line));
         if TheText <> nil then
           TextOut(PaintDC, 0, Line * Scroller^.YUnit, TheText,
              StrLen(TheText));
         Inc(Line);
       end;
     end;

     procedure TTextPrint.PrintPage(Page: Word; var Rect: TRect;
                                    Flags: Word);
     var
       Line: Integer;
       TheText: PChar;
     begin
       FirstOnPage := (Page - 1) * LinesPerPage;
       LastOnPage := (Page * LinesPerPage) - 1;
       if LastOnPage >= TheLines^.Count then
         LastOnPage := TheLines^.Count - 1;
       for Line := FirstOnPage to LastOnPage do
       begin
         TheText := Theines^.At(Line);
         if TheText <> nil then
           TextOut(DC, 0, (Line - FirstOnPage) * TextHeight,
               TheText, StrLen(TheText));
       end;
     end;

    При написании методов PrintPage следует иметь в виду следующее:

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

    * Возможности устройств. Хотя большинство видеоустройств поддерживают все операции GDI, некоторые принтеры этого не делают. Например, многие устройства печати (такие как графопостроители) совсем не воспринимают графических изображений. При выполнении сложных задач вывода в программе следует вызывать функцию API Windows GetDeviceCaps, которая возвращает важную информацию о данном устройстве вывода.

Указание оставшихся страниц


    У объектов распечатки имеется также последняя обязанность указать объекту принтера, имеются ли после данной страницы еще печатаемые страницы. Метод HasNextPage воспринимает в качестве параметра номер строки и возвращает значение Boolean, указывающее, существуют ли еще страницы. По умолчанию HasNextPage всегда возвращает значение False. Чтобы напечатать несколько страниц, ваши объекты распечатки должны переопределять HasNextPage для возврата True, если документ имеет больше страниц для печати, и False, если переданным параметром является последняя страница.

    Например, PrnTest сравнивает номер последней напечатанной строки с последней строкой в файле и определяет, нужно ли печатать еще страницы:

     function TTextPrint.HasNextPage(Page: Word): Boolean;
     begin
       HasNextPage := LastOnPage < TheLines^.Count - 1;
     end;

    Убедитесь, что HasNextPage возвращает в некоторой точке значение False. Если HasNextPage всегда возвращает True, то процесс печати попадет в бесконечный цикл.

Другие соглашения по печати


    Объекты распечатки содержат также несколько других методов, которые вы можете при необходимости переопределить. Методы BeginPrintint и EndPrinting вызываются, соответственно, перед печатью и после печати любого документа. Если вам требуется специальная установка, вы можете выполнить ее в BeginPrinting и отменить в EndPrinting.

    Печать страниц выполняется последовательно. То есть, для каждой страницы в последовательности принтер вызывает метод PrintPage. Однако, перед первым вызовом PrintPage объект принтера вызывает BeginDocument, передавая номер первой и последней страницы, которые будут печататься. Если для вашего документа при печати страниц, отличных от первой, требуется специальная подготовка, переопределите метод BeginDocument. После распечатки последней страницы вызывается соответствующий метод EndDocument.

    Может потребоваться также переопределение метода GetSelection. GetSelection указывает в своем возвращаемом булевском значении, имеет ли документ выделенную часть. Если это так, диалоговое окно печати предоставляет вам возможность распечатать только эту выделенную часть. Позицию выделения указывают два параметра-переменных Start и Stop. Например, TEditPrintout интерпретирует Start и Stop как позиции символов, но может представлять также строки текста, страницы и т.д.

Печать содержимое окна


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

    Чтобы еще более облегчить эту общую операцию, ObjectWindows обеспечивает дополнительный вид объекта распечатки TWindowPrint. Любой оконный объект ObjectWindows может без модификации печатать свое содержимое в объект TWindowPrintout. Объекты распечатки масштабируют образ для заполнения нужного числа страниц и поддерживают коэффициент относительного удлинения.

    Создание объекта распечатки окна требует только одного шага. Все, что требуется сделать - это построить объект печати окна, передавая ему строку заголовка и указатель на окно, которое требуется напечатать:

     PImage := New(PWindowPrintout, Init('Заголовок',
                                                PSomeWindow));

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

     procedure TSomeWindow.CMPrint(var Msg: TMessage);
     var P: PPrintout;
     begin
       P := New(PWindowPrintout, Init('Дамп экрана', @Self));
{ передать образ на экран }
       Dispose(P, One);
     end;

    TWindowPrintout не предусматривает разбивки на страницы. При печати документа вам нужно печатать каждую страницу отдельно, но так как окна не имеют страниц, вам нужно напечатать только один образ. Окно уже знает, как создать этот образ - оно имеет метод Paint. TWindowsPrintout печатается путем вызова метода Paint окна объекта с контекстом устройства печати вместо контекста дисплея.

Вывод распечатки на принтер


    При наличии объекта принтера и объекта распечатки фактическая печать выполняется достаточно просто, независимо от того, происходит это из документа или из окна. Все, что вам нужно сделать - это вызов метода Paint объекта принтера, передача указателя на порождающее окно и указателя на объект распечатки:

     Printer^.Print(PParentWindow, PPrintoutObject);

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

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

     procedure TWidgetWindow.CMPrint(var Msg: TMessage);
     var P: PPrintout;
     begin
       P := New(PWindowPrint, Init('Widgets', @Self));
       Printer^.Print(@Self, P);
       Dispose(P, Done);
     end;

Выбор другого принтера


    Когда у вас в приложении есть объект принтера, вы можете связать его с любым установленным в Windows устройстве печати. По умолчанию TPrinter использует заданный по умолчанию принтер Windows (как это определено в разделе устройств файла WIN.INI).

    Существует два способа задания альтернативного принтера: непосредственно в программе и через диалоговое окно пользователя.

Выбор принтера пользователем


    Наиболее общим способом назначения другого принтера является вывод диалогового окна, предоставляющего пользователю возможность выбора из списка установленных устройств печати. TPtinter делает это автоматически при вызове его метода Setup. Как показано на Рис. 15.1, Setup использует для этого диалогового окна объект TPrinterSetupDlg.


Рис. 15.1 Диалоговое окно задания принтера.

Настройка конфигурации принтера

    Одна из командных кнопок в диалоге выбора принтера позволяет пользователям изменить конфигурацию конкретного принтера. Кнопка Setup выводит диалоговый блок конфигурации, определенной в драйвере принтера. Ваше приложение не может управлять внешним видом или функциями диалогового блока конфигурации драйвера.

Назначение конкретного принтера


    В некоторых случаях вам может потребоваться назначить для своего объекта принтера специфическое устройство печати. TPrinter имеет метод SetDevice, который именно это и делает.

    SetDevice воспринимает в качестве параметров три строки: имя устройства, имя драйвера и имя порта.