Modern technology gives us many things.

Bad Apple на значках рабочего стола — работаем с WinAPI

46

Уровень сложности Средний Время на прочтение 11 мин Количество просмотров 4.2K .NET *C++ *Разработка под Windows * Туториал

Если что-то существует, на этом можно запустить Bad Apple
Правило 86

За последние лет 15, Bad Apple запустили множестве вещей — на самодельном RISC-V процессоре, на осциллографе, на яблоках. Попробуем запустить Bad Apple на значках рабочего стола с помощью вызовов API Windows и нескольких других.

Требования

Visual Studio 2022

Нужная нагрузка для Visual Studio:

Bad Apple на значках рабочего стола — работаем с WinAPI

Требуемые пакеты VS

.NET Core

Подойдет SDK версии 7+:

winget install Microsoft.DotNet.SDK.7

ffmpegwinget install —id=Gyan.FFmpeg -e

Опционально: yt-dlp

Нужно только для скачивания видео с YouTube, можно и по другому

winget install yt-dlp

  • Windows 10 с пустым рабочим столом

На чем будем отображать

Цвета значков

Рендерить видео будем в 2 цветах — черных и белых. Есть два варианта, как поменять значок файла:

  • Если файл — изображение, то поменять изображение, оно будет отображаться в миниатюрном варианте

  • Изменить расширение файла так, чтобы Windows выбрала нужный нам значок.

Первый вариант требует каждый раз переписывать содержимое файла, это очень дорогая операция. Воспользуемся вторым вариантом.

Придумаем 2 расширения файлов, для каждых из которых Windows будет отображать свой значок. Например: .baclrw — белый цвет, и .baclrb — черный. Теперь необходимо зарегистрировать в реестре иконки для этих расширений.

Создадим 2 иконки в формате .ico в папкеC:badappleresources с черным и белым сплошным заполнением. Создадим и запустим файл badapple.reg с таким содержимым:

Windows Registry Editor Version 5.00 [HKEY_CLASSES_ROOT.baclrb] [HKEY_CLASSES_ROOT.baclrbDefaultIcon] @=»C:\badappleresources\badapple_black.ico» [HKEY_CLASSES_ROOT.baclrw] [HKEY_CLASSES_ROOT.baclrwDefaultIcon] @=»C:\badappleresources\badapple_white.ico»

В результате, Windows начнет правильно отображать файлы с нашими расширениями:

Bad Apple на значках рабочего стола — работаем с WinAPI

Значки для придуманных нами расширений файлов

Размеры значков

Все значки рабочего стола отображаются с внешним отступом. Необходимо подобрать такой размер значка, при котором заполнение значка было бы максимальным.

Рабочий стол в Windows NT — это практически такое-же окно проводника как и любая папка. Значит, с ним можно работать как и с любой другой папкой — извлекать информацию о расположении значков, их параметрах и т.д.

Для того, чтобы с помощью Win32 изменить размер значков рабочего стола нужно выполнить несколько COM вызовов:

  • Получить IFolderView2 — интерфейс папки, которая отображается в окне рабочего стола.

  • Вызвать метод IFolderView2::GetViewModeAndIconSize , чтобы получить текущий режим отображения и размер значка

  • Вызвать метод IFolderView2::SetViewModeAndIconSize для задания нового размера значка

CoInitialize(NULL); // Запускаем COM CComPtr<IFolderView2> spView; FindDesktopFolderView(IID_PPV_ARGS(&spView)); //Получим IFolderView2 окна рабочего стола const auto desiredSize = 67; FOLDERVIEWMODE viewMode; int iconSize; spView->GetViewModeAndIconSize(&viewMode, &iconSize); spView->SetViewModeAndIconSize(viewMode, desiredSize);Код функции FindDesktopFolderViewvoid FindDesktopFolderView(REFIID riid, void** ppv) { CComPtr<IShellWindows> spShellWindows; // Создаем экземпляр IShellWindows spShellWindows.CoCreateInstance(CLSID_ShellWindows); CComVariant vtLoc(CSIDL_DESKTOP); CComVariant vtEmpty; long lhwnd; CComPtr<IDispatch> spdisp; // Находим окно по его идентификатору SW (SW_DESKTOP в случае рабочего стола) spShellWindows->FindWindowSW( &vtLoc, &vtEmpty, SWC_DESKTOP, &lhwnd, SWFO_NEEDDISPATCH, &spdisp); CComPtr<IShellBrowser> spBrowser; CComQIPtr<IServiceProvider>(spdisp)-> QueryService(SID_STopLevelBrowser, IID_PPV_ARGS(&spBrowser)); CComPtr<IShellView> spView; // Находим активный IShellView в выбранном окне spBrowser->QueryActiveShellView(&spView); // Находим выбранный объект (в нашем случае IFolderView2) в IShellView spView->QueryInterface(riid, ppv); }

