Печать в WTL

Печать документов с использованием библиотеки WTL

Автор: Алексей Ширшов
The RSDN Group

Источник: RSDN Magazine #3-2003
Опубликовано: 09.11.2003
Версия текста: 1.0
Архитектура печати в Win32 API
Архитектура печати в WTL
Ошибки библиотеки WTL
Пример простой одностраничной печати
Многостраничная печать
Создание панели инструментов (тулбара) для предварительного просмотра
Заключение

Архитектура печати в Win32 API

Одним из основных достоинств вывода графической информации под Windows является абстрагирование от конкретного устройства вывода, будь то монитор, принтер или, скажем, какой-нибудь другое «экзотическое» устройство. С помощью одних и тех же функций GDI вы можете «рисовать» на любом устройстве. Однако сам процесс «рисования» при этом существенно различается. Это связано с тем, что при печати вы должны учитывать некоторые особенности принтера (например, разрешение), параметры бумаги (ориентация, размер) и проч.

Для печати создается специальный контекст принтера (printer device context). Создать контекст принтера можно с помощью функции CreateDC, которой передается имя принтера. Имя принтера, выбранного текущим пользователем в качестве «принтера по умолчанию», можно получить, воспользовавшись функцией GetDefaultPrinter, пример использования которой будет приведен ниже. К сожалению, функция GetDefaultPrinter доступна только в операционных системах Windows 2000/XP и старше. Поэтому в версиях ОС Windows 9х и NT 4.0 приходится пользоваться функцией GetProfileString, получающей принтер по умолчанию из системного ini-файла:

TCHAR szName[0x200];
GetProfileString(_T("Windows"),_T("Device"),_T(",,,"),szName, 
  sizeof szName/sizeof(TCHAR));

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

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

Вот примерный код того, как это может быть сделано:

      //Получение хэндла принтера по умолчанию
HANDLE hPrinter = NULL;
DWORD dwSz = 0;
DWORD dwLastErr = -1;
PTSTR pBuf = NULL;
while(!GetDefaultPrinter(pBuf, &dwSz))
{
  dwLastErr = GetLastError();
  if(dwLastErr == ERROR_INSUFFICIENT_BUFFER)
  {
    if(pBuf)
      delete[] pBuf;
    pBuf = new TCHAR[dwSz];
    dwLastErr = ERROR_SUCCESS;
  }
  elsebreak;
}

if(dwLastErr == ERROR_SUCCESS && OpenPrinter(pBuf, &hPrinter, NULL))
{

  // Получение структуры PRINTER_INFO_2 для данного хэндла
  LPBYTE pBufpi = 0;
  DWORD dwSzNeeded = 0;
  dwLastErr = -1;
  while(!GetPrinter(hPrinter, 2, pBufpi, dwSzNeeded, &dwSzNeeded))
  {
     dwLastErr = GetLastError();
  if(dwLastErr == ERROR_INSUFFICIENT_BUFFER)
    {
      if (pBufpi)
        delete[] pBufpi;
      pBufpi = new BYTE[dwSzNeeded];
      dwLastErr = ERROR_SUCCESS;
    }
    elsebreak;
  }

  if(dwLastErr == ERROR_SUCCESS)
  {
    LPPRINTER_INFO_2 pPI = (LPPRINTER_INFO_2)pBufpi;
    // Работаем со структурой
    HDC hDC = CreateDC(NULL, pPI->pPrinterName, NULL, pPI->pDevMode);
    // Работаем с контекстом// Удаляем контекст
    DeleteDC(hDC);
  }

  ClosePrinter(hPrinter);

  if(pBufpi)
    delete[] pBufpi;
}

if(pBuf)
  delete[] pBuf;

Итак, мы успешно создали контекст принтера, однако рисовать на нем пока еще рано.

ПРИМЕЧАНИЕ

Есть еще один способ получить контекст принтера. При вызове функций PrintDlg (или PrintDlgEx для Windows 2000/XP) в качестве одного из флагов структуры PRINTDLG можно указать PD_RETURNDC. Тогда после выбора принтера поле hDC этой структуры будет содержать действительный контекст принтера.

Дело в том, что весь вывод на печать организуется специальным процессом – спулером печати, а сам процесс печати разбивается на задания (print job) или документы. Документ – это условное понятие, которое логически определяется функциями StartDoc и EndDoc. При вызове функции StartDoc спулер печати создает расширенный метафайл (enhanced metafile). В него записываются функции GDI, вызванные для данного контекста принтера. Только после закрытия документа вызовом EndDoc расширенный метафайл выводится на принтер.

ПРИМЕЧАНИЕ

На самом деле ситуация выглядит более серьезно. :) В самом процессе печати используются несколько компонентов операционной системы: спулер печати (spooler), процессор печати (print processor) и монитор печати (print monitor). Спулер координирует работу остальных компонентов, поддерживает очереди печати и выполняет много другой работы, которая очень подробно описана в DDK. При печати в расширенный метафайл спулер создает специальный файл в системной директории (будем называть его кассетным файлом – spool file), который содержит записи метафайла, тип задания печати и другую информацию. После завершения работы с документом (при вызове функции EndDoc), спулер обращается к процессору печати, который знает, как преобразовывать записи кассетного файла в «сырой» поток данных для принтера. Процессор печати – это просто динамическая библиотека, экспортирующая определенный набор функций, которые описаны в DDK. Добавить свой процессор в систему можно с помощью вызова AddPrintProcessor. Для записи «сырого» потока данных в принтер, процессор использует стандартную функцию GDI – WritePrinter. Эта функция вновь вызывает спулер, однако данные, которые были выданы на принтер, уже не находятся в формате расширенного метафайла, поэтому спулер передает их монитору печати. Монитор печати – это тоже простая DLL, которая экспортирует определенный набор функций, описанных в DDK. Добавить свой монитор можно с помощью функции AddMonitor. Монитор печати предназначен для передачи пакетов данных конкретному драйверу через последовательные или параллельные порты, или через нестандартные сетевые каналы.

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

