Глава 19.
Печать и предварительный просмотр документов
По почерку принтера можно судить о том,
как нервничает компьютер.
Дмитрий Пашков
Для взаимодействия приложений с графическими устройствами, такими как экран дисплея, принтер или плоттер, в Windows используется интерфейс графических устройств (GDI). И когда приложению требуется выполнить операцию вывода графического изображения, оно осуществляет это посредством GDI и работает не с реальным (физическим), а с логическим устройством. Выполняя запрос приложения, GDI обращается к драйверу соответствующего устройства вывода, который уже непосредственно работает с физическим устройством. Такой подход позволяет приложениям Windows единообразно работать на любом оборудовании, лишь бы был установлен соответствующий драйвер. Само приложение обращается к устройству вывода посредством контекста устройства, который представляет собой структуру данных, содержащую информацию о том, как нужно выполнять операции вывода на данном устройстве. Эта структура при ее создании ассоциируется с драйвером конкретного физического устройства, преобразующего все направляемые ему вызовы в такие, которые им поддерживаются.
Все сказанное в полной мере относится и к выводу на принтер: сначала необходимо создать контекст отображения, связанный с принтером, куда затем и направлять весь графический вывод (как и при выводе на экран). Однако отличия все-таки есть. И связаны они, прежде всего, с тем, что при печати графический вывод осуществляется постранично, а листы бумаги могут иметь различные размеры и/или ориентацию (книжную или альбомную). За отслеживание этих параметров целиком отвечает программист. Есть и некоторые другие отличия.
Следует совершенно четко представлять, что вывод на принтер осуществляется исключительно через буфер, который, однако, служит не только для временного хранения данных. Все несколько сложнее. Дело в том, что при вызове функций рисования для контекста принтера эти команды GDI выполняются не сразу, а накапливаются в специальном метафайле. И только после того, как приложение завершит рисование одной страницы документа, созданный метафайл "проигрывается" в контексте принтера. Печать происходит именно в этот момент. Сам механизм скрыт от приложения, и единственное, что требуется, — это сообщить GDI о начале и завершении процесса печати листа.
Вторым важным моментом является то, что печать в системе Windows организована через специальную очередь, которая формируется приложением Print Manager (Диспетчер печати). Для печати документа приложения помещают в эту очередь свои данные (задания на печать), выводимые на принтер в фоновом режиме, в порядке поступления.
Для печати документов Win32 API предоставляет следующие функции:
Как видите, функций немного. А это значит, что всю основную работу необходимо выполнить самостоятельно. Сам же процесс графического вывода на принтер достаточно прост:
1. Прежде всего формируется контекст отображения для принтера.
2. Затем обеспечивается возможность принудительного (аварийного) завершения печати, заполняется структура DOCINFO и вызывается функция StartDoc.
3. После завершения подготовительных действий организуется цикл печати страниц документа, где должны быть проверены специально созданные флаги отмены и завершения печати. Вывод на принтер в Windows организуется постранично, поэтому перед началом печати каждой страницы необходимо вызвать функцию StartPage, которая устанавливает параметры специального метафайла, куда будет направляться вывод для всей страницы.
4. После того как вся страница записана в метафайл, следует закрыть его и отправить непосредственно на принтер. Для этой цели используется функция EndPage. Цикл выполняется для всех страниц и завершается либо аварийно, либо после вывода всего документа. В первом случае следует вызвать функцию AbortDoc, а во втором — EndDoc.
5. В заключение необходимо освободить используемый контекст устройства.
Примечание
Пока мы описываем этот процесс в упрощенном виде, не вдаваясь в некоторые детали, например, как определять окончание страницы или как настроить параметры принтера.
Подобный процесс, с теми или иными вариациями, выполняется при лю- \ бом выводе документа на печать.
После того как сложилось общее представление об организации процесса печати в Windows, вернемся к библиотеке MFC и рассмотрим, как реализован в ней вывод данных документа на принтер и другие связанные с этим вопросы. Начнем с выбора и настройки параметров принтера.
Выбор и настройка параметров принтера
Поскольку вся информация об установленных принтерах .хранится в самой операционной системе, то вполне логичным выглядит то, что за рассматриваемый процесс отвечает приложение. В классе CWinApp реализован специальный обработчик стандартной команды ID_FILE_PRINT_SETUP:
afx_msg void CWinApp::OnFilePrintSetup ()
Выводит на экран стандартный блок диалога Print Setup (Настройка печати) для настройки текущей конфигурации принтера.
Для подключения обработчика к команде в карту сообщений объекта "приложение" необходимо добавить оператор
ON_COMMAND(ID_FILE_PRINT_SETUP, CWinApp::OnFilePrintSetup)
В большинстве случаев этого стандартного обработчика вполне достаточно, но при необходимости можно создать и что-нибудь свое. Изобретать велосипед мы не будем, а для иллюстрации рассмотрим лишь, каким образом можно реализовать индивидуальную настройку стандартного блока диалога Print Setup. Прежде всего добавляем в карту сообщений класса приложения оператор
ON_COMMAND«ID_PRINT_SETUP>, <OnPrintSetup>)
Примечание
Как вы понимаете, имена в угловых скобках могут быть произвольными.
Затем в описании класса регистрируем этот обработчик:
class CNDApp : public CWinApp ,
{
...
//{{AFX_MSG(CNDApp)
...
afx_msg void OnPrintSetup();
...
//}}AFX_MSG
};
И, наконец, реализуем его. В данном примере при инициализации блока диалога устанавливаем размер бумаги Letter 8'/2 х 11 in и альбомную ориентацию (рис. 19.1).
void CNDApp::OnPrintSetup()
{
// Создаем объект "стандартный блок диалога
// Print Setup (Настройка печати)
CPrintDialog dlgPD(TRUE);
// Для получения параметров принтера, установленного
// по умолчанию, создаем объект
PRINTDLG pd;
// Устанавливаем указатель на глобальный блок памяти в NULL
// с тем, чтобы запросить у системы параметры принтера
pd.hDevMode = NULL;
// Позволяем получить текущую конфигурацию принтера
GetPrinterDeviceDefaults(Spd);
// Поскольку параметр pd.hDevMode указывает на глобальный
// перемещаемый блок памяти, перед началом работы с ним
// необходимо его зафиксировать, иначе ничего не получится
DEVMODE *dm = (DEVMODE *)LocalLock(pd.hDevMode);
// Теперь можно заполнять поля структуры DEVMODE
// значениями, необходимыми для инициализации блока диалога
// Print Setup
dm->dmFields = DMJDRIENTATION | DM_PAPERSIZE;
dm->dmOrientation = DMORIENT_LANDSCAPE;
dm->dmPaperSize = DMPAPER_LETTER;
// Передаем указатель на заполненную структуру DEVMODE
// объекту класса CPrintDialog
dlgPD.m_pd.hDevMode = pd. hDevMode;
// Теперь зафиксированный блок памяти можно освободить
LocalUnlock(pd.hDevMode);
// Выводим блок диалога на экран
if(dlgPD.DoModalf) == IDOK)
{
// Здесь можно зафиксировать выбор пользователя
...
}
}
Рис. 19.1. Стандартный блок диалога Print Setup
Возможностей для индивидуальных настроек достаточно много, но, повторяем, библиотека MFC достаточно хорошо выполняет значительную часть работы, и в большинстве случаев нет необходимости вмешиваться в этот процесс.
Для того чтобы узнать текущую конфигурацию принтера, мы воспользовались функцией:
BOOL CWinApp::GetPrinterDeviceDefaults (PRINTDLG *pd)
Возвращает информацию либо о принтере, заданном по умолчанию, либо о последней конфигурации принтера, установленной с помощью блока диалога Print Setup. Параметр pd указывает на структуру PRINTDLG, которая служит для настройки параметров блоков диалога Print (Печать) и Print Setup:
...
PRINTDLG pd;
...
// Запрашиваем параметры текущего
// или установленного по умолчанию принтера
...
GetPrinterDeviceDefaults(&pd);
Эта часть работы, конечно же, достаточно важна, и обеспечить ее поддержку необходимо, но библиотека MFC делает это прекрасно, а нас интересует дальнейший процесс. Итак, принтер выбран, его параметры установлены — можно печатать. Первоочередной задачей является, естественно, создание контекста устройства для выбранного принтера.
Библиотека MFC и здесь приходит на помощь — у класса приложения (CWinApp) имеется специальная функция: .
BOOL CWinApp::CreatePrinterDC (CDC &dc)
Создает контекст устройства (параметр dc) для выбранного в текущий момент принтера.
Перед тем как выводить что-либо на печать, достаточно вызвать эту функцию:
...
CDC dc;
// Создаем контекст устройства для принтера
AfxGetApp()->CreatePrinterDC(&dc);
// Теперь можно выводить информацию в контекст устройства dc
...
Вот и все. Не нужно каждый раз выполнять "утомительные" настройки специальных структур, как это приходится делать при использовании Win32 API.
Для того чтобы полностью разобраться в рассматриваемом вопросе, нам осталось только рассмотреть организацию собственно процесса печати.
Печать документов и библиотека MFC
Дальнейшее изложение мы будем вести применительно к архитектуре "документ/представление". Это не случайно. Дело в том, что в классах представлений реализована достаточно мощная поддержка процесса печати, которая избавит вас от многих дополнительных трудностей, освободив время для решения конкретных задач.
В процессе печати основная роль принадлежит объектам двух классов библиотеки MFC — контексту устройства и представлению, которые тесно взаимодействуют между собой. На рис. 19.2 представлена схема цикла печати, реализованная в библиотеке MFC.
Как видите, представленный цикл очень напоминает используемый в Windows для любого вывода на печать: на экран выводится блок диалога Print, создается контекст устройства для принтера и вызывается функция StartDoc
Рис. 19.2. Цикл печати библиотеки MFC
объекта CDC. Далее для каждой страницы документа библиотека вызывает функцию StartPage контекста принтера, осуществляет вывод данных в метафайл (функция OnPrint объекта "представление") и завершает печать страницы вызовом функции EndPage объекта CDC. Когда весь документ выведен на принтер, библиотека вызывает функцию EndDoc, завершая цикл печати. Разница заключается только в том, что в библиотеке MFC как сам цикл, так и все участвующие в нем функции уже реализованы. Наряду с этим, благодаря механизму виртуальных функций, программисту предоставляются широкие возможности для "более тонкой" настройки всего процесса печати. Виртуальные функции OnPreparePrinting, OnBeginPrinting, OnPrepareDC, OnPrint и OnEndPrinting определены в классе Cview, и при их переопределении необходимо соблюдать логику, заложенную в библиотеке MFC. Но сначала рассмотрим сами функции:
virtual BOOL CView::OnPreparePrinting (CPrintlnfo *plnfo)
Вызывается библиотекой MFC до начала процесса печати или предварительного просмотра документа. Это самый удобный момент для определения длины документа и настройки параметров печати. Для реализации последней возможности необходимо просто вызвать функцию DoPreparePrinting:
BOOL CView::OnPreparePrinting(CPrintlnfo *plnfo)
{
return DoPreparePrinting(pInfo);
}
В качестве параметра функция принимает указатель на структуру CPrintlnfo, которая хранит информацию о задачах печати или предварительного просмотра:
Примечание
О режиме предварительного просмотра документа см. далее в этом разделе.
struct CPrintlnfo
{
CPrintlnfo(); // Конструктор
~CPrintlnfo(); //Деструктор
CPrintDialog *m_pPD; // Указатель на объект "блок диалога Print"
BOOL m_bPreview; // TRUE, если предварительный просмотр'
BOOL m_bDirect; // TRUE, если без отображения на экране
// блока диалога Print .
BOOL m_bContinuePrinting; // FALSE для прерывания печати
UINT m_nCurPage; // Номер текущей печатаемой страницы
UINT m_bNumPreviewPages; // Число просматриваемых страниц
CString m_strPageDesc; // Номер изображаемой страницы
LPVOID m_lpUserData; // Указатель на данные пользователя
CRect m_rectDraw; // Прямоугольник, определяющий текущую
// используемую область страницы
void SetMinPage(UINT nMiriPage); // Устанавливает номер первой
// печатаемой страницы документа
void SetMaxPage(UINT nMaxPage); // Устанавливает номер последней
// печатаемой страницы документа
UINT GetMinPageO const; // Получает номер первой
// печатаемой страницы документа
UINT GetMaxPageO const; // Получает номер последней
// печатаемой страницы документа
UINT GetFromPage() const; // Получает номер первой
// печатаемой страницы диапазона
UINT GetToPageO const; // Получает номер последней
// печатаемой страницы диапазона
};
Объект этой структуры используется для обмена информацией между библиотекой и классом представления во время процесса печати. Он создается при выборе команд ID_FILE_PRINT_PREVIEW, ID_FILE_PRINT_DIRECT или ID_FILE_PRINT и разрушается после их выполнения.
Если возникнет необходимость инициализации блока диалога Print значениями, отличными от заданных по умолчанию, то лучше всего это сделать до вызова функции Do Prepare Printing.
BOOL CView::DoPreparePrinting (CPrintlnfo *plnfo)
Возвращает TRUE, если можно начинать печать или предварительный просмотр документа, и FALSE в противном случае. Поведение этой функции зависит от того, вызывается ли она для печати документа или для его предварительного просмотра, что определяется параметром m_bPreview структуры, на которую указывает plnfo. В первом случае на экран выводится блок диалога Print. После его закрытия функция создает контекст устройства на основе установок, сделанных пользователем в этом блоке диалога для принтера, который будет использоваться при печати документа. Если функция вызвана для предварительного просмотра документа, то контекст устройства создается на основе текущих установок принтера.
Для осуществления всех корректных установок в переопределенной версии этой функции обязательно следует вызвать функцию базового класса CView::DoPreparePrinting.
В приведенном ниже фрагменте кода демонстрируется принцип инициализации блока диалога Print в зависимости от того, имеется ли во введенном тексте выделенный фрагмент (рис. 19.3).
BOOL CNoteView::OnPreparePrinting(CPrintlnfo* plnfo)
{
if (plnfo != NULL && !pInfo->m_bPreview)
{
// Документ не будет иметь больше одной страницы
pInfo->SetMaxPage(l);
// Получаем указатель на документ, с которым
// ассоциировано текущее представление
CNoteDoc* pDoc = (CNoteDoc*)GetDocument();
// Получаем позицию первого представления, хранящегося
//в списке представлений, ассоциированных с документом
POSITION pos = pDoc->GetFirstViewPositipn();
// Получаем указатель на первое представление в списке
pDoc->GetNextView(pos);
// Получаем указатель на второе представление в списке
CTextView *pText = (CTextView *)pDoc->GetNextView(pos);
CString str;
// Читаем данные представления
pText->GetSelectedText(str);
iffIstr.IsEmptyO)
{
// Если есть выделенный фрагмент, то переключатель
// Print range (Печатать) устанавливаем в положение
// Selection (Выделенный фрагмент)
CPrintDialog *pPD = p!nfo->m_pPD;
pPD->mjpd.Flags &= ~PD_NOSELECTION;
pPD->m_pd.Flags |= PD_SELECTION;
}
}
// Если же выделенного фрагмента нет, то используем
// установки по умолчанию
//Не забываем вызвать функцию базового класса
// для проведения корректной инициализации
return DoPreparePrinting(plnfo);
}
Рис. 19.3. Стандартный блок диалога Print
Если необходим предварительный доступ к контексту устройства, то это можно сделать, переопределив функцию:
virtual void CView::OnBeginPrinting (
CDC *pDC,
CPrintlnfo *plnfo)
Вызывается библиотекой MFC перед началом процесса печати или предварительного просмотра после того, как была вызвана функция OnPreparePrinting.
Эта функция используется для размещения необходимых для печати ресурсов GDI, таких как карандаши или шрифты. Сами объекты GDI выбираются в контекст устройства в функции OnPrint отдельно для каждой страницы, которая их использует. Если одно и то же представление используется для вывода на экран и на принтер, рекомендуется использовать различные переменные для ресурсов GDI, необходимых для каждого изображения, что позволит обновлять их на экране непосредственно во время процесса печати. Кроме того, эту функцию можно использовать для инициализации, зависящей от свойств контекста устройства принтера, например, от числа печатаемых страниц. В качестве параметров используются указатели на контекст устройства (параметр pDC) и на структуру pPrintlnfo, которая описывает текущее задание печати (параметр plnfo).
Для корректного завершения процесса печати или предварительного просмотра документа необходимо использовать функцию:
virtual void CView::OnEndPrinting (
CDC *pDC,
CPrintlnfo *plnfo)
Вызывается библиотекой MFC после окончания процесса печати или предварительного просмотра. Если в функции OnBeginPrinting были задействованы какие-либо ресурсы GDI, то здесь их необходимо вернуть системе.
Если две последние рассмотренные функции используются для документа в целом, то для настройки параметров отдельной страницы разработчики реализовали функцию:
virtual void CView::OnPrepareDC (
CDC *pDC,
CPrintlnfo *plnfo = NULL)
Вызывается библиотекой MFC до функций OnDraw (предназначенной для изображения на экране) или OnPrint (используемой для каждой печатаемой или просматриваемой страницы). Если функция вызвана для печати документа, то она проверяет информацию о странице, имеющуюся в структуре, на которую указывает параметр plnfo. Если длина документа не определена, она принудительно прерывает печать после того, как будет выведена одна страница. Можно принудительно остановить процесс печати, записав в поле m_bContinuePrinting структуры plnfo значение FALSE. Параметры: pDC— указатель на контекст устройства, используемый для представления образа документа, и plnfo — указатель на структуру CPrintlnfo, описывающую текущее задание. В режиме печати или предварительного просмотра поле т_пСигРаде структуры CPrintlnfo определяет номер печатаемой страницы (если функция вызвана для вывода на экран, этот параметр должен быть установлен в NULL). При переопределении этой функции прежде всего следует вызвать обработчик OnPrepareDC базового класса.
Переопределение этой функции необходимо в следующих случаях:
Теперь, когда общая структура процесса печати достаточно ясна, самое время посмотреть, где же осуществляется собственно вывод (или, если угодно, рисование) данных документа. В главе 18 рассматривалась функция OnDraw, которая в рамках архитектуры "документ/представление" отвечает за вывод на любое устройство, будь то экран, принтер или что-либо другое. Его конкретный тип задается объектом контекста устройства, передаваемого функции в качестве параметра. Например, для вывода на экран она получает в качестве параметра указатель на объект класса CPaintDC, а для печати документа — на объект класса CDC, ассоциированного с текущим принтером.
В принципе, можно было бы просто переопределить функцию OnDraw для организации вывода на принтер, но ведь печать осуществляется на листы бумаги, имеющие вполне определенные размеры, которые необходимо учитывать. И для того чтобы не перегружать функцию OnDraw, разработчики библиотеки для работы с принтером реализовали в базовом классе всех представлений CView отдельную функцию:
virtual void CView::OnPrint ( .
CDC *pDC,
CPrintlnfd *plnfo = NULL)
Вызывается библиотекой MFC после вызова функции OnPrepareDC для печати или предварительного просмотра каждой страницы документа. Реализация по умолчанию просто вызывает функцию OnDraw с контекстом устройства принтера.
Переопределение этой функции необходимо, например, в следующих случаях:
Здесь есть два тонких момента, требующих пояснения. Пока речь идет о печати одной страницы, то все в порядке — можно использовать реализацию по умолчанию, в которой просто вызывается функция OnDraw, Но, как вы помните, для вывода на принтер нескольких страниц необходимо организовать цикл печати. И такой цикл действительно организован в обработчике:
void CView::OnFilePrint {)
Именно из него осуществляются вызовы всех рассмотренных функций, и в большинстве случаев вполне достаточно переопределить только те из них, которые необходимы для индивидуальной организации печати документа.
Второй момент, на который следует обратить внимание, связан с принудительным завершением вывода данных на принтер, т. е. с функциями AbortDoc и SetAbortProc из Win32 API. Как уже отмечалось, нужно установить при помощи функции SetAbortProc специальную функцию, которая и будет отслеживать требование на завершение печати, периодически вызывая функцию CDC::QueryAbort, сообщающую, можно ли продолжать процесс печати или его нужно срочно завершить.
Всех этих сложностей можно избежать, если оставить функцию CView:: OnFilePrint в покое. Пусть она делает свое дело, тем более, что у нее это получается хорошо.
Остановившись на последнем варианте (не переопределяя функцию OnFilePrint), продолжим рассмотрение процесса печати и того, как он реализован в библиотеке MFC.
Начиная по командам ID_FILE_PRINT или ID_FILE_PRINT_DIRECT прогресс печати, библиотека MFC вызывает функцию OnPreparePrinting нашего объекта "представление", передавая ей в качестве параметра указатель на уже подготовленную структуру CPrintlnfo. Поскольку в этот момент приложению неизвестно количество страниц печатаемого документа, то значения первой и последней печатаемой страницы устанавливаются, соответственно, в 1 и -1 (OxFFFF). Здесь удобнее всего определить длину документа (число его страниц), вызвав функцию SetMaxPage структуры CPrintlnfo. Второй задачей функции OnPrepare Printing является вызов Do Prepare Printing, которая выводит на экран блок диалога Print и создает контекст устройства для принтера. Если по каким-либо причинам вас не устраивает внешний вид блока диалога или начальные установки принтера, то перед вызовом этой функции их можно изменить. Те параметры, которые пользователь установил в блоке диалога, доступны через поля структуры CPrintlnfo. Так, например, если пользователь задал номера страниц, которые требуется напечатать, то их можно получить при помощи функций GetFromPage и GetToPage. Лаконичность последней фразы не должна создавать иллюзию, что все так уж просто. Для того чтобы воспользоваться функциями GetFromPage и" GetToPage, сначала необходимо проверить состояние группы переключателей Pagerange (Диапазон печати). Функции вернут достоверные значения только в том случае, если установлен переключатель Pages (Страницы).
Примечание
Следует иметь в виду, что при выполнении команды ID_FILE_PRINT_DIRECT блок диалога не выводится, а непосредственно начинается процесс печати документа.
Следующей функцией, вызываемой библиотекой MFC, является функция OnBeginPrinting. Ее наличие в цикле печати свидетельствует о похвальном желании разработчиков библиотеки предоставить программистам возможность именно здесь определиться и получить от системы все необходимые ресурсы GDI. Правда, на практике мало кто использует эту возможность — обычно ресурсы запрашиваются у системы непосредственно в функциях OnDraw или OnPrint и там же, кстати говоря, и возвращаются ей обратно.
После того как функция CDC::StartDoc сформирует задание на печать, начинается собственно цикл, включающий в себя две интересующих нас функции — OnPrepareDC и OnPrint, которые библиотека вызывает для каждой
печатаемой страницы, сообщая представлению ее номер в поле m_nCurPage структуры CPrintlnfo. Первая функция служит для решения трех основных задач:
Следует также отметить, что эта функция вызывается как для печати документа, так и для отображения его на экране. Различие заключается в первом передаваемом ей параметре -т- указателе на контекст принтера или экрана — и в том, что при выводе на экран в качестве второго параметра передается NULL. Для самой печати страницы предназначена функция OnPrint, и передаваемые ей параметры несут всю необходимую для этого информацию. Однако, особенно если посмотреть на ее реализацию по умолчанию в классе CView.
void CView::OnPrint(CDC* pDC, CPrintlnfo*)
{
ASSERT_VALID(pDC);
OnDraw(pDC);
}
приходишь к выводу, что более правильным подходом является такой, при котором эта функция "занимается своим делом", т. е. отвечает за изображение данных, специфичных именно для вывода на принтер, например, для печати колонтитулов и примечаний, оставляя всю остальную часть работы функции OnDraw.
Примечание
Действительно — зачем дублировать код, если в Windows организация вывода не зависит от конкретного устройства (аппаратно-независимый вывод).
Завершается цикл печати вызовом функций CDC::EndDoc и OnEndPrinting. И если первая функция завершает процесс печати документа, то вторая призвана освободить все использованные при печати ресурсы GDI.
Примечание
...если вы все-таки последовали рекомендациям и запросили их у системы в функции OnBeginPrinting.
Таким образом, для того чтобы "организовать" процесс печати, используя возможности библиотеки MFC, достаточно переопределить некоторые (или все) из функций OnPreparePrinting, OnBeginPrinting, OnPrepareDC, OnPrint и OnEndPrinting. Правда, и это немало — чего стоит только определение длины документа!
В заключение обсуждения организации вывода документа на принтер рассмотрим пример переопределения перечисленных функций. Подробные комментарии делают излишним более детальное их обсуждение. Скажем лишь, что рассматриваемые фрагменты демонстрируют вывод колонтитула и номера страницы при печати документа.
void CNoteView::OnPrint(CDC* pDC, CPrintlnfo* plnfo)
{
CNDApp *pApp = (CNDApp*)AfxGetApp();
// Создаем контекст устройства для принтера
CDC dc;
pApp->CreatePrinterDC(dc);
// Определяем размеры бумаги, исходя из установок принтера
CSize sizePaper = CSize(dc.GetDeviceCaps(HORZSIZE)*10,
dc.GetDeviceCaps(VERTSIZE)*10);
CRect rect(0, 0, sizePaper.ex, sizePaper.cy);
// Учитываем величины полей
rect.left += pApp->m_rtMargin.left;
rect.right -= pApp->m_rtMargin.right;
rect.top -= pApp->m_rtMargin.top;
rect.bottom += pApp->m_rtMargin.bottom;
// Преобразуем логические координаты в физические
pDC->LPtoDP(&rect);
// Изменяем параметры прямоугольника рисования
pIhfo->m_rectDraw.left = rect.left;
pInfo->ra_rectDraw.right = rect.right;
pInfo->m_rectDraw.top = rect.top;
pInfo->m_rectDraw.bottom = rect.bottom;
// Устанавливаем новые координаты области вывода
pDC->SetViewportOrg(rect.left, rect.top);
CString strHeader = GetDocument()->GetTitle();
// Печатаем заголовок
PrintHeader(pDC, plnfo, strHeader);
// Рисуем данные документа OnDraw(pDC);
}
void CNoteView::PrintHeader(CDC *pDC,
CPrintlnfo *plnfo,
CString strHeader)
{
// Устанавливаем выравнивание влево
pDC->SetTextAlign(TA_LEFT);
// Для учета величины смещения получаем
// характеристики шрифта
TEXTMETRIC tm;
pDC->GetTextMetrics(&tm);
int у = tm.tniHeight + 15;
// Выводим заголовок
pDC->TextOut(25, у, strHeader);
// Формируем смещение для линии
у -= tm.tmHeight +5;
// Определяем длину текста
CSize len = pDC->GetTextExtent(strHeader);
// Рисуем разделительную линию
pDC->MoveTo(0, у);.
pDC->LineTo(len.cx + 250, у);
// Устанавливаем выравнивание вправо
pDC->SetTextAlign(TA_RIGHT);
// Формируем строку с номером страницы
TCHAR szBuf[32];
wsprintf(szBuf, "%d", p!nfo->m_nCurPage);
// Выводим номер страницы в правом нижнем углу
pDC->TextOut(pInfo->m_rectDraw.right - 525,
pInfo->m_rectDraw.bottom — 1250, szBuf);
}
Предварительный просмотр документа
Помимо рассмотренных возможностей по выводу документа на экран и на принтер, библиотека MFC поддерживает еще один не менее важный и интересный режим. Речь идет о предварительном просмотре документа перед печатью, т. е. получении на экране образа одной или нескольких страниц в том виде, в каком они будут выведены на принтер.
Для начала перечислим те возможности, которые библиотека предоставляет "по умолчанию". Другими словами — то, что получаешь, добавив к своей программе единственную строчку кода в карту сообщений своего класса представления:
ON_COmMD(ID_FILE_PRINT_PREVIEW, CScrollView: :OnFilePrintPreview)
Примечание
...если за вас это еще не сделал мастер AppWizard (надеемся, что не требует пояснения возможность использования вместо класса CScrollView любого другого класса представления).
А возможности достаточно широки и включают, помимо отображения уменьшенного образа одной или двух страниц документа, печать изображаемых страниц или всего документа, переход к предыдущей и/или следующей странице, масштабирование изображения и выход из режима предварительного просмотра. А если предварительно сообщить библиотеке MFC размер документа, т. е. число страниц, то она дополнительно выведет полосу прокрутки для перехода от одной страницы к другой.
Но... Нет предела совершенству — всегда хочется чего-то большего! И библиотека MFC предоставляет достаточно широкие возможности для реализации собственных замыслов. Начнем с самого простого, опустив вопросы различия контекстов принтера и экрана.
Для режима предварительного просмотра характерна та же последовательность действий, что и при выводе на печать (см. рис. 19.2). Библиотека MFC сама формирует необходимый контекст устройства. Поэтому, как и при печати документа, задачей программиста является всего лишь переопределение функций OnPreparePrinting, OnBeginPrinting, OnPrepareDC, OnPrint и OnEndPrinting. Поскольку эти действия достаточно просты, ограничимся несколькими фрагментами кода программы (с подробными комментариями), которые иллюстрируют описываемый материал на примере вывода рамки вокруг текста в режиме предварительного просмотра (рис. 19.4).
BOOL CNoteView::OnPreparePrinting(CPrintlnfo* plnfo)
{
if (plnfo != NULL && !pInfo->m__bPreview)
{
// Подготовка данных для инициализации блока диалога
// Print (Печать)
}
// Не забываем вызвать - функцию базового класса
return DoPreparePrinting(plnfo);
}
void CNoteView::OnPrint(CDC* pDC, CPrintlnfo* plnfo)
{
...
// В режиме предварительного просмотра рисуем рамку вокруг текста
if (plnfo != NULL && p!nfo->m_bPreview)
DrawMargins(pDC, rect);
...
// Выводим сам текст
OnDraw(pDC);
}
void CNoteView::DrawMargins(CDC* pDC, CRect &rt)
{
CNDApp *pApp = (CNDApp*)AfxGetApp();
if (pDC->m_hAttribDC != NULL)
{
CRect rect, rectMargin = pApp->m_rtMargin;
// Устанавливаем границы для рамки
rect.left = pApp->m_rtMargin.left — 5;
rect.right = rt.right — pApp->m_rtMargin.right + 10;
rect.top = -pApp->m_rtMargin.top;
rect.bottom = -rt.bottom + pApp->m_rtMargin.bottom;
// Создаем "карандаш"
CPen pen(PS_DOT, 0, pDC->GetTextColor());
// Выбираем его в контекст устройства
CPen* ppen = pDC->SelectObject(&pen);
// Рисуем рамку pDC->MoveTo(0, rect.top);
pDC->LineTo(10000, rect.top);
pDC->MoveTo(rect.left, 0);
pDC->LineTo(rect.left, -10000);
pDC->MoveTo(0, rect.bottom);
pDC->LineTo(10000, rect.bottom);
pDC->MoveTo(rect.right, 0) ;
pDC->LineTo(rect.right, -10000);
// Удаляем карандаш из контекста устройства
pDC->SelectObject(ppen);
}
}
Рис. 19.4. Режим предварительного просмотра документа
Рассмотренной техники вполне достаточно в подавляющем большинстве случаев. Однако разработчики библиотеки MFC изменили бы себе, если бы не предоставили дополнительные возможности для работы в этом-режиме.
Итак, при выполнении команды ID_FILE_PRINT_PREVIEW вместо непосредственного отображения образа документа на принтере необходима его эмуляция на экране. Для этого в библиотеке MFC реализованы два специальных класса: CPreviewDC и CPreviewView. Основную роль в эмуляции принтера играет объект класса CPreviewDC, производного от CDC. Как известно, все объекты CDC содержат члены двух контекстов устройства — собственно контекст устройства и контекст отображения, которые обычно совпадают. Но у объекта класса CPreviewDC они используются для разных целей: контекст устройства представляет принтер, а контекст отображения — экран, на который и осуществляется вывод. И когда приложение выполняет какие-либо операции, изменяющие характеристики контекста принтера, библиотека MFC автоматически выполняет необходимые операции .эмуляции для контекста экрана. Например, если приложение устанавливает какой-либо шрифт для печати, то библиотека выбирает шрифт для экрана, который эмулирует шрифт принтера.
Вторым классом, участвующим в организации работы режима предварительного просмотра, является CPreviewView, базирующийся на классе CView. Этот класс предоставляет поддержку для уникальных особенностей, окна предварительного просмотра, таких как отображение и функционирование панели инструментов, возможность просмотра одной или двух страниц, изменение масштаба изображения, например, увеличение просматриваемого образа. Класс спроектирован достаточно органично и в большинстве случаев нет надобности непосредственно вызывать или переопределять какую-либо его функцию. Другое дело, если требуется реализовать свой интерфейс для предварительного просмотра (например, если необходима поддержка редактирования в этом режиме). Для тех, кому это может понадобиться, рассмотрим последовательность действий.
Поскольку все начинается с выбора команды ID_FILE_PRINT_PREVIEW, которой соответствует обработчик OnFilePrintPreview, рассмотрим его реализацию, предоставляемую по умолчанию — ведь при переопределении придется делать нечто подобное:
void CView::OnFilePrintPreview()
{
CPrintPreviewState* pState = new CPrintPreviewState;
if (!DoPrintPreview(AFX_IDD_PREVIEW_TOOLBAR, this,
RUNTIME_CLASS(CPreviewView), pState))
{
TRACED("Error: DoPrintPreview failed.\n");
AfxMessageBox(AFX_IDP_COMMAND_FAILURE);
delete pState;
}
}
Для нас интерес здесь представляют структура библиотеки MFC CPrintPreviewState и функция DoPrintPreview. Начнем со структуры:
struct CPrintPreviewState {
UINT nIDMainPane; // Идентификатор скрываемой основной области
HMENU hMenu; // Сохраненный дескриптор меню
DWORD dwStates; // Состояние видимости панелей элементов
// управления
CView *pViewActive01d; // Сохраненное представление
// Указатель на функцию, позволяющую "безопасно" выйти
// из режима предварительного просмотра
BOOL(CALLBACK *lpfnCloseProc)(CFrameWnd *pFrameWnd);
HACCEL hAccelTable; // Сохраненная таблица командных клавиш
CPrintPreviewState(); // Конструктор
};
Основным ее назначением является сохранение параметров активного фрейма, которые он имел перед переходом в режим предварительного просмотра. При необходимости их в этом режиме можно использовать.
Основную роль играет, конечно, функция DoPrintPreview:
BOOL CView::DoPrintPreview (
UINT nIDResource,
CView* pPrintView,
CRuntimeClass* pPreviewViewClass,
CPrintPreviewState* pState)
Выполняет основную работу для подготовки и перехода в режим предварительного просмотра. Параметры: nIDResource— идентификатор ресурса панели инструментов в режиме предварительного просмотра; pPrintView— указатель на объект "представление", для которого выполняется предварительный просмотр (чаще всего используется значение this, но оно может быть и другим, если режим предварительного просмотра реализуется не для одного представления); pPreviewViewClass— указатель на runtime-структуру класса CPreviewView, что необходимо для динамического создания объекта этого класса во время выполнения функции; pState— указатель на структуру CPrintPreviewState (или производную от нее, если требуются какие-либо дополнительные параметры), которая должна создаваться именно здесь — функция DoPrintPreview немодальная, и структура существует до завершения работы в этом режиме.
Для того чтобы совершенно ясно представлять важность действий, выполняемых этой функцией, перечислим основные задачи, которые она решает:
Как видите, задач много, и если кто-то вместо предоставляемой DoPrintPreview захочет реализовать свою функцию, то не следует выпускать какую-либо из них из поля зрения. Но прежде чем это делать, стоит задаться вопросом: "А зачем?", и только после получения четкого ответа продолжать работать в этом направлении. В действительности библиотека MFC предоставляет достаточный простор для творчества и без такого рода работы, позволяя активно использовать механизм виртуальных функций. Вот и для режима предварительного просмотра она реализует две такие функции:
virtual void CFrameWnd::OnSetPreviewMode (
BOOL bPreview,
CPrintPreviewState *pModeStuff)
Переводит главное окно приложения в режим предварительного просмотра и обратно, в зависимости от значения параметра bPreview. Реализация по умолчанию блокирует все стандартные панели инструментов и скрывает основное меню и рабочую область главного окна приложения, т. е. переводит фрейм MDI во временный фрейм SDI. При переопределении следует вызывать аналогичную функцию базового класса для проведения необходимых инициализирующих действий. Параметр pModeStuff содержит указатель на структуру CPrintPreviewState.
virtual void CFrameWnd::OnEndPrintPreview (
CDC *pDC,
CPrintlnfo *plnfo POINT point,
CPreviewView *pView)
Вызывается библиотекой MFC, когда пользователь выходит из режима предварительного просмотра. Реализация этой функции по умолчанию вызывает функцию OnEndPrinting и восстанавливает главное окно приложения в том состоянии, которое оно имело до начала предварительного просмотра. При ее переопределении необходим вызов функции базового класса OnEndPrintPreview после всех дополнительных действий. Параметр point определяет последнюю изображаемую точку страницы в режиме предварительного просмотра, a pView указывает на используемый объект "представление". Именно внутри этой функции следует освободить занимаемую структурой CPrintPreviewState память. На структуру указывает pState, значение которого устанавливается функцией DoPrintPreview.
Представленных функций в подавляющем большинстве случаев вполне достаточно. Переопределяя функции, можно без лишних усилий решать такие, например, задачи:
Естественно, список может быть значительно шире. Мы же в заключение этого раздела приведем небольшой пример, иллюстрирующий некоторые из перечисленных возможностей.
void CDrawView::OnFilePrintPreview()
{
// При переопределении функции CView::OnFilePrintPreview
// необходимо самим создать структуру для сохранения
// текущего состояния фрейма
CPrintPreviewState* pState = new CPrintPreviewState;
// Обязательный вызов, в котором задаем идентификатор
// блока диалога для отображаемой панели инструментов
if (!DoPrintPreview(IDD_PREVIEW, this,
RUNTIME_CLASS(CPreviewView), pState))
{
// Для отображения в окне отладки
TRACED("Ошибка: DoPrintPreview отработала неправильно.\n");
// Для отображения на экране
AfxMessageBox(AFX_IDP_COMMAND_FAILURE);
delete pState;
}
}
Внешний вид новой панели инструментов, активизирующейся в режиме предварительного просмотра, представлен на рис. 19.5.
Поскольку как при выводе на принтер, так и в режиме предварительного просмотра необходимо привязываться к размерам листа бумаги, в качестве еще одного примера рассмотрим настройку блока диалога Page Setup (Параметры страницы) (рис. 19.6).
class CNoteView : public CScrollView
{
...
public:
//{(AFX_MSG(CNoteView)
afx_msg void OnPageSetup();
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};
Рис.19.5. Окно предварительного просмотра с измененной панелью инструментов
Рис. 19.6. Блок диалога Page Setup
BEGIN_MESSAGE_MAP(CNoteView, CScrollView)
//{{AFX_MSG_MAP(CNoteView)
ON_COMMAND(ID_PAGE_SETUP, OnPageSetup)
//}}AFX_MSG_MAP
...
END_MESSAGE_MAP()
...
void CNoteView::OnPageSetup()
{
CNDApp *pApp = (CNDApp*)AfxGetApp();
// Создаем объект блока диалога Page Setup
CPageSetupDialog dig;
Настройка внешнего вида осуществляется с помощью структуры
AGESETUPDLG& psd = dlg.m_psd;
// Будем настраивать значения полей, а размеры задаются
//в сотых долях миллиметра
psd.Flags |= PSD_MARGINS | PSD_INHUNDREDTHSOFMILLIMETERS;
// Устанавливаем необходимые значения
psd.rtMargin.left = pApp->m_rtMargin.left*10;
psd.rtMargiii. right = pApp->m_rtMargin.right*10;
psd.rtMargin.top = pAppJ>m_rtMargin.top*10;
psd.rtMargin.bottom = pApp->m_rtMargin.bottom*10;
// Для фиксации значений получаем параметры принтера
PRINTDLG pd;
pd.hDevNames = NULL;
pd.hDevMode = NULL;
pApp->GetPrinterDeviceDefaults(Spd);
psd.hDevNames = pd.hDevNames;
psd.hDevMode = pd.hDevMode;
// Выводим блок диалога на экран
if (dlg.DoModaH) == IDOK)
{
// Если пользователь нажал кнопку ОК, фиксируем его установки
pApp->m_rtMargin.left = psd.rtMargin.left/10;
pApp->m_rtMargin.right = psd.rtMargin.right/10;
pApp->m_rtMargin.top = psd.rtMargin.top/10;
pApp->m_rtMargin.bottom = psd.rtMargin.bottom/10;
// При необходимости заменяем принтер,
// используемый по умолчанию
pApp->SelectPrinter(psd.hDeVNames, psd.hDevMode, FALSE);
CDC dc;
pApp->CreatePrinterDC(dc);
// Получаем размеры бумаги
pApp->m_sizePaper = CSize(dc.GetDeviceCaps(HORZSIZE)*10,
dc.GetDeviceCaps(VERTSIZE)*10) ;
// После того как были изменены параметры страницы,
// необходимо обновить изображение данных на экране
CNoteDoc* pDoc = (CNoteDoc*)GetDocument ();
// Сообщение об обновлении передается всем представлениям,
// ассоциированным с документом, но реально предназначается
// только объекту класса, на который указывает this
pDoc->UpdateAHViews (NULL, 0, this);
}
}