О чём не пишут в книгах по Delphi - Антон Григорьев 12 стр.


Следует отметить, что TBitmap иногда создает DIB-секции, даже если свойства HandleType и PixelFormat явно указывают на использование DDB. Особенно часто это наблюдается для изображений большого размера. По всей видимости, это происходит, когда в системном пуле нет места для хранения DDB-изображения такого размера, и разработчики TBitmap решили, что в таком случае лучше создать DIB-изображение, чем не создавать никакого. Пример BitmapSpeed на прилагаемом компакт-диске позволяет сравнить скорость выполнения различных операций с DDB- и DIB-изображениями.

1.1.12. ANSI и Unicode

Windows поддерживает две кодировки: ANSI и Unicode. В кодировке ANSI (American National Standard Institute) каждый символ кодируется однобайтным кодом. Коды от 0 до 127 совпадают с кодами ASCII, коды от 128 до 255 могут означать разные символы различных языков в зависимости от выбранной кодовой страницы. Кодовые страницы позволяют уместить многочисленные символы различных языков в однобайтный код, но при этом можно работать только с одной кодовой страницей, т.е. с одним языком. Неверно выбранная кодовая страница приводит к появлению непонятных символов (в Интернете их обычно называют "кракозябрами") вместо осмысленного текста.

В кодировке Unicode используется 2 байта на символ, что даёт возможность закодировать 65 536 символов. Этого хватает для символов латиницы и кириллицы, греческого алфавита, китайских иероглифов, арабских и еврейских букв, а также многочисленных дополнительных (финансовых, математических и т. п.) символов. Кодовых страниц в Unicode нет.

Примечание

Кодовая страница русского языка в ANSI имеет номер 1251. Кодировка символов в ней отличается от принятой в DOS так называемой альтернативной кодировки. В целях совместимости для DOS-программ, а также для консольных приложений Windows использует альтернативную кодировку. Именно поэтому при выводе русского текста в консольных приложениях получаются те самые "кракозябры". Чтобы избежать этого, следует перекодировать символы из кодировки ANSI в DOS при выводе, и наоборот - при вводе. Это можно сделать с помощью функций CharToOem и OemToChar.

Windows NT/2000/XP поддерживает ANSI и Unicode в полном объеме. Это значит, что любая функция, работающая со строками, представлена в двух вариантах: для ANSI и для Unicode. Windows 9x/МЕ в полном объеме поддерживает только ANSI. Unicode-варианты в этих системах есть у относительно небольшого числа функций. Каждая страница MSDN, посвященная функции, работающей со строками (или со структурами, содержащими строки), в нижней части содержит надпись, показывающую, реализован ли Unicode-вариант этой функции только для NT/2000/XP или для всех платформ.

Примечание

В качестве примера рассмотрим функции вывода текста на экран. Unicode-версию на всех платформах имеют только две из них TextOut и ExtTextOut. Функции DrawText и DrawTextEx имеют Unicode-варианты только в Windows NT/2000/XP. Если же смотреть функции для работы с окнами, то среди них нет ни одной, которая имела бы Unicode-вариант в Windows 9х/МЕ. Следует отметить, что с относительно недавнего времени Microsoft предоставляет расширение для Windows 9x/МЕ которое позволяет добавить полную поддержку Unicode в эти системы. Это расширение называется MSLU (Microsoft Layer for Unicode), его можно скачать с официального сайта Microsoft.

Рассмотрим, как сосуществуют два варианта на примере функции RegisterWindowMessage. Согласно справке, она экспортируется библиотекой user32.dll. Однако если посмотреть список функций, экспортируемых этой библиотекой (это можно сделать, например, при помощи утилиты TDump.exe, входящей в состав Delphi), то там этой функции не будет, зато будут функции RegisterWindowMessageA и RegisterWindowMessageW. Первая из них - это ANSI-вариант функции, вторая - Unicode-вариант (буква W означает Wide - широкий; символы кодировки Unicode часто называются широкими из-за того, что на один символ приходится не один, а два байта).

Сначала разберемся с тем, как используются два варианта одной функции в Microsoft Visual C++. В стандартных заголовочных файлах учитывается наличие макроопределения UNICODE. Есть два символьных типа - CHAR для ANSI и WCHAR для Unicode. Если макрос UNICODE определен, тип TCHAR соответствует типу WCHAR, если не определен - типу CHAR (после этого производные от TCHAR типы, такие как LPCTSTR автоматически начинают соответствовать кодировке, определяемой наличием или отсутствием определения UNICODE). В заголовочных файлах импортируются оба варианта функции, а также определяется макрос RegisterWindowMessage. Его смысл также зависит от макроса UNICODE: если он определен, RegisterWindowMessage эквивалентен RegisterWindowMessageW, если не определен - RegisterWindowMessageA. Все функции, поддерживающие два варианта кодировки, импортируются точно так же. Таким образом, вставляя или убирая макроопределение UNICODE, можно, не меняя ни единого символа в программе, компилировать ее ANSI- или Unicode-версию.

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

Рассмотрим, как та же функция RegisterWindowMessage импортируется модулем Windows (листинг 1.19).

Листинг 1.19. Импорт функции RegisterWindowMessage

interface

...

function RegisterWindowMessage(lpString: PChar): UINT; stdcall;

function RegisterWindowMessageA(lpString: PAnsiChar): UINT;

stdcall; function RegisterWindowMessageW(lpString: PWideChar): UINT; stdcall;

...

implementation

...

function RegisterWindowMessage;

external user32 name 'RegisterWindowMessageA';

function RegisterWindowMessageA;

external user32 name 'RegisterWindowMessageA';

function RegisterWindowMessageW;

external user32 name 'RegisterWindowMessageW';

Видно, что функция RegisterWindowMessageA импортируется дважды: один раз под своим настоящим именем, а второй раз - под именем RegisterWindowMessage. Любое из этих имен подходит для вызова ANSI-варианта этой функции (напоминаю, что типы PChar и PAnsiChar эквивалентны). Чтобы вызвать Unicode-вариант функции, потребуется функция RegisterWindowMessageW.

Структуры, содержащие строковые данные, также имеют ANSI- и Unicode-вариант. Например, структура WNDCLASS в модуле Windows представлена типами TWndClassA (с синонимами WNDCLASSA и tagWNDCLASSA) и TWndClassW (с синонимами WNDCLASSW и tagWHDCLASSW). Тип TWndClass (и его синонимы WNDCLASS и tagWNDCLASS) эквивалентен типу TWndClassA. Соответственно, при вызове функций RegisterClassA и RegisterClassExA используется тип TWndClassA, при вызове RegisterClassW и RegisterClassExW - тип TWndClassW.

1.1.13. Строки в Windows API

Unicode в Delphi встречается редко, т.к. программы, использующие эту кодировку, не работают в Windows 9x/МЕ. Библиотека VCL также игнорирует возможность работы с Unicode, ограничиваясь ANSI. Поэтому далее мы будем говорить только об ANSI. С кодировкой Unicode можно работать аналогично, заменив PChar на PWideChar и string на WideString.

Для работы со строками в Delphi наиболее распространен тип AnsiString, обычно называемый просто string (более детально отношения между этими типами рассмотрены в главе 3). Переменная типа string является указателем на строку, хранящуюся в динамической памяти. Этот указатель указывает на первый символ строки. По отрицательному смещению хранится число символов в строке и счетчик ссылок, который позволяет избежать ненужных копирований строки, реализуя так называемое "копирование при необходимости". Если присвоить одной строковой переменной значение другой строковой переменной, то строка не копируется, а просто обе переменные начинают указывать на одну и ту же строку. Счетчик ссылок при этом увеличивается на единицу. Когда строка модифицируется, проверяется счетчик ссылок: если он не равен единице, то строка копируется, счетчик ссылок старой копии уменьшается на единицу, у новой копии счетчик ссылок будет равен единице, и переменная, которая меняется, будет указывать на новую копию. Таким образом, строка копируется только тогда, когда одна из ссылающихся на нее переменных начинает изменять эту строку, чтобы изменения не коснулись остальных переменных. При любых модификациях строки в ее конец автоматически добавляется нулевой символ (при подсчете длины строки с помощью функции Length он игнорируется). Но если присвоить строковой переменной пустую строку, то эта переменная станет нулевым указателем (nil), память для хранения одного символа #0 выделена не будет. При выходе строковой переменной из области видимости (т. е., например, при завершении процедуры, в которой она является локальной переменной, или при уничтожении объекта, полем которого она является) она автоматически финализируется, т.е. счетчик ссылок уменьшается на единицу и, если он оказывается равен нулю, память, выделенная для строки, освобождается. (О внутреннем устройстве AnsiString см. также разд. 3.3.)

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

Примечание

В отличие от string, тип WideString не имеет счетчика ссылок, и любое присваивание переменных этого типа приводит к копированию строки. Это сделано в целях совместимости с системным типом BSTR, использующимся в COM/DCOM и OLE.

Функции Windows API не поддерживают тип string. Они работают со строками, оканчивающимися на #0 (нуль-терминированные строки, null-terminated strings). Это означает, что строкой называется указатель на цепочку символов. Признаком конца такой цепочки является символ с кодом 0. Раньше для таких строк существовал термин ASCIIZ. ASCII - название кодировки, Z - zero. Сейчас кодировка ASCII в чистом виде не встречается, поэтому этот термин больше не применяется, хотя это те же самые по своей сути строки. Как уже говорилось, в Delphi ко всем строкам типа string неявно добавляется нулевой символ, не учитывающийся при подсчете числа символов. Это сделано для совместимости с нуль-терминированными строками. Однако эта совместимость ограничена.

Для работы с нуль-терминированными строками в Delphi обычно служит тип PChar. Формально это указатель на один символ типа Char, но подразумевается, что это только первый символ строки, и за ним следуют остальные символы. Где будут эти символы размещены и какими средствами для них будет выделена память, программист должен решить самостоятельно. Он же должен позаботиться о том, чтобы в конце цепочки символов стоял #0.

Строку, на которую указывает PChar, можно использовать везде, где требуется string - компилятор сам выполнит необходимые преобразования. Обратное неверно. Фактически, string - это указатель на начало строки, завершающейся нулем, т.е. тот самый указатель, который требуется при работе с PChar. Однако, как уже отмечалось, некорректные манипуляции с этим указателем могут привести к нежелательным эффектам, поэтому компилятор требует явного приведения переменных и выражений типа string к PChar. В свою очередь, программист должен ясно представлять, к каким последствиям это может привести.

Если посмотреть описание функций API, имеющих строковые параметры, в справке, можно заметить, что в некоторых случаях строковые параметры имеют тип LPCTSTR (как, например, у функции SetWindowText), а в некоторых - LPTSTR (GetWindowText). Ранее мы говорили, что появление префикса C после LP указывает на то, что это указатель на константу, т.е. то, на что указывает такой указатель, не может быть изменено. Тип LPCTSTR имеют те строковые параметры, содержимое которых функция только читает, но не модифицирует. С такими параметрами работать проще всего. Рассмотрим на примере функции SetWindowText, как можно работать с такими параметрами (листинг 1.20).

Листинг 1.20. Вызов функции с параметром типа LPCTSTR

{ Вариант 1 }

SetWindowText(Handle, 'Строка');

{ Вариант 2

S -переменная типа string }

SetWindowText(PChar(S));

{ Вариант 3

X - переменная типа Integer }

SetWindowText(PChar('Выполнено ' + IntToStr(X) + '%'));

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

Во втором варианте функции передается указатель, хранящийся в переменной S. Такое приведение string к PChar безопасно, т.к. строка, на которую ссылается переменная S, не будет модифицироваться. Но здесь существует одна тонкость: конструкция PChar(S) - это не просто приведение типов, при ее использовании неявно вызывается функция _LStrToPChar. Как мы уже говорили, когда string хранит пустую строку, указатель просто имеет значение nil. Функция _LStrToPChar проверяет, пустая ли строка хранится в переменной, и, если не пустая, возвращает этот указатель, а если пустая, то возвращает не nil, а указатель на символ #0, который специально для этого размещен в сегменте кода. Поэтому даже если содержит пустую строку, в функцию будет передан ненулевой указатель.

Вычисление строковых выражений требует перераспределения памяти, а это компилятор делает только с выражениями типа string. Поэтому результат выражения, приведенного в третьем варианте, также имеет тип string. Но его можно привести к PChar. Память для хранения результата выражения выделяется динамически, как и для обычных переменных типа string. Чтобы передать указатель на но выражение в функцию, следует привести его к PChar. В эпилог процедуры, вызывающей функцию SetWindowText или иную функцию с подобным аргументом, добавляется код, который освобождает динамически сформированную строку, поэтому утечек памяти не происходит. Разумеется, существуют и другие способы формирования параметра типа LPCTSTR, кроме предложенных здесь. Можно, например, выделить память для нуль-терминированной строки с помощью StrNew или родственной ей функции из модуля SysUtils. Можно использовать массив типа Char. Можно выделять память какими-либо другими способами. Но предложенные здесь три варианта в большинстве случаев наиболее удобны.

Параметры типа LPTSTR применяются в тех случаях, когда функция может не только читать, но и модифицировать передаваемое ей значение. В большинстве случаев такие параметры чисто выходные, т.е. функция не интересуется, какое значение имел параметр при вызове, используя его только для возврата значения. При возврате строкового значения всегда возникает проблема: где, кем и как будет выделена память, в которую будет записана строка? Функции Windows API, за очень редким исключением, решают эту проблему следующим образом: память должна выделить вызывающая программа, а в функцию передается указатель на этот заранее выделенный блок. Сама функция только копирует строку в этот блок.

Назад Дальше