在 Swift 中捕获 OSX 个媒体控制按钮

Capture OSX media control buttons in Swift

我希望我的应用响应 F7F8F9 键盘媒体控制按钮。

我知道这个可爱的库,但它不能与 Swift 结合使用:https://github.com/nevyn/SPMediaKeyTap

前几天我自己解决了这个问题。我写了一个blog post on it, as well as a Gist

我将嵌入博客 post 和最终代码,以防博客或 Gist 消失。 注意: 这是一篇很长的 post,其中详细介绍了 class 的构造方式以及您可以如何调用应用程序委托中的其他方法。如果您想要的只是成品(MediaApplication class),请朝底部走。它就在 XML 和 Info.plist 信息之上。


对于初学者来说,要从媒体密钥中获取密钥事件,您需要创建一个扩展 NSApplication 的 class。这很简单

import Cocoa

class MediaApplication: NSApplication {
}

接下来,我们需要覆盖sendEvent()函数

override func sendEvent(event: NSEvent) {
    if (event.type == .SystemDefined && event.subtype.rawValue == 8) {
        let keyCode = ((event.data1 & 0xFFFF0000) >> 16)
        let keyFlags = (event.data1 & 0x0000FFFF)
        // Get the key state. 0xA is KeyDown, OxB is KeyUp
        let keyState = (((keyFlags & 0xFF00) >> 8)) == 0xA
        let keyRepeat = (keyFlags & 0x1)
        mediaKeyEvent(Int32(keyCode), state: keyState, keyRepeat: Bool(keyRepeat))
    }

    super.sendEvent(event)
}

现在,我并不假装完全理解这里发生的事情,但我想我有一个不错的主意。 NSEvent 对象包含几个关键属性:typesubtypedata1data2Typesubtype 是不言自明的,但是 data1data2 非常模糊。由于代码仅使用 data1,这就是我们要查看的内容。据我所知,data1 包含围绕关键事件的所有数据。这意味着它包含密钥代码和任何密钥标志。似乎键标志包含有关键状态的信息(是否按下键?是否释放键?)以及是否按住键并重复信号。我还猜测关键代码和关键标志都占用了 data1 中包含的一半数据,并且按位运算将这些数据分离到适当的变量中。在我们得到我们需要的值之后,我们调用 mediaKeyEvent() ,我稍后会讲到。无论向我们的 MediaApplication 发送什么事件,我们都希望默认的 NSApplication 也能处理所有事件。为此,我们在函数末尾调用 super.sendEvent(event)。现在,让我们来看看 mediaKeyEvent().

func mediaKeyEvent(key: Int32, state: Bool, keyRepeat: Bool) {
    // Only send events on KeyDown. Without this check, these events will happen twice
    if (state) {
        switch(key) {
        case NX_KEYTYPE_PLAY:
            // Do work
            break
        case NX_KEYTYPE_FAST:
            // Do work
            break
        case NX_KEYTYPE_REWIND:
            // Do work
            break
        default:
            break
        }
    }
}

这就是事情开始变得有趣的地方。首先,如果 state 为真,我们只想检查按下的是哪个键,在本例中是每当按下键时。一旦我们开始检查密钥,我们就会寻找 NX_KEYTYPE_PLAYNX_KEYTYPE_FASTNX_KEYTYPE_REWIND。如果他们的功能不明显,NX_KEYTYPE_PLAY是play/pause键,NX_KEYTYPE_FAST是下一个键,NX_KEYTYPE_REWIND是上一个键。现在,按下这些键中的任何一个都不会发生任何事情,所以让我们回顾一下一些可能的逻辑。我们将从一个简单的场景开始。

case NX_KEYTYPE_PLAY:
    print("Play")
    break

使用此代码,当您的应用程序检测到 play/pause 键已被按下时,您将看到 "Play" 打印到控制台。简单吧?让我们通过调用应用程序 NSApplicationDelegate 中的函数来提高赌注。首先,我们假设您的 NSApplicationDelegate 有一个名为 printMessage 的函数。我们将在进行过程中对其进行修改,因此请密切注意更改。它们会很小,但更改会影响您从 mediaEventKey.

中调用它们的方式
func printMessage() {
    print("Hello World")
}

这是最简单的情况。当调用 printMessage() 时,您将在控制台中看到 "Hello World"。您可以通过在您的 NSApplicationDelegate 上调用 performSelector 来调用它,该 NSApplicationDelegate 可通过 MediaApplication 访问。 performSelector 接受一个 Selector ,它只是 NSApplicationDelegate.

中函数的名称
case NX_KEYTYPE_PLAY:
    delegate!.performSelector("printMessage")
    break

现在,当您的应用程序检测到 play/pause 键已被按下时,您将看到 "Hello World" 打印到控制台。让我们用接受参数的新版本 printMessage 来提升一个档次。

func printMessage(arg: String) {
    print(arg)
}

现在的想法是,如果调用 printMessage("Hello World"),您将在控制台中看到 "Hello World"。我们现在可以修改 performSelector 调用来处理传入的参数。

case NX_KEYTYPE_PLAY:
    delegate!.performSelector("printMessage:", withObject: "Hello World")
    break

