基于Go编写一个Windows剪贴板监控器

 更新时间:2025年11月18日 09:40:37   作者:Mgx  
这篇文章主要为大家详细介绍了如何基于Go编写一个Windows剪贴板监控器,可以在后台默默监听你的复制行为,感兴趣的小伙伴可以跟随小编一起学习一下

你有没有想过,当你按下 Ctrl+C 的那一瞬间,你的剪贴板内容其实可以被"悄悄"记录下来?别慌,这不是什么黑客教程,而是一次用 Go 语言 和 Windows API 玩转系统底层的趣味实验!

今天,我们就来手把手实现一个 剪贴板监视器 —— 它会在后台默默监听你的复制行为,并把内容打印出来(当然,也可以做更多事情,比如自动保存、翻译、甚至发到邮箱里 )。

为什么要做这个

  • 学习目的:理解 Windows 消息机制、剪贴板架构、Unicode/ANSI 编码处理。
  • 实用场景:自动化工具、开发调试、剪贴板历史记录等。
  • 装 X 需求:在同事面前演示:"看,我一复制,程序就知道我抄了啥!"

 技术原理:Windows 剪贴板监听机制

Windows 提供了一套经典的"剪贴板查看器链"(Clipboard Viewer Chain)机制:

  • 你可以把自己的窗口注册为"剪贴板查看器"。
  • 一旦剪贴板内容发生变化,系统会向链中的每个窗口发送 WM_DRAWCLIPBOARD 消息。
  • 你收到消息后,就可以打开剪贴板、读取内容、然后优雅地关掉它。

听起来是不是有点像"订阅-发布"模式?没错!只不过这是 1980 年代的 Windows 版 Pub/Sub。

注意事项

微软官方已不推荐使用 SetClipboardViewer(推荐用 AddClipboardFormatListener),但为了兼容性和教学目的,我们仍用经典方式。

核心挑战:剪贴板数据格式

剪贴板可不是只存文本!它支持多种格式:

格式常量含义
CF_TEXTANSI 文本(通常是 GBK 编码)
CF_UNICODETEXTUTF-16 文本(现代应用主流)
CF_HDROP文件拖放(比如从资源管理器复制文件)
其他自定义格式比如 Word 的富文本、图片等

所以我们的程序必须:

  • 枚举所有格式
  • 优先读取 CF_UNICODETEXT
  • 降级处理 CF_TEXT(并转码为 UTF-8)
  • 识别文件拖放
  • 对未知格式友好提示

代码实现:Go + Windows API

我们使用 golang.org/x/sys/windows 调用 Windows API,并配合 unsafe 和 reflect 直接操作内存(别怕,有安全兜底)。

下面就是完整源码(已加详细注释):

package main

import (
	"bytes"
	"fmt"
	"io"
	"reflect"
	"syscall"
	"unicode/utf16"
	"unsafe"

	"golang.org/x/sys/windows"
	"golang.org/x/text/encoding/simplifiedchinese"
	"golang.org/x/text/transform"
)

// ===== Win32 API Constants =====
const (
	WM_CREATE        = 0x0001
	WM_DESTROY       = 0x0002
	WM_DRAWCLIPBOARD = 0x0308
	WM_CHANGECBCHAIN = 0x030D
	CF_TEXT          = 1
	CF_BITMAP        = 2
	CF_UNICODETEXT   = 13
	CF_HDROP         = 15
)

// ===== Win32 Structures =====
type WNDCLASSEX struct {
	Size       uint32
	Style      uint32
	WndProc    uintptr
	ClsExtra   int32
	WndExtra   int32
	Instance   syscall.Handle
	Icon       syscall.Handle
	Cursor     syscall.Handle
	Background uintptr
	MenuName   *uint16
	ClassName  *uint16
	IconSm     syscall.Handle
}

type MSG struct {
	HWnd    uintptr
	Message uint32
	WParam  uintptr
	LParam  uintptr
	DwTime  uint32
	PtX     int32
	PtY     int32
}

// ===== Win32 API Procs =====
var (
	user32   = windows.NewLazySystemDLL("user32.dll")
	kernel32 = windows.NewLazySystemDLL("kernel32.dll")
	shell32  = windows.NewLazySystemDLL("shell32.dll")

	procRegisterClassEx      = user32.NewProc("RegisterClassExW")
	procCreateWindowEx       = user32.NewProc("CreateWindowExW")
	procDefWindowProc        = user32.NewProc("DefWindowProcW")
	procOpenClipboard        = user32.NewProc("OpenClipboard")
	procCloseClipboard       = user32.NewProc("CloseClipboard")
	procEnumClipboardFormats = user32.NewProc("EnumClipboardFormats")
	procGetClipboardData     = user32.NewProc("GetClipboardData")
	procSetClipboardViewer   = user32.NewProc("SetClipboardViewer")
	procGetMessage           = user32.NewProc("GetMessageW")
	procTranslateMessage     = user32.NewProc("TranslateMessage")
	procDispatchMessage      = user32.NewProc("DispatchMessageW")
	procShowWindow           = user32.NewProc("ShowWindow")
	procGlobalLock           = kernel32.NewProc("GlobalLock")
	procGlobalUnlock         = kernel32.NewProc("GlobalUnlock")
	procDragQueryFile        = shell32.NewProc("DragQueryFileW")
)

// ===== 窗口过程函数 =====
func windowProc(hwnd syscall.Handle, msg uint32, wParam uintptr, lParam uintptr) uintptr {
	switch msg {
	case WM_CREATE:
		_, _, _ = procSetClipboardViewer.Call(uintptr(hwnd))

	case WM_DRAWCLIPBOARD:
		if ret, _, _ := procOpenClipboard.Call(uintptr(hwnd)); ret == 0 {
			fmt.Println("❌ 无法打开剪切板")
			break
		}
		var format uint32 = 0
		var finalText string

		for {
			r1, _, _ := procEnumClipboardFormats.Call(uintptr(format))
			format = uint32(r1)
			if format == 0 {
				break
			}

			hMem, _, _ := procGetClipboardData.Call(uintptr(format))
			if hMem == 0 {
				continue
			}

			switch format {
			case CF_UNICODETEXT:
				text, err := getUnicodeText(hMem)
				if err == nil {
					finalText = text
					goto done
				}
			case CF_TEXT:
				text, err := getAnsiText(hMem)
				if err == nil && finalText == "" {
					finalText = text
				}
			case CF_HDROP:
				finalText = ""
				files, err := getHDropFiles(hMem)
				if err != nil {
					fmt.Println("❌ 文件列表: 读取文件失败:", err)
				} else {
					fmt.Printf("📎 拖放的文件 (%d):\n", len(files))
					for _, f := range files {
						fmt.Println(" -", f)
					}
				}
			default:
				finalText = ""
				fmt.Printf("📎 %s\n", getClipboardFormatName(format))
			}
		}
	done:
		if finalText != "" {
			fmt.Printf("📝 文本内容:\n%s\n", finalText)
		}
		procCloseClipboard.Call()

	case WM_CHANGECBCHAIN:
		fmt.Println("⛓️ 剪切板查看器链已更改")

	default:
		ret, _, _ := procDefWindowProc.Call(uintptr(hwnd), uintptr(msg), wParam, lParam)
		return ret
	}
	return 0
}

// ===== 辅助函数 =====
func getAnsiText(hMem uintptr) (string, error) {
	lpLock, _, _ := procGlobalLock.Call(hMem)
	if lpLock == 0 {
		return "", fmt.Errorf("无法锁定内存")
	}
	defer procGlobalUnlock.Call(hMem)

	var size int
	for {
		b := *(*byte)(unsafe.Pointer(lpLock + uintptr(size)))
		if b == 0 {
			break
		}
		size++
	}

	data := make([]byte, size)
	for i := 0; i < size; i++ {
		data[i] = *(*byte)(unsafe.Pointer(lpLock + uintptr(i)))
	}

	decoder := simplifiedchinese.GBK.NewDecoder()
	reader := transform.NewReader(bytes.NewReader(data), decoder)
	utf8Bytes, err := io.ReadAll(reader)
	if err != nil {
		return "", err
	}
	return string(utf8Bytes), nil
}