Читать на TechLife:  Kontron Electronic IP Lite: что внутри у промышленного переносного компьютера из 90-х

Регулируя значение desiredSize ищем такой размер значка, при котором отступы по краям иконки будут минимальными. Такое значение — 67.

Bad Apple на значках рабочего стола — работаем с WinAPI

Разные отступы при разных размерах значка (67 — слева)

Данный код будет изменять размер значков на 67 каждый раз при запуске программы.

Разрешение «виртуального экрана»

Теперь необходимо узнать, сколько значков помещается на рабочий стол по горизонтали и вертикали. Можно посчитать вручную (неинтересный вариант), можно посчитать автоматически после корректировки размера иконок (вариант поинтереснее):

RECT desktop; // Получаем HANDLE окна рабочего стола HWND hDesktop = GetDesktopWindow(); // Получаем прямоугольник окна рабочего стола GetWindowRect(hDesktop, &desktop); POINT spacing; //Получаем ширину значка вместе с отступами spView->GetSpacing(&spacing); auto xCount = desktop.right / spacing.x; auto yCount = desktop.bottom / spacing.y;

IFolderView2::GetSpacing возвращает размеры иконки с учетом отступов (что нам и нужно). Делим сторону экрана на сторону значка и получаем количество значков на сторону, на моем мониторе 2560×1440 пикселей это 34×11 значков. Запоминаем эти числа, они пригодятся позже.

Что будем отображать

Подготовка видео

Подготовим нужные файлы. Скачаем оригинальное видео:

yt-dlp «https://www.youtube.com/watch?v=FtutLA63Cp8» -o «badapple.mp4»

Необходимо понизить разрешение видео до 34×11, и частоту до 10 кадров в секунду:

ffmpeg -i badapple.mp4 -s 34×12 -c:a copy -filter:v fps=10 downscaled-33×12.mp4

Обратите внимание на указанное разрешение — 34 на 12. К сожалению, ffmpeg требует четного количества пикселей по вертикали, придется игнорировать последнюю строку.

Преобразование видео в пиксели

Можно считать покадрово все видео и сохранить значение пикселей в бинарный файл. Однако, в этом моменте можно сделать небольшую по коду но существенную по производительности оптимизацию.

Сделаем так, чтобы приложение рендерило только измененные пиксели. Для этого, предыдущий кадр будем хранить в буфере и записывать в бинарный файл данные только изменившихся пикселей. Для этого напишем приложение на C#, для чтения видео будем использовать пакет GleamTech.VideoUltimate.

Код конвертера видео в бинарный форматusing System.Runtime.CompilerServices; using GleamTech.Drawing; using GleamTech.VideoUltimate; // Ширина входного видео const int WIDTH = 34; // Высота входного видео (с учетом игнорируемой последней строки) const int HEIGHT = 11; // Значение байта для БЕЛОГО пикселя const byte BYTE_ONE = 255; // Значение байта для ЧЕРНОГО пикселя const byte BYTE_ZERO = 0; // Значение байта для команды «ПОКРАСИТЬ ПИКСЕЛЬ» const byte BYTE_FRAME_PIXEL = 0; // Значение байта для команды «Сделать скриншот» const byte BYTE_FRAME_SCREENSHOT = 1; // Ссылка на выходной файл var outputPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), «framedata.bapl»); // Поток выходного файла using var outputStream = File.Open(outputPath, FileMode.Create, FileAccess.ReadWrite); // Входной файл using var videoReader = new VideoFrameReader(File.OpenRead(«downscaled-33×12.mp4»)); // Буфер предыдущего кадра var buffer = new Span<bool>(new bool[WIDTH * HEIGHT]); //Считываем кадры, пока они доступны for (var i = 0; videoReader.Read(); i++) { Console.WriteLine(i); //Получаем кадр и преобразуем его к Bitmap из .NET using var frame = videoReader.GetFrame(); using var bitmap = frame.ToSystemDrawingBitmap(); for (byte x = 0; x < WIDTH; x++) { for (byte y = 0; y < HEIGHT; y++) { //Индекс пикселя в буфере var bufferValue = WIDTH * y + x; //Получим значение пикселя (канал значения не имеет, видео черно-белое) var color = bitmap.GetPixel(x, y).R > 128; if (buffer[bufferValue] != color) { // Записываем байт команды изменения пикселя outputStream.WriteByte(BYTE_FRAME_PIXEL); // Записываем данные измененного пикселя outputStream.WriteByte(x); outputStream.WriteByte(y); outputStream.WriteByte(color ? BYTE_ONE : BYTE_ZERO); buffer[bufferValue] = color; } } } //Записываем байт команды скриншота outputStream.WriteByte(BYTE_FRAME_SCREENSHOT); }

