2D графика на основе WinApi C++

Автор: | 05.02.2018

Шаблон WinAPI приложения
Рисуем треугольник в оконной СК
Логическая система координат
Поворот плоскости
Анимация изображения
Аффинные преобразования треугольника
Контрольные задания
Исходные файлы проекта приложения

Шаблон WinAPI приложения

Для начала нам потребуется шаблон , на основе которого можно будет создавать дальнейшие приложения. Создайте в Visual Studio пустой проект (File → New → Project… → Visual C++ →Win32→Win32 Project → Application Settings → Empty Project→ Finish), создайте там main.cpp файл  (Project → Add New Item…)  и вставьте туда программный код (см. ниже).

При запуске программы  должно появиться окно, залитое синим цветом.

Проект содержит файл main.cpp, ознакомьтесь с программным кодом.

#include <windows.h>
const double PI = 3.141592653;
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
char szClassName[] = "CG_WAPI_Template";

////////////////////////////////////////////////////////////////////////////////////////////////////
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
 HWND hWnd; 
 MSG lpMsg;
 WNDCLASS wc;

// Заполняем структуру класса окна
 wc.style = CS_HREDRAW | CS_VREDRAW;
 wc.lpfnWndProc = WndProc;
 wc.cbClsExtra = 0;
 wc.cbWndExtra = 0;
 wc.hInstance = hInstance;
 wc.hIcon = NULL; 
 wc.hCursor = LoadCursor(NULL, IDC_ARROW);
 wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
 wc.lpszMenuName = NULL;
 wc.lpszClassName = (LPCWSTR)szClassName;
 
 // Регистрируем класс окна
 if (!RegisterClass(&wc))
 {
 MessageBox(NULL, (LPCWSTR)"Не могу зарегистрировать класс окна!", (LPCWSTR)"Ошибка", MB_OK);
 return 0;
 }

// Создаем основное окно приложения
 hWnd = CreateWindow(
 (LPCWSTR)szClassName, // Имя класса 
 L"Шаблон WinAPI приложения", // Текст заголовка
 WS_OVERLAPPEDWINDOW, // Стиль окна 
 50, 50, // Позиция левого верхнего угла 
 600, 600, // Ширина и высота окна 
 (HWND) NULL, // Указатель на родительское окно NULL 
 (HMENU) NULL, // Используется меню класса окна 
 (HINSTANCE)hInstance, // Указатель на текущее приложение
 NULL ); // Передается в качестве lParam в событие WM_CREATE

if (!hWnd)
 {
 MessageBox(NULL, (LPCWSTR)"Не удается создать главное окно!", (LPCWSTR)"Ошибка", MB_OK);
 return 0;
 }

// Показываем наше окно
 ShowWindow(hWnd, nCmdShow);
 UpdateWindow(hWnd);

// Выполняем цикл обработки сообщений до закрытия приложения
 while (GetMessage(&lpMsg, NULL, 0, 0)) 
 {
 TranslateMessage(&lpMsg);
 DispatchMessage(&lpMsg);
 }
 return (lpMsg.wParam);
}

////////////////////////////////////////////////////////////////////////////////////////////////////
LRESULT CALLBACK WndProc(HWND hWnd, UINT messg, WPARAM wParam, LPARAM lParam)
{
 PAINTSTRUCT ps;
 RECT Rect;
 HDC hdc, hCmpDC;
 HBITMAP hBmp;

switch (messg)
 {
 case WM_PAINT:
 GetClientRect(hWnd, &Rect);
 hdc = BeginPaint(hWnd, &ps);

// Создание теневого контекста для двойной буферизации
 hCmpDC = CreateCompatibleDC(hdc);
 hBmp = CreateCompatibleBitmap(hdc, Rect.right - Rect.left,
 Rect.bottom - Rect.top);
 SelectObject(hCmpDC, hBmp);

// Закраска фоновым цветом
 LOGBRUSH br;
 br.lbStyle = BS_SOLID;
 br.lbColor = 0xEECCCC;
 HBRUSH brush;
 brush = CreateBrushIndirect(&br);
 FillRect(hCmpDC, &Rect, brush);
 DeleteObject(brush);

// Здесь рисуем на контексте hCmpDC

// Копируем изображение из теневого контекста на экран
 SetStretchBltMode(hdc, COLORONCOLOR);
 BitBlt(hdc, 0, 0, Rect.right - Rect.left, Rect.bottom - Rect.top,
 hCmpDC, 0, 0, SRCCOPY);

// Удаляем ненужные системные объекты
 DeleteDC(hCmpDC);
 DeleteObject(hBmp);
 hCmpDC = NULL;

EndPaint(hWnd, &ps);
 break;

case WM_DESTROY:
 PostQuitMessage(0);
 break;

default:
 return (DefWindowProc(hWnd, messg, wParam, lParam));
 }
 return (0);
}

Директива препроцессора  #include <windows.h> открывает доступ к тысячам описаний констант, структур, типов данных и  функций Windows.

WinAPI приложение является в своей основе процедурным приложением и содержит два основных модуля – функции WinMain и WndProc.