ПРИМЕЧАНИЕ

Работа функций StartPage и EndPage под Windows NT/2000/XP и Windows 9x существенно отличаются. Под Windows 9x функция StartPage сбрасывает настройки контекста отображения, так что при печати следующей страницы необходимо снова выбирать в контекст кисти, перья и другие объекты GDI. Под Windows NT/2000/XP, выбранные один раз GDI-объекты остаются в контексте принтера на протяжении печати всего документа. Если нужно сбросить контекст отображения, вызовите функцию ResetDC между печатью страниц.

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

  1. Получаем контекст принтера.
  2. Формируем задание на печать с помощью вызова функции StartDoc.
  3. Организуем цикл печати страниц документа.
  4. Перед печатью каждой страницы вызываем StartPage.
  5. После печати страницы вызываем EndPage, при этом данные записываются в кассетный файл спулера печати.
  6. После завершения печати всех страниц вызываем EndDoc, после чего спулер посылает запрос процессору печати, либо AbortDoc, после чего все записи удаляются из кассетного файла данной задачи .
  7. Освобождаем контекст устройства и другие ресурсы.

Кажется, все просто. Основная трудность связана с самим рисованием. Вызвано это тем, что, как правило, вам всегда нужно будет масштабировать текст, картинки и фигуры, которые выводятся на принтер. Дело в том, что разрешение принтера в пикселях намного больше разрешения монитора. Например, на моем принтере по умолчанию размер бумаги в пикселях 4476х6714, а у монитора – всего 1024x768. Почувствуйте разницу, как говорится! Мало того, что мониторы еще нескоро достигнут таких разрешений, маловероятно, что у них будут такие же пропорции горизонтальной и вертикальной осей.

Чтобы узнать размеры листа в пикселях, необходимо воспользоваться функцией GetDeviceCaps.

phys_x = GetDeviceCaps(hDC, PHYSICALOFFSETX);
phys_y = GetDeviceCaps(hDC, PHYSICALOFFSETY);

phys_cx = GetDeviceCaps(hDC, PHYSICALWIDTH) - 2 * phys_x;
phys_cy = GetDeviceCaps(hDC, PHYSICALHEIGHT) - 2 * phys_y;

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

LogFont.lfHeight = phys_cy/koff;

где koff (число строк на листе) я обычно варьирую в диапазоне от 60 до 90. При этом размер шрифта более-менее нормально масштабируется в зависимости от размеров листа бумаги. Если вы хотите, чтобы он зависел от общего коэффициента масштабирования, можно вычислить его по-другому:

LogFont.lfHeight = fontHeight*yScale/normal_yScale;

где fontHeight – высота шрифта (от 80 до 200), normal_yScale – коэффициент масштабирования при нормальном размере бумаги, yScale – текущий коэффициент масштабирования.

Коэффициент масштабирования вычисляется по формуле:

      double yScale = phys_cy/double(real_figure_height);

где real_figure_height – высота фигуры или экрана в пикселях.

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

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

Архитектура печати в WTL

Все, что касается печати в WTL, собрано в файле atlprint.h. В нем содержатся классы-помощники (helper classes) для функций WinAPI, таких как, например, GetPrinter, классы-обертки (wrapper classes) для структуры DEVMODE и хендла принтера, а также классы оконных представлений (views) для реализации функции предварительного просмотра документа. Начнем с самого главного – с функций печати.

Печать в WTL выполняется с помощью объектов класса CPrintJob. В интерфейс этого класса входят всего три функции: IsJobComplete, CancelPrintJob и StartPrintJob. Первые две не имеют параметров, их названия соответствуют их назначению, то есть функции производят проверку завершения печати и отмену печати, соответственно. StartPrintJob – это более сложная функция.

      bool StartPrintJob(
   bool bBackground,   //печать в фоне
   HANDLE hPrinter,   //хендл принтера
   DEVMODE* pDefaultDevMode,   
   IPrintJobInfo* pInfo,   
   LPCTSTR lpszDocName,   //имя документаunsignedlong nStartPage,   //начальная страница печатиunsignedlong nEndPage   //конечная страница печати
)

Рассмотрим параметры этой функции.

Функция StartPrintJob выполняет за вас следующее:

  1. Создает контекст принтера.
  2. Вызывает функцию StartDoc.
  3. Вызывает функцию IPrintJobInfo::BeginPrintJob.
  4. Входит в цикл, в котором перебирает каждую страницу документа:
  1. Вызывает IPrintJobInfo::EndPrintJob.
  2. Вызывает функцию WinAPI EndDoc. Если печать была прервана, вызывает функцию WinAPI AbortDoc.

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

IPrintJobInfo не является СОМ-интерфейсом, он не наследуется от IUnknown. Это просто абстрактный класс, все функции которого я перечислил в списке выше. Любой класс, который наследует этот интерфейс, может быть напечатан в прямом смысле слова при помощи объектов класса CPrintJob.

В файле atlprint.h уже содержится частичная реализация этого интерфейса – CPrintJobInfo. Она реализует все функции IPrintJobInfo, за исключением PrintPage. Если вам не нужна многостраничная печать, унаследуйте свой класс от этого класса и реализуйте метод PrintPage – это самое простое решение.

