Форум программистов
 

Восстановите пароль или Зарегистрируйтесь на форуме, о проблемах и с заказом рекламы пишите сюда - alarforum@yandex.ru, проверяйте папку спам!

Вернуться   Форум программистов > C/C++ программирование > Общие вопросы C/C++
Регистрация

Восстановить пароль
Повторная активизация e-mail

Купить рекламу на форуме - 42 тыс руб за месяц

Ответ
 
Опции темы Поиск в этой теме
Старый 04.08.2009, 21:29   #1
oleg kutkov
Unix C++ developer
Форумчанин
 
Аватар для oleg kutkov
 
Регистрация: 16.04.2007
Сообщений: 651
По умолчанию Анимированный осцилограф на 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
Каждое 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;

Последний раз редактировалось oleg kutkov; 04.08.2009 в 21:48.
oleg kutkov вне форума Ответить с цитированием
Старый 04.08.2009, 21:31   #2
oleg kutkov
Unix C++ developer
Форумчанин
 
Аватар для oleg kutkov
 
Регистрация: 16.04.2007
Сообщений: 651
По умолчанию

После объявления переменных, следует заполнить поля структуры, ниже показано как, с комментариями:

Код:
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 завершена.
oleg kutkov вне форума Ответить с цитированием
Старый 04.08.2009, 21:33   #3
oleg kutkov
Unix C++ developer
Форумчанин
 
Аватар для oleg kutkov
 
Регистрация: 16.04.2007
Сообщений: 651
По умолчанию

Теперь на очереди реализация вышеобъявленной функции WindowProcedure. В ней происходит обработка всех сообщений и выполнение соответствующих действий. Сразу следует сообщить, т.к. в нашем приложении будет рисоваться анимация в окне - в данной функции объявлены необходимые переменные и обработчики сообщений. Ниже представлен код всей функции с комментариями:

Код:
LRESULT CALLBACK WindowProcedure (HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{	
        PAINTSTRUCT ps;  //структура для рисования
	HDC hdc;  //дескриптор устройства (монитор)
	RECT rect;  //область, где рисовать
	static int offset = 0;  //смещение для синусоиды
	SetTimer(hWnd, 1, 200, NULL);  //устанавливаем таймер обновления рисунка
	switch (message)  //обрабатываем сообщения ос
	{
		case WM_TIMER: 	 // если сообщение - сработал таймер, то...
			GetClientRect(hWnd, &rect);  //получаем рабочую область
			InvalidateRect(hWnd, &rect, true); //объявляем ее недействительной
			UpdateWindow(hWnd); //перерисовываем окно
			++offset; //меняем значение смещения синусоиды
			break;
		case WM_PAINT:   // если сообщение - перерисовать окно, то...
			hdc = BeginPaint(hWnd, &ps); //получаем дескриптор устройства
			DrawDiagram(hWnd, hdc, offset); //рисуем синусоиду
			EndPaint(hWnd, &ps); //заканчиваем рисование
			break;
		case WM_DESTROY: // если сообщение - уничтожить окно, то...
			PostQuitMessage(0); //посылаем "прощальное" сообщение операционке
			break;
		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);

Последний раз редактировалось oleg kutkov; 04.08.2009 в 22:15.
oleg kutkov вне форума Ответить с цитированием
Старый 04.08.2009, 21:35   #4
oleg kutkov
Unix C++ developer
Форумчанин
 
Аватар для oleg kutkov
 
Регистрация: 16.04.2007
Сообщений: 651
По умолчанию

И в конце концов самая большая и сложная функция программы - функция рисования синусоиды. Её заголовок уже приведен вышею
На самом деле данная функция рисует не только синусоиду, он так же отвечает за отрисовку вертикальных и горизонтальных линий координатной сетки, а так же числовые отметки осей абсцис и ординат. Пусть это будет напряжение и время.
Начать функцию следует с объявление важных констант - координаты рисования сетки, максимальные, минимальные значения, а так же два массива, содержащие текст - числа, которые будут нарисованы возле осей. Так же тут вызываются четыер 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[i], strlen(yMark[i])); //выводим текст
	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;
}

