Tutorials - TonSharp/WAPITIS GitHub Wiki

Быстрое перемещение:

Quick Start

Минимальный набор функций для запуска программы:

int _main_(MainArgs args)
{
  return 0;
}

Подключаем библиотеку для использования всех возможностей фреймворка:

#include "libs.hpp"

Создаём две глобальные переменные: одну для хранения названия класса, вторую для хранения заголовка окна. Будем использовать переменные типа wstring для корректного отображения русских символов:

wstring szMainClass = L"MainClass"; //Модификатор L перед строкой показывает, что её тип - wstring
wstring szTitle = L"Title";

Создаём глобальную переменную окна и инициализируем экземпляр:

Window* wnd; // Объявляем до _main_

int _main_(MainArgs args)
{
  wnd = new Window(szTitle, &szMainClass, args);
  return 0;
}

Создадим функцию обработки сообщений, которая будет обрабатывать наше главное окно. Функция должна быть объявлена до _main_ (а также должна иметь возвращаемый тип int и принимать единственный аргумент CallbackArgs):

int MainCallback(CallbackArgs args)
{
  return 1; // "1" обозначает завершение необработанного сообщения
}

Создаём и сразу отображаем окно по умолчанию. В качестве аргументов указываем родительское окно (у главного окна его нет, поэтому пишем NULL) и только что созданный класс обработки сообщений:

wnd->CreateDefaultWindow(NULL, MainCallback);

Готовый код

#pragma once
#include "libs.hpp"

wstring szMainClass = L"MainClass";
wstring szTitle = L"Title";

Window* wnd;

int MainCallback(CallbackArgs);

int _main_(MainArgs args)
{
  wnd = new Window(szTitle, &szMainClass, args);
  wnd->CreateDefaultWindow(NULL, MainCallback);
  return 0;
}

int MainCallback(CallbackArgs args)
{
  return 1;
}

Вверх

UI Tutorial

В данном примере мы разберем создание пользовательского интерфейса (в качестве основы возьмем лабораторную работу из университета).

Инициализация окна

Для начала напишем код создания окна из примера в начале и получим следующий каркас:

#pragma once
#include "libs.hpp"

wstring szMainClass = L"MainClass";
wstring szTitle = L"Title";

Window* wnd;

int MainCallback(CallbackArgs);

int _main_(MainArgs args)
{
  wnd = new Window(szTitle, &szMainClass, args);
  wnd->CreateDefaultWindow(NULL, MainCallback);
  return 0;
}

int MainCallback(CallbackArgs args)
{
  return 1;
}

Создание UI элементов

Добавим переменные необходимых нам элементов интерфейса:

Button* AddButton;
ListBox* list;

ComboBox* FirstList;
ComboBox* SecondList;

RadioButton* FirstRadio;
RadioButton* SecondRadio;

Проинициализируем все элементы в _main_ (обязательно после создания главного окна):

FirstList = new ComboBox(L"1", wnd, args.hInstance);
SecondList = new ComboBox(L"1", wnd, args.hInstance);

FirstRadio = new RadioButton(L"Список 1", wnd, args.hInstance);
SecondRadio = new RadioButton(L"Список 2", wnd, args.hInstance);

AddButton = new Button(L"Добавить", wnd, args.hInstance);

list = new ListBox(L"", wnd, args.hInstance, false);

Создадим все элементы:

//Позиции и размеры элементов просчитаны заранее
FirstList->Create(DROPDOWN, { 10, 10 }, { 270, 100 });     //DROPDOWN обозначает, что список будет "выпадать"
SecondList->Create(DROPDOWN, { 10, 40 }, { 270, 100 });

FirstRadio->Create(BORDER, {10, 70}, {100, 20});           //BORDER обозначает, что элемент будет в тонкой рамке
SecondRadio->Create(BORDER, {180, 70}, {100, 20});

AddButton->Create(NULL, { 10, 100 }, { 270, 30 });

