修改 NSEvent 以发送与按下的不同键相对应的键

24

我正在尝试为辅助技术目的(即不用担心,不是键盘记录器)创建一个OS X键盘钩子。

当用户按下一个键时,我想要阻止真正的按键事件并代替发送一个虚拟的按键事件(我选择的字符)。

我有以下代码:

- (void) hookTheKeyboard {
    CGEventMask keyboardMask = CGEventMaskBit(kCGEventKeyDown);
    id eventHandler = [NSEvent addGlobalMonitorForEventsMatchingMask:keyboardMask handler:^(NSEvent *keyboardEvent) {
        NSLog(@"keyDown: %c", [[keyboardEvent characters] characterAtIndex:0]);
        //Want to: Stop the keyboard input
        //Want to: Send another key input instead
    }];
}
任何帮助实现这两个目标吗?基本上是修改NSEvent "keyboardEvent" 发送不同的字符。谢谢。
3个回答

57
你不能使用 NSEvent API 来完成这个功能,但是你可以使用 CGEventTap。你可以创建一个活动事件监听器并注册回调函数,该函数会接收一个CGEventRef, 并可以修改它(如果需要)并返回修改后的事件流以修改实际事件流。

编辑

这是一个简单的程序,当运行时,将每个"b"键击替换为"v":

#import <Cocoa/Cocoa.h>

CGEventRef myCGEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) {
  //0x0b is the virtual keycode for "b"
  //0x09 is the virtual keycode for "v"
  if (CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode) == 0x0B) {
    CGEventSetIntegerValueField(event, kCGKeyboardEventKeycode, 0x09);
  }

  return event;
}

int main(int argc, char *argv[]) {
  NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
  CFRunLoopSourceRef runLoopSource;

  CFMachPortRef eventTap = CGEventTapCreate(kCGHIDEventTap, kCGHeadInsertEventTap, kCGEventTapOptionDefault, kCGEventMaskForAllEvents, myCGEventCallback, NULL);

  if (!eventTap) {
    NSLog(@"Couldn't create event tap!");
    exit(1);
  }

  runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0);

  CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, kCFRunLoopCommonModes);

  CGEventTapEnable(eventTap, true);

  CFRunLoopRun();

  CFRelease(eventTap);
  CFRelease(runLoopSource);
  [pool release];

  exit(0);
}

(有趣的事情是:当我编辑这篇文章时,我一直试图写“替换每个'b'按键”,但它总是变成“替换每个'v'按键”。我很困惑。然后我想起来我还没有停止应用程序。)

还不太熟悉Objective-C...能提供一些示例代码帮忙吗?它与我上面的代码有相似之处吗? - ck_
4
如果有帮助的话:CGEvent APIs 都是使用 C 语言编写的,不需要 Objective-C。 - justin
4
我需要在“系统偏好设置”>“辅助功能”中启用“为辅助设备开启访问权限”,才能使这项工作起效。如果您使用管理员帐户登录,则我认为对于CGEventTap而言这并非必需。 - DenVog
1
在启用辅助设备访问后,您还需要重新启动Xcode。 - RyanM
1
有没有办法进行注入?比如不修改关键事件,而是添加一个新的事件?我正在尝试做这件事。 - Noitidart
显示剩余3条评论

6

我偶然看到了这个答案,需要做同样的事情,但只针对我的应用程序内部的事件而不是全局的。对于这个简单得多的问题,有一个更简单的解决方案,我在这里记录下来,以防对其他人有用:

  • 我通过创建sendEvent:的覆盖来拦截窗口事件。然后检查键事件(KeyUp或KeyDown),然后仅使用几乎所有来自先前事件的数据创建一个新事件,然后调用NSWindow超类使用此事件。

这对我来说似乎完美无缺,我甚至不必修改keyCode部分-但也许这可能是一个问题...

Swift示例:

class KeyInterceptorWindow : NSWindow {

    override func sendEvent(theEvent: NSEvent) {

        if theEvent.type == .KeyDown || theEvent.type == .KeyUp {
            println(theEvent.description)
            let newEvent = NSEvent.keyEventWithType(theEvent.type, 
                location: theEvent.locationInWindow, 
                modifierFlags: theEvent.modifierFlags, 
                timestamp: theEvent.timestamp, 
                windowNumber: theEvent.windowNumber, 
                context: theEvent.context, 
                characters: "H", 
                charactersIgnoringModifiers: theEvent.charactersIgnoringModifiers!, 
                isARepeat: theEvent.ARepeat, 
                keyCode: theEvent.keyCode)
            super.sendEvent(newEvent!)
        } else {
            super.sendEvent(theEvent)
        }

    }

}

2

以下是适用于Swift 4+版本的james_alvarez的最初回答:

class KeyInterceptorWindow: NSWindow {
    override func sendEvent(_ event: NSEvent) {
        if [.keyDown, .keyUp].contains(event.type) {
            let newEvent = NSEvent.keyEvent(with: event.type,
                                            location: event.locationInWindow,
                                            modifierFlags: event.modifierFlags,
                                            timestamp: event.timestamp,
                                            windowNumber: event.windowNumber,
                                            context: nil,
                                            characters: "H",
                                            charactersIgnoringModifiers: event.charactersIgnoringModifiers ?? "",
                                            isARepeat: event.isARepeat,
                                            keyCode: event.keyCode)

            if let newEvent = newEvent {
                super.sendEvent(newEvent)
            }
        } else {
            super.sendEvent(event)
        }
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接