По трем из рассмотренных признаков (ширина штриха, ширина символов и наличию засечек) в Windows принято выделять так называемые семейства шрифтов. Для того, что бы пояснить разницу между принятыми семействами, приведем небольшую табличку, содержащую сводку характеристик разных семейств шрифтов в Windows:
Семейство | Ширина штриха | Ширина символов | Наличие засечек | примеры |
MODERN | постоянная | постоянная | — | Courier New |
ROMAN | переменная | переменная | есть | Times New Roman, Antiqua, Garamond, Palatino, Bodoni |
SWISS | переменная | переменная | нет | Arial, Helvetica, Futura, Avantgarde, Optima, Swiss |
SCRIPT | — | — | — | Script, Odessa Script FWF, Decor, Jikharev, Parsek |
DECORATIVE | — | — | — | Windings, MusicalSymbols, Symbol |
DONTCARE | — |
Обычно к семейству MODERN относят все шрифты фиксированной ширины. Название MODERN указывает на сравнительно недавнюю историю этих шрифтов - они получили распространение с развитием печатающей техники и компьютеров, тогда как другие виды шрифтов возникали с XV века, когда заканчивалась эпоха готического шрифта.
В те времена начинал формироваться шрифт, похожий по своему начертанию на один из древнейших шрифтов - римский капитальный. Это был пропорциональный шрифт с засечками и нормальной контрастностью, позже он получил очень широкое распространение в типографском деле и дожил до наших дней. За свою долгую историю он многократно видоизменялся и стал родоначальником большого числа поколений шрифтов. В Windows такие шрифты относятся к семейству ROMAN.
Реально этому семейству соответствует очень большое число разных шрифтов, выделяемых в других классификационных системах. В частности, можно выделить так называемые брусковые шрифты, обычно со слабо выраженным контрастом и засечками, перпендикулярными штрихам и имеющими примерно такую же ширину (пример - Courier); В XX веке возникли шрифты, получившие очень широкое распространение. Наиболее распространенное название для этих шрифтов - рубленые. Эти шрифты не имеют контраста и засечек (sans serif), в Windows им соответствует семейство SWISS. Семейство DONTCARE реально не соответствует никакой группе шрифтов. Оно используется только при указании, из какого семейства надо выбирать шрифт - при этом оно обозначает “любое семейство”.
1.1 Стандартные шрифты Windows
В стандартной поставке Windows присутствует небольшой набор шрифтов, представляющий все (определенные в Windows) семейства шрифтов. Этот набор включает в себя растровые, векторные и TrueType шрифты, информация о которых сведена в небольшую таблицу:
Имя шрифта | семейство | кодовая таблица | файл(ы) |
растровые шрифты | |||
System | SWISS | ANSI |
xxxSYS.FON |
FixedSys | DONTCARE | ANSI |
xxxFIX.FON |
Terminal | MODERN | OEM |
xxxOEM.FON |
Courier | MODERN | ANSI |
COURy.FON |
MS Sans Serif | SWISS | ANSI |
SSERIFy.FON |
MS Serif | ROMAN | ANSI |
SERIFy.FON |
Small Fonts | ROMAN | ANSI |
SMALLy.FON |
Symbol | DECORATIVE | SYMBOL |
SYMBOLy.FON |
векторные шрифты | |||
Modern | MODERN | OEM | MODERN.FON |
Roman | ROMAN | OEM | ROMAN.FON |
Script | SCRIPT | OEM | SCRIPT.FON |
TrueType | |||
Arial | SWISS | ANSI |
ARIALzz.TTF(.FOT) |
Courier New | MODERN | ANSI |
COURzz.TTF(.FOT) |
Times New Roman | ROMAN | ANSI | TMSRMN.TTF(.FOT) |
Windings | DONTCARE | ANSI | WINDINGS.TTF(.FOT) |
В этой таблице следует внимательно рассмотреть имена файлов. В этих именах маленькими буквами (xxx,y,zz) обозначены изменяющиеся части.
Так xxx обозначает устройство, для которого был спроектирован шрифт. Вместо этой последовательности реально написано CGA, EGA, VGA или 8514.
Символ y обозначает категорию устройств, к которой относится данный шрифт. Возможные значения можно найти в таблице:
Буква | Соотношение сторон |
Разрешающая способность X/Y (пиксел/дюйм) |
Устройство |
A | 200 | 96/48 | CGA |
B | 133 | 96/72 | EGA |
C | 83 | 60/72 | Okidata printers |
D | 167 | 120/72 | IBM, Epson printers |
E | 100 | 96/96 | VGA |
F | 100 | 120/120 | IBM 8514 |
Последовательность zz указывает, какого типа шрифт описан в этом файле: нормальный (пустая последовательность), жирный BD, наклонный I или жирный наклонный BI.
1.2Получение хендла шрифта
Для того, что бы Вы могли применять шрифт в Вашей программе, Вы должны сначала получить хендл соответствующего шрифта. Шрифт является объектом GDI, поэтому работа с ним похожа на работу с другими объектами GDI. Как правило вы должны выполнить следующие действия:
получить хендл либо стандартного шрифта, либо создав "новый" шрифт
При создании "нового" шрифта создается соответствующий объект GDI, а не новый файл описания шрифта.
выбрать шрифт в контекст устройства
осуществить вывод, используя текущий шрифт
если шрифт был создан, то его надо уничтожить.
Создание шрифтов занимает некоторое время (особенно для отображения TrueType шрифтов - Windows автоматически генерирует промежуточный растровый шрифт, который и используется при выводе). Если надо создавать шрифты, то это удобно делать при создании окна или даже при запуске приложения, а уничтожать - при закрытии окна или при завершении приложения (как и все объекты GDI, созданный шрифт уничтожается с помощью функции DeleteObject).
Если Вам надо получить хендл стандартного шрифта, то Вы можете воспользоваться функцией:
HFONT GetStockObject( nIndex );
Параметр nIndex может быть:
ANSI_FIXED_FONT | соответствует шрифту Courier |
ANSI_VAR_FONT | соответствует шрифту MS Sans Serif |
OEM_FIXED_FONT | соответствует шрифту Terminal |
SYSTEM_FONT | соответствует шрифту System; этот шрифт используется по умолчанию |
SYSTEM_FIXED_FONT | соответствует шрифту FixedSys; До версии Windows 3.0 системный шрифт был фиксированной ширины, он включен в Windows 3.1 для совместимости. |
DEVICE_DEFAULT_FONT | соответствует шрифту, загруженному в устройство; для дисплея не определен |
Если Вам надо создавать собственный шрифт, то Вы можете воспользоваться одной из двух функций:
HFONT CreateFont(
nHeight, nWidth, nEscapement, nOrientation, nWeight,
bItalic, bUnderline, bStrikeOut,
bCharSet, bOutputPrecision, bClipPrecision, bQuality, bPitchAndFamily,
lpszFacename);
или
HFONT CreateFontIndirect( lpLogFont );
Чаще применяется функция CreateFontIndirect(), получающая в качестве параметра указатель на структуру LOGFONT. Поля этой структуры совпадают с аргументами функции CreateFont().
typedef struct tagLOGFONT {
int lfHeight;
int lfWidth;
int lfEscapement;
int lfOrientation;
int lfWeight;
BYTE lfItalic;
BYTE lfUnderline;
BYTE lfStrikeOut;
BYTE lfCharSet;
BYTE lfOutPrecision;
BYTE lfClipPrecision;
BYTE lfQuality;
BYTE lfPitchAndFamily;
BYTE lfFaceName[LF_FACESIZE];
} LOGFONT;
Рассмотрим назначение полей этой структуры:
lfHeight задает требуемый размер шрифта в точках (пунктах). Если значение положительно, то в высоту включается межстрочный промежуток, а если отрицательно, то модуль указывает высоту символа шрифта. Значение 0 указывает, что используется значение высоты по умолчанию.
lfWidth задает среднюю ширину символов в пунктах. Значение 0 соответствует ширине по умолчанию.
lfEscapement задает наклон базовой линии строки в десятых долях градуса. Для растровых шрифтов игнорируется.
lfOrientation задает ориентацию символа относительно базовой линии в десятых долях градуса. Игнорируется для растровых и TrueType шрифтов, для векторных используются значения: 0, 900, 1800 и 2700
lfWeight задает вес символа (жирность). Соответствует количеству закрашенных пиксел из 1000. Предусмотрены условные обозначения для определения веса, начинающиеся на FW_... Например, нормальный текст (400) соответствует FW_NORMAL (FW_REGULAR), жирный (700) FW_BOLD.
lfItalic ненулевое значение задает наклон символов (начертание slanted). Значение 0 соответствует обычному тексту.
lfUnderline ненулевое значение задает подчеркивание строки текста линией. Значение 0 соответствует обычному тексту.
lfStrikeOut ненулевое значение задает перечеркивание строки текста линией. Значение 0 соответствует обычному тексту.
lfCharSet задает кодовую таблицу, которую должен поддерживать данный шрифт. Используются следующие условные обозначения:
ANSI_CHARSET кодовая таблица ANSI
OEM_CHARSET кодовая таблица OEM
SYMBOL_CHARSET символы
SHIFTJIS_CHARSET японская азбука
DEFAULT_CHARSET любая кодовая таблица.
lfOutPrecision указывает, насколько точно должен соответствовать подбираемый шрифт указанному размеру. (Реально Вы можете заказать растровый шрифт несуществующего размера). См. условные обозначения вида OUT_???_PRECIS в windows.h.
lpClipPrecision указывает, как должен отображаться частично невидимый символ. См. условные обозначения вида CLIP_???_PRECIS в windows.h.
lfQuality указывает качество получаемого шрифта. Обычно используется значение PROOF_QUALITY (или DEFAULT_QUALITY). Если размер растрового шрифта меньше, чем требуется, то Windows может масштабировать шрифт. Однако при этом резко ухудшается качество, поэтому масштабирование можно запретить, используя значение PROOF_QUALITY.
lfPitchAndFamily два младших бита задают тип шрифта - DEFAULT_PITCH (любой тип), VARIABLE_PITCH (пропорциональный) или FIXED_PITCH (моноширинный); старший байт указывает семейство, которое задается одним из следующих символов: FF_DECORATIVE, FF_DONTCARE, FF_MODERN, FF_ROMAN, FF_SCRIPT, FF_SWISS.
lfFaceName массив из LF_FACESIZE символов, содержащий заканчивающееся \0 имя шрифта. Пустое имя соответствует шрифту устройства.
1.3 Основы подбора шрифтов в Windows
Когда Вы вызываете функцию, создающую шрифт, Windows перебирает все имеющиеся шрифты, определяя шрифт, точнее всего соответствующий желаемому.
При подборе шрифта используется система “пенальти”: для каждого из шрифтов он вычисляет “пенальти”, соответствующие отличию данного шрифта от желаемого. Шрифт с минимальным пенальти считается наиболее точно соответствующим желаемому.
Пенальти вычисляются следующим образом: для существенных параметров вводятся бальные оценки. Если заказанный параметр соответствует шрифту, пенальти равно 0, а если отличается, то размер пенальти зависит от параметра. Значения пенальти приведены в следующей таблице:
параметр | пенальти |
fCharSet | 4 |
fPitchAndFamily: pitch family |
3 3 |
lfFaceName | 3 |
lfHeight | 2 |
lfWidth | 2 |
lfItalic | 1 |
lfUnderline | 1 |
lfStrikeOut | 1 |
В этой таблице можно заметить несколько интересных особенностей. Например, пенальти за несоответствие кодовой таблице больше, а за не–принадлежность к семейству равна пенальти за несоответствие имени шрифта. Практически, если Вы хотите использовать шрифт с конкретным именем, то Вы должны обязательно указать правильные кодовую страницу и семейство шрифтов, иначе Windows может использовать шрифт с другим именем.
1.4 Функции для работы со шрифтами
После того, как Вы получили хендл шрифта, Вы можете осуществить вывод текста, используя этот шрифт. Для этого Вы должны выбрать шрифт в контекст устройства с помощью функции
SelectObject( hDC, hFont );
Все последующие операции вывода будут использовать тот шрифт, который Вы выбрали в контекст устройства. Если Вам надо получить информацию о загруженном шрифте, то Вы можете воспользоваться функциями:
int GetTextFace( hDC, nMaxBuffer, lpsBuffer );
int GetObject( hFont, sizeof(LOGFONT), &stLogfont );
Функция GetTextFace() заполняет указанный буфер именем применяемого шрифта, а функция GetObject() позволяет заполнить структуру LOGFONT информацией о конкретном шрифте.
Более сложным представляется все-таки выбор шрифта и определение характеристик вновь создаваемого шрифта. Это связано с тем, что в большинстве случаев Вы заранее не знаете, какие шрифты используются в данном комплекте Windows, то есть Вы должны уметь выбирать нужный шрифт из числа имеющихся в Windows.
Это можно осуществить двумя разными способами - Вы можете перебирать все шрифты и выбрать из них нужный Вам (например создать меню, содержащее имена шрифтов), или Вы можете воспользоваться функцией ChooseFont() для вызова стандартного диалога выбора шрифта.
Сначала мы разберемся с основными правилами перебора шрифтов. В Windows для этих целей существует две функции:
int EnumFonts( hDC, lpszFace, lpfnEnumProc, lParam );
int EnumFontFamilies( hDC, lpszFace, lpfnEnumProc, lParam );
Обе эти функции осуществляют перебор шрифтов, которые могут быть применены на указанном устройстве, и имеющие заданное имя (значение NULL указывает на перебор всех доступных шрифтов). Эти функции осуществляют перебор чуть-чуть различающимся образом.
Разница связана с тем, что TrueType шрифты обычно существуют в нескольких вариантах, отличающихся начертанием (жирный, наклонный и др.). Эти варианты существуют как отдельные шрифты, поэтому функция EnumFonts() перечисляет один и тот–же TrueType шрифт несколько раз, соответственно с количеством разных начертаний. Функция EnumFontFamilies() перебирает только по одному начертанию каждого TrueType шрифта.
Для каждого перечисляемого шрифта вызывается функция lpfnEnumProc(), которой передается структуры типа LOGFONT и TEXTMETRIC (функция EnumFontFamilies() передает структуры NEWLOGFONT и NEWTEXTMETRIC, содержащие дополнительные данные), номер типа шрифта (DEVICE_FONTTYPE, RASTER_FONTTYPE или TRUETYPE_FONTTYPE) и параметр lParam, который Вы указали для функции EnumFonts() или EnumFontFamilies().
Общий вид функции, вызываемой при переборе шрифтов, следующий:
int CALLBACK _export EnumFontProc( lpLF, lpTM, nType, lParam );
int CALLBACK _export EnumFontFamProc( lpNLF, lpNTM, nType, lParam );
Параметры:
lpLF является дальним адресом структуры LOGFONT
lpNLF является дальним адресом структуры NEWLOGFONT
lpTM является дальним адресом структуры TEXTMETRIC
lpNTM является дальним адресом структуры NEWTEXTMETRIC
nType указывает тип шрифта
lParam дополнительный параметр, определяемый Вами.
Осуществляя перебор шрифтов, Вы можете, например, заполнить меню, содержащее имена нужных Вам шрифтов, или выбрать тот шрифт, который Вас устраивает и т.д.
Во многих случаях удобнее, однако, не перебирать шрифты, а воспользоваться каким-либо диалогом для выбора нужного шрифта. Это можно сделать с помощью функции ChooseFont(), описанной в файле COMMDLG.H.
Файл COMMDLG.H содержит описания нескольких функций и структур данных, позволяющих вызывать “диалоги общего пользования” (COMMon DiaLoGs). Помимо файла COMMDLG.H Вы должны включить в Ваше приложение файл COMMDLG.LIB, с помощью которого осуществляется связывание Вашего приложения с динамической библиотекой COMMDLG.DLL, содержащей требуемые функции.
Функция, вызывающая диалог для выбора шрифта выглядит следующим образом:
BOOL ChooseFont( lpChooseFont );
причем параметр lpChooseFont указывает на структуру типа CHOOSEFONT:
typedef struct tagCHOOSEFONT { /* cf */
DWORD lStructSize; // = sizeof(CHOOSEFONT)
HWND hwndOwner;
HDC hDC; // используется только для принтера
LOGFONT FAR* lpLogFont;
int iPointSize;
DWORD Flags;
COLORREF rgbColors;
LPARAM lCustData;
UINT (CALLBACK* lpfnHook)(HWND, UINT, WPARAM, LPARAM);
LPCSTR lpTemplateName;
HINSTANCE hInstance;
LPSTR lpszStyle;
UINT nFontType;
int nSizeMin;
int nSizeMax;
} CHOOSEFONT;
Вы должны заполнить нужные поля этой структуры и вызвать функцию ChooseFont() для выбора нужного шрифта. Функция возвращает результат TRUE (не 0), если шрифт был выбран, или FALSE (0), если была нажата кнопка “Cancel”.
Заполнение полей этой функции элементарно, рассмотреть стоит только поле Flags, описывающее характеристики диалога и выбираемого шрифта. С помощью этого поля Вы можете уточнить, из какой группы Вы собираетесь выбирать шрифт:
по устройствам:
CF_PRINTERFONTS шрифты принтера (Вы должны указать hDC принтера)
CF_SCREENFONTS дисплейные шрифты
CF_BOTH все
по типам шрифтов:
CF_TTONLY только TrueType
CF_NOVECTORFONTS растровые и TrueType
CF_SCALABLEONLY векторные, TrueType и некоторые шрифты принтера
CF_WYSIWYG шрифты, используемые и дисплеем и принтером. (вместе с CF_WYSIWYG надо установить CF_BOTH|CF_SCALABLEONLY)
по кодировке:
CF_ANSIONLY только ANSI шрифты
CF_NOOEMFONTS все шрифты кроме OEM
по особенностям
CF_FIXEDPITCHONLY только моноширинные шрифты
CF_FORCEFONTEXIST шрифт с выбранными атрибутами должен существовать (не допускается автоматическое преобразование) по размеру
CF_LIMITSIZE установив этот флаг Вы должны задать поля nSizeMin и nSizeMax, которые определят допустимые размеры шрифтов.
А также Вы можете несколько видоизменять диалог:
по наличию кнопок:
CF_APPLY присутствует кнопка “Apply” (Применить)
CF_USEHELP присутствует кнопка “Help” (Справка)
по правилам инициализации:
CF_INITTOLOGFONTSTRUCT использовать данные структуры LOGFONT (указанной в CHOOSEFONT) для инициализации диалога
по возможности выбирать параметры:
CF_EFFECTS диалог позволит установить стили подчеркивание (underline) и перечеркивание (overstrike)
CF_NOFACESEL нельзя выбирать имя шрифта из списка
CF_NOSIMUALTIONS запрещена имитация шрифта с помощью GDI
CF_NOSIZESEL нельзя выбирать размер шрифта
CF_NOSTYLESEL нельзя выбирать стиль шрифта.
Дополнительные возможности этой функции связаны с возможностью видоизменять сам диалог, (в том числе самостоятельно спроектировать шаблон диалога), и функцию диалога, для выполнения дополнительных действий.
2. Шрифты в качестве ресурсов
2.1 Применение шрифтовых ресурсов
Сейчас мы рассмотрим последнюю тему, связанную со шрифтами, а именно - создание собственных шрифтовых ресурсов. Ранее мы встречались с одной из разновидностей ресурсов - битмапом. Тогда битмап включался в ресурс и становился доступным приложению. Для этого мы в файле описания ресурсов включали строку вида:
name BITMAP “file.bmp”
По аналогии хочется поступить также и со шрифтом, тем более, что существует такой вид ресурсов - FONT. Однако этот метод не работает. Это связано с тем, что все шрифты в Windows доступны всем приложениям. В этом случае включать шрифт в приложение становиться невозможным - так как в момент его завершения шрифт может использоваться другим приложением. Поэтому шрифтовые ресурсы в Windows оформляются в виде отдельных файлов.
Так как шрифты доступны всем приложениям, то мы сначала должны включить свой шрифт в системную таблицу шрифтов. При этом шрифт становится доступным всем приложениям Windows (в том числе и нашему). Теперь мы можем вызвать функцию CreateFont() или CreateFontIndirect() для получения хендла шрифта, а в конце работы, после уничтожения созданного шрифта, мы должны удалить его из системной таблицы.
В некоторых случаях может быть удобным добавление шрифта в список шрифтов, автоматически попадающих в системную таблицу при запуске Windows. Для этого Вы должны добавить строку в файл WIN.INI, секция [fonts] (как это делается - позже, когда будем рассматривать настройку приложений). При этом все последующие запуски Windows будет автоматически добавлять Ваш шрифт в системную таблицу. Однако в текущем сеансе этого автоматически не происходит, так что Вы должны сами добавить его в таблицу.
Для включения шрифта в системную таблицу надо воспользоваться функцией:
int AddFontResource( lpszFileName );
возвращаемое значение указывает число шрифтов, добавленных в системную таблицу из этого файла, значение 0 указывает на ошибку.
Обычно шрифтовые файлы имеют расширение .FON; Такой файл может содержать несколько шрифтов с общим именем, но разными размерами символов. Windows будет использовать шрифт того размера, который наиболее точно подходит к запрашиваемому.
Если добавленный шрифт не предназначен строго для внутреннего использования, то Вы должны послать всем приложениям сообщение о смене шрифта:
SendMessage( HWND_BROADCAST, WM_FONTCHANGE, 0, 0L );
Для удаления шрифта из системной таблицы Вы должны воспользоваться функцией
BOOL RemoveFontResource( lpszFileName );
Как и при добавлении шрифта, если этот шрифт может применяться другими приложениями, то Вы должны послать сообщение WM_FONTCHANGE.
Добавлять или удалять шрифты из системной таблицы удобно либо в начале и конце приложения, либо при создании и удалении главного окна приложения.
2.2 Создание шрифтовых ресурсов
В этом разделе мы будем говорить только о создании собственных растровых шрифтовых ресурсов. Это связано с тем, что стандартные редакторы шрифтовых ресурсов (включаемые в SDK и компиляторы) позволяют создавать только растровые шрифты.
Для начала мы должны нарисовать требуемые нам шрифты с помощью какого-либо редактора ресурсов. Растровые шрифты обычно размещаются в файлах с расширением .FNT (это не шрифтовой файл, а отдельный ресурс, как, скажем, битмап). Нам может понадобится нарисовать несколько шрифтов разного размера, но имеющих общее начертание. Эти шрифты будут сохранены в разных .FNT файлах.
Далее мы должны построить шрифтовой файл .FON, содержащий наши шрифты. Этот файл являться библиотекой ресурсов. Практически он оформлен как обычное Windows приложение, которое содержит только ресурсы. Мы можем описать такое приложение, как приложение вообще не имеющее сегмента кода, или как библиотеку.
Так как наша библиотека должна содержать шрифтовые ресурсы, то мы должны задать файл описания ресурсов .RC, содержащий список нарисованных нами шрифтов, например:
1 FONT fonta.fnt
2 FONT fontb.fnt
3 FONT fontc.fnt
Конечно нам понадобится файл описания приложения .DEF в несколько специфичном виде:
LIBRARY
DESCRIPTION 'FONTRES DISPLAY : 40-char terminal'
EXETYPE WINDOWS
STUB 'WINSTUB.EXE'
DATA NONE
Надо обратить внимание на использование слова LIBRARY, вместо NAME, для указания того, что это не обычное приложение, а библиотека. Далее мы должны указать, что наша библиотека не имеет данных 'DATA NONE' и составить описание нашего шрифта. Для этого мы должны в DESCRIPTION указать строку специального формата:
DESCRIPTION 'FONTRES aspect,logpixelsx,logpixelsy : comment'
DESCRIPTION 'FONTRES DEVICESPECIFIC device : comment'
DESCRIPTION 'FONTRES DISPLAY : comment'
Здесь представлены три разных формата таких описаний. Первый формат указывает характеристики устройства, для которого разработан шрифт; значения характеристик можно найти, получив информацию о контексте устройства, или из таблички со сводкой системных шрифтов, приведенной выше.
Второй формат задает шрифт, созданный для конкретного устройства. Параметр device задает имя устройства, для которого шрифт спроектирован, например: IBM 8514. Третий формат указывает, что шрифт спроектирован для дисплея.
Далее мы должны построить библиотеку ресурсов. Как уже говорилось, это можно сделать двумя способами - создав приложение не имеющее кода или создав приложение, являющееся разделяемой библиотекой.
Если мы хотим описать приложение не имеющее кода, то нам будет удобнее воспользоваться компиляторами Borland, так как построители задач других фирм часто, встретив сегмент нулевой длины, предполагают длину 65536. Для создания модуля не имеющего кода, нам надо написать простейший ассемблерный файл:
_TEXT segment byte public 'CODE'
_TEXT ends
end
Этот файл описывает только один сегмент нулевой длины.
После этого мы можем приступить к построению шрифтового файла. Для этого мы сначала компилируем ассемблерный файл:
tasm file.asm
затем мы должны построить нашу библиотеку и включить в нее спроектированные ресурсы:
tlink file.obj,file.exe,nul,,file.def
rc file.rc
rename file.exe file.fon
Теперь мы располагаем собственным шрифтовым ресурсом, который мы можем применять в нашем приложении. Еще раз надо отметить, что обязательно применение 'Borland TLINK' для построения файла, так как другие сборщики могут построить неверный модуль.
Ранее мы сделали замечание о том, что мы можем строить шрифтовой файл двумя способами - как библиотеку не имеющую кода, или как обычную библиотеку. Если мы хотим строить обычную библиотеку, то вместо ассемблерного файла нам надо написать небольшой файл на C:
#include <windows.h>
extern "C" {
int CALLBACK LibMain(
HANDLE hInstance, WORD wDataSeg, WORD cbHeapSize,LPSTR lpszCmdLine) {
// обычно функция LibMain() разблокирует сегмент данных
// если он имеет динамический heap.
// if ( cbHeapSize ) UnlockData( 0 );
// так как мы вообще не имеем данных (и heap тоже)
// то можем этого не делать.
return 1;}
int CALLBACK WEP( int bSystemExit ) {
return TRUE;}}
Этот файл содержит две процедуры - LibMain() - которая заменяет обычный WinMain() и вызывается при инициализации библиотеки - и процедуру WEP(), которая вызывается при удалении ненужной библиотеки.
В этом файле надо обратить внимание на то, что функция LibMain описана как CALLBACK (PASCAL FAR), в отличие от WinMain. К сожалению, некоторые компиляторы предполагают, что она должна быть NEAR для моделей памяти с одним сегментом кода. В качестве выхода можно изменить имя, например, написать его только большими буквами. Тогда компилятор не распознает эту функцию как стандартную и не сделает ошибки, а сборщик осуществит правильное связывание, так как функция декларирована как CALLBACK (PASCAL FAR).
Вторая особенность - указание, что имена функций не должны кодироваться как C++ имена ( extern "C" ). Это опять–же связано с особенностями некоторых компиляторов, которые не распознают функцию WEP() (Windows Exit Procedure) как стандартную, и осуществляют для нее C++ кодирование имени - при этом сборщик не может правильно построить задачу.
В остальном построение шрифтового файла не отличается от рассмотренного, конечно кроме компиляции исходного текста, которая выполняется как для обычного Windows-приложения:
bcc -ms -W file.c file.def
2.3 Особенности TrueType шрифтов
Сейчас мы рассмотрим некоторые особенности применения собственных TrueType шрифтов. Эти особенности связаны с тем, что TrueType шрифт представлен в совершенно иной форме, чем растровые шрифты. Если Вы с помощью какого-либо инструментального средства (например, FontoGrapher) создадите TrueType шрифт, то обнаружите, что он представлен в виде файла с расширением .TTF.
Однако применять .TTF файл непосредственно нельзя. Для того, что бы можно было использовать TrueType шрифт надо построить промежуточный файл, обычно с расширением .FOT, который можно использовать вместо файла .FON для добавления шрифта в таблицу ресурсов. Для построения этого файла Вы должны воспользоваться функцией
BOOL CreateScalableFontResource(
nHidden, lpszResourceFile, lpszFontFile, lpszPath);
Эта функция создает файл с именем lpszResourceFile для доступа к .TTF файлу, заданному параметром lpszFontFile, параметр lpszPath указывает путь до .TTF файла. Последний оставшийся параметр nHidden указывает возможность использования шрифта другими приложениями. Если он 0, то другие приложения имеют доступ к этому шрифту, а если 1, то доступ запрещен и этот шрифт не перечисляется при переборе шрифтов (EnumFonts(), EnumFontFamilies()).
2.4 Настройка приложений
Заканчивая разговор о шрифтах, удобно рассмотреть еще один компонент Windows - средства для настройки приложений. Под настройкой (иногда "профилированием") понимается задание характеристик приложения и их сохранение для использования при следующем запуске.
Обычно такие задачи решаются с помощью создания конфигурационных файлов. Однако конфигурация описывается каждой задачей по-своему, что не всегда удобно. Windows предлагает общий для всех приложений механизм описания их характеристик, с использованием файлов настройки.
Такие файлы (обычно имеющие расширение .INI) являются обычными ASCII–файлами, разделенными на секции, начинающиеся с имени секции, заключенного в квадратные скобки. Далее следует список параметров в виде ‘параметр=значение’, каждый параметр размещается в отдельной строке. В этот файл можно вставлять комментарии - строки начинающиеся с ‘;’.
Пример взят из файла WORKSHOP.INI:
[User Controls]
BorShade=E:\BORLANDC\WORKSHOP\BWCC.DLL
[RWS_Bitmap]
PercentLeft=50
ZoomLeft=1
ZoomRight=1
bVert=0
[RWS_Font]
PercentLeft=50
ZoomLeft=4
ZoomRight=1
bVert=1
Для работы с такими файлами Windows предоставляет набор функций, осуществляющих запись и чтение параметров:
int GetProfileInt(lpszSection, lpszEntry, nDefault);
int GetProfileString(lpszSection, lpszEntry, lpszDefault, lpsBuffer, nMaxBuffer);
BOOL WriteProfileString(lpszSection, lpszEntry, lpszString);
Параметр lpszSection задает имя секции (скобок в имени указывать не надо), lpszEntry - имя параметра. Если мы получаем значение параметра, то можем указать значение по умолчанию, которое возвращается, если данный параметр не найден.
С помощью функции GetProfileString() можно получить список имен всех параметров в секции, указав lpszEntry= NULL. При этом имена параметров секции будут скопированы в буфер последовательно друг за другом, каждое имя будет заканчиваться 0 и после последнего имени будут стоять два 0.
Функция WriteProfileString() позволяет не только записывать параметры, но и удалять, для чего надо указать lpszString=NULL. Можно удалить целиком всю секцию, указав lpszEntry=NULL.
Все три рассмотренных функции используют файл WIN.INI. При этом имя секции часто ассоциируется с именем приложения. (Поэтому в документации имя секции часто называется именем приложения).
Конечно, часто бывает неудобно использовать общий файл настройки для всех существующих приложений (при этом, в частности, трудно организовать удаление приложений). Windows предоставляет возможность использовать собственный файл настройки (и даже несколько). Для работы с собственными файлами настройки предусмотрены еще три функции:
int GetPrivateProfileInt( lpszSection, lpszEntry, nDefault, lpszIniFile );
int GetPrivateProfileString(
lpszSection, lpszEntry, lpszDefault, lpsBuffer, nMaxBuffer, lpszIniFile);
BOOL WritePrivateProfileString(
lpszSection, lpszEntry, lpszString, lpszIniFile);
Последний параметр этих функций задает имя файла настройки. Если Вы не указываете путь к файлу, то он размещается в каталоге Windows.
2.5 Информация стандартных файлов настройки (win.ini и system.ini) о шрифтах и о принтере
Рассматривая настройку приложений надо подробнее остановиться на файле WIN.INI. Этот файл содержит большое количество характеристик, определяющих работу как самого Windows, так и многих его приложений. Позже мы будем иногда ссылаться на этот файл, например, при определении даты и времени.
Сейчас мы рассмотрим информацию, хранимую в этом файле, касающуюся шрифтов. Самая существенная для нас секция - [fonts]. В этой секции перечислены все шрифты, которые помещаются в системную таблицу при запуске Windows. Формат записей:
face name = file.fon
face name - это имя шрифта, а file.fon - имя шрифтового файла, содержащего растровый или векторный шрифт, или file.fot - TrueType шрифт.
Пример:
[fonts]
Academy (TrueType)=ACADEMY.FOT
Academy Bold (TrueType)=ACADEMY0.FOT
Следующая секция, относящаяся к шрифтам, [FontSubstitutes], указывающая, какие шрифты, входящие в Windows 3.1 должны использоваться вместо некоторых шрифтов, входивших в Windows 3.0. Формат записей:
new name= old name
Пример:
[FontSubstitutes]
Helv=MS Sans Serif
Courier=Helv
Если Вы адаптируете приложение Windows 3.0 для работы в Windows 3.1, то Вам надо ссылаться на эту секцию для определения имен шрифтов, которые Вы должны применять.
Дополнительно информация о системных шрифтах содержится в файле SYSTEM.INI, где в секции [boot] указываются системный (ANSI), терминальный (OEM) и системный фиксированной ширины (ANSI) шрифты. Это делается с помощью параметров:
[boot]
fonts.fon=vgasys.fon
fixedfon.fon=vgafix.fon
oemfonts.fon=vgaoem.fon
Эти шрифты используются Windows при загрузке, когда системная таблица шрифтов еще не инициализирована. Кроме того некоторые шрифты, используемые DOS окном в Windows определены в секциях:
[boot.description]
woafont.fon=English (437)
[386Enh]
woafont=dosapp.fon
EGA80WOA.FON=EGA80WOA.FON
EGA40WOA.FON=EGA40WOA.FON
CGA80WOA.FON=CGA80WOA.FON
CGA40WOA.FON=CGA40WOA.FON
Рассмотрение стандартных файлов настройки мы продолжим, перейдя к параметрам принтера. Для этого мы возвращаемся к файлу WIN.INI, секция [devices]. В этой секции перечислены все принтеры, которые были использованы инсталлированы. Каждая запись определяет имя принтера, имя драйвера и имя выходного устройства (последовательный или параллельный порт, файл); формат записей:
printer name= driver, port1 [,port2[,...]]
где printer name - имя принтера, driver - имя драйвера и portN - имя выводного устройства.
Пример:
[devices]
Epson FX-80=EPSON9,FILE:,LPT1:
то есть: принтер 'Epson FX-80' обслуживается драйвером 'EPSON9.DRV' и может направлять вывод в файл или в параллельный порт #1.
Характеристики выводных устройств должны быть указаны еще в двух местах - в секции [ports], где перечислены имена всех разрешенных выводных устройств и заданы характеристики последовательных портов, и в секции [PrinterPorts], где указаны предельные времена ожидания готовности и ошибки.
В секции [ports] записывается примерно такая информация:
[ports]
LPT1:=
LPT2:=
COM1:=9600,n,8,1
COM2:=9600,n,8,1,x
EPT:=
FILE:=
LPT1.DOS=
А в секции [PrinterPorts] записи похожи на секцию [device] с дополнительно указанными временами ожидания готовности и повторения для каждого устройства.
[PrinterPorts]
Epson FX-80=EPSON9,FILE:,15,45,LPT1:,15,45
С помощью всех рассмотренных параметров описываются установленные в данный момент принтеры, однако нас зачастую не интересуют все мыслимые принтеры, а только один - тот, который подключен непосредственно сейчас. Для того, что бы определить этот принтер надо обратить внимание на секцию [windows], в которой есть параметр:
device= printer name, driver, port
Описывающий имя текущего принтера, обслуживающего его драйвера и используемое выводное устройство. Обычно приходится пользоваться именно этой информацией.
3. Работа с принтером
3.1 Получение контекста устройства
Теперь, когда мы научились определять характеристики принтера, мы можем перейти непосредственно к работе с ним. Для начала нам надо получить хендл контекста устройства, связанного с принтером. Существует две возможности сделать это: самим разобраться с файлом WIN.INI и создать контекст устройства или воспользоваться стандартным диалогом для выбора и настройки принтера. Чаще применяется второй способ (как более "дружественный"), однако иногда проще все сделать самим (особенно, если Вы должны сами проверить наличие и характеристики принтеров - например при инсталляции программы).
Если мы хотим сами разобраться с принтером, то для создания контекста принтера мы должны воспользоваться процедурой:
HDC CreateDC( lpszDriver, lpszDevice, lpszOutput, lpvData );
При этом мы сначала должны определить имя драйвера lpszDriver, имя принтера lpszDevice и имя выводного устройства lpszOutput. Параметр lpvData мы будем устанавливать в NULL, для того, что бы произвести инициализацию по умолчанию (так как это было определено при инсталляции принтера или изменено через “Control Panel”).
Всю нужную для создания контекста устройства информацию мы получим из параметра “device=” секции [windows]:
HDC GetPrinterDC( void ) {
char buf[ 80 ];
char *Device, *Drive, *Output;
static char delimiters[]= ", ";
GetProfileString( "windows", "device", ",,,", buf, sizeof(buf)-1 );
Device= strtok( buf, "," );
Drive= strtok( NULL, delimiters );
Output= strtok( NULL, delimiters );
return Device && Drive && Output ?
CreateDC( Drive, Device, Output, NULL ) : NULL;}
Этот способ позволяет легко получить хендл контекста текущего выбранного принтера. А если нам желательно выбирать или настраивать принтер, то самым удобным будет способ с использованием стандартного диалога (каждый драйвер принтера содержит собственный диалог используемый для его настройки; этот диалог мы можем вызвать, обратившись к функции ExtDeviceMode драйвера - однако то–же самое делает стандартный диалог).
При использовании стандартного диалога мы должны вызвать функцию PrintDlg(...), определенную в COMMDLG.H:
#include "commdlg.h"
BOOL PrintDlg( PRINTDLG FAR* lppd );
typedef struct tagPD {
DWORD lStructSize;
HWND hwndOwner;
HGLOBAL hDevMode;
HGLOBAL hDevNames;
HDC hDC;
DWORD Flags;
UINT nFromPage;
UINT nToPage;
UINT nMinPage;
UINT nMaxPage;
UINT nCopies;
HINSTANCE hInstance;
LPARAM lCustData;
UINT (CALLBACK* lpfnPrintHook)(HWND, UINT, WPARAM, LPARAM);
UINT (CALLBACK* lpfnSetupHook)(HWND, UINT, WPARAM, LPARAM);
LPCSTR lpPrintTemplateName;
LPCSTR lpSetupTemplateName;
HGLOBAL hPrintTemplate;
HGLOBAL hSetupTemplate;
} PRINTDLG;
Вы должны заполнить нужные поля (обычно почти все - 0) этой структуры и вызвать функцию PrintDlg() для выбора текущего принтера и его настройки. Функция возвращает результат TRUE (не 0), если контекст принтера успешно создан, или FALSE (0), если была нажата кнопка “Cancel” или возникла ошибка.
Параметр lStructSize задает размер данной структуры, он должен быть равен sizeof(PRINTDLG), hwndOwner задает хендл окна–пользователя диалога (может быть 0).
Два хендла глобальных объектов hDevMode и hDevNames используются для начальной инициализации диалога. Обычно они задаются 0, а функция PrintDlg() сама создает эти блоки и указывает их хендлы. Если Вы не планируете больше обращаться к функции PrintDlg(), то вы должны их уничтожить (с помощью функции GlobalFree()). Однако обычно эти блоки уничтожаются только при завершении всего приложения - так что при повторном вызове PrintDlg() в структуре PRINTDLG уже содержаться эти хендлы и информация из этих блоков используется для повторной инициализации диалога.
Поле hDC при вызове диалога игнорируется, а после завершения может (если Вы это закажете) содержать хендл созданного контекста принтера.
Интересно рассмотреть только некоторые значения поля Flags:
PD_PRINTSETUP вместо диалога “выбор принтера” вызвать диалог “настройка принтера”.
PD_RETURNDC создать контекст принтера и вернуть его хендл в поле hDC
PD_RETURNIC создать информационный контекст принтера и вернуть его хендл в поле hDC
PD_RETURNDEFAULT не вызывать никакого диалога, просто инициализировать глобальные блоки hDevMode и hDevNames (перед таким вызовом функции PrintDlg() они должны быть равны 0)
После получения хендла контекста устройства принтера мы можем перейти к выводу на принтер. Но сначала нам надо разобраться в том, как Windows организует работу с принтером.
3.2 Основы печати
Когда Вы получили контекст устройства для принтера, на самом деле Вы не получили доступа к самому принтеру, а только лишь к промежуточному метафайлу. Пока Вы рисуете на принтере, все нужные команды запоминаются в этом метафайле и только после того, как Вы объявите о завершении работы со страницей, Windows осуществит печать подготовленной страницы из метафайла.
Реальный механизм сложнее, так как спулинг принтера может быть отключен, а, кроме того, Ваше приложение может использовать специальный метод пополосной печати, который заметно усложняет внутреннюю логику печати.
Такой сложный механизм позволяет осуществить спулинг печати и более-менее эффективное разделение принтера между разными приложениями.
Общие правила, с точки зрения приложения, сводятся к следующему:
начиная работу с принтером, Вы должны зарегистрировать собственное задание на печать. Это исключает одновременное печатание данных разными приложениями.
Далее Вы должны заполнить страницу нужными данными и подать команду на печать этой страницы и на переход к следующей.
Когда все страницы напечатаны, Вы должны сказать Windows о завершении печати Вашего задания.
В этой схеме есть несколько недостатков:
Во-первых, процесс печати даже одной страницы может быть весьма продолжительным (до 30-60 минут) - а при такой схеме Вы не можете даже прервать процесс печати;
Во-вторых, графический образ одной страницы на принтере высокого разрешения (особенно цветном) может занимать десятки мегабайт, что достаточно жестко ограничивает возможности печати.
Для того, что бы решить эти проблемы применяют различные приемы. Для того, что бы можно было выполнять какую-либо иную работу одновременно с печатью, и что бы печать можно было прервать в любой момент, добавляют специальную функцию, называемую AbortProc(), которая вызывается автоматически во время печати для обработки сообщений и для того, что бы работу принтера можно было прервать.
В этом случае, помимо регистрации задания, Вы должны установить собственную AbortProc до начала самой печати.
Для решения второй задачи вводят специальный метод пополосной печати (banding). В этом случае Вы запрашиваете у принтера данные о полосе, которую надо отобразить и заполняете эту полосу (или всю страницу - контекст устройства реально соответствует только этой полосе). Тогда максимальный размер временного файла, содержащего образ страницы, ограничен размерами полосы.
В этом случае Вам надо иметь в виду несколько нюансов:
при пополосной печати функция AbortProc() автоматически вызывается слишком редко, поэтому Вы должны сами предусмотреть вызовы этой функции с достаточной частотой.
ориентация полос не обязательно горизонтальна, так как принтер может печатать как в режиме “портрет” (Portrait), так и в режиме “ландшафт” (Landscape).
Полосу для печати у Вас запрашивает Windows. Вы не знаете заранее ни ориентацию этой полосы, ни ее размеры. Windows формирует запрос для печатания полосы до тех пор, пока не будет напечатана вся страница.
Теперь нам надо рассмотреть непосредственно текст программы, осуществляющий печать.
3.3 Печатание на принтере
Вообще для управления печатью достаточно только одной процедуры - Escape(), которая использовалась в прежних версиях Windows.
int Escape( hDC, nEscape, cbInput, lpsInData, lpvOutput );
Параметр nEscape указывает код выполняемой операции, параметры cbInput и lpsInData указывают исходные данные для операции, а lpvOutput - результат операции. Применение и форма исходных данных и результатов зависит от операции.
В версии Windows 3.1 к этой процедуре добавлен ряд вспомогательных, которые просто выполняют отдельные операции.
Сейчас мы рассмотрим некоторые основные операции, необходимые для печати. Первая операция регистрирует наше задание на печать:
Escape( hDC, STARTDOC, cbName, lpsName, NULL );
или
StartDoc( hDC, lpDocInfo );
struct DOCINFO {
int cbSize;
LPSTR lpszDocName;
LPSTR lpszOutput;};
Параметры cbName и lpsName определяют название нашего задания. Возвращаемое значение, большее 0, указывает на успешную постановку задания в очередь, а значение 0 и меньше указывает на возникшую ошибку.
Далее, если мы хотим печатать страницу целиком, то после ее заполнения мы должны подать команду на печать страницы:
// заполнить страницу
Escape( hDC, NEWFRAME, 0, NULL, NULL );
или
StartPage( hDC );
// заполнить страницу
EndPage( hDC );
В случае успешного завершения задания мы должны сообщить об этом с помощью операции ENDDOC:
Escape( hDC, ENDDOC, 0, NULL, NULL );
или
EndDoc( hDC );
Однако нам может понадобится прервать печать досрочно. Для этого мы должны разработать и установить нашу собственную процедуру AbortProc. Предположим, что такая функция нами написана и ее имя AbortProc(). Тогда мы можем установить ее следующим способом:
FARPROC lpfnAbortProc= MakeProcInstance( (FARPROC)AbortProc, hInstance );
Escape( hDC, SETABORTPROC, 0, (LPSTR)lpfnAbortProc, NULL );
// печатаем...
FreeProcInstance( lpfnAbortProc );
или
FARPROC lpfnAbortProc= MakeProcInstance( (FARPROC)AbortProc, hInstance );
SetAbortProc( hDC, (ABORTPROC)lpfnAbortProc );
// печатаем...
FreeProcInstance( lpfnAbortProc );
При этом, если наше задание будет досрочно снято с печати, то мы должны сообщить об этом (что бы задание было корректно удалено из очереди):
Escape( hDC, ABORTDOC, 0, NULL, NULL );
или
AbortDoc( hDC );
Небольшой пример (поясняющий правила разработки процедуры AbortProc):
static BOOL bAbort;
BOOL PASCAL FAR _export AbortProc( HDC hdcPRN, short nCode ) {
MSG msg;
while ( !bAbort && PeekMessage( &msg,NULL,0,0,PM_REMOVE ) ) {
TranslateMessage( &msg );
DispatchMessage( &msg );}
return !bAbort;}
static BOOL DoPrint( void ) {
static char szName[]= "Test Printing";
FARPROC lpfnAbort;
BOOL answer= FALSE;
HDC hdcPRN;
hdcPRN= GetPrinterDC(); // получение hdcPRN см. выше
lpfnAbort= MakeProcInstance( (FARPROC)AbortProc, hInstance );
Escape( hdcPRN, SETABORTPROC, 0, (LPSTR)lpfnAbort, NULL );
if ( Escape( hdcPRN, STARTDOC, sizeof(szName)-1, szName, NULL ) > 0 ) {
while ( !bAbort && /* есть что печатать */ ) {
// рисуем на странице
if ( Escape( hdcPRN, NEWFRAME, 0,NULL,NULL ) <= 0 )bAbort= TRUE;}
if ( bAbort ) {
Escape( hdcPRN, ABORTDOC, 0,NULL,NULL );
} else {
Escape( hdcPRN, ENDDOC, 0,NULL,NULL );
answer= TRUE;}}
FreeProcInstance( (FARPROC)lpfnAbort );
DestroyDC( hdcPRN );
return answer;}
В этом примере надо обратить внимание на переменную bAbort. Она может быть установлена в значение TRUE для завершения печати. В данном примере досрочное завершение не предусмотрено, хотя его легко можно сделать, устанавливая эту переменную, например, в ответ на выбор пункта меню, на нажатие кнопки диалога или даже по таймеру.
Если мы будем использовать пополосную печать, то нам надо вместо операции NEWFRAME выполнять серию операций NEXTBAND до тех пор, пока полоса для печати не окажется пустой (что соответствует полностью напечатанной странице); при этом внутренний цикл для печати страниц придется несколько изменить:
...RECT rc;
...while ( !bAbort && /* есть что печатать */ ) {
while ( Escape(hdcPRN, NEXTBAND, 0, NULL, (LPSTR)&rc) > 0 ) {
if ( IsRectEmpty( &rc ) ) goto ok_page;
// заполняем полосу или всю страницу, иногда вызывая AbortProc:
lpfnAbortProc( hdcPRN, 0 );
// Внимание! используется указатель на функцию, а не она сама!}
bAbort= TRUE;
ok_page:;}...