Сообщений 15    Оценка 180 [+2/-0]         Оценить  
Система Orphus

Как узнать, что мышь вышла за пределы окна?

Авторы: Александр Шаргин
Антон Баула
Опубликовано: 11.07.2001
Исправлено: 13.03.2005
Версия текста: 2.0

При создании пользовательского интерфейса иногда требуется определить момент, когда курсор мыши выходит за пределы окна. Для решения этой задачи существуют различные методы. Можно воспользоваться готовой функцией TrackMouseEvent, которая появилась в Win32 API, начиная с Windows 98/NT4, или же эквивалентной ей функцией _TrackMouseEvent из библиотеки comctl32.dll. А можно добиться требуемого поведения "вручную", использую стандартные средства.

Способ 1

Демонстрационный проект MouseOut

Сначала рассмотрим функцию _TrackMouseEvent. Она появилась вместе с программой Internet Explorer 3.0. На новых платформах эта функция просто вызывает TrackMouseEvent, а на более старых эмулирует её поведение. Вот почему использовать её предпочтительнее, чем аналог "без подчёрка".

Функция _TrackMouseEvent позволяет отслеживать события, связанные с мышью, которые не входят в стандартный набор. В случае выхода за границы клиентской области окна она посылает ему сообщение WM_MOUSELEAVE, а в случае "зависания" курсора на одном месте - сообщение WM_MOUSEHOVER. Существуют также сообщения WM_NCMOUSELEAVE и WM_NCMOUSEHOVER, относящиеся к окну целиком, а не только к клиентской области.

Нужно подчеркнуть два важных момента, связанных с функцией _TrackMouseEvent. Во-первых, она посылает не все перечисленные сообщения, а только те, которые вы ей "закажете". Во-вторых, она обладает "одноразовым действием". Это означает, что после отправки ровно одного сообщения она прекращает следить за окном. Если вы хотите получать уведомления и дальше, вам придётся вызывать эту функцию снова. Где это лучше делать - зависит от ситуации. В своём примере я вызываю её из обработчика WM_MOUSEMOVE. Возможно, вы захотите вызывать её в другом месте.

Демонстрационная программа MouseOut отслеживает положение курсора мыши с помощью _TrackMouseEvent. Когда курсор находится над клиентской областью окна, она принимает красный цвет, в противном случае окрашивается в синий. Функция окна, использованная в примере, выглядит так (обратите внимание на обработчики WM_MOUSEMOVE и WM_MOUSELEAVE).

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    int wmId, wmEvent;
    PAINTSTRUCT ps;
    HDC hdc;

    static BOOL bMouseInside;

    switch (message) 
    {
        case WM_CREATE:
            {
                RECT rt;
                POINT pt = { 0, 0 };

                GetClientRect(hWnd, &rt);
                ClientToScreen(hWnd, &pt);
                OffsetRect(&rt, pt.x, pt.y);

                GetCursorPos(&pt);
                bMouseInside = PtInRect(&rt, pt);
            }
            break;

        case WM_COMMAND:
            wmId    = LOWORD(wParam); 
            wmEvent = HIWORD(wParam); 
            // Parse the menu selections:
            switch (wmId)
            {
                case IDM_ABOUT:
                   DialogBox(hInst, (LPCTSTR)IDD_ABOUTBOX, hWnd, (DLGPROC)About);
                   break;
                case IDM_EXIT:
                   DestroyWindow(hWnd);
                   break;
                default:
                   return DefWindowProc(hWnd, message, wParam, lParam);
            }
            break;

        case WM_MOUSEMOVE:
            {
                TRACKMOUSEEVENT tme;
                tme.cbSize = sizeof(tme);
                tme.hwndTrack = hWnd;
                tme.dwFlags = TME_LEAVE;

                _TrackMouseEvent(&tme);

                if(!bMouseInside)
                {
                    bMouseInside = TRUE;
                    InvalidateRect(hWnd, NULL, FALSE);
                    UpdateWindow(hWnd);
                }
            }
            break;

        case WM_MOUSELEAVE:
            bMouseInside = FALSE;
            InvalidateRect(hWnd, NULL, FALSE);
            UpdateWindow(hWnd);
            break;

        case WM_PAINT:
            {
                hdc = BeginPaint(hWnd, &ps);

                RECT rt;
                GetClientRect(hWnd, &rt);

                HBRUSH hBr = CreateSolidBrush(bMouseInside ? RGB(255,0,0) : RGB(0,0,255));
                HBRUSH hOld = (HBRUSH)SelectObject(hdc, hBr);
                Rectangle(hdc, rt.left, rt.top, rt.right, rt.bottom);
                SelectObject(hdc, hOld);
                DeleteObject(hBr);

                EndPaint(hWnd, &ps);
            }
            break;

        case WM_DESTROY:
            PostQuitMessage(0);
            break;

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

    return 0;
}

