MFC C++ CButton鼠标悬停事件

lb3vh1jj  于 11个月前  发布在  其他
关注(0)|答案(4)|浏览(143)
BEGIN_MESSAGE_MAP(CPQVDirect2DControlsDlg, CDialogEx)

    ON_WM_TIMER()
    ON_WM_PAINT()
    ON_WM_DRAWITEM()
    ON_WM_ERASEBKGND()
    ON_WM_QUERYDRAGICON()


    ON_BN_CLICKED(IDC_BUTTON1, &CPQVDirect2DControlsDlg::OnBnClickedTestButton)
    ON_BN_CLICKED(IDC_CHECK1, &CPQVDirect2DControlsDlg::OnBnClickedTestCheckbox)

    ON_BN_CLICKED(IDOK, &CPQVDirect2DControlsDlg::OnBnClickedOk)
    ON_NOTIFY(NM_CUSTOMDRAW, IDC_SLIDER1, &CPQVDirect2DControlsDlg::OnNMCustomdrawSlider1)

    ON_BN_CLICKED(IDC_BUTTON3, &CPQVDirect2DControlsDlg::OnBnClickedButton3)
    ON_BN_CLICKED(IDC_BUTTON2, &CPQVDirect2DControlsDlg::OnBnClickedButton2)
 
    ON_WM_MOUSEMOVE() // I HAVE ADDED THIS
 

END_MESSAGE_MAP()

void CPQVDirect2DControlsDlg::OnMouseMove(UINT nFlags, CPoint point)
{

    CRect rect;
    CButton* pButton = static_cast<CButton*>(GetDlgItem(IDC_BUTTON1)); // Replace IDC_MY_BUTTON with your button's ID

    // Check if the mouse is over the button
    if (pButton != nullptr && pButton->GetSafeHwnd() != nullptr) {
        pButton->GetClientRect(&rect);
        pButton->ScreenToClient(&point);

        if (rect.PtInRect(point)) {
            // Change the button's color when the mouse hovers over it
            int i = 10;
            pButton->Invalidate();
        }
        else {
            // Revert to the default color when the mouse leaves
            int i = 20;
            pButton->Invalidate();
        }
    }
 
    CDialogEx::OnMouseMove(nFlags, point);
}

字符串
我的断点应该在int i = 10;,但它总是去int i = 20;,甚至我的鼠标在IDC_BUTTON1。有人可以帮助如何改变鼠标悬停的按钮边框吗?我是MFC C++的新手

rsl1atfo

rsl1atfo1#

正如在其他答案中提到的,在试图确定鼠标指针是否在按钮控件内的代码中存在一个错误。它检索按钮的客户端大小(在客户端坐标中),随后将这些坐标解释为屏幕坐标。计算的结果是确定性的,但没有意义。
然而,这是一个Red Herring,它分散了核心问题的注意力:鼠标输入总是1传递到鼠标光标下的窗口。当鼠标光标在按钮上移动时,WM_MOUSEMOVE消息会发布到按钮窗口。只要鼠标光标在子窗口内,其父窗口(对话框)就不会接收WM_MOUSEMOVE消息。
因此,有必要处理按钮控件中的WM_MOUSEMOVE消息。系统允许客户端代码通过一种称为subclassing的技术来修改控件的标准行为。
子类化是一个复杂的主题,但MFC提供了强大的工具,使它的使用更加方便2。

工具

使用MFC子类别化控件需要下列步骤:
1.定义衍生自个别MFC控件 Package 函数3的类别。
1.通过在其父对话框中声明类成员来示例化此类的对象。
1.连接邮件路由。
1.自定义按钮行为。

派生自定义类

派生一个自定义类就像键入class CMyButton : public CButton {};一样简单。然而,消息路由需要更多的基础设施(即消息Map),我们将让 Add New Item 向导来为我们完成,而不是自己键入:

  • 选择 * 项目 * → * 添加新项...*。
  • 这会开启一个对话方块,其右上角会有一个搜寻方块。输入搜寻字词mfc class后,筛选清单会包含一个名为 MFC Class 的项目。选取它,然后按一下 Add
  • 这会开启 * 加入MFC类别 * 对话方块,我们可以在其中输入任意的 * 类别名称 *(例如CMyButton)以及 * 基底类别 *(CButton)。按一下 * 确定 *,最后会建立下列档案:
  • CMy按钮.h*
#pragma once

class CMyButton : public CButton
{
public:
    CMyButton();
    virtual ~CMyButton();

protected:
    DECLARE_MESSAGE_MAP()
};

