Выбор шрифта для контекста отображения
Рассмотрим способы отображения символов кириллицы на примере программы WINDOW, сокращенный текст которой приведен в листинге 2 (полный текст приложен к электронной версии этой статьи; там же имеются файлы resource.h и window.rc к программе WINDOW, их адреса - соответственно http://www.pcworld.ru/1998/12/unicode/h.htm и http://www.pcworld.ru/1998/12/unicode/rc.htm). Программа выводит в своем окне номер и название установленного в системе набора символов, затем название текущего шрифта и строку символов "AaBbCc АаБбВв". В окне есть меню Font, позволяющее выбрать для этого сообщения шрифт и набор символов.
Вообще говоря, стандартная диалоговая панель Windows для выбора шрифта, которую открывает наша программа при выборе в меню Font пункта Select font, содержит в правом нижнем углу поле Script, позволяющее выбрать набор символов для заданного шрифта. Однако нам необходима непосредственная работа с наборами символов, поэтому мы реализовали в программе специальное меню Font.
Для выбора шрифта в программе применяется функция ChooseFont, которая берет необходимую ей информацию из структуры типа CHOOSEFONT. Эта структура содержит поле с именем lpLogFont, указывающее, в свою очередь, на структуру типа LOGFONT. Номер набора символов задается в поле lfCharSet структуры LOGFONT. Программа работает с четырьмя наборами: ANSI_CHARSET, OEM_ CHARSET, RUSSIAN_CHARSET и DEFAULT_CHARSET.
Описанная картина, возможно, покажется знакомой тем, кто имеет опыт программирования в Windows 3.1. Однако если в Windows 3.1 можно работать с русскими буквами, задав набор символов ANSI_CHARSET или DEFAULT_CHARSET, то здесь, как мы сейчас убедимся, ситуация совершенно иная.
Откомпилируйте программу WINDOW в двух вариантах - с инструкцией #define UNICODE и без нее. Первый вариант, рассчитанный на Unicode, можно будет запускать только под управлением Windows NT, второй, работающий с символами ANSI, - и под управлением Windows NT, и под управлением Windows 95.
Начнем наши эксперименты с ANSI-программы. Запустим ее и выберем шрифт Courier New. Символы кириллицы отобразятся неправильно. Почему?
Запустив утилиту Character Map, несложно определить, что Courier New - это шрифт Unicode. Следовательно, ANSI-программа не может работать с ним непосредственно, и система строит на основе Courier New виртуальный шрифт для кодовой страницы, которая определяется набором символов, выбранным для данного контекста отображения. Поскольку мы не заказывали никакой конкретный набор, шрифт был построен для набора DEFAULT_CHARSET, не содержащего символов кириллицы. Отметим, что DEFAULT_CHARSET никак не связан с набором символов, установленным в системе (в нашем случае - кириллическим), хотя это и можно было бы предположить, исходя из его названия.
Если выбрать тот же шрифт, предварительно отметив в меню Font строку RUSSIAN_CHARSET, русские буквы появятся, поскольку система построит заказанный ей виртуальный шрифт на основе кириллического набора. Более того, если выбрать шрифт, не содержащий символов кириллицы, система попытается подобрать ближайший эквивалент, в котором эти символы присутствуют. Что же касается набора ANSI_CHARSET, то для него, как и для DEFAULT_CHARSET, будет построен шрифт без кириллицы. Таким образом, единственный надежный способ получить кириллицу в ANSI-программе, при том что шрифт построен по стандарту Unicode, - это задать в поле lfCharSet структуры LOGFONT значение RUSSIAN_CHARSET.
Запустим теперь Unicode-программу. Как легко убедиться, она правильно выводит кириллицу независимо от того, какой набор символов задать в меню Font (если, конечно, выбран шрифт Unicode, содержащий кириллицу). Оно и понятно: программа работает непосредственно с Unicode-шрифтом, а набор символов, который нужен для построения виртуального шрифта, здесь никак не используется.
Однако картина кардинально изменится при выборе шрифта Baltica, разработанного для Windows 3.1. Программа ANSI будет правильно выводить русские буквы независимо от заданного набора символов, а программа Unicode не будет их выводить вообще. Правда, если в последнем случае заранее отметить в меню Font строку RUSSIAN_CHARSET, русские буквы появятся, но шрифт будет другим (например, MS Sans Serif).
Дело в том, что в шрифте Baltica, подготовленном в старом формате, кириллическая область Unicode не представлена. В результате ANSI-программа, которая работает с этим шрифтом непосредственно (т. е. без построения виртуального шрифта), показывает кириллицу, а Unicode-программа - нет.
Этот эффект, несомненно, знаком многим пользователям Office 97. В программах Office 97 (и для Windows NT, и для Windows 95) последовательно проведено представление всей текстовой информации в Unicode. Поэтому если, например, открыть в Word 97 документ, подготовленный в Word 6 или 7 с использованием старых шрифтов, вместо русских букв в окне появятся квадратики: так обозначается отсутствие в выбранном шрифте нужного символа. Единственный возможный выход здесь - изменить шрифт на соответствующий стандарту Unicode.
Перекодирование однобайтовых символов в Unicodе и обратно
Для перекодирования символов из ANSI в Unicode и обратно служат, как уже упоминалось, четыре функции: mbstowcs и MultiByteToWideChar преобразуют строку однобайтовых символов в строку Unicode, а wcstombs и WideCharToMultiByte выполняют обратное преобразование (mbstowcs и wcstombs - это функции стандартной библиотеки Си, а MultiByteToWideChar и WideCharToMultiByte - функции программного интерфейса Win32).
Функции mbstowcs и wcstombs принимают по три параметра: указатель на буфер, в который будет записан результат перекодирования, указатель на исходную строку и число перекодируемых символов. Тем самым они никак не учитывают при преобразовании набор символов, а значит, ими можно пользоваться лишь в самом простом случае (фактически их корректная работа гарантирована лишь при условии, что все символы относятся к набору Latin 1).
В более сложных ситуациях (в частности, если предполагается работа с кириллицей) следует использовать функции MultiByteToWideChar и WideCharToMultiByte.
У функции MultiByteToWideChar пять параметров. Первый - это номер кодовой страницы для исходной строки. Страницу можно задать как непосредственно, так и константой типа CP_ACP (установленная в системе кодовая страница ANSI), CP_OEMCP (установленная в системе страница OEM), CP_THREAD_ACP (страница текущего потока). Второй параметр - набор флагов, определяющий способ обработки диакритик (надстрочных и подстрочных знаков) и неправильных символов. Флаг MB_PRECOMPOSED означает, что базовый символ и диакритика объединены, несовместимый с ним флаг MB_COMPOSITE - что диакритики записаны как отдельные символы. Флаг MB_ERR_INVALID_CHARS задает проверку корректности символов в исходной строке. Оставшиеся параметры определяют адрес и длину входной и выходной строк.
Функция WideCharToMultiByte дополнительно к перечисленным принимает еще два параметра, которые нужны, если применяется обработка неправильных символов.
В листинге 3 приводится текст простейшей программы txt2uni, перекодирующей текстовые файлы из ANSI в Unicode. Программа открывает файл в двоичном режиме и считывает его по байтам в символьную переменную ch. Для каждого байта вызывается функция mbstowcs или MultiByteToWideChar (вы можете попробовать любую), а результат перекодирования помещается в переменную wch типа wchar_t, после чего записывается в выходной файл функцией fputwc, специально предназначенной для работы с символами Unicode.
Листинг 4 содержит текст аналогичной программы uni2txt.c, преобразующей файл Unicode в текстовый файл ANSI. Это Unicode-программа, ее первая строка представляет собой инструкцию #define UNICODE. Вместо привычной вам функции main использована функция wmain, которая получает параметры в виде строк Unicode. Таким образом, путь к исходному файлу передается функции _wfopen как строка Unicode.
Для открытия исходного файла применяется функция _wfopen, а для считывания из него - функция fgetwc; обе они предназначены специально для Unicode-программ.
В самое начало выходного файла программа txt2uni записывает байты 0xFF и 0xFE. Это сигнатура, определяющая порядок следования байтов в символах Unicode. Байты следуют в том же порядке, что и разряды в машинном слове на той компьютерной платформе, где создается файл. Для процессоров Intel, где первым идет самый младший бит, сигнатура имеет значение 0xFEFF. Противоположному порядку (первый бит - старший), применяемому, например, в компьютерах Macintosh, соответствует сигнатура 0xFFEF.
Программа uni2txt проверяет сигнатуру и, если она не равна 0xFEFF, завершается с ошибкой. В чуть более сложном варианте программа могла бы осуществлять необходимую перестановку.
Об авторах
Братья Александр Вячеславович и Григорий Вячеславович Фроловы - авторы серий книг "Библиотека системного программиста" и "Персональный компьютер. Шаг за шагом". E-mail: frolov@glasnet.ru http://www.glasnet.ru/~frolov