"О чём не пишут в книгах по Delphi" - читать интересную книгу автора (Григорьев А. Б.)
1.2.1. Пример EnumWnd
Программа EnumWnd представляет собой простой пример использования функций EnumWindows и EnumChildWindows, а также функций обратного вызова, которые необходимы для работы этих двух функций. Программа ищет все окна, созданные на данный момент в системе, и отображает их в виде дерева: каждый узел дерева соответствует одному окну, дочерние узлы соответствуют дочерним окнам данного окна (рис. 1.8).
Программа EnumWnd является также примером того, как можно работать с параметрами типа LPTSTR, через которые функции Windows API возвращают программе строковые значения. В разд. 1.1.13 были перечислены три способа создания буфера для работы с такими параметрами: выделение памяти в стеке в виде массива элементов типа Char, использование строк типа string и строк типа PChar. Все три способа реализованы в примере EnumWnd. На главной и единственной форме программы EnumWnd размещены два компонента: TreeWindow типа TTreeView и кнопка BtnBuild. Обработчик нажатия кнопки выглядит очень лаконично (листинг 1.21).
Все, что делает этот обработчик, — это очищает компонент TreeWindows и вызывает EnumWindows, передавая ей функцию обратного вызова EnumWindowsProc, в которой и выполняется основная работа. Сразу отметим, что в этом примере мы будем использовать одну и ту же функцию обратного вызова как для EnumWindows, так и для EnumWindowsProc. Сама функция обратного вызова выглядит следующим образом (листинг 1.22).
Листинг 1.22. Функция обратного вызова EnumWindowsProc (первый вариант)
// Это функция обратного вызова, которая будет
// использоваться при вызове EnumWindows и EnumChildWindows.
// Тип второго параметра не совпадает с типом, который
// указан MSDN. Однако TTreeNode, как и любой класс,
// является указателем, поэтому может использоваться везде,
// где требуется нетипизированный указатель - на двоичном
// уровне между ними нет разницы. Указатель на функцию
// обратного вызова в EnumWindows и EnumChildWindows в
// модуле Windows.dcu объявлен как нетипизированный
// указатель, поэтому компилятор не контролирует
// соответствие реального прототипа заявленному.
function EnumWindowsProc(Wnd: HWND; ParentNode: TTreeNode): Bool; stdcall;
// Система не предусматривает возможности узнать, какова
// длина имени класса, поэтому при получении этого имени
// приходится выделять буфер большой длины в надежде, что
// имя класса не окажется еще длиннее. В данном примере
// размер этого буфера определяется константой ClassNameLen.
// Крайне маловероятно, что имя класса скажется длиннее,
// чем 511 символов (512-й зарезервирован для завершающего
// нулевого символа).
const
ClassNameLen = 512;
var
// Здесь будет храниться заголовок окна
Text: string;
TextLen: Integer;
// Это - буфер для имени класса
ClassName: array[0..ClassNameLen - 1] of Char;
Node: TTreeNode;
NodeName: string;
begin
Result := True;
// Функция EnumChildWindows перечисляет не только
// непосредственно дочерние окна данного окна, но и
// дочерние окна его дочерних окон и т.п. Но при
// построении дерева на каждом шаге нам нужны только
// прямые потомки, поэтому все окна, не являющиеся прямыми
// потомками, мы здесь игнорируем.
if Assigned(ParentNode) and (GetParent(Wnd) lt;gt; HWND(ParentNode.Data)) then Exit;
// Получаем длину заголовка окна. Вместо функций
// GetWindowText и GetWindowTextLength мы здесь
// используем сообщения WM_GETTEXT и WM_GETTEXTLENGTH,
Как мы помним, первый параметр функции обратного вызова для EnumWindows содержит дескриптор найденного окна, а второй параметр может быть произвольным 4-байтным значением, которое система игнорирует, просто копируя сюда то значение, которое было передано при вызове EnumWindows или EnumChildWindows. Мы задействуем этот параметр для передачи ссылки на узел дерева, соответствующий родительскому окну. Также договоримся, что в свойство Data каждого узла будем записывать дескриптор связанного с ним окна. Для окон верхнего уровня ссылка будет иметь значение nil — это обеспечивается тем, что при вызове EnumWindows второй параметр равен нулю (см. листинг 1.21).
Работа функции начинается с проверки того, что родительским окном для данного окна действительно является то окно, чей дескриптор связан с узлом родительского окна. Эта проверка нужна потому, что функция EnumChildWindows перечисляет не только дочерние, но и "внучатые", "правнучатые" и т.д. окна. Нам здесь это не нужно, на каждом шаге нас интересуют только непосредственные "дети" окна, а до "внуков" мы доберемся, когда вызовем EnumChildWindows для дочерних окон, поэтому и отсеиваем лишнее.
Следующий шаг — получение заготовка окна. Для этого мы используем сообщение WM_GETTEXT (разница между этим сообщением и функцией GetWindowText обсуждается в разд. 1.3.1). Буфером является переменная Text типа string. Сначала с помощью сообщения WM_GETTEXTLENGTH мы узнаем длину заголовка окна, а затем выделяем под строку Text требуемое количество памяти с помощью SetLength. После этого можно получить строку с помощью сообщения WM_GETTEXT. Второй параметр этого сообщения — адрес буфера, в который будет помещена строка. Так как переменная типа string и есть указатель на буфер строки (это детально обсуждается в разд. 3.3), достаточно просто привести переменную Text к типу LParam и передать получившееся значение.
Примечание
Строго говоря, у нас здесь нигде нет параметра типа LPTSTR, однако при работе с параметрами этого типа можно действовать точно так же: выделить для строки типа string нужное количество памяти и передать эту переменную, приведенную к типу LPTSTR, в качестве параметра.
Далее получаем название класса окна. Для этого мы используем статический массив ClassName, т.е. размер буфера определяется на этапе компиляции. С одной стороны, это неправильно, потому что ограничений на длину имени класса не существует (по крайней мере, в документации они не упомянуты), а мы уже говорили, что такой метод следует применять только тогда, когда существует известное на этапе компиляции ограничение длины. По с другой стороны, когда речь идет об имени класса, не существует ничего подобного сообщению WM_SETTEXTLENGTH, т.е. API не дает возможности получить длину имени класса, что делает бессмысленными все манипуляции с размером буфера во время работы программы. Поэтому мы определяем размер буфера еще на этапе компиляции, исходя из того, что слишком уж длинные имена классов встречаются редко. При вызове функции с параметром типа LPTSTR можно просто передавать массив без приведения типа, т.к. LPTSTR — это PChar, а массивы символов Char, индексирующиеся с нуля, компилятор полагает совместимыми с этим типом и все необходимые преобразования делает неявно.
И, хотя мы и взяли размер буфера с хорошим запасом, нельзя исключать ситуации, когда имя класса окажется длиннее, чем буфер. Ничего страшного при этом не произойдет, т.к. мы передаем в функцию размер буфера специально для того, чтобы она не пыталась что-то записать за пределами буфера. Но в этом случае завершающий строку символ #0 не попадет в буфер, и при попытке дальше работать с этой строкой какая-нибудь другая функция может, не найдя конца строки в пределах буфера, попытаться поискать этот конец за его пределами, что приведет к непредсказуемым результатам. Поэтому на всякий случай записываем #0 в последний символ буфера. Если имя класса оказалось длиннее буфера, это обрежет строку по границе буфера, а если короче, то это ничему не повредит, т.к. признак конца строки будет в буфере где-то раньше, а все символы после него все равно игнорируются. После этого остается только создать новый элемент в дереве, а чтобы заполнить его дочерние элементы — вызвать EnumChildWindows для получения списка дочерних окон. Так как в EnumChildWindows передается та же функция обратного вызова, получается рекурсия, которая останавливается тогда, когда функция доходит до окна, не имеющего дочерних окон. Ранее мы говорили, что программа EnumWnd демонстрирует три метода получения строки через параметр типа LPTSTR, но пока мы увидели только два (действительно, трудно показать три различных метода на примере получения двух строк). Чтобы показать третий вариант — организацию буфера через строки типа PChar — перепишем функцию EnumWindowsProc (листинг 1.23). В исходном коде программы EnumWnd этот вариант присутствует в виде комментария. Можно убрать этот комментарий, а закомментировать, наоборот, первый вариант, чтобы попробовать, как работает получение строки с помощью PChar.
Листинг 1.23. Функция обратного вызова EnumWindowsProc (второй вариант)
// Ниже приведен другой вариант функции
// EnumWindowsРrос, который отличается от предыдущего тем,
// что буфер для получения заголовка окна организуется
// вручную с помощью переменной типа PChar, а не string. По
// своим функциональным возможностям оба варианта равноценны.
function EnumWindowsProc(Wnd: HWND; ParentNode: TTreeNode): Bool; stdcall;
const
ClassNameLen = 512;
var
TextLen: Integer;
Text: PChar;
ClassName: array[0..ClassNameLen - 1] of Char;
Node: TTreeNode;
NodeName: string;
begin
Result := True;
if Assigned(ParentNode) and (GetParent(Wnd) lt;gt; HWND(ParentNode.Data)) then Exit;
// Здесь, в отличие от предыдущего варианта к длине,
// получаемой через WM_GETTEXTLENGTH, добавляется
// единица, потому что нужно вручную учесть добавочный
// Вручную освобождаем память, выделенную для буфера
StrDispose(Text);
end;
end;
Второй вариант функции EnumWindowsProc отличается от первого только тем что для организации буфера для получения имени окна вместо переменной типа string используется переменная типа PChar. Соответственно, все манипуляции с динамической памятью теперь выполняются вручную, а просто отсечь конец слишком длинной строки и прибавить к результату другую строку (многоточие) мы не можем, приходится модифицировать строку посимвольно. Тем не менее видно, что и с помощью типа PChar задача создания буфера для строки, возвращаемой API-функцией, достаточно легко решается.