Кроме простой печати документа, библиотека WTL поддерживает функцию предварительного просмотра, который реализуется при помощи класса CPrintPreview. В состав этого класса входят только методы SetPrintPreviewInfo и SetPage. Работа с классом CPrintPreview кардинально отличается от работы с CPrintJob. Дело в том, что CPrintPreview выводит документ не на принтер, а в расширенный метафайл, который затем может быть воспроизведен на нормальном контексте отображения окна или сохранен на диск. С помощью функции SetPage в расширенный метафайл может быть выведена только одна страница, т.е. для одновременного просмотра нескольких страниц вам нужно реализовать свой аналог класса CPrintPreview. Второй метод SetPrintPreviewInfo предназначен для инициализации класса.

CPrintPreview не является классом окна. Функции вывода графической информации на экран предварительного просмотра реализует класс CPrintPreviewWindowImpl. Он является наследником CPrintPreview и CWindowImpl. В своей карте сообщений он содержит запись для обработки сообщения WM_PAINT, в котором и проигрывается полученный от CPrintPreview расширенный метафайл.

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

Ошибки библиотеки WTL

Предположим, в вашем оконном классе есть член m_printer класса CPrinter. В самом начале работы программы вы получаете хендл принтера по умолчанию с помощью метода CPrinter:: OpenDefaultPrinter. Рассмотрим код этой функции:

      bool OpenDefaultPrinter(const DEVMODE* pDevMode = NULL)
{
  ClosePrinter();
  TCHAR buffer[512];
  buffer[0] = 0;
  ::GetProfileString(_T("windows"), _T("device"), _T(",,,"), 
    buffer, sizeof(buffer));
  int nLen = lstrlen(buffer);
  if (nLen != 0)
  {
    LPTSTR lpsz = buffer;
    while (*lpsz)
    {
      if (*lpsz == ',')
      {
        *lpsz = 0;
        break;
      }
      lpsz = CharNext(lpsz);
    }
    PRINTER_DEFAULTS pdefs = { NULL, (DEVMODE*)pDevMode, PRINTER_ACCESS_USE };
    ::OpenPrinter(buffer, &m_hPrinter, (pDevMode == NULL) ? NULL : &pdefs);
  }
  return m_hPrinter != NULL;
}

Мало того, что этот метод не использует новую функцию Windows 2000 GetDefaultPrinter, в нем еще допущены грубые ошибки, выделенные в коде красным. Во-первых, размер буфера для передачи в GetProfileString вычисляется в байтах, а не в символах. Во-вторых, для сравнения символа строки используется байтовое представление символа «,». Вот исправленный вариант метода OpenDefaultPrinter.

      bool OpenDefaultPrinter(const DEVMODE* pDevMode = NULL)
{
  ClosePrinter();
#if _WIN32_WINNT >= 0x0500
  DWORD dwSz = 0x100;
  DWORD dwLastErr = -1;
  PTSTR pBuf = 0;
  while (!::GetDefaultPrinter(pBuf, &dwSz))
  {
    dwLastErr = ::GetLastError();
    if(dwLastErr == ERROR_INSUFFICIENT_BUFFER)
    {
      if (pBuf)
        delete[] pBuf;
      pBuf = new TCHAR[dwSz];
      dwLastErr = ERROR_SUCCESS;
    }
    elsebreak;
  }

  if(dwLastErr == ERROR_SUCCESS)
  {
    PRINTER_DEFAULTS pdefs = { NULL, (DEVMODE*)pDevMode, PRINTER_ACCESS_USE };
    ::OpenPrinter(pBuf, &m_hPrinter, (pDevMode == NULL) ? NULL : &pdefs);
  }

  if (pBuf) delete[] pBuf;


#else
  TCHAR buffer[512];
  buffer[0] = 0;
  ::GetProfileString(_T("windows"), _T("device"), _T(",,,"), buffer, sizeof(buffer)/sizeof(TCHAR));
  int nLen = lstrlen(buffer);
  if(nLen != 0)
  {
    LPTSTR lpsz = buffer;
    while (*lpsz)
    {
      if (*lpsz == _T(','))
      {
        *lpsz = 0;
        break;
      }
      lpsz = CharNext(lpsz);
    }
    PRINTER_DEFAULTS pdefs = { NULL, (DEVMODE*)pDevMode, PRINTER_ACCESS_USE };
    ::OpenPrinter(buffer, &m_hPrinter, (pDevMode == NULL) ? NULL : &pdefs);
  }
#endifreturn m_hPrinter != NULL;
}
ПРИМЕЧАНИЕ

В MSDN есть Q246772, где приведен код, динамически определяющий версию ОС и использующий наиболее подходящую для этой версии функцию. При этом используется динамическое связывание с функциями, что позволяет приложениям, использующим этот код, работать в ОС, не поддерживающих этих функций. На сегодняшний день получение принтера по умолчанию с помощью GetProfileString работает под всеми версиями ОС Windows, но не факт, что это положение не изменится. – примечание редакции.

Рассмотрим деструктор класса CPrintJob.

~CPrintJob()
{
  ATLASSERT(IsJobComplete()); //premature destruction?
}

...

bool IsJobComplete() const
{
  return m_bComplete;
}

Как видите, класс не дает себя уничтожить до тех пор, пока не завершится печать документа. В конструкторе и после печати документа, переменная m_bComplete устанавливается в true, однако перед печатью в фоновом режиме она устанавливается в false. Вот часть кода функции StartPrintJobInfo:

  m_bComplete = false;
...
  //Create a thread and return
  DWORD dwThreadID = 0;
  HANDLE hThread = ::CreateThread(NULL, 0, 
    StartProc, (void*)this, 0, &dwThreadID);
  ::CloseHandle(hThread);

  return (hThread != NULL);
}

Если поток по каким-либо причинам не создастся, переменная m_bComplete так и останется равной false, и вы получите ошибку в процессе уничтожения класса CPrintJob. Очевидно, что перед выходом из метода нужно установить переменную m_bComplete в true.

  m_bComplete = false;
