16+
ComputerPrice
НА ГЛАВНУЮ СТАТЬИ НОВОСТИ О НАС




Яндекс цитирования


Версия для печати

Модуль поиска не установлен.

Организация печати растрового изображения в приложениях для Windows

16.12.2003

Александр Теут <2alex@tut.by>

Однако в реальных приложениях Windows с печатью возникает немало проблем, поскольку толком не описано, как же реализуются сколько-нибудь нетривиальные возможности печати.
Фень Юань. "Программирование графики для Windows"

Начиная создавать пользовательские интерфейсы программ под Windows с помощью библиотеки визуальных компонент (VCL), невольно приходишь к мысли, что разработчики Delphi и C++ Builder свели эту работу для программистов к перемещению мышкой по "Палитре компонент" и нескольким щелчкам её левой или правой кнопки в определённых местах формы. И, действительно, до некоторой степени такая точка зрения справедлива, а такой способ программирования приемлем: при сноровке, можно довольно быстро раскидать различные контролы по форме и далее полностью сосредоточиться на реализации функций, которые они будут выполнять, что позволяет создавать сложные и одновременно качественные программные продукты в кратчайшие сроки.

Но, с приобретением опыта и усложнением задач, становится тесно в рамках только VCL и приходится всё чаще (и надо признать не без удовольствия) окунаться в мир функций пользовательского интерфейса (API) и, в частности, интерфейса графических устройств (GDI), благо обширнейшая справка по ним поставляется вместе с вышеназванными продуктами.

В данной статье мы опишем процедуру вывода на печать растрового изображения средствами VCL и GDI. Сразу подчеркнём, что при кажущейся своей простоте - ведь имеется компонент PrinterDialog и класс TPrinter, вопросы вывода изображения на принтер, пожалуй, являются лидерами по трафику в конференциях по Delphi и C++ Builder.

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

Растровые изображения в ОС Windows

Растровое изображение представляет собой массив элементов (пикселей) каждый из которых имеет определённый цвет. В настоящее время Windows поддерживает три типа растров: аппаратно-независимый растр (DIB -Device-Independent Bitmap), DIB-секция и аппаратно-зависимый растр (DDB - Device-Dependent Bitmap). К объектам GDI относятся последние два из этого списка, то есть, в основном, только с ними можно работать в рамках GDI. Возможности последней для работы с аппаратно-независимыми растрами сильно ограничены и сводятся всего лишь к двум функциям, позволяющим отображать их на контексты графических устройств. В то же время преимуществом DIB является то, что в отличие от двух других типов растров он содержит в себе полную цветовую информацию, что позволяет выводить такой растр на любом графическом устройстве. Самым распространённым примером подобного типа растра является файл с расширением BMP. Он описывается рядом структур и массивом байтов и схематично представлен на рис.1.

Сразу отметим, мы рассматриваем в данной статье только одну из возможных версий подобных файлов, но зато самую распространённую в настоящее время.

Файл начинается с заголовка, представляющего собой структуру BITMAPFILEHEADER:

typedef struct tagBITMAPFILEHEADER {
WORD bfType;
DWORD bfSize;
DWORD bfReserved1;
DWORD bfReserved2;
DWORD bfOffBits;
} BITMAPFILEHEADER;

Заголовок позволяет приложениям определять, что данный файл содержит данные в формате BMP.

Далее следует ещё один заголовок, но уже блока описания растра - это структура BITMAPINFO. Она объявляется следующим образом:

typedef struct tagBITMAPINFO {
BITMAPINFOHEADER bmiHeader;
RGBQUAD bmiColors[1];
} BITMAPINFO;

Как видно, членами данной структуры, в свою очередь, являются две другие структуры.

Структура BITMAPINFOHEADER, с помощью своих членов, описывает, в частности, геометрические размеры изображения, цветовую глубину пиксела, тип сжатия растра, размер растра и т.д. Более подробную информацию об этой структуре можно почерпнуть из справочного файла по WinAPI.

Массив структур RGBQUAD описывает цветовую таблицу растра в том случае, когда информация о цвете не содержится в самих пикселах. Сама структура RGBQUAD содержит четыре члена:

