C 语言

关注公众号 jb51net

关闭
首页 > 软件编程 > C 语言 > C++ 捕获鼠标按键和滚轮操作

C++ 捕获鼠标按键(左/右/中键)和滚轮操作的几种路子

作者:ziguo1122

这篇文章手把手教你用GetAsyncKeyState、窗口消息和低级钩子三种方法检测左键、右键、中键、滚轮和侧键,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

说完了键盘,咱们来填鼠标的坑。日常开发里,左键右键中键用得最勤,滚轮上下翻页也是刚需。至于侧键(前进/后退),捎带手讲一下就行,不喧宾夺主。

Windows 下捕获鼠标输入,路子有好几条,各有利弊。下面我把左键、右键、中键、滚轮滚动这四种最常见的操作,用不同方法挨个捋一遍。

一、准备工作:先搞清楚对应的虚拟键码和消息

在动手之前,先把用到的常数列出来,后面就不重复解释了:

按键/操作虚拟键码(GetAsyncKeyState)Windows消息(窗口过程)
左键VK_LBUTTONWM_LBUTTONDOWN / WM_LBUTTONUP
右键VK_RBUTTONWM_RBUTTONDOWN / WM_RBUTTONUP
中键(按下)VK_MBUTTONWM_MBUTTONDOWN / WM_MBUTTONUP
滚轮滚动❌ 没有对应的虚拟键码WM_MOUSEWHEEL(垂直滚动)
WM_MOUSEHWHEEL(水平滚动)
侧键1(后退)VK_XBUTTON1WM_XBUTTONDOWN / WM_XBUTTONUP
侧键2(前进)VK_XBUTTON2WM_XBUTTONDOWN / WM_XBUTTONUP

注意一个关键点:滚轮没有虚拟键码,所以 GetAsyncKeyState 拿滚轮没办法。想检测滚轮,必须走消息机制(窗口消息或者钩子)。这个很多人一上来就踩坑。

二、方法一: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

三、方法二:窗口消息 —— 最正统的 Win32 姿势

如果你已经有窗口了(MFC、Qt 的 nativeEvent、或者纯 Win32),消息处理是最干净的方式。按键和滚轮都能覆盖,而且不占CPU。

3.1 处理三个按键

case WM_LBUTTONDOWN:
    // 左键按下,lParam 里是坐标
    OutputDebugString(L"左键按下\n");
    break;
case WM_RBUTTONDOWN:
    OutputDebugString(L"右键按下\n");
    break;
case WM_MBUTTONDOWN:
    OutputDebugString(L"中键按下\n");
    break;

3.2 重点:滚轮 操作(WM_MOUSEWHEEL)

滚轮的消息稍微麻烦一丢丢,因为需要判断滚动方向和滚动量。

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;
}

几个要点

3.3 完整窗口过程片段

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);
}

四、方法三:低级鼠标钩子 WH_MOUSE_LL —— 全局无窗口拦截

如果你的程序没有窗口,或者想监控整个系统的鼠标动作(不管焦点在哪),就得用钩子。这个法子能覆盖全局的按键和滚轮。

优点:全局监控,不需要窗口。
缺点:需要管理员权限(部分系统),回调里不能干重活,否则系统鼠标会卡顿。

#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++ 捕获鼠标按键和滚轮操作内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文