Приветствую, Хабравчане!

В прошлой статье получилось создать минимальную программу "Hello world!" размером 3,5 кб. Теперь будем рисовать нативными средствами Windows. Создадим окно, нарисуем пару примитивов.

Основной репозиторий RetroFan.

Поехали.

Видеокарта ISA Trident 9000B , память 512 кб
Видеокарта ISA Trident 9000B , память 512 кб

В те далекие года, когда видеокарта имела на борту 512 кб памяти. Подход к выводу графики был немножко другим. Рисование на экран происходило только с помощью библиотеки GDI в Windows и xlib в Linux, предоставляя свои функции рисования. Сейчас нам доступны API вроде OpenGL, Vulkan и DirectX. В которых центральный процессор не принимает минимальное участие, все рисование, вывод примитивов, фигур все лежит на плечах видеокарты. Но раннее всем этим занимался центральный процессор. Уже в поздние годы, в некоторые видеокарты встраивался аппаратный блиттер. Который ускорял копирование байтов из озу, в память видеокарты.

Шаги рисования линии в GDI тех лет, назовем его нативной отрисовкой.

  1. Вызов функции рисования

  2. Блокировать память на видеокарте для чтения.

  3. ЦП в цикле заполняет память видеокарты.

  4. Происходит разблокировка памяти.

  5. Обновление экрана, теперь на экране видны изменения.

Ещё есть вариант ускорения отрисовки с помощью буфера экрана в озу. Каждый пиксель это 3 байта RGB. Написать свои функции рисования примитивов, вывода изображений. ЦП так же своими усилиями меняет значение байт, но не происходит блокировки и не требуется после каждого рисования примитива отправлять данные в видеокарту. И как только рисование завершено, отрисованы сотни спрайтов, примитивов, только после этого происходит копирование буфера на видеокарту, с последующим отображением на экране.

Но у такого способа есть свои недостатки, даже сейчас.

Если мы сейчас захотим таким же способом рисовать на экране при разрешении full hd. Нам понадобиться 1920 *1080 * 3 = 6 мб озу. И получается, что при анимации в 60 кадров потребуется переслать 60*6 = 360 мб памяти в секунду. Это прям очень плохо. Я уже молчу про то, что видеокарта простаивает и у нас еще даже не 4k разрешение.

А теперь представьте, что каждая программа выделяет для своего буфера 6 мегабайт. И если мы запустим 100 программ, только на буфер экрана для таких программ понадобится 600 мб.

Но данный метод отлично работал в 90-ые так как экраны были небольшими и буфер создавали к примеру под разрешение 800 на 600 при RGB цвете это всего лишь 1,5 мб. А если использовать ещё и палитру вместо RGB цветов, то экранный буфер будет занимать всего 500 кб. Таким методом рисовали графику в том числе и под MS-DOS.

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

В данной статье я буду описывать именно нативную версию рисования, мы не будем рисовать в буфер, будем использовать только нативные средства Windows.

Так я пишу не опираясь на внешние зависимости. То для создания окна нужно, объявить функции. Это сделано в данном файле.

Основная идея: Объявления функций, констант и макросов лежат в заголовочном файле Windows.h, каждый компилятор под Windows его имеет в наличии. В нашем случае, я вручную прописал требуемые функции и константы. Но для того, что бы оставить совметимость при сборке со стандартными заголовками и библиотеками такими как libc. Я создал каталог WinAPI и в данном каталоге храню уже копипаст и в итоге весь функционал экспортируется из файла Windows.h.

Если не делать так:

   include_directories("WinAPI")

Тогда не меняя код, будет использован стандартный Windows.h из поставки компилятора. И тем самым достигается совместимость. То же верно и для моих реализаций libc и libcpp.

Для версии под Windows я создал файл wWin.cpp для Linux xWin.cpp. В следующей статье проделаем тоже, самое но уже используя библиотеку xlib.

Примеры кода беру с данного сайта.

Полный код, программы.

Скрытый текст
#include <Windows.h>
#include <string.h>
#include <string>

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) 
{
    switch (msg)
    {

    case WM_DESTROY:
    {
        PostQuitMessage(0);
    }
    break;

    }

    return DefWindowProc(hwnd, msg, wParam, lParam);
}

unsigned int random(unsigned int start_range, unsigned int end_range)
{
    static unsigned int rand = 0xACE1U; /* Any nonzero start state will work. */

    /*check for valid range.*/
    if (start_range == end_range) 
    {
        return start_range;
    }

    /*get the random in end-range.*/
    rand += 0x3AD;
    rand %= end_range;

    /*get the random in start-range.*/
    while (rand < start_range)
    {
        rand = rand + end_range - start_range;
    }

    return rand;
}

