- PVSM.RU - https://www.pvsm.ru -

Nuklear — идеальный GUI для микро-проектов?

Nuklear — идеальный GUI для микро-проектов? - 1 Nuklear [1] — это библиотека для создания immediate mode пользовательских интерфейсов. Библиотека не имеет никаких зависимостей (только C89! только хардкор!), но и не умеет создавать окна операционной системы или выполнять реальный рендеринг. Nuklear — встраиваемая библиотека, которая предоставляет удобные интерфейсы для отрисовки средствами реализованного приложения. Есть примеры на WinAPI, X11, SDL, Allegro, GLFW, OpenGL, DirectX. Родителем концепции была библиотека ImGUI [2].

Чем прекрасна именно Nuklear? Она имеет небольшой размер (порядка 15 тысяч строк кода), полностью содержится в одном заголовочном файле, создавалась с упором на портативность и простоту использования. Лицензия Public Domain.

Постановка задачи

У меня часто возникают задачи, для реализации которых приходится писать мелкие утилитки в несколько сотен строк кода. Обычно в результате получается консольное приложение, которое кроме меня никто толком использовать не может. Возможно, простой GUI сможет сделать эти утилиты более удобными?

Итак, требования к результату:

  1. Малый размер, до сотен килобайт.
  2. Кроссплатформенность, для начала хотя бы Windows и Linux.
  3. Отсутствие зависимости от внешних библиотек в Windows, всё должно быть в одном EXE-файле.
  4. Приличный/красивый внешний вид.
  5. Поддержка картинок в форматах JPG и PNG.
  6. Простота разработки, возможность разработки в Windows и Linux.
    Справится ли Nuklear?

Nuklear NodeEdit

Для примера рассмотрим создание утилиты dxBin2h (GitHub [3]) — она считывает файл побайтово и записывает в виде Си-массива. Кроме основного функционала программа имеет всякие "плюшки", типа удаления ненужных символов и т.п. Обычно ради стороннего функционала и создаются свои маленькие утилиты. Например, dxBin2h создавалась для Winter Novel [4], для предварительной обработки ASCII-файлов.

Простота разработки, кроссплатформенность

Nuklear — идеальный GUI для микро-проектов? - 3 Уж с чем, а с простотой разработки проблем быть не должно. Ведь с прицелом на неё библиотека и создавалась, так? Прямо в 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 и SDL

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

Отсутствие зависимостей в Windows

Ну хорошо, берём рендером 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:
dxBin2h GDI+ Arial 12pt

Linux, отрисовка через SDL2 и OpenGL, шрифт по умолчанию:
dxBin2h SDL2 linux stdfont

Приложение выглядит совсем по-разному! И первое, что бросается в глаза — шрифт.

Шрифт

Чтобы приложение выглядело одинаково во всех операционных системах нужно использовать один и тот же шрифт. Можно было бы взять какой-нибудь системный шрифт, который гарантированно есть везде. Но такого шрифта нет. Поэтому шрифт придётся включать в своё приложение. ttf-шрифты обычно весят сотни килобайт, но из них хорошо создаются подмножества с необходимыми символами. Например, с помощью веб-сервиса FontSquirrel [8]. DejaVu Serif ужался до 40kb, хотя и содержит в себе кириллицу, польский и ещё кучу языков.

Всё было бы отлично, но GDI+ драйвер для Nuklear не умел загружать шрифт из памяти, только из файла. Пришлось исправлять [9]… Кстати, шрифт можно включить в своё приложение с помощью той же dxBin2h [3].

Windows, DejaVu Serif:
dxBin2h Windows  DejaVu Serif

Linux, DejaVu Serif:
dxBin2h Linux DejaVu Serif

Уже намного лучше. Но мне не нравится внешний вид чекбоксов. И хотелось бы увидеть картинки.

Картинки: PNG, JPGNuklear — идеальный GUI для микро-проектов? - 8

И SDL2 и GDI+ умеют загружать картинки. Но для SDL при загрузке JPG и PNG появляется дополнительная зависимость — SDL_image. Избавиться от неё довольно просто: используем stb_image.h [10], если проект собирается с SDL.

С GDI+ тоже не всё было хорошо. А именно, GDI+ драйвер для Nuklear не умел отрисовывать изображения средствами GDI+. Пришлось вникать в работу с изображениями и реализовывать самому (Pull Request [11]). Теперь всё исправлено и код в официальном репозитории.

Код загрузки изображения через stb_image для OpenGL

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 выглядит так:
dxBin2h Windows

В Linux:
dxBin2h Linux

Что в итоге?

  1. Windows EXE после компиляции 200kb, после ужатия UPX [13] 90kb. В Linux из-за использования stb_image размер приложения в среднем на 100kb больше.
  2. Проверена работа в Windows и Linux.
  3. Шрифт и картинки хранятся как массивы в памяти приложения. Зависимостей не от WinAPI в Windows нет.
  4. Движок изменения стиля приложения работает.
  5. PNG и JPG загружаются средствами GDI+ и stb_image.
  6. Весь "грязный" платформенно-зависимый код вынесен в отдельный файл. Разработчик фокусируется именно на создании приложения.

Известные проблемы

  • Различное сглаживание шрифтов в разных операционных системах
  • Разный размер чекбоксов
  • Разная поддержка изображений (при использовании stb_image нужно избегать проблемных изображений)
  • Не полная поддержка юникода при урезанном шрифте
  • Нет примера на технологиях Mac OS X

Как пользоваться наработками

  1. Склонировать репозиторий https://github.com/DeXP/dxBin2h [3]
  2. Скопировать оттуда папку "GUI" в свой проект
  3. Подключить "GUI/nuklear_cross.h", использовать функции оттуда
  4. При необходимости обновления файлов Nuklear скопировать их из официального репозитория поверх текущих.

Заключение

Приложение выглядит немного по-разному в разных операционных системах. Однако отличия незначительны, полученный результат меня удовлетворил. 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