- PVSM.RU - https://www.pvsm.ru -
Nuklear [1] — это библиотека для создания immediate mode пользовательских интерфейсов. Библиотека не имеет никаких зависимостей (только C89! только хардкор!), но и не умеет создавать окна операционной системы или выполнять реальный рендеринг. Nuklear — встраиваемая библиотека, которая предоставляет удобные интерфейсы для отрисовки средствами реализованного приложения. Есть примеры на WinAPI, X11, SDL, Allegro, GLFW, OpenGL, DirectX. Родителем концепции была библиотека ImGUI [2].
Чем прекрасна именно Nuklear? Она имеет небольшой размер (порядка 15 тысяч строк кода), полностью содержится в одном заголовочном файле, создавалась с упором на портативность и простоту использования. Лицензия Public Domain.
У меня часто возникают задачи, для реализации которых приходится писать мелкие утилитки в несколько сотен строк кода. Обычно в результате получается консольное приложение, которое кроме меня никто толком использовать не может. Возможно, простой GUI сможет сделать эти утилиты более удобными?
Итак, требования к результату:
Для примера рассмотрим создание утилиты dxBin2h (GitHub [3]) — она считывает файл побайтово и записывает в виде Си-массива. Кроме основного функционала программа имеет всякие "плюшки", типа удаления ненужных символов и т.п. Обычно ради стороннего функционала и создаются свои маленькие утилиты. Например, dxBin2h создавалась для Winter Novel [4], для предварительной обработки ASCII-файлов.
Уж с чем, а с простотой разработки проблем быть не должно. Ведь с прицелом на неё библиотека и создавалась, так? Прямо в Readme на GitHub [5] есть пример. Абсолютно понятные и лаконичные 20 строк кода дают красивый и чёткий результат.
/* init gui state */
struct nk_context ctx;
nk_init_fixed(&ctx, calloc(1, MAX_MEMORY), MAX_MEMORY, &font);
enum {EASY, HARD};
int op = EASY;
float value = 0.6f;
int i = 20;
if (nk_begin(&ctx, "Show", nk_rect(50, 50, 220, 220),
NK_WINDOW_BORDER|NK_WINDOW_MOVABLE|NK_WINDOW_CLOSABLE)) {
/* fixed widget pixel width */
nk_layout_row_static(&ctx, 30, 80, 1);
if (nk_button_label(&ctx, "button")) {
/* event handling */
}
/* fixed widget window ratio width */
nk_layout_row_dynamic(&ctx, 30, 2);
if (nk_option_label(&ctx, "easy", op == EASY)) op = EASY;
if (nk_option_label(&ctx, "hard", op == HARD)) op = HARD;
/* custom widget pixel width */
nk_layout_row_begin(&ctx, NK_STATIC, 30, 2);
{
nk_layout_row_push(&ctx, 50);
nk_label(&ctx, "Volume:", NK_TEXT_LEFT);
nk_layout_row_push(&ctx, 110);
nk_slider_float(&ctx, 0, &value, 1.0f, 0.1f);
}
nk_layout_row_end(&ctx);
}
nk_end(&ctx);
Но не всё так просто. Часть, отвечающая непосредственно за просчёт GUI действительно проста. Только должен быть ещё и рендер. Идём в папку demo [6], выбираем понравившийся. И видим уже далеко не 20 строк. Мало того, хотя примеры и рисуют на экране примерно одинаковый результат, но код значительно отличается именно из-за рендера.
WinAPI:
static LRESULT CALLBACK
WindowProc(HWND wnd, UINT msg, WPARAM wparam, LPARAM lparam)
{
switch (msg) {
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
if (nk_gdip_handle_event(wnd, msg, wparam, lparam))
return 0;
return DefWindowProcW(wnd, msg, wparam, lparam);
}
int main(void)
{
GdipFont* font;
struct nk_context *ctx;
WNDCLASSW wc;
RECT rect = { 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT };
DWORD style = WS_OVERLAPPEDWINDOW;
DWORD exstyle = WS_EX_APPWINDOW;
HWND wnd;
int running = 1;
int needs_refresh = 1;
/* Win32 */
memset(&wc, 0, sizeof(wc));
wc.lpfnWndProc = WindowProc;
wc.hInstance = GetModuleHandleW(0);
wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.lpszClassName = L"NuklearWindowClass";
RegisterClassW(&wc);
AdjustWindowRectEx(&rect, style, FALSE, exstyle);
wnd = CreateWindowExW(exstyle, wc.lpszClassName, L"Nuklear Demo",
style | WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT,
rect.right - rect.left, rect.bottom - rect.top,
NULL, NULL, wc.hInstance, NULL);
SDL:
int
main(int argc, char* argv[])
{
/* Platform */
SDL_Window *win;
SDL_GLContext glContext;
struct nk_color background;
int win_width, win_height;
int running = 1;
/* GUI */
struct nk_context *ctx;
/* SDL setup */
SDL_SetHint(SDL_HINT_VIDEO_HIGHDPI_DISABLED, "0");
SDL_Init(SDL_INIT_VIDEO);
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2);
win = SDL_CreateWindow("Demo",
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
WINDOW_WIDTH, WINDOW_HEIGHT, SDL_WINDOW_OPENGL|SDL_WINDOW_SHOWN|SDL_WINDOW_ALLOW_HIGHDPI);
glContext = SDL_GL_CreateContext(win);
SDL_GetWindowSize(win, &win_width, &win_height);
Ну хорошо, берём рендером SDL2 с OpenGL и получаем результирующее приложение под Windows, Linux, Mac OS X, Android, iOS и ещё кучу чего! Всё супер, только вот в стандартной поставке Windows библиотеки SDL нет. Значит, придётся тащить с собой. А это нарушает первое требование (малый размер), т.к. сама SDL весит порядка мегабайта.
Зато в списке примеров виднеется GDI+, которая есть в Windows начиная с XP. GDI+ умеет ttf-шрифты, картинки PNG и JPG, и всё это возможно загружать прямо из памяти. Пускай в итоге будет 2 возможных рендера: GDI+ для Windows и SDL для всех остальных случаев. Можно вынести часть кода, зависящую от рендера, в отдельный Си-файл (nuklear_cross.c [7]). Тогда основной код не будет перегружен, и можно будет сфокусироваться именно на интерфейсе, что значительно упрощает разработку. Дополнительным плюсом получаем ускорение компиляции — весь Nuklear будет компилироваться в отдельный объектный файл, который будет редко изменяться.
Windows, отрисовка через GDI+, шрифт Arial 12pt:
Linux, отрисовка через SDL2 и OpenGL, шрифт по умолчанию:
Приложение выглядит совсем по-разному! И первое, что бросается в глаза — шрифт.
Чтобы приложение выглядело одинаково во всех операционных системах нужно использовать один и тот же шрифт. Можно было бы взять какой-нибудь системный шрифт, который гарантированно есть везде. Но такого шрифта нет. Поэтому шрифт придётся включать в своё приложение. ttf-шрифты обычно весят сотни килобайт, но из них хорошо создаются подмножества с необходимыми символами. Например, с помощью веб-сервиса FontSquirrel [8]. DejaVu Serif ужался до 40kb, хотя и содержит в себе кириллицу, польский и ещё кучу языков.
Всё было бы отлично, но GDI+ драйвер для Nuklear не умел загружать шрифт из памяти, только из файла. Пришлось исправлять [9]… Кстати, шрифт можно включить в своё приложение с помощью той же dxBin2h [3].
Windows, DejaVu Serif:
Linux, DejaVu Serif:
Уже намного лучше. Но мне не нравится внешний вид чекбоксов. И хотелось бы увидеть картинки.
И SDL2 и GDI+ умеют загружать картинки. Но для SDL при загрузке JPG и PNG появляется дополнительная зависимость — SDL_image. Избавиться от неё довольно просто: используем stb_image.h [10], если проект собирается с SDL.
С GDI+ тоже не всё было хорошо. А именно, GDI+ драйвер для Nuklear не умел отрисовывать изображения средствами GDI+. Пришлось вникать в работу с изображениями и реализовывать самому (Pull Request [11]). Теперь всё исправлено и код в официальном репозитории.
struct nk_image dxNkLoadImageFromMem(const void* buf, int bufSize){
int x,y,n;
GLuint tex;
unsigned char *data = stbi_load_from_memory(buf, bufSize, &x, &y, &n, 0);
glGenTextures(1, &tex);
glBindTexture(GL_TEXTURE_2D, tex);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR_MIPMAP_NEAREST);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, x, y, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
return nk_image_id((int)tex);
}
Чтобы изменить вид чекбоксов в Nuklear есть механизм выставления стилей. Здесь включенный и выключенный чекбокс являются отдельными PNG-картинками. В этом же коде же выставляется красная тема из примеров Nuklear (файл style.c [12]):
nk_image checked = dxNkLoadImageFromMem( (void*)checked_image, sizeof(checked_image) );
nk_image unchecked = dxNkLoadImageFromMem( (void*)unchecked_image, sizeof(unchecked_image) );
set_style(ctx, THEME_RED);
{struct nk_style_toggle *toggle;
toggle = &ctx->style.checkbox;
toggle->border = -2; /* cursor must overlap original image */
toggle->normal = nk_style_item_image(unchecked);
toggle->hover = nk_style_item_image(unchecked);
toggle->active = nk_style_item_image(unchecked);
toggle->cursor_normal = nk_style_item_image(checked);
toggle->cursor_hover = nk_style_item_image(checked);
}
Приложение в Windows выглядит так:
В Linux:
Приложение выглядит немного по-разному в разных операционных системах. Однако отличия незначительны, полученный результат меня удовлетворил. Nuklear не входит в категорию "я уверен, что будет работать везде и без тестирования". Зато входит в категорию "если что будет нужно — легко допишу".
Автор: DeXPeriX
Источник [18]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/linux/231556
Ссылки в тексте:
[1] Nuklear: https://github.com/vurtun/nuklear
[2] ImGUI: https://github.com/ocornut/imgui
[3] GitHub: https://github.com/DeXP/dxBin2h
[4] Winter Novel: http://store.steampowered.com/app/485350
[5] Readme на GitHub: https://github.com/vurtun/nuklear#example
[6] demo: https://github.com/vurtun/nuklear/tree/master/demo
[7] nuklear_cross.c: https://github.com/DeXP/dxBin2h/blob/master/GUI/nuklear_cross.c
[8] веб-сервиса FontSquirrel: https://www.fontsquirrel.com/tools/webfont-generator
[9] исправлять: https://github.com/vurtun/nuklear/pull/318
[10] stb_image.h: https://github.com/nothings/stb
[11] Pull Request: https://github.com/vurtun/nuklear/pull/316
[12] style.c: https://github.com/vurtun/nuklear/blob/master/demo/style.c
[13] UPX: https://upx.github.io
[14] Список моих правок для корректной работы драйвера GDI+: https://github.com/vurtun/nuklear/commits?author=DeXP
[15] Тема на форуме GameDev, где анонсируется релиз Nuklear: https://www.gamedev.net/topic/669332-immediate-mode-gui-toolkit/
[16] Обсуждение на тему полезности immediate mode GUI: http://gamedev.stackexchange.com/questions/24103/immediate-gui-yae-or-nay
[17] Пример создания приложений на ImGUI: https://eliasdaler.wordpress.com/2016/05/31/imgui-sfml-tutorial-part-1/
[18] Источник: https://habrahabr.ru/post/319106/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.