获取 Swift 中键盘代码的键名
Getting key names for keyboard codes in Swift
我知道其他人也问过类似的问题,但我没有看到明确的答案,我仍然卡住了。我正在尝试编写一个 Swift 函数,该函数采用硬件生成的键盘扫描代码(例如来自 NSEvent)和 returns 字母大写锁定的键名称,用于特定键当前在 OS 中有效的排列(Dvorak、Qwerty 等)(这可能与生成代码时有效的排列不同)。
据我了解,唯一的方法是调用一些非常古老的 Carbon 函数,绕过很多 Swift 的极端类型安全,我觉得这样做不太舒服。这是到目前为止的节目:
import Cocoa
import Carbon
func keyName (scanCode: UInt16) -> String?
{ let maxNameLength = 4, modifierKeys: UInt32 = 0x00000004 // Caps Lock (Carbon Era)
let deadKeys = UnsafeMutablePointer<UInt32>(bitPattern: 0x00000000),
nameBuffer = UnsafeMutablePointer<UniChar>.alloc(maxNameLength),
nameLength = UnsafeMutablePointer<Int>.alloc(1),
keyboardType = UInt32(LMGetKbdType())
let source = TISGetInputSourceProperty ( TISCopyCurrentKeyboardLayoutInputSource()
.takeRetainedValue(),
kTISPropertyUnicodeKeyLayoutData )
let dataRef = unsafeBitCast(source, CFDataRef.self)
let dataBuffer = CFDataGetBytePtr(dataRef)
let keyboardLayout = unsafeBitCast(dataBuffer, UnsafePointer <UCKeyboardLayout>.self)
let osStatus = UCKeyTranslate (keyboardLayout, scanCode, UInt16(kUCKeyActionDown),
modifierKeys, keyboardType, UInt32(kUCKeyTranslateNoDeadKeysMask),
deadKeys, maxNameLength, nameLength, nameBuffer)
switch osStatus
{ case 0: return NSString (characters: nameBuffer, length: nameLength[0]) as String
default: NSLog (“Code: 0x%04X Status: %+i", scanCode, osStatus); return nil }
}
它没有崩溃,在这一点上我几乎认为这本身就是一个游戏成就,但它也没有用。 UCKeyTranslate总是returns状态为-50,我理解是参数错误。我怀疑是“keyboardLayout”,因为它的设置最复杂。谁能看到参数问题?或者是否有针对此类事情的更新框架?
好的,我相信我已经找到问题所在了。虽然回答我自己的问题感觉很奇怪,但我知道在这种情况下这是正确的做法。
违规参数似乎是 deadKeys。在我遵循的模型代码中,这被定义为位模式。虽然据说是指向可变内容的指针,但我不确定它是否真的是那个,因为当我决定重新定义它以匹配 UCKeyTranslate 的其他两个按引用调用参数时,一切都开始完美运行。解决方案是执行显式 .alloc,然后将引用值显式归零。这是我更新的函数:
func keyName ( scanCode: UInt16 ) -> String?
{ let maxNameLength = 4, modifierKeys: UInt32 = 0x00000004, // Caps Lock (Carbon Era Mask)
nameBuffer = UnsafeMutablePointer <UniChar> .alloc (maxNameLength),
nameLength = UnsafeMutablePointer <Int> .alloc (1),
deadKeys = UnsafeMutablePointer <UInt32> .alloc (1); deadKeys[0] = 0x00000000
let source = TISGetInputSourceProperty ( TISCopyCurrentKeyboardLayoutInputSource()
.takeRetainedValue(),
kTISPropertyUnicodeKeyLayoutData ),
keyboardLayout = unsafeBitCast ( CFDataGetBytePtr (unsafeBitCast (source, CFDataRef.self)),
UnsafePointer <UCKeyboardLayout>.self),
keyboardType = UInt32 (LMGetKbdType())
let osStatus = UCKeyTranslate (keyboardLayout, scanCode, UInt16 (kUCKeyActionDown),
modifierKeys, keyboardType, UInt32 (kUCKeyTranslateNoDeadKeysMask),
deadKeys, maxNameLength, nameLength, nameBuffer)
switch osStatus
{ case 0: return String.init (utf16CodeUnits: nameBuffer, count: nameLength[0])
default: NSLog ("Code: 0x%04X Status: %+i", scanCode, osStatus); return nil }
}
还有一些其他更改,非常漂亮:我删除了导致定义 keyboardLayout 的几个中间常量。 (“BitCasts”只是为了满足 Swiftian 类型安全:它们实际上并没有做我能看到的任何其他事情。)但真正的问题是 deadKeys 的原始定义。我希望这对某些人有用,至少在出现非碳替代品之前是这样。 (那会发生吗?)
正如您已经发现的,您必须传递 UInt32
的 地址
作为 deadKeyState
参数的变量。分配内存是一个
解决该问题的方法,但您一定不要忘记释放内存
最终,否则程序会泄漏内存。
另一种可能的解决方案是将变量的地址传递为
带有 &
:
的输入参数
var deadKeys : UInt32 = 0
// ...
let osStatus = UCKeyTranslate(..., &deadKeys, ...)
这个更短更简单,你不需要发布
记忆。同样可以应用于 nameBuffer
和 nameLength
.
unsafeBitCast()
可以通过使用Unmanaged
类型来避免,
比较 的类似问题和
更详细的解释。
您还可以利用之间的免费电话桥接
CFData
和 NSData
.
那么您的函数可能如下所示 (Swift 2):
import Carbon
func keyName(virtualKeyCode: UInt16) -> String?
{
let maxNameLength = 4
var nameBuffer = [UniChar](count : maxNameLength, repeatedValue: 0)
var nameLength = 0
let modifierKeys = UInt32(alphaLock >> 8) & 0xFF // Caps Lock
var deadKeys : UInt32 = 0
let keyboardType = UInt32(LMGetKbdType())
let source = TISCopyCurrentKeyboardLayoutInputSource().takeRetainedValue()
let ptr = TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData)
let layoutData = Unmanaged<CFData>.fromOpaque(COpaquePointer(ptr)).takeUnretainedValue() as NSData
let keyboardLayout = UnsafePointer<UCKeyboardLayout>(layoutData.bytes)
let osStatus = UCKeyTranslate(keyboardLayout, virtualKeyCode, UInt16(kUCKeyActionDown),
modifierKeys, keyboardType, UInt32(kUCKeyTranslateNoDeadKeysMask),
&deadKeys, maxNameLength, &nameLength, &nameBuffer)
guard osStatus == noErr else {
NSLog("Code: 0x%04X Status: %+i", virtualKeyCode, osStatus);
return nil
}
return String(utf16CodeUnits: nameBuffer, count: nameLength)
}
Swift3 的更新:
import Carbon
func keyName(virtualKeyCode: UInt16) -> String? {
let maxNameLength = 4
var nameBuffer = [UniChar](repeating: 0, count : maxNameLength)
var nameLength = 0
let modifierKeys = UInt32(alphaLock >> 8) & 0xFF // Caps Lock
var deadKeys: UInt32 = 0
let keyboardType = UInt32(LMGetKbdType())
let source = TISCopyCurrentKeyboardLayoutInputSource().takeRetainedValue()
guard let ptr = TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData) else {
NSLog("Could not get keyboard layout data")
return nil
}
let layoutData = Unmanaged<CFData>.fromOpaque(ptr).takeUnretainedValue() as Data
let osStatus = layoutData.withUnsafeBytes {
UCKeyTranslate([=12=], virtualKeyCode, UInt16(kUCKeyActionDown),
modifierKeys, keyboardType, UInt32(kUCKeyTranslateNoDeadKeysMask),
&deadKeys, maxNameLength, &nameLength, &nameBuffer)
}
guard osStatus == noErr else {
NSLog("Code: 0x%04X Status: %+i", virtualKeyCode, osStatus);
return nil
}
return String(utf16CodeUnits: nameBuffer, count: nameLength)
}
Swift4 的更新:
从 Swift 4 开始,Data.withUnsafeBytes
使用 UnsafeRawBufferPointer
调用闭包,它必须 绑定 指向 [=25 的指针=]:
import Carbon
func keyName(virtualKeyCode: UInt16) -> String? {
let maxNameLength = 4
var nameBuffer = [UniChar](repeating: 0, count : maxNameLength)
var nameLength = 0
let modifierKeys = UInt32(alphaLock >> 8) & 0xFF // Caps Lock
var deadKeys: UInt32 = 0
let keyboardType = UInt32(LMGetKbdType())
let source = TISCopyCurrentKeyboardLayoutInputSource().takeRetainedValue()
guard let ptr = TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData) else {
NSLog("Could not get keyboard layout data")
return nil
}
let layoutData = Unmanaged<CFData>.fromOpaque(ptr).takeUnretainedValue() as Data
let osStatus = layoutData.withUnsafeBytes {
UCKeyTranslate([=13=].bindMemory(to: UCKeyboardLayout.self).baseAddress, virtualKeyCode, UInt16(kUCKeyActionDown),
modifierKeys, keyboardType, UInt32(kUCKeyTranslateNoDeadKeysMask),
&deadKeys, maxNameLength, &nameLength, &nameBuffer)
}
guard osStatus == noErr else {
NSLog("Code: 0x%04X Status: %+i", virtualKeyCode, osStatus);
return nil
}
return String(utf16CodeUnits: nameBuffer, count: nameLength)
}
我知道其他人也问过类似的问题,但我没有看到明确的答案,我仍然卡住了。我正在尝试编写一个 Swift 函数,该函数采用硬件生成的键盘扫描代码(例如来自 NSEvent)和 returns 字母大写锁定的键名称,用于特定键当前在 OS 中有效的排列(Dvorak、Qwerty 等)(这可能与生成代码时有效的排列不同)。
据我了解,唯一的方法是调用一些非常古老的 Carbon 函数,绕过很多 Swift 的极端类型安全,我觉得这样做不太舒服。这是到目前为止的节目:
import Cocoa
import Carbon
func keyName (scanCode: UInt16) -> String?
{ let maxNameLength = 4, modifierKeys: UInt32 = 0x00000004 // Caps Lock (Carbon Era)
let deadKeys = UnsafeMutablePointer<UInt32>(bitPattern: 0x00000000),
nameBuffer = UnsafeMutablePointer<UniChar>.alloc(maxNameLength),
nameLength = UnsafeMutablePointer<Int>.alloc(1),
keyboardType = UInt32(LMGetKbdType())
let source = TISGetInputSourceProperty ( TISCopyCurrentKeyboardLayoutInputSource()
.takeRetainedValue(),
kTISPropertyUnicodeKeyLayoutData )
let dataRef = unsafeBitCast(source, CFDataRef.self)
let dataBuffer = CFDataGetBytePtr(dataRef)
let keyboardLayout = unsafeBitCast(dataBuffer, UnsafePointer <UCKeyboardLayout>.self)
let osStatus = UCKeyTranslate (keyboardLayout, scanCode, UInt16(kUCKeyActionDown),
modifierKeys, keyboardType, UInt32(kUCKeyTranslateNoDeadKeysMask),
deadKeys, maxNameLength, nameLength, nameBuffer)
switch osStatus
{ case 0: return NSString (characters: nameBuffer, length: nameLength[0]) as String
default: NSLog (“Code: 0x%04X Status: %+i", scanCode, osStatus); return nil }
}
它没有崩溃,在这一点上我几乎认为这本身就是一个游戏成就,但它也没有用。 UCKeyTranslate总是returns状态为-50,我理解是参数错误。我怀疑是“keyboardLayout”,因为它的设置最复杂。谁能看到参数问题?或者是否有针对此类事情的更新框架?
好的,我相信我已经找到问题所在了。虽然回答我自己的问题感觉很奇怪,但我知道在这种情况下这是正确的做法。
违规参数似乎是 deadKeys。在我遵循的模型代码中,这被定义为位模式。虽然据说是指向可变内容的指针,但我不确定它是否真的是那个,因为当我决定重新定义它以匹配 UCKeyTranslate 的其他两个按引用调用参数时,一切都开始完美运行。解决方案是执行显式 .alloc,然后将引用值显式归零。这是我更新的函数:
func keyName ( scanCode: UInt16 ) -> String?
{ let maxNameLength = 4, modifierKeys: UInt32 = 0x00000004, // Caps Lock (Carbon Era Mask)
nameBuffer = UnsafeMutablePointer <UniChar> .alloc (maxNameLength),
nameLength = UnsafeMutablePointer <Int> .alloc (1),
deadKeys = UnsafeMutablePointer <UInt32> .alloc (1); deadKeys[0] = 0x00000000
let source = TISGetInputSourceProperty ( TISCopyCurrentKeyboardLayoutInputSource()
.takeRetainedValue(),
kTISPropertyUnicodeKeyLayoutData ),
keyboardLayout = unsafeBitCast ( CFDataGetBytePtr (unsafeBitCast (source, CFDataRef.self)),
UnsafePointer <UCKeyboardLayout>.self),
keyboardType = UInt32 (LMGetKbdType())
let osStatus = UCKeyTranslate (keyboardLayout, scanCode, UInt16 (kUCKeyActionDown),
modifierKeys, keyboardType, UInt32 (kUCKeyTranslateNoDeadKeysMask),
deadKeys, maxNameLength, nameLength, nameBuffer)
switch osStatus
{ case 0: return String.init (utf16CodeUnits: nameBuffer, count: nameLength[0])
default: NSLog ("Code: 0x%04X Status: %+i", scanCode, osStatus); return nil }
}
还有一些其他更改,非常漂亮:我删除了导致定义 keyboardLayout 的几个中间常量。 (“BitCasts”只是为了满足 Swiftian 类型安全:它们实际上并没有做我能看到的任何其他事情。)但真正的问题是 deadKeys 的原始定义。我希望这对某些人有用,至少在出现非碳替代品之前是这样。 (那会发生吗?)
正如您已经发现的,您必须传递 UInt32
的 地址
作为 deadKeyState
参数的变量。分配内存是一个
解决该问题的方法,但您一定不要忘记释放内存
最终,否则程序会泄漏内存。
另一种可能的解决方案是将变量的地址传递为
带有 &
:
var deadKeys : UInt32 = 0
// ...
let osStatus = UCKeyTranslate(..., &deadKeys, ...)
这个更短更简单,你不需要发布
记忆。同样可以应用于 nameBuffer
和 nameLength
.
unsafeBitCast()
可以通过使用Unmanaged
类型来避免,
比较
您还可以利用之间的免费电话桥接
CFData
和 NSData
.
那么您的函数可能如下所示 (Swift 2):
import Carbon
func keyName(virtualKeyCode: UInt16) -> String?
{
let maxNameLength = 4
var nameBuffer = [UniChar](count : maxNameLength, repeatedValue: 0)
var nameLength = 0
let modifierKeys = UInt32(alphaLock >> 8) & 0xFF // Caps Lock
var deadKeys : UInt32 = 0
let keyboardType = UInt32(LMGetKbdType())
let source = TISCopyCurrentKeyboardLayoutInputSource().takeRetainedValue()
let ptr = TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData)
let layoutData = Unmanaged<CFData>.fromOpaque(COpaquePointer(ptr)).takeUnretainedValue() as NSData
let keyboardLayout = UnsafePointer<UCKeyboardLayout>(layoutData.bytes)
let osStatus = UCKeyTranslate(keyboardLayout, virtualKeyCode, UInt16(kUCKeyActionDown),
modifierKeys, keyboardType, UInt32(kUCKeyTranslateNoDeadKeysMask),
&deadKeys, maxNameLength, &nameLength, &nameBuffer)
guard osStatus == noErr else {
NSLog("Code: 0x%04X Status: %+i", virtualKeyCode, osStatus);
return nil
}
return String(utf16CodeUnits: nameBuffer, count: nameLength)
}
Swift3 的更新:
import Carbon
func keyName(virtualKeyCode: UInt16) -> String? {
let maxNameLength = 4
var nameBuffer = [UniChar](repeating: 0, count : maxNameLength)
var nameLength = 0
let modifierKeys = UInt32(alphaLock >> 8) & 0xFF // Caps Lock
var deadKeys: UInt32 = 0
let keyboardType = UInt32(LMGetKbdType())
let source = TISCopyCurrentKeyboardLayoutInputSource().takeRetainedValue()
guard let ptr = TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData) else {
NSLog("Could not get keyboard layout data")
return nil
}
let layoutData = Unmanaged<CFData>.fromOpaque(ptr).takeUnretainedValue() as Data
let osStatus = layoutData.withUnsafeBytes {
UCKeyTranslate([=12=], virtualKeyCode, UInt16(kUCKeyActionDown),
modifierKeys, keyboardType, UInt32(kUCKeyTranslateNoDeadKeysMask),
&deadKeys, maxNameLength, &nameLength, &nameBuffer)
}
guard osStatus == noErr else {
NSLog("Code: 0x%04X Status: %+i", virtualKeyCode, osStatus);
return nil
}
return String(utf16CodeUnits: nameBuffer, count: nameLength)
}
Swift4 的更新:
从 Swift 4 开始,Data.withUnsafeBytes
使用 UnsafeRawBufferPointer
调用闭包,它必须 绑定 指向 [=25 的指针=]:
import Carbon
func keyName(virtualKeyCode: UInt16) -> String? {
let maxNameLength = 4
var nameBuffer = [UniChar](repeating: 0, count : maxNameLength)
var nameLength = 0
let modifierKeys = UInt32(alphaLock >> 8) & 0xFF // Caps Lock
var deadKeys: UInt32 = 0
let keyboardType = UInt32(LMGetKbdType())
let source = TISCopyCurrentKeyboardLayoutInputSource().takeRetainedValue()
guard let ptr = TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData) else {
NSLog("Could not get keyboard layout data")
return nil
}
let layoutData = Unmanaged<CFData>.fromOpaque(ptr).takeUnretainedValue() as Data
let osStatus = layoutData.withUnsafeBytes {
UCKeyTranslate([=13=].bindMemory(to: UCKeyboardLayout.self).baseAddress, virtualKeyCode, UInt16(kUCKeyActionDown),
modifierKeys, keyboardType, UInt32(kUCKeyTranslateNoDeadKeysMask),
&deadKeys, maxNameLength, &nameLength, &nameBuffer)
}
guard osStatus == noErr else {
NSLog("Code: 0x%04X Status: %+i", virtualKeyCode, osStatus);
return nil
}
return String(utf16CodeUnits: nameBuffer, count: nameLength)
}