...
  //Create a thread and return
  DWORD dwThreadID = 0;
  HANDLE hThread = ::CreateThread(NULL, 0, 
    StartProc, (void*)this, 0, &dwThreadID);
  ::CloseHandle(hThread);
  m_bComplete = true;

  return (hThread != NULL);
}

Метод SetPage класса CPrintPreview вычисляет размер бумаги в миллиметрах следующим образом:

      //логическая высота и ширина
      int iWidth = dcPrinter.GetDeviceCaps(PHYSICALWIDTH);
int iHeight = dcPrinter.GetDeviceCaps(PHYSICALHEIGHT);
//физическая высота и ширина в дюймахint nLogx = dcPrinter.GetDeviceCaps(LOGPIXELSX);
int nLogy = dcPrinter.GetDeviceCaps(LOGPIXELSY);

RECT rcMM = { 0, 0, ::MulDiv(iWidth, 2540, nLogx), ::MulDiv(iHeight, 2540, nLogy) };

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

RECT rcMM = {0, 0,
  dcPrinter.GetDeviceCaps(HORZSIZE)*100,
  dcPrinter.GetDeviceCaps(VERTSIZE)*100};

Зачем производится умножение на 100 полученных значений (а в предыдущем примере – на 2540 вместо 25,4)? Дело в том, что при создании расширенного метафайла необходимо указывать размеры прямоугольника в единицах, равных 0.01мм.

Другие ошибки стандартной реализации связаны с проектированием: при предварительном просмотре не вызываются методы IPrintJobInfo::BeginPrintJob и IPrintJobInfo::EndPrintJob. Если вы хотите в этих методах выбрать в контекст принтера нужные кисти, шрифт или еще что-нибудь, то сможете это сделать только при печати. Даже если включить эти функции в предварительный просмотр, вы все равно будете вынуждены выбирать нужные графические объекты для каждой страницы. Это связано с тем, что контекст расширенного метафайла создается и закрывается для каждой функции CPrintPreview::SetPage, следовательно, использовать один контекст отображения для предварительного просмотра всех страниц вы не можете. Для того чтобы вы могли печатать с помощью той же функции, которая выводит графическую информацию на экран (и для устранения различия между платформами 9х и NT), класс CPrintJobInfo специально сохраняет контекст перед печатью и восстанавливает его после печати страницы. Вы можете переопределить функции CPrintJobInfo::PrePrintPage и CPrintJobInfo::PostPrintPage, в которых производится сохранение и восстановление контекста соответственно, чтобы не выбирать нужные объекты при печати для каждой страницы. Но в этом случае вам нужно будет различать, когда производится печать, а когда – предварительный просмотр. К сожалению, интерфейс IPrintJob не имеет соответствующих функций проверки (чего-то типа IsInPreviewMode). Здесь у вас есть два выхода. Первый – это смириться с существующими ограничениями, второй - переписать/дописать соответствующий код WTL. Хорошей новостью для тех, кто выбрал второе, является то, что я уже переписал около половины файла atlprint.h и реализовал недостающую функциональность. Результат этой работы я приводить не буду, так как он займет слишком много места: этот и другие исправленные файлы библиотеки WTL вы можете найти на прилагающемся компакт-диске.

Еще одна ошибка, связанная с печатью, имеется в файле atldlgs.h. Как правило, для выбора принтера и настройки его параметров вызывается функция WinAPI PageSetupDlg. Она относится к библиотеке стандартных диалогов. В WTL «оберткой» этой функции служит класс CPageSetupDialog, точнее, он является «оберткой» для структуры PAGESETUPDLG. PageSetupDlg можно настроить таким образом, чтобы он «на лету» при изменении параметров листа выводил примерную картинку того, что будет напечатано. Делается это с помощью установки флага PSD_ENABLEPAGEPAINTHOOK в члене Flags структуры PAGESETUPDLG. После этого PageSetupDlg будет вызывать функцию, адрес которой содержится в члене lpfnPagePaintHook, и передавать в нее различные параметры, характеризующие этапы прорисовки. Детально этот процесс я рассматривать не буду, ограничившись рассказом о том, как работает WTL.

Класс CPageSetupDialog всегда выставляет флаг PSD_ENABLEPAGEPAINTHOOK и в качестве функции обратного вызова передает адрес следующей функции:

      static UINT CALLBACK PaintHookProc(HWND hWnd, UINT uMsg, 
  WPARAM wParam, LPARAM lParam)
{
  CPageSetupDialogImpl< T >* pDlg = (CPageSetupDialogImpl< T >*)hWnd;
  ATLASSERT(pDlg->m_hWnd == ::GetParent(hWnd));
  UINT uRet = 0;
  switch(uMsg)
  {
  case WM_PSD_PAGESETUPDLG:
    uRet = pDlg->PreDrawPage(LOWORD(wParam), HIWORD(wParam), 
      (LPPAGESETUPDLG)lParam);
    break;
  case WM_PSD_FULLPAGERECT:
  case WM_PSD_MINMARGINRECT:
  case WM_PSD_MARGINRECT:
  case WM_PSD_GREEKTEXTRECT:
  case WM_PSD_ENVSTAMPRECT:
  case WM_PSD_YAFULLPAGERECT:
    uRet = pDlg->OnDrawPage(uMsg, (HDC)wParam, (LPRECT)lParam);
    break;
  default:
    ATLTRACE2(atlTraceUI, 0, 
     _T("CPageSetupDialogImpl::PaintHookProc - unknown message received\n"));
    break;
  }
  return uRet;
}

