本文介绍了在Swift中捕获OSX媒体控制按钮的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我希望我的应用程序响应,和键盘媒体控制按钮.

I would like my app to respond to the , and keyboard media control buttons.

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

I am aware of this lovely library but it is not working in combination with Swift: https://github.com/nevyn/SPMediaKeyTap

推荐答案

前几天我实际上自己解决了这个问题.我在上面写了一个博客帖子,以及要点

I actually solved this problem myself just the other day. I wrote a blog post on it, as well as a Gist

我将嵌入博客文章和最终代码,以防博客或Gist消失. 注意:这是一篇很长的文章,详细介绍了如何构造类以及如何在App的委托中调用其他方法.如果您只需要完成的产品(MediaApplication类),请移至底部.它位于XML和Info.plist信息的上方.

I'll embed the blog post and final code just in case the blog or Gist ever go away. Note: This is a very long post that goes into detail about how the class in constructed and what you can do to call other methods in your App's delegate. If all you want is the finished product (the MediaApplication class), head towards the bottom. It's just above the XML and the Info.plist informaton.

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

For starters, to get the key events from the media keys you need to create a class that extends NSApplication. This is as simple as

import Cocoa

class MediaApplication: NSApplication {
}

下一步,我们需要覆盖sendEvent()函数

Next, we need to override the sendEvent() function

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对象包含几个关键属性:typesubtypedata1data2. Typesubtype相当不言自明,但是data1data2却非常模糊.由于代码仅使用data1,因此我们将要研究.据我所知,data1包含键事件周围的所有数据.这意味着它包含键代码和所有键标志.似乎按键标志包含有关按键状态(按键是否被按下?按键是否被释放?)以及按键是否被按下并重复信号的信息.我还猜测键代码和键标志都占用了data1中包含的数据的一半,而按位运算将这些数据分离为适当的变量.在获得所需的值之后,我们调用mediaKeyEvent(),稍后我将介绍它.无论将什么事件发送到我们的MediaApplication,我们都希望默认的NSApplication也能够处理所有事件.为此,我们在函数末尾调用super.sendEvent(event).现在,让我们看一下mediaKeyEvent().

Now, I don't pretend to entirely understand what is going on here, but I think I have a decent idea. NSEvent objects contain several key properties: type, subtype, data1, and data2. Type and subtype are fairly self-explanatory, but data1 and data2 are extremely vague. Since the code only uses data1, that's what we'll be looking at. From what I can tell, data1 contains all of the data surrounding a key event. That means it contains the key code and any key flags. It appears that key flags contain information about the key's state (Is the key pressed down? Has the key been released?) as well as whether or not the key is being held down and repeating the signal. I'm also guessing that the key code and key flags both take up half of the data contained in data1 and the bitwise operations are separating that data out into appropriate variables. After we get the values we need, we call mediaKeyEvent() which I will get to in a moment. Regardless of what events get sent to our MediaApplication, we do want the default NSApplication to handle all events as well. To do this, we call super.sendEvent(event) at the end of the function. Now, let's take a look at 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为true时正在按下的键,在这种情况下,只要按下该键即可.一旦检查完密钥,我们将查找NX_KEYTYPE_PLAYNX_KEYTYPE_FASTNX_KEYTYPE_REWIND.如果它们的功能不明显,则NX_KEYTYPE_PLAY是播放/暂停键,NX_KEYTYPE_FAST是下一个键,而NX_KEYTYPE_REWIND是上一个键.现在,当任何一个按键被按下时,什么也没有发生,因此让我们回顾一些可能的逻辑.我们将从一个简单的场景开始.

This is where things start to get fun. First things first, we only want to check what key is being pressed if state is true, which in this case is whenever the key is pressed down. Once we get into checking the keys, we look for NX_KEYTYPE_PLAY, NX_KEYTYPE_FAST, and NX_KEYTYPE_REWIND. If their functions aren't obvious, NX_KEYTYPE_PLAY is the play/pause key, NX_KEYTYPE_FAST is the next key, and NX_KEYTYPE_REWIND is the previous key. Right now, nothing happens when any of those keys is pressed down, so lets go over some possible logic. We'll start with a simple scenario.

case NX_KEYTYPE_PLAY:
    print("Play")
    break