字符串

  • CMyButton.cpp *(英文)
#include "pch.h"

#include "CMyButton.h"

CMyButton::CMyButton() {}
CMyButton::~CMyButton() {}

BEGIN_MESSAGE_MAP(CMyButton, CButton)
END_MESSAGE_MAP()


我已经删除了DECLARE_DYNAMICIMPLEMENT_DYNAMIC宏,因为我们不需要MFC的运行时类型信息支持。

示例化CMyButton

定义了CMyButton类之后,我们必须在需要的时候示例化它。最方便的方法是将它声明为使用它的对话框类的(private)类成员:

#include "CMyButton.h"

class CPQVDirect2DControlsDlg : public CDialogEx {
    // ...
private:
    CMyButton m_MyButton;
}


这样做会将m_MyButton的生存期与其父对话框的生存期联系起来。它会在对话框旁边构造,并在对话框被破坏时自动销毁。

连接消息路由

到目前为止,我们已经创建了一个“自定义”按钮控件,它的生命周期与其父对话框的生命周期一致。但是,按钮消息仍然要通过标准按钮控件实现。要通过CMyButton的消息Map来路由它们,本机按钮控件需要与CMyButton类相关联。
这是通过在对话框的DoDataExchange覆盖中调用DDX_Control来完成的(有关更多详细信息,请参见Dialog Data Exchange):

#include "resource.h"

void CPQVDirect2DControlsDlg::DoDataExchange(CDataExchange* pDX)
{
    // ...
    DDX_Control(pDX, IDC_BUTTON1, m_MyButton);
    CDialogEx::DoDataExchange(pDX);
}


这样做会将CMyButton的消息Map链接到默认按钮消息处理之前,从而允许C++实现增加或覆盖默认处理。

自定义行为

此时,一切都已经设置好了,CMyButton可以控制与之关联的本机控件的外观和感觉。实际上,由message map macros发出的代码已经在执行,即使是无关紧要的。让我们通过连接WM_MOUSEMOVE消息处理程序来快速改变这一点。

  • CMy按钮.h*
#pragma once

class CMyButton : public CButton
{
    // ...
private:
    void OnMouseMove(UINT nFlags, CPoint point);
};

  • CMyButton.cpp *(英文)
void CMyButton::OnMouseMove(UINT nFlags, CPoint point) {
    ::OutputDebugStringW(L"Hit\n");
}

BEGIN_MESSAGE_MAP(CMyButton, CButton)
    ON_WM_MOUSEMOVE()
END_MESSAGE_MAP()


在调试器下运行这个程序,每当鼠标在CMyButton控件上移动时,调试器output window中就会显示Hit。这是一个进步,但是问题还没有解决。
回想一下,鼠标消息只会传递到鼠标光标下的窗口。由于上面的实现只与按钮控件有关,因此我们不必再仔细检查鼠标是否在控件内部。尽管如此,它还是引入了一个新的问题:我们如何知道鼠标何时移出了控件?
要解决此问题,我们需要联系系统:它可以在任何时候发布WM_MOUSELEAVE消息。但是事情并不像将ON_WM_MOUSELEAVE()放到我们的消息Map中并完成它那么简单。我们必须通过调用TrackMouseEvent来主动请求鼠标跟踪。复杂性并没有到此为止。WM_MOUSELEAVE是一次性通知,一旦消息生成,跟踪请求就会被取消。每当鼠标进入我们的控件时,我们都必须重新请求跟踪。而there is no WM_MOUSEENTER message也必须在内部记录状态信息。
这是一个相当多的信息需要消化。幸运的是,实现并不像前面的解释那么令人生畏。下面是下一个迭代:

  • CMy按钮.h*
#pragma once

class CMyButton : public CButton
{
    // ...
private:
    // ...
    void OnMouseLeave();

    // State tracking
    bool m_IsMouseInside = false;
};

  • CMyButton.cpp *(英文)
void CMyButton::OnMouseMove(UINT nFlags, CPoint point)
{
    if (!m_IsMouseInside)
    {
        ::OutputDebugStringW(L"WM_MOUSEENTER\n");

        // Request mouse tracking
        auto tme = TRACKMOUSEEVENT { sizeof(TRACKMOUSEEVENT), TME_LEAVE, m_hWnd, 0 };
        ::TrackMouseEvent(&tme);

        // Update state
        m_IsMouseInside = true;
    }
}