Выделенная строка вызывает недоумение. WTL очень часто пользуется полиморфизмом времени компиляции, при котором вызываются методы производного класса из базового. В данном случае «полиморфными» методами могли бы быть PreDrawPage и OnDrawPage, но так как указатель this не приводится к производному классу, переопределение этих функций не дает никакого эффекта. Кроме этого, совершенно неясен смысл следующего за выделенной строкой утверждения (ASSERT’а).

Вот исправленный фрагмент функции PaintHookProc:

CPageSetupDialogImpl< T >* pDlg = (CPageSetupDialogImpl< T >*)hWnd;
//ATLASSERT(pDlg->m_hWnd == ::GetParent(hWnd));T* pT = static_cast<T*>(pDlg);
...
    case WM_PSD_PAGESETUPDLG:
      uRet = pT->PreDrawPage(LOWORD(wParam), HIWORD(wParam), 
        (LPPAGESETUPDLG)lParam);
...
    case WM_PSD_YAFULLPAGERECT:
      uRet = pT->OnDrawPage(uMsg, (HDC)wParam, (LPRECT)lParam);

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

      class CMyPageSetup : public CPageSetupDialogImpl<CMyPageSetup>
{
  typedef CPageSetupDialogImpl<CMyPageSetup> _baseClass;
public:
  CMyPageSetup(DWORD dwFlags = PSD_MARGINS|PSD_INWININIINTLMEASURE, 
    HWND hWndParent = NULL)
    :_baseClass(dwFlags, hWndParent)
  {
  }

  UINT OnDrawPage(UINT uMsg, HDC hDC, LPRECT lpRect)
  {
    CDCHandle hdc(hDC);
    switch(uMsg){
    case WM_PSD_GREEKTEXTRECT:
      //рисуемreturn 1;
    case WM_PSD_MARGINRECT:
      //выводим прямоугольникreturn 1;
    }
    return 0; // действия по умолчанию
  }
};

Пример простой одностраничной печати

Ниже приводится полный исходный текст WTL-приложения, которое может выводить строку на предварительный просмотр и печать. В нем не реализованы функции выбора принтера и настройки параметров листа: для печати используется принтер по умолчанию.

      // simpleprn.cpp : Defines the entry point for the application.
      //
      #include
      "stdafx.h"
      //Идентификаторы кнопок
      const
      int ID_PREVIEW_BTN = 1101;
constint ID_PRINT_BTN = 1102;

//Главное окно приложенияclass CMainWindow :
  public CWindowImpl<CMainWindow1,CWindow,CWinTraits<WS_OVERLAPPEDWINDOW> >,
  //Это нужно для предварительного просмотраpublic CPrintJobInfo
{
public:
  CMainWindow()
  {
    fPreview = false;
  }

//Закрытые членыprivate:
  CButton btnPreview,btnPrint;
  bool fPreview;
  CPrintPreviewWindow wndPreview;
  CPrinter m_printer;
  CString strPrint;

//Защищенные методыprotected:
  BEGIN_MSG_MAP(CMainWindow)
    MESSAGE_HANDLER(WM_DESTROY,OnDestroy)
    MESSAGE_HANDLER(WM_CREATE,OnCreate)
    MESSAGE_HANDLER(WM_KEYDOWN,OnKeyDown)
    COMMAND_ID_HANDLER(ID_PREVIEW_BTN, OnPreview)
    COMMAND_ID_HANDLER(ID_PRINT_BTN, OnPrint)
  END_MSG_MAP()

//Закрытые методыprivate:
  LRESULT OnCreate(UINT /*uMsg*/,WPARAM /*wParam*/, LPARAM /*lParam*/,BOOL& /*bHandled*/)
  {
    //Создаем кнопку предварительного просмотра
    btnPreview.Create(m_hWnd,CRect(10,10,110,30),_T("Preview"),
      WS_CHILD|WS_VISIBLE|WS_CLIPSIBLINGS|WS_CLIPCHILDREN|BS_PUSHBUTTON,
      0,ID_PREVIEW_BTN);

    //Устанавливаем шрифт по умолчанию
    btnPreview.SetFont(AtlGetDefaultGuiFont());
    
    //Создаем кнопку печати
    btnPrint.Create(m_hWnd,CRect(10,40,110,60),_T("Print"),
      WS_CHILD|WS_VISIBLE|WS_CLIPSIBLINGS|WS_CLIPCHILDREN|BS_PUSHBUTTON,
      0,ID_PRINT_BTN);

    btnPrint.SetFont(AtlGetDefaultGuiFont());
    
    //Открываем принтер по умолчанию
    m_printer.OpenDefaultPrinter();

    //Печатаемая строка
    strPrint = _T("Hi from WTL!");
    return 0;
  }

  //По Escape закрываем окно предварительного просмотра
  LRESULT OnKeyDown(UINT /*uMsg*/,WPARAM wParam, LPARAM /*lParam*/, 
    BOOL& /*bHandled*/)
  {
    if (wParam == VK_ESCAPE && fPreview)
    {
      wndPreview.DestroyWindow();
      fPreview = false;
      UpdateWindow();
    }
    return 0;
  }

  //Создаем окно предварительного просмотра
  LRESULT OnPreview(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, 
    BOOL& /*bHandled*/)
  {
    if (!fPreview){
      CRect rc;
      GetClientRect(&rc);
      
      //Получем структуру DEVMODE из принтера
      CDevMode dm;

      //GetPrinter(PRINTER_INFO_2)
      dm.CopyFromPrinter(m_printer);
      wndPreview.SetPrintPreviewInfo(m_printer,dm,this,0,0);
      wndPreview.Create(m_hWnd,rc,_T("Preview"),WS_CHILD|WS_VISIBLE);
      
      //Нужно вернуть фокус главному окну
      SetFocus();
      
      if (wndPreview.IsWindow())
        fPreview = !fPreview;
    }
    return 0;
  }

  LRESULT OnPrint(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, 
    BOOL& /*bHandled*/)
  {
    CPrintJob pj;
    CDevMode dm;
    dm.CopyFromPrinter(m_printer);

    //Печать занимает ровно одну строчку!
    pj.StartPrintJob(false,m_printer,dm,this,_T("Simple WTL Doc"),0,0);
    return 0;
  }

  LRESULT OnDestroy(UINT /*uMsg*/,WPARAM /*wParam*/, LPARAM /*lParam*/,
    BOOL& /*bHandled*/)
  {
    PostQuitMessage(0);
    return 0;
  }

public:

  //Вывод графической информацииbool PrintPage(UINT,HDC hdc)
  {
    CDCHandle dc(hdc);

    //Смещение
    CSize phys_offset(
      dc.GetDeviceCaps(PHYSICALOFFSETX)+100,
      dc.GetDeviceCaps(PHYSICALOFFSETY)+100
    );
    
    //Размер листа в пикселах
    CSize physical_size(
      dc.GetDeviceCaps(PHYSICALWIDTH) - 2 * phys_offset.cx,
      dc.GetDeviceCaps(PHYSICALHEIGHT) - 2 * phys_offset.cy
    );

    //Стандартный шрифт с нестандартным размером :) 
    LOGFONT LogFont = {0};
    GetObject(AtlGetDefaultGuiFont(), sizeof(LOGFONT), &LogFont);    
    LogFont.lfHeight = 320;
    CFont f;
    f.CreateFontIndirect(&LogFont);
    
    //Выбор шрифта
    dc.SelectFont(f);
    
    CRect r(0,0,physical_size.cx,physical_size.cy);

    //Рисуем по центру
    dc.DrawText(strPrint,strPrint.GetLength(),&r,DT_CENTER|DT_VCENTER|DT_SINGLELINE);
    
    returntrue;
  }
};

