本文详细讲解如何在 MFC 对话框中实现按钮长按检测。主要内容包括:① 为什么 OnMouseMove 在鼠标进入按钮后会失效;② 定时器轮询的设计思路;③ GetCursorPos、GetAsyncKeyState、PtInRect 等关键 API 的使用;④ 进度条控件的实时更新;⑤ 部分代码及注释。
思路一:(未能成功)
利用MFC的ON_WM_MOUSEMOVE()消息及其响应函数OnMouseMove()结合使用,当鼠标移动时,会不停触发ON_WM_MOUSEMOVE()消息,然后不断调用其响应函数,响应函数内填写相关逻辑代码,大致内容为:获取当前鼠标坐标,与按钮所在区域不断对比,若鼠标在按钮区域内,则返回TRUE,让按钮内文本变为:"鼠标来了",(验证了这个后续再写按住和进度条增加)
响应函数代码如下:
cpp
void CMouse_Press_DetectDlg::OnMouseMove(UINT nFlags, CPoint point)
{
CPoint screenPt = point;
ClientToScreen(&screenPt);
CRect rcBtn;
GetDlgItem(IDC_BUTTON1)->GetWindowRect(&rcBtn);
CPoint clientPt = point;
// 【诊断】输出坐标数值
CString dbg;
dbg.Format(_T("client=(%d,%d) screen=(%d,%d) btn=(%d,%d,%d,%d) in=%d"),
clientPt.x, clientPt.y,
screenPt.x, screenPt.y,
rcBtn.left, rcBtn.top, rcBtn.right, rcBtn.bottom,
rcBtn.PtInRect(screenPt));
SetWindowText(dbg); // 临时显示在标题栏
if (rcBtn.PtInRect(screenPt))
{
SetDlgItemText(IDC_BUTTON1, _T("鼠标来了"));
}
else
{
SetDlgItemText(IDC_BUTTON1, _T("按钮"));
}
CDialog::OnMouseMove(nFlags, point);
}
此处需要注意的是:OnMouseMove 的 point 参数是客户区坐标(大窗口) ,而 GetWindowRect 返回的是屏幕坐标 ,两者坐标系不同,必须统一。故需要 CPoint screenPt = point;
ClientToScreen(&screenPt);这样将窗口坐标转化成屏幕坐标。
PtInRect:判断点是否在矩形区域内(包括左、上边界,不包括右、下边界)
理想很美好,现实很残酷。这段代码逻辑上是没什么问题的,但是执行时会出现如下问题:
原始按钮名设定位Button1,在窗口外鼠标活动时,文本仍为Button1,工作正常;
当鼠标移动到窗口内后,文本变成"按钮",仍然正常。
但当鼠标移动到按钮上方时,按钮名称不变,仍为"按钮"二字。
根据显示在标题栏中的坐标信息显示,当鼠标移动到按钮上方时,除非鼠标离开按钮区域,否则坐标不会再变化,即并未不断触发响应函数。
这是因为:鼠标消息(如 WM_MOUSEMOVE)的目标窗口 由光标下方的窗口决定,当鼠标在对话框客户区运动时,消息正常发送,响应函数正常执行。
而当鼠标移动到按钮上方时,按钮是一个独立的子窗口,目标窗口转换为按钮,鼠标消息发送给了按钮,按钮默认不处理这个消息,故不会执行大窗口消息的响应函数OnMouseMove(),自然也不会触发函数里更新标题栏数据,更新按钮文本的代码。
直接解决方案:(未实现)
直接重写Button类,或者强制设置鼠标消息接收目标,后续可能会尝试,会填坑
思路二:(已经实现)
绕过MFC的消息机制,直接采用定时器进行轮询,不依赖鼠标消息的触发,自然也无需担心响应函数是否响应的问题。
具体就是设置一个定时器,固定时间间隔触发,触发执行逻辑其实和上面的是一样的,具体执行为
cpp
void CMouse_Press_DetectDlg::OnTimer(UINT_PTR nID)
{
CPoint screenPt;
GetCursorPos(&screenPt);
CRect rcBtn;
GetDlgItem(IDC_BUTTON1)->GetWindowRect(&rcBtn);
if (rcBtn.PtInRect(screenPt))
SetDlgItemText(IDC_BUTTON1, _T("鼠标来了"));
else
SetDlgItemText(IDC_BUTTON1, _T("按钮"));
}
需要注意的是,定时器需要初始化,故初始化代码为:SetTimer(1, 50, NULL);放置于InitDialog中,消息为:ON_WM_TIMER(),头文件定义OnTimer时需要在返回类型前面加上afx_msg,不过也可留可不留
afx_msg 是 MFC 的文档标记宏,展开后是空的,只用来告诉阅读代码的人"这是一个消息处理函数",去掉也不影响程序运行。但为了遵循 MFC 规范和类向导工具识别,建议保留
然后去实现长按按钮让进度条增长,具体思路如下:
由于原生Button类不存在按住这个状态,故要不还是重写这个类,要不就是去找寻其他方案,这里我选择的是找其他方案:检测鼠标左键状态,当鼠标在按钮上时,然后鼠标左键按下时,触发进度条增长效果,定时器函数实现如下:
cpp
void CMouse_Press_DetectDlg::OnTimer(UINT_PTR nID)
{
CPoint screenPt;
GetCursorPos(&screenPt);
CRect rcBtn;
GetDlgItem(IDC_BUTTON1)->GetWindowRect(&rcBtn);
CPoint clientPt = screenPt;
// 【诊断】输出坐标数值
CString dbg;
dbg.Format(_T("client=(%d,%d) screen=(%d,%d) btn=(%d,%d,%d,%d) in=%d"),
clientPt.x, clientPt.y,
screenPt.x, screenPt.y,
rcBtn.left, rcBtn.top, rcBtn.right, rcBtn.bottom,
rcBtn.PtInRect(screenPt));
SetWindowText(dbg);
if (rcBtn.PtInRect(screenPt))
SetDlgItemText(IDC_BUTTON1, _T("鼠标来了"));
else
SetDlgItemText(IDC_BUTTON1, _T("按钮"));
BOOL bLeftPressed = (GetAsyncKeyState(VK_LBUTTON) & 0x8000) != 0;
int iPos = m_Progress_Slow.GetPos();
if (rcBtn.PtInRect(screenPt) && bLeftPressed)
{
if (iPos < 100)
{
iPos++;
m_Progress_Slow.SetPos(iPos);
}
SetDlgItemText(IDC_BUTTON1, _T("按住中"));
}
}
此时就可以完成,按住按钮,进度条不断增加的效果了。但是逻辑上仍然存在漏洞:
判断语句里写的是:鼠标左键按下,且鼠标在按钮范围内,就可以实现增长,那么满足这个条件的共有两个情况,第一种:想要的情况,在按钮内按住按钮,进度条不断增长;第二种:非常规情况,我在按钮外按下左键,然后在一直按着的情况下将光标移动到按钮上,同样可以实现增长,很明显第二种不是我们想要的,还需要再打一些补丁。
未完待续......