Печать в 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.
|