Функция WinMain составляет основу любого WinAPI приложения. Она служит как бы точкой входа в приложение и отвечает за следующее:

  • начальную инициализацию приложения;
  • создание и регистрацию объекта класса окна приложения;
  • создание и инициализацию цикла обработки событий.

Обработанное в бесконечном цикле событие переправляется (опосредовано через Windows) оконной функции WndProc.  События в ней идентифицируются именем константы (WM_PAINT, WM_DESTROY  и др.). Рисование осуществляется при помощи объектов типа HDC (дескриптор контекста устройства).

Рисуем треугольник в оконной СК

Нарисуем треугольник, задав координаты его вершин в оконной (физической) системе координат. Начало этой системы координат располагается в левом верхнем углу экрана. Ось X направлена слева направо, ось Y – сверху вниз. В качестве единицы длины в этой системе координат используется пиксел.

Для этого создадим новый файл draw.cpp:

#include <windows.h>

void Draw(HDC hdc){
MoveToEx(hdc, 100, 100, NULL);
LineTo(hdc, 150, 150);
LineTo(hdc, 50, 200);
LineTo(hdc, 100, 100);
}

И файл draw.h:

void Draw(HDC hdc);

В основном файле добавим в начало:

#include "draw.h"

А между заполнением области фоновым цветом и выводом изображения на основной контекст, появляется вызов процедуры рисования:

// Закраска фоновым цветом
//…
DeleteObject(brush);
// Рисование
Draw(hCmpDC);
// Вывод на экран 
SetStretchBltMode(hdc, COLORONCOLOR);
//…

Рисуем мы на контексте-двойнике, а уже потом перекидываем изображение на основной контекст.

Логическая система координат

Оконная СК неудобна для пользователя из-за непривычного расположения осей (ось y направлена вниз), задания координат в пикселях и др. Устраним  эти недостатки, используя логическую СК, в которой:

  • центр координат перенесен из левого верхнего угла в центр экрана;
  • направление оси Y меняется на противоположное;
  • координаты (от -1 до +1) соотнесены с шириной и высотой окна;
  • введен отступ от края окна (margin).

Преобразования из логической системы координат в оконную осуществляются при помощи зависимостей:

X_Window = MARGIN + (1.0/2)*(X_Log + 1)*(Width - 2 * MARGIN);
Y_Window = MARGIN + (-1.0/2)*(Y_Log - 1)*(Height - 2 * MARGIN);

Ниже приводится новый код модуля draw.cpp:

#include <windows.h>

int Width, Height;
const int MARGIN = 10;

void SetWindowSize(int _Width, int _Height){
Width = _Width;
Height = _Height;
}

int Tx(double X_Log){
int X_Window;
X_Window = MARGIN + (1.0 / 2) * (X_Log + 1) * (Width - 2 * MARGIN);
return X_Window;
}

int Ty(double Y_Log){
int Y_Window;
Y_Window = MARGIN +(-1.0 / 2)*(Y_Log - 1)*(Height - 2* MARGIN);
return Y_Window;
}

void Draw(HDC hdc){
MoveToEx(hdc, Tx(0.0), Ty(0.5), NULL);
LineTo(hdc, Tx(0.5), Ty(0.0));
LineTo(hdc, Tx(-0.5), Ty(-0.5));
LineTo(hdc, Tx(0.0), Ty(0.5));
}

В draw.h добавляется объявление новой функции:

void Draw(HDC hdc);
void SetWindowSize(int _Width, int _Height);

В модуль main.cpp добавляем обработчик события WM_SIZE и WM_ERASEBKGND:

case WM_SIZE:
GetClientRect(hWnd, &Rect);
SetWindowSize(Rect.right - Rect.left, Rect.bottom - Rect.top);
break;
case WM_ERASEBKGND:
return 1;
break;

Теперь, при изменении размеров окна наш треугольник подстраивается под новые размеры.  При изменении размеров окна происходит перерисовка изображения, поскольку событие  WM_SIZE  автоматически вызывает событие WM_PAINT.

Контрольные вопросы

  1. Чем обусловлен переход от оконной СК к логической?
  2. Какие задачи решают функции Tx(0.0), Ty(0.5)?
  3. Какие задачи решает функция SetWindowSize?

Поворот плоскости

Алгоритмическая часть

Мы нарисовали треугольник в логической системе координат. Теперь обеспечим возможность поворота треугольника при каждом нажатии клавиш ← или →.  Для пересчета положения каждой вершины треугольника используем преобразование координат при повороте.

Рассмотрим произвольный вектор r, задающий некоторую точку в системе координат ху:

При повороте на угол φ координаты точки запишутся в виде:

Выделим из этих уравнений 2 пары зависимостей – для пересчета координат точек в новой СК (1.1) и  для  обновления значений косинуса и синуса угла (1.2):

Для решения задачи пошагового вращения треугольника необходимо сохранять параметры предыдущего положения треугольника. В качестве таких параметров могут использоваться предыдущие координаты, либо суммарный угол поворота. Будем сохранять угловые  характеристики – синусы  и косинусы, поскольку именно они используются в  выражениях (1.1 и 1.2).