list->Create(BORDER, { 10, 140 }, { 270, 120 });
list->AddVScroll(); //VSCROLL обозначает, что элемент будет иметь вертикальную полосу прокрутки

Для примера добавим по одной строке в каждый список:

FirstList->AddItem(L"FIRST");
SecondList->AddItem(L"SECOND");

И сразу выберем их при запуске программы (по умолчанию в выпадающем списке элементы не выбраны):

FirstList->SelectItem(0);
SecondList->SelectItem(0);

Сделаем первую радиокнопку активной по умолчанию:

FirstRadio->SetCheck(TRUE);

Обработка сообщений

Опишем базовый случай выхода из приложения:

int MainCallback(CallbackArgs args)
{
  if(Closing(args)) //Если мы закрываем окно
    Quit();         //То выходим полностью из программы
  
  return 1;
}

Приступим к обработке нажатия кнопки в функции обработки сообщений `MainCallback`:

```c++
int MainCallback(CallbackArgs args)
{
  if (AddButton && AddButton->IsClicked(args)) //Если кнопка существует и была нажата
  {
    if (FirstRadio->IsChecked())                   //Если выбрана первая радиокнопка:
      list->AddItem(FirstList->GetSelectedText()); //Добавляем в нижний список текущий выбранный элемент из первого списка

    else if(SecondRadio->IsChecked())               //Иначе, если выбрана вторая радиокнопка:
      list->AddItem(SecondList->GetSelectedText()); //Добавляем в нижний список текущий выбранный элемент из второго списка
  }

  return 1;
}

Осталось добавить возможность удаления элементов из нижнего списка по двойному щелчку левой кнопки мыши:

if (list && list->IsDoubleClick(args)) //Если нижний список существует и был выполнен двойной щелчок левой кнопкой мыши:
  list->RemoveSelectedItems();         //Удаляем текущий выбранный элемент

Готовый код

#pragma once
#include "libs.hpp"

wstring szMainClass = L"MainClass";
wstring szTitle = L"Title";

Window* wnd;

Button* AddButton;
ListBox* list;

ComboBox* FirstList;
ComboBox* SecondList;

RadioButton* FirstRadio;
RadioButton* SecondRadio;

int MainCallback(CallbackArgs);

int _main_(MainArgs args)
{
  wnd = new Window(szTitle, &szMainClass, args);
  wnd->CreateCustomWindow(NULL, WS_OVERLAPPEDWINDOW, { 10, 10 }, { 300, 300 }, NULL, NULL, NULL, MainCallback);

  FirstList = new ComboBox(L"1", wnd, args.hInstance);
  SecondList = new ComboBox(L"1", wnd, args.hInstance);

  FirstRadio = new RadioButton(L"Список 1", wnd, args.hInstance);
  SecondRadio = new RadioButton(L"Список 2", wnd, args.hInstance);

  AddButton = new Button(L"Добавить", wnd, args.hInstance);

  list = new ListBox(L"", wnd, args.hInstance, false);

  FirstList->Create(DROPDOWN, { 10, 10 }, { 270, 100 });
  SecondList->Create(DROPDOWN, { 10, 40 }, { 270, 100 });

  FirstRadio->Create(BORDER, {10, 70}, {100, 20});
  SecondRadio->Create(BORDER, {180, 70}, {100, 20});

  AddButton->Create(NULL, { 10, 100 }, { 270, 30 });

  list->Create(BORDER, { 10, 140 }, { 270, 120 }, false);
  list->AddVScroll();

  FirstList->AddItem(L"FIRST");
  SecondList->AddItem(L"SECOND");

  FirstList->SelectItem(0);
  SecondList->SelectItem(0);

  FirstRadio->SetCheck(TRUE);

  return 0;
}

int MainCallback(CallbackArgs args)
{
  if(Closing(args))
    Quit(); 

  if (AddButton && AddButton->IsClicked(args))
  {
    if (FirstRadio->IsChecked())
      list->AddItem(FirstList->GetSelectedText());

    else if(SecondRadio->IsChecked())
      list->AddItem(SecondList->GetSelectedText());
  }

  if (list && list->IsDoubleClick(args))
    list->RemoveSelectedItems();

  return 1;
}

Вверх

Menu Tutorial

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

Инициализация элементов

Нам необходимо два экземпляра меню: одно будет верхним меню, которое располагается под окном, а второе - контекстным, которое отображается при щелчке правой кнопкой мыши по списку.

Объявим нужные глобальные переменные:

wstring szMainClass = L"MainClass";
wstring szTitle = L"Laba 5";

Window* wnd;

ListBox* list;

Menu* mainMenu;
Menu* listContext;

int MainCallback(CallbackArgs);

Проинициализируем эти переменные в _main_:

int _main_(MainArgs args)
{
  wnd = new Window(szTitle, &szMainClass, args);
  wnd->CreateDefaultWindow(NULL, MainCallback);
  wnd->SetBackgroundColor(RGB(100, 100, 100));

  list = new ListBox(L"", wnd, args.hInstance, false);
  list->Create(BORDER, { 10, 10 }, { 500, 500 });
}

Создадим основное меню и заполним его:

mainMenu = new Menu(L"Main", false, 500); //Создаем меню. L"Main" является просто отличительным словом. False говорит нам о том, что меню постоянное, а 
                                          //не всплывающее
mainMenu->AddSubMenu(                     //Добавляем первый элемент меню, который будет выпадающим
  {
    {L"Открыть", MF_STRING },    //Он содержит пункты "Открыть", "Сохранить", разделительную полосу и "Выход"
    {L"Сохранить", MF_STRING },
    {L"", MF_SEPARATOR },
    {L"Выход", MF_STRING }
  },
  1000,                          //ID должен быть уникальным у каждого меню и подменю, а также быть не меньше 1000
  L"Файл"                        //А называться главный элемент будет "Файл"
);
mainMenu->AddSubMenu(            //Добавим еще одно меню с выпадающим списком Сортировка
  {
    {L"По производителю 🠕", MF_STRING },
    {L"По производителю 🠗", MF_STRING },
    {L"По частоте 🠕", MF_STRING },
    {L"По частоте 🠗", MF_STRING }
  },
  2000,
  L"Сортировка"
);

Привяжем меню к окну:

mainMenu->Register(*wnd);

Повторим операцию с контекстным меню:

listContext = new Menu(L"List context menu", true, 0);  //True говорит нам о том, что это всплывающее меню, а не постоянное

listContext->AddItems(
  {
    {L"Добавить", MF_STRING},
    {L"Изменить", MF_STRING},
    {L"Удалить", MF_STRING},
  },
  3000
);

//Это меню привязывать не нужно, потому что оно является контекстным

Обработка сообщений

Опишем базовый случай выхода из приложения:

int MainCallback(CallbackArgs args)
{
  if(Closing(args)) //Если мы закрываем окно:
    Quit();         //Полностью выходим из программы
  
  return 1;
}

Проверим, было ли вызвано контекстное меню у списка (щелчок правой кнопкой мыши по области списка):

if (list->IsContextMenu(args))
{
  //Do something
}

Если было вызвано, то отображаем контекстное меню и сохраняем ID выбранного пункта:

auto val = listContext->Track(list->Get(), args); //Показываем меню в области list и сохраняем ID выбранной опции в val

Далее мы можем проверить это значение в if:

if (val == listContext->GetIDByMenuIndex(0, 0)) //Проверит, выбран ли первый элемент меню (так как в нашем меню есть только одно подменю с несколькими 
{                                               //элементами, то первый 0 указывает на первое подменю, а второй 0 указывает на первый элемент подменю
  MessageBox(wnd->Get(), L"Option A", L"Notify", MB_OK);
}
if (val == listContext->GetIDByMenuIndex(0, 1)) //Проверка на второй элемент
{
  MessageBox(wnd->Get(), L"Option B", L"Notify", MB_OK);
}
if (val == listContext->GetIDByMenuIndex(0, 2)) //Проверка на третий элемент
{
  MessageBox(wnd->Get(), L"Option С", L"Notify", MB_OK);
}

Теперь за проверкой на контекстное меню, проверим был ли нажат пункт "Выход" основного меню:

if (mainMenu->IsClicked(args, 0, 3)) //Если был нажат четвертый пункт первого подменю (третий пункт - это разделительная полоса):
  Quit();                            //Выходим из программы

Готовый код

#pragma once
#include "libs.hpp"

wstring szMainClass = L"MainClass";
wstring szTitle = L"Laba 5";

Window* wnd;

ListBox* list;

Menu* mainMenu;
Menu* listContext;

int MainCallback(CallbackArgs);

int _main_(MainArgs args)
{
  wnd = new Window(szTitle, &szMainClass, args);

  wnd->CreateDefaultWindow(NULL, MainCallback);
  wnd->SetBackgroundColor(RGB(100, 100, 100));

  list = new ListBox(L"", wnd, args.hInstance, false);
  list->Create(BORDER, { 10, 10 }, { 500, 500 });

  mainMenu = new Menu(L"Main", false, 500);

  mainMenu->AddSubMenu(
    {
      {L"Открыть", MF_STRING },
      {L"Сохранить", MF_STRING },
      {L"", MF_SEPARATOR },
      {L"Выход", MF_STRING }
    },
    1000,
    L"Файл"
  );
  mainMenu->AddSubMenu(
    {
      {L"По производителю 🠕", MF_STRING },
      {L"По производителю 🠗", MF_STRING },
      {L"По частоте 🠕", MF_STRING },
      {L"По частоте 🠗", MF_STRING }
    },
    2000,
    L"Сортировка"
  );

  mainMenu->Register(*wnd);

  listContext = new Menu(L"List context menu", true, 0);

  listContext->AddItems(
    {
      {L"Добавить", MF_STRING},
      {L"Изменить", MF_STRING},
      {L"Удалить", MF_STRING},
    },
    3000
  );

  return 0;
}

int MainCallback(CallbackArgs args)
{
  if (Closing(args))
    Quit();

  if (list->IsContextMenu(args))
  {
    auto val = listContext->Track(list->Get(), args);

    if (val == listContext->GetIDByMenuIndex(0, 0))
      MessageBox(wnd->Get(), L"Option B", L"Notify", MB_OK);

    if (val == listContext->GetIDByMenuIndex(0, 1))
      MessageBox(wnd->Get(), L"Option B", L"Notify", MB_OK);

    if (val == listContext->GetIDByMenuIndex(0, 2))
      MessageBox(wnd->Get(), L"Option C", L"Notify", MB_OK);

    return 0;
  }

  if (mainMenu->IsClicked(args, 0, 3))
    Quit();

  return 1;
}

Вверх

OpenGL Initialization

Первоначальная настройка

Создадим окно приложения, используя базовый код:

#pragma once
#include "libs.hpp"

wstring szMainClass = L"MainClass";
wstring szTitle = L"Title";

Window* wnd;

int MainCallback(CallbackArgs);

int _main_(MainArgs args)
{
  wnd = new Window(szTitle, &szMainClass, args);
  wnd->CreateCustomWindow(0, GL_WINDOW, { 10, 10 }, { 800, 600 }, NULL, NULL, NULL, MainCallback);

return 0;
}

int MainCallback(CallbackArgs args)
{
  if (Closing(args))
  {
    Quit();
  }
  return 1;
}

За инициализацию и настройку OpenGL отвечает класс GLContext, создадим указатель на данный класс в глобальных переменных:

GLContext* context;

Создадим функцию-отрисовщик. Данная функция будет осуществлять всю необходимую отрисовку объектов, которые мы создадим:

void MainRenderer()
{
  context->ClearBuffers(0); //обязательно очищаем все буферы перед каждой последующей отрисовкой
  glLoadIdentity(); //сбрасываем также матрицу обзора.

  //вот тут мы будем рисовать

  if(context)                       //эти две строчки будут упрощены в будущем (Если существует контекст)
    SwapBuffers(context->GetHDC()); //(Мы меняем второй буфер на котором рисовали на главный (отображаем нарисованное))
}

Создадим экземпляр контекста в _main_, после создания окна:

wnd->CreateCustomWindow(0, GL_WINDOW, { 10, 10 }, { 800, 600 }, NULL, NULL, NULL, MainCallback); // строчка с нашим созданием окна
context = new GLContext(wnd, NULL, 16, 16, MainRenderer); //наше главное окно, флаги (NULL), число битов цвета (16), число битов глубины (16)

Для того, что бы иметь возможность обрабатывать каждый кадр при отрисовке, добавим функцию Update():

void Update()
{
  context->Render() //будем вызывать отрисовку нашей графики каждый кадр
  //здесь будет наша логика, данная функция будет вызываться каждый кадр.
}

Осталось подключить `Update` к центральному обработчику сообщений. Для этого, в `_main_` добавим следующую строку:
```c++
UpdateCallback.push_back(Update); //где Update - название нашей функции

Отрисовка базовых фигур

Отобразим квадрат в центре экрана, для этого воспользуемся функцией главного отрисовщика. Для рисования многоугольников предусмотрена функция DrawPolygon, для рисования квадратов по 4 точки каждый - DrawQuads, для рисования треугольников по 3 точки каждый - DrawTriangles, у всех них одинаковые параметры.

Что бы правильно выставить координаты при рисовании объекта, важно понимать, как они отсчитываются. Начало координат располагается в центре экрана, вверх уходит ось y в положительном направлении до 1, вниз до -1. Вправо уходит ось x до 1, влево до -1, то же самое и с осью z.

Для рисования квадрата воспользуемся функцией главного отрисовщика, а так же методом DrawQuads:

void MainRenderer()
{
  context->ClearBuffers(0); //очищаем экран перед отрисовкой
  glLoadIdentity(); //сбрасывает вид 
  
  glTranslatef(0, 0, -3.5); // немного отодвинем квадрат вглубь экрана (-3.5 по z координате)

  DrawQuads( //первым аргументом является вектор всех точек
    {
      {-0.5, 0.5, 0}, //первая точка верхняя левая
      {0.5, 0.5, 0}, //верхняя правая
      {0.5, -0.5, 0}, //нижняя правая
      {-0.5, -0.5, 0} //левая нижняя
    },
    { 100, 100, 100, 255 } //цвет в формате RGBA (альфа канал в настоящий момент не поддерживается)
  );

  if(context)           //если контекст существует
    SwapBuffers(context->HDC()); //меняем буферы
}

Базовое вращение

Добавим постоянное вращение, для этого создадим глобальную переменную Angle:

int Angle;

Теперь будем увеличивать ее каждый кадр:

void Update()
{
  context->Render();
  Angle+=2;
}

В функции отрисовщике будем изменять вращение:

void MainRenderer()
{
  //тут какой-то код
  glTranslatef(0, 0, zPos - 3.5); //отодвигаем объекты вглубь
  glRotatef(Angle, 1, 1, 1); //вращаем на угол Angle по всем осям одинаково (x = 1, y = 1, z = 1)
  //тут какой-то код
}

Готовый код

#pragma once
#include "libs.hpp"


//Сюда добавляйте свои библиотеки:

wstring szMainClass = L"MainClass";
wstring szTitle = L"Title";

Window* wnd;
GLContext* context;

int Angle, zPos;

int MainCallback(CallbackArgs);
void Update();
void MainRenderer();

int _main_(MainArgs args)
{
  wnd = new Window(szTitle, &szMainClass, args);
  wnd->CreateCustomWindow(0, GL_WINDOW, { 10, 10 }, { 800, 600 }, NULL, NULL, NULL, MainCallback);

  context = new GLContext(wnd, 0, 16, 16, MainRenderer);  //инициализация графики opengl
  context->AddAmbienLight({ 255, 255, 255, 255 }); ///пока не работает 
  glEnable(GL_LIGHT0);

  UpdateCallback.push_back(Update); //чтобы функция вызывалась каждую перерисовку кадра

  return 0;
}

