Автор: Олег Кутков
В этой небольшой статье я бы хотел продемонстрировать, как создается окно и как рисовать средствами GDI+. Возможно данный материал будет полезен всем тем, кто хочет разобраться с созданием графический приложений Windows, средствами WinAPI, тем более в преддверии нового учебного года, новых лабораторных, новых сессий. Анимироваться, в данной статье, будет синусоида, получиться своего рода осциллограф.
Для создания этого приложения я использовал среду Microsoft Visual C++ 6.0. Вы можете использовать более поздние версии Visual Studio, а так же Dev C++. Запустите IDE и создайте новое Win32 приложение, но укажите опцию, запрещающую генерацию любого кода, нам нужен чистый проект.
Так как мы собираемся использовать WinAPI функции, а так же некоторые математические функции, в начале программы следует подключить два заголовочных файла:
#include <windows.h> #include <math.h>
Так же, в программе, нам понадобится значение числа Пи, объявим его здесь же:
#define Pi 3.14159265 #define WIN32_LEAN_AND_MEAN // для более быстрой компиляции
Каждое Windows приложение имеет так называемую оконную функцию обратного вызова. Это особая функция, которая не вызывается непосредственно в приложении, ее вызывает операционная система, отсюда и название функции. Вызов это функции происходит каждый раз, когда приложению приходит какое-либо сообщение: перерисовать окно, нажата кнопка, приложение закрыто. Данная функция будет реализована чуть ниже, но что бы ее можно было вызывать из любой точки программы, объявим ее в начале:
LRESULT CALLBACK WindowProcedure (HWND, UINT, WPARAM, LPARAM);
Функции передаются параметры, указывающие какому окну адресуется это сообщение, а также, какое именно сообщение пришло. Объявим еще две вспомогательные переменные:
TCHAR szTitle[] = "Осциллограф"; TCHAR szWindowClass[] = "oscill";
Это две строки, первая – текст, который будет отображен в заголовке окна, вторая – имя класса окна (это имя выбирается самостоятельно программистом).
Готовим тело приложения…
Вот мы и подобрались к самой главной функции Windows приложения – WinMain. Эта функция выполняет роль, аналогичную роли функции main. WinMain принимает ряд аргументов:
- HINSTANCE hInstance – дескриптор приложения, присваеваемый операционной системой;
- HINSTANCE hPrevInstance – параметр, ныне не используемы, оставленный для совместимости с очень старыми приложениями;
- LPSTR lpCmdLine – строка, содержащая аргументы запуска приложения, аналог argv[];
- int nCmdShow – режим показа главного окна (свернутое, развернутое, по умолчанию).
Общее объявление функции выглядит как:
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { }
Параметр WINAPI перед функцией обозначает, что функция является особой WINAPI функцией и нужен операционной системе. Далее, в самой функции, нам следует объявить три переменные, дескритор окна, системное сообщение и структура окна:
HWND hWnd; MSG msg; WNDCLASSEX wcex;
После объявления переменных, следует заполнить поля структуры, ниже показано как, с комментариями:
wcex.cbSize = sizeof(WNDCLASSEX); //размер структуры wcex.style = CS_HREDRAW | CS_VREDRAW; //задаем стиль окна, подробнее смотрите в MSDN wcex.lpfnWndProc = (WNDPROC)WindowProcedure; //указываем оконную процедуру wcex.cbClsExtra = 0; wcex.cbWndExtra = 0; wcex.hInstance = hInstance; //указываем дескриптор приложениея wcex.hIcon = LoadIcon(NULL, IDI_APPLICATION); //устанавливаем иконку приложения по умолчанию wcex.hCursor = LoadCursor(NULL, IDC_ARROW); //устанавливаем курсор по умолчанию wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1); //задаем цвет окна wcex.lpszMenuName = 0; //меню окна - нет меню wcex.lpszClassName = szWindowClass; //указываем класс окна wcex.hIconSm = LoadIcon(NULL, IDI_APPLICATION); //загружаем иконку окна
Далее необходимо зарегистрировать класс окна, с обязательной проверкой результата:
if(!RegisterClassEx(&wcex)) { MessageBox(hWnd, "Ошибка регистрации класса окна", "Ошибка", IDI_ERROR || MB_OK); return 1; }
Теперь пришло время создания окна. Для этого будем использовать функцию CreateWindow. Ниже показано, как создать обычное окно, с координатами по умолчанию. Тут так же следует проводить проверку:
hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL); if(!hWnd) { MessageBox(hWnd, "Ошибка создания окна", "Ошибка", IDI_ERROR || MB_OK); //в случае чего - говорим об ошибке return 1; }
Теперь окно можно показать на экране:
ShowWindow(hWnd, nCmdShow);
Мы подобрались к самому концу функции, здесь нас ждет очень важный код – именно тут запускается цикл обработки сообщений операционной системы:
while(GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); }
Это цикл, с помощью функции GetMessage, выбирающий следующее сообщение из очереди сообщений и выполняющий его преобразование и обработку. Цикл заканчивается, как только приходит сообщение WM_QUIT и GetMessage возвращает false.
В самом конце следует написать return msg.wParam;
Функция WinMain завершена.
Теперь на очереди реализация вышеобъявленной функции WindowProcedure. В ней происходит обработка всех сообщений и выполнение соответствующих действий. Сразу следует сообщить, так как в нашем приложении будет орисоваться анимация в окне — в данной функции объявлены необходимые переменные и обработчики сообщений. Ниже представлен скорректированный код, от форумчанина DomiNick, всей функции с комментариями:
LRESULT CALLBACK WindowProcedure (HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { RECT rect; static int offset = 0; switch (message) { case WM_CREATE: SetTimer(hWnd, 1, 150, NULL); // "включаем" таймер return 0; case WM_TIMER: GetClientRect(hWnd, &rect); InvalidateRect(hWnd, &rect, true); UpdateWindow(hWnd); ++offset; return 0; case WM_PAINT: PAINTSTRUCT ps; HDC hdc; hdc = BeginPaint(hWnd, &ps); DrawDiagram(hWnd, hdc, offset); EndPaint(hWnd, &ps); return 0; case WM_DESTROY: PostQuitMessage(0); return 0; default: return DefWindowProc(hWnd, message, wParam, lParam); } return 0; }
Думаю, что все тут все наглядно и понятно. Как вы заметили, в сообщении WM_PAINT происходит вызов функции DrawDiagram(hWnd, hdc, offset) – это не стандартная функция и нам следует ее реализовать. Ей передаются, в качестве параметров, дескриптор окна, дескриптор устройства вывода, а так же новое значение смещения для синусоиды. Для того, что бы вызывать функцию в этом месте, мы должно объявить ее ранее, что и сделаем, добавьте, в самом верху, после LRESULT CALLBACK WindowProcedure (HWND, UINT, WPARAM, LPARAM) объявление:
void DrawDiagram(HWND hwnd, HDC hdc, int offset);
И в конце концов самая большая и сложная функция программы – функция рисования синусоиды. Её заголовок уже приведен выше. На самом деле данная функция рисует не только синусоиду, он так же отвечает за отрисовку вертикальных и горизонтальных линий координатной сетки, а так же числовые отметки осей абсцис и ординат. Пусть это будет напряжение и время.
Начать функцию следует с объявление важных констант – координаты рисования сетки, максимальные, минимальные значения, а так же два массива, содержащие текст – числа, которые будут нарисованы возле осей. Так же тут вызываются четыре API функции, задающие необходимые параметры рисования, подробнее о них Вы можете прочесть в MSDN.
RECT rect; GetClientRect(hwnd, &rect); const int xVE = rect.right - rect.left; const int yVE = rect.bottom - rect.top; const int xWE = xVE; const int yWE = yVE; double nPixPerVolt = yVE / 1000.0; double nPixPerMs = xVE / 60.0; SetMapMode(hdc, MM_ISOTROPIC); SetWindowExtEx(hdc, xWE, yWE, NULL); SetViewportExtEx(hdc, xVE, -yVE, NULL); SetViewportOrgEx(hdc, 10*nPixPerMs, yVE/2, NULL); const int tMin = 0; const int tMax = 40; const int uMin = -400; const int uMax = 400; const int tGridStep = 5; const int uGridStep = 100; int x, y; int u = uMin; int xMin = tMin * nPixPerMs; int xMax = tMax * nPixPerMs; char* xMark[] = {"0", "5", "10", "15", "20", "25", "30", "35", "40"}; char* yMark[] = {"-40", "-30", "-20", "-10", "0", "10", "20", "30", "40"};
Пока оставьте все как есть, потом, изменяя числовые значения, Вы сможете наблюдать за отрисовкой графика и координатной сетки. Далее создадим наше «перо», которым будет осущестляться рисование линй, так же зададим ему цвет.
HPEN hPen0 = CreatePen(PS_SOLID, 1, RGB(0, 160, 0)); HPEN hOldPen = (HPEN)SelectObject(hdc, hPen0);
Теперь выполним отрисовку сетки – линии ординат и соответсвущющих числовых меток. Для этого сначала переместимся в определенную точку, с помощью функции MoveToEx(hdc, x, y, NULL), а затем нарисуем, «пером», линию в другую точку, с помощью LineTo(hdc, x, y). Рисование текста выполняется с помощью TextOut(hdc, x, y, string, strlen(string)):
for(int i = 0; i < 9; ++i) { y = u * nPixPerVolt; MoveToEx(hdc, xMin, y, NULL); //перемещаемся в заданную точку LineTo(hdc, xMax, y); //рисуем туда линию TextOut(hdc, xMin-40, y+8, yMark, strlen(yMark)); //выводим текст u += uGridStep; }
Теперь выполним небольшие вычисления:
int t = tMin; int yMin = uMin * nPixPerVolt; int yMax = uMax * nPixPerVolt;
И аналогично нарисуем ось абсцис:
for(int a = 0; a < 9; ++a) { x = t * nPixPerMs; MoveToEx(hdc, x, yMin, NULL); //перемещаемся в заданную точку LineTo(hdc, x, yMax); //рисуем туда линию TextOut(hdc, x, yMin-10, xMark[a], strlen(xMark[a])); //выводим текст t += tGridStep; }
Сетка нарисована, теперь нужно нарисовать сами оси, для этого выберем другое «перо»:
HPEN hPen1 = CreatePen(PS_SOLID, 3, RGB(0, 0, 0)); //создаем кисть SelectObject(hdc, hPen1);
И с помощью уже знакомых нам функций нарисуем оси:
MoveToEx(hdc, 0, 0, NULL); LineTo(hdc, xMax, 0); MoveToEx(hdc, 0, yMin, NULL); LineTo(hdc, 0, yMax);
Теперь самое интересное, отрисовка графика функции. Вновь берем “перо”:
HPEN hPen2 = CreatePen(PS_SOLID, 5, RGB(200, 0, 100)); SelectObject(hdc, hPen2);
Сначала код с комментариями, затем некоторые пояснения:
int tStep = 1; //задаем шаг графика double radianPerx = 2 * Pi / 30; вычисляем угол радиан const double uAmplit = 250; //задаем амплитуду t = tMin; MoveToEx(hdc, 0, ((uAmplit * sin(t * radianPerx - offset)) * nPixPerVolt), NULL); //вычисляем начальную точку while(t <= tMax) { //до достижения максимального значения х u = uAmplit * sin(t * radianPerx - offset); //вычисляем синус и точку, куда рисовать линию LineTo(hdc, t * nPixPerMs, u * nPixPerVolt); //рисуем линию t += tStep; } SelectObject(hdc, hOldPen);
Сначала выполняются необходимые вычисления аргументов функции синуса, затем вычисляется собственно синус и в полученную точку осуществляется переход, с помощью MoveToEx. Затем запускается цикл, который, поточечно, начиная с точки, куда мы только что перешли (если-бы этого не сделали, то у синусоиды-бы появилась некрасивая «тянучка» в начале) рисует линию графика. цикл прерывается, как только достигается максимальное значение, заданное выше, в константах.
Все, программа закончена, можете смело компилировать, не обращая внимания на предупреждения компилятора (преобразования из int в double и наооборот, не существенно, в данном случае), и запускать.