Программная реализация

В программе модифицированы все модули  (main.cpp,  draw.h, draw.cpp) и добавлены новые (geometry.h,  matrix.h, matrix.cpp).  В файле main.cpp добавляются обработчики  WM_CREATE и  WM_KEYPRESSED, клавиши ← и → идентифицируются в программе константами VK_LEFT и VK_RIGHT, начальный поворот плоскости инициализируется вызовом функции InitRotation, текущий поворот – функцией AddRotation. Перерисовка изображения (вызов события WM_PAINT) обеспечивается функцией  InvalidateRect.

case WM_CREATE:
InitRotation();
break;
case WM_KEYDOWN:
int KeyPressed;
KeyPressed = int(wParam);
if (KeyPressed == VK_RIGHT)
{
 AddRotation(-PI / 10);
}
if (KeyPressed == VK_LEFT)
{
AddRotation(PI / 10);
}
InvalidateRect(hWnd, NULL, FALSE); //вызов события WM_PAINT
break;

В файлы draw.h, draw.cpp и main.cpp вносим изменения, которые обеспечивают рисование треугольника в зависимости от текущего значения угла. В определение функций  InitRotation и AddRotation входит вызов функции  SetRotationMatrix, которая осуществляет инициализацию массива из 4-х тригонометрических характеристик угла. Переменная массива объявляется типом  Matrix. Обновление массива реализует функция MultiplyMatrices. В функции Draw каждая вершина треугольника объявляется типом _Point. При каждом запуске функции  Draw вершины инициализируются начальными координатами точек, затем функция  ApplyMatrixtoPoint обеспечивает пересчет координат в соответствии с текущими значениями массива  тригонометрических характеристик угла. Поворот точек треугольника реализуется в логической системе координат, затем осуществляется преобразование в оконные координаты и  рисование сторон треугольника.

draw.h

void Draw(HDC hdc);
void SetWindowSize(int _Width, int _Height);
void InitRotation();
void AddRotation(double alpha);

draw.cpp

#include <windows.h>
#include "geometry.h"
#include "matrix.h"
int Width, Height;
Matrix current_rot;
const int MARGIN = 10;
void InitRotation()
{
SetRotationMatrix(0.0, current_rot);
}
void AddRotation(double alpha)
{
Matrix additional_rot;
SetRotationMatrix(alpha, additional_rot);
MultiplyMatrices(current_rot, current_rot, additional_rot);
}
void SetWindowSize(int _Width, int _Height)
{
Width = _Width;
Height = _Height;
}
_Point T(_Point point)
{
_Point TPoint;
TPoint.x = MARGIN + (1.0 / 2)*(point.x + 1)*(Width - 2 * MARGIN);
TPoint.y = MARGIN +(-1.0 / 2)*(point.y - 1)*(Height - 2 * MARGIN);
return TPoint;
}
void Draw(HDC hdc)
{
_Point triangle[3];
triangle[0].x = 0.0;
triangle[0].y = 0.5;
triangle[1].x = 0.5;
triangle[1].y = 0.0;
triangle[2].x = -0.5;
triangle[2].y = -0.5;
for (int i = 0; i < 3; i++)
{
ApplyMatrixtoPoint(current_rot, triangle[i]);
 triangle[i] = T(triangle[i]);
}
for (int i = 0; i <= 3; i++)
{
int j = i % 3;
if (i == 0)
{
MoveToEx(hdc, triangle[j].x, triangle[j].y, NULL);
}
else
{
LineTo(hdc, triangle[j].x, triangle[j].y);
}
}
}

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

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

geometry.h

#ifndef _POINT
      struct _Point
      {
            double x, y;
      };
      #define _POINT
#endif

matrix.h

#include "geometry.h"
typedef double Matrix[4]; // typedef дает новое имя Matrix[4]
void SetRotationMatrix(double alpha, Matrix &matrix);
void MultiplyMatrices(Matrix &dest, Matrix &left, Matrix &right);
void ApplyMatrixtoPoint(Matrix rot, _Point &point);

matrix.cpp

#include <math.h>
#include <memory.h>
#include "matrix.h"
#include "geometry.h"
void SetRotationMatrix(double alpha, Matrix &matrix)
{
      matrix[0] = cos(alpha);
      matrix[1] = -sin(alpha);
      matrix[2] = sin(alpha);
      matrix[3] = cos(alpha);
}

void ApplyMatrixtoPoint(Matrix rot, _Point &point)
{
      double _x, _y;
      _x = point.x;
      _y = point.y;
      point.x = _x * rot[0] + _y * rot[1];
      point.y = _x * rot[2] + _y * rot[3];
}
void MultiplyMatrices(Matrix &dest, Matrix &left, Matrix &right) 
{        Matrix _dest;       
       _dest[0] = left[0] * right[0] + left[1] * right[2];
       _dest[1] = left[0] * right[1] + left[1] * right[3];
       _dest[2] = left[2] * right[0] + left[3] * right[2];
       _dest[3] = left[2] * right[1] + left[3] * right[3];
       memcpy(dest, _dest, sizeof(Matrix)); }