func getUnicodeText(hMem uintptr) (string, error) {
	lpLock, _, _ := procGlobalLock.Call(hMem)
	if lpLock == 0 {
		return "", fmt.Errorf("无法锁定内存")
	}
	defer procGlobalUnlock.Call(hMem)

	var length int
	for {
		w := *(*uint16)(unsafe.Pointer(lpLock + uintptr(length*2)))
		if w == 0 {
			break
		}
		length++
	}

	sliceHeader := reflect.SliceHeader{
		Data: lpLock,
		Len:  length,
		Cap:  length,
	}
	utf16Data := *(*[]uint16)(unsafe.Pointer(&sliceHeader))

	runes := utf16.Decode(utf16Data)
	return string(runes), nil
}

func getHDropFiles(hMem uintptr) ([]string, error) {
	lpLock, _, _ := procGlobalLock.Call(hMem)
	if lpLock == 0 {
		return nil, fmt.Errorf("无法锁定HDROP内存")
	}
	defer procGlobalUnlock.Call(hMem)

	count, _, _ := procDragQueryFile.Call(lpLock, 0xFFFFFFFF, 0, 0)
	if count == 0 {
		return nil, fmt.Errorf("HDROP中未找到文件")
	}

	files := make([]string, count)
	for i := uintptr(0); i < count; i++ {
		var buf [260]uint16
		_, _, _ = procDragQueryFile.Call(lpLock, i, uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf)))
		files[i] = syscall.UTF16ToString(buf[:])
	}
	return files, nil
}

func getClipboardFormatName(format uint32) string {
	if format <= 0xC000 {
		switch format {
		case CF_TEXT:
			return "CF_TEXT (ANSI文本)"
		case CF_UNICODETEXT:
			return "CF_UNICODETEXT"
		case CF_BITMAP:
			return "CF_BITMAP (位图)"
		case CF_HDROP:
			return "CF_HDROP (文件列表)"
		default:
			return "标准格式"
		}
	}
	buf := make([]uint16, 256)
	r1, _, _ := user32.NewProc("GetClipboardFormatNameW").Call(
		uintptr(format),
		uintptr(unsafe.Pointer(&buf[0])),
		uintptr(len(buf)),
	)
	if r1 == 0 {
		return fmt.Sprintf("未知自定义格式 (%d)", format)
	}
	return "自定义格式: " + syscall.UTF16ToString(buf)
}

func init() {
	if err := shell32.Load(); err != nil {
		panic("无法加载shell32.dll: " + err.Error())
	}
}

func main() {
	className := syscall.StringToUTF16Ptr("ClipboardMonitorClass")

	var wc WNDCLASSEX
	wc.Size = uint32(unsafe.Sizeof(wc))
	wc.WndProc = syscall.NewCallback(windowProc)
	wc.Instance = 0
	wc.ClassName = className
	wc.Style = 0x0002 // CS_HREDRAW

	ret, _, err := procRegisterClassEx.Call(uintptr(unsafe.Pointer(&wc)))
	if ret == 0 {
		panic("注册窗口类失败: " + err.Error())
	}

	hwnd, _, err := procCreateWindowEx.Call(
		0,
		uintptr(unsafe.Pointer(className)),
		uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr("剪切板监视器"))),
		0,
		0, 0, 100, 100,
		0, 0, 0, 0,
	)
	if hwnd == 0 {
		panic("创建窗口失败: " + err.Error())
	}

	procShowWindow.Call(hwnd, 0) // 隐藏窗口

	fmt.Println("🚀 剪贴板监视器已启动。现在可以复制内容进行测试!")

	var msg MSG
	for {
		ret, _, _ := procGetMessage.Call(uintptr(unsafe.Pointer(&msg)), 0, 0, 0)
		if ret <= 0 {
			break
		}
		procTranslateMessage.Call(uintptr(unsafe.Pointer(&msg)))
		procDispatchMessage.Call(uintptr(unsafe.Pointer(&msg)))
	}
}

