在 golang 中实现全局热键?

Posted

技术标签:

【中文标题】在 golang 中实现全局热键?【英文标题】:Implement a global hotkey in golang? 【发布时间】:2016-12-03 11:04:17 【问题描述】:

假设有人想在 Go (golang) 中创建一个跨平台(Mac、Linux、Windows)全局热键 - 您在操作系统的任意位置按下热键组合,假设在终端中打印了一些内容。

目前,(2016 年 7 月)我还没有找到任何图书馆可以做到这一点,所以也许我们可以一起找到方法。

当然,这会涉及为每个操作系统调用一些本机操作系统绑定,但是关于如何做的信息非常少。

Mac

从谷歌搜索来看应该使用addGlobalMonitorForEventsMatchingMask

编辑:删除无用的示例

Linux

看起来嫌疑人是XGrabKey,不过,附近没有任何示例代码https://github.com/search?utf8=%E2%9C%93&q=language%3Ago+XGrabKey&type=Repositories&ref=searchresults

窗口

似乎我们需要使用RegisterHotKey,但试图找到一些示例代码无济于事:https://github.com/search?utf8=%E2%9C%93&q=language%3Ago+RegisterHotKey

一些有趣的跨平台研究项目(用Java)是https://github.com/tulskiy/jkeymaster

任何帮助将不胜感激!

【问题讨论】:

我认为它不应该属于go标签。 @JiangYD 根据 SO 规则 - 必须存在一个标签。如果您有更好的建议 - 请分享 我不认为这个功能可以用 Go 实现。这样的跨平台特性,不是典型的 Go 东西。对于您的任务,也许您可​​以使用电子:electron.atom.io 您也可以使用电子应用调用 Go 应用。 我并不是说这很容易,这就是我提供赏金的原因。但它可能是可行的 :) Electron 很好,但对于一个简单的应用程序来说它是 100Mb,这在很多情况下是不合理的。 好吧,Java 示例只是我的两分钱......由于 JVM,Java 意味着在任何地方都可以运行。另一方面,必须为每个平台编译 Golang。那么,为了使其交叉兼容,真的需要额外的工作吗? 【参考方案1】:

这在大多数具有简单系统调用的操作系统上都是可能的,在 Go 中,您可以使用包 syscall 进行系统调用,而无需任何额外的 C 代码或 cgo 编译器。

注意syscall包的在线官方文档只显示了linux界面。其他操作系统的界面略有不同,例如在 Windows 上,syscall 包还包含 syscall.DLL 类型、syscall.LoadDLL()syscall.MustLoadDLL() 函数等。要查看这些,请在本地运行 godoc 工具,例如

godoc -http=:6060

这将启动一个托管类似于godoc.org 的网页的网络服务器,导航到http://localhost:6060/pkg/syscall/。也可以在线查看特定平台的文档,详情见How to access platform specific package documentation?

在这里,我展示了一个完整的、可运行的纯 Go 的 Windows 解决方案。 Go Playground 上提供了完整的示例应用程序。它不在操场上运行,下载并在本地运行(在 Windows 上)。


让我们定义类型来描述我们想要使用的热键。这不是特定于 Windows 的,只是为了使我们的代码更好。 Hotkey.String() 方法提供了一个人性化的热键显示名称,例如 "Hotkey[Id: 1, Alt+Ctrl+O]"

const (
    ModAlt = 1 << iota
    ModCtrl
    ModShift
    ModWin
)

type Hotkey struct 
    Id        int // Unique id
    Modifiers int // Mask of modifiers
    KeyCode   int // Key code, e.g. 'A'


// String returns a human-friendly display name of the hotkey
// such as "Hotkey[Id: 1, Alt+Ctrl+O]"
func (h *Hotkey) String() string 
    mod := &bytes.Buffer
    if h.Modifiers&ModAlt != 0 
        mod.WriteString("Alt+")
    
    if h.Modifiers&ModCtrl != 0 
        mod.WriteString("Ctrl+")
    
    if h.Modifiers&ModShift != 0 
        mod.WriteString("Shift+")
    
    if h.Modifiers&ModWin != 0 
        mod.WriteString("Win+")
    
    return fmt.Sprintf("Hotkey[Id: %d, %s%c]", h.Id, mod, h.KeyCode)

Windows user32.dll 包含全局热键管理功能。让我们加载它:

user32 := syscall.MustLoadDLL("user32")
defer user32.Release()

它有一个RegisterHotkey()函数用于注册全局热键:

reghotkey := user32.MustFindProc("RegisterHotKey")

使用这个,让我们注册一些热键,即ALT+CTRL+OALT+SHIFT+MALT+CTRL+X (将用于退出应用程序):

// Hotkeys to listen to:
keys := map[int16]*Hotkey
    1: &Hotkey1, ModAlt + ModCtrl, 'O',  // ALT+CTRL+O
    2: &Hotkey2, ModAlt + ModShift, 'M', // ALT+SHIFT+M
    3: &Hotkey3, ModAlt + ModCtrl, 'X',  // ALT+CTRL+X


// Register hotkeys:
for _, v := range keys 
    r1, _, err := reghotkey.Call(
        0, uintptr(v.Id), uintptr(v.Modifiers), uintptr(v.KeyCode))
    if r1 == 1 
        fmt.Println("Registered", v)
     else 
        fmt.Println("Failed to register", v, ", error:", err)
    

我们需要一种方法来“监听”按下这些热键的事件。为此,user32.dll 包含 PeekMessage() 函数:

peekmsg := user32.MustFindProc("PeekMessageW")

PeekMessage() 将消息存储到 MSG 结构中,让我们定义它:

type MSG struct 
    HWND   uintptr
    UINT   uintptr
    WPARAM int16
    LPARAM int64
    DWORD  int32
    POINT  struct X, Y int64 

现在这是我们的监听循环,它监听并作用于全局按键(这里只是简单地将按下的热键打印到控制台,如果按下 CTRL+ALT+X,则退出应用程序) :

for 
    var msg = &MSG
    peekmsg.Call(uintptr(unsafe.Pointer(msg)), 0, 0, 0, 1)

    // Registered id is in the WPARAM field:
    if id := msg.WPARAM; id != 0 
        fmt.Println("Hotkey pressed:", keys[id])
        if id == 3  // CTRL+ALT+X = Exit
            fmt.Println("CTRL+ALT+X pressed, goodbye...")
            return
        
    

    time.Sleep(time.Millisecond * 50)

我们完成了!

启动上述应用程序打印:

Registered Hotkey[Id: 1, Alt+Ctrl+O]
Registered Hotkey[Id: 2, Alt+Shift+M]
Registered Hotkey[Id: 3, Alt+Ctrl+X]

现在让我们按下一些已注册的热键(任何应用都处于焦点位置,不一定是我们的应用),我们将在控制台上看到:

Hotkey pressed: Hotkey[Id: 1, Alt+Ctrl+O]
Hotkey pressed: Hotkey[Id: 1, Alt+Ctrl+O]
Hotkey pressed: Hotkey[Id: 2, Alt+Shift+M]
Hotkey pressed: Hotkey[Id: 3, Alt+Ctrl+X]
CTRL+ALT+X pressed, goodbye...

【讨论】:

完美答案!谢谢! for 循环中某处存在内存泄漏。如果你在任务管理器中观察内存,它会慢慢增长,永远不会下降。 是否可以使用for channel 之类的东西来代替time.Sleep?我对库或user32 dll 不太熟悉,但很好的解释和答案。 @Datsik 不,我不这么认为。如果可以的话,我会使用频道。 你会说这是一种“hacky”方式来实现这一点,最好还是用不同的语言来完成,或者这样可以吗?最后一个问题;)【参考方案2】:

这是可以做到的。例如游戏引擎 Azul3d 可以做到这一点https://github.com/azul3d/engine/tree/master/keyboard。您的 OSX sn-p 无法编译,因为缺少 AppDelegate.h 文件。您可以手动编写它,也可以先使用 XCode 编译项目,它会为您记账,然后查看 AppDelegate.h 和 AppDelegate.m。在 linux 上记录击键流,您可以读取 /dev/input/event$ID 文件,该文件在此处完成 https://github.com/MarinX/keylogger。在 Windows 上,您将需要一些系统调用,例如此代码 https://github.com/eiannone/keyboard。在 nix 和 win 上,你甚至不需要 cgo 并且可以对系统调用和纯 Go 感到满意。

【讨论】:

优秀的指点!根据 SO 规则,我将在 23 小时内奖励赏金 :) 我用过github.com/MarinX/keylogger。在 Linux 上运行良好。【参考方案3】:

icza's answer 很棒,对我有很大帮助,但我发现使用 PeekMessageW API 调用比 GetMessageW API 调用更昂贵且可靠性更低。

根据Microsoft documentation:

与 GetMessage 不同,PeekMessage 函数在返回之前不会等待消息发布。

根据我对 Windows 内部的有限理解,这意味着 PeekMessageW 会不断检查消息队列并返回“0”直到找到事件,从而导致不必要的计算。

只需将代码更改为:

getmsg := user32.MustFindProc("GetMessageW")

for 
    var msg = &MSG
    getmsg.Call(uintptr(unsafe.Pointer(msg)), 0, 0, 0)

为我解决了这个问题。这意味着您也可以摆脱超时而不会显着影响性能:

time.Sleep(time.Millisecond * 50)

我想知道可靠性问题是否来自我在 goroutine 中运行热键循环这一事实。我对 Go 或 Windows API 的经验不够了解,但希望这对某人有所帮助!虽然我无法验证 Programming4life 建议的原始代码是否存在内存泄漏,但我也没有经历过任何类型的内存泄漏。

【讨论】:

很好的答案,这不仅可以防止忙等待,还可以摆脱轮询滞后!

以上是关于在 golang 中实现全局热键?的主要内容,如果未能解决你的问题,请参考以下文章

如何在Golang中实现正确的并行性? goroutines是否与Go1.5 +并行?

golang gin框架中实现大文件的流式上传

在golang中递归压缩目录

在 golang 中收集 Kubernetes 指标

Golang 入门系列-八怎样实现定时任务,极简版.

golang cron定时任务简单实现