Третий способ — выделение памяти для буфера с помощью StrAlloc или аналогичной ей функции. Память, выделенную таким образом, следует обязательно освобождать с помощью StrDispose. При этом крайне желательно использовать конструкцию try/finally, чтобы возникновение исключений не привело к утечкам памяти.
Все три способа получения строковых данных от функций Windows API показаны в примере EnumWnd, находящемся на прилагаемом компакт-диске.
1.2. Примеры использования Windows API
В этом разделе разобраны простые примеры, находящиеся на компакт-диске. Все эти примеры уже упоминались ранее, и каждый из них иллюстрирует какую-то отдельно взятую возможность API. Более сложным обобщающим примерам, которые задействуют сразу несколько возможностей API и VCL, посвящен следующий, третий раздел данной главы.
Программа 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) <> 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 > 0.
if TextLen > 0 then
SendMessage(Wnd, WM_GETTEXT, TextLen + 1, LParam (Text));
// Заголовок окна может быть очень длинным - например, в
// Memo заголовком считается весь текст, который там
// есть. Практика показывает, что существуют проблемы
// при добавлении в TTreeView узлов с очень длинным
// названиями: при попытке открыть такой узел программа,
// запущенная из Delphi, вылетает в отладчик (при
// запуске вне среды Delphi проблем не замечено). Чтобы
// этого не происходило, слишком длинные строки
// обрезаются.
if TextLen > 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 для дочерних окон, поэтому и отсеиваем лишнее.