Приветствую, Хабравчане!
В прошлой статье получилось создать минимальную программу "Hello world!" размером 3,5 кб. Теперь будем рисовать нативными средствами Windows. Создадим окно, нарисуем пару примитивов.
Основной репозиторий RetroFan.
Поехали.
В те далекие года, когда видеокарта имела на борту 512 кб памяти. Подход к выводу графики был немножко другим. Рисование на экран происходило только с помощью библиотеки GDI в Windows и xlib в Linux, предоставляя свои функции рисования. Сейчас нам доступны API вроде OpenGL, Vulkan и DirectX. В которых центральный процессор не принимает минимальное участие, все рисование, вывод примитивов, фигур все лежит на плечах видеокарты. Но раннее всем этим занимался центральный процессор. Уже в поздние годы, в некоторые видеокарты встраивался аппаратный блиттер. Который ускорял копирование байтов из озу, в память видеокарты.
Шаги рисования линии в 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.
В итоге получим такое.
Windows API умеет рисовать разные примитивы, эллипсы, полигоны, окружности, текст. Но сейчас это не важно. Я об этом расскажу в следующих статьях. Когда начну написание мини графического фреймворка, абстрагирующий от windows и linux.
Под Windows бинарник подрос на 1 кб. Теперь для 32-ух битной сборки он равен 4,5 для 64-ёх битной целых 5,0 кб.
Вывод изображений, оставим до реализации мини фреймворка. Так как потребуется не только вывод RGB изображений, но и паллитровых. Для этого нужно будет написать свой загрузчик 8-ми битных bmp изображений, с палитрой. Пока код довольно примитивен, поэкспериментирую, какой компилятор тех лет, сможет нативно собрать исходник под Windows 3.1.
Комментарии (9)
eugeneyp
13.01.2025 14:18Очень интересно было в DOS работать с экраном char (*screen)[80][2]=0xB000;
И для вывода на экран можно просто заполнять массив char.
И правила именования с типом переменной вначалеlpszClassName
я не встречал с тех времен.
Также тогда в Windows активно использовались диалоги, и хранение форм для окна в ресурсах. Что-то типа VCL форм от Delphi.AlexeyK77
13.01.2025 14:18охх, тем диалоговым формам до VCL было как до луны. Там эти диалоги делались через редактор ресурса, так-же как и иконки, строки и прочее. Во всяком случае в четвертом борланде (не билдер естественно). Это был мрак, закат и рассвет солнца вручную.
vvviperrr
на своей первой работе делал кросплатформенный гуй для кассовой проги на основе sdl и opengl. до сих пор в шоке.