Node.Data := Pointer(Wnd);
EnumChildWindows(Wnd, @EnumWindowsProc, LParam(Node));
end;
function EnumTopWindowsProc(Wnd: HWnd; PIDNeeded: Cardinal): BOOL; stdcall;
var
Text: string;
TextLen: Integer;
ClassName: array[0..ClassNameLen - 1] of Chars;
Node: TTreeNode;
NodeName: string;
WndPID: Cardinal;
begin
Result := True;
// Здесь отсеиваются окна, которые не принадлежат
// выбранному процессу
GetWindowThreadProcessID(Wnd, @WndPID);
if WndPID = PIDNeeded then
begin
TextLen := GetWindowTextLength(Wnd);
SetLength(Text, TextLen);
if TextLen > 0 then GetWindowText(Wnd, PChar(Text), TextLen + 1);
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 + ')';
NodeName := '$' + IntToHex(Wnd, 8) + ' ' + NodeName;
Node := ProcessesInfoForm.TreeWindows.Items.AddChild(nil, NodeName);
Node.Data := Pointer(Wnd);
EnumChildWindows(Wnd, @EnumWindowsProc, LParam(Node));
end;
end;
procedure TProcessesInfoForm.FillWindowList(PID: Cardinal);
begin
if PID = 0 then Exit;
EnumWindows(@EnumTopWindowsProc, PID);
end;
В отличие от примера EnumWnd из разд. 1.2.1 здесь для функций EnumWindows и EnumChildWindows предусмотрены разные процедуры обратного вызова. Связано это с тем, что окна верхнего уровня необходимо фильтровать по принадлежности к выбранному процессу, и параметр функции обратного вызова служит для передачи PID этого процесса. А их дочерние окна фильтровать по процессам не обязательно (мы предполагаем, что если некоторое окно верхнего уровня принадлежит данному процессу, то и все его дочерние окна также принадлежат этому процессу), зато нужно передавать указатель на элемент дерева, соответствующий родительскому окну, чтобы процедура знала, где размещать новый элемент. Таким образом, смысл параметра меняется, поэтому требуется новая процедура обратного вызова. (В принципе, можно было бы проверять, есть ли у найденного окна родитель, и в зависимости от этого трактовать параметр так или иначе, используя приведение типов. Но сильно усложняет код, поэтому в учебном примере мы не будем использовать такой способ.)
Примечание
Как уже было сказано ранее, мы полагаем, что если некоторое окно верхнего уровня принадлежит данному процессу, то и все его дочерние окна также принадлежат этому процессу. В общем случае это неверно: функция CreateWindow(Ex) позволяет при создании нового окна использовать в качестве родительского окно другого процесса. Поэтому наш код ошибочно отнесет подобные окна к тому процессу, к которому относятся их родители, а не к тому, который их реально создал. Здесь мы пренебрегаем такой возможностью, потому что для ее учета не нужны дополнительные знания API, необходимо просто запрограммировать более сложный алгоритм отсева. В учебном примере, посвященном API, реализация такого алгоритма была бы неоправданным усложнением. но в реальных программах эту возможность следует учесть.
Для получения названия окна в приведенном коде используется функция GetWindowText. Эта функция безопасна при работе с зависшими приложениями, поскольку она гарантирует, что вызвавшее ее приложение не зависнет само, пытаясь получить ответ от зависшего приложения. Но GetWindowText не всегда может получить названия элементов управления, расположенных в окнах чужих приложений (точнее, в MSDN написано, что она вообще не может получать названия элементов управления в чужих окнах, но практика показывает, что нередко GetWindowText все же делает это). Существует альтернативный способ получения названия окна — отправка ему сообщения WM_GETTEXT. При этом ограничений на работу с чужими элементами управления нет, но и гарантии, что из-за отсутствия ответа от чужого приложения программа не зависнет, тоже нет.
Использование WM_GETTEXT показано в другой части программы — при заполнении списка параметров окна, отображающегося в правом нижнем углу формы. Чтобы программа не зависала, вместо SendMessage для отправки WM_GETTEXT применяется SendMessageTimeout. Код получения имени показан в листинге 1.44.
Листинг 1.44. Получение заголовков "чужих" окон
if SendMessageTimeout(Wnd, WM_GETTEXTLENGTH, 0, 0,
SMTO_NORMAL or SMTO_ABORTIFHUNG, 5000, TextLen) = 0 then
begin
LastError := GetLastError;
if LastError = 0 then Text := 'Приложение не отвечает'
else
Text:= 'Ошибка при получении длины заголовка: ' + IntToStr(LastError);
end
else
begin
SetLength(Text, TextLen);
if TextLen > 0 then
if SendMessageTimeout(Wnd, WM_GETTEXT, TextLen + 1, LParam(Text),
SMTO_NORMAL or SMTO_ABORTIFHUNG, 5000, TextLen) = 0 then
begin
LastError := GetLastError;
if LastError = 0 then Text := 'Приложение не отвечает'
else Text := 'Ошибка при получении заголовка:' + IntToStr(LastError);
end;
end;
Для каждого окна программа выводит имя оконного класса и реальный класс окна. В большинстве случаев эти два класса совпадают. Они различаются только для тех окон, чьи классы "унаследованы" от стандартных классов, таких как EDIT, COMBOBOX и т.п.
Вообще, наследования оконных классов в Windows нет. Но существует один нехитрый прием, который позволяет имитировать наследование. Оконная процедура обычно не обрабатывает все сообщения, а передает часть их в одну из стандартных оконных процедур (DefWindowProc, DefFrameProc и т.п.). Программа может с помощью функции GetClassInfo узнать адрес оконной процедуры, назначенной стандартному классу, и использовать ее вместо стандартной оконной процедуры. Так как большая часть свойств окна определяется тем, как и какие сообщения оно обрабатывает, использование оконной процедуры другого класса позволяет почти полностью унаследовать свойства этого класса. (В VCL для наследования оконных классов существует метод TWinControl.CreateSubClass.) Функция RealGetWindowClass позволяет узнать имя класса-предка, если такой имеется. Соответствующая часть кода примера приведена в листинге 1.45.
Листинг 1.45. Получение реального класса окна
GetClassName(Wnd, ClassName, ClassNameLen);
ClassName[ClassNameLen - 1] := #0;
ListParams.Items[2].SubItems[0] := ClassName;
RealGetWindowClass(Wnd, ClassName, ClassNameLen);
ClassName[ClassNameLen - 1] := #0;
ListParams.Items[3].SubItems[0] := ClassName;
У окна, если оно имеет стиль WS_CHILD, должно быть родительское окно. Если такого стиля нет, то окно располагается непосредственно на рабочем столе. Кроме того, такое окно может (но не обязано) иметь владельца. Получить дескриптор родительского окна можно с помощью функции GetParent. Владельца — с помощью функции GetWindow с параметром GW_OWNER.
Примечание
Кроме GetParent существует функция GetAncestor, которая также возвращает дескриптор родительского окна, если она вызвана с параметром GA_PARENT. Разница между этими функциями заключается в том. что для окон верхнего уровня (т.е. расположенных непосредственно на рабочем столе) GetParent возвращает 0, a GetAncestor — дескриптор рабочего стопа (этот дескриптор можно получить через функцию GetDesktopWindow).
Значительную часть кода программы составляет анализ того, какие флаги присутствуют в стиле окна. В этом нет ничего сложного, но он громоздкий из-за большого числа флагов. Следует также учесть, что для стандартных классов одни и те же числовые значения могут иметь разный смысл. Так, например, константы ES_NOHIDESEL и BS_LEFT имеют одинаковые значения. Поэтому при расшифровке стиля следует также учитывать класс окна. Приводить здесь этот код мы не будем по причине его тривиальности. Его можно посмотреть в примере на компакт-диске.
1.3.2. Обобщающий пример 2 — Ассоциированные файлы и предотвращение запуска второй копии приложения
Расширения файлов могут быть связаны (ассоциированы) с определенной программой. Такие ассоциации помогают системе выбрать программу для выполнения различных действий с файлом из Проводника. Так, например, если на компьютере установлен Microsoft Office, двойной щелчок в Проводнике на файле с расширением xls приведет к запуску Microsoft Excel и открытию файла в нем. Это происходит потому, что расширение xls ассоциировано с приложением Microsoft Excel.
Примечание
Добиться аналогичного эффекта в своей программе можно используя функцию ShellExecute (стандартная системная функция, в Delphi импортируется в модуле ShellAPI). Эта функция запускает файл, имя которого передано ей как параметр. Если это исполняемый файл, он запускается непосредственно, если нет — функция ищет ассоциированное с расширением файла приложение и открывает файл в нем.