Последний раз редактировалось oleg kutkov; 04.08.2009 в 23:13.
oleg kutkov вне форума Ответить с цитированием
Старый 04.08.2009, 21:37   #5
oleg kutkov
Unix C++ developer
Форумчанин
 
Аватар для oleg kutkov
 
Регистрация: 16.04.2007
Сообщений: 651
По умолчанию

Сетка нарисована, теперь нужно нарисовать сами оси, для этого выберем другое "перо":

Код:
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 и наооборот, не существенно, в данном случае), и запускать. Ниже представлен скриншот того, что у Вас должно в итоге получиться, а так же приложен файл с полным листингом программы и комментариями.
oleg kutkov вне форума Ответить с цитированием
Старый 04.08.2009, 21:40   #6
oleg kutkov
Unix C++ developer
Форумчанин
 
Аватар для oleg kutkov
 
Регистрация: 16.04.2007
Сообщений: 651
По умолчанию

Скриншот (разумеется анимацию тут не видно):

Исходный код:
Вложения
Тип файла: zip main.zip (2.8 Кб, 418 просмотров)
oleg kutkov вне форума Ответить с цитированием
Старый 05.08.2009, 15:17   #7
oleg kutkov
Unix C++ developer
Форумчанин
 
Аватар для oleg kutkov
 
Регистрация: 16.04.2007
Сообщений: 651
По умолчанию

Если можно - закрепите пожалуйста
Жалко будет, если статья канет в бездне постов...
oleg kutkov вне форума Ответить с цитированием
Старый 06.08.2009, 13:35   #8
ds.Dante
Старожил
 
Аватар для ds.Dante
 
Регистрация: 06.08.2009
Сообщений: 2,992
По умолчанию

У меня есть свой Starter Kit на разные случаи жизни, и одна из программ - окно, рисующая синусоиду. вот мой вариант:
Код:
// Notes:
// 1) click inside window to pause/unpause animation;
// 2) Project Properties (Release) -> C/C++ -> Code Generation -> Runtime Library
// is set to "Multi-threaded" for compatibility;
// 3) Project Properties (Release) -> Linker -> Manifest File -> Generate Manifest
// is set to "No" because of no need.

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <cmath>

LRESULT CALLBACK WindowProc (HWND, UINT, WPARAM, LPARAM);

HDC dc;

int __stdcall WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
	WNDCLASS wc;
	MSG msg;

						// Create window
	ZeroMemory (&wc, sizeof WNDCLASS);
	wc.style		= CS_OWNDC | CS_HREDRAW | CS_VREDRAW;
	wc.lpfnWndProc	= WindowProc;
	wc.hInstance	= hInstance;
	wc.hCursor		= LoadCursor (NULL, IDC_ARROW);
	wc.lpszClassName= L"CMyWnd";
	RegisterClass (&wc);
	HWND hWnd = CreateWindowA ("CMyWnd", "WinMain sample", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, 320, 240, NULL, NULL, hInstance, NULL);
	dc = GetDC (hWnd);
	ShowWindow (hWnd, nCmdShow);

						// Message loop (timer, etc)
	SetTimer (hWnd, 1, USER_TIMER_MINIMUM, NULL);
	while (GetMessage(&msg,NULL,0,0) > 0)	// while not WM_QUIT (0) nor some error (-1)
		DispatchMessage (&msg);

	return msg.wParam;
}

						// Message processing function