Читать на TechLife:  Samsung уже тестирует One UI 6.1 для Galaxy S21, Galaxy S21 FE, Galaxy S22, Galaxy A54, Galaxy A34 и ряда других моделей

Конвертер покадрово просматривает видео и сравнивает пиксели с буфером предыдущего кадра. Если значение цвета отличается, то в выходной файл записывается команда отрисовки пикселя:

  • Байт BYTE_FRAME_PIXEL(0)

  • Координата X пикселя

  • Координата Y пикселя

  • ЗначениеBYTE_ONE или BYTE_ZERO — белый или черный цвет пикселя.

Перед завершением обработки кадра, в выходной файл записывается байт BYTE_FRAME_SCREENSHOT(1) — отрисовщик будет делать скриншот рабочего стола, когда встретит этот байт.

Еще одно небольшое улучшение (на самом деле нет)

Можно сэкономить примерно 25% бинарного файла, если не передавать четвертый байт команды отрисовки «виртуального пикселя» — отрисовщик может хранить последнее состояние каждой иконки и менять его на противоположное. Однако, это приведет к потере FPS при отрисовке видео — будут тратиться дополнительные ресурсы на вычисление нового состояния иконки

В результате работы конвертера на рабочем столе появляется файл framedata.bapl, который содержит порядок отрисовки пикселей и выполнения снимка экрана рабочего стола.

Как будем отображать

Заполним рабочий стол файлами с иконками черного цвета. Для этого, получим путь к рабочему столу текущего пользователя с помощью SHGetSpecialFolderPathA:

// desktopPath будет указывать на строку wstring — путь к рабочему столу текущего пользователя char desktop_path[MAX_PATH + 1]; SHGetSpecialFolderPathA(HWND_DESKTOP, desktop_path, CSIDL_DESKTOP, FALSE); const auto totalScreenCapacity = desktopResolution.x * desktopResolution.y; auto desktopPath = std::string(desktop_path); auto desktopPathW = std::wstring(desktopPath.begin(), desktopPath.end());

Далее, заполним 2 вектора — полные пути к файлам белых цветов и черных цветов.

for (auto y = 0; y < desktopResolution.y; y++) { for (auto x = 0; x < desktopResolution.x; x++) { blacks[y * desktopResolution.x + x] = desktopPathW + line_separator + std::to_wstring(x) + L»_» + std::to_wstring(y) + black_extension; whites[y * desktopResolution.x + x] = desktopPathW + line_separator + std::to_wstring(x) + L»_» + std::to_wstring(y) + white_extension; } }

По итогу, в blacks и whites будут лежать строки вида C:Users[User]Desktop[x]_[y].baclr[‘w’|’b’] .

Иконку файла будем менять переименованием.

Переименование файла

Для переименования файла в Win32 API предусмотрена функция MoveFile. Мы могли бы изменить иконку с индексом i с белого на черный цвет так:

MoveFile(whites[i], blacks[i]);

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

Мы можем использовать немного другой подход — создать файл при помощи функции CreateFile, сохранить полученный дескриптор в вектор и использовать его для будущего переименования без закрытия файла.