void CMyButton::OnMouseLeave()
{
    ::OutputDebugStringW(L"WM_MOUSELEAVE\n");

    // Update state
    m_IsMouseInside = false;
}

BEGIN_MESSAGE_MAP(CMyButton, CButton)
    ON_WM_MOUSEMOVE()
    ON_WM_MOUSELEAVE()
END_MESSAGE_MAP()


这还不算太糟。代码仍然相当紧凑,浏览一下就可以推断出它的意图。这出奇地容易实现。
可疑的容易。

实际上,这是一个回归,默认的悬停高亮显示从一个按钮消失了,该按钮的行为通过CMyButton的实现重定向(enabling visual styles使这很容易观察)。
这个缺陷不是刚刚才介绍的。它是在我们开始定制行为时开始出现的,我们在第一次迭代中选择处理WM_MOUSEMOVE消息。OnMouseMove处理程序做的不多。关键的细节不在于它做了什么。而在于它不做什么:也就是说,获取默认实现。MFC子类链表示为类层次结构。为了让默认实现回到循环中,我们需要调用基类:

  • CMy按钮.h*
using BaseClass = CButton;

class CMyButton : public BaseClass
{
    // ...
};

  • CMyButton.cpp *(英文)
void CMyButton::OnMouseMove(UINT nFlags, CPoint point)
{
    // ...

    BaseClass::OnMouseMove(nFlags, point);
}

void CMyButton::OnMouseLeave()
{
    // ...

    BaseClass::OnMouseLeave();
}

BEGIN_MESSAGE_MAP(CMyButton, BaseClass)
    // ...
END_MESSAGE_MAP()


我继续介绍了BaseClass类型别名,如果我们决定从一个不同的类派生CMyButton,那么所需要的只是修改一行代码。
1* 除非该窗口被禁用(参见EnableWindow),或者鼠标输入被其他窗口捕获(参见SetCapture)。*
[2]这一主题仍然很复杂,工具无法使使用者摆脱理解其内在和复杂性的必要性。
3* 以下内容源自CButton,仅用于说明。CMFCButton更适合,因为它已经提供了我们手动实现的自定义点。*
4* 事后看来,我应该添加.h/.cpp文件并手动记下。*

qni6mghb

qni6mghb2#

你可能想要这个:

...
    // Check if the mouse is over the button
    if (pButton != nullptr && pButton->GetSafeHwnd() != nullptr) {
        // pButton->EnableWindow(false);
        pButton->GetWindowRect(&rect);  // get button rectangle
        ScreenToClient(rect);           // convert it to coordinates relative 
                                        // to dialog client rectangle

        // point is already relative to the dialog's client rectangle
        TRACE("%d, %d\n", point.x, point.y);

        if (rect.PtInRect(point)) {
            // Change the button's color when the mouse hovers over it
            int i = 10;
            pButton->Invalidate();
        }
        else {
            // Revert to the default color when the mouse leaves
            int i = 20;
            pButton->Invalidate();
        }
    }
    ...

字符串
但还有一个问题:OnMouseMove只会在鼠标指针不在可点击控件上时被调用,因为鼠标移动事件被传递到鼠标光标下的窗口(注意按钮 * 是 * 一个窗口)。你可以通过查看调试输出来检查这一点,其中将显示TRACE s。
如果取消注解pButton->EnableWindow(false);,按钮将被禁用,即使您将鼠标悬停在按钮上,鼠标移动事件也将被传递到对话框窗口(而不是按钮)。这不是真正有帮助的,它只是为了演示。
你要找的真实的答案是here

ih99xse1

ih99xse13#

正如许多人所指出的,OnMouseMove()传递了一个在对话框客户端坐标中的点。不要为按钮调用GetClientRect(),这没有意义,因为左上角的点总是(0,0)-这个函数基本上返回控件的客户端区域大小。相反,为按钮调用GetWindowRect(),它返回窗口的rect屏幕坐标。然后当然你必须将它们转换为对话框客户端坐标,或者将point的客户端坐标转换为屏幕,然后检查点是否位于按钮的矩形中。

pkwftd7m

pkwftd7m4#

已更新答案。
这应该可以:

CRect winRect;
this->GetClientRect(&winRect); // get client rect of window

CRect buttonRect;
pButton->GetClientRect(&buttonRect);

// I believe this cast is safe
pButton->MapWindowPoints(this, (LPPOINT)&buttonRect, 2);

// buttonRect is now in client window coordinates
bool isPointOnButton = buttonRect.PtInRect(point);

字符串

相关问题