Tutorials - TonSharp/WAPITIS GitHub Wiki
Быстрое перемещение:
Quick Start
- Установите фреймворк согласно инструкции в README.MD на главной странице репозитория;
- Откройте файл main.hpp.
Минимальный набор функций для запуска программы:
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());
}