typedef struct tagRGBQUAD {
BYTE rgbBlue;
BYTE rgbGreen;
BYTE rgbRed;
BYTE rgbReserved;
} RGBQUAD;

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

Аппаратно-независимый растр без файлового заголовка, а именно с таким объектом работают в GDI, называют упакованным (packed) DIB-растром.

В отличие от DIB, формат DDB-растров известен только драйверу того графического устройства, в котором он выбран. Преимуществом аппаратно-зависимых растров является то, что для работы с ними в GDI существует большое количество функций и структур, к тому же отображение DDB на контексте устройства происходит очень быстро. С другой стороны, так как формат DDB является внутренним, вы не получаете прямого доступа к пикселам растра, и следовательно, не можете обеспечить надёжное копирование его на другое графическое устройство. Это является основной причиной всех трудностей организации печати растров созданных на базе аппаратно-зависимых растров, и приводит к необходимости конвертации DDB в универсальный формат, понятный всем графическим устройствам, то есть в DIB.

Процедура данного преобразования основывается на общей структуре упакованного DIB-растра. То есть, необходимо проинициализировать структуру BITMAPINFO и массив пикселей, извлекая необходимые значения из исходного DDB-растра.

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

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

Построение пользовательского интерфейса и организация загрузки изображения

Создадим рабочее приложение, которое даст нам возможность распечатать растровое изображение. В качестве среды программирования будем использовать Borland C++ Builder v.5.0.

Открываем новый проект и помещаем в форму шесть компонент (рис.2): BitBtn1, BitBtn2, BitBtn3, ScrollBox1, PaintBox1 и OpenPictureDialog1 из "Палитры компонент".

Назначение кнопок можно понять из надписей на рисунке 2. Компонент ScrollBox очень удобен в качестве контейнера для других компонентов, не имеющих среди своих свойств вертикальной и горизонтальной прокрутки. Как раз таким компонентом и является PaintBox, который мы будем использовать для отображения на форме растрового изображения. Одним из основным его свойств является - Canvas, представляющее собой экземпляр класса TCanvas, с помощью которого выполняется большая часть графических операций в приложениях C++ Builder и Delphi (рис. 2).

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

Для работы с растровыми изображениями незаменим VCL класс TBitmap, он, по сути, является оболочкой объекта GDI DIB-секция. Объявим переменную Bitmap в секции private заголовочного файла:

private: // User declarations
Graphics::TBitmap* Bitmap;
В секции public опишем прототип деструктора формы:
public: // User declarations
__fastcall ~TForm1();

Дело в том, что хорошим стилем программирования в C++ считается создание экземпляров класса в конструкторе, а их уничтожение в деструкторе, что несколько отличается от программирования с использованием Delphi, где для подобных целей, как правило, применяются обработчики событий для формы, соответственно, OnCreate и OnDelete.

Создадим в конструкторе формы объект TBitmap и сразу явно зададим его пиксельный формат:

__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
Bitmap=new(Graphics::TBitmap);
Bitmap->PixelFormat = pf24bit;
}

Кстати, по умолчанию, формат пиксела определяется текущими установками цветовой палитры экрана монитора компьютера.

Далее, заранее побеспокоимся об обеспечении уничтожения нашего объекта при закрытии программы, для этого нам понадобится деструктор:

__fastcall TForm1:: ~TForm1()
{
delete Bitmap;
}

Теперь у нас имеется объект, в который мы сможем загрузить растровое изображение.

Растровые изображения будем брать из файлов с расширением BMP. Для этой цели мы подготовили два компонента: BitBtn3 и OpenPictureDialog1. Нам необходимо при щелчке по кнопке BitBtn3 вызвать стандартный диалог открытия файлов, загрузить растровое изображение в Bitmap и нарисовать его на канве компонента PaintBox1. Для решения этой задачи наполним программным кодом тело обработчика события OnClick компонента BitBtn3.

В самом начале вызываем окно навигации для открытия файлов - это одна строка!

if (!OpenPictureDialog1 -> Execute()) return;

Также просто реализована в VCL загрузка аппаратно-независимого растра в объект класса TBitmap:

