【问题标题】:Getting key names for keyboard codes in Swift在 Swift 中获取键盘代码的键名
【发布时间】:2016-01-31 05:28:08
【问题描述】:

我知道其他人也问过类似的问题,但我还没有看到明确的答案,我仍然陷入困境。我正在尝试编写一个 Swift 函数,该函数采用硬件生成的键盘扫描代码,例如来自 NSEvent,并返回特定键排列(Dvorak、Qwerty 等)的 alpha-caps-locked 键的名称。 ) 当前在操作系统中有效(可能与生成代码时的有效排列不同)。

据我了解,这样做的唯一方法是调用一些非常古老的 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 总是返回 -50 的状态,我理解这意味着有一个参数错误。我怀疑“keyboardLayout”,因为它是最复杂的设置。谁能看到参数问题?或者这种事情有更新的框架吗?

【问题讨论】:

  • 您看到了吗:How to use UCKeyTranslate?看起来很相似。
  • @Martin 我做了,发现它和 Ken Thomases 的回答很有帮助,但在最终版本中,看起来作者试图在最后一个参数中传递一个指向 Swift 数组的指针,这可能期待更像是指向旧 C 样式数组的指针。我对 Swift 很陌生,但这看起来很危险,我无法让 Swift 接受它。而且我不知道是否有人解决了返回 -50 状态值的问题。
  • 不需要modifierKeys 幻数。根据 UCKeyTranslate 文档使用modifierKeys = UInt32(((alphaLock) &gt;&gt; 8) &amp; 0xFF)

标签: swift keyboard-events macos-carbon


【解决方案1】:

正如您已经发现的那样,您必须传递UInt32地址 变量作为deadKeyState 参数。分配内存是其中之一 解决这个问题的方法,但你不能忘记释放内存 最终,否则程序会泄漏内存。

另一种可能的解决方案是将变量的地址作为 与&amp; 的inout 参数:

var deadKeys : UInt32 = 0
// ...
let osStatus = UCKeyTranslate(..., &deadKeys, ...)

这有点短和简单,你不需要释放 记忆。这同样适用于nameBuffernameLength

unsafeBitCast() 可以通过使用 Unmanaged 类型来避免, 比较 Swift: CFArray : get values as UTF Strings 的类似问题和 更详细的解释。

您还可以利用之间的免费桥接 CFDataNSData

那么你的函数可能看起来像这样(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)
}

Swift 3 更新:

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($0, 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 4 更新:

从 Swift 4 开始,Data.withUnsafeBytes 使用 UnsafeRawBufferPointer 调用闭包,它必须绑定指向UCKeyboardLayout 的指针:

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($0.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)
}

【讨论】:

  • 经过一些实验,我不得不说我真的更喜欢你的做事方式:它更干净,inout 参数更容易看到,并且不玩类型安全的游戏。很高兴知道我可以直接在最后一个参数中使用 Swift 数组。这真是太好了。非常感谢。
  • 嗨,let keyboardLayout = UnsafePointer&lt;UCKeyboardLayout&gt;(layoutData.bytes) 不适用于 Swift 3.0。
  • @Vayn: keyboardLayout 已成为layoutData.withUnsafeBytes {...} 中的$0。这是Data 的通用方法,$0 的类型由编译器从UCKeyTranslate() 的签名中自动推断出来,UnsafePointer&lt;UCKeyboardLayout&gt; 作为第一个参数。 – Swift 3 指针魔术 :)
  • 值得注意的是,术语“扫描码”在这里可能会让人感到困惑。例如,在 ANSI 键盘上,来自物理硬件的“a”键的扫描码是 0x04,而“a”的虚拟键码是 0x00。将扫描码直接传递给 UCKeyTranslate 将导致返回意外的 unicode 字符串。可能值得将参数名称重命名为“virtualKeyCode”。
  • @Chris:感谢您的建议和信息。这也匹配 UCKeyTranslate 函数的记录参数名称。
【解决方案2】:

好的,我相信我找到了问题所在。尽管回答我自己的问题感觉很奇怪,但我知道在这种情况下这样做是正确的。

有问题的参数似乎是 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   }
  }

还有一些其他的变化,几乎是装饰性的:我消除了一些导致定义键盘布局的中间常量。 (“BitCasts”只是为了满足 Swiftian 的类型安全性:它们并没有真正做任何我能看到的事情。)但真正的问题是 deadKeys 的原始定义。我希望这对某人有用,至少在有非碳替代品之前。 (会发生这种情况吗?)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-11-19
    • 2014-10-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多