LRESULT CALLBACK WindowProc (HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	static bool	Move=1;
	static int	Phase=0, Width, Height;

	switch (message)
	{
	case WM_LBUTTONDOWN:
	case WM_RBUTTONDOWN:
		Move = !Move;
		// no break

	case WM_TIMER:
		if (Move)
			Phase++;
			// no break
		else
			break;

	case WM_PAINT:
		Rectangle (dc, -1, -1, Width+1, Height+1); 
		MoveToEx (dc, 0, Height * (0.5 + 0.3*sin(0.1*Phase)), NULL);
		for (int i=0; i<Width; i++)
			LineTo (dc, i, Height * (0.5 + 0.3*sin(0.1*(i+Phase))) );
		break;

	case WM_SIZE:
		Width  = LOWORD(lParam),
		Height = HIWORD(lParam);
		break;

	case WM_KEYDOWN:
		if (wParam != VK_ESCAPE)
			break;
		// else no break

	case WM_DESTROY:
		PostQuitMessage (0);
	}

	return DefWindowProc (hWnd, message, wParam, lParam);
}
ds.Dante вне форума Ответить с цитированием
Старый 06.08.2009, 13:54   #9
ds.Dante
Старожил
 
Аватар для ds.Dante
 
Регистрация: 06.08.2009
Сообщений: 2,992
По умолчанию

А теперь комментарии:

#define WIN32_LEAN_AND_MEAN
делает компиляцию чуть быстрее (отсекаются редко используемые функции), а также требуется для модулей типа WinSock 2.0 чтобы не было конфликта со старыми версиями, включенными в <windows.h>.

Я предпочитаю не объявлять отдельно число пи, а объявлять
#define _USE_MATH_DEFINES
#include <cmath>
и использовать стандартный M_PI


Код:
ZeroMemory (&wc, sizeof WNDCLASS);
wc.style = CS_OWNDC | CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = WindowProc;
wc.hInstance = hInstance;
wc.hCursor = LoadCursor (NULL, IDC_ARROW);
wc.lpszClassName = L"CMyWnd";
Мой вариант короче.


Я не разобрался сходу, зачем нужен TranslateMessage(&msg); (в Олеговом коде), но работать должно и без него.

Вопрос Олегу: SetTimer() находится в WindowProcedure(). То есть при любом событии (при движении мышки, и т. д) будет устанавливаться таймер? Странно, если это работает...

Переход от одного case к другому без break - может и некрасиво, но гораздо проще, чем отдельный вызов WM_PAINT. В этих случаях я обычно ставлю комментарий // no break.

Насколько я знаю желательно всегда вызывать DefWindowProc (hWnd, message, wParam, lParam); и возвращать его значение. Поэтому эта строчка у меня за пределами switch.



А вообще - хоршая статья. Кстати, могу выложить свои Starter Kit-ы по OpenGL, динамическому определению типов (RTTI), Multithreaded DLL и по C++/.Net.
ds.Dante вне форума Ответить с цитированием
Старый 06.08.2009, 15:32   #10
RomT24
Пользователь
 
Регистрация: 10.01.2009
Сообщений: 71
Восклицание

Цитата:
Сообщение от oleg kutkov Посмотреть сообщение
Все, программа закончена, можете смело компилировать, не обращая внимания на предупреждения компилятора (преобразования из int в double и наооборот, не существенно, в данном случае), и запускать. Ниже представлен скриншот того, что у Вас должно в итоге получиться, а так же приложен файл с полным листингом программы и комментариями.
Добрый день!
Как в Dev C++ запустить эту программу , "закрыв глаза" на эти ошибки (преобразование типов и тд)? Компилятор закрывать на это глаза не хочет, как настроить его, чтобы все запустилось?
RomT24 вне форума Ответить с цитированием
Ответ


Купить рекламу на форуме - 42 тыс руб за месяц

Опции темы Поиск в этой теме
Поиск в этой теме:

Расширенный поиск


Похожие темы
Тема Автор Раздел Ответов Последнее сообщение
Анимированный Gif в компоненте Image Ermiss Мультимедиа в Delphi 17 12.09.2010 06:11
БД на WinApi yngwie Win Api 3 09.03.2009 20:54
Анимированный фон Delph1n Мультимедиа в Delphi 11 31.01.2009 14:47
USB осцилограф Toha Chorniy Помощь студентам 1 13.10.2008 19:47
WinApi Andr Безопасность, Шифрование 3 17.06.2007 13:38