Входная http://www.alexdver.ru/ железная дверь в Москве по низкой цене.

Анимированный осцилограф на WinAPI в С++

Автор: Олег Кутков

В этой небольшой статье я бы хотел продемонстрировать, как создается окно и как рисовать средствами 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(&amp;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(&amp;msg, NULL, 0, 0))
{
  TranslateMessage(&amp;msg);
  DispatchMessage(&amp;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, &amp;rect);
      InvalidateRect(hWnd, &amp;rect, true);
      UpdateWindow(hWnd);
      ++offset;
      return 0;
    case WM_PAINT:
      PAINTSTRUCT ps;
      HDC hdc;
      hdc = BeginPaint(hWnd, &amp;ps);
      DrawDiagram(hWnd, hdc, offset);
      EndPaint(hWnd, &amp;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, &amp;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 и наооборот, не существенно, в данном случае), и запускать.

Вы можете ответить или разместить запись на вашем сайте.

Ответить

Powered by Procoder