您的位置:首页 > 其它

监听Mac OS X的全局鼠标事件

2012-06-28 16:18 204 查看
转自:http://www.keakon.net/2011/11/10/%E7%9B%91%E5%90%ACMacOSX%E7%9A%84%E5%85%A8%E5%B1%80%E9%BC%A0%E6%A0%87%E4%BA%8B%E4%BB%B6

因为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点需要注意:

它是异步接收的,因此不能修改事件和阻止事件传播。

这个程序本身不能接收到自己的事件,但可以用+ 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:@"%d", ++rightClicked];
rightClickedText.stringValue = text;
break;
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;
break;
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, void *refcon) {
CGPoint location = CGEventGetLocation(event);
NSInteger windowNumber = [NSWindow windowNumberAtPoint:location belowWindowWithWindowNumber:0];
CGWindowID windowID = (CGWindowID)windowNumber;

CFArrayRef array = CFArrayCreate(NULL, (const void **)&windowID, 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,这样事件就不会被继续传播了。

接着再把右键改成左键:

static CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) {
if (type == kCGEventRightMouseDown) {
CGEventSetType(event, kCGEventLeftMouseDown);
} else {
CGEventSetType(event, kCGEventLeftMouseUp);
}
return event;
}


再试试把它替换成按回车键:

#include <Carbon/Carbon.h>

static CGEventRef createEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) {
CGEventRef newEvent = CGEventCreateKeyboardEvent(NULL, kVK_Return, type == kCGEventRightMouseDown);
return newEvent;
}

注意这里回调函数的名字需要带create或copy,因为我创建了一个对象,却没有释放它。

或者用CGEventTapPostEvent()和CGEventPost()来一次传递2个事件:

static CGEventRef createEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) {
if (type == kCGEventRightMouseDown) {
return NULL;
} else {
CGEventRef newEvent = CGEventCreateKeyboardEvent(NULL, kVK_Return, true);
CGEventPost(kCGSessionEventTap, newEvent);
// CGEventTapPostEvent(proxy, newEvent);
CFRelease(newEvent);

return CGEventCreateKeyboardEvent(NULL, kVK_Return, false);
}
}

注意这里最好是发送到kCGSessionEventTap,这样就不会再被kCGHIDEventTap捕捉到,避免重复捕捉。

如果要使用快捷键,不能直接模拟控制键按下的事件,而是要用CGEventSetFlags来设置。例如下面这段代码可以实现Shift + Command + T:

CGEventRef event = CGEventCreateKeyboardEvent(NULL, kVK_ANSI_T, true);
CGEventSetFlags(event, kCGEventFlagMaskShift | kCGEventFlagMaskCommand);
CGEventPost(kCGSessionEventTap, event);
CFRelease(event);
event = CGEventCreateKeyboardEvent(NULL, kVK_ANSI_T, false);
CGEventSetFlags(event, kCGEventFlagMaskShift | kCGEventFlagMaskCommand);
CGEventPost(kCGSessionEventTap, 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 < 0) {
if (lastDirection != UP) {
directions.push_back(UP);
}
} else if (lastDirection != DOWN) {
directions.push_back(DOWN);
}
}
}

static CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) {
switch (type) {
case kCGEventRightMouseDown:
lastLocation = CGEventGetLocation(event);
break;
case kCGEventRightMouseDragged:
updateDirections(event);
break;
case kCGEventRightMouseUp:
updateDirections(event);
for (vector<DIRECTION>::iterator iter = directions.begin(); iter != directions.end(); ++iter) {
NSLog(@"%d", *iter);
}
NSLog(@"----");
directions.clear();
break;
default:
return event;
break;
}
return NULL;
}


如果不喜欢和CGEventRef打交道的话,也可以转换成NSEvent,不过注意Y轴的方向是相反的:

static NSPoint lastLocation;

static void updateDirections(NSEvent* event) {
NSPoint newLocation = event.locationInWindow;
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 > 0) {
if (lastDirection != UP) {
directions.push_back(UP);
}
} else if (lastDirection != DOWN) {
directions.push_back(DOWN);
}
}
}

static CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) {
NSEvent *mouseEvent = [NSEvent eventWithCGEvent:event];
switch (mouseEvent.type) {
case NSRightMouseDown:
lastLocation = mouseEvent.locationInWindow;
break;
case NSRightMouseDragged:
updateDirections(mouseEvent);
break;
case NSRightMouseUp:
updateDirections(mouseEvent);
for (vector<DIRECTION>::iterator iter = directions.begin(); iter != directions.end(); ++iter) {
NSLog(@"%d", *iter);
}
NSLog(@"----");
directions.clear();
break;
default:
return event;
break;
}
return NULL;
}

[/code]
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: