问题描述
我希望我的应用程序响应,和键盘媒体控制按钮.
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
对象包含几个关键属性:type
,subtype
,data1
和data2
. Type
和subtype
相当不言自明,但是data1
和data2
却非常模糊.由于代码仅使用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_PLAY
,NX_KEYTYPE_FAST
和NX_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
参数. withObject
将AnyObject?
用作值.在这种情况下,我们只传入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
参数.像以前一样,此withObject
将AnyObject?
作为值,而我们传入了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媒体控制按钮的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!