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

And*_* Ho 7 macos media-player swift

我想我的应用程序来应对F7,F8F9键盘媒体控制按钮.

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

小智 15

前几天我自己解决了这个问题.我写了一篇关于它的博客文章,以及一个要点

我将嵌入博客文章和最终代码,以防万一博客或Gist消失.注意:这是一篇非常长的帖子,详细介绍了如何构建类以及如何在App的委托中调用其他方法.如果你想要的只是成品(MediaApplication类),那就走向最底层.它位于XML和Info.plist信息之上.


对于初学者来说,要从媒体键获取关键事件,您需要创建一个扩展的类NSApplication.这很简单

import Cocoa

class MediaApplication: NSApplication {
}
Run Code Online (Sandbox Code Playgroud)

接下来,我们需要覆盖该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)
}
Run Code Online (Sandbox Code Playgroud)

现在,我并没有假装完全理解这里发生了什么,但我认为我有一个不错的主意.NSEvent对象包含几个关键特性:type,subtype,data1,和data2.Type并且subtype是相当不言自明,但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
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这是事情开始变得有趣的地方.首先,我们只想检查正在按下的键是什么state,在这种情况下,只要按下键就可以了.一旦我们进入检查的钥匙,我们寻找NX_KEYTYPE_PLAY,NX_KEYTYPE_FASTNX_KEYTYPE_REWIND.如果它们的功能不明显,NX_KEYTYPE_PLAY则播放/暂停键NX_KEYTYPE_FAST是下一个键,NX_KEYTYPE_REWIND是前一个键.现在,当按下任何键时没有任何反应,所以让我们回顾一些可能的逻辑.我们将从一个简单的场景开始.

case NX_KEYTYPE_PLAY:
    print("Play")
    break
Run Code Online (Sandbox Code Playgroud)

使用此代码后,当您的应用程序检测到已按下播放/暂停键时,您将看到打印到控制台的"播放".简单吧?让我们通过调用应用程序中的函数来提高赌注NSApplicationDelegate.首先,我们假设您NSApplicationDelegate有一个名为的函数printMessage.我们将随时修改它,因此请密切关注这些变化.它们将是次要的,但这些变化将影响你如何称呼它们mediaEventKey.

func printMessage() {
    print("Hello World")
}
Run Code Online (Sandbox Code Playgroud)

这是最简单的情况.当printMessage()被调用时,你会看到"Hello World",在您的控制台.您可以通过调用调用这个performSelectorNSApplicationDelegate是通过访问MediaApplication.performSelector接受一个Selector只是你的功能的名称NSApplicationDelegate.

case NX_KEYTYPE_PLAY:
    delegate!.performSelector("printMessage")
    break
Run Code Online (Sandbox Code Playgroud)

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

func printMessage(arg: String) {
    print(arg)
}
Run Code Online (Sandbox Code Playgroud)

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

case NX_KEYTYPE_PLAY:
    delegate!.performSelector("printMessage:", withObject: "Hello World")
    break
Run Code Online (Sandbox Code Playgroud)

关于这一变化,有几点需要注意.首先,重要的是要注意到:添加到Selector.这将函数名称与参数发送给委托时将其分开.如何工作并不是太重要,不能记住,但这是委托调用的方式printMessage:"Hello World".我很确定这不是100%正确,因为它可能会使用某种对象ID,但我没有对细节进行任何广泛的挖掘.无论哪种方式,重要的是要记住:在传递参数时添加..第二件要注意的是我们添加了一个withObject参数.withObjectAnyObject?一个值作为一个值.在这种情况下,我们只是传入一个String因为那是什么printMessage在寻找.当您的应用程序检测到已按下播放/暂停键时,您仍应在控制台中看到"Hello World".让我们看一个最终的用例:一个版本printMessage不是一个,而是两个参数.

func printMessage(arg: String, _ arg2: String) {
    print(arg)
}
Run Code Online (Sandbox Code Playgroud)

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

case NX_KEYTYPE_PLAY:
    delegate!.performSelector("printMessage::", withObject: "Hello", withObject: "World")
    break
Run Code Online (Sandbox Code Playgroud)

和以前一样,这里有两件事需要注意.首先,我们现在添加两个:到结尾Selector.像以前一样,这样委托可以传递包含参数的信息.在一个非常基本的层面上,它会看起来像printMessage:"Hello":"World",但我再也不知道它在更深层次上的真实情况.需要注意的第二件事是我们withObjectperformSelector调用中添加了第二个参数.像以前一样,这withObject需要AnyObject?一个价值,我们传递的是String因为这就是printMessage想要的东西.当您的应用程序检测到已按下播放/暂停键时,您仍应在控制台中看到"Hello World".

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

这就是一个非常简单的MediaApplication类,只要你完成上面的所有内容,就会打印出一些文本:

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
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

的Info.plist

否则,它看起来像这样:

<?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>
Run Code Online (Sandbox Code Playgroud)

在任何一种情况下,您都需要更改NSPrincipalClass属性.新值将包含项目的名称,因此它将是类似的Notify.MediaApplication.完成更改后,运行您的应用程序并使用这些媒体键!