使用此代码,当您的应用程序检测到已按下播放/暂停"键时,您会看到播放"打印到控制台.简单吧?让我们通过调用应用程序NSApplicationDelegate中的函数来完成事前准备.首先,我们假设您的NSApplicationDelegate具有一个名为printMessage的函数.我们将不断进行修改,因此请密切注意所做的更改.它们很小,但是更改将影响您从mediaEventKey调用它们的方式.

With this code in place, when your application detects that the play/pause key has been pressed you will see "Play" printed out to the console. Simple, right? Let's up the ante by calling functions in your application's NSApplicationDelegate. First we will assume that your NSApplicationDelegate has a function called printMessage. We will be modifying it as we go, so pay close attention to the changes. They will be minor, but the changes will impact how you call them from mediaEventKey.

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

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

This is the simplest case. When printMessage() is called, you will see "Hello World" in your console. You can call this by calling performSelector on your NSApplicationDelegate which is accessible through the MediaApplication. performSelector takes in a Selector which is just the name of the function in your NSApplicationDelegate.

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

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

Now, when your application detects that the play/pause key has been pressed, you will see "Hello World" printed to the console. Let's kick things up a notch with a new version of printMessage that takes in a parameter.

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

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

The idea is now that if printMessage("Hello World") is called, you will see "Hello World" in your console. We can now modify the performSelector call to handle passing in a parameter.

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

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

There are a few things to note about this change. First, it's important to notice the : that was added to the Selector. This separates the function name from the parameter when it gets sent to the delegate. How it works isn't too important to remember, but it's something along the lines of the delegate calling printMessage:"Hello World". I'm fairly certain that is not 100% correct as it would likely use an object ID of some sort, but I haven't done any extensive digging into the specifics. Either way, the important thing to remember is to add : when passing in a parameter.. The second thing to note is that we added a withObject parameter. withObject takes an AnyObject? as a value. In this case, we just pass in a String because that's what printMessage is looking for. When your application detects that the play/pause key has been pressed, you should still see "Hello World" in the console. Let's look at one final use-case: a version of printMessage that takes in not one, but two parameters.

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

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

Now, if printMessage("Hello", "World") is called, you will see "Hello World" in your console. We can now modify the performSelector call to handle passing in two parameters.

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

和以前一样,这里有两件事要注意.首先,我们现在在Selector的末尾添加两个:.像以前一样,这样做是为了使委托可以传递包含参数的信息.在非常基本的级别上,它看起来像printMessage:"Hello":"World",但是再次,我不知道它在更深层次上的真正外观.注意的第二件事是我们在performSelector调用中添加了第二个withObject参数.像以前一样,此withObjectAnyObject?作为值,而我们传入了String,因为这正是printMessage想要的.当您的应用程序检测到已按下播放/暂停键时,您仍然应该在控制台中看到"Hello World".

As before, there are two things to notice here. First, we now add two : to the end of the Selector. Like before, this is so that the delegate can pass information along that contains the parameters. At a very basic level, it would look something like printMessage:"Hello":"World", but again I don't know what it really looks like at a deeper level. The second thing to notice is that we have added a second withObject parameter to the performSelector call. Like before, this withObject takes an AnyObject? as a value and we're passing in a String because that's what printMessage wants. When your application detects that the play/pause key has been pressed, you should still see "Hello World" in the console.

最后要注意的一点是,performSelector最多只能接受两个参数.我真的很想看到Swift添加诸如splatting或varargs之类的概念,以便最终消除此限制,但现在就避免尝试调用需要两个以上参数的函数.

One final thing to note is that performSelector can only accept up to two parameters. I'd really like to see Swift add concepts like splatting or varargs so that this limitation eventually goes away, but for now just avoid trying to call functions that require more than two parameters.

这是只打印一些文本的非常简单的MediaApplication类的外观,一旦完成上述所有操作,该类将看起来像这样:

This is what a very simple MediaApplication class that just prints out some text would look like once you are done with everything above:

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文件.如果您在图形视图中,它将看起来像这样:

Now, I should also add that, by default, your application is going to use the standard NSApplication when it runs. If you want to use the MediaApplication that this whole post is about, you'll need to go ahead and modify your application's Info.plist file. If you're in the graphical view, it will look something like this:



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

Otherwise, it will look something like this:

<?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.进行更改后,运行您的应用程序并使用那些媒体密钥!

In either case, you will want to change the NSPrincipalClass property. The new value will include you project's name, so it will be something like Notify.MediaApplication. Once you make the change, run your application and use those media keys!

这篇关于在Swift中捕获OSX媒体控制按钮的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

07-30 16:44