Контрольные  задания: 

  1. Согласно системы уравнений (1.2)  sin и cos суммы 2-х углов, определяются по двум аргументам – sin и cos этих углов. В функции MultiplyMatrices(Matrix &dest, Matrix &left, Matrix &right), которая реализует в программе систему уравнений (1.2),  используются 3 параметра (один параметр лишний).  Модифицируйте функцию MultiplyMatrices, чтобы она принимала 2 параметра. Упрощенный аналог решения этого задания: Fun (&A, B) {A = A + B}
  2. Внесите изменения в программу, которые обеспечат поворот треугольника на угол (PI/10) при нажатии на левую кнопку мышки (событие WM_LBUTTONDOWN) и поворот на угол (-PI/10) при нажатии на правую кнопку (событие WM_RBUTTONDOWN).

Анимация изображения

Мы уже обеспечили возможность поворота треугольника при каждом нажатии клавиш ← или →.  Теперь поставим задачу обеспечения  непрерывного вращения треугольника. Для реализации задачи необходимо обеспечить изменение угла поворота треугольника с последующим обновлением изображения. Причем, это должно выполняться в цикле.

Рассмотрим и оценим  два способа циклического обновления изображения:

  1. Бесконечный цикл обработки сообщений (в функции WinMain);
  2. Цикл, осуществляемый через обработку события  WM_TIMER.

В первом случае в бесконечный цикл помещаются 2 функции:

AddRotation(PI / 10);
InvalidateRect(hWnd, NULL, FALSE);  //вызов события WM_PAINT

При запуске приложения мы получим изменение угла поворота треугольника со скоростью, которая приблизительно соответствует скорости обработки сообщений. При этом отрегулировать скорость вращения можно за счет изменения входного параметра функции AddRotation.

Если для анимации использовать бесконечный цикл обработки сообщений, то добиться точной скорости обновления изображения сложно. В WinAPI для работы со временем обычно используют сообщение WM_TIMER:

case WM_TIMER:     // обработка сообщения WM_TIMER
AddRotation(PI / 10);
InvalidateRect(hWnd, NULL, FALSE);  //вызов события WM_PAINT
break;

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

SetTimer(hWnd,TIMER_2SEC,2000,NULL);// интервал две секунды

Создание таймера необходимо предусматривать до его использования, например – в событии WM_CREATE. Кроме этого необходимо в заголовке файла определить идентификатор таймера:

const unsigned int TIMER_2SEC = 1; // идентификатор таймера

Уничтожение таймера предусматривается, когда необходимо остановить анимацию:

KillTimer (hWnd,TIMER_2SEC);  // уничтожение таймера

 

Контрольные вопросы и задания

  1. Почему нецелесообразно использовать бесконечный цикл обработки сообщений для анимации изображения?
  2. Обеспечьте при  запуске программы вращение треугольника как стрелки секундомера.
  3. Обеспечьте возможность запуска вращения треугольника при нажатии на клавишу P (пуск) и остановку – при нажатии на  клавишу S (stop). Пример оформления события нажатия на клавишу приводится ниже:
case WM_KEYDOWN: 
int KeyPressed;
KeyPressed = int(wParam);
if (KeyPressed == int('P'))
{ ...
}
break;

Аффинные преобразования треугольника

Рассматриваемое ниже приложение  базируется на приложении, рассмотренном выше. Отличие лишь в том, что вместо матрицы поворота используется матрица аффинных преобразований в однородных координатах. При нажатии на клавишу «T» или перемещении мыши (при нажатой клавише CTRL)  матрица инициализируется конкретными значениями, задавая соответствующие преобразования треугольника.

Учитывая большое количество изменений по сравнению с приложением   поворот плоскости мышью, предлагается  создать новое приложение с соответствующими файлами:

Header Files

action.h
engine.h
geometry.h
matrix.h
vec.h
viewport.h

Source Files

main.cpp
action.cpp
engine.cpp
matrix.cpp
viewport.cpp

В файлах описаны классы, в которых сгруппированы функции и переменные для комплексного решения задачи аффинных преобразований на плоскости.

Класс Engine объединяет  функцию формирования изображения  (Draw) и объекты классов Action и Viewport,  обеспечивающие действия по перестройке изображения, вызванной событиями нажатия на кнопки, перемещения мышки и изменения размеров окна.

Класс Action объединяет  действия и данные, связанные с аффинными преобразованиями. Функция Rotate (Translate) устанавливает преобразования вращения (перемещения), опираясь на текущее и предыдущее положения мыши. Функция InitAction запоминает координаты курсора в объекте класса vec. Эти функции вызываются при событиях нажатия кнопки мышки и перемещения курсора. Функция Transform устанавливает преобразования через инициализацию коэффициентов матрицы

Класс Viewport объединяет  действия и данные, связанные с изменением размеров окна, Кроме этого, в класс включена функция T, которая выполняет преобразование точек фигуры из экранных координат экрана, в логические координаты, а также, функция T_inv , которая преобразует координаты курсора из экранных координат в логические.  Функция  T_inv отсутствовала в приложении, рассмотренном ранее, поскольку в нем реализовывалось лишь преобразование вращения, а значение угла поворота не зависело от СК. В этом приложении с помощью курсора задается и преобразование перемещения, а оно зависит от используемой СК.

