Back

[截圖工具] 3. 新增快捷鍵功能

建立 keyboard 的 package

這個 package 用來監聽鍵盤事件,當按下特定的鍵盤按鍵時,會觸發一個 callback function。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
// pkg/keyboard/keyboard.go
package keyboard

import (
    "strings"

    "github.com/moutend/go-hook/pkg/keyboard"
    "github.com/moutend/go-hook/pkg/types"
    "golang.org/x/exp/slices"
)

const (
    defaultEventChanSize = 256
    defaultListenerSize  = 256
    defaultPressedSize   = 256
)

var (
    // eventChan is a channel to receive keyboard events.
    eventChan chan types.KeyboardEvent
    // listeners is a map of hotkey listeners.
    listeners map[string]*hotkeyListener
    // pressed is a map of pressed keys.
    pressed [defaultPressedSize]bool
)

// init initializes variables.
func init() {
    reset()
}

// reset resets all variables.
func reset() {
    eventChan = make(chan types.KeyboardEvent, defaultEventChanSize)
    listeners = make(map[string]*hotkeyListener, defaultListenerSize)
    pressed = [defaultPressedSize]bool{}
}

// Hotkey represents a hotkey.
type Hotkey []types.VKCode

// hotkeyListener listens the hotkey.
type hotkeyListener struct {
    // hotkey is a hotkey to listen.
    hotkey Hotkey
    // callback is a callback function which is called when the hotkey is pressed.
    callback func()
}

// NewHotkey creates a new hotkey.
func NewHotkey(keys ...types.VKCode) Hotkey {
    slices.Sort(keys)
    return Hotkey(keys)
}

// String returns a string representation of the hotkey.
func (h Hotkey) String() string {
    b := strings.Builder{}
    for i, key := range h {
        if i > 0 {
            b.WriteString("+")
        }
        b.WriteString(key.String())
    }
    return b.String()
}

// Pressed returns true if the hotkey is pressed.
func (h Hotkey) Pressed() bool {
    for _, key := range h {
        if !pressed[key] {
            return false
        }
    }
    return true
}

// newHotkeyListener creates a new hotkey listener.
func newHotkeyListener(hotkey Hotkey, callback func()) *hotkeyListener {
    if callback == nil {
        callback = func() {}
    }
    return &hotkeyListener{
        hotkey:   hotkey,
        callback: callback,
    }
}

// Notify calls the callback function if the hotkey is pressed.
func (l *hotkeyListener) Notify() {
    l.mu.Lock()
    defer l.mu.Unlock()
    if !l.hotkey.Pressed() {
        l.justPressed = false
        return
    }
    if l.justPressed {
        return
    }
    l.justPressed = true
    l.callback()
}

// RegisterHotkey registers the hotkey.
func RegisterHotkey(hotkey Hotkey, callback func()) {
    listeners[hotkey.String()] = newHotkeyListener(hotkey, callback)
}

// UnregisterHotkey unregisters the hotkey.
func UnregisterHotkey(hotkey Hotkey) {
    delete(listeners, hotkey.String())
}

// handleEvent handles the keyboard event.
func handleEvent(event types.KeyboardEvent) {
    switch event.Message {
    case types.WM_KEYDOWN, types.WM_SYSKEYDOWN:
        pressed[event.VKCode] = true
        for _, listener := range listeners {
            listener.Notify()
        }
    case types.WM_KEYUP, types.WM_SYSKEYUP:
        pressed[event.VKCode] = false
    }
}

// Start starts the keyboard hook.
func Start() error {
    go func() {
        for event := range eventChan {
            handleEvent(event)
        }
    }()
    return keyboard.Install(nil, eventChan)
}

// Stop stops the keyboard hook.
func Stop() error {
    err := keyboard.Uninstall()
    if err != nil {
        return err
    }
    close(eventChan)
    reset()
    return nil
}

接著講解每個部份的設計:

init()

init() 會在 package 被載入時執行,這裡會呼叫 reset() 來初始化變數。

1
2
3
func init() {
    reset()
}

reset()

reset() 會初始化所有變數,這裡會初始化 eventChanlistenerspressed

1
2
3
4
5
func reset() {
    eventChan = make(chan types.KeyboardEvent, defaultEventChanSize)
    listeners = make(map[string]*hotkeyListener, defaultListenerSize)
    pressed = [defaultPressedSize]bool{}
}

Hotkey

Hotkey 是一個按鍵組合,例如 Ctrl+Shift+P,這個按鍵組合可以用 VKCode 來表示:

1
type Hotkey []types.VKCode

Hotkey 有幾個 method:

NewHotkey()

NewHotkey() 用來建立一個新的 Hotkey,並且將 VKCode 依照順序排序。

1
2
3
4
func NewHotkey(keys ...types.VKCode) Hotkey {
    slices.Sort(keys)
    return Hotkey(keys)
}

String()

String() 用來將 Hotkey 轉換成字串,例如 Ctrl+Shift+PString() 也被用來識別 Hotkey,因為 Hotkey 是一個 slice,所以無法直接用 == 來比較,必須要用 String() 來比較。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func (h Hotkey) String() string {
    b := strings.Builder{}
    for i, key := range h {
        if i > 0 {
            b.WriteString("+")
        }
        b.WriteString(key.String())
    }
    return b.String()
}

這邊使用 strings.Builder 來建立字串,因為 strings.Builder 是一個 buffer,可以有效率的建立字串。

Pressed()

Pressed() 用來檢查 Hotkey 是否被按下。

1
2
3
4
5
6
7
8
func (h Hotkey) Pressed() bool {
    for _, key := range h {
        if !pressed[key] {
            return false
        }
    }
    return true
}

hotkeyListener

hotkeyListener 是一個監聽器,當按下特定的按鍵組合時,會觸發一個 callback function。

1
2
3
4
5
6
type hotkeyListener struct {
    // hotkey is a hotkey to listen.
    hotkey Hotkey
    // callback is a callback function which is called when the hotkey is pressed.
    callback func()
}

hotkeyListener 有幾個 method:

newHotkeyListener()

newHotkeyListener() 用來建立一個新的 hotkeyListener

1
2
3
4
5
6
7
8
9
func newHotkeyListener(hotkey Hotkey, callback func()) *hotkeyListener {
    if callback == nil {
        callback = func() {}
    }
    return &hotkeyListener{
        hotkey:   hotkey,
        callback: callback,
    }
}

hotkeyListener 會儲存 Hotkey 與 callback function。

Notify()

Notify() 會檢查 Hotkey 是否被按下,如果有被按下,就會呼叫 callback function。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func (l *hotkeyListener) Notify() {
    l.mu.Lock()
    defer l.mu.Unlock()
    if !l.hotkey.Pressed() {
        l.justPressed = false
        return
    }
    if l.justPressed {
        return
    }
    l.justPressed = true
    l.callback()
}

這邊使用 justPressed 來避免重複觸發 callback function。 除此之外,Notify() 也使用 sync.Mutex 來避免同時觸發 callback function。

RegisterHotkey()

RegisterHotkey() 用來註冊 Hotkey

1
2
3
func RegisterHotkey(hotkey Hotkey, callback func()) {
    listeners[hotkey.String()] = newHotkeyListener(hotkey, callback)
}

這邊使用 listeners 來儲存 Hotkeylisteners 是一個 mapkeyHotkey 的字串表示,valuehotkeyListener

UnregisterHotkey()

UnregisterHotkey() 用來取消註冊 Hotkey

1
2
3
func UnregisterHotkey(hotkey Hotkey) {
    delete(listeners, hotkey.String())
}

UnregisterHotkey() 的作法很簡單,就是從 listeners 中刪除 Hotkey

handleEvent()

handleEvent() 用來處理鍵盤事件,當鍵盤事件發生時,會通知所有的 hotkeyListener

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func handleEvent(event types.KeyboardEvent) {
    switch event.Message {
    case types.WM_KEYDOWN, types.WM_SYSKEYDOWN:
        pressed[event.VKCode] = true
        for _, listener := range listeners {
            listener.Notify()
        }
    case types.WM_KEYUP, types.WM_SYSKEYUP:
        pressed[event.VKCode] = false
    }
}

Start()

Start() 用來啟動鍵盤監聽器。

1
2
3
4
5
6
7
8
func Start() error {
    go func() {
        for event := range eventChan {
            handleEvent(event)
        }
    }()
    return keyboard.Install(nil, eventChan)
}

Start() 被呼叫時,會使用一個 goroutine 來處理鍵盤事件,當事件發生時就透過 handleEvent() 來處理建盤事件,並且呼叫 keyboard.Install() 來啟動鍵盤監聽器。

Stop()

Stop() 用來停止鍵盤監聽器。

1
2
3
4
5
6
7
8
9
func Stop() error {
    err := keyboard.Uninstall()
    if err != nil {
        return err
    }
    close(eventChan)
    reset()
    return nil
}

Stop() 被呼叫時,會呼叫 keyboard.Uninstall() 來停止鍵盤監聽器,同時關閉 eventChan,並呼叫 reset() 來重設所有的按鍵狀態。

修改 app.go

新增 bindKeyboard()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// bindKeyboard binds the keyboard to the app
func (a *App) bindKeyboard() {
    // Register the hotkey
    keyboard.RegisterHotkey(keyboard.NewHotkey(types.VK_LCONTROL, types.VK_LMENU, types.VK_A), func() {
        runtime.EventsEmit(a.ctx, "capture")
    })
    // Start the keyboard
    if err := keyboard.Start(); err != nil {
        log.Fatal(err)
    }
    // Stop the keyboard when the app closes
    go func() {
        <-a.ctx.Done()
        if err := keyboard.Stop(); err != nil {
            log.Fatal(err)
        }
    }()
}

這邊註冊了一個 Ctrl+Alt+A 的熱鍵,當按下熱鍵時,就會觸發 runtime.EventsEmit(a.ctx, "capture")。 而 runtime.EventsEmit(a.ctx, "capture") 會發出一個 Wailscapture 事件,讓 frontend 可以接收到。

修改 startup()

將 bindKeyboard() 加入到 startup() 中。

1
2
3
4
5
func (a *App) startup(ctx context.Context) {
    a.ctx = ctx
    a.bindSystray()
    a.bindKeyboard()
}

修改 frontend

接收 capture 事件

修改 App.svelte,加入 EventsOn("capture", () => {})

1
2
3
4
5
6
7
import { EventsOn } from "../wailsjs/runtime/runtime.js"

...

EventsOn("capture", () => {
    greet()
})

測試

執行程式,按下 Ctrl+Alt+A,就會看到成功觸發 capture 事件並顯示截圖。

Built with Hugo
Theme Stack designed by Jimmy
© Licensed Under CC BY-NC-SA 4.0