CAppModule _Module;

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
  LPSTR lpCmdLine,int nCmdShow)
{   

  _Module.Init(NULL,hInstance,NULL);

  CMainWindow mainwnd;
  mainwnd.Create(NULL,CWindow::rcDefault,_T("Very simple window"));
  mainwnd.ShowWindow(nCmdShow);
  mainwnd.UpdateWindow();

  CMessageLoop ml;
  int res = ml.Run();

  _Module.Term();
  
  return res;
}

Здесь комментировать нечего: программа состоит из одного класса CMainWindow, которое наследуется от CPrintJobInfo. Собственно печать производится в функции PrintPage, она же используется и для предварительного просмотра. Я специально ничего не выводил на экран в самом классе CMainWindow (отсутствует обработчик WM_PAINT), чтобы подчеркнуть, что печать совершенно не зависит от простого вывода графической информации на экран.

Многостраничная печать

Следующий пример не будет правильно работать со старым файлом atlprint.h по той причине, что для определения количества страниц документа необходимо знать параметры страницы. Идеальный вариант вычисления количества страниц – метод BeginPrintJob, который вызывается после вызова функции StartDoc. Однако при предварительном просмотре он вообще не вызывается. И что еще хуже, в самом BeginPrintJob невозможно определить, что производится – печать или предварительный просмотр. При использовании стандартной реализации многостраничная печать с предварительным просмотром является очень сложным делом.

Этот пример базируется на моих многочисленных исправлениях в файле atlprint.h. Он очень похож на предыдущий, поэтому я приведу лишь отличия.

В класс CMainWindow добавились следующие члены:

CSize m_physical_size;
CSize m_phys_offset;
UINT m_number_of_pages;
double m_xScale;
double m_yScale;
CFont print_font;
constint number_of_lines;

Конструктор класса CMainWindow

CMainWindow1():number_of_lines(6)
{
  fPreview = false;
  m_number_of_pages = 0;
  m_xScale = m_yScale = 0;
}

Обработчик нажатия клавиш. Для перехода по страницам используются курсорные клавиши.

      //По Escape закрываем окно предварительного просмотра