В класс Matrix была добавлена функция SetTranslationMatrix, в которой непосредственно инициализируются коэффициенты матрицы преобразований.
В классе  Vec переопределяются операции с векторами. Эти операции используются в программе для определения преобразования перемещения,  опираясь на текущее и предыдущее положения курсора при перемещении мыши.

Контрольные задания

Задание 1. Определите коэффициенты матрицы элементарных аффинных преобразований в соответствии с вариантом задания

В каждый вариант задания включены по 3 преобразования

Пример оформления  задания показан ниже на рисунке

Выполнить тестовые запуски приложения для проверки соответствия матриц заданным преобразованиям

Коэффициенты матрицы преобразования инициализируются в функции Matrix::SetTranslationMatrix1, которая определена в файле   matrix.cpp.  Преобразование осуществляется  при нажатии клавиши T.

Задание 2.   Определить произведение матриц AB,   (AB)C и C(AB)   и протестировать композицию элементарных преобразований запуском приложения. Преобразование осуществлять нажатием клавиши T.

Задание 3.   Определить последовательность элементарных аффинных преобразований (преобразований не более 3-х), которые от начального положения треугольника приведут к заданному результату.

Варианты задания:

Для тестирования добавьте в  программу дополнительные реакции на нажатие клавиш 1, 2 и 3, которые обеспечивают соответствующее элементарное преобразование:

case WM_KEYDOWN:
int KeyPressed;
KeyPressed = int(wParam);
if (KeyPressed == int('T'))   { action->Transform (); }
if (KeyPressed == int('1'))   { action->Transform_1(); }          
if (KeyPressed == int('2'))   { action->Transform_2(); }
if (KeyPressed == int('3'))   { action->Transform_3(); }
InvalidateRect(hWnd, NULL, FALSE);
break;

Объявления и определения функций action->Transform вместе с входящими функциями описываются по аналогии с описанием действий при нажатии на клавишу “T”.

Задание 4.   Добавьте в приложение программный код, обеспечивающий возможность переключение преобразований из глобальной СК в локальную СК. Для тестирования  приложения  переместите треугольник и поверните его при помощи мышки на угол 135 градусов. Если включена глобальная СК, вращение будет происходить относительно центра окна, если локальная СК – относительно вершины прямого угла.

В соответствии с алгоритмом для выполнения  этого задания необходимо поменять местами  текущую матрицу и матрицу, переданную в качестве параметра для функции MultiplyMatrices(Matrix &right).

Прежде всего, необходимо создать переключатель систем  координат. В файле main.cpp  введем булеву переменную-флажок “w”, которая инициализируется при нажатии на клавишу “W” .  Также, в файле matrix.cpp определим функцию SetW, которая передает ему значение переменной “w”  из файла main.cpp.

main.cpp

