c++版鼠标连点器(MacOS)

caiziling 2026-10-04 14:27:45

#include<ApplicationServices/ApplicationServices.h>
#include<CoreFoundation/CoreFoundation.h>
#include<CoreGraphics/CoreGraphics.h>
#include<algorithm>
#include<atomic>
#include<chrono>
#include<csignal>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<iostream>
#include<mutex>
#include<random>
#include<string>
#include<thread>
#include<vector>
#include<poll.h>
#include<unistd.h>
static std::atomic<bool>      g_isClicking{false};
static std::atomic<bool>      g_shouldExit{false};
static std::atomic<int>       g_delayMs{100};
static std::atomic<int>       g_clickButton{0};   
static std::atomic<long long>g_clickCount{0};
static std::atomic<bool>      g_jitterEnabled{false};
static std::atomic<int>       g_jitterRange{10};
static std::atomic<bool>      g_isRecording{false};
static std::mutex             g_recordMutex;
static std::vector<struct RecordedEvent>g_recordedEvents;
static long long              g_recordStartMs=0;
struct RecordedEvent{
    int       type=0;    
    double    x=0,y=0;
    long long keyCode=0;
    long long scrollDelta=0;
    long long timeMs=0;
};
static std::atomic<CFMachPortRef>g_eventTap{nullptr};
static std::atomic<CFRunLoopRef>  g_tapRunLoop{nullptr};
static std::atomic<bool>g_f6Down{false};
static std::atomic<bool>g_f7Down{false};
static std::atomic<bool>g_escDown{false};
static std::mutex g_printMutex;
static const long long kInjectedTag=0x4753434CLL;   
static long long nowMs(){
    using namespace std::chrono;
    return duration_cast<milliseconds>(
        steady_clock::now().time_since_epoch()).count();
}
static bool isAccessibilityTrusted(bool prompt){
    CFStringRef key=kAXTrustedCheckOptionPrompt;
    CFBooleanRef value=prompt?kCFBooleanTrue:kCFBooleanFalse;
    const void*keys[]   ={key};
    const void*values[]={value};
    CFDictionaryRef opts=CFDictionaryCreate(
        kCFAllocatorDefault,
        keys,values,1,
        &kCFTypeDictionaryKeyCallBacks,
        &kCFTypeDictionaryValueCallBacks);
    if(!opts)return false;
    Boolean trusted=AXIsProcessTrustedWithOptions(opts);
    CFRelease(opts);
    return trusted!=0;
}
static void markInjected(CGEventRef ev){
    if(!ev)return;
    CGEventSetIntegerValueField(ev,kCGEventSourceUserData,kInjectedTag);
}
static bool isInjected(CGEventRef ev){
    if(!ev)return false;
    return CGEventGetIntegerValueField(ev,kCGEventSourceUserData)
           ==kInjectedTag;
}
template<typename...Args>
static void printLine(Args&&...args){
    std::lock_guard<std::mutex>lock(g_printMutex);
    (std::cout<<...<<args)<<std::endl;
}
static void requestExit(){
    g_shouldExit.store(true);
    CFRunLoopRef rl=g_tapRunLoop.load();
    if(rl){
        CFRunLoopStop(rl);
        CFRunLoopWakeUp(rl);
    }
}
static void postClick(int button,CGPoint pt,bool warp){
    if(warp)CGWarpMouseCursorPosition(pt);
    CGEventSourceRef src=CGEventSourceCreate(kCGEventSourceStateHIDSystemState);
    if(!src)return;
    CGEventType   downType,upType;
    CGMouseButton cgBtn;
    switch(button){
        case 0:downType=kCGEventLeftMouseDown;
                upType   =kCGEventLeftMouseUp;
                cgBtn    =kCGMouseButtonLeft;   break;
        case 1:downType=kCGEventOtherMouseDown;
                upType   =kCGEventOtherMouseUp;
                cgBtn    =kCGMouseButtonCenter;break;
        case 2:downType=kCGEventRightMouseDown;
                upType   =kCGEventRightMouseUp;
                cgBtn    =kCGMouseButtonRight;  break;
        default:CFRelease(src);return;
    }
    CGEventRef down=CGEventCreateMouseEvent(src,downType,pt,cgBtn);
    CGEventRef up   =CGEventCreateMouseEvent(src,upType,   pt,cgBtn);
    if(down&&up){
        markInjected(down);
        markInjected(up);
        CGEventPost(kCGHIDEventTap,down);
        CGEventPost(kCGHIDEventTap,up);
    }
    if(down)CFRelease(down);
    if(up)   CFRelease(up);
    CFRelease(src);
}
static void handleHotkey(long long keyCode,bool isDown){
    if(keyCode==97){
        if(isDown){
            if(!g_f6Down.exchange(true)){
                g_isClicking.store(!g_isClicking.load());
                printLine("[热键 F6] 连点 ",
                          g_isClicking.load()?"开启":"关闭");
            }
        }else{
            g_f6Down.store(false);
        }
        return;
    }
    if(keyCode==98){
        if(isDown){
            if(!g_f7Down.exchange(true)){
                if(g_isRecording.load()){
                    g_isRecording.store(false);
                    size_t n;
                    {
                        std::lock_guard<std::mutex>lock(g_recordMutex);
                        n=g_recordedEvents.size();
                    }
                    printLine("[热键 F7] 停止录制,共 ",n," 个事件");
                }else{
                    {
                        std::lock_guard<std::mutex>lock(g_recordMutex);
                        g_recordedEvents.clear();
                        g_recordStartMs=nowMs();
                    }
                    g_isRecording.store(true);
                    printLine("[热键 F7] 开始录制");
                }
            }
        }else{
            g_f7Down.store(false);
        }
        return;
    }
    if(keyCode==53){
        if(isDown){
            if(!g_escDown.exchange(true)){
                printLine("[热键 ESC] 请求退出");
                requestExit();
            }
        }else{
            g_escDown.store(false);
        }
    }
}
static CGEventRef eventTapCallback(CGEventTapProxy proxy,CGEventType type,
                                   CGEventRef event,void*userInfo){
    (void)proxy;
    (void)userInfo;
    if(type==kCGEventTapDisabledByTimeout||
        type==kCGEventTapDisabledByUserInput){
        CFMachPortRef tap=g_eventTap.load();
        if(tap)CGEventTapEnable(tap,true);
        return event;
    }
    if(isInjected(event))return event;
    if(type==kCGEventKeyDown||type==kCGEventKeyUp){
        long long kc=CGEventGetIntegerValueField(
            event,kCGKeyboardEventKeycode);
        handleHotkey(kc,type==kCGEventKeyDown);
    }
    if(g_isRecording.load()){
        RecordedEvent rec;
        rec.timeMs=nowMs()-g_recordStartMs;
        bool has=false;
        CGPoint pt=CGEventGetLocation(event);
        switch(type){
            case kCGEventLeftMouseDown:  rec.type=1;has=true;break;
            case kCGEventLeftMouseUp:    rec.type=2;has=true;break;
            case kCGEventRightMouseDown:rec.type=3;has=true;break;
            case kCGEventRightMouseUp:   rec.type=4;has=true;break;
            case kCGEventOtherMouseDown:rec.type=5;has=true;break;
            case kCGEventOtherMouseUp:   rec.type=6;has=true;break;
            case kCGEventMouseMoved:
            case kCGEventLeftMouseDragged:
            case kCGEventRightMouseDragged:
            case kCGEventOtherMouseDragged:
                rec.type=0;has=true;break;
            case kCGEventKeyDown:
                rec.type=7;
                rec.keyCode=CGEventGetIntegerValueField(
                    event,kCGKeyboardEventKeycode);
                has=true;break;
            case kCGEventKeyUp:
                rec.type=8;
                rec.keyCode=CGEventGetIntegerValueField(
                    event,kCGKeyboardEventKeycode);
                has=true;break;
            case kCGEventScrollWheel:
                rec.type=9;
                rec.scrollDelta=CGEventGetIntegerValueField(
                    event,kCGScrollWheelEventDeltaAxis1);
                has=true;break;
            default:break;
        }
        if(has){
            rec.x=pt.x;
            rec.y=pt.y;
            std::lock_guard<std::mutex>lock(g_recordMutex);
            if(rec.type==0&&!g_recordedEvents.empty()){
                long long last=g_recordedEvents.back().timeMs;
                if(rec.timeMs-last<10)return event;
            }
            g_recordedEvents.push_back(rec);
        }
    }
    return event;
}
static void tapThreadFunc(){
    CGEventMask mask=
        CGEventMaskBit(kCGEventLeftMouseDown)     |
        CGEventMaskBit(kCGEventLeftMouseUp)       |
        CGEventMaskBit(kCGEventRightMouseDown)    |
        CGEventMaskBit(kCGEventRightMouseUp)      |
        CGEventMaskBit(kCGEventOtherMouseDown)    |
        CGEventMaskBit(kCGEventOtherMouseUp)      |
        CGEventMaskBit(kCGEventMouseMoved)        |
        CGEventMaskBit(kCGEventLeftMouseDragged)  |
        CGEventMaskBit(kCGEventRightMouseDragged)|
        CGEventMaskBit(kCGEventOtherMouseDragged)|
        CGEventMaskBit(kCGEventScrollWheel)       |
        CGEventMaskBit(kCGEventKeyDown)           |
        CGEventMaskBit(kCGEventKeyUp);
    CFMachPortRef tap=CGEventTapCreate(
        kCGSessionEventTap,
        kCGHeadInsertEventTap,
        kCGEventTapOptionDefault,
        mask,
        eventTapCallback,
        nullptr);
    if(!tap){
        printLine("错误:无法创建事件监听。请在"
                  "「系统设置 → 隐私与安全性 → 辅助功能」中"
                  "勾选本程序(终端)。");
        return;
    }
    g_eventTap.store(tap);
    CFRunLoopSourceRef src=CFMachPortCreateRunLoopSource(
        kCFAllocatorDefault,tap,0);
    if(!src){
        printLine("错误:无法创建 run loop source。");
        CFRelease(tap);
        g_eventTap.store(nullptr);
        return;
    }
    CFRunLoopRef rl=CFRunLoopGetCurrent();
    g_tapRunLoop.store(rl);
    CFRunLoopAddSource(rl,src,kCFRunLoopCommonModes);
    CGEventTapEnable(tap,true);
    if(!g_shouldExit.load()){
        CFRunLoopRun();
    }
    CFRunLoopRemoveSource(rl,src,kCFRunLoopCommonModes);
    CFRelease(src);
    CGEventTapEnable(tap,false);
    CFRelease(tap);
    g_eventTap.store(nullptr);
    g_tapRunLoop.store(nullptr);
}
static void clickerLoop(){
    std::mt19937 rng(std::random_device{}());
    while(!g_shouldExit.load()){
        if(!g_isClicking.load()){
            std::this_thread::sleep_for(std::chrono::milliseconds(10));
            continue;
        }
        CGPoint pt=CGPointMake(0,0);
        CGEventRef cur=CGEventCreate(nullptr);
        if(cur){
            pt=CGEventGetLocation(cur);
            CFRelease(cur);
        }
        postClick(g_clickButton.load(),pt,false);
        g_clickCount.fetch_add(1);
        int d=g_delayMs.load();
        if(g_jitterEnabled.load()&&d>0&&g_jitterRange.load()>0){
            std::uniform_int_distribution<int>dist(
                -g_jitterRange.load(),g_jitterRange.load());
            d+=dist(rng);
            if(d<1)d=1;
        }
        if(d>0){
            std::this_thread::sleep_for(std::chrono::milliseconds(d));
        }
    }
}
static void playbackLoop(const std::vector<RecordedEvent>&events,int speedMs){
    if(events.empty())return;
    double factor=50.0/std::max(1,speedMs);
    long long lastT=0;
    bool first=true;
    for(const auto&ev:events){
        if(g_shouldExit.load())break;
        if(!first){
            long long gap=ev.timeMs-lastT;
            if(gap<0)gap=0;
            long long delay=static_cast<long long>(gap*factor);
            if(delay>0){
                long long remain=delay;
                while(remain>0&&!g_shouldExit.load()){
                    long long chunk=std::min<long long>(remain,30);
                    std::this_thread::sleep_for(
                        std::chrono::milliseconds(chunk));
                    remain-=chunk;
                }
            }
        }
        first=false;
        lastT=ev.timeMs;
        if(g_shouldExit.load())break;
        CGEventSourceRef src=CGEventSourceCreate(
            kCGEventSourceStateHIDSystemState);
        if(!src)continue;
        switch(ev.type){
            case 0:
                CGWarpMouseCursorPosition(CGPointMake(ev.x,ev.y));
                break;
            case 1:case 2:case 3:case 4:case 5:case 6:{
                CGPoint pt=CGPointMake(ev.x,ev.y);
                CGWarpMouseCursorPosition(pt);
                CGEventType t;
                CGMouseButton b;
                switch(ev.type){
                    case 1:t=kCGEventLeftMouseDown;  b=kCGMouseButtonLeft;   break;
                    case 2:t=kCGEventLeftMouseUp;    b=kCGMouseButtonLeft;   break;
                    case 3:t=kCGEventRightMouseDown;b=kCGMouseButtonRight;  break;
                    case 4:t=kCGEventRightMouseUp;   b=kCGMouseButtonRight;  break;
                    case 5:t=kCGEventOtherMouseDown;b=kCGMouseButtonCenter;break;
                    default:t=kCGEventOtherMouseUp;   b=kCGMouseButtonCenter;break;
                }
                CGEventRef e=CGEventCreateMouseEvent(src,t,pt,b);
                if(e){
                    markInjected(e);
                    CGEventPost(kCGHIDEventTap,e);
                    CFRelease(e);
                }
                break;
            }
            case 7:case 8:{
                CGEventRef e=CGEventCreateKeyboardEvent(
                    src,(CGKeyCode)ev.keyCode,ev.type==7);
                if(e){
                    markInjected(e);
                    CGEventPost(kCGHIDEventTap,e);
                    CFRelease(e);
                }
                break;
            }
            case 9:{
                CGPoint pt=CGPointMake(ev.x,ev.y);
                CGWarpMouseCursorPosition(pt);
                CGEventRef e=CGEventCreateScrollWheelEvent2(
                    src,kCGScrollEventUnitLine,1,
                    (int32_t)ev.scrollDelta,0,0);
                if(e){
                    CGEventSetLocation(e,pt);
                    markInjected(e);
                    CGEventPost(kCGHIDEventTap,e);
                    CFRelease(e);
                }
                break;
            }
        }
        CFRelease(src);
    }
}
static const char*buttonName(int b){
    switch(b){
        case 0:return"左键";
        case 1:return"中键";
        case 2:return"右键";
    }
    return"?";
}
static void printMenu(){
    std::lock_guard<std::mutex>lock(g_printMutex);
    std::cout<<"\n===== 自动连点器 (macOS · C++/C API) =====\n";
    std::cout<<"  1. 设置间隔      当前: "<<g_delayMs.load()<<" ms\n";
    std::cout<<"  2. 设置按键      当前: "<<buttonName(g_clickButton.load())<<"\n";
    std::cout<<"  3. "<<(g_isClicking.load()?"停止连点":"开始连点")<<"\n";
    std::cout<<"  4. 抖动          当前: "
              <<(g_jitterEnabled.load()?"开":"关")
              <<"  范围 "<<g_jitterRange.load()<<"\n";
    std::cout<<"  5. "<<(g_isRecording.load()?"停止录制":"开始录制")<<"\n";
    std::cout<<"  6. 回放录制内容\n";
    std::cout<<"  7. 显示状态\n";
    std::cout<<"  0. 退出\n";
    std::cout<<"  热键:F6 连点  F7 录制  ESC 退出\n";
    std::cout<<"请选择:";
    std::cout.flush();
}
static bool readLineWithTimeout(std::string&out,int timeoutMs){
    struct pollfd pfd;
    pfd.fd=STDIN_FILENO;
    pfd.events=POLLIN;
    pfd.revents=0;
    int r=::poll(&pfd,1,timeoutMs);
    if(r>0&&(pfd.revents&POLLIN)){
        out.clear();
        if(!std::getline(std::cin,out)){
            g_shouldExit.store(true);
            return false;
        }
        return true;
    }
    return false;
}
static bool waitInput(std::string&out){
    while(!g_shouldExit.load()){
        if(readLineWithTimeout(out,200))return true;
    }
    return false;
}
static void runInteractive(){
    std::string line;
    while(!g_shouldExit.load()){
        printMenu();
        if(!waitInput(line))break;
        if(line.empty())continue;
        int choice=std::atoi(line.c_str());
        switch(choice){
            case 0:
                requestExit();
                return;
            case 1:{
                std::cout<<"输入延迟 (0-10000 ms):";
                if(!waitInput(line))return;
                int v=std::atoi(line.c_str());
                v=std::max(0,std::min(10000,v));
                g_delayMs.store(v);
                std::cout<<"已设置: "<<v<<" ms\n";
                break;
            }
            case 2:{
                std::cout<<"0=左键 1=中键 2=右键:";
                if(!waitInput(line))return;
                int v=std::atoi(line.c_str());
                if(v>=0&&v<=2){
                    g_clickButton.store(v);
                    std::cout<<"已设置: "<<buttonName(v)<<"\n";
                }else{
                    std::cout<<"无效按键\n";
                }
                break;
            }
            case 3:
                g_isClicking.store(!g_isClicking.load());
                std::cout<<(g_isClicking.load()?"已开始连点"
                                                  :"已停止连点")<<"\n";
                break;
            case 4:{
                std::cout<<"抖动 0=关 1=开:";
                if(!waitInput(line))return;
                g_jitterEnabled.store(std::atoi(line.c_str())!=0);
                std::cout<<"抖动范围 (0-100):";
                if(!waitInput(line))return;
                int r=std::atoi(line.c_str());
                g_jitterRange.store(std::max(0,std::min(100,r)));
                std::cout<<"已设置:抖动 "
                          <<(g_jitterEnabled.load()?"开":"关")
                          <<",范围 "<<g_jitterRange.load()<<"\n";
                break;
            }
            case 5:
                if(g_isRecording.load()){
                    g_isRecording.store(false);
                    size_t n;
                    {
                        std::lock_guard<std::mutex>lock(g_recordMutex);
                        n=g_recordedEvents.size();
                    }
                    std::cout<<"录制结束,共 "<<n<<" 个事件\n";
                }else{
                    {
                        std::lock_guard<std::mutex>lock(g_recordMutex);
                        g_recordedEvents.clear();
                        g_recordStartMs=nowMs();
                    }
                    g_isRecording.store(true);
                    std::cout<<"开始录制(按 F7 或再次选 5 停止)\n";
                }
                break;
            case 6:{
                std::vector<RecordedEvent>events;
                {
                    std::lock_guard<std::mutex>lock(g_recordMutex);
                    events=g_recordedEvents;
                }
                if(events.empty()){
                    std::cout<<"没有录制数据\n";
                    break;
                }
                std::cout<<"输入速度 ms (默认 50,数值越大越快):";
                if(!waitInput(line))return;
                int speed=line.empty()?50
                                         :std::max(1,std::atoi(line.c_str()));
                std::cout<<"开始回放...\n";
                playbackLoop(events,speed);
                std::cout<<"回放结束\n";
                break;
            }
            case 7:
                std::cout<<"状态:连点 "
                          <<(g_isClicking.load()?"开启":"关闭")
                          <<",总点击 "<<g_clickCount.load()<<"\n";
                break;
            default:
                std::cout<<"无效选项\n";
        }
    }
}
static void onSignal(int){
    g_shouldExit.store(true);
}
int main(int argc,char**argv){
    (void)argc;
    (void)argv;
    std::signal(SIGINT,onSignal);
    std::signal(SIGTERM,onSignal);
    std::cout<<"===== 自动连点器 (macOS · C++/C API) =====\n";
    std::cout<<"C++ 标准:"<<__cplusplus<<"\n";
    if(!isAccessibilityTrusted(true)){
        std::cout<<"\n警告:本程序没有「辅助功能」权限。\n"
                     "请前往:系统设置 → 隐私与安全性 → 辅助功能\n"
                     "勾选「终端」(或你用的 Shell),然后重新运行。\n";
    }
    std::thread tapThread(tapThreadFunc);
    for(int i=0;i<50;++i){
        if(g_tapRunLoop.load()||g_shouldExit.load())break;
        std::this_thread::sleep_for(std::chrono::milliseconds(20));
    }
    std::thread clickThread(clickerLoop);
    runInteractive();
    requestExit();
    if(tapThread.joinable())  tapThread.join();
    if(clickThread.joinable())clickThread.join();
    std::cout<<"程序退出。\n";
    return 0;
}

编译与运行

clang++ -std=c++17 -O2 -o gsclicker main.cpp \
    -framework ApplicationServices \
    -framework CoreFoundation \
    -framework CoreGraphics

./gsclicker

首次运行

  1. 运行 ./gsclicker。

  2. 若提示没有权限,去: 系统设置 → 隐私与安全性 → 辅助功能

  3. 勾选你用的 终端(Terminal / iTerm2)。

  4. 完全退出终端并重开,再运行程序。

  5. 菜单选 3 开始连点,按 F6 也可以切换。

由于 技术 原因,暂时无法实现窗口

链接参考