Способ 2

Демонстрационный проект MouseExEvent

В данном случае мы будем делать всё сами. Алгоритм работы этого способа следующий: как только курсор входит в область нашего окна, окно начинает получать сообщение WM_MOUSEMOVE, обрабатывая которое мы создаём таймер с небольшим интервалом и выставляем флаг, указывающий, что курсор находится в нашем окне (в данном примере роль флага одновременно выполняет идентификатор созданного таймера m_uTimer: если он не равен нулю, значит курсор находится в нашем окне). После этого мы посылаем нашему окну сообщение UWM_MOUSE_ENTER, которое определено как WM_USER+0.

BOOL CMouseEvent::Notify(CWnd *pWnd)
{
    if( m_uTimer != 0 )
        return TRUE;

    m_uTimer = ::SetTimer( pWnd->m_hWnd, IDS_TIMER_MOUSE_EVENT, 10, NULL ); 
    if( m_uTimer )
    {
        m_hWnd = pWnd->m_hWnd;
        ::SendMessage( pWnd->m_hWnd, UWM_MOUSE_ENTER, 0, 0 );
    }

    return m_uTimer != 0 ? TRUE : FALSE ;
}

Обратите внимание, что если таймер уже запущен, то функция ничего не делает. HWND окна, для которого создан таймер, сохраняется в переменной. Это сделано для того, чтобы корректно уничтожить таймер в деструкторе объекта. Это может потребоваться, если мы закрываем окно нажатием на ALT+F4 или это делает приложение в тот момент, когда курсор находится в окне.

CMouseEvent::~CMouseEvent()
{
    if( m_uTimer && m_hWnd )
    {
        ::KillTimer( m_hWnd, m_uTimer );
        m_uTimer = 0;
        m_hWnd = NULL;
    }
}

Далее с заданным интервалом наше окно получает сообщения WM_TIMER, которые мы обрабатываем следующим образом: получаем координаты курсора, а затем получаем по ним указатель на CWnd окна, которое находится под курсором, с помощью CWnd::WindowFromPoint(CPoint pt). Сравнивая полученное значение с указателем на наше окно, мы можем сказать, оставил курсор окно, или нет. Если нет, ничего не делаем, иначе уничтожаем таймер и посылаем нашему окну сообщение UWM_MOUSE_LEAVE, которое определено как WM_USER+1.

BOOL CMouseEvent::Verify(CWnd *pWnd )
{
    CPoint pt;
    ::GetCursorPos( &pt );

    if( pWnd != CWnd::WindowFromPoint( pt ) )
    {
        ::KillTimer( pWnd->m_hWnd, m_uTimer );
        m_uTimer = 0;
        m_hWnd = NULL;
        ::SendMessage( pWnd->m_hWnd, UWM_MOUSE_LEAVE, 0, 0 );
    }

    return TRUE;
}

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


Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав.
    Сообщений 15    Оценка 180 [+2/-0]         Оценить