LRESULT OnKeyDown(UINT /*uMsg*/,WPARAM wParam, LPARAM /*lParam*/,
  BOOL& /*bHandled*/)
{
  if (fPreview){
    switch (wParam){
    case VK_ESCAPE:
      wndPreview.DestroyWindow();
      fPreview = false;
      UpdateWindow();
      break;
    case VK_LEFT:
      wndPreview.PrevPage();
      break;
    case VK_RIGHT:
      wndPreview.NextPage();
      break;
    }
  }
  return 0;

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

LRESULT OnPrint(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, 
  BOOL& /*bHandled*/)
{
  CPrintJob pj;
  CDevMode dm;
  dm.CopyFromPrinter(m_printer);

  //Печать занимает ровно одну строчку!
  pj.StartPrintJob(false, m_printer, dm, 
    this, _T("Simple WTL Doc"), 0, m_number_of_pages+1);
  return 0;
}

Переопределение всех функции класса CPrintJobInfo.

      //защищенные методы интерфейса IPrintJobInfo
      private:
  void PrePrintPage(UINT /*nPage*/, HDC hDC){}

  void PostPrintPage(UINT /*nPage*/, HDC hDC){}

  void BeginPrintJob(HDC hdc)
  {
    m_phys_offset.SetSize(
      ::GetDeviceCaps(hdc, PHYSICALOFFSETX)+100,
      ::GetDeviceCaps(hdc, PHYSICALOFFSETY)+100
    );
    
    m_physical_size.SetSize(
      ::GetDeviceCaps(hdc, PHYSICALWIDTH) - 2 * m_phys_offset.cx,
      ::GetDeviceCaps(hdc, PHYSICALHEIGHT) - 2 * m_phys_offset.cy
    );

    CRect r;
    GetClientRect(&r);

    m_xScale = m_physical_size.cx / double(r.Width());
    if (m_xScale == 0)    // уже слишком много
      m_xScale = 1;

    int header_height = m_physical_size.cy/4;

    m_yScale = m_physical_size.cy / double(header_height*4);
    if(m_yScale == 0)    // уже слишком много
      m_yScale = 1;

    m_number_of_pages = 
      ceil(m_yScale*header_height*number_of_lines/(m_physical_size.cy))-1;

    wndPreview.m_nMaxPage = m_number_of_pages;

    if (IsPrinting()){
      LOGFONT LogFont = {0};
      GetObject(AtlGetDefaultGuiFont(), sizeof(LOGFONT), &LogFont);
      LogFont.lfHeight = 320;
      print_font.CreateFontIndirect(&LogFont);
      
      CDCHandle dc(hdc);
      dc.SelectFont(print_font);
    }
  }

  void EndPrintJob(HDC /*hDC*/, bool/*bAborted*/)
  {
    if (IsPrinting()) print_font.DeleteObject();
  }

  bool IsValidPage(UINT nPage)
  {
    return nPage <= m_number_of_pages;
  }

  //Вывод графической информацииbool PrintPage(UINT nPage,HDC hdc)
  {
    if (nPage > m_number_of_pages) returnfalse;

    CDCHandle dc(hdc);

    if (!IsPrinting()){
      LOGFONT LogFont = {0};
      GetObject(AtlGetDefaultGuiFont(), sizeof(LOGFONT), &LogFont);
      LogFont.lfHeight = 320;
      print_font.CreateFontIndirect(&LogFont);
      
      dc.SelectFont(print_font);
    }

    constint h = m_physical_size.cy/4;
    CRect r(0,0,m_physical_size.cx,h);
    int cur_pages = (nPage+1)*4;
    
    if (cur_pages > number_of_lines)
      cur_pages = number_of_lines;

    for(int i = nPage*4;i < cur_pages;i++)
    {
      TCHAR buf[100];
      _itot(i,buf,10);
      CString s(strPrint+_T(" №"));
      s += buf;
      
      dc.DrawText(s,s.GetLength(),&r,DT_SINGLELINE|DT_CENTER|DT_VCENTER);
      r.OffsetRect(0,h);
    }

    if (!IsPrinting()){
      print_font.DeleteObject();
    }
    returntrue;
  }

Здесь я переопределяю методы PrePrintPage и PostPrintPage для того, чтобы не выбирать в контекст отображения нужные объекты при печати каждой страницы. Для предварительного просмотра это все равно нужно делать, поэтому я использую новую функцию интерфейса IPrintJobInfo, которая позволяет определить текущий режим – IsPrinting. Если идет предварительный просмотр, я создаю и выбираю шрифт для каждой страницы, если нет – создаю его непосредственно перед печатью, в функции BeginPrintJob.

Вот так выглядит главное окно программы в режиме предварительного просмотра первой страницы документа.


Создание панели инструментов (тулбара) для предварительного просмотра

Если у вас появилась мысль, что на приведенном выше рисунке чего-то не хватает, вы правы. Переход по страницам с помощью курсорных клавиш и выход по Escape – это хорошо, но хотелось бы большего. В этом разделе мы поговорим о встраивании тулбара в окно предварительного просмотра.

Рассмотрим два случая: когда приложение не использует Rebar control и когда использует. Второй случай более сложный, поэтому начнем с первого. Предположим у нас в ресурсах создан тулбар с идентификатором IDR_TOOLBAR1. Тогда вот такой код подменяет главный тулбар приложения.

      void ToggleToolbars()
{
  staticbool fMain = false;
  
  UINT idToolbar = IDR_TOOLBAR1;
  if (fMain)
    idToolbar = IDR_MAINFRAME;

  //Удаляем тулбар из UI-карты
  UIRemoveToolBar(m_hWndToolBar);

  //Удаляем тулбар как окно
  ::DestroyWindow(m_hWndToolBar);

  //Создаем новый тулбар
  CreateSimpleToolBar(idToolbar, ATL_SIMPLE_TOOLBAR_STYLE);
  
  //Добавляем его к UI-карте
  UIAddToolBar(m_hWndToolBar);
  DWORD dwState = UIGetState(ID_VIEW_TOOLBAR);
  UISetState(ID_VIEW_TOOLBAR,dwState);

  fMain = !fMain;
  UpdateLayout();
  return 0;
}

Все просто, только не нужно забывать о карте обновления (UI-карте). Функция UIAddToolBar добавляет новый элемент к внутреннему массиву в карте обновлений, поэтому нужно перед удалением самого окна тулбара удалить его из этого массива. Исходный код atlframe.h не имеет этой функции удаления, поэтому вы должны реализовать ее сами. Вот как сделал это я:

BOOL UIRemoveToolBar(HWND hWnd)      // toolbar
{
  if(hWnd == NULL)
    return FALSE;
  _AtlUpdateUIElement e;
  e.m_hWnd = hWnd;
  e.m_wType = UPDUI_TOOLBAR;
  return m_UIElements.Remove(e);
}

Теперь перейдем к случаю, когда используется Rebar control. Предположим, у нас уже определен в ресурсах тулбар для предварительного просмотра с тремя кнопками: кнопка влево – идентификатор IDT_PREV_PAGE, кнопка вправо – IDT_NEXT_PAGE, и кнопка печати.

      void ToggleToolbars()
{
  staticbool lParam = true;

  CReBarCtrl rebar = m_hWndToolBar;
  constint nBandIndex = rebar.IdToIndex(ATL_IDW_BAND_FIRST + 1);
  
  REBARBANDINFO rbi = {sizeof(REBARBANDINFO),RBBIM_CHILD};
  rebar.GetBandInfo(nBandIndex,&rbi);
  const HWND hOldToolbar = rbi.hwndChild;

  UINT id = IDR_MAINFRAME;
  if (lParam)
    id = IDR_TOOLBAR1;
  
  HWND hWndToolBar = CreateSimpleToolBarCtrl(m_hWnd, id,
      FALSE, ATL_SIMPLE_TOOLBAR_PANE_STYLE|TBSTYLE_LIST);

  CToolBarCtrl tb = hWndToolBar;

  //Если тулбар для предварительного просмотра, добавим пару кнопокif (lParam){
    TBBUTTONINFO tbi = {sizeof(TBBUTTONINFO),TBIF_STATE,0};
    
    //Если несколько страниц, активизируем кнопкиif (GetPageCount() > 0)
      tbi.fsState = TBSTATE_ENABLED;

    tbi.pszText = _T("Prev");
    tbi.cchText = 4;
    tb.SetButtonInfo(IDT_PREV_PAGE,&tbi);
    
    tbi.pszText = _T("Next");
    tbi.cchText = 4;
    tb.SetButtonInfo(IDT_NEXT_PAGE,&tbi);
 
    tbi.pszText = _T("Print");
    tbi.cchText = 5;
    tbi.dwMask = TBIF_TEXT;
    tb.SetButtonInfo(ID_FILE_PRINT,&tbi);
TTONS);
    
    TBBUTTON btns[] = {
      {0,0,0,TBSTYLE_SEP},
      //only for Windows 2000/Me and later
#if (_WIN32_IE >= 0x0501)
      {I_IMAGENONE,IDT_CLOSE_PREVIEW,(BYTE)TBSTATE_ENABLED,
        (BYTE)TBSTYLE_LIST|BTNS_BUTTON|BTNS_AUTOSIZE|BTNS_SHOWTEXT},
#else
      {-1,IDT_CLOSE_PREVIEW,(BYTE)TBSTATE_ENABLED,
        (BYTE)TBSTYLE_LIST|TBSTYLE_BUTTON|TBSTYLE_AUTOSIZE},
#endif
      };
    
    btns[1].iString = (INT_PTR)_T("Close");

    tb.AddButtons(2,btns);
  }

  rbi.hwndChild = hWndToolBar;
  rebar.SetBandInfo(nBandIndex,&rbi);
  
  if (::IsWindow(hOldToolbar)){
    if (lParam){
      UIRemoveToolBar(hOldToolbar);
    }
    //Only for CCommandBarXPCtrl//m_CmdBar.RemoveToolbar(hOldToolbar);

    ::DestroyWindow(hOldToolbar);
  }

  if (!lParam){
    UIAddToolBar(hWndToolBar);
    DWORD dwState = UIGetState(ID_VIEW_TOOLBAR);
    UISetState(ID_VIEW_TOOLBAR,dwState);
    rebar.ShowBand(nBandIndex,(dwState & UPDUI_CHECKED));
  }

  //Only for CCommandBarXPCtrl//m_CmdBar.AddToolbar(hWndToolBar);//Скрываем меню
  rebar.ShowBand(rebar.IdToIndex(ATL_IDW_BAND_FIRST),!lParam);

  lParam = !lParam;
  UpdateLayout();

  return 0;
}