int main()
{
    MSG         message;
    HWND        handleWindow;
    HDC         handleDeviceContext;
    WNDCLASSA   windowClass;
    PAINTSTRUCT paintStruct;
    std::string title = "Hello Habr!";

    memset(&windowClass, 0, sizeof(WNDCLASSA));

    windowClass.style         = CS_HREDRAW | CS_VREDRAW;
    windowClass.cbClsExtra    = 0;
    windowClass.cbWndExtra    = 0;
    windowClass.lpszClassName = "Window";
    windowClass.hInstance     = GetModuleHandle(NULL);
    windowClass.hbrBackground = (HBRUSH)COLOR_APPWORKSPACE;
    windowClass.lpszMenuName  = NULL;
    windowClass.lpfnWndProc   = WndProc;

    RegisterClass(&windowClass);
    handleWindow = CreateWindow(windowClass.lpszClassName, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, 0, 0, 800, 600, NULL, NULL, windowClass.hInstance, NULL);

    handleDeviceContext = BeginPaint(handleWindow, &paintStruct);

    for (size_t i = 0; i < 100; i++)
    {
        MoveToEx(handleDeviceContext, 0, 0, NULL);
        LineTo(handleDeviceContext, random(0, 600), random(0, 800));
    }

    for (size_t i = 0; i < 100; i++)
    {
        RECT rect;
        rect.left   = random(0, 600);
        rect.top    = random(0, 800);
        rect.right  = random(0, 600);
        rect.bottom = random(0, 300);

        HBRUSH brush = CreateSolidBrush(RGB(random(0, 255), random(0, 255), random(0, 255)));

        FillRect(handleDeviceContext, &rect, brush);

        DeleteObject(brush);
    }

    EndPaint(handleWindow, &paintStruct);

    ShowWindow(handleWindow, SW_SHOW);
    UpdateWindow(handleWindow);

    while (GetMessage(&message, NULL, 0, 0))
    {
        TranslateMessage(&message);
        DispatchMessage(&message);
    }

    return 0;
}

104 строки не так уж и много. Опишу код для понимая происходящего.

#include <Windows.h> //В данном файле обявлены весь функционал Windows
#include <string.h>  // Нужен для экспорта функции meset
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) 
{
    switch (msg)
    {

    case WM_DESTROY:
    {
        PostQuitMessage(0);
    }
    break;

    }

    return DefWindowProc(hwnd, msg, wParam, lParam);
}

Функция WndProc нужна для обработки событий. При создании окна мы указываем, что все события слать в нашу функцию. Подробнее об этом чуть ниже. В msg прилетает событие, я обрабатываю только одно событие закрытия окна WM_DESTROY. Если пользователь нажал на крестик в правой части окна генерируется событие и я вызываю функцию PostQuitMessage, которая завершает исполняемый поток, то есть нашу программу с окном и выделенными ресурсами.

    MSG         message;             //Тип для сообщений Windows
    HWND        handleWindow;        //Идентификатор окна
    HDC         handleDeviceContext; //Идентификатор устройства для рисования в окне
    WNDCLASSA   windowClass;         //Структура содержит свойства окна
    PAINTSTRUCT paintStruct;         //Структура содержит контест рисования

Обнуляем переменную windowClass и заполняем ее корректными данными. Если не обнулить и не заполнить структуру В ней будут некорректные данные, и окно просто не получится создать.

  memset(&windowClass, 0, sizeof(WNDCLASSA));

    windowClass.style         = CS_HREDRAW | CS_VREDRAW;
    windowClass.cbClsExtra    = 0;
    windowClass.cbWndExtra    = 0;
    windowClass.lpszClassName = "Window";
    windowClass.hInstance     = GetModuleHandle(NULL);
    windowClass.hbrBackground = (HBRUSH)COLOR_APPWORKSPACE;
    windowClass.lpszMenuName  = NULL;
    windowClass.lpfnWndProc   = WndProc;

Опишу основное.

Это стиль окна, в нашем случае мы задаем, что бы окно можно было растягивать по высоте и ширине. В Windows.h довольно много констант для стиля. Отличный сайт по Windows API из которого я черпаю информацию.

windowClass.style         = CS_HREDRAW | CS_VREDRAW;

А вот где мы указываем указатель на нашу функцию обработки событий.

windowClass.lpfnWndProc   = WndProc;

После заполнения, создаем окно функцией CreateWindow. Передаем заполненную структуру windowClass, позиция по x и y и размер 800 на 600. Пока проверки корректности излишни, обработку ошибок добавлю при создании мини фреймворка. А сейчас будет только отвлекать.

CreateWindow(windowClass.lpszClassName, title.c_str(), 
             WS_OVERLAPPEDWINDOW | WS_VISIBLE, 
             0, 0, 800, 600, NULL, NULL, windowClass.hInstance, NULL);