Bitmap->LoadFromFile (OpenPictureDialog1 -> FileName);

Здесь следует ещё раз обратить внимание на мощь библиотеки VCL. Если реализовывать загрузку подобного типа файлов средствами WinAPI, пожалуй, не обойтись без сотни строк кода.

Далее, предвидя повторяемость операции загрузки изображения, будем возвращать вертикальную и горизонтальную прокрутки компонента ScrollBox1 в начальное положение:

ScrollBox1->HorzScrollBar->Position=0;
ScrollBox1->VertScrollBar->Position=0;

Настало время подготовить компонент PaintBox1 к выводу на него изображения, содержащегося к данному моменту в объекте Bitmap. Вся подготовка сводится к указанию геометрических размеров PaintBox1:

PaintBox1->Width=Bitmap->Width;
PaintBox1->Height=Bitmap->Height;

Затем вызовем событие, отвечающее за его перерисовку:

PaintBox1->Repaint();

Сама перерисовка программируется в обработчике события OnPaint компонента PaintBox1 и занимает всего одну строку в его теле:

PaintBox1->Canvas->Draw (0, 0, Bitmap);

На этом этапе, уже можно запустить проект на выполнение и поупражняться в загрузке различных изображений, рис.3.

Печать изображения

Печать изображения реализуем через вызов обработчика события OnClick кнопки BitBtn2.

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

Поскольку мы определили для нашего изображения 24-битный формат пиксела, то структура BITMAPINFO имеет только один член, а именно: BITMAPINFOHEADER.

Заполним её члены.

Сначала инициализируем структуру целиком:

BITMAPINFOHEADER bmiHeader={sizeof(BITMAPINFOHEADER),0};

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

bmiHeader.biWidth=Bitmap->Width;
bmiHeader.biHeight=Bitmap->Height;

Следующий член, начиная с 32-битных версий Windows, всегда должен равняться 1:

bmiHeader.biPlanes=1;

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

bmiHeader.biBitCount=24;

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

Определим размер массива, который будет содержать растр. Для этого, предварительно, необходимо найти размер строки растра в байтах. Используем стандартную формулу:

int BytesPerRow=((((Bitmap->Width * bmiHeader.biBitCount)+31)&~31)/8);

Если теперь найденную величину мы умножим на высоту изображения, то получим размер растра в байтах (кстати, это значение можно присвоить члену biSizeImage структуры BITMAPINFOHEADER):

bmiHeader.biSizeImage=BytesPerRow * Bitmap->Height;

Динамически создаём искомый массив для пикселей растра:

BYTE* pBits=new BYTE [bmiHeader.biSizeImage];

Далее нам потребуется манипулятор контекста устройства, в котором отображается наш растр, в данном случае это экран нашего монитора:

HDC ScreenDC=::GetDC(0);

Инициализируем конкретными значениями только что созданный массив, для этого используем функцию GetDIBits() (она позволяет копировать пикселы из DDB в DIB). Данная функция через свой 5 параметр возвращает указатель на массив пикселей растра, если в качестве других параметров ей указать манипулятор экрана, манипулятор нашего начального изображения, его геометрические размеры, указатель на структуру BITMAPINFO и, в общем случае, формат цветовой таблицы:

::GetDIBits(ScreenDC, Bitmap->Handle, 0, bmiHeader.biHeight, pBits, (BITMAPINFO*)&bmiHeader, DIB_RGB_COLORS);

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

Не забудем вернуть манипулятор экрана системе, чтобы избежать утечек ресурсов GDI:

::ReleaseDC (0, ScreenDC);

Здесь, для справки, укажем, что в VCL существуют две функции GetDIBSizes() и GetDIB() (они описаны в файле graphics.pas), применяя которые, также можно производить преобразование DDB->DIB, но такого контроля над процессом преобразования, как в методе, описанном выше, они не обеспечивают.

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

Будем создавать данный компонент динамически. Это позволит использовать приложение и в том случае, если у пользователя в системе не установлен ни один принтер (в противном случае при запуске программы будет возникать ошибка, приводящая к её немедленному закрытию). Разумеется, печатать без принтера не получится, но другие функциональные возможности программы останутся доступными. Кстати, трудно представить себе программу, ориентированную исключительно на печать, даже наша демонстрационная программа позволяет хотя бы просто просматривать изображения, содержащиеся в файлах с расширением BMP.

