Bad Apple on desktop icons – working with WinAPI

If something exists, you can run Bad Apple on it
Rule 86

Over the past 15 years, Bad Apple has launched a lot of things – on homemade RISC-V processoron oscilloscopeon apples. Let’s try running Bad Apple on desktop icons using Windows API calls and a few others.

Requirements

Visual Studio 2022

Required load for Visual Studio:

Required VS Packages

Required VS Packages

.NET Core

Suitable SDK version 7+:

winget install Microsoft.DotNet.SDK.7
ffmpeg
winget install --id=Gyan.FFmpeg  -e
Optional: yt-dlp

It is only needed to download videos from YouTube, there is another way

winget install yt-dlp

What will we display on?

Icon colors

We will render the video in 2 colors – black and white. There are two options for changing the file icon:

  • If the file is an image, then change the image, it will be displayed in a miniature version

  • Change the file extension so that Windows selects the icon we need.

The first option requires rewriting the contents of the file each time, this is a very expensive operation. Let’s use the second option.

Let’s come up with 2 file extensions, for each of which Windows will display its own icon. For example: .baclrw – white color, and .baclrb – black. Now you need to register icons for these extensions in the registry.

Let’s create 2 icons in the format .ico in folderC:\badappleresources with black and white solid filling. Let’s create and run the file badapple.reg with this content:

Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\.baclrb]
[HKEY_CLASSES_ROOT\.baclrb\DefaultIcon]
@="C:\\badappleresources\\badapple_black.ico"

[HKEY_CLASSES_ROOT\.baclrw]
[HKEY_CLASSES_ROOT\.baclrw\DefaultIcon]
@="C:\\badappleresources\\badapple_white.ico"

As a result, Windows will begin to correctly display files with our extensions:

Icons for our invented file extensions

Icons for our invented file extensions

Icon sizes

All desktop icons are displayed with outer indentation. It is necessary to select an icon size that will maximize the filling of the icon.

The desktop in Windows NT is practically the same Explorer window as any folder. This means that you can work with it like with any other folder – extract information about the location of icons, their parameters, etc.

In order to change the size of desktop icons using Win32, you need to make several COM calls:

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);
FindDesktopFolderView function code
void 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);
}

Adjusting the value desiredSize We are looking for an icon size such that the margins along the edges of the icon will be minimal. This value is 67.

Different paddings for different icon sizes (67 - left)

Different paddings for different icon sizes (67 – left)

This code will resize the icons to 67 each time the program is launched.

“Virtual screen” resolution

Now you need to find out how many icons fit on the desktop horizontally and vertically. You can calculate it manually (an uninteresting option), or you can calculate it automatically after adjusting the size of the icons (a more interesting option):

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 returns the dimensions of the icon taking into account indentations (which is what we need). We divide the screen side by the icon side and get the number of icons per side, on my 2560×1440 pixel monitor that’s 34×11 icons. Remember these numbers, they will come in handy later.

What will we display?

Video preparation

Let’s prepare the necessary files. Download the original video:

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

It is necessary to reduce the video resolution to 34×11, and the frequency to 10 frames per second:

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

Pay attention to the specified resolution – 34 by 12. Unfortunately, ffmpeg requires an even number of vertical pixels, you will have to ignore the last line.

Converting Video to Pixels

You can read the entire video frame by frame and save the pixel value into a binary file. However, at this point you can make a small optimization in terms of code but significant in terms of performance.

Let’s make the application render only the changed pixels. To do this, we will store the previous frame in a buffer and write only the data of the changed pixels to a binary file. To do this, we will write an application in C#; to read the video we will use the package GleamTech.VideoUltimate.

Video to binary format converter code
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-33x12.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);
}

The converter watches the video frame by frame and compares the pixels with the buffer of the previous frame. If the color value is different, then the pixel drawing command is written to the output file:

  • Byte BYTE_FRAME_PIXEL(0)

  • Coordinate X pixel

  • Coordinate Y pixel

  • MeaningBYTE_ONE or BYTE_ZERO – white or black pixel color.

Before completing frame processing, a byte is written to the output file BYTE_FRAME_SCREENSHOT(1) – the renderer will take a screenshot of the desktop when it encounters this byte.

Another small improvement (not really)

You can save about 25% of the binary file by not passing the fourth byte of the “virtual pixel” drawing command – the renderer can store the last state of each icon and change it to the opposite. However, this will lead to a loss of FPS when rendering the video – additional resources will be spent on calculating the new state of the icon

As a result of the converter’s operation, a file appears on the desktop framedata.baplwhich contains the order of drawing pixels and taking a screenshot of the desktop.

How will we display

Let’s fill the desktop with files with black icons. To do this, get the path to the current user’s desktop using 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());

Next, let’s fill in 2 vectors – the full paths to the files of white flowers and black flowers.

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;
        }
    }

As a result, in blacks And whites there will be lines like C:\Users\[User]\Desktop\[x]_[y].baclr['w'|'b'] .

We will change the file icon by renaming it.

Renaming a file

To rename a file, the Win32 API provides a function MoveFile. We could change the index icon i from white to black like this:

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

However, this approach has a significant drawback – with each such rename, a new file descriptor will be created and deleted, and creating a descriptor is a very expensive operation.

We can use a slightly different approach – create a file using the function CreateFilesave the resulting descriptor into a vector and use it for future renaming without closing the file.

 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);
}

All that remains is to directly rename the file, knowing it HANDLEfor this there is SetFileInformationByHandle. Let’s write a function:

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 название файла должно кончаться на \0)
    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);
}

Taking screenshots

We will use GDI+let’s write a function SaveScreenshotToFile.

Code SaveScreenshotToFile
void 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);
}

Before taking a screenshot, you need to refresh the contents of the window and wait until the update occurs. For this we will use SendMessage and write a small function:

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);
}

Team SendMessage is intended to send a message to the selected window (0 argument), we use HWND_BROADCAST, so all windows will be the recipients. In our case the message is WM_SETTINGCHANGE – change window settings. This message causes windows to re-render themselves and their child windows.

Almost final – putting it all together

Function main The renderer will look something like this:

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;
}

The renderer reads the file framedata.bapl byte by byte, renames the corresponding file or takes a screenshot at the right moment. At the output we get many files of the format .bmp – screenshots of the window for each frame of the Bad Apple video.

Assembling screenshots back into videos

There’s still a little time left, we’ll get to the video soon)

We use ffmpeg:

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

All that remains is to add sound in your favorite video editor.

The final

Results

As part of the article, we were able to run Bad Apple on desktop icons. During the work, we used many Windows API functions, learned how to access some COM interfaces, take screenshots using GDI+ and compose them into videos using ffmpeg.

Link to project repository

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *