Печать в Delphi Объект Printer автоматически создается в случае, если в программе указана ссылка на модуль Printers. Этот объект предоставляет программисту все необходимое для того, чтобы научить приложение выводить данные на один из подключенных к компьютеру принтеров. Вывод на принтер в Windows ничем не отличается от вывода на экран: в распоряжение программиста предоставляется свойство Canvas объекта Printer, содержащее набор чертежных инструментов, и методы, свойственные классу TCanvas. Размер листа бумаги в пикселах определяют свойства Height и Width, а набор принтерных шрифтов – свойство Fonts. Печать текста Существует множество способов печати текста на принтере. Прежде всего следует назвать глобальную процедуру AssignPrn (она определена в модуле Printers), позволяющую использовать принтер как текстовый файл и печатать текстовые строки с помощью процедуры WriteLn. В листинге 1 (PrintText.dpr) приведен полный текст модуля, на форме которого расположены многострочный текстовый редактор Memo1 и четыре кнопки: для выбора текстового файла и ввода его содержимого в редактор, для выбора нужного шрифта отображения/печати документа, для инициации процесса печати и для завершения работы программы. Листинг 1 unit Unit1; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, Buttons; type TForm1 = class(TForm) Memo1: TMemo; Button1: TButton; Button2: TButton; OpenDialog1: TOpenDialog; BitBtn1: TBitBtn; Button3: TButton; FontDialog1: TFontDialog; procedure Button1Click(Sender: TObject); procedure Button2Click(Sender: TObject); procedure Button3Click(Sender: TObject); private { Private declarations } public { Public declarations } end; var Form1: TForm1; implementation uses Printers; // Эта ссылка обязательна! {$R *.DFM} procedure TForm1.Button1Click(Sender: TObject); // Выбор файла с текстом и его загрузка в редактор begin if OpenDialog1.Execute then Memo1.Lines.LoadFromFile(OpenDialog1.FileName) end; procedure TForm1.Button3Click(Sender: TObject); // Выбор шрифта и связывание его с Memo1 begin if FontDialog1.Execute then Memo1.Font := FontDialog1.Font end; procedure TForm1.Button2Click(Sender: TObject); // Печать содержимого редактора как вывод в текстовый файл var Prn: TextFile; k: Integer; begin AssignPrn(Prn); // Переназначаем вывод в файл на вывод в принтер Rewrite(Prn); // Готовим принтер к печати (аналог BeginDoc) { Для печати используем такой же шрифт, как и для показа в редакторе: } Printer.Canvas.Font := Memo1.Font; // Цикл печати: for k := 0 to Memo1.Lines.Count-1 do WriteLn(Prn, Memo1.Lines[k]); CloseFile(Prn); // Аналог EndDoc end; end. Описанный способ печати — самый примитивный: с его помощью невозможно вывести линии, разделяющие колонки или строки, трудно форматировать текст, вставлять заголовки, номера страниц и т.д. Значительно более гибкие средства обеспечивает свойство Printer.Canvas. Покажем, как с его помощью можно напечатать текст, содержащийся в редакторе Memo1 (PrintText.dpr, листинг 2): Листинг 2 procedure TForm1.Button2Click(Sender: TObject); // Печать содержимого редактора c помощью свойства Printer.Canvas var Y,dY,X,k: Integer; S: String; begin if Memo1.Lines.Count=0 then Exit; Screen.Cursor := crHourGlass; with Printer do begin BeginDoc; with Canvas do begin Font := Memo1.Font; dY := TextHeight('1'); // Определяем высоту строки Y := 3*dY; // Отступ от верхнего края листа X := PageWidth div 15; // Отступ от левого края for k := 0 to Memo1.Lines.Count-1 do begin // Выводим очередную строку TextOut(X,Y,Memo1.Lines[k]); // Смещаемся на следующую строку листа inc(Y,dY); if PageHeight-Y<2*dY then // Нижний край листа? begin // Да NewPage; // Переход на новый лист // Выводим номер страницы посередине листа: S := '- '+IntToStr(PageNumber)+' -'; TextOut((PageWidth-TextWidth(S)) div 2, dy, S); // и отчеркиваем его от текста: MoveTo(X, 3*dy div 2); LineTo(PageWidth-X, 9*dy div 4); // Ордината первой строки: Y := 3*dY end; // if PageHeight-Y<2*dY end; // for k := 0 to Memo1.Lines.Count-1 do end; // with Canvas do EndDoc; end; // with Printer do Screen.Cursor := crDefault; end; Как можно увидеть, прямое обращение к чертежным инструментам свойства Canvas требует от программиста значительно больших усилий, но зато предоставляет ему полный контроль над печатным изображением. Во многих случаях для печати документа и внесения в него элементарных средств форматирования (печать общего заголовка, заголовка на каждой странице, номеров страниц и т.п.) проще использовать специальные компоненты, расположенные на странице QReport палитры компонентов Delphi. Эти компоненты разработаны для создания отчетов по базам данных, но могут с успехом использоваться и для печати обычных документов (PrintText.dpr). Наконец, очень хороших результатов можно достичь, используя специализированные средства просмотра/печати документов, как, например, текстовый процессор MS Word. Печать изображений Печать изображений может показаться очень сложным делом, однако свойство Printer.Canvas содержит метод: procedure StretchDraw(const Rect: TRect; Graphic: TGraphic ); который легко справляется с этой задачей. При обращении к нему в качестве первого параметра указывается прямоугольная область, отводимая на поверхности листа для распечатки изображения, а в качестве второго — объект класса TGraphic, в котором хранится изображение, например: with Printer do begin BeginDoc; Canvas.StretchDraw(Canvas.ClipRect, Image1.Picture.Graphic); EndDoc; end; Отображение файла в память Для работы с файлом динамической подкачки страниц виртуальной памяти в Windows 32 используется механизм отображения файлов в адресное пространство приложения. Соответствующие функции API доступны любому приложению и могут применяться к любому файлу (кстати, таким способом загружаются в адресное пространство процесса исполняемые файлы и DLL). В результате отображения приложение может работать с файловыми данными как с размещенными в динамической памяти. В большинстве случаев такая возможность не только повышает скорость работы с данными, но и предоставляет программисту уникальные средства обработки сразу всех записей файла. Например, он может с помощью единственного оператора проверить, входит ли заданный образец поиска в какую-либо строку текстового файла. Отображение файла осуществляется в три приема. Вначале файл создается обращением к функции: function FileCreate (FileName: String): Integer; или открывается с помощью: function FileOpen (const FileName: String; Mode: LongWord): Integer; В обеих функциях FileName — имя файла, возможно, с маршрутом доступа. Параметр Mode определяет режим доступа к файлу и может принимать одно из следующих значений: fmOpenRead — только чтение; fmOpenWrite — только запись; fmOpenReadWrite — чтение и запись. С помощью операции or эти константы можно комбинировать с одной из следующих нескольких функций, регулирующих совместный доступ к файлу: fmShareExclusive — совместный доступ запрещен; fmShareDenyWrite — другим приложениям запрещается запись; fmShareDenyRead — другим приложениям запрещается чтение; fmSchareDenyNone — совместный доступ неограничен. Обе функции возвращают дескриптор созданного (открытого) файла или 0, если операция оказалась неудачной. На втором этапе создается объект отображения в память. Для этого используется функция: function CreateFileMapping (hFile: THandle; lpFileMappingAttributes: PSecurityAttributes; flProtect, dwMaximumSizeHigh, dwMaximumSizeLow: DWord; lpName: PChar): THandle; Здесь hFile — дескриптор файла; lpFileMappingAttributes — указатель на структуру, в которой определяется, может ли создаваемый объект порождать дочерние объекты (обычно не может — NIL); flProtect — определяет тип защиты, применяемый к окну отображения файла (см. об этом ниже); dwMaximumSizeHigh, dwMaximumSizeLow — соответственно старшие и младшие 32 разряда числа, содержащего размер файла (если вы будете отображать файлы длиной до 4 Гбайт, поместите в dwMaximumSizeHigh 0, если в dwMaximumSizeLow — длину файла; а если оба параметра равны 0, то размер окна отображения равен размеру файла); lpName — имя объекта отображения или NIL. Параметр flProtect задает тип защиты, применяемый к окну просмотра файла, и может иметь одно из следующих значений: PAGE_READONLY — файл можно только читать (файл должен быть создан или открыт в режиме fmOpenRead); PAGE_READWRITE — файл можно читать и записывать в него новые данные (файл открывается в режиме fmOpenReadWrite); PAGE_WRITECOPY — файл открыт для записи и чтения, однако обновленные данные сохраняются в отдельной защищенной области памяти (отображенные файлы могут разделяться приложениями, в этом режиме каждое приложение сохраняет изменения в отдельной области памяти или участке файла подкачки); файл открывается в режиме fmOpenReadWrite или fmOpenWrite; (этот тип защиты нельзя использовать в Windows 95/98). С помощью операции or к параметру flProtect можно присоединить такие атрибуты: SEC_COMMIT — выделяет для отображения физическую память или участок файла подкачки; SEC_IMAGE — информация об атрибутах отображения берется из образа файла; SEC_NOCASHE — отображаемые данные не кэшируются и записываются непосредственно на диск; SEC_RESERVE — резервируются страницы раздела без выделения физической памяти. Функция возвращает дескриптор объекта отображения или 0, если обращение было неудачным. Наконец, на третьем этапе создается окно просмотра, то есть собственно отображение данных в адресное пространство программы. function MapViewOfFile(hFileMappingObject: THandle;dwDesiresAccess: DWord; dwFileOffsetHigh, dwFileIffsetLow, dwNumberOfBytesToMap: DWord): Pointer; Здесь hFileMappingObject — дескриптор объекта отображения; dwDesiresAccess — определяет способ доступа к данным и может иметь одно из следующих значений: FILE_MAP_WRITE — разрешает чтение и запись (при этом в функции CreateFileMapping должен использоваться атрибут PAGE_READWRITE); FILE_MAP_READ — разрешает только чтение (в функции CreateFileMapping должен использоваться атрибут PAGE_READONLY или PAGE_READWRITE); FILE_MAP_ALL_ACCESS — то же, что и FILE_MAP_WRITE; FILE_MAP_COPY — данные доступны для записи и чтения, однако обновленные данные сохраняются в отдельной защищенной области памяти (в функции CreateFileMapping должен использоваться атрибут PAGE_WRITECOPY); dwFileOffsetHigh, dwFileIffsetLow — определяют соответственно старшие и младшие разряды смещения от начала файла, начиная с которого осуществляется отображение; dwNumberOfBytesToMap — определяет длину окна отображения (0 — длина равна длине файла). Функция возвращает указатель на первый байт отображенных данных или NIL, если обращение к функции оказалось безуспешным. После использования отображенных данных ресурсы окна отображения нужно освободить функцией: function UnMapViewOfFile(lpBaseAddress: Pointer): BOOL; единственный параметр обращения к которой должен содержать адрес первого отображенного байта, то есть адрес, возвращаемый функцией MapViewOfFile. Закрытие объекта отображения и самого файла осуществляется обращением к функции: function CloseHandle(hObject: THandle). В листинге 3 приводится текст модуля (FileInMemory.dpr), который создает окно. Листинг 3 unit Unit1; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ComCtrls, Spin; type TForm1 = class(TForm) btMem: TButton; btFile: TButton; se: TSpinEdit; Label1: TLabel; pb: TProgressBar; Label2: TLabel; lbMem: TLabel; lbFile: TLabel; procedure btMemClick(Sender: TObject); procedure btFileClick(Sender: TObject); private { Private declarations } public { Public declarations } end; var Form1: TForm1; implementation {$R *.DFM} procedure TForm1.btMemClick(Sender: TObject); // Создание файла методом его отображения type PReal = ^Real; var HFile, HMap: THandle; AdrBase, AdrReal: PReal; k: Integer; FSize: Cardinal; BegTime: TDateTime; begin BegTime := Time; // Засекаем время пуска // Готовим ProgressBar: pb.Max := se.Value; pb.Position := 0; pb.Show; FSize := se.Value * SizeOf(Real); // Длина файла HFile := FileCreate('test.dat'); // Создаем файл if HFile = 0 then // Ошибка: возбуждаем исключение raise Exception.Create('Ошибка создания файла'); try // Отображаем файл в память HMap := CreateFileMapping( HFile, NIL, PAGE_READWRITE, 0, FSize, NIL); if HMap = 0 then // Ошибка: возбуждаем исключение raise Exception.Create('Ошибка отображения файла'); try // Создаем окно просмотра: AdrBase := MapViewOfFile(HMap, FILE_MAP_WRITE, 0, 0, FSize); if AdrBase = NIL then // Ошибка: возбуждаем исключение raise Exception.Create('Невозможно просмотреть файл'); // Сохраняем начальный адрес для правильной ликвидации // окна просмотра: AdrReal := AdrBase; for k := 1 to se.Value do begin AdrReal^ := Random; // Помещаем в файл новое число // Перед наращиванием текущего адреса необходимо // привести его к типу Integer или Cardinal: AdrReal := Pointer(Integer(AdrReal) + SizeOf(Real)); lbMem.Caption := IntToStr(k); pb.Position := k; Application.ProcessMessages; end; // Освобождаем окно просмотра: UnmapViewOfFile(AdrBase) finally // Освобождаем отображение CloseHandle(HMap) end finally // Закрываем файл CloseHandle(HFile) end; // Сообщаем время счета pb.Hide; lbMem.Caption := TimeToStr(Time-BegTime) end; procedure TForm1.btFileClick(Sender: TObject); // Создание файла обычным методом var F: File of Real; k: Integer; BegTime: TDateTime; R: Real; // Буферная переменная для обращения к Write begin BegTime := Time; // Засекаем начальное время счета // Готовим ProgressBar: pb.Max := se.Value; pb.Position := 0; pb.Show; // Создаем файл: AssignFile(F, 'test.dat'); Rewrite(F); for k := 1 to se.Value do begin R := Random; // Параметрами обращения к Write Write(F, R); // могут быть только переменные lbFile.Caption := IntToStr(k); pb.Position := k; Application.ProcessMessages; end; CloseFile(F); pb.Hide; lbFile.Caption := TimeToStr(Time-BegTime) end; end. Проект создает дисковый файл, состоящий из 100 тыс. случайных вещественных чисел (можно выбрать другую длину файла, если изменить значение редактора Длина массива). Файл с именем test.dat создается путем отображения файла в память (кнопка Память) и традиционным способом (кнопка Файл). В обоих случаях показывается время счета. Чем больше частота процессора и объем свободной оперативной памяти, тем больше будет разница во времени (листинг 3). О таймере Компонент Timer (таймер) служит для отсчета интервалов реального времени. Его свойство Interval определяет интервал временив миллисекундах , который должен пройти от включения таймера до наступления события OnTimer. Таймер включается при установке значения True в его свойство Enabled. Единожды включенный таймер все время будет возбуждать события OnTimer до тех пор, пока его свойство Enabled не примет значения False. Следует учесть, что в силу специфики реализации стандартного аппаратного таймера IBM-совместимого компьютера минимальный реально достижимый интервал отсчета времени не может быть меньше 55 мс (этот интервал называется тиком), более того, любой интервал времени, отсчитываемый с помощью таймера, всегда кратен 55 мс. Чтобы убедиться в этом, проведите эксперимент, в котором подсчитывается среднее время между двумя срабатываниями таймера (Timer.dpr): Начните новый проект с пустой формой и положите на нее компонент TTimer. Установите в свойство Enabled таймера значение False. Напишите такой модуль главной формы (листинг 4): Листинг 4 unit Unit1; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, Buttons, ExtCtrls; type TfmExample = class(TForm) Panel1: TPanel; bbRun: TBitBtn; bbClose: TBitBtn; edInput: TEdit; lbOutput: TLabel; mmOutput: TMemo; Timer1: TTimer; procedure bbRunClick(Sender: TObject); procedure Timer1Timer(Sender: TObject); procedure FormActivate(Sender: TObject); private { Private declarations } BegTime: TDateTime; // Начальное время цикла Counter: Integer; // Счетчик цикла public { Public declarations } end; var fmExample: TfmExample; implementation {$R *.DFM} procedure TfmExample.bbRunClick(Sender: TObject); // Запускает таймер. edInput содержит период его срабатывания. var Delay: Word; begin // Проверяем задание интервала if edInput.Text='' then Exit; try Delay := StrToInt(edInput.Text); except ShowMessage('Ошибка в записи числа'); edInput.SelectAll; edInput.SetFocus; Exit end; Counter := 0; // Сбрасываем счетчик Timer1.Interval := Delay; // Устанавливаем интервал BegTime := Time; // Засекаем время Timer1.Enabled := True; // Пускаем таймер Screen.Cursor := crHourGlass end; procedure TfmExample.Timer1Timer(Sender: TObject); var h, m, s, ms: Word; // Переменные для декодирования времени const MaxCount = 55; // Количество срабатываний таймера begin Counter := Counter + 1; // Наращиваем счетчик срабатываний if Counter=MaxCount then // Конец цикла? begin // - Да Timer1.Enabled := False; // Останавливаем таймер // Находим среднее время срабатывания: DecodeTime((Time-BegTime)/MaxCount, h, m, s, ms); mmOutput.Lines.Add( // Выводим результат Format('Задано %s ms. Получено %d ms.', [edInput.Text, ms])); edInput.Text := ''; // Готовим следующий запуск edInput.SetFocus; Screen.Cursor := crDefault end; end; procedure TfmExample.FormActivate(Sender: TObject); begin edInput.SetFocus end; end. Необходимость нескольких (MaxCount) срабатываний для точного усреднения результата связана с тем, что системные часы обновляются каждые 55 мс. После запуска программы и ввода 1 как требуемого периода срабатывания в редакторе mmOutput вы увидите строку Задано 1 ms. Получено 55 ms. в которой указывается, какое реальное время разделяет два соседних события OnTimer. Если вы установите период таймера в диапазоне от 56 до 110 мс, в строке будет указано 110 ms и т.д. (в силу дискретности обновления системных часов результаты могут несколько отличаться в ту или иную сторону). В ряде практически важных областей применения (при разработке игр, в системах реального времени для управления внешними устройствам и т.п.) интервал 55 мс может оказаться слишком велик. Современный ПК имеет мультимедийный таймер, период срабатывания которого может быть от 1 мс и выше, однако этот таймер не имеет компонентного воплощения, поэтому для доступа к нему приходится использовать функции API. Общая схема его использования такова. Сначала готовится процедура обратного вызова (call back) с заголовком: procedure TimeProc(uID, uMsg: UINT; dwUser, dw1, dw2: DWORD); stdcall; Здесь uID — идентификатор события таймера (см. об этом ниже); uMsg — не используется; dwUser — произвольное число, передаваемое процедуре в момент срабатывания таймера; dw1, dw2 — не используются. Запуск таймера реализуется функцией: function timeSetEvent(uDelay, uResolution: UINT; lpTimeProc: Pointer; dwUser: DWORD; fuEvent: UINT): UINT; stdcall; external 'winmm.dll'; Здесь uDelay — необходимый период срабатывания таймера (в мс); uResolution — разрешение таймера (значение 0 означает, что события срабатывания таймера будут возникать с максимально возможной частотой; в целях снижения нагрузки на систему вы можете увеличить это значение); lpTimeProc — адрес процедуры обратного вызова; dwUser — произвольное число, которое передается процедуре обратного вызова и которым программист может распоряжаться по своему усмотрению; fuEvent — параметр, управляющий периодичностью возникновения события таймера: TIME_ONESHOT (0) — событие возникает только один раз через uDelay миллисекунд; TIME_PERIODIC (1) — события возникают периодически каждые uDelay мс. При успешном обращении функция возвращает идентификатор события таймера и 0, если обращение было ошибочным. Таймер останавливается, и связанные с ним системные ресурсы освобождаются функцией: function timeKillEvent(uID: UINT): UINT; stdcall; external 'winmm.dll'; Здесь uID — идентификатор события таймера, полученный с помощью timeSetEvent. В следующем примере (Timer.dpr) иллюстрируется использование мультимедийного таймера (листинг 5). Листинг 5 unit Unit1; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, Buttons, ExtCtrls; type TfmExample = class(TForm) Panel1: TPanel; bbRun: TBitBtn; bbClose: TBitBtn; edInput: TEdit; lbOutput: TLabel; mmOutput: TMemo; procedure bbRunClick(Sender: TObject); procedure FormActivate(Sender: TObject); private { Private declarations } public { Public declarations } end; var fmExample: TfmExample; implementation {$R *.DFM} // Объявление экспортируемых функций: function timeSetEvent(uDelay, uReolution: UINT; lpTimeProc: Pointer; dwUser: DWORD; fuEvent: UINT): Integer; stdcall; external 'winmm'; function timeKillEvent(uID: UINT): Integer; stdcall; external 'winmm'; // Объявление глобальных переменных var uEventID: UINT; // Идентификатор события таймера BegTime: TDateTime; // Засекаем время< Counter: Integer; // Счетчик повторений Delay: Word; // Период срабатывания procedure ProcTime(uID, msg: UINT; dwUse, dw1, dw2: DWORD); stdcall; // Реакция на срабатывание таймера (процедура обратного вызова) var h, m, s, ms: Word; // Переменные для декодирования времени const MaxCount = 55; // Количество повторений begin timeKillEvent(uEventID); // Останавливаем таймер Counter := Counter+1; // Наращиваем счетчик if Counter=MaxCount then // Конец цикла? begin // - Да: декодируем время DecodeTime((Time-BegTime)/MaxCount, h, m, s, ms); fmExample.mmOutput.Lines.Add( // Сообщаем результат Format('Задано %s ms. Получено %d ms', [fmExample.edInput.Text,ms])); fmExample.edInput.Text := ''; // Готовим повторение fmExample.edInput.SetFocus end else // - Нет: вновь пускаем таймер uEventID := timeSetEvent(Delay,0,@ProcTime,0,1); end; procedure TfmExample.bbRunClick(Sender: TObject); // Запускает таймер. edInput содержит требуемый период. begin // Проверяем задание периода if edInput.Text='' then Exit; try Delay := StrToInt(edInput.Text) except ShowMessage('Ошибка ввода числа'); edInput.SelectAll; edInput.SetFocus; Exit end; Counter := 0; // Сбрасываем счетчик BegTime := Time; // Засекаем время // Запускаем таймер: uEventID := timeSetEvent(Delay,0,@ProcTime,0,1); if uEventID=0 then ShowMessage('Ошибка запуска таймера') end; procedure TfmExample.FormActivate(Sender: TObject); begin edInput.SetFocus end; end.
|