Следующий код, динамически создаёт объект компонента PrintDialog с минимальным набором опций:

TPrintDialog *PrintDlg;

PrintDlg=new TPrintDialog(this);

PrintDlg->Options.Clear();

PrintDlg->Options<<poPrintToFile;

PrintDlg->Copies=1;

Четвёртая строка нужна главным образом, для обеспечения возможности распечатать изображение в файл на этапе отладки программы, что позволяет экономить расходные материалы принтера. Более подробно тестирование печати мы ещё рассмотрим в данной статье.

Далее вызываем стандартное окно для настройки печати:

if(!PrintDlg->Execute())
{
delete [] pBits;
delete PrintDlg;
return;
}

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

Как мы уже упоминали, в VCL для подготовки и выполнения операций с печатью существует класс TPrinter. Методами и свойствами данного класса мы сейчас и воспользуемся.

Чтобы начать процесс печати, необходимо вызвать метод BeginDoc():

Printer()->BeginDoc();

Теперь нелишним будет проверить, поддерживает ли выбранный принтер аппаратную палитру, передав функции GetDeviceCaps() манипулятор контекста устройства, выбранного принтера Printer()->Canvas->Handle и индекс RASTERCAPS и затем выполнив над результатом, который вернёт данная функция, битовую операцию "AND" с константой RC_PALETTE:

bool bFlag =::GetDeviceCaps(Printer()->Canvas->Handle, RASTERCAPS) & RC_PALETTE;

Если, в итоге, булевая переменная bFlag будет равняться истине, то следует выбрать палитру объекта Bitmap в контекст устройства принтера и затем позволить системе адаптировать её к нему:

if (bFlag = = true)
{
HPALETTE hOldPal =static_cast<HPALETTE>(SelectPalette(Printer()->Canvas->Handle, Bitmap->Palette, FALSE));
RealizePalette(Printer()->Canvas->Handle);
}

Наконец, рисуем (печатаем) на контексте принтера, вызывая одну из немногих функций (их всего две), позволяющую отобразить аппаратно-независимый растр на графический контекст устройства:

StretchDIBits(Printer()->Canvas->Handle, 0, 0, Bitmap->Width, Bitmap->Height, 0,0, Bitmap->Width, Bitmap->Height, (CONST VOID*)pBits, (BITMAPINFO*)&bmiHeader, DIB_RGB_COLORS, SRCCOPY);

Именно для этой функции мы получали экземпляр bmiHeader структуры BITMAPINFOHEADER и указатель на массив пикселей растра pBits.

Если использовалась палитра, вернём контексту устройства исходную.

if (bFlag = = true)
{
SelectPalette(Printer()->Canvas->Handle, hOldPal, TRUE);
}

Осталось завершить процесс печати и уничтожить динамические переменные и объекты:

Printer()->EndDoc();
delete[] pBits;
delete PrintDlg;

В принципе, код, обеспечивающий печать растрового изображения, мы написали. Можно попробовать запустить приложение на выполнение, так как кнопка BitBtn2 должна уже функционировать. Но если мы сейчас попробуем распечатать наше изображение, результат печати разочарует. Его размер на листе бумаги, как видно из рис.4, будет неприемлемо мал по сравнению с исходным, представленным на рис.3. К тому же, он будет зависеть от выбранного разрешения печати принтера и размера нашего растра (рис. 4).

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

Настройка положения и размера изображения на листе бумаги

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

Размеры листа можно определить, передав функции GetDeviceCaps() индексы PHYSICALWIDTH и PHYSICALWIDTH:

int WidthPaper=GetDeviceCaps(Printer()->Canvas->Handle, PHYSICALWIDTH);
int HeightPaper=GetDeviceCaps(Printer()->Canvas->Handle, PHYSICALWIDTH);

Также просто получить и отступы, передавая индексы PHYSICALOFFSETX и PHYSICALOFFSETY:

