CNN TürkCAT31! Kedinin gözünden COP31 ZirvesiPunchChina accuses US of using spy claims to justify tech blockadeBollywood HungamaKareena Kapoor Khan faces legal notice over Quranic verse on 30-carat emerald pendant worn for Diwali 2026The Jerusalem PostWhy India and Israel still need an economic deal - opinionInquirerCops seize P16-M shabu in Zamboanga Sibugay raidUOLIrlanda sofre pressão para barrar envio à Rússia de matéria-prima militarSeeking AlphaHigh Liner Foods declares CAD 0.175 dividendFutura SciencesTasmanie : l’île australienne où la Terre raconte 500 millions d’années d’histoireVilaWebLa consellera Parlon titlla de “quatre arreplegats facinerosos” els participants en els aldarulls a BarcelonaOnetŻołnierz nakrzyczał na Pete'a Hegsetha i jego ludzi. Nie zachowali należytej powagiPremium TimesGovernor sacks task force over building demolitionTVN24Rosja ma problem z niedźwiedziami. Coraz więcej ataków
The Daily Newsstand · Free, Always
Thursday, November 12, 2026

Кошка на рамках окон: сколько WinAPI нужно, чтобы десктопный питомец не мешал работать

Translate

Всем привет! Мой первый пост, но не первый проект, хотелось бы собрать фидбек от опытных разрабов или проггеров. Каждый совет делает меня лучше:)

Начну с главного: «навайбкодил», и мне не стыдно (немного если только). Claude писал код, а я сидел и пил чай. На самом деле кучу времени потратил на тесты и несколько дней на составление плана: «что должно быть внутри» и «какой функционал».

Что получилось: пиксельная кошка для Windows 10/11. Живёт на краях экрана и на верхних рамках окон, ест, спит, обижается, смотрит с тобой кино и прячется на созвонах. Главное требование: питомец не должен мешать. Из него и выросла вся техническая часть.

Стек — Tauri 2: Rust для WinAPI, Svelte 5 внутри WebView2. Rust только собирает геометрию и флаги, а поведение кошки — чистые функции на TypeScript, поэтому почти всё покрыто тестами без запуска окна.

Вывод: прозрачный WebView ловит мышь всем прямоугольником

Кошка нарисована в квадрате 32×32 и занимает от силы половину, а клики глотает весь квадрат: у WebView2 прозрачны только пиксели, не hit‑test. SetWindowRgn и layered‑окна не подходят — WebView2 рисует сам. WM_NCHITTEST тоже: мышь получает дочернее окно WebView2, а не наше.

Сделал наоборот. Окно по умолчанию прозрачно для мыши (WS_EX_TRANSPARENT). Отдельный поток раз в 33 мс смотрит, над какой клеткой спрайта курсор, и снимает флаг, только пока курсор над нарисованным пикселем. Форму кадра фронт присылает маской из 32 u32:

let want = !masks.contains(&label, x, y) || alt;   // row >> x & 1
if want != ignoring && window.set_ignore_cursor_events(want).is_ok() {
    ignoring = want;
}

Грабли: окно нельзя сделать уже SM_CXMINTRACK (136 px при 100%), поэтому спрайт центрируется и координаты считаются от квадрата. Пока кнопка зажата, мышь не отпускаем, иначе перетаскивание рвётся. С Alt клик проходит насквозь всегда.

Опрос по таймеру — не самое красивое решение. Если знаете способ лучше для WebView2, жду в комментариях.

Фокус и Esc, который улетал в игру

Все окна неактивируемые (WS_EX_NOACTIVATE), иначе кошка свернёт игру. Но show() в Tauri вызывает ShowWindow(SW_SHOW), а тот активирует окно несмотря ни на что, поэтому меню показывается через SetWindowPos с SWP_NOACTIVATE.

Следом вылез баг: у меню нет фокуса, значит, клавиатура у игры. Нажал Esc, чтобы закрыть меню, — и в игре открылось главное меню. Пришлось ставить WH_KEYBOARD_LL, который съедает Esc, но только пока меню открыто. Важная тонкость: поток хука обязан разбирать сообщения (MsgWaitForMultipleObjects, а не sleep), иначе ввод всей системы ждёт его.

