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

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

Шаги рисования линии в GDI тех лет, назовем его нативной отрисовкой.
Вызов функции рисования
Блокировать память на видеокарте для чтения.
ЦП в цикле заполняет память видеокарты.
Происходит разблокировка памяти.
Обновление экрана, теперь на экране видны изменения.
Ещё есть вариант ускорения отрисовки с помощью буфера экрана в озу. Каждый пиксель это 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.
В итоге получим такое.

Добавил нормальный rand в libc. Реализация с просторов интернета
static size_t next = 1;
int rand(void)
{
next = next * 1103515245 + 12345;
return (size_t)(next / 65536) % 32768;
}
void srand(size_t seed)
{
next = seed;
}
И если поменять реализацию, то получаем такой вариант.
int random(unsigned int min, unsigned int max)
{
return rand() % ((max + min) + min);
}
for (size_t i = 0; i < 500; i++)
{
RECT rect;
rect.left = random(0, 800);
rect.top = random(0, 600);
rect.right = random(25, 50);
rect.bottom = random(25, 50);
HBRUSH brush = CreateSolidBrush(RGB(random(0, 255), random(0, 255), random(0, 255)));
FillRect(handleDeviceContext, &rect, brush);
DeleteObject(brush);
}

Windows API умеет рисовать разные примитивы, эллипсы, полигоны, окружности, текст. Но сейчас это не важно. Я об этом расскажу в следующих статьях. Когда начну написание мини графического фреймворка, абстрагирующий от windows и linux.
Под Windows бинарник подрос на 1 кб. Теперь для 32-ух битной сборки он равен 4,5 для 64-ёх битной целых 5,0 кб.
Вывод изображений, оставим до реализации мини фреймворка. Так как потребуется не только вывод RGB изображений, но и паллитровых. Для этого нужно будет написать свой загрузчик 8-ми битных bmp изображений, с палитрой. Пока код довольно примитивен, поэкспериментирую, какой компилятор тех лет, сможет нативно собрать исходник под Windows 3.1.
Комментарии (31)
eugeneyp
13.01.2025 14:18Очень интересно было в DOS работать с экраном char (*screen)[80][2]=0xB000;
И для вывода на экран можно просто заполнять массив char.
И правила именования с типом переменной вначалеlpszClassName
я не встречал с тех времен.
Также тогда в Windows активно использовались диалоги, и хранение форм для окна в ресурсах. Что-то типа VCL форм от Delphi.AlexeyK77
13.01.2025 14:18охх, тем диалоговым формам до VCL было как до луны. Там эти диалоги делались через редактор ресурса, так-же как и иконки, строки и прочее. Во всяком случае в четвертом борланде (не билдер естественно). Это был мрак, закат и рассвет солнца вручную.
MasterMentor
13.01.2025 14:18Заплюсовал за codestyle, олдскульно.
Теперь по сути: на 2025-й год называть рисование через MoveToEx, LineTo, FillRect со всеми прелестями вроде HWND, WNDCLASSA, PAINTSTRUCT "ненормальным программированием" - можно, "демосценой" - нельзя.
Ненормальность в том, в каждом процессоре уже сидит тензорная алгебра (о шейдерах - я молчу), а мы всё юзаем BRUSH.
Что по демосцене - так это вообще от другом.
PS Из истории
"Демосцен" было две:
-
стерильная русская демосцена (https://ru.wikipedia.org/wiki/Русскоязычная_демосцена) , где пара-тройка мажор-кексов из МГУ, МФТИ и иже с ними с хорошей физ и мат. подготовкой создало пару графических нетленок вроде Zoom 3 by AND https://www.youtube.com/watch?v=n92Is47luCI .
В годы расцвета ~1993-2003 эта "демосцена" умещалась в ~30 человек. Затем - всё.
(нельзя писания на ассемблере Zilog Z80 называть "демосценой" в 2025 году).Краткая история русской демосцены
"демосцена" - как часть международного андеграунда, где демки - были лишь intro (вступлениями) и greetings (приветами) в среде андеграундных групп.
Так что, статья:
- на олдскул - тянет;
- на демосцену "по-русски" *** (ассемблер Zilog Z80+) - да;
- на демосцену иную - увольте. Впрочем, к этому не привыкать. ;)***
игра слов, аллюзия на Пинбол по-русски https://small-games.info/?go=game&c=21&i=2617 (кто знает - тот знает)
Jijiki
13.01.2025 14:18просто область окна надо захендлить и обновлять буффер чтото такое толи напрямую толи пока не понял как, но вроде область окна можно выделить, тоесть захендлить кадр и тогда поидее можно в него рисовать матрицами/тензорами, но это не точно, там надо точно тогда знать как выделять видео память и прочее прочее
MasterMentor
13.01.2025 14:18Да можно. Выделяй в памяти буфер и рисуй на нём чем угодно - хоть тензорами (хотя они не для этого), хоть - попиксельно, а затем через memcpy перекидывай битовую картинку в Виндовое окно.
Кроссплатформ до повсеместной аппаратной поддержки Graphics Languages (вроде DX, OpenGL, GLSL итд) так и писался
*
Кто такой Андре Ламот и почему вам следует прочесть его книги?
Jijiki
13.01.2025 14:18я сейчас вот что делаю https://github.com/richKirl/TestDSAWorld
MasterMentor
13.01.2025 14:18Милые обезьянки на GL-е.
Что до меня, так считаю, что писать графику на Си-подобных языках - мовитон. Правильней делать специальный язык+vm-ку и кодировать графику на нём. Поэтому у меня любовь с первой ложки к SideFX Houdini (хоть язык там и кривоват).
есть и такие проекты
-
Shaman_RSHU
13.01.2025 14:18Использовал для графики DOS/4GW на Watcom C.
Много экономило времени и сил.
JordanCpp Автор
13.01.2025 14:18Ненормальность в том, в каждом процессоре уже сидит тензорная алгебра, а мы всё юзаем BRUSH.
Это только начало. Этот режим нужен для совместимости со старыми windows. Потом будет режим рисования в буфер, потом opengl и directx. Поверх 2d абстракции с единып api. В этом цель.
MasterMentor
13.01.2025 14:18Идея понятна сразу - уйти от тухлятины вроде многотомных stdlib-ов с их несовместимостями в разных версиях винды. Сделать lightweight + zero dependency в бинарниках.
Но я не вижу продолжения. Нет аудитории , понимающей и принимающей задумку.
PS Картинка IDE VC6 в Пилим движок Arcanum. Урок 03 - "резанула глаз" ;) . Это самая продуманная и лучшая IDE (+ VisualAssist от Tomato Software) за время существовования кодирования. ;)
JordanCpp Автор
13.01.2025 14:18Но я не вижу продолжения. Нет аудитории , понимающей и принимающей задумку.
В первую очередь это интересно мне. Это мое желание рассказать и показать. И я понимаю и принимаю, что это скорей всего интересно паре десятков человек. Я не создаю платный контент, просто пилю статьи в удовольствие. Поэтому мне не нужны подписчики в ТГ, на Ютубе, что бы впаривать им рекламу. Меня это полностью устраивает.
И у меня нет таймингом и сроков. Захотел написал одно, потом другое.
Идея понятна сразу - уйти от тухлятины вроде многотомных stdlib-ов с их несовместимостями в разных версиях винды. Сделать lightweight + zero dependency в бинарниках.
Да именно так.
vvviperrr
на своей первой работе делал кросплатформенный гуй для кассовой проги на основе sdl и opengl. до сих пор в шоке.
JordanCpp Автор
Это здорово. Я все как то намеревался разобраться в данной теме, но дальше кнопки никогда не уходил. Что бы именно с нуля рисовать все контролы и писать их логику.
Jijiki
там не сложно, вьюшку сделать с кнопками под проект, например вью для базы данных по назначению, или еще чонить на СДЛ не так сложно сделать, или лаунчер, да любую вьюшку, opengl если, то скорее всего вы так отозвались потомучто есть 2 вида матриц 3д матрица и 2д - ортогональная, вы задаёте просто координаты квадрата в кратце и рисуете вашу область нужную 2д, а 2д в 3д просто убираете z в шейдере, вообщем просто матешу чуть чуть потдянуть, там не сложно на самом деле, тоесть это двух матричное рисование в окне
Скрытый текст
это вводная в партиклы, в отображение текста в мире и прочее, ну кароче плоская плоскость в 3д
вот вы делаете сейчас функционал под окна я хотел еще вчера вам напомнить что в рисовании окон тоже матеша есть
Arenoros
ну рисование квадратов то это одно, но как по мне основная проблема гуя на "видюхе" это нормальный рендер текста и дополнительная сложность когда нужно нарисовать "много текста", даже в том же imgui где это неплохо решили, вещь не тривиальная.
MasterMentor
При случае сделайте в оупенсор создание нативного GUI (мобильники+винда) из Json.
Заслуживающих внимание GUI библиотек - туча. Есть библиотеки:
дающие кросплатформенный канвас - рисуй что хошь:
дающие zerodependency (почти) stb Ebedded GUI - встраивай:
дающие кросплатформенные native GUI
Но, чтоб строили кросплатформенный stb GUI из Json, я не встречал.
GUI Json можно генерить здесь:
GUI->Json (Editor)
https://github.com/myfoundation/forms_editor/
JordanCpp Автор
Уже добавил в закладки. Надо будет посмотреть. Спасибо.
NosferatuDima2
а https://www.wxwidgets.org/ не думали посмотреть?
vdudouyt
Прежде чем кому-либо советовать wxWidgets рекомендую обратить внимание на то, что главная страница данного проекта содержит политические заявления. Было бы не очень здорово, если бы кому-нибудь из пользователей софтины ничего не подозревающего JordanCpp отформатировало ЖД за неправильную юрисдикцию.
Jijiki
GLFW только пк, SDL даёт порты в большее количество платформ
да жесон это просто способ настройки коммуникации с бинарником, суть в том что сдл на 2д своём даёт простоту кода для создания нужного 2д функционала, а на опенжль просто тоже самое, но с шейдерами, но есть прям гуй либы тут да вы правы
я крайнюю игру делал под пк/андроид на сдл3 и там у меня был свой бинарник ассета(менеджер - загрузчик), врятли на данном этапе сделаю жесон, жесон же можно получить из SQLite - пока я пользовался максимум скулайтом, а жесоном не пользовался
это надо gltf читать разбираться, а тут ассимп он поудобнее и нет имгуя как вы видите всё родное то что в игре будет (ну скорее не в игре, а демосцене - игра очень амбициозно)
MasterMentor
SQLite - излишен. Я немного математик.
В качестве бонуса. Полноценная реализация SQL на Pure JS в 482 LOC (не моя).
https://github.com/myfoundation/SQLike
То есть, таблицы в +- пару млн записей (что для GUI, и не только, - крыша), можно гонять в Json-е. :)
Jijiki
https://github.com/moleium/imjson вот посмотрите вроде чтото такое, есть еще gltf-viewer и сам imgui