Первым делом мы получаем индекс band-а, на котором лежит тулбар. Затем получаем хэндл окна самого тулбара – он будет нужен для удаления. После этого создается новый тулбар и, если это тулбар предварительного просмотра, в него добавляется разделитель и кнопка закрытия. Далее новый тулбар «ложится» на заранее определенный band Rebar контрола, после чего производится очистка. Так как в режиме предварительного просмотра нам не нужно главное меню приложения, оно скрывается с помощью следующего кода:

rebar.ShowBand(rebar.IdToIndex(ATL_IDW_BAND_FIRST),!lParam);

Вот, собственно и все.

ПРИМЕЧАНИЕ

Замечание для тех, кто использует известный нестандартный control от Bjarke Viksoe (CCommandBarXPCtrl). При удалении тулбара его необходимо также удалять и из Command bar’а, однако функции удаления опять нет. Добавьте в atlctrlxp.h следующий код:

BOOL RemoveToolbar(HWND hwndTB)

{

ATLASSERT(::IsWindow(hwndTB));

return m_Toolbars.Remove(hwndTB);

}

И не забывайте добавлять тулбар в Command bar после его создания.

m_CmdBar.AddToolbar(hWndToolBar);

И еще. Старые версии CCommandBarXPCtrl не поддерживали стиль TBSTYLE_EX_MIXEDBUTTONS. Если при использовании данного стиля у вас возникли проблемы, скачайте последнюю версию control-а с сайта http://home.worldonline.dk/~viksoe/index.htm.

Вот так, примерно, будет выглядеть окно предварительного просмотра со своим тулбаром и поддержкой CCommandBarXPCtrl.


Заключение

Печать в WTL не так хорошо развита и проработана, как в других библиотеках, и имеет серьезные недостатки, такие как отсутствие масштабирования в режиме предварительного просмотра, недостаточно продуманная архитектура и отсутствие связи между выводом графической информации на печать, предварительный просмотр и экран. Однако, благодаря открытой и очень гибкой структуре, библиотека WTL позволяет программисту реализовать то, что в других библиотеках вызывает большие трудности. Например, в следующей статье я опишу методы реализации предварительного просмотра документа с возможностью масштабирования: это будет всего лишь один класс, унаследованный от CScrollImpl и CPrintPreviewWindow, с одной(!) переопределенной функцией. До встречи!


Эта статья опубликована в журнале RSDN Magazine #3-2003. Информацию о журнале можно найти здесь