2人参与 • 2026-09-18 • C/C++
说完了键盘,咱们来填鼠标的坑。日常开发里,左键右键中键用得最勤,滚轮上下翻页也是刚需。至于侧键(前进/后退),捎带手讲一下就行,不喧宾夺主。
windows 下捕获鼠标输入,路子有好几条,各有利弊。下面我把左键、右键、中键、滚轮滚动这四种最常见的操作,用不同方法挨个捋一遍。
在动手之前,先把用到的常数列出来,后面就不重复解释了:
| 按键/操作 | 虚拟键码(getasynckeystate) | windows消息(窗口过程) |
|---|---|---|
| 左键 | vk_lbutton | wm_lbuttondown / wm_lbuttonup |
| 右键 | vk_rbutton | wm_rbuttondown / wm_rbuttonup |
| 中键(按下) | vk_mbutton | wm_mbuttondown / wm_mbuttonup |
| 滚轮滚动 | ❌ 没有对应的虚拟键码 | wm_mousewheel(垂直滚动) wm_mousehwheel(水平滚动) |
| 侧键1(后退) | vk_xbutton1 | wm_xbuttondown / wm_xbuttonup |
| 侧键2(前进) | vk_xbutton2 | wm_xbuttondown / wm_xbuttonup |
注意一个关键点:滚轮没有虚拟键码,所以 getasynckeystate 拿滚轮没办法。想检测滚轮,必须走消息机制(窗口消息或者钩子)。这个很多人一上来就踩坑。
如果你在写游戏循环或者后台监控线程,不想折腾窗口,用这个最省事。它只查“此时此刻”某个键是否按着。
优点:代码就一行,不需要窗口,没有回调。
缺点:只适用于按键(左/右/中/侧键),滚轮没法查。而且需要你自己写轮询循环,吃cpu(记得加sleep)。
#include <windows.h>
#include <iostream>
#define key_down(vk) ((getasynckeystate(vk) & 0x8000) != 0)
int main() {
std::cout << "轮询监控开始,按 esc 退出..." << std::endl;
while (true) {
// 三个主键
if (key_down(vk_lbutton))
std::cout << "左键按住" << std::endl;
if (key_down(vk_rbutton))
std::cout << "右键按住" << std::endl;
if (key_down(vk_mbutton))
std::cout << "中键按住" << std::endl;
// 侧键顺便带一下
if (key_down(vk_xbutton1))
std::cout << "侧键1(后退)按住" << std::endl;
if (key_down(vk_xbutton2))
std::cout << "侧键2(前进)按住" << std::endl;
if (key_down(vk_escape))
break;
sleep(10); // 别把cpu干到100%
}
return 0;
}
这里多说一句:getasynckeystate 返回的 short,最高位是1表示当前按着。有时候看网上有人写 & 0x8000,也有人写 & 1,后者是查“自从上次调用后有没有被按过”,那个是一次性 事件,容易漏消息,平时检测按住状态就用 0x8000。
如果你已经有窗口了(mfc、qt 的 nativeevent、或者纯 win32),消息处理是最干净的方式。按键和滚轮都能覆盖,而且不占cpu。
case wm_lbuttondown:
// 左键按下,lparam 里是坐标
outputdebugstring(l"左键按下\n");
break;
case wm_rbuttondown:
outputdebugstring(l"右键按下\n");
break;
case wm_mbuttondown:
outputdebugstring(l"中键按下\n");
break;
滚轮的消息稍微麻烦一丢丢,因为需要判断滚动方向和滚动量。
case wm_mousewheel: {
// wparam 的低字是辅助键状态(ctrl/shift等),高字是滚轮增量
int delta = get_wheel_delta_wparam(wparam);
// delta > 0 表示向上滚,delta < 0 表示向下滚
// 通常一个"格"是 120,但有些鼠标驱动支持精细滚动,可能更小
// 顺便拿一下鼠标位置(屏幕坐标)
point pt;
pt.x = get_x_lparam(lparam);
pt.y = get_y_lparam(lparam);
// 注意这个坐标是屏幕坐标,如果想转成客户区坐标需要 screentoclient
if (delta > 0) {
outputdebugstring(l"滚轮向上滚动");
// 可以计算滚了几格: delta / wheel_delta
int lines = delta / wheel_delta;
} else {
outputdebugstring(l"滚轮向下滚动");
int lines = -delta / wheel_delta;
}
break;
}
几个要点:
get_wheel_delta_wparam 拿到的是累计增量,通常以 120 为单位。为什么是 120?微软定的,方便鼠标驱动做精细控制(比如有些高端鼠标可以按 1 度 1 度地滚)。wm_mousehwel,用 get_wheel_delta_wparam 同样拿增量,正数表示向右,负数表示向左。lresult callback wndproc(hwnd hwnd, uint msg, wparam wparam, lparam lparam) {
switch (msg) {
case wm_lbuttondown:
outputdebugstring(l"[消息] 左键按下\n");
return 0;
case wm_rbuttondown:
outputdebugstring(l"[消息] 右键按下\n");
return 0;
case wm_mbuttondown:
outputdebugstring(l"[消息] 中键按下\n");
return 0;
case wm_mousewheel: {
int delta = get_wheel_delta_wparam(wparam);
if (delta > 0)
outputdebugstring(l"[消息] 滚轮向上\n");
else
outputdebugstring(l"[消息] 滚轮向下\n");
return 0;
}
case wm_xbuttondown: {
// 侧键也顺带一提,不多占篇幅
uint btn = get_xbutton_wparam(wparam);
if (btn == xbutton1) outputdebugstring(l"[消息] 侧键1\n");
else if (btn == xbutton2) outputdebugstring(l"[消息] 侧键2\n");
return 0;
}
case wm_destroy:
postquitmessage(0);
return 0;
}
return defwindowproc(hwnd, msg, wparam, lparam);
}
如果你的程序没有窗口,或者想监控整个系统的鼠标动作(不管焦点在哪),就得用钩子。这个法子能覆盖全局的按键和滚轮。
优点:全局监控,不需要窗口。
缺点:需要管理员权限(部分系统),回调里不能干重活,否则系统鼠标会卡顿。
#include <windows.h>
#include <iostream>
hhook g_hook = null;
lresult callback mouseproc(int ncode, wparam wparam, lparam lparam) {
if (ncode >= 0) {
msllhookstruct* p = (msllhookstruct*)lparam;
switch (wparam) {
case wm_lbuttondown:
std::cout << "[钩子] 全局左键按下" << std::endl;
break;
case wm_rbuttondown:
std::cout << "[钩子] 全局右键按下" << std::endl;
break;
case wm_mbuttondown:
std::cout << "[钩子] 全局中键按下" << std::endl;
break;
case wm_mousewheel: {
// 注意:滚轮增量在 msllhookstruct 的 mousedata 字段里
int delta = get_wheel_delta_wparam(p->mousedata);
if (delta > 0)
std::cout << "[钩子] 全局滚轮向上" << std::endl;
else
std::cout << "[钩子] 全局滚轮向下" << std::endl;
break;
}
case wm_xbuttondown: {
uint btn = get_xbutton_wparam(p->mousedata);
if (btn == xbutton1)
std::cout << "[钩子] 全局侧键1" << std::endl;
else if (btn == xbutton2)
std::cout << "[钩子] 全局侧键2" << std::endl;
break;
}
}
}
return callnexthookex(g_hook, ncode, wparam, lparam);
}
int main() {
g_hook = setwindowshookex(wh_mouse_ll, mouseproc, getmodulehandle(null), 0);
if (!g_hook) {
std::cout << "钩子安装失败,尝试管理员身份运行" << std::endl;
return 1;
}
std::cout << "钩子已安装,esc退出..." << std::endl;
msg msg;
while (getmessage(&msg, null, 0, 0)) {
translatemessage(&msg);
dispatchmessage(&msg);
}
unhookwindowshookex(g_hook);
return 0;
}
钩子的 msllhookstruct 里,mousedata 这个字段比较杂:
所以拿到后统一用 get_wheel_delta_wparam 或 get_xbutton_wparam 去解就行,windows 已经帮我们分好了。
很多人纠结“滚轮滚动一格,delta 一定等于 120 吗?”其实不一定。大多数普通鼠标是一格 120,但如果你用的是罗技 g 系列那种高精度滚轮,系统可能一次给你发多个 120 的累积值,或者发 30、60 这样的精细值。正确的做法是把 delta 累加,累加到 120 的倍数时当作完整一格处理,不要假设每次都是一格。
下面是一个常见的“把滚轮累积成格数”的小技巧:
static int accumulated = 0;
case wm_mousewheel: {
int delta = get_wheel_delta_wparam(wparam);
accumulated += delta;
while (accumulated >= wheel_delta) {
// 向上滚动一格
accumulated -= wheel_delta;
}
while (accumulated <= -wheel_delta) {
// 向下滚动一格
accumulated += wheel_delta;
}
break;
}
这样不管你鼠标发多少细碎增量,最后都会“凑整”成一格一格来处理,不会丢手感。
| 方法 | 左/右/中键 | 滚轮滚动 | 侧键 | 是否全局 | 是否需要窗口 | 性能开销 | 典型场景 |
|---|---|---|---|---|---|---|---|
| getasynckeystate 轮询 | ✅ 支持 | ❌ 不支持 | ✅ 支持 | ❌ 仅本进程 | ❌ 不需要 | 极低(轮询间隔决定) | 游戏循环、无窗口的后台热键 |
| 窗口消息 wm_* | ✅ 支持 | ✅ 支持(wm_mousewheel) | ✅ 支持 | ❌ 仅自身窗口客户区 | ✅ 必须有 | 极低(消息驱动) | 桌面软件、编辑器、gui工具 |
| 低级钩子 wh_mouse_ll | ✅ 支持 | ✅ 支持 | ✅ 支持 | ✅ 全局 | ❌ 不需要 | 中等(每条消息都进回调) | 全局手势软件、鼠标增强工具 |
选型一句话:
下面把三种方式揉到一个程序里,你可以根据自己的需求切换模式,直接复制编译就能跑。
#include <windows.h>
#include <iostream>
#include <atomic>
// ==================== 方法1:轮询 ====================
#define key_down(vk) ((getasynckeystate(vk) & 0x8000) != 0)
void runpollingmode() {
std::cout << "[轮询模式] 按 esc 退出" << std::endl;
while (true) {
if (key_down(vk_lbutton)) std::cout << "[轮询] 左键" << std::endl;
if (key_down(vk_rbutton)) std::cout << "[轮询] 右键" << std::endl;
if (key_down(vk_mbutton)) std::cout << "[轮询] 中键" << std::endl;
if (key_down(vk_xbutton1)) std::cout << "[轮询] 侧键1" << std::endl;
if (key_down(vk_xbutton2)) std::cout << "[轮询] 侧键2" << std::endl;
// 滚轮在轮询模式下无解,这里就不硬写了
if (key_down(vk_escape)) break;
sleep(10);
}
}
// ==================== 方法2:窗口消息 ====================
lresult callback wndproc(hwnd hwnd, uint msg, wparam wparam, lparam lparam) {
static int wheelaccum = 0;
switch (msg) {
case wm_lbuttondown:
outputdebugstring(l"[窗口] 左键按下\n");
return 0;
case wm_rbuttondown:
outputdebugstring(l"[窗口] 右键按下\n");
return 0;
case wm_mbuttondown:
outputdebugstring(l"[窗口] 中键按下\n");
return 0;
case wm_mousewheel: {
int delta = get_wheel_delta_wparam(wparam);
wheelaccum += delta;
while (wheelaccum >= wheel_delta) {
outputdebugstring(l"[窗口] 滚轮向上滚动一格\n");
wheelaccum -= wheel_delta;
}
while (wheelaccum <= -wheel_delta) {
outputdebugstring(l"[窗口] 滚轮向下滚动一格\n");
wheelaccum += wheel_delta;
}
return 0;
}
case wm_xbuttondown: {
uint btn = get_xbutton_wparam(wparam);
if (btn == xbutton1) outputdebugstring(l"[窗口] 侧键1(后退)\n");
else if (btn == xbutton2) outputdebugstring(l"[窗口] 侧键2(前进)\n");
return 0;
}
case wm_destroy:
postquitmessage(0);
return 0;
}
return defwindowproc(hwnd, msg, wparam, lparam);
}
void runwindowmode() {
wndclass wc = {};
wc.lpfnwndproc = wndproc;
wc.hinstance = getmodulehandle(null);
wc.lpszclassname = l"mousedemo";
registerclass(&wc);
hwnd hwnd = createwindowex(0, l"mousedemo", l"鼠标测试窗口",
ws_overlappedwindow, 100, 100, 400, 300,
null, null, wc.hinstance, null);
if (!hwnd) {
std::cout << "创建窗口失败" << std::endl;
return;
}
showwindow(hwnd, sw_show);
updatewindow(hwnd);
std::cout << "[窗口模式] 请在窗口内点击或滚动" << std::endl;
msg msg;
while (getmessage(&msg, null, 0, 0)) {
translatemessage(&msg);
dispatchmessage(&msg);
}
}
// ==================== 方法3:低级钩子 ====================
hhook g_hook = null;
lresult callback hookproc(int ncode, wparam wparam, lparam lparam) {
if (ncode >= 0) {
msllhookstruct* p = (msllhookstruct*)lparam;
switch (wparam) {
case wm_lbuttondown: std::cout << "[钩子] 全局左键" << std::endl; break;
case wm_rbuttondown: std::cout << "[钩子] 全局右键" << std::endl; break;
case wm_mbuttondown: std::cout << "[钩子] 全局中键" << std::endl; break;
case wm_mousewheel: {
int delta = get_wheel_delta_wparam(p->mousedata);
std::cout << "[钩子] 全局滚轮" << (delta > 0 ? "向上" : "向下") << std::endl;
break;
}
case wm_xbuttondown: {
uint btn = get_xbutton_wparam(p->mousedata);
const char* name = (btn == xbutton1) ? "侧键1" : "侧键2";
std::cout << "[钩子] 全局" << name << std::endl;
break;
}
}
}
return callnexthookex(g_hook, ncode, wparam, lparam);
}
void runhookmode() {
g_hook = setwindowshookex(wh_mouse_ll, hookproc, getmodulehandle(null), 0);
if (!g_hook) {
std::cout << "[钩子] 安装失败,请以管理员身份运行" << std::endl;
return;
}
std::cout << "[钩子模式] 已安装,esc 退出" << std::endl;
msg msg;
while (getmessage(&msg, null, 0, 0)) {
translatemessage(&msg);
dispatchmessage(&msg);
}
unhookwindowshookex(g_hook);
}
// ==================== 主入口 ====================
int main() {
std::cout << "选择模式: 1-轮询 2-窗口消息 3-全局钩子" << std::endl;
int choice;
std::cin >> choice;
switch (choice) {
case 1: runpollingmode(); break;
case 2: runwindowmode(); break;
case 3: runhookmode(); break;
default: std::cout << "无效选择" << std::endl;
}
return 0;
}
到此这篇关于c++ 捕获鼠标按键(左/右/中键)和滚轮操作的几种路子的文章就介绍到这了,更多相关c++ 捕获鼠标按键和滚轮操作内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
您想发表意见!!点此发布评论
版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。
发表评论