【问题标题】:Capture OSX media control buttons in Swift在 Swift 中捕获 OSX 媒体控制按钮
【发布时间】:2015-09-10 10:42:54
【问题描述】:

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

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

【问题讨论】:

    标签: macos swift media-player


    【解决方案1】:

    实际上我前几天自己解决了这个问题。我在上面写了blog post,还有Gist

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


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

    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 是播放/暂停键,NX_KEYTYPE_FAST 是下一个键,NX_KEYTYPE_REWIND 是上一个键。现在,按下这些键中的任何一个都不会发生任何事情,所以让我们回顾一下一些可能的逻辑。我们将从一个简单的场景开始。

    case NX_KEYTYPE_PLAY:
        print("Play")
        break
    

    使用此代码,当您的应用程序检测到已按下播放/暂停键时,您将看到控制台打印出“播放”。很简单,对吧?让我们通过调用应用程序的NSApplicationDelegate 中的函数来提高赌注。首先我们假设您的NSApplicationDelegate 有一个名为printMessage 的函数。我们将随时对其进行修改,因此请密切注意更改。它们将是次要的,但这些更改会影响您从 mediaEventKey 调用它们的方式。

    func printMessage() {
        print("Hello World")
    }
    

    这是最简单的情况。当printMessage() 被调用时,您将在控制台中看到“Hello World”。您可以通过在您的NSApplicationDelegate 上调用performSelector 来调用此功能,可通过MediaApplication 访问。 performSelector 接受一个 Selector,这只是你的 NSApplicationDelegate 中的函数名称。

    case NX_KEYTYPE_PLAY:
        delegate!.performSelector("printMessage")
        break
    

    现在,当您的应用程序检测到已按下播放/暂停键时,您将看到控制台打印出“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 正在寻找的。当您的应用程序检测到已按下播放/暂停键时,您仍应在控制台中看到“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 参数。像以前一样,这个withObjectAnyObject? 作为一个值,我们传入一个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
                }
            }
        }
    }
    

    现在,我还应该补充一点,默认情况下,您的应用程序在运行时将使用标准 NSApplication。如果您想使用整篇文章所涉及的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。进行更改后,运行您的应用程序并使用这些媒体密钥!

    【讨论】:

    • 嘿@Serneum 感谢您提供详细信息。这是我的问题:我们可以为 iTunes 阻止此活动吗?实际上,只要播放键按下它就会启动 iTunes。
    • 在媒体按键上处理 iTunes 启动。 github.com/nevyn/SPMediaKeyTap
    • 不错的解决方案,但是当您以编程方式创建应用程序时它不起作用(使用 main.swift 而不是使用故事板或 xib 文件)
    猜你喜欢
    • 2013-12-22
    • 1970-01-01
    • 2015-10-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多