Вызываем функции. показать окно и обновить окно.

  ShowWindow(handleWindow, SW_SHOW);
  UpdateWindow(handleWindow);

Это главный цикл обработки сообщений окна. В цикле получаем сообщение и транслируем его в наше окно. Что бы уже окно вызвала функцию WndProc в которой происходит уже непосредственное реагирование на события.

    while (GetMessage(&message, NULL, 0, 0))
    {
        TranslateMessage(&message);
        DispatchMessage(&message);
    }

Не так уж и сложно и кода получилось не так много.

Теперь код рисования. Будем выводить случайным образом линии и закрашенные прямоугольники. Но пока функционала в минимальной libc нет, поэтому в этом нам поможет stackoverflow, так сказать random курильщика:)

Я только изменил название на random. И он коряво рандомизирует, но всё же работает. Для визуализации данных, нам его вполне хватит.

unsigned int random(unsigned int start_range, unsigned int end_range)
{
    static unsigned int rand = 0xACE1U; /* Any nonzero start state will work. */

    /*check for valid range.*/
    if (start_range == end_range) 
    {
        return start_range;
    }

    /*get the random in end-range.*/
    rand += 0x3AD;
    rand %= end_range;

    /*get the random in start-range.*/
    while (rand < start_range)
    {
        rand = rand + end_range - start_range;
    }

    return rand;
}

Все рисование средствами GDI библиотеки происходит между вызовами BeginPaint и EndPaint.

    handleDeviceContext = BeginPaint(handleWindow, &paintStruct);

    for (size_t i = 0; i < 100; i++)
    {
        MoveToEx(handleDeviceContext, 0, 0, NULL);
        LineTo(handleDeviceContext, random(0, 600), random(0, 800));
    }

    for (size_t i = 0; i < 100; i++)
    {
        RECT rect;
        rect.left   = random(0, 600);
        rect.top    = random(0, 800);
        rect.right  = random(0, 600);
        rect.bottom = random(0, 300);

        HBRUSH brush = CreateSolidBrush(RGB(random(0, 255), random(0, 255), random(0, 255)));

        FillRect(handleDeviceContext, &rect, brush);

        DeleteObject(brush);
    }

    EndPaint(handleWindow, &paintStruct);

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

    for (size_t i = 0; i < 100; i++)
    {
        MoveToEx(handleDeviceContext, 0, 0, NULL);
        LineTo(handleDeviceContext, random(0, 600), random(0, 800));
    }

Теперь рисуем разноцветные закрашенные прямоугольники.

    for (size_t i = 0; i < 100; i++)
    {
        RECT rect;
        rect.left   = random(0, 600);
        rect.top    = random(0, 800);
        rect.right  = random(0, 600);
        rect.bottom = random(0, 300);

        HBRUSH brush = CreateSolidBrush(RGB(random(0, 255), random(0, 255), random(0, 255)));

        FillRect(handleDeviceContext, &rect, brush);

        DeleteObject(brush);
    }

Для начала мы создаем перо и устанавливаем цвет RGB. И передаем его в функцию рисования FillRect.

В итоге получим такое.

Windows API умеет рисовать разные примитивы, эллипсы, полигоны, окружности, текст. Но сейчас это не важно. Я об этом расскажу в следующих статьях. Когда начну написание мини графического фреймворка, абстрагирующий от windows и linux.

Под Windows бинарник подрос на 1 кб. Теперь для 32-ух битной сборки он равен 4,5 для 64-ёх битной целых 5,0 кб.

Вывод изображений, оставим до реализации мини фреймворка. Так как потребуется не только вывод RGB изображений, но и паллитровых. Для этого нужно будет написать свой загрузчик 8-ми битных bmp изображений, с палитрой. Пока код довольно примитивен, поэкспериментирую, какой компилятор тех лет, сможет нативно собрать исходник под Windows 3.1.

Комментарии (9)


  1. vvviperrr
    13.01.2025 14:18

    на своей первой работе делал кросплатформенный гуй для кассовой проги на основе sdl и opengl. до сих пор в шоке.


  1. eugeneyp
    13.01.2025 14:18

    Очень интересно было в DOS работать с экраном char (*screen)[80][2]=0xB000;
    И для вывода на экран можно просто заполнять массив char.

    И правила именования с типом переменной вначале lpszClassNameя не встречал с тех времен.

    Также тогда в Windows активно использовались диалоги, и хранение форм для окна в ресурсах. Что-то типа VCL форм от Delphi.


    1. AlexeyK77
      13.01.2025 14:18

      охх, тем диалоговым формам до VCL было как до луны. Там эти диалоги делались через редактор ресурса, так-же как и иконки, строки и прочее. Во всяком случае в четвертом борланде (не билдер естественно). Это был мрак, закат и рассвет солнца вручную.