Всё остальное время хука нет, ввод читается через Raw Input в message‑only окно: он не встаёт в цепочку обработки нажатий и не добавляет задержку в играх.

Прогулки по рамкам: GetWindowRect цепляет невидимую рамку

EnumWindows отдаёт окна по z‑order, дальше фильтр, и каждое правило в нём — исправленный баг:

 — GetWindowRect врёт. В Windows 10 у окна невидимые поля для ресайза, около 7 px, и кошка висела в воздухе. Видимую рамку отдаёт DwmGetWindowAttribute(DWMWA_EXTENDED_FRAME_BOUNDS).
 — Невидимые видимые окна. Окна других рабочих столов и призраки UWP проходят IsWindowVisible. Отсекает DWMWA_CLOAKED.
 — Оверлеи. NVIDIA и Steam держат прозрачные окна на весь экран поверх всего и «закрывают» все кромки. Признак — WS_EX_TRANSPARENT.
 — Перекрытия. От кромки окна вычитаются куски, закрытые окнами выше. Это чистая функция на отрезках с тестами.
 — Развёрнутое окно. Ходить можно по заголовку левее кнопок, их даёт DWMWA_CAPTION_BUTTON_BOUNDS, но относительно GetWindowRect, вместе с невидимыми полями.
 — Полноэкранное видео. Windows не убирает панель задач из rcWork, и пол висел поверх фильма. Ещё Chrome на весь экран оставляет 1439 px из 1440, так что «на весь экран» считается с допуском.

Заголовки окон не читаются нигде: только рамка, стили, имя класса и exe.

Созвон без списка программ

Кошка на созвоне прячется. Список Zoom/Teams/Discord вечно отстаёт, поэтому я взял источник, откуда Windows берёт значок микрофона в трее:

HKCU\Software\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\microphone

У каждой программы там LastUsedTimeStart и LastUsedTimeStop. Пока устройство занято, Stop равен нулю. Meet в браузере, Telegram, что угодно — видно без списка.

На живой машине вылезли два исключения. Программа упала, не отпустив микрофон, — Stop остался нулём навсегда, поэтому занятость старше 12 часов не считается. А NVIDIA Broadcast, Voicemeeter и OBS держат микрофон всегда, их пришлось внести в список из четырёх строк. Наружу модуль отдаёт только да/нет, звук не пишется.

Месть, которую не видно на записи

Обидишь кошку — уйдёт за край, а вернувшись, оставит лужу на окне программы, где ты сегодня провёл больше всего времени. Лужа на чужом окне в скриншоте или на демонстрации экрана выглядит как настоящая порча, поэтому её окно всегда исключено из захвата:

 SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE)

На мониторе видно, в OBS и Zoom — нет. Побочный эффект: для гифки лужу пришлось вклеивать при монтаже.

Мою кошку зовут Нищета, а вашу?

Мою кошку зовут Нищета, а вашу?

Цифры и неприятное

— установщик ~2,3 МБ, exe ~6 МБ;
 — память — Tauri не волшебный: около 400 МБ частной памяти через 20 минут работы, exe плюс 14 процессов msedgewebview2. Каждое окно (кошка, миска, лежак, облачко, меню…) — свой WebView. Диспетчер задач показывает 8 МБ, но врёт. Ужать — следующая задача, советы приветствуются;
 — процессор: 1–3% от восьми ядер;
 — 762 теста vitest и 125 cargo;
 — в сеть уходят только дата и язык — для новостей по выбранным темам.

Windows при установке скажет «Windows защитила ваш компьютер»: сертификат подписи стоит денег. Лицензия PolyForm Noncommercial — код открыт, продавать нельзя, так что это не open source в строгом смысле. Только Windows: под Linux почти ничего из этого не переносится.

Если поставите — напишите, что мешает. Именно «мешает» мне интереснее всего.

 — исходники: Тык (гитхаб)
 — сайт и установщик: Тык (тоже гитхаб)
 — итч.ио: Тык

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.