此更改有几点需要注意。首先,重要的是要注意添加到 Selector:。这在将函数名称发送给委托时将函数名称与参数分开。它是如何工作的并不重要,需要记住,但它与委托调用 printMessage:"Hello World" 的思路类似。我相当确定这不是 100% 正确,因为它可能会使用某种对象 ID,但我还没有深入研究细节。无论哪种方式,重要的是要记住在传入参数时添加 :.. 第二个要注意的是我们添加了一个 withObject 参数。 withObjectAnyObject? 作为值。在这种情况下,我们只传递 String 因为这就是 printMessage 正在寻找的内容。当您的应用程序检测到 play/pause 键已被按下时,您应该仍会在控制台中看到 "Hello World"。让我们看看最后一个用例:printMessage 的一个版本,它接受的不是一个参数,而是两个参数。

func printMessage(arg: String, _ arg2: String) {
    print(arg)
}

现在,如果调用 printMessage("Hello", "World"),您将在控制台中看到 "Hello World"。我们现在可以修改 performSelector 调用来处理两个参数的传递。

case NX_KEYTYPE_PLAY:
    delegate!.performSelector("printMessage::", withObject: "Hello", withObject: "World")
    break

和以前一样,这里有两点需要注意。首先,我们现在在 Selector 的末尾添加两个 :。像以前一样,这是为了使委托可以传递包含参数的信息。在非常基本的层面上,它看起来像 printMessage:"Hello":"World",但我还是不知道在更深层次上它到底是什么样子。要注意的第二件事是我们在 performSelector 调用中添加了第二个 withObject 参数。和以前一样,这个 withObject 接受一个 AnyObject? 作为值,我们传入一个 String 因为那是 printMessage 想要的。当您的应用程序检测到 play/pause 键已被按下时,您应该仍会在控制台中看到 "Hello World"。

最后要注意的是performSelector最多只能接受两个参数。我真的很想看到 Swift 添加像 splatting 或 varargs 这样的概念,这样这个限制最终就会消失,但现在只是避免尝试调用需要两个以上参数的函数。

这是一个非常简单的 MediaApplication class,它只是打印出一些文本,当您完成上述所有操作后,它看起来像这样:

import Cocoa

class MediaApplication: NSApplication {
    override func sendEvent(event: NSEvent) {
        if (event.type == .SystemDefined && event.subtype.rawValue == 8) {
            let keyCode = ((event.data1 & 0xFFFF0000) >> 16)
            let keyFlags = (event.data1 & 0x0000FFFF)
            // Get the key state. 0xA is KeyDown, OxB is KeyUp
            let keyState = (((keyFlags & 0xFF00) >> 8)) == 0xA
            let keyRepeat = (keyFlags & 0x1)
            mediaKeyEvent(Int32(keyCode), state: keyState, keyRepeat: Bool(keyRepeat))
        }

        super.sendEvent(event)
    }

    func mediaKeyEvent(key: Int32, state: Bool, keyRepeat: Bool) {
        // Only send events on KeyDown. Without this check, these events will happen twice
        if (state) {
            switch(key) {
            case NX_KEYTYPE_PLAY:
                print("Play")
                break
            case NX_KEYTYPE_FAST:
                print("Next")
                break
            case NX_KEYTYPE_REWIND:
                print("Prev")
                break
            default:
                break
            }
        }
    }
}

现在,我还应该补充一点,默认情况下,您的应用程序将在 运行 时使用标准 NSApplication。如果您想使用整个 post 所涉及的 MediaApplication,您需要继续修改应用程序的 Info.plist 文件。如果您在图形视图中,它将看起来像这样:


(来源:sernprogramming.com

否则,它将看起来像这样:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>CFBundleDevelopmentRegion</key>
  <string>en</string>
  <key>CFBundleExecutable</key>
  <string>$(EXECUTABLE_NAME)</string>
  <key>CFBundleIconFile</key>
  <string></string>
  <key>CFBundleIdentifier</key>
  <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
  <key>CFBundleInfoDictionaryVersion</key>
  <string>6.0</string>
  <key>CFBundleName</key>
  <string>$(PRODUCT_NAME)</string>
  <key>CFBundlePackageType</key>
  <string>APPL</string>
  <key>CFBundleShortVersionString</key>
  <string>1.0</string>
  <key>CFBundleSignature</key>
  <string>????</string>
  <key>CFBundleVersion</key>
  <string>1</string>
  <key>LSApplicationCategoryType</key>
  <string>public.app-category.utilities</string>
  <key>LSMinimumSystemVersion</key>
  <string>$(MACOSX_DEPLOYMENT_TARGET)</string>
  <key>LSUIElement</key>
  <true/>
  <key>NSHumanReadableCopyright</key>
  <string>Copyright © 2015 Chris Rees. All rights reserved.</string>
  <key>NSMainNibFile</key>
  <string>MainMenu</string>
  <key>NSPrincipalClass</key>
  <string>NSApplication</string>
</dict>
</plist>

无论哪种情况,您都需要更改 NSPrincipalClass 属性。新值将包括您的项目名称,因此它将类似于 Notify.MediaApplication。进行更改后,运行 您的应用程序并使用这些媒体密钥!