"О чём не пишут в книгах по 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).

Листинг 1.21. Обработчик нажатия кнопки BtnBuild

procedure TFomWindows.BtnBuildClick(Sender: TObject);

begin

 Screen.Cursor := crHourGlass;

 try

  TreeWindows.Items.Clear;

  EnumWindows(@EnumWindowsProc, 0);

 finally

  Screen.Cursor := crDefault;

 end;

end;

Рис. 1.8. Окно программы EnumWnd


Все, что делает этот обработчик, — это очищает компонент 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,

 // потому что функции, в отличие от сообщений, не

 // умеют работать с элементами управления,

 // принадлежащими окнам чужих процессов.

 TextLen := SendMessage(Wnd, WM_GETTEXTLENGTH, 0, 0);

 // Устанавливаем длину строковой переменной, которая

 // будет служить буфером для заголовка окна.

 // Использование SetLength гарантирует, что будет

 // выделена специальная область памяти, на которую не

 // будет других ссылок.

 SetLength(Text, TextLen);

 // Если заголовок окна - пустая строка, TextLen будет

 // иметь значение 0, и указатель Text при выполнении

 // Set Length получит значение nil. Но при обработке

 // сообщения WM_GETTEXT оконная процедура в любом случае

 // попытается записать строку по переданному адресу,

 // даже если заголовок окна пустой - в этом случае в

 // переданный буфер будет записан один символ -

 // завершающий ноль. Но если будет передан nil, то

 // попытка записать что-то в такой буфер приведет к

 // Access violation, поэтому отправлять окну WM_GETTEXT

 // можно только в том случае, если TextLen gt; 0.

 if TextLen gt; 0 then

  SendMessage(Wnd, WM_GETTEXT, TextLen + 1, LParam (Text));

 // Заголовок окна может быть очень длинным - например, в

 // Memo заголовком считается весь текст, который там

 // есть. Практика показывает, что существуют проблемы

 // при добавлении в TTreeView узлов с очень длинным

 // названиями: при попытке открыть такой узел программа,

 // запущенная из Delphi, вылетает в отладчик (при

 // запуске вне среды Delphi проблем не замечено). Чтобы

 // этого не происходило, слишком длинные строки

 // обрезаются.

 if TextLen gt; 100 then

  Text := Copy(Text, 1, 100) + '...';

 GetClassName(Wnd, ClassName, ClassNameLen);

 ClassName[ClassNameLen - 1] := #0;

 if Text = '' then NodeName := 'Без названия (' + ClassName + ') '

 else NodeName := Text + ' (' + ClassName + ')';

 Node := FormWindows.TreeWindows.Items.AddChild(ParentNode, NodeName);

 // Записываем в данные узла дескриптор соответствующего

 // ему окна, чтобы иметь возможность отбросить непрямые

 // потомки.

 Node.Data := Pointer(Wnd);

 // Вызываем EnumChildWindows, передавая функцию

 // EnumWindowsProc в качестве параметра, а указатель на

 // созданный узел - в качестве параметра этой функции.

 // При этом EnumWindowsProc будет вызываться из

 // EnumChildWindows, т.е. получается рекурсия.

 EnumChildWindows(Wnd, @EnumWindowsProc, LParam(Mode));

end;

Как мы помним, первый параметр функции обратного вызова для 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, добавляется

 // единица, потому что нужно вручную учесть добавочный

 // байт для завершающего нуля.

 TextLen := SendMessage(Wnd, WM_GETTEXTLENGTH, 0, 0) + 1;

 // Выделяем требуемое количество памяти. Так как

 // компилятор не освободит эту памяти автоматически,

 // необходимо использовать блок try/finally, иначе будут

 // утечки памяти при исключениях.

 Text := StrAlloc(TextLen);

 try

  // Так как для буфера даже при пустом заголовке будет

  // выделен хотя бы один байт, здесь можно отправлять

  // WM_GETTEXT, не проверяя длину строки, как это было

  // в предыдущем варианте - буфер всегда будет

  // корректным.

  SendMessage(Wnd, WM_GETTEXT, TextLen, LParam(Text));

  // Обрезаем слишком длинною строку. Модифицировать

  // PChar сложнее, чем string. Вставка нуля в середину

  // строки приводит к тому, что все API-функции будут

  // игнорировать "хвост", но на работу StrDispose это не

  // повлияет, т.к. функция StrAlloc (а также прочие

  // функции выделения памяти для нуль-терминированных

  // строк модуля SysUtils) сохраняет размер выделенной

  // памяти рядом с самой строкой, и StrDispose

  // ориентируется именно на этот размер, а не на

  // завершающий ноль.

  if TextLen gt; 104 then

  begin

   (Text + 104)^ := #0;

   (Text + 103)^ := '.';

   (Text + 102)^ := '.';

   (Text + 101)^ := '.';

   (Text + 100)^ := ' ';

  end;

  GetClassName(Wnd, ClassName, ClassNameLen);

  if Text^ = #0 then NodeName := 'Без названия (' + ClassName + ') '

  else NodeName := Text + ' (' + ClassName + ');

  Node := FormWindows.TreeWindows.Items.AddChild(ParentNode, NodeName);

  Node.Data := Pointer(Wnd);

  EnumChildWindows(Wnd, @EnumWindowsProc, LParam(Node));

 finally

  // Вручную освобождаем память, выделенную для буфера

  StrDispose(Text);

 end;

end;

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