因为Mac OS X下没有给力的鼠标手势软件,所以昨天突然想自己实现个玩玩,便研究了一番怎么监听全局的鼠标事件。
首先不能错过的是Cocoa Event-Handling Guide这篇文档。它详细介绍了Mac OS X下的事件机制,这里只简要说一下事件传播的流程。
考虑一个鼠标点击事件。鼠标硬件先接收到用户点击,然后交给鼠标驱动来处理。这个驱动是在Mac OS X内核运行的,处理完就通过I/O Kit传递给window server的事件队列。而window server则负责分派这些事件到对应进程的run-loop。
接着是Stack Overflow的这篇Global Mouse Moved Events in Cocoa,列出了监听全局鼠标事件的2个API。
其中之一是NSEvent的+ addGlobalMonitorForEventsMatchingMask:handler:方法。
这个方法有2点需要注意:
不过拿来做其他的事倒也不错,于是我就写了个记录鼠标使用状况的程序。
先定义一下要用到的属性:
实现也很简单:
最后测试一下,一切工作正常。唯一要担心的就是像素数实在增长太快了,看来显示时应该改下单位,例如转化成米或千米。
另一个方法则是使用CGEventTap,它更为底层,可以修改事件。
先来捕捉事件:
首先是eventMask,它是我要捕捉的事件掩码。这里我只是演示,因此就不捕捉拖动了。
接着是eventTap,它是一个CFMachPort对象,是与Mac OS X内核通信的通道。第一个参数有3种值,注意事件传播是沿着硬件系统 → window server → 用户session → 应用程序这条路径的,每个箭头处都可以捕捉事件,而这个参数就决定了在哪捕捉事件。kCGHeadInsertEventTap是指要放在其他event taps之前,避免事件被修改或停止传播。kCGEventTapOptionDefault是指可以修改或停止传播事件,用kCGEventTapOptionListenOnly的话就和上一种方法一样了。eventCallback是事件发生时会被调用的函数,稍后列出。最后一个参数是传给回调函数的值,这里我用不到,所以设为NULL。
然后是拿eventTap创建一个加到RunLoopSource对象,太抽象了也没啥好解释的。
再是把这个runLoopSource加到当前线程(即主线程)的RunLoop。
后面那行代码其实是多余的,用于启用eventTap。事实上添加到RunLoop后就已经启用了,要停用时可以将参数改成false。
再来看看刚才那个回调函数:
NSEvent可以直接获取windowNumber,但CGEventRef能在事件传递到窗口之前就捕捉到,也就没法知道这个值了。于是我只能用了NSWindow的类方法来获得窗口,再获取该窗口的名字。
最后返回NULL,这样事件就不会被继续传播了。
接着再把右键改成左键:
再试试把它替换成按回车键:
或者用CGEventTapPostEvent()和CGEventPost()来一次传递2个事件:
如果要使用快捷键,不能直接模拟控制键按下的事件,而是要用CGEventSetFlags来设置。例如下面这段代码可以实现Shift + Command + T:
接着附送些鼠标手势的算法和代码:
Mouse Gesture Recognition
鼠标手势算法
smoothgestures-chromium
最后也送上自己实现的一个4方向的手势检测:
如果不喜欢和CGEventRef打交道的话,也可以转换成NSEvent,不过注意Y轴的方向是相反的:
首先不能错过的是Cocoa Event-Handling Guide这篇文档。它详细介绍了Mac OS X下的事件机制,这里只简要说一下事件传播的流程。
考虑一个鼠标点击事件。鼠标硬件先接收到用户点击,然后交给鼠标驱动来处理。这个驱动是在Mac OS X内核运行的,处理完就通过I/O Kit传递给window server的事件队列。而window server则负责分派这些事件到对应进程的run-loop。
接着是Stack Overflow的这篇Global Mouse Moved Events in Cocoa,列出了监听全局鼠标事件的2个API。
其中之一是NSEvent的+ addGlobalMonitorForEventsMatchingMask:handler:方法。
这个方法有2点需要注意:
- 它是异步接收的,因此不能修改事件和阻止事件传播。
- 这个程序本身不能接收到自己的事件,但可以用+ addLocalMonitorForEventsMatchingMask:handler:。
不过拿来做其他的事倒也不错,于是我就写了个记录鼠标使用状况的程序。
先定义一下要用到的属性:
#import <Cocoa/Cocoa.h> @interface AppDelegate : NSObject <NSApplicationDelegate> { @private NSInteger leftClicked; NSInteger rightClicked; NSInteger moved; NSTextField *leftClickedText; NSTextField *rightClickedText; NSTextField *movedText; } @property (assign) IBOutlet NSWindow *window; @property (retain) IBOutlet NSTextField *leftClickedText; @property (retain) IBOutlet NSTextField *rightClickedText; @property (retain) IBOutlet NSTextField *movedText; @end其中3个整数分别记录左键点击数、右键点击数和移动的像素数,而3个NSTextField则是用于显示它们的。
实现也很简单:
#include "math.h" #import "AppDelegate.h" @implementation AppDelegate @synthesize window = _window; @synthesize leftClickedText; @synthesize rightClickedText; @synthesize movedText; - (void)dealloc { [super dealloc]; [leftClickedText release]; [rightClickedText release]; [movedText release]; } - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { [NSEvent addGlobalMonitorForEventsMatchingMask:NSLeftMouseDownMask | NSRightMouseDownMask | NSMouseMovedMask | NSLeftMouseDraggedMask | NSRightMouseDraggedMask handler:^(NSEvent *event) { Nsstring *text; NSInteger delta; switch (event.type) { case NSLeftMouseDown: text = [[Nsstring alloc] initWithFormat:@"%d",++leftClicked]; leftClickedText.stringValue = text; break; case NSRightMouseDown: text = [[Nsstring alloc] initWithFormat:@ottom:0px; margin-left:0px; padding-top:0px; padding-right:0px; padding-bottom:0px; padding-left:0px; border-top-width:0px; border-right-width:0px; border-bottom-width:0px; border-left-width:0px; border-style:initial; border-color:initial; outline-width:0px; outline-style:initial; outline-color:initial; vertical-align:baseline; background-color:transparent; color:rgb(101,++rightClicked]; rightClickedText.stringValue = text; case NSMouseMoved: case NSLeftMouseDragged: case NSRightMouseDragged: delta = (NSInteger)sqrt(event.deltaX * event.deltaX + event.deltaY * event.deltaY); moved += delta; text = [[Nsstring alloc] initWithFormat:@"%d px",moved]; movedText.stringValue = text; default: break; } [text release]; }]; } @end唯一要解释的就是NSLeftMouseDragged和NSRightMouseDragged,当按下按键并移动鼠标时,会触发这2个事件,而不是NSMouseMoved,所以要合并在一起。
最后测试一下,一切工作正常。唯一要担心的就是像素数实在增长太快了,看来显示时应该改下单位,例如转化成米或千米。
另一个方法则是使用CGEventTap,它更为底层,可以修改事件。
先来捕捉事件:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification { CGEventMask eventMask = CGEventMaskBit(kCGEventRightMouseDown) | CGEventMaskBit(kCGEventRightMouseUp); CFMachPortRef eventTap = CGEventTapCreate(kCGHIDEventTap,kCGHeadInsertEventTap,kCGEventTapOptionDefault,eventMask,eventCallback,NULL); CFRunLoopSourceRef runLoopSource = CFMachPortCreateRunLoopSource(kcfAllocatorDefault,eventTap,0); CFRunLoopAddSource(CFRunLoopGetCurrent(),runLoopSource,kcfRunLoopCommonModes); CGEventTapEnable(eventTap,true); CFRelease(eventTap); CFRelease(runLoopSource); }这段代码有点复杂,于是逐行来解释。
首先是eventMask,它是我要捕捉的事件掩码。这里我只是演示,因此就不捕捉拖动了。
接着是eventTap,它是一个CFMachPort对象,是与Mac OS X内核通信的通道。第一个参数有3种值,注意事件传播是沿着硬件系统 → window server → 用户session → 应用程序这条路径的,每个箭头处都可以捕捉事件,而这个参数就决定了在哪捕捉事件。kCGHeadInsertEventTap是指要放在其他event taps之前,避免事件被修改或停止传播。kCGEventTapOptionDefault是指可以修改或停止传播事件,用kCGEventTapOptionListenOnly的话就和上一种方法一样了。eventCallback是事件发生时会被调用的函数,稍后列出。最后一个参数是传给回调函数的值,这里我用不到,所以设为NULL。
然后是拿eventTap创建一个加到RunLoopSource对象,太抽象了也没啥好解释的。
再是把这个runLoopSource加到当前线程(即主线程)的RunLoop。
后面那行代码其实是多余的,用于启用eventTap。事实上添加到RunLoop后就已经启用了,要停用时可以将参数改成false。
再来看看刚才那个回调函数:
static CGEventRef eventCallback(CGEventTapProxy proxy,CGEventType type,CGEventRef event,100)">void *refcon) { CGPoint location = CGEventGetLocation(event); NSInteger windowNumber = [NSWindow windowNumberAtPoint:location belowWindowWithWindowNumber:0]; CGWindowID windowID = (CGWindowID)windowNumber; CFArrayRef array = CFArrayCreate(NULL,(const void **)&windowID,204)">1,NULL); NSArray *windowInfos = (NSArray *)CGWindowListCreateDescriptionFromArray(array); CFRelease(array); if (windowInfos.count > 0) { NSDictionary *windowInfo = [windowInfos objectAtIndex:0]; NSLog(@"Window name: %@",[windowInfo objectForKey:(Nsstring *)kCGWindowName]); NSLog(@"Window owner: %@",[windowInfo objectForKey:(Nsstring *)kCGWindowOwnerName]); } [windowInfos release]; return NULL; }这一大段代码实际上是获取鼠标坐标对应的窗口信息的。
NSEvent可以直接获取windowNumber,但CGEventRef能在事件传递到窗口之前就捕捉到,也就没法知道这个值了。于是我只能用了NSWindow的类方法来获得窗口,再获取该窗口的名字。
最后返回NULL,这样事件就不会被继续传播了。
接着再把右键改成左键:
void *refcon) { if (type == kCGEventRightMouseDown) { CGEventSetType(event,kCGEventLeftMouseDown); } else { CGEventSetType(event,kCGEventLeftMouseUp); } return event; }
再试试把它替换成按回车键:
#include <Carbon/Carbon.h> static CGEventRef createEventCallback(CGEventTapProxy proxy,100)">void *refcon) { CGEventRef newEvent = CGEventCreateKeyboardEvent(NULL,kVK_Return,type == kCGEventRightMouseDown); return newEvent; }注意这里回调函数的名字需要带create或copy,因为我创建了一个对象,却没有释放它。
或者用CGEventTapPostEvent()和CGEventPost()来一次传递2个事件:
if (type == kCGEventRightMouseDown) { return NULL; } else { CGEventRef newEvent = CGEventCreateKeyboardEvent(NULL,100)">true); CGEventPost(kCGSessionEventTap,newEvent); // CGEventTapPostEvent(proxy,newEvent); CFRelease(newEvent); return CGEventCreateKeyboardEvent(NULL,100)">false); } }注意这里最好是发送到kCGSessionEventTap,这样就不会再被kCGHIDEventTap捕捉到,避免重复捕捉。
如果要使用快捷键,不能直接模拟控制键按下的事件,而是要用CGEventSetFlags来设置。例如下面这段代码可以实现Shift + Command + T:
CGEventRef event = CGEventCreateKeyboardEvent(NULL,kVK_ANSI_T,100)">true); CGEventSetFlags(event,kCGEventFlagMaskShift | kCGEventFlagMaskCommand); CGEventPost(kCGSessionEventTap,event); CFRelease(event); event = CGEventCreateKeyboardEvent(NULL,100)">false); CGEventSetFlags(event,event); CFRelease(event);
接着附送些鼠标手势的算法和代码:
Mouse Gesture Recognition
鼠标手势算法
smoothgestures-chromium
最后也送上自己实现的一个4方向的手势检测:
typedef enum { NONE,RIGHT,UP,LEFT,DOWN,} DIRECTION; static vector<DIRECTION> directions; static CGPoint lastLocation; static void updateDirections(CGEventRef event) { CGPoint newLocation = CGEventGetLocation(event); float deltaX = newLocation.x - lastLocation.x; float deltaY = newLocation.y - lastLocation.y; float absX = fabs(deltaX); float absY = fabs(deltaY); if (absX + absY < 20) { return; } lastLocation = newLocation; DIRECTION lastDirection = directions.empty() ? NONE : directions.back(); if (absX > absY) { if (deltaX > 0) { if (lastDirection != RIGHT) { directions.push_back(RIGHT); } } else if (lastDirection != LEFT) { directions.push_back(LEFT); } } else { if (deltaY < if (lastDirection != UP) { directions.push_back(UP); } } if (lastDirection != DOWN) { directions.push_back(DOWN); } } } switch (type) { case kCGEventRightMouseDown: lastLocation = CGEventGetLocation(event); break; case kCGEventRightMouseDragged: updateDirections(event); case kCGEventRightMouseUp: updateDirections(event); for (vector<DIRECTION>::iterator iter = directions.begin(); iter != directions.end(); ++iter) { NSLog(@ottom:0px; margin-left:0px; padding-top:0px; padding-right:0px; padding-bottom:0px; padding-left:0px; border-top-width:0px; border-right-width:0px; border-bottom-width:0px; border-left-width:0px; border-style:initial; border-color:initial; outline-width:0px; outline-style:initial; outline-color:initial; vertical-align:baseline; background-color:transparent; color:rgb(101,*iter); } NSLog(@"----"); directions.clear(); default: return event; break; } return NULL; }
如果不喜欢和CGEventRef打交道的话,也可以转换成NSEvent,不过注意Y轴的方向是相反的:
static NSPoint lastLocation; void updateDirections(NSEvent* event) { NSPoint newLocation = event.locationInWindow; if (deltaY > void *refcon) { NSEvent *mouseEvent = [NSEvent eventWithCGEvent:event]; switch (mouseEvent.type) { case NSRightMouseDown: lastLocation = mouseEvent.locationInWindow; case NSRightMouseDragged: updateDirections(mouseEvent); case NSRightMouseUp: updateDirections(mouseEvent); for (vector<DIRECTION>::iterator iter = directions.begin(); iter != directions.end(); ++iter) { NSLog(@ottom:0px; margin-left:0px; padding-top:0px; padding-right:0px; padding-bottom:0px; padding-left:0px; border-top-width:0px; border-right-width:0px; border-bottom-width:0px; border-left-width:0px; border-style:initial; border-color:initial; outline-width:0px; outline-style:initial; outline-color:initial; vertical-align:baseline; background-color:transparent; color:rgb(101,*iter); } NSLog(@return NULL; }
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。