当前位置: 首页 > news >正文

剪贴板监控记:用 Go 写一个 Windows 剪贴板监控器

“复制即监控,粘贴即暴露。”

—— 一位不愿透露姓名的剪贴板监视器开发者

📑 目录

  • 🧠 为什么要做这个?
  • 🛠️ 技术原理:Windows 剪贴板监听机制
  • ⚠️ 注意事项
  • 🧩 核心挑战:剪贴板数据格式
  • 🧪 代码实现:Go + Windows API
  • ▶️ 如何运行?
  • 🔒 安全提醒
  • 🎉 结语

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

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

🧠 为什么要做这个?

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

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

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

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

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

⚠️ 注意事项

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

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

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

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

所以我们的程序必须:

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

🧪 代码实现:Go + Windows API

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

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

package mainimport ("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        = 0x0001WM_DESTROY       = 0x0002WM_DRAWCLIPBOARD = 0x0308WM_CHANGECBCHAIN = 0x030DCF_TEXT          = 1CF_BITMAP        = 2CF_UNICODETEXT   = 13CF_HDROP         = 15
)// ===== Win32 Structures =====
type WNDCLASSEX struct {Size       uint32Style      uint32WndProc    uintptrClsExtra   int32WndExtra   int32Instance   syscall.HandleIcon       syscall.HandleCursor     syscall.HandleBackground uintptrMenuName   *uint16ClassName  *uint16IconSm     syscall.Handle
}type MSG struct {HWnd    uintptrMessage uint32WParam  uintptrLParam  uintptrDwTime  uint32PtX     int32PtY     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 = 0var finalText stringfor {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 = textgoto 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 intfor {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 intfor {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 WNDCLASSEXwc.Size = uint32(unsafe.Sizeof(wc))wc.WndProc = syscall.NewCallback(windowProc)wc.Instance = 0wc.ClassName = classNamewc.Style = 0x0002 // CS_HREDRAWret, _, 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 MSGfor {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 的 defer:你的“善后管家“,别让他变成“背锅侠“!
  • 你知道程序怎样优雅退出吗?—— Go 开发中的"体面告别"全指南
  • 用golang解救PDF文件中的图片只要200行代码!
  • 200KB 的烦恼,Go 语言 20 分钟搞定!—— 一个程序员的图片压缩自救指南
  • 从"CPU 烧开水"到优雅暂停:Go 里 sync.Cond 的正确打开方式
  • 时移世易,篡改天机:吾以 Go 语令 Windows 文件"返老还童"记
  • golang圆阵列图记:天灵灵地灵灵图标排圆形
  • golang解图记
  • 从 4.8 秒到 0.25 秒:我是如何把 Go 正则匹配提速 19 倍的?
  • 用 Go 手搓一个内网 DNS 服务器:从此告别 IP 地址,用域名畅游家庭网络!
  • 我用Go写了个华容道游戏,曹操终于不用再求关羽了!
  • 用 Go 接口把 Excel 变成数据库:一个疯狂但可行的想法
  • 穿墙术大揭秘:用 Go 手搓一个"内网穿透"神器!
  • 布隆过滤器(go):一个可能犯错但从不撒谎的内存大师
  • 自由通讯的魔法:Go从零实现UDP/P2P 聊天工具
  • Go语言实现的简易远程传屏工具:让你的屏幕「飞」起来
  • 当你的程序学会了"诈尸":Go 实现 Windows 进程守护术
  • 验证码识别API:告别收费接口,迎接免费午餐
  • 用 Go 给 Windows 装个"顺风耳":两分钟写个录音小工具
  • 无奈!我用go写了个MySQL服务
  • 使用 Go + govcl 实现 Windows 资源管理器快捷方式管理器
  • 用 Go 手搓一个 NTP 服务:从"时间混乱"到"精准同步"的奇幻之旅
  • 用 Go 手搓一个 Java 构建工具:当 IDE 不在身边时的自救指南
  • 深入理解 Windows 全局键盘钩子(Hook):拦截 Win 键的 Go 实现
http://www.dtcms.com/a/574964.html

相关文章:

  • 建设网站套餐ui平面设计
  • anchor-based与anchor-free对比
  • 8B/10B编码技术深度解析
  • 给六人游做网站开发的企业营销网站建设的基本步骤
  • 网站设计的发展趋势义乌网站设计制作价格
  • 国外网站建设模板河南省水利建设管理处网站
  • 开封河南网站建设以下哪个不属于网络营销的特点
  • 数据挖掘:python招聘数据分析预测系统 招聘数据平台 +爬虫+可视化 +django框架+vue框架 大数据技术✅
  • 天津河西做网站公司学校网站做网页飘窗怎么做
  • 【Algorithm】Day-11
  • dw做的网站有什么缺陷四川平台网站建设哪里有
  • 张琦加盟 2025 创始人 IP+AI 万人大会:AI 时代,IP 破局增长的实战方法都在这
  • 南京建设网站的公司dw网页制作成品12页
  • 手机轻松控制电脑:局域网内远程操控B站/抖音实战教程
  • 做网站要求高吗最新wordpress模板
  • 企业网站现状wordpress 安全狗
  • Spring Cloud 总览:微服务的生态基石
  • 网站制作最新技术的新媒体网站建设十大的经典成功案例
  • 20251105在荣品RD-RK3588-MID开发板的Android13系统的导航栏左右两边增加音量+-按钮
  • 湘潭做网站 要到磐石网络电商平台怎么加入
  • wordpress单页导出广东企业网站seo哪里好
  • 鲜花网站素材网站建设与管理结课论文
  • 线性表之链表的介绍和使用
  • 企业网站管理系统螺栓球网架
  • 做的网站在百度找不到wordpress更换皮肤
  • 网络传输协议的介绍——SSE
  • 河南省建设厅网站 吴浩浙江省建设执业注册中心网站
  • 桂林网站客户管理系统免费
  • 基于ssm的实验室耗材管理系统
  • wordpress做分类信息网站东莞市住建局官网