AI Windows — дескрипторы окон
AI-first карта дескрипторов окон, селекторов, матчеров и провайдеров.
Карта API по HWND, сопоставлению и отслеживанию окон
Справочная карта по нативным дескрипторам окон, преобразованию сырого HWND, перечислению окон, сопоставлению, отслеживанию foreground-окна и изменений границ окна.
Интерактивный выбор окна описан в osd/selection.md.
Что обычно нужно сделать
- Преобразовать сырой
HWND/IntPtrвIWindowHandle. - Прочитать для окна process id, title, bounds, owner, parent и visibility.
- Перечислить известные окна.
- Сопоставлять окна по title, process, path, handle или выражению.
- Поддерживать живой выбор целевого окна.
- Отслеживать foreground window или изменения bounds.
Модель сущностей
IWindowHandle— это обёртка PoeShared над нативным окном.IWindowHandleProviderпреобразует сырые handle в обёртки.IWindowListProviderотвечает за живое перечисление, ignored windows, blacklists и определение foreground window.IWindowMatcherвыполняет stateless-сопоставление по выражению.IWindowSelectorхранит stateful active target на основе match expression.IWindowHandleMatcherиIComplexWindowHandleMatcher— это инфраструктурные include/exclude predicates.- Трекеры foreground и bounds работают поверх нативных event hooks.
Найти и активировать окно
Откройте Блокнот и выполните в C# Action:
IFactory<IWindowSelector> WindowSelectorFactory { get; init; }
{
using var selector = WindowSelectorFactory.Create();
var window = selector.FindWindow("notepad.exe");
if (window == null)
{
Log.Info("Подходящее окно пока не найдено");
return;
}
window.Activate(TimeSpan.FromMilliseconds(500));
Log.Info($"Выбрано окно: {window.Title}");
}
Отдельным selector владеет этот запуск, using освобождает его подписки. Дескриптор и внедрённая фабрика остаются за инфраструктурой host. В обычный класс передайте фабрику через конструктор. Пример требует работающего desktop-host, его нативных регистраций и UI Core; SDK предоставляет типы, а не живой список окон.
FindWindow назначает выражение и сразу читает ActiveWindow; он не ждёт появления окна. GetWindow делает то же, но при null бросает InvalidOperationException. ActiveWindow — выбранная цель selector, не foreground ОС. Живой список обновляется с задержкой; текущая цель сохраняется, пока ещё соответствует выражению.
Selector по умолчанию не требует видимость (OnlyVisible = false), а при назначении цели выбирает тип top-level и внешние окна, если выражение не меняет эти параметры. Простой текст ищется в path/title/args; process name сравнивается целиком. Поэтому notepad.exe не равно ограничителю AHK ahk_exe. #2 означает второе совпадение после сортировки, несмотря на комментарий о zero-based index в XML.
Активация синхронна, может восстановить свёрнутое окно и не принимает токен отмены. Успех зависит от foreground-политики Windows; не переносите её на UI-поток только потому, что это операция с окном.
Дождаться окна конкретного процесса
Чтобы искать только процесс, задайте WindowFilter, а затем читайте живую
цель selector. В этом примере верхнеуровневого C# Script откройте Блокнот
после запуска скрипта:
using System;
using System.IO;
using System.Threading.Tasks;
using EyeAuras.Shared.Services;
using PoeShared.Native;
using PoeShared.Prism;
{
var runToken = cancellationToken;
var factory = GetService<IFactory<IWindowSelector>>();
using var selector = factory.Create();
selector.OnlyVisible = true;
selector.WindowFilter = candidate => string.Equals(
Path.GetFileNameWithoutExtension(candidate.ProcessName),
"notepad", StringComparison.OrdinalIgnoreCase);
selector.TargetWindow = "#1";
IWindowHandle window;
while (true)
{
runToken.ThrowIfCancellationRequested();
window = selector.ActiveWindow;
if (window != null && window.IsWindow)
{
break;
}
await Task.Delay(250, runToken);
}
Log.Info($"Найден Блокнот: {window.Title}");
}
#1 выбирает первое совпадение после фильтрации; наличие нескольких окон
не гарантирует выбор нужного документа. ProcessName в этом срезе берётся
из имени исполняемого файла, а при недоступности пути — из
Process.ProcessName. Поэтому здесь убрано расширение и сравнивается всё
имя без учёта регистра. Совпадение текста в чужом заголовке больше не подходит.
Цикл ждёт появления цели; Stop прерывает задержку и освобождает selector.
IsWindow проверяет HWND в момент чтения, но окно всё равно может исчезнуть
перед следующим вызовом. Для повторяющейся работы заново читайте
selector.ActiveWindow на каждой итерации: сохранённая переменная window
сама не переключается на новое окно. Дескриптор заимствован у инфраструктуры,
его не добавляют в собственный using.
Получить независимый список окон процесса
Чтобы сохранить выбор другого selector, читайте снимок IWindowListProvider,
не меняя TargetWindow. В C# Script с работающим desktop-host:
using System;
using System.IO;
using System.Linq;
using EyeAuras.Shared.Services;
var provider = GetService<IWindowListProvider>();
provider.RefreshWindowList();
var snapshot = provider.WindowList.Items.ToArray();
var matches = snapshot.Where(w => w.IsWindow && string.Equals(
Path.GetFileNameWithoutExtension(w.ProcessName),
"notepad", StringComparison.OrdinalIgnoreCase)).ToArray();
foreach (var w in matches)
Log.Info($"{w.Title}, PID: {w.ProcessId}");
Это снимок списка, а не гарантия существования каждого окна во время
последующей операции. Провайдер и handles заимствованы; не освобождайте их.
Для строкового выражения, которое может совпадать с разными полями,
используйте GetService<IWindowMatcher>().FindMatches(snapshot, expression).
Передавайте материализованный массив: контракт метода принимает
IReadOnlyList<IWindowHandle>, а не произвольный ленивый IEnumerable.
Проверить процесс foreground-окна
Это отдельный запрос, он не читает и не меняет цель существующего selector:
using System;
using System.IO;
using EyeAuras.Shared.Services;
var foreground = GetService<IWindowListProvider>().GetForegroundWindow();
var isNotepad = foreground != null && foreground.Handle != IntPtr.Zero &&
foreground.IsWindow && string.Equals(
Path.GetFileNameWithoutExtension(foreground.ProcessName),
"notepad", StringComparison.OrdinalIgnoreCase);
Log.Info($"Блокнот на переднем плане: {isNotepad}");
Провайдер может вернуть обёртку нулевого HWND, поэтому одной проверки
managed-ссылки на null недостаточно. Состояние foreground меняется сразу
после чтения; эта проверка не блокирует переключение окна пользователем.
Перевести точку относительно рамки окна
У найденного IWindowHandle методы ToScreen(Point) и FromScreen(Point) прибавляют или вычитают начало DwmFrameBounds; невалидные границы дают исключение. Это координаты относительно рамки, а не обещание Win32 client area. Сначала выберите систему координат входа и backend ввода; свойства матриц OSD сами по себе не доказывают нужное преобразование.
Для наблюдения границ IWindowBoundsTrackerFactory.Track(window) возвращает IObservable<Rectangle?>. Освобождайте свою подписку в конце операции; тип допускает null. В реализации этого среза связь освобождения нативного hook с подпиской явно не установлена, поэтому это не обещание полного завершения всех нативных ресурсов. Не предполагайте UI-поток доставки без проверки host.
Наблюдать границы выбранного окна
Полный пример для C# Action: откройте Блокнот, запустите скрипт и перемещайте окно в течение десяти секунд. Выбор здесь использует обычное широкое выражение; для ограничения процессом примените фильтр из предыдущей операции.
using System;
using System.Reactive.Linq;
using System.Threading.Tasks;
using EyeAuras.Shared.Services;
using EyeAuras.Shared.Scaffolding;
using PoeShared.Native;
using PoeShared.Prism;
{
using var selector = GetService<IFactory<IWindowSelector>>().Create();
var window = selector.FindWindow("notepad.exe");
if (window == null || !window.IsWindow)
{
Log.Info("Окно пока не найдено; запустите пример ещё раз");
return;
}
var tracker = GetService<IWindowBoundsTrackerFactory>();
using var subscription = tracker.Track(window).Subscribe(
bounds => Log.Info(bounds.HasValue
? $"Границы: {bounds.Value}"
: "Границы недоступны"),
error => Log.Error("Отслеживание границ завершилось ошибкой", error));
await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken);
}
Первое значение приходит при подписке, следующие — при изменении границ.
Через десять секунд или при Stop подписка освобождается. Это наблюдение за
одним HWND, оно не переподключается к новой цели selector. Допустимый null
не следует считать гарантированным событием закрытия окна; поток может
завершиться ошибкой. Фабрика — общий сервис, поэтому сам tracker здесь
не освобождается.
Детали API
-
PoeShared.Native.IWindowHandle- Используйте
Handleтолько там, где действительно нужен сыройIntPtr. - Вместо повторных Win32-запросов предпочитайте
ProcessId,ProcessName,Title,Boundsи метаданные owner/parent/state.
- Используйте
-
PoeShared.Native.IWindowHandleProviderGetByWindowHandle(IntPtr hwnd)преобразует сырой handle.GetMonitorByWindowHandle(IntPtr hwnd)определяет monitor context.- Не занимается выбором или перечислением окон.
-
IWindowListProviderAllWindowList,WindowList,IgnoredWindows,BlacklistedWindows.ResolveByHandle(IntPtr),GetForegroundWindow(),RefreshWindowList().
-
IWindowMatcherIsMatch(...)иFindMatches(...)дляWindowMatchExpression.
-
IWindowSelectorTargetWindow,ActiveWindow,MatchingWindowList,OnlyVisible,WindowFilter.- Используйте, когда коду нужно постоянное выбранное целевое окно, которое следует за match expression.
-
WindowSelectorExtensionsFindWindow(...)устанавливаетTargetWindowи возвращает выбранноеActiveWindow, либоnull, если совпадений нет.GetWindow(...)устанавливаетTargetWindowи возвращает выбранноеActiveWindow; используйте его, когда отсутствие окна считается исключительной ситуацией.- Используйте
TargetWindowвместе сActiveWindow, если отсутствие окна ожидаемо и его нужно обрабатывать без исключений, при этом коду важно сохранить состояние selector. - Используйте
IWindowMatcher.FindMatches(...), если коду намеренно нужны несколько совпадений или сопоставление по переданному списку.
-
WindowHandleExtensionsActivate(...)выводитIWindowHandleна передний план.Activate(...)делегирует вызов вUnsafeNative.ActivateWindow(...).ToWindowMatchExpressionOrDefault(...)создаёт переиспользуемое match expression из известного handle окна.
-
UnsafeNative.ActivateWindow(...)- Низкоуровневый helper для активации с overload-ами для
IntPtr,IWindowHandle, timeout и log. - Перед активацией восстанавливает свёрнутые окна и ждёт переключения foreground window.
- При неудаче бросает
InvalidStateException. Нулевой foreground может продлить ожидание до 10 секунд; timeout не является строгим пределом. Для null/нулевого HWND вызов ничего не делает. - В примерах для скриптов и документации предпочтительнее
WindowHandleExtensions.Activate(...).
- Низкоуровневый helper для активации с overload-ами для
-
IForegroundWindowTracker,IWindowTracker- Отслеживание foreground/active window.
-
IWindowBoundsTrackerFactory- Отслеживает изменения прямоугольника окна.
Типовые сценарии
-
Сырой handle в обёртку:
- получите
IntPtr hwnd; - вызовите
IWindowHandleProvider.GetByWindowHandle(hwnd); - передайте дальше
IWindowHandle.
- получите
-
Постоянный выбор целевого окна:
- получите
IWindowSelector; - установите
TargetWindow; - читайте
ActiveWindowиMatchingWindowList.
- получите
-
Поиск одного целевого окна:
- получите
IWindowSelector; - вызывайте
FindWindow(...), если отсутствие окна ожидаемо и должно обрабатываться без исключения; - вызывайте
GetWindow(...)только если отсутствие окна — исключительная ситуация; - используйте
TargetWindowвместе сActiveWindow, если коду нужно постоянное состояние selector; - сохраняйте возвращённый
IWindowHandle, а не запрашивайте окно повторно по сырому handle или строке.
- получите
-
Несколько совпадений или stateless-запрос:
- получите
IWindowListProviderиIWindowMatcher; - прочитайте
WindowsProvider.WindowList.Items; - вызовите
IWindowMatcher.FindMatches(..., targetExpression).
- получите
-
Одноразовая проверка "окно активно":
- получите
IWindowListProviderиIWindowMatcher; - вызовите
IWindowListProvider.GetForegroundWindow(); - вызовите
IWindowMatcher.IsMatch(foregroundWindow, targetExpression).
- получите
-
Активация найденного окна:
- получите
IWindowSelector; - вызывайте
FindWindow(...), если сценарий с отсутствующим окном должен обрабатываться явно; - вызывайте
GetWindow(...)только если отсутствие окна — исключительная ситуация; - вызовите
Activate(...)у handle.
- получите
-
Базовые правила
WindowMatchExpression:- обычный текст сопоставляется с process path, process name или title;
- сравнение текста, включая полное совпадение в кавычках, использует
OrdinalIgnoreCase; regex используетRegexOptions.IgnoreCase. Проверяются также аргументы запуска; process name сравнивается целиком, остальные поля могут давать частичное совпадение; - текст в кавычках — строгое совпадение;
/pattern/— это regex;0x...или&H...сопоставляется с нативным handle;#2выбирает второе подходящее окно;- в
FindMatches&&последовательно сужает результат, а||выбирает первую непустую альтернативу; это не объединение всех результатов.IsMatchпринимает простое выражение; - selector обрабатывает
[type=toplevel],[type=child]и[ownedBy=...]; свойства[visible]и[focus]не становятся фильтрами только оттого, что парсер их прочитал. Видимость задаётсяOnlyVisible.
-
Отслеживание bounds:
- получите
IWindowBoundsTrackerFactory; - вызовите
Track(window); - освободите subscription вместе с lifetime владельца.
- получите
Что предпочтительно использовать
- Предпочитайте
IWindowHandleпосле выбора/разрешения окна. - Используйте
IWindowHandleProviderтолько для преобразования сырого handle. - Для живого сопоставления целевого окна предпочитайте
IWindowSelector. - Для поиска одного окна без исключений предпочитайте
IWindowSelector.FindWindow(...). IWindowSelector.GetWindow(...)используйте только когда отсутствие окна — исключительная ситуация.- Для сопоставления нескольких окон, работы с переданными списками или проверок active window без изменения состояния selector предпочитайте
IWindowMatcherвместе сIWindowListProvider. - Для простых операций с окнами предпочитайте прямые API, работающие с window handle.
- Вместо сырых native event hooks предпочитайте trackers.
Чего лучше избегать
- Не храните сырой
IntPtrкак долгоживущий контракт. - Не используйте
IWindowHandleProviderкак selector или enumerator. - Не используйте aura Actions, Triggers или Overlays как лёгкие обёртки для простых script/API-операций. Это stateful-объекты runtime Aura Tree со своей конфигурацией и lifecycle.
- Не копируйте AutoHotkey
WinTitletokens вродеahk_exeилиahk_classнапрямую вWindowMatchExpression; переносите их осознанно. - Не считайте активацию гарантированной: foreground activation может завершиться по timeout или быть заблокирована Windows.
- Не пишите прямой низкоуровневый WinEvent code, если уже есть trackers.
Опорные сущности для поиска
IWindowHandle,IWindowHandleProvider,IWindowListProvider,IWindowMatcher,IWindowSelector,WindowMatchExpression,WindowSelectorExtensions,WindowHandleExtensions,UnsafeNative.ActivateWindow,IWindowHandleMatcher,IComplexWindowHandleMatcher,IForegroundWindowTracker,IWindowBoundsTrackerFactory,IWinEventHookWrapper.
Поисковые синонимы
- HWND
- IntPtr window handle
- window handle wrapper
- window selector
- window matcher
- foreground window
- active window
- target window
- window exists
- WinExist
- WinActive
- WinActivate
- window bounds tracker
- WinEvent hook
Связанные карты
osd/selection.mdosd/screen-overlay.mdwindows-subsystems/input-hooks-hotkeys.mdmemory/processes.md