int OffsetXPaper=GetDeviceCaps(Printer()->Canvas->Handle, PHYSICALOFFSETX);
int OffsetYPaper=GetDeviceCaps(Printer()->Canvas->Handle, PHYSICALOFFSETY);

Зная эти значения, легко вычислить размеры печатной области листа:

int WidthWork=WidthPaper-2*OffsetXPaper;
int HeightWork=HeightPaper-2*OffsetYPaper;

Теперь, чтобы распечатать изображение, например, на весь лист, надо вызвать функцию StretchDIBits() со следующими параметрами:

StretchDIBits(Printer()->Canvas->Handle, 0, 0, WidthWork, HeightWork, 0,0, Bitmap->Width, Bitmap->Height, (CONST VOID*)pBits, (BITMAPINFO*)&bmiHeader, DIB_RGB_COLORS, SRCCOPY);

Если необходимо распечатать изображение в определённом месте листа, используйте значения из интервала (0, WidthWork) по ширине листа и (0, HeightWork) по высоте, подставляя их вместо второго и третьего параметров функции StretchDIBits().

Важными характеристиками контекста устройства являются величины, определяющие количество пикселей на дюйм по его высоте и ширине. Найдём их для контекстов устройств экрана монитора и принтера соответственно:

float fScreenX=(float)::GetDeviceCaps(Canvas->Handle, LOGPIXELSX);
float fScreenY=(float)::GetDeviceCaps(Canvas->Handle, LOGPIXELSY);
float fPrinterX=(float)::GetDeviceCaps(Printer()->Canvas->Handle, LOGPIXELSX);
float fPrinterY=(float)::GetDeviceCaps(Printer()->Canvas->Handle, LOGPIXELSY);

С помощью этих величин мы можем вычислить ширину и высоту растрового изображения на листе бумаги принтера в пикселях и использовать их значения, когда требуется распечатать изображение в реальном масштабе:

int iWidthPrint=(int)((float) bmiHeader.biWidth * (fPrinterX / fScreenX));
int iHeightPrint=(int)((float) bmiHeader.biHeight * (fPrinterY / fScreenY));

В данном случае, вызывать функцию StretchDIBits() необходимо со следующими параметрами:

StretchDIBits(Printer()->Canvas->Handle, 0, 0, iWidthPrint, iHeightPrint, 0,0, Bitmap->Width, Bitmap->Height, (CONST VOID*)pBits, (BITMAPINFO*)&bmiHeader, DIB_RGB_COLORS, SRCCOPY);

Результат печати представлен на рис.6.

Прерывание печати

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

if (Printer()->Printing)
{
Printer()->Abort();
MessageDlg("Процесс печати прерван.", mtInformation, TMsgDlgButtons() << mbOK, 0);
}

То есть, сначала через свойство Printing проверяется - находится ли принтер в состоянии печати, а затем, в случае положительного результата, вызывается функция Abort(), которая и прекращает печать. И, наконец, не лишним будет информировать пользователя, о том, что произошло, показав окно с сообщением.

Тестовая печать

После написания программы, значительное время занимает её тестирование. Тестирование печати, кроме времени, ещё и довольно дорогостоящий процесс (принтер, бумага, картриджи), если не предпринять ряд дополнительных мер. Для этого нам понадобится сделать следующее:

1. Обеспечить в программе возможность печати в файл - это мы уже сделали.

2. Скачать из Интернета любой вьювер PostScript-файлов. Автор использовал программы GSview и Ghostscript (http://www.cs.wisc.edu/~ghost/).

3. Найти на CD c Windows и установить драйвер какого-нибудь PostScript-принтера (например, Tektronix Phaser 540).

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

Дальнейшие действия несложны. Даём команду на печать, в появившемся диалоговом окне "Настройка печати" выбираем из списка принтер (если их несколько), ставим галочку в квадрате "Печать в файл" и нажимаем кнопку OK (рис.5).

Далее появится ещё одно окно. В нём следует указать полное имя файла, в который будет производиться печать.

Для просмотра полученного таким способом файла (имеющего, между прочим, расширение prn), запускаем программу GSview и открываем в ней наш файл (рис.6).

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



статьи
статьи
 / 
новости
новости
 / 
контакты
контакты