在 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+O、ALT+SHIFT+M、ALT+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 中实现全局热键?的主要内容,如果未能解决你的问题,请参考以下文章