#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
首次运行
-
运行 ./gsclicker。
-
若提示没有权限,去: 系统设置 → 隐私与安全性 → 辅助功能
-
勾选你用的 终端(Terminal / iTerm2)。
-
完全退出终端并重开,再运行程序。
-
菜单选 3 开始连点,按 F6 也可以切换。
由于 技术 原因,暂时无法实现窗口

粤公网安备44010602015266号