for (int i = 0; i < totalScreenCapacity; i++) { // Создаем файлы с черной иконкой с доступом на чтение, запись и удаление handles[i] = CreateFile(blacks[i].c_str(), GENERIC_READ | GENERIC_WRITE | DELETE, 0, NULL, CREATE_ALWAYS, 0, NULL); }

Осталось непосредственно переименовать файл, зная его HANDLE, для этого есть SetFileInformationByHandle. Напишем функцию:

void RenameFileByHandle(HANDLE handle, std::wstring newName) { auto newNameStr = newName.c_str(); // Создадим структуру с информацией о длине файла union { FILE_RENAME_INFO file_rename_info; BYTE buffer[offsetof(FILE_RENAME_INFO, FileName[MAX_PATH])]; }; file_rename_info.ReplaceIfExists = TRUE; file_rename_info.RootDirectory = nullptr; //Заполним информацию о длине названия файла file_rename_info.FileNameLength = (ULONG)wcslen(newNameStr) * sizeof(WCHAR); // Запишем нули в название файла (для нормальной работы SetFileInformationByHandle название файла должно кончаться на ) memset(file_rename_info.FileName, 0, MAX_PATH); // Скопируем нужное название файла в память memcpy_s(file_rename_info.FileName, MAX_PATH * sizeof(WCHAR), newNameStr, file_rename_info.FileNameLength); // Переименуем файл SetFileInformationByHandle(handle, FileRenameInfo, &buffer, sizeof buffer); }

Делаем скриншоты экрана

Будем использовать GDI+, напишем функцию SaveScreenshotToFile.

Код SaveScreenshotToFilevoid SaveScreenshotToFile(const std::wstring& filename) { // Получим контекст устройства экрана HDC hScreenDC = CreateDC(L»DISPLAY», NULL, NULL, NULL); // Получим размер экрана int ScreenWidth = GetDeviceCaps(hScreenDC, HORZRES); int ScreenHeight = GetDeviceCaps(hScreenDC, VERTRES); // Создадим изображение HDC hMemoryDC = CreateCompatibleDC(hScreenDC); HBITMAP hBitmap = CreateCompatibleBitmap(hScreenDC, ScreenWidth, ScreenHeight); HBITMAP hOldBitmap = (HBITMAP)SelectObject(hMemoryDC, hBitmap); // Скопируем скриншот из контекста экрана в контекст памяти (изображения) BitBlt(hMemoryDC, 0, 0, ScreenWidth, ScreenHeight, hScreenDC, 0, 0, SRCCOPY); hBitmap = (HBITMAP)SelectObject(hMemoryDC, hOldBitmap); // Сохраним изображение в файл BITMAPFILEHEADER bmfHeader; BITMAPINFOHEADER bi; bi.biSize = sizeof(BITMAPINFOHEADER); bi.biWidth = ScreenWidth; bi.biHeight = ScreenHeight; bi.biPlanes = 1; bi.biBitCount = 32; bi.biCompression = BI_RGB; bi.biSizeImage = 0; bi.biXPelsPerMeter = 0; bi.biYPelsPerMeter = 0; bi.biClrUsed = 0; bi.biClrImportant = 0; DWORD dwBmpSize = ((ScreenWidth * bi.biBitCount + 31) / 32) * 4 * ScreenHeight; HANDLE hDIB = GlobalAlloc(GHND, dwBmpSize); char* lpbitmap = (char*)GlobalLock(hDIB); // Скопируем биты изображения в буффер GetDIBits(hMemoryDC, hBitmap, 0, (UINT)ScreenHeight, lpbitmap, (BITMAPINFO*)&bi, DIB_RGB_COLORS); // Создадим файл с будущим скриншотом HANDLE hFile = CreateFile(filename.c_str(), GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); // Размер в байтах заголовка изображения DWORD dwSizeofDIB = dwBmpSize + sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER); // Сдвиг данных пикселей bmfHeader.bfOffBits = (DWORD)sizeof(BITMAPFILEHEADER) + (DWORD)sizeof(BITMAPINFOHEADER); //Размер файла bmfHeader.bfSize = dwSizeofDIB; //0x4d42 = ‘BM’ в кодировке ASCII, обязательное значение bmfHeader.bfType = 0x4D42; //BM DWORD dwBytesWritten = 0; WriteFile(hFile, (LPSTR)&bmfHeader, sizeof(BITMAPFILEHEADER), &dwBytesWritten, NULL); WriteFile(hFile, (LPSTR)&bi, sizeof(BITMAPINFOHEADER), &dwBytesWritten, NULL); WriteFile(hFile, (LPSTR)lpbitmap, dwBmpSize, &dwBytesWritten, NULL); //Очищаем данные контекстов GlobalUnlock(hDIB); GlobalFree(hDIB); //Закрываем файлы CloseHandle(hFile); //Очищаем мусор после себя DeleteObject(hBitmap); DeleteDC(hMemoryDC); DeleteDC(hScreenDC); }

Читать на TechLife:  Китайские Volkswagen начали официально продавать в России

Перед снимком экрана необходимо обновить содержимое окна и дождаться пока обновление произойдет. Для этого воспользуемся SendMessage и напишем небольшую функцию:

void TakeScreenshot(int index){ // Путь i-того скриншота auto path = screenshot_path + L»shot_» + std::to_wstring(index) + L».png»; // Отправляем сообщение для обновления SendMessage(HWND_BROADCAST, WM_SETTINGCHANGE, 0, 0); // Ждем DEFAULT_SLEEP_TIME миллисекунд std::this_thread::sleep_for(DEFAULT_SLEEP_TIME); // Делаем скриншот SaveScreenshotToFile(path); }

Команда SendMessage предназначена для отправки сообщения в выбранное окно (0 аргумент), мы используем HWND_BROADCAST, поэтому адресатами будут все окна. В нашем случае сообщением является WM_SETTINGCHANGE — изменение настроек окна. Это сообщение заставляет окна отрисовать заново себя и свои дочерние окна.

Почти финал — собираем все воедино

Функция main отрисовщика будет выглядеть примерно так:

int main() { // Получаем параметры рабочего стола auto desktopResolution = GetDesktopParams(); const auto totalScreenCapacity = desktopResolution.x * desktopResolution.y; // Создаем файлы и заполняем векторы с названиями файлов и дескрипторами std::vector<HANDLE> handles(totalScreenCapacity); std::vector<std::wstring> blacks(totalScreenCapacity); std::vector<std::wstring> whites(totalScreenCapacity); FillDesktop(desktopResolution, handles, blacks, whites); // Считываем содержимое файла framedata.bapl auto bytes = ReadAllBytes(pixel_source_path); auto i = 0; auto frame = 0; // Отрисовываем созданные файлы SendMessage(HWND_BROADCAST, WM_SETTINGCHANGE, 0, 0); while (i < bytes.size()) { i++; //Считываем очередной байт char value = bytes[i]; // Если это команда для снимка экрана — делаем скриншот if (value == BYTE_FRAME_SCREENSHOT) { TakeScreenshot(frame); frame++; } else { // Получаем координаты и цвет пикселя auto x = bytes[i + 1]; auto y = bytes[i + 2]; auto color = bytes[i + 3]; i += 3; // Переименовываем соответствующий файл auto position = y * desktopResolution.x + x; RenameFileByHandle(handles[position], color == BYTE_ONE ? whites[position] : blacks[position]); } } // Делаем финальный скриншот TakeScreenshot(frame); return 0; }

Отрисовщик считывает файл framedata.bapl байт за байтом, переименовывает соответствующий файл или делает скриншот в нужный момент. На выходе получаем множество файлов формата .bmp — скриншоты окна для каждого из кадров видео Bad Apple.

Сборка снимков экрана обратно в видео

Осталось еще немного, скоро дойдем до видео)

Используем ffmpeg:

ffmpeg -framerate 10 -i «scan_%d.bmp» output.mp4

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

Финал

Итоги

В рамках статьи мы смогли запустить Bad Apple на значках рабочего стола. В ходе работы мы использовали множество функций API Windows, научились обращаться к некоторым COM интерфейсам, делать скриншоты с помощью GDI+ и компоновать их в видео при помощи ffmpeg.

Ссылка на репозиторий проекта

Теги:

  • winapi
  • desktop
  • c++14
  • gdi+
  • ffmpeg

Хабы:

  • .NET
  • C++
  • Разработка под Windows

Источник

Каталог товаров с купонами и промокодами онлайн

Оставьте ответ

Ваш электронный адрес не будет опубликован.

©Купоно-Мания.ру