CodeNet / Платформы / Windows / MFC / Основы программирования с помощью библиотеки Microsoft Foundation Classes
Обработка сообщений - MFC
Windows взаимодействует с приложением, посылая ему сообщения. Поэтому обработка сообщений является ядром всех приложений. В традиционных приложениях Windows (написанных с использованием только API) каждое сообщение передается в качестве аргументов оконной функции. Там обычно с помощью большого оператора switch определяется тип сообщения, извлекается инфоромация и производятся нужные действия. Это громоздкая и чреватая ошибками процедура. С помощью MFC все делается намного проще. Здесь мы рассмотрим обработку в программе некоторых наиболее часто используемых сообщений.
Обработка сообщений в MFC
В MFC включен набор предопределенных
функций-обработчиков сообщений, которые можно
использовать в программе. Если программа
содержит такую функцию, то она будет вызываться
всякий раз, когда поступает связанное с ней
сообщение. При наличии дополнительной
информации в сообщении она передается в качестве
аргументов функции.
Для организации обработки сообщений
нужно выполнить следующие действия:
- В карту сообщений программы должна быть включена команда соответствующего сообщения.
- Прототип функции-обработчика должен быть включен в описание класса, ответственного за обработку данного сообщения.
- В программу должна быть включена реализация функции-обработчика.
Включение макрокоманд в карту сообщений
Чтобы программа могла ответить на сообщение, в карту сообщений должна быть включена соответствующая макрокоманда. Названия макрокоманд соответствуют именам стандартных сообщений Windows, но дополнительно имеют префикс ON_ и заканчиваются парой круглых скобок. Из этого правила есть исключение: сообщению WM_COMMAND соответствует макрокоманда ON_COMMAND(). Причина в том, что это сообщение обрабатывается особым образом.
Чтобы включить макрокоманду в очередь сообщений, нужно просто поместить ее между командами BEGIN_MESSAGE_MAP() и END_MESSAGE_MAP(). Например, если необходимо обработать в программе сообщение WM_CHAR, то очередь должна выглядеть так:
BEGIN_MESSAGE_MAP(CMainWin, CFrameWnd) ON_WM_CHAR() END_MESSAGE_MAP()В очереди может находиться более одной макрокоманды. Сообщение WM_CHAR генерируется при нажатии алфавитно-цифровой клавиши на клавиатуре.
Включение обработчиков сообщений в описание класса
Каждое сообщение, явно обрабатываемое в программе, должно быть связано с одним из обработчиков. Обработчик - это член-функция класса, принимающего сообщения. Прототипы для обработчиков всех сообщений заранее заданы в MFC. Как правило, имя обработчика состоит из имени сообщения и префикса On. Например, обработчик сообщения WM_CHAR называется OnChar(), а для WM_LBUTTONDOWN - OnLButtonDown(). Последнее сообщение генерируется при нажатии левой кнопки мыши.
Например, объявим класс с обработчиком сообщения WM_PAINT. Это сообщение посылается окну, когда оно должно перерисовать свою клиентскую область.
Class CMainWin: public CFrameWnd { public: CMainWin(); afx_msg void OnPaint(); DECLARE_MESSAGE_MAP() }Спецификатор afx_msg означает объявление обработчика сообщения. На данный момент он не используется и представляет собой пустой макрос. Но в будущем возможны расширения. Поэтому использование спецификатора нужно считать обязательным.
Для каждого обработчика должна быть описана его реализация. В ней могут производиться самые разные действия, которые требуются по логике работы программы.
Пример программы с обработкой сообщений
Рассмотрим программу, которая реагирует на нажатие левой и правой кнопок мыши в клиентской области окна, а также на нажатие клавиш. При нажатии левой кнопки в клиентскую область окна, начиная с текущих координат курсора мыши, выводится строка "Нажата левая кнопка", а при нажатии правой кнопки - "Нажата правая кнопка".
При нажатии на левую и правую кнопки соответственно генерируются сообщения WM_LBUTTONDOWN и WM_RBUTTONDOWN, и им соответствуют обработчики с прототипами:
afx_msg void OnLButtonDown(UINT Flags, CPoint Loc); afx_msg void OnRButtonDown(UINT Flags, CPoint Loc);Первый параметр указывает на то, была ли при генерации сообщения нажата какая-нибудь клавиша или кнопка мыши. Этот параметр нас не будет пока интересовать. Второй параметр определяет координаты курсора мыши во момент нажатия кнопки. Класс CPoint порождается от структуры POINT, определенной так:
typedef struct tagPOINT { LONG x; LONG y; } POINT;Таким образом, мы легко можем определить координаты для вывода текстовой строки.
Обработчик сообщения WM_CHAR имеет прототип:
afx_msg void OnChar(UINT Char, UINT Count, UINT Flags);Нас здесь будет интересовать только первый параметр. Он представляет собой ASCII-код символа, соответствующего нажатой клавише. При нажатии несимвольных клавиш сообщение WM_CHAR не посылается.
Контекст устройства
В программах Windows, прежде чем вывести что-либо на экран, необходимо получить контекст устройства, и весь вывод производить через него. Например, в Windows нет совершенно никакой возможности выводить точки прямо на экран, как это делалось в DOS. Контекст устройства - это достаточно условное название (даже для английского языка, device context), не отражающее сути понятия. На самом деле, это структура данных, обеспечивающая связь графических функций с драйвером конкретного устройства. Эта структура определяет состояние драйвера, и способ вывода графики. В MFC есть классы, инкапсулирующие контексты устройств, поэтому нам не придется работать с ними напрямую.
Например, в MFC можно получить контекст клиентской области окна, создав экземпляр класса CClientDС. Конструктор этого класса принимает один параметр - указатель на объект окна, обычно подставляется this. После создания этого объекта можно выводить графику в окно, используя функции-члены класса.
Сообщение WM_PAINT
Windows устроена таким образом, что за обновление содержимого окна отвечает программа. Например, если часть окна была перекрыта другим окном, а затем вновь открыта, или минимизированное окно было восстановлено, то окну посылается сообщение WM_PAINT. В ответ на него окно должно обновить свою клиентскую область.
Прототип обработчика WM_PAINT следующий:
afx_msg void OnPaint();Макрокоманда называется ON_WM_PAINT(). Рассмотрим простой обработчик, который выводит строку "Привет" в клиентскую область по координатам x = 10, y = 20:
afx_msg void CMainWin::OnPaint() { CPaintDC paintDC(this); paintDC.TextOut(10, 20, CString("Привет")); }В обработчике WM_PAINT нужно всегда пользоваться классом CPaintDC, который представляет собой класс клиентской области, но предназначенный для использования именно с этим сообщением. Это обусловлено архитектурой самой Windows.
Функция TextOut() предназначена для вывода текста в контекст устройства (в данном случае - в окно). При ее использовании по умолчанию первые два параметра определяют координаты верхнего левого угла текстовой строки. По умолчанию координаты представляют собой реальные пиксели, ось x направлена слева направо, ось y - сверху вниз. Эта функция перегруженная, наиболее удобный для нас вариант - когда третий параметр имеет тип CString. Этот класс входит в MFC и является очень удобной заменой для строк, завершаемых нулем. Он имеет много полезных функций - аналогов стандартных строковых функций, и поддерживает преобразования типов, в частности, из char* в CString (поэтому в примере можно было обойтись без явного создания объекта этого класса). Вы можете легко самостоятельно изучить этот класс и многие другие полезные классы общего назначения из состава MFC, пользуясь справочной системой Visual C++.
Большинство реальных окон (за исключением диалогов, которые мы рассмотрим позднее) должны обрабатывать сообщение WM_PAINT. Более того, если Вы хотите написать корректную программу, то весь вывод в окно должен осуществляться только в обработчике WM_PAINT, и никак иначе. Например, крайне нежелательно получать контекст устройства в обработчике сообщения мыши и пытаться там что-то выводить. Это будет работать в большинстве случаев, но далеко не всегда. Дело в том, что Windows может предоставить одновременно всем программам лишь очень небольшое число контекстов устройств (Windows 95 - всего пять). Поэтому при запросе контекста не из обработчика WM_PAINT создание класса контекста может потерпеть провал, если другие приложения уже заняли все контексты. Тогда выводить информацию будет вообще невозможно. Это может вылиться в непредсказуемое поведение программы. В случае же получения контекста из обработчика WM_PAINT, обязательно с помощью класса CPaintDC, Windows гарантирует наличие свободного контекста. На самом деле, Windows не пошлет программе это сообщение до тех пор, пока в системе не будет свободного контекста. К сожалению, эта проблема не так широко известна, и во многих книгах сплошь и рядом правило вывода информации только по сообщению WM_PAINT не соблюдается. Это вызвано тем, что описанная проблема не проявляется до тех пор, пока в системе не будет запущено несколько задач, постоянно что-то рисующих (кроме отображения видео). Но так одна из наших целей - научиться создавать надежные программы, то в методических указаниях мы будем придерживаться данной рекомендации. Также Вам рекомендуется делать это и в дальнейшем. Нужно отметить, что все среды визуального программирования действуют согласно этой рекомендации.
Генерация сообщения WM_PAINT
Так как мы решили весь вывод в окно организовать через сообщение WM_PAINT, то необходим способ принудительной отправки этого сообщения, если необходимо что-то вывести в окно. Это осуществляется с помощью функции со следующим прототипом:
void CWnd::InvalidateRect(LPRECT pRegion, BOOL EraseBackground = TRUE);Параметр pRegion задает область, которую нужно перерисовать. В большинстве программ эта область вообще не анализируется, поэтому этот параметр почти всегда равен 0. Параметр EraseBackground определяет, нужно ли перед отправкой сообщения закрасить окно фоном (по умолчанию белым). Мы будем почти всегда использовать параметр по умолчанию, так как это очень удобно для учебных примеров.
Пример программы
message handling.hpp #include <afxwin.h> // Класс основного окна class CMainWin: public CFrameWnd { public: CMainWin(); // Функции обработки сообщений afx_msg void OnChar(UINT ch, UINT, UINT); afx_msg void OnPaint(); afx_msg void OnLButtonDown(UINT flags, CPoint Loc); afx_msg void OnRButtonDown(UINT flags, CPoint Loc); // Вспомогательные член-данные char str[50]; int nMouseX, nMouseY, nOldMouseX, nOldMouseY; char pszMouseStr[50]; // Декларирование карты откликов на сообщения DECLARE_MESSAGE_MAP() }; // Класс приложения class CApp: public CWinApp { public: BOOL InitInstance(); }; message handling.cpp #include <afxwin.h> #include <string.h> #include "MESSAGE HANDLING.HPP" // Реализация BOOL CApp::InitInstance() { m_pMainWnd = new CMainWin; m_pMainWnd->ShowWindow(SW_RESTORE); m_pMainWnd->UpdateWindow(); return TRUE; } CMainWin::CMainWin() { // Создать основное окно this->Create(0, "Обработка сообщений"); // Инициализировать переменные объекта strcpy(str, ""); strcpy(pszMouseStr, ""); nMouseX = nMouseY = nOldMouseX = nOldMouseY = 0; } // Реализация карты сообщений главного окна BEGIN_MESSAGE_MAP(CMainWin /* класс */, CFrameWnd /* базовый класс */) ON_WM_CHAR() ON_WM_PAINT() ON_WM_LBUTTONDOWN() ON_WM_RBUTTONDOWN() END_MESSAGE_MAP() // Реализация функций отклика на сообщения afx_msg void CMainWin::OnChar(UINT ch, UINT, UINT) { sprintf(str, "%c", ch); // Посылаем сообщение WM_PAINT // с необходимостью стереть и обновить все окно this->InvalidateRect(0); } afx_msg void CMainWin::OnPaint() { // Создадим контекст устройства для обработки WM_PAINT CPaintDC dc(this); // Затираем текст и снова выводим (возможно уже другой текст) dc.TextOut(nOldMouseX, nOldMouseY, " ", 30); dc.TextOut(nMouseX, nMouseY, pszMouseStr); dc.TextOut(1, 1, " "); dc.TextOut(1, 1, str); } afx_msg void CMainWin::OnLButtonDown(UINT, CPoint loc) { // Запоминаем в переменных класса координаты // мыши и текст. // Затем посылаем сообщение WM_PAINT - его // обработчик выведет все на экран. nOldMouseX = nMouseX; nOldMouseY = nMouseY; strcpy(pszMouseStr, "Нажата левая кнопка"); nMouseX = loc.x; nMouseY = loc.y; this->InvalidateRect(0); } afx_msg void CMainWin::OnRButtonDown(UINT, CPoint loc) { // Запоминаем в переменных класса координаты // мыши и текст. // Затем посылаем сообщение WM_PAINT - его // обработчик выведет все на экран. nOldMouseX = nMouseX; nOldMouseY = nMouseY; strcpy(pszMouseStr, "Нажата правая кнопка"); nMouseX = loc.x; nMouseY = loc.y; this->InvalidateRect(0); } CApp App; // Единственный экземпляр приложения
Рис. 3. Пример работы программы с обработкой сообщений.
Сообщения WM_TIMER и WM_DESTROY
Сообщение WM_TIMER
В Windows существуют специальные объекты, называемые таймерами. Программа (точнее, окно) может запросить один или несколько таких объектов. После этого каждый таймер через регулярные заранее указанные промежутки времени будет посылать сообщение WM_TIMER. Они будут помещаться в очередь сообщений окна. Таким образом, в функции-обработчике этого сообщения можно выполнять некоторые действия через регулярные промежутки времени. Если создано несколько таймеров, то их можно различать по номерам, присвоенным им при запросе.
Для запроса таймера у системы используется следующая функция:
UINT CWnd::SetTimer(UINT Id, UINT Interval, void (CALLBACK EXPORT *TFunc)(HWND, UINT, UINT, DWORD));Третьим параметром пользуются очень редко, и обычно он равен 0. Мы не будем его использовать. Параметр Id задает уникальный идентификационный номер таймера. По этим номерам обычно различаются несколько созданных таймеров. Параметр Interval задает интервал между двумя посылками сообщений (интервал таймера) в миллисекундах. Разрешающая способность таймеров 55 мс, поэтому интервалы измеряются с такой точностью. Если даже задать значение интервала равным 1, то все равно будет использовано значение 55. Если задать 0, то таймер приостановит свою работу.
Сообщения таймера обрабатываются функцией:
afx_msg void OnTimer(UINT Id);Все таймеры вызывают один и тот же обработчик. Узнать, какой таймер послал сообщение, можно с помощью параметра Id, в котором передается номер таймера.
Сообщение WM_DESTROY
Это сообщение посылается окну, когда последнее должно быть удалено. Если его получает главное окно приложения, то это означает завершение приложения. В этом случае обычно приложение должно выполнить действия по выгрузке. Обработчик имеет прототип:afx_msg void OnDestroy();В нашем случае, мы должны удалить запрошенные таймеры. Все запрошенные ресурсы перед завершением программы необходимо освобождать. Для таймеров это делается с помощью функции:
BOOL CWnd::KillTimer(int Id);Функция освобождает таймер с идентификатором Id.
Ниже приведен пример программы-"часов", которая создает обычное окно, только небольшого размера, и по сообщениям от таймера выводит в него текущие дату и время. Так как мы используем всего один таймер, то нет необходимости анализировать номер таймера, пославшего сообщение.
Пример программы
message handling II.hpp #include <afxwin.h> // Класс основного окна class CMainWin: public CFrameWnd { public: CMainWin(); afx_msg void OnPaint(); // Обработчик сообщения WM_DESTROY afx_msg void OnDestroy(); // Обработчик сообщения WM_TIMER afx_msg void OnTimer(UINT ID); char str[50]; DECLARE_MESSAGE_MAP() }; // Класс приложения class CApp: public CWinApp { public: BOOL InitInstance(); }; message handling II.cpp #include <afxwin.h> #include <time.h> #include <string.h> #include "MESSAGE HANDLING - II.HPP" // Реализация BOOL CApp::InitInstance() { m_pMainWnd = new CMainWin; m_pMainWnd->ShowWindow(SW_RESTORE); m_pMainWnd->UpdateWindow(); // Установка таймера с идентификатором 1 и // интервалом 500 мс m_pMainWnd->SetTimer(1, 500, 0); return TRUE; } CMainWin::CMainWin() { // Определение прямоугольника, в котором будет // размещено окно RECT rect; rect.left = rect.top = 10; rect.right = 200; rect.bottom = 60; // Создание окна в определенном экранном // прямоугольнике this->Create(0, "CLOCK", WS_OVERLAPPEDWINDOW, rect); strcpy(str, ""); } // Реализация карты сообщений главного окна BEGIN_MESSAGE_MAP(CMainWin, CFrameWnd) ON_WM_PAINT() ON_WM_DESTROY() ON_WM_TIMER() END_MESSAGE_MAP() afx_msg void CMainWin::OnPaint() { CPaintDC dc(this); // Выводим в окно строку текущего времени dc.TextOut(1, 1, " ", 3); dc.TextOut(1, 1, str); } afx_msg void CMainWin::OnTimer(UINT ID) { // Предполагаем, что в программе один таймер, поэтому // не проверяем ID. // Получаем строку текущего времени CTime curtime = CTime::GetCurrentTime(); tm *newtime; newtime = curtime.GetLocalTm(); sprintf(str, asctime(newtime)); str[strlen(str) - 1] = '\0'; // Посылаем сообщение WM_PAINT -- его обработчик // отобразит строку. this->InvalidateRect(0); } afx_msg void CMainWin::OnDestroy() { // При закрытии окна удаляем связанный с ним // таймер. KillTimer(1); } CApp App; // Единственный экземпляр приложения.
Рис. 4. Программа-"часы"
Мы получаем текущее время с помощью класса CTime. Это еще один полезный класс общего назначения из MFC. Вы можете использовать приведенный выше код для получения текущего локального времени. Также, мы при создании окна явно задали координаты прямоугольника, в котором оно должно быть отображено, с помощью структуры RECT. Таким образом, всегда можно задать начальные размеры и положение окна на экране.
Оставить комментарий
Комментарии
Я его переделал теперь выводит время.
CTime curtime = CTime::GetCurrentTime();
tm newtime;
curtime.GetLocalTm(&newtime);
sprintf(str, asctime(&newtime));
str[strlen(str) - 1] = '\0';
dc.TextOut(nOldMouseX, nOldMouseY,
CString(""), 30);
у мну заработало, но вопрос такой получается обязательно все строки преобразовывать в CString?
Этот код в лучшем случае выведет на экран мусор, а в худшем уронит приложение по Access Violation. 30 - это сколько символов считать из строки. Здесь надо просто вывести строку типа " ".