int MainCallback(CallbackArgs args)
{
  if (Closing(args))
  {
    Quit();
  }
  return 1;
}

void Update()
{
  context->Render();
  Angle += 2;
}

void MainRenderer()
{
  context->ClearBuffers(0);
  glLoadIdentity();

  glTranslatef(0, 0, zPos - 3.5);
  glRotatef(Angle, 1, 1, 1);

  DrawQuads(
    {
      {-0.5, 0.5, 0},
      {0.5, 0.5, 0},
      {0.5, -0.5, 0},
      {-0.5, -0.5, 0}
    },
    { 100, 100, 100, 255 }
  );

  if(context)
    SwapBuffers(context->HDC());
}

Вверх

GameObject Tutorial

Класс GameObject предназначен для работы с каждым объектом отдельно. Вы загружаете или рисуете модель, а потом взаимодействуете с ней.

Загрузка модели

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

Создадим глобальную переменную объекта:

GameObject* dObj;

Теперь в _main_ инициализируем объект, загрузим модель и наложим текстуры из файла "D.tga":

dObj = new GameObject();
dObj->LoadMesh("D.obj");
dObj->LoadTexture("Shrek.tga");

Немного переместим, повернем модель, а так же уменьшим модель:

dObj->Rotation.X = 90;
dObj->Transform.Z = -2;
dObj->Transform.Y = -0.5;
dObj->ScaleObject(0.5);

