在 Swift 中捕获 OSX 个媒体控制按钮
Capture OSX media control buttons in Swift
我希望我的应用响应 F7、F8 和 F9 键盘媒体控制按钮。
我知道这个可爱的库,但它不能与 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
对象包含几个关键属性:type
、subtype
、data1
和 data2
。 Type
和 subtype
是不言自明的,但是 data1
和 data2
非常模糊。由于代码仅使用 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_PLAY
、NX_KEYTYPE_FAST
和 NX_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
参数。 withObject
将 AnyObject?
作为值。在这种情况下,我们只传递 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
。进行更改后,运行 您的应用程序并使用这些媒体密钥!
我希望我的应用响应 F7、F8 和 F9 键盘媒体控制按钮。
我知道这个可爱的库,但它不能与 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
对象包含几个关键属性:type
、subtype
、data1
和 data2
。 Type
和 subtype
是不言自明的,但是 data1
和 data2
非常模糊。由于代码仅使用 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_PLAY
、NX_KEYTYPE_FAST
和 NX_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
参数。 withObject
将 AnyObject?
作为值。在这种情况下,我们只传递 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
。进行更改后,运行 您的应用程序并使用这些媒体密钥!