如何运行

安装依赖

go mod init clipboard-monitor
go get golang.org/x/sys/windows
go get golang.org/x/text/encoding/simplifiedchinese

运行程序

保存为 main.go,然后:

go run main.go

测试效果

复制一段文字、一张图片、或几个文件试试! 你会看到类似这样的输出:

🚀 剪贴板监视器已启动。现在可以复制内容进行测试!
📝 文本内容:
Hello, 剪贴板!
📎 拖放的文件 (2):
 - C:\Users\Alice\Pictures\cat.jpg
 - C:\Users\Alice\Documents\report.pdf
📎 CF_BITMAP (位图)

安全提醒

  • 本程序仅用于学习和本地调试。
  • 实际产品中应避免滥用剪贴板监控(涉及隐私!)。
  • 在企业环境中,此类行为可能违反安全策略。

结语

通过几十行 Go 代码,我们撬动了 Windows 底层的消息系统,实现了对剪贴板的"温柔窥探"。这不仅是一次技术实践,更是一次穿越回 Win32 时代的浪漫冒险。

下次当你复制密码时,记得看看控制台——说不定你的程序正在偷笑 。

以上就是基于Go编写一个Windows剪贴板监控器的详细内容,更多关于Go Windows剪贴板监控器的资料请关注脚本之家其它相关文章!

相关文章

  • Golang中函数的使用方法详解

    Golang中函数的使用方法详解

    这篇文章主要详细介绍了Golang中函数的使用方法,文中有详细的示例代码,对大家的学习或工作有一定的帮助,需要的朋友可以参考下
    2023-05-05
  • Go语言使用Cobra实现强大命令行应用

    Go语言使用Cobra实现强大命令行应用

    Cobra是一个强大的开源工具,能够帮助我们快速构建出优雅且功能丰富的命令行应用,本文为大家介绍了如何使用Cobra打造强大命令行应用,感兴趣的小伙伴可以了解一下
    2023-07-07
  • golang的Pseudo-versions使用问题解析

    golang的Pseudo-versions使用问题解析

    这篇文章主要为大家介绍有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪了golang的Pseudo-versions使用问题解析,
    2023-07-07
  • Go中的代码换行问题

    Go中的代码换行问题

    这篇文章主要介绍了Go中的代码换行问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-12-12
  • Go实现将任何网页转化为PDF

    Go实现将任何网页转化为PDF

    在许多应用场景中,可能需要将网页内容转化为 PDF 格式,使用Go编程语言,结合一些现有的库,可以非常方便地实现这一功能,下面我们就来看看具体实现方法吧
    2024-11-11
  • Golang标准库和外部库的性能比较

    Golang标准库和外部库的性能比较

    这篇文章主要介绍Golang标准库和外部库的性能比较,下面文章讲围绕这两点展开内容,感兴趣的小伙伴可以参考一下
    2021-10-10
  • Go 语言中关于接口的三个

    Go 语言中关于接口的三个

    这篇文章主要介绍了Go 语言中关于接口的三个"潜规则",本文通过实例代码相结合给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-06-06
  • Golang处理gRPC请求/响应元数据的示例代码

    Golang处理gRPC请求/响应元数据的示例代码

    前段时间实现内部gRPC框架时,为了实现在服务端拦截器中打印请求及响应的头部信息,便查阅了部分关于元数据的资料,因为中文网络上对于该领域的信息较少,于是在这做了一些简单的总结,需要的朋友可以参考下
    2024-03-03
  • Go语言常用字符串处理方法实例汇总

    Go语言常用字符串处理方法实例汇总

    这篇文章主要介绍了Go语言常用字符串处理方法,实例汇总了Go语言中常见的各种字符串处理技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-03-03
  • Golang中的godoc使用简介(推荐)

    Golang中的godoc使用简介(推荐)

    Godoc是go语言的文档化工具,类似于文档化工具godoc,类似于Python的Docstring和Java的Javadoc,这篇文章主要介绍了Golang中的godoc使用简介,需要的朋友可以参考下
    2022-10-10

最新评论