LRESULT CALLBACK WndProc(....)
{
     static bool w /*= false*/;
…
case WM_CREATE:
…
         w = true;
         SetW(w);
        break;
case WM_KEYDOWN:
            int KeyPressed;
            KeyPressed = int(wParam);
             if (KeyPressed == int('W'))
            {
              w = (w == true) ? false : true;    
                SetW(w); 
            }

файл matrix.h

...
#define _MATRIX
#endif
void SetW(bool _w);

файл matrix.cpp

#include <math.h>
#include <memory.h>
#include "matrix.h"
#include "geometry.h"
bool w;
void SetW(bool _w){  w = _w;}
...
void Matrix::MultiplyMatrices(Matrix &right){
    double temp[3][3];
    double val;
    memcpy(temp, data, sizeof(data));
    for (int i = 0; i < 3; i++){
        for (int j = 0; j < 3; j++){
            val = 0;
            for (int v = 0; v < 3; v++){
    //val += temp[i][v] * right.data[v][j];  //комментируем
       if (w == true)
         val += temp[i][v] * right.data[v][j];  // глобальная СК
    else  
        val +=  right.data[i][v] * temp[v][j];  // локальная СК  
            }
            data[i][j] = val;
        }
    }
}

Исходные файлы проекта приложения

Header Files

action.h

#include "matrix.h"
#include "vec.h"

class Action {
public:
 vec old_mouse;
 Matrix CurrentMatrix;
 void InitAction(double x, double y);
 void Rotate(double x, double y);
 void Translate(double x, double y);
 void Transform();
};

engine.h

#include "action.h"
#include "viewport.h"
class Engine{
 //Matrix current_rot;
 Action *action;
public:
 Viewport viewport;
 void Draw(HDC hdc);
 void SetAction(Action *_action);
};

geometry.h

#ifndef _POINT 
 struct _Point
 {
 double x, y;
 };
 #define _POINT
#endif

matrix.h

#include "geometry.h"
#ifndef _MATRIX
class Matrix{
 double data[3][3];
public:
 Matrix();
 void SetUnit();
 void SetRotationMatrix(double alpha);
 void SetRotationMatrixbySinCos(double sinalpha, double cosalpha);
 void SetTranslationMatrix(double tx, double ty);
 void SetTranslationMatrix1();
 void MultiplyMatrices(Matrix &right);
 void ApplyMatrixtoPoint(_Point &point);
};
#define _MATRIX
#endif

vec.h

#include <math.h>
typedef double vec_float;
class vec
{
public:
 vec_float x, y;
 vec() {}
 vec(vec_float xx, vec_float yy)
 {
 x = xx;
 y = yy;
 }
 vec(const vec& vector){
 x = vector.x;
 y = vector.y;
 }
 inline void set(vec_float xx, vec_float yy)
 {
 x = xx;
 y = yy;
 }
 inline vec operator + (vec t) // сложение
 {
 return vec(x + t.x, y + t.y);
 }
 inline vec operator - (vec t) // вычитание
 {
 return vec(x - t.x, y - t.y);
 }
 inline vec operator * (vec_float t) // произведение на число
 {
 return vec(x * t, y * t);
 }
 inline vec_float operator * (vec t) // скалярное произведение
 {
 return x * t.x + y * t.y;
 }
 inline vec_float operator ^ (vec t) // длина результата векторного произведения с учетом направления
 {
 return x * t.y - y * t.x;
 }
 inline vec_float length() // длина вектора
 {
 return sqrt(x * x + y * y);
 //return x * x + y * y;
 }
 inline vec unit() // нормализация вектора
 {
 vec_float l = length();
 if (l == 0.0f) return vec(0.0f, 0.0f);
 return vec(x / l, y / l);
 }
 inline bool zero() // определяет нулевой ли вектор
 {
 return (x == 0.0f) && (y == 0.0f);
 }
 inline bool equals(vec t) // проверяет вектора на точное совпадение
 {
 return (x == t.x) && (y == t.y);
 }
};

viewport.h

#include "geometry.h"
#ifndef _VIEWPORT
class Viewport{
 int Margin;
 int Height, Width;
public:
 Viewport();
 void SetWindowSize(int _Width, int _Height);
 _Point T(_Point point);
 _Point T_inv(_Point point);
 void SetMargin(int _Margin = 10);
};
#define _VIEWPORT
#endif

Source Files

main.cpp

#include <windows.h>
#include "engine.h"
const double PI = 3.141592653;
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
char szClassName[] = "CG6";
char szWindowCaption[] = "CG #6 Mouse Tracking and Rotation";
////////////////////////////////////////////////////////////////////////////////////////////////////
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
 HWND hWnd; 
 MSG lpMsg;
 WNDCLASS wc;
 // Заполняем структуру класса окна
 wc.style = CS_HREDRAW | CS_VREDRAW;
 wc.lpfnWndProc = WndProc;
 wc.cbClsExtra = 0;
 wc.cbWndExtra = 0;
 wc.hInstance = hInstance;
 wc.hIcon = NULL;
 wc.hCursor = LoadCursor(NULL, IDC_ARROW);
 wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
 wc.lpszMenuName = NULL;
 wc.lpszClassName = (LPCWSTR)szClassName;

// Регистрируем класс окна
 if (!RegisterClass(&wc))
 {
 MessageBox(NULL, L"Cannot register class", L"Error", MB_OK);
 return 0;
 }
 // Создаем основное окно приложения
 hWnd = CreateWindow(
 (LPCWSTR)szClassName, // Имя класса 
 L"Шаблон WinAPI приложения", // Текст заголовка 
 WS_OVERLAPPEDWINDOW, // Стиль окна 
 50, 50, // Позиция левого верхнего угла 
 600, 600, // Ширина и высота окна 
 (HWND) NULL, // Указатель на родительское окно NULL 
 (HMENU) NULL, // Используется меню класса окна 
 (HINSTANCE)hInstance, // Указатель на текущее приложение
 NULL ); // Передается в качестве lParam в событие WM_CREATE
 if (!hWnd)
 {
 MessageBox(NULL, L"Cannot create main window", L"Error", MB_OK);
 return 0;
 }
// Показываем наше окно
 ShowWindow(hWnd, nCmdShow);
 UpdateWindow(hWnd);
// Выполняем цикл обработки сообщений до закрытия приложения
 while (GetMessage(&lpMsg, NULL, 0, 0)) {
 TranslateMessage(&lpMsg);
 DispatchMessage(&lpMsg);
 }
return (lpMsg.wParam);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
LRESULT CALLBACK WndProc(HWND hWnd, UINT messg, WPARAM wParam, LPARAM lParam)
{
 PAINTSTRUCT ps;
 static RECT Rect;
 HDC hdc, hCmpDC;
 HBITMAP hBmp;
 static Action *action;
 static Engine *engine;
//static bool x = true;
switch (messg)
 {
 case WM_CREATE:
 engine = new Engine(); 
 action = new Action(); //вызыв. констр. Matrix и иниц. матрица (станов. единичной)
 engine->SetAction(action); // иниц.закрытой перем engine->action = action;
 break;
 case WM_PAINT:
//x = x;
 GetClientRect(hWnd, &Rect);
 hdc = BeginPaint(hWnd, &ps);
 SetBkColor(hdc, 0xEECCCC);
 // Создание нового контекста для двойной буфферизации
 hCmpDC = CreateCompatibleDC(hdc);
 hBmp = CreateCompatibleBitmap(hdc, Rect.right - Rect.left,
 Rect.bottom - Rect.top);
 SelectObject(hCmpDC, hBmp);
 // Закраска фоновым цветом
 LOGBRUSH br;
 br.lbStyle = BS_SOLID;
 br.lbColor = 0xEECCCC;
 HBRUSH brush;
 brush = CreateBrushIndirect(&br);
 FillRect(hCmpDC, &Rect, brush);
 DeleteObject(brush);
 // Рисование
 engine->Draw(hCmpDC);
 // Вывод на экран
 SetStretchBltMode(hdc, COLORONCOLOR);
 BitBlt(hdc, 0, 0, Rect.right - Rect.left, Rect.bottom - Rect.top,
 hCmpDC, 0, 0, SRCCOPY);
 DeleteDC(hCmpDC);
 DeleteObject(hBmp);
 hCmpDC = NULL;
 EndPaint(hWnd, &ps);
 break;
case WM_SIZE: // какое раньше событие WM_PAINT или WM_SIZE
 GetClientRect(hWnd, &Rect);
 engine->viewport.SetWindowSize(Rect.right - Rect.left, Rect.bottom - Rect.top);
 // x = false;
 break; //определение закрытых перем. viewport.Height, viewport.Width;
case WM_ERASEBKGND:
 return 1;
 break;
case WM_LBUTTONDOWN:
 _Point mouse_point;
 mouse_point.x = LOWORD(lParam); //Сохраним координаты курсора мыши в системе окна
 mouse_point.y = HIWORD(lParam);
 //преобразуем координаты курсора в логические
 mouse_point = engine->viewport.T_inv(mouse_point);
 // запоминаем координаты курсора в объекте old_mouse
 action->InitAction(mouse_point.x, mouse_point.y);
 //InvalidateRect(hWnd, NULL, FALSE);
 break;
case WM_MOUSEMOVE:
 if (UINT(wParam) & MK_LBUTTON) {
 _Point mouse_point;
 mouse_point.x = LOWORD(lParam); // Координаты в системе окна
 mouse_point.y = HIWORD(lParam);
 mouse_point = engine->viewport.T_inv(mouse_point);// Логические координаты
 if (UINT(wParam) & MK_CONTROL) {
//Перемещает систему координат, опираясь на текущее и предыдущее полощения мыши
 action->Translate(mouse_point.x, mouse_point.y);
 } else {
 action->Rotate(mouse_point.x, mouse_point.y);
 }
 InvalidateRect(hWnd, NULL, FALSE);
 }
 break;
 case WM_KEYDOWN:
 int KeyPressed;
 KeyPressed = int(wParam);
 if (KeyPressed == int('T'))
 {
 action->Transform();
}
 InvalidateRect(hWnd, NULL, FALSE);
 break;
 case WM_DESTROY:
 PostQuitMessage(0);
 break;
default:
 return (DefWindowProc(hWnd, messg, wParam, lParam));
 }
return (0);
}

action.cpp

#include "action.h"
/*
 Сигнализирует о начале некоторого действия
*/
void Action::InitAction(double x, double y){
 old_mouse.set(x, y);
}
/*
 Поворачивает систему координат, опираясь на текущее
 и предыдущее полощения мыши
*/
void Action::Rotate(double x, double y){
 vec new_mouse(x, y);
 vec_float sina, cosa;
 sina = old_mouse.unit() ^ new_mouse.unit();
 cosa = old_mouse.unit() * new_mouse.unit(); 
 Matrix Rot;
 Rot.SetRotationMatrixbySinCos(sina, cosa);
 CurrentMatrix.MultiplyMatrices(Rot);
 old_mouse = new_mouse;
}
/*
 Перемещает систему координат, опираясь на текущее
 и предыдущее положения мыши
*/
void Action::Translate(double x, double y){
 vec new_mouse(x, y);
 vec delta = new_mouse - old_mouse;
 Matrix Tr;
 Tr.SetTranslationMatrix(delta.x, delta.y);
 CurrentMatrix.MultiplyMatrices(Tr);
 old_mouse = new_mouse;
}
void Action::Transform(){
 //vec new_mouse(x, y);
 //vec delta = new_mouse - old_mouse;
 Matrix Tr;
 Tr.SetTranslationMatrix1();
 CurrentMatrix.MultiplyMatrices(Tr);
 //old_mouse = new_mouse;
}

engine.cpp

#include <windows.h>
#include "geometry.h"
#include "matrix.h"
#include "engine.h"
/*
 Привязываем к движку объект, который отвечает за действия пользователя
*/
void Engine::SetAction(Action *_action)
{
 action = _action;
}
/*
 Выводит графику на контекст hdc
*/
void Engine::Draw(HDC hdc){
 _Point triangle[3];
 triangle[0].x = 0.0;
 triangle[0].y = 0.0;
 triangle[1].x = 0.5;
 triangle[1].y = 0.0;
 triangle[2].x = 0.0;
 triangle[2].y = 0.25;
 for (int i = 0; i < 3; i++){
 action->CurrentMatrix.ApplyMatrixtoPoint(triangle[i]);
 triangle[i] = viewport.T(triangle[i]);
 }
 for (int i = 0; i <= 3; i++){
 int j = i % 3;
 if (i == 0){
 MoveToEx(hdc, triangle[j].x, triangle[j].y, NULL);
 }
 else
 {
 LineTo(hdc, triangle[j].x, triangle[j].y);
 }
 }
}

matrix.cpp

#include <math.h>
#include <memory.h>
#include "matrix.h"
#include "geometry.h"
/*
 Конструктор, инициализирует матрицу единичной (матрица тождественного преобразования)
*/
Matrix::Matrix(){
 SetUnit();
}
/*
 Текущая матрица становится единичной
*/
void Matrix::SetUnit(){
 //memset(data, sizeof(data), 0);
 for (int i = 0; i < 3; i++){
 for(int j = 0; j < 3; j++){
 data[i][j] = 0.0;
 }
}
 for (int i = 0; i < 3; i++){
 data[i][i] = 1.0;
 }
}
/*
 Устанавливает текущую матрицу в качестве матрицы вращения на угол alpha,
 заданный косинусом и синусом
*/
void Matrix::SetRotationMatrixbySinCos(double sinalpha, double cosalpha){
 SetUnit();
 data[0][0] = cosalpha;
 data[0][1] = sinalpha;
 data[1][0] = -sinalpha;
 data[1][1] = cosalpha;
}
/*
 Устанавливает текущую матрицу в качестве матрицы вращения на угол alpha
*/
void Matrix::SetRotationMatrix(double alpha){
 SetRotationMatrixbySinCos(sin(alpha), cos(alpha));
}
/*
 Устанавливает текущую матрицу в качестве матрицы параллельного переноса на вектор (tx, ty)
*/
void Matrix::SetTranslationMatrix(double tx, double ty){
 SetUnit();
 //
 //data[0][0] = 1 - tx;
 //data[1][1] = 1 - ty;
 data[2][0] = tx;
 data[2][1] = ty;
}
void Matrix::SetTranslationMatrix1(){
 SetUnit();
 data[0][0] = 0.0; // a // a d 0 //
 data[0][1] = -1.0; // d // b e 0 //
 //data[0][2] = 0.1; // 0 // c f 1 //
 data[1][0] = 1.0; // b
 data[1][1] = 0.0; // e
 //data[1][2] = 0.1; // 0 
 //data[2][0] = 0.1; // c
 //data[2][1] = 0.1; // f
 //data[2][2] = 1.0; // 1
}
/*
 Умножает текущую матрицу на матрицу, переданную в качестве параметра
*/
void Matrix::MultiplyMatrices(Matrix &right){
 double temp[3][3];
 double val;
 memcpy(temp, data, sizeof(data));
 for (int i = 0; i < 3; i++){
 for (int j = 0; j < 3; j++){
 val = 0;
 for (int v = 0; v < 3; v++){
 val += temp[i][v] * right.data[v][j];
 }
 data[i][j] = val;
 }
 }
}
/*
 Перемножает точку, переданную в качестве параметра на текущую матрицу.
 При этом последний столбец матрицы не учитывается
*/
void Matrix::ApplyMatrixtoPoint(_Point &point){
 double _x, _y;
 _x = point.x;
 _y = point.y;
 point.x = _x * data[0][0] + _y * data[1][0] + data[2][0];
 point.y = _x * data[0][1] + _y * data[1][1] + data[2][1];
}

viewport.cpp

#include "viewport.h"
/*
 Задает размер окна
*/
void Viewport::SetWindowSize(int _Width, int _Height){
 Width = _Width;
 Height = _Height;
}
/*
 Выполняет преобразование из координат [-1, 1] x [-1, 1]
 в координаты экрана, с учетом отступов
*/
_Point Viewport::T(_Point point){
 _Point TPoint;
 TPoint.x = Margin + (1.0 / 2) * (point.x + 1) * (Width - 2 * Margin);
 TPoint.y = Margin + (-1.0 / 2) * (point.y - 1) * (Height - 2 * Margin);
 return TPoint;
}
/*
 Выполняет преобразование из координат экрана, в координаты
 [-1, 1] x [-1, 1] с учетом отступов
*/
_Point Viewport::T_inv(_Point point){
 _Point TPoint;
 TPoint.x = double(point.x - Margin) / (1.0 / 2) / (Width - 2 * Margin) - 1;
 TPoint.y = double(point.y - Margin) / (-1.0 / 2) / (Height - 2 * Margin) + 1;
 return TPoint;
}
/*
 Конструктор, выставляет отступы по умолчанию
*/
Viewport::Viewport(){
 SetMargin();
}
/*
 Устанавливает отступ по краям экрана
*/
void Viewport::SetMargin(int _Margin){
 Margin = _Margin;
}

 

Автор: Николай Свирневский

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *