clipboard/listener_windows.go
2025-11-10 10:17:06 +08:00

408 lines
8.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//go:build windows
package clipboard
import (
"b612.me/win32api"
"errors"
"fmt"
"runtime"
"sync"
"sync/atomic"
"syscall"
"time"
"unsafe"
)
var stopSign chan struct{}
var isListening uint32
/*
func listenMethod2() (<-chan Clipboard, error) {
if atomic.LoadUint32(&isListening) == 1 {
return nil, errors.New("Already listening")
}
atomic.StoreUint32(&isListening, 1)
res := make(chan Clipboard, 3)
stopSign = make(chan struct{})
hWnd, err := win32api.CreateWindowEx(0,
"Message",
"B612 Clipboard Listener",
0,
0, 0, 500, 500,
0, 0, 0, nil)
if hWnd == 0 || err != nil {
return nil, fmt.Errorf("Failed to create window: %v,hWnd:%v", err, hWnd)
}
set, err := win32api.AddClipboardFormatListener(hWnd)
if !set || err != nil {
return nil, fmt.Errorf("Failed to set clipboard listener: %v", err)
}
fetcher := make(chan struct{}, 8)
go fetchListener(hWnd, fetcher)
go func() {
for {
select {
case <-stopSign:
fmt.Println("stopped")
atomic.StoreUint32(&isListening, 0)
close(res)
close(stopSign)
win32api.RemoveClipboardFormatListener(win32api.HWND(hWnd))
win32api.DestoryWindow(hWnd)
return
case <-fetcher:
cb, err := Get()
if err != nil {
fmt.Println(err)
}
if atomic.LoadUint32(&isListening) == 1 {
res <- cb
continue
}
}
}
}()
return res, nil
}
func fetchListener(hWnd win32api.HWND, res chan struct{}) {
for {
var msg win32api.MSG
_, err := win32api.GetMessage(&msg, hWnd, 0, 0)
if msg.Message == 0x0012 {
return
}
if err == nil && win32api.DWORD(msg.Message) == win32api.WM_CLIPBOARDUPDATE {
res <- struct{}{}
}
}
}
func PauseListen() error {
if atomic.LoadUint32(&isListening) == 0 {
return errors.New("Not listening")
}
atomic.StoreUint32(&isListening, 2)
return nil
}
func RecoverListen() error {
if atomic.LoadUint32(&isListening) == 0 {
return errors.New("Not Listening")
}
atomic.StoreUint32(&isListening, 1)
return nil
}
func StopListen() error {
defer func() {
if r := recover(); r != nil {
fmt.Println("StopListen panic:", r)
}
}()
if atomic.LoadUint32(&isListening) == 0 {
return nil
}
stopSign <- struct{}{}
return nil
}
func Listen(onlyMeta bool) (<-chan Clipboard, error) {
if atomic.LoadUint32(&isListening) != 0 {
return nil, errors.New("Already listening")
}
atomic.StoreUint32(&isListening, 1)
res := make(chan Clipboard, 3)
stopSign = make(chan struct{})
go func() {
var storeSeq win32api.DWORD
for {
select {
case <-stopSign:
atomic.StoreUint32(&isListening, 0)
close(res)
close(stopSign)
return
case <-time.After(time.Millisecond * 900):
seq, err := win32api.GetClipboardSequenceNumber()
if err != nil {
continue
}
if seq != storeSeq {
storeSeq = seq
//fmt.Println("Clipboard updated", seq, storeSeq)
if atomic.LoadUint32(&isListening) == 1 {
if !onlyMeta {
cb, err := Get()
if err != nil {
continue
}
if atomic.LoadUint32(&isListening) == 1 {
res <- cb
continue
}
} else {
cb, err := GetMeta()
if err != nil {
continue
}
if atomic.LoadUint32(&isListening) == 1 {
res <- cb
continue
}
}
}
}
}
}
}()
return res, nil
}
*/
const WM_USER_STOP = win32api.WM_USER + 1
var clipboardChannel chan Clipboard
var onlyMetaGlobal bool
var hwndGlobal win32api.HWND
var lastSequence win32api.DWORD
var sequenceMux sync.Mutex
func clipboardWndProc(hWnd win32api.HWND, uMsg win32api.UINT, wParam win32api.WPARAM, lParam win32api.LPARAM) win32api.LRESULT {
switch uMsg {
case 0x031D: // WM_CLIPBOARDUPDATE
// 标记需要检查剪贴板
go checkClipboard(false)
return 0
case WM_USER_STOP:
win32api.PostQuitMessage(0)
return 0
case 0x0002: // WM_DESTROY
win32api.PostQuitMessage(0)
return 0
}
return win32api.DefWindowProc(hWnd, uMsg, wParam, lParam)
}
// 检查剪贴板是否真的有变化
func checkClipboard(ignoreMessage bool) {
if atomic.LoadUint32(&isListening) != 1 {
return
}
sequenceMux.Lock()
defer sequenceMux.Unlock()
// 获取当前剪贴板序列号
var currentSeq win32api.DWORD
runInWorker(func() {
currentSeq, _ = win32api.GetClipboardSequenceNumber()
})
// 如果序列号没有变化,跳过
if !ignoreMessage && currentSeq == lastSequence {
return
}
lastSequence = currentSeq
// 尝试读取剪贴板,带重试机制
var cb Clipboard
var err error
maxRetries := 3
retryDelay := 10 * time.Millisecond
for i := 0; i < maxRetries; i++ {
runInWorker(func() {
if !onlyMetaGlobal {
cb, err = getClipboardWithRetry()
} else {
cb, err = getMetaWithRetry()
}
})
if err == nil {
break
}
// 如果剪贴板被占用,稍等再试
if i < maxRetries-1 {
time.Sleep(retryDelay)
retryDelay *= 2 // 指数退避
}
}
if err == nil && clipboardChannel != nil {
select {
case clipboardChannel <- cb:
default:
// channel满了移除最旧的
select {
case <-clipboardChannel:
clipboardChannel <- cb
default:
}
}
}
}
func handleClipboardUpdate() {
if atomic.LoadUint32(&isListening) != 1 {
return
}
var cb Clipboard
var err error
if !onlyMetaGlobal {
cb, err = Get()
} else {
cb, err = GetMeta()
}
if err == nil && clipboardChannel != nil {
select {
case clipboardChannel <- cb:
default:
// channel满了丢弃旧数据
}
}
}
func Listen(onlyMeta bool) (<-chan Clipboard, error) {
if atomic.LoadUint32(&isListening) != 0 {
return nil, errors.New("Already listening")
}
atomic.StoreUint32(&isListening, 1)
res := make(chan Clipboard, 10) // 增大缓冲区
clipboardChannel = res
onlyMetaGlobal = onlyMeta
stopSign = make(chan struct{})
// 初始化序列号
runInWorker(func() {
lastSequence, _ = win32api.GetClipboardSequenceNumber()
})
go func() {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
defer func() {
if hwndGlobal != 0 {
win32api.RemoveClipboardFormatListener(hwndGlobal)
win32api.DestroyWindow(hwndGlobal)
hwndGlobal = 0
}
atomic.StoreUint32(&isListening, 0)
close(res)
clipboardChannel = nil
}()
hInstance, err := win32api.GetModuleHandle(nil)
if err != nil {
return
}
className := fmt.Sprintf("ClipListener_%d", time.Now().UnixNano())
var wc win32api.WNDCLASSEX
wc.CbSize = win32api.UINT(unsafe.Sizeof(wc))
wc.Style = 0
wc.LpfnWndProc = syscall.NewCallback(clipboardWndProc)
wc.CbClsExtra = 0
wc.CbWndExtra = 0
wc.HInstance = hInstance
wc.HIcon = 0
wc.HCursor = 0
wc.HbrBackground = 0
wc.LpszMenuName = nil
wc.LpszClassName = syscall.StringToUTF16Ptr(className)
wc.HIconSm = 0
_, err = win32api.RegisterClassEx(&wc)
if err != nil {
return
}
hwnd, err := win32api.CreateWindowEx(
0, className, "", 0,
0, 0, 0, 0,
0, 0, hInstance, unsafe.Pointer(nil))
if err != nil {
return
}
hwndGlobal = hwnd
ok, err := win32api.AddClipboardFormatListener(hwnd)
if err != nil || !ok {
win32api.DestroyWindow(hwnd)
return
}
go func() {
ticker := time.NewTicker(5000 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-stopSign:
return
case <-ticker.C:
// 定期检查,防止遗漏
go checkClipboard(false)
}
}
}()
// 监听停止信号
go func() {
<-stopSign
if hwndGlobal != 0 {
win32api.PostMessage(hwndGlobal, WM_USER_STOP, 0, 0)
}
}()
// 消息循环
for {
var msg win32api.MSG
ret, err := win32api.GetMessage(&msg, 0, 0, 0)
if ret == 0 {
// 收到WM_QUIT消息正常退出
break
}
if ret == 0xFFFFFFFF {
// GetMessage出错
if err != nil {
fmt.Printf("GetMessage error: %v\n", err)
}
continue
}
win32api.TranslateMessage(&msg)
win32api.DispatchMessage(&msg)
}
}()
return res, nil
}
func StopListen() {
if atomic.LoadUint32(&isListening) == 1 && stopSign != nil {
close(stopSign)
}
}
// 在工作线程中执行剪贴板操作
func runInWorker(fn func()) {
done := make(chan struct{})
clipboardWorker <- func() {
fn()
close(done)
}
<-done
}