Отрисовка модели

В главном отрисовщике нам необходимо вызвать единственную функцию объекта:

dObj->Draw();

Поворот объекта мышью

Привяжем курсор к окну и заблокируем его в _main_.

Mouse::Link(wnd);
Mouse::LockCursor();
Mouse::HideCursor();

Добавим выход из приложения при нажатии Esc в обработчике сообщений:

if (Closing(args) || Keyboard::GetKeyDown(args, VK_ESCAPE))
{
  Quit();
}

Теперь перед отрисовкой объекта будем менять его угол, в зависимости от разницы в позиции курсора:

dObj->Rotation.Z += Mouse::GetDX();
dObj->Rotation.X += Mouse::GetDY();

Готовый Код

#pragma once
#include "libs.hpp"

wstring szMainClass = L"MainClass";
wstring szTitle = L"Title";

Window* wnd;
GLContext* context;

GameObject *dObj;

int Angle;
float zPos;

int MainCallback(CallbackArgs);
void Update();
void MainRenderer();

int _main_(MainArgs args)
{
    dObj = new GameObject();

    wnd = new Window(szTitle, &szMainClass, args);
    wnd->CreateCustomWindow(0, GL_WINDOW, { 10, 10 }, { 800, 600 }, NULL, NULL, NULL, MainCallback);

    context = new GLContext(wnd, 0, 16, 16, MainRenderer);
    context->AddSmooth();
    context->AddAmbienLight({ 255, 255, 255, 255 });

    UpdateCallback.push_back(Update);

    Mouse::Link(wnd);
    Mouse::LockCursor();
    Mouse::HideCursor();

    dObj->LoadMesh("D.obj");
    dObj->LoadTexture("D.tga");

    dObj->Rotation.X = 90;
    dObj->Transform.Z = -2;
    dObj->Transform.Y = -0.5;
    dObj->ScaleObject(0.5);

    return 0;
}

int MainCallback(CallbackArgs args)
{
    if (Closing(args) || Keyboard::GetKeyDown(args, VK_ESCAPE))
    {
        Quit();
    }

    return 1;
}

void Update()
{
    context->Render();
}

void MainRenderer()
{
    context->ClearBuffers(0);

    dObj->Rotation.Z += Mouse::GetDX();
    dObj->Rotation.X += Mouse::GetDY();
    dObj->Draw();

    if(context)
        SwapBuffers(context->HDC());
}

Вверх