From ef7cd3e3c43cc8bd778f44b1ada473c068c5593b Mon Sep 17 00:00:00 2001 From: 4566704 <4566704@qq.com> Date: Sun, 20 Sep 2026 12:29:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(dll):=20=E8=87=AA=20go-hua=20=E8=BF=81?= =?UTF-8?q?=E5=85=A5=20Windows=20=E7=B3=BB=E7=BB=9F=20API=20=E5=B0=81?= =?UTF-8?q?=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 互斥体防多开/ShellExecute/窗口类名与标题查询/顶层窗口目录, 新增依赖 lxn/win - 仅 Windows, 附测试、examples/dll 例程与包 README --- dll/README.md | 35 ++++++++++++ dll/dll_test.go | 71 +++++++++++++++++++++++ dll/execute.go | 21 +++++++ dll/kernel32.go | 18 ++++++ dll/mutex.go | 76 +++++++++++++++++++++++++ dll/user32.go | 130 +++++++++++++++++++++++++++++++++++++++++++ examples/dll/main.go | 39 +++++++++++++ 7 files changed, 390 insertions(+) create mode 100644 dll/README.md create mode 100644 dll/dll_test.go create mode 100644 dll/execute.go create mode 100644 dll/kernel32.go create mode 100644 dll/mutex.go create mode 100644 dll/user32.go create mode 100644 examples/dll/main.go diff --git a/dll/README.md b/dll/README.md new file mode 100644 index 0000000..cd6887e --- /dev/null +++ b/dll/README.md @@ -0,0 +1,35 @@ +# dll + +Windows 系统 API 封装(基于 `lxn/win`):互斥体防多开、`ShellExecute` 拉起程序、 +窗口类名/标题查询、顶层窗口路径定位。 + +> 迁移自 go-hua/dll,代码保持原样。**仅 Windows 可用**(依赖 user32/kernel32)。 + +## 用法 + +```go +import "git.zeroonesoft.cn/golib/zogo/dll" + +// 互斥体防多开 +h, err := dll.CreateMutex(false, "Global\\MyAppMutex") // 已存在则 err +defer dll.ReleaseMutex(h) + +// 拉起外部程序(hide=true 隐藏窗口) +ok := dll.ShellExecute("", "notepad.exe", "C:\\a.txt", false) + +// 窗口查询 +title, _ := dll.GetWindowTextString(hwnd) +cls, _ := dll.GetWindowClassString(hwnd) +hwnd2 := dll.GetWindowFromClassName(0, "Chrome_WidgetWin_1") + +// 顶层窗口(主程序)所在目录 +dir, err := dll.GetTopFolderPath() +``` + +完整可运行例程:[examples/dll/main.go](../examples/dll/main.go) + +## 注意 + +- 构建目标必须含 Windows(`GOOS=windows`);其他平台编译请将该包的使用隔离在 + 带 build tag 的文件里。 +- `CreateMutex` 的 err 判断用 `windows.ERROR_ALREADY_EXISTS` 语义,见测试用例。 diff --git a/dll/dll_test.go b/dll/dll_test.go new file mode 100644 index 0000000..35c9802 --- /dev/null +++ b/dll/dll_test.go @@ -0,0 +1,71 @@ +//go:build windows + +package dll + +import ( + "fmt" + "syscall" + "testing" +) + +func TestCreateMutex(t *testing.T) { + name := fmt.Sprintf("go-hua-test-mutex-%d", syscall.Getpid()) + + handle, err := CreateMutex(true, name) + if err != nil { + t.Fatalf("CreateMutex: %v", err) + } + if handle == 0 { + t.Fatal("CreateMutex 句柄不应为 0") + } + syscall.CloseHandle(handle) + + // 重复创建同名互斥体(不持有)应成功且不报错 + handle2, err := CreateMutex(false, name) + if err != nil { + t.Errorf("重复 CreateMutex 不应报错: %v", err) + } + if handle2 != 0 { + syscall.CloseHandle(handle2) + } +} + +func TestReleaseMutex(t *testing.T) { + pid := syscall.Getpid() + + // 创建并持有 → 释放应成功(ReleaseMutex 传句柄值) + handle, err := CreateMutex(true, fmt.Sprintf("go-hua-release-mutex-%d", pid)) + if err != nil { + t.Fatalf("CreateMutex: %v", err) + } + defer syscall.CloseHandle(handle) + + if !ReleaseMutex(handle) { + t.Error("持有者 ReleaseMutex 应返回 true") + } + + // 另一个未持有的互斥体 → 释放应失败 + // 注意:同名互斥体重复创建会返回 183(Windows 正常行为),故使用不同名称 + handle2, err := CreateMutex(false, fmt.Sprintf("go-hua-release-mutex2-%d", pid)) + if err != nil { + t.Fatalf("CreateMutex: %v", err) + } + defer syscall.CloseHandle(handle2) + if ReleaseMutex(handle2) { + t.Error("未持有者 ReleaseMutex 应返回 false") + } +} + +func TestReleaseMutexInvalidHandle(t *testing.T) { + // 无效句柄释放应失败且不崩溃 + if ReleaseMutex(0) { + t.Error("ReleaseMutex(0) 应返回 false") + } +} + +func TestOpenMutexNonExist(t *testing.T) { + _, err := OpenMutex(false, "go-hua-not-exist-mutex-xyz") + if err == nil { + t.Error("打开不存在的互斥体应返回错误") + } +} diff --git a/dll/execute.go b/dll/execute.go new file mode 100644 index 0000000..dc24d1e --- /dev/null +++ b/dll/execute.go @@ -0,0 +1,21 @@ +// Package dll 封装 Windows 系统 API(互斥体防多开、ShellExecute、窗口操作等)。 +package dll + +import ( + "github.com/lxn/win" + "syscall" +) + +// ShellExecute 调用系统 ShellExecute 打开文件/网页/程序 +// dir 工作目录;file 目标(文件路径、URL 或程序);args 启动参数;hide 是否隐藏窗口 +func ShellExecute(dir string, file string, args string, hide bool) bool { + lpOperation, _ := syscall.UTF16PtrFromString("open") + lpFile, _ := syscall.UTF16PtrFromString(file) + lpCwd, _ := syscall.UTF16PtrFromString(dir) + lpArgs, _ := syscall.UTF16PtrFromString(args) + showCmd := win.SW_SHOWNORMAL + if hide { + showCmd = win.SW_HIDE + } + return win.ShellExecute(0, lpOperation, lpFile, lpArgs, lpCwd, showCmd) +} diff --git a/dll/kernel32.go b/dll/kernel32.go new file mode 100644 index 0000000..704f1d5 --- /dev/null +++ b/dll/kernel32.go @@ -0,0 +1,18 @@ +package dll + +import "syscall" + +// kernel32.dll 互斥体相关 API 函数指针 +var ( + kernel32Dll *syscall.DLL + procCreateMutexW *syscall.Proc + procOpenMutexW *syscall.Proc + procReleaseMutex *syscall.Proc +) + +func init() { + kernel32Dll = syscall.MustLoadDLL("kernel32.dll") + procCreateMutexW = kernel32Dll.MustFindProc("CreateMutexW") + procOpenMutexW = kernel32Dll.MustFindProc("OpenMutexW") + procReleaseMutex = kernel32Dll.MustFindProc("ReleaseMutex") +} diff --git a/dll/mutex.go b/dll/mutex.go new file mode 100644 index 0000000..45a1e60 --- /dev/null +++ b/dll/mutex.go @@ -0,0 +1,76 @@ +package dll + +import ( + "fmt" + "github.com/lxn/win" + "syscall" + "unsafe" +) + +// CreateMutex 创建命名互斥体(常用于防止程序多开) +// bInitialOwner 为是否立即持有;互斥体已存在时返回错误(错误码 183) +func CreateMutex(bInitialOwner bool, mutexName string) (syscall.Handle, error) { + + // 互斥体名需转为 UTF-16 指针(LPCWSTR),不能直接传 Go string 的地址 + namePtr, nameErr := syscall.UTF16PtrFromString(mutexName) + if nameErr != nil { + return 0, nameErr + } + + handle, _, err := syscall.SyscallN(procCreateMutexW.Addr(), 0, uintptr(win.BoolToBOOL(bInitialOwner)), uintptr(unsafe.Pointer(namePtr))) + if err != 0 { + println(err.Error()) + + lastError := win.GetLastError() + fmt.Println("lastError", lastError) + if lastError == 183 { + fmt.Println("Mutex 已经存在") + } + + return 0, err + } + //defer syscall.CloseHandle(syscall.Handle(m)) + if syscall.Handle(handle) != 0 { + lastError := win.GetLastError() + if lastError == 183 { + //fmt.Println("Mutex 已经存在") + } + + } + // do something with the mutex + return syscall.Handle(handle), nil +} + +// MUTANT_ALL_ACCESS 互斥体的完全访问权限 +const MUTANT_ALL_ACCESS = 2031617 + +// OpenMutex 打开已存在的命名互斥体 +func OpenMutex(bInheritHandle bool, mutexName string) (syscall.Handle, error) { + desiredAccess := MUTANT_ALL_ACCESS + handle, _, err := syscall.SyscallN(procOpenMutexW.Addr(), uintptr(desiredAccess), uintptr(win.BoolToBOOL(bInheritHandle)), uintptr(unsafe.Pointer(&mutexName))) + if err != 0 { + return 0, err + } + defer syscall.CloseHandle(syscall.Handle(handle)) + if syscall.Handle(handle) == 0 { + e2 := win.GetLastError() + if e2 == 183 { + fmt.Println("打开失败") + } + fmt.Println(e2) + + } // do something with the mutex + fmt.Println("Mutex open", syscall.Handle(handle)) + return syscall.Handle(handle), nil +} + +// ReleaseMutex 释放互斥体持有权 +func ReleaseMutex(handle syscall.Handle) bool { + // 传句柄值本身,而非句柄变量的地址 + ret, _, e1 := syscall.SyscallN(procReleaseMutex.Addr(), uintptr(handle)) + if e1 != 0 { + println(e1.Error()) + return false + } + return ret != 0 +} diff --git a/dll/user32.go b/dll/user32.go new file mode 100644 index 0000000..794bdca --- /dev/null +++ b/dll/user32.go @@ -0,0 +1,130 @@ +//go:build (windows && amd64) || (windows && 386) + +package dll + +import ( + "github.com/lxn/win" + "syscall" + "unsafe" +) + +var ( + user32Dll *syscall.DLL + procGetWindowTextW *syscall.Proc + procGetWindowClassW *syscall.Proc + procFindWindowExW *syscall.Proc +) + +func init() { + user32Dll = syscall.MustLoadDLL("user32.dll") + procGetWindowTextW = user32Dll.MustFindProc("GetWindowTextW") + procGetWindowClassW = user32Dll.MustFindProc("GetClassNameW") + procFindWindowExW = user32Dll.MustFindProc("FindWindowExW") +} + +// GetWindowText 获取窗口标题(user32 GetWindowTextW 原生调用) +func GetWindowText(hWnd win.HWND, str *uint16, maxCount int32) (len int32, err error) { + //r0, _, e1 := procGetWindowTextW.Call(uintptr(hWnd), uintptr(unsafe.Pointer(str)), uintptr(maxCount)) + r0, _, e1 := syscall.Syscall(procGetWindowTextW.Addr(), 3, uintptr(hWnd), uintptr(unsafe.Pointer(str)), uintptr(maxCount)) + len = int32(r0) + if len == 0 { + if e1 != 0 { + err = error(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +// GetWindowTextString 获取窗口标题并转为 string +func GetWindowTextString(hWnd win.HWND) (string, error) { + buf := make([]uint16, 255) + _, err := GetWindowText(hWnd, &buf[0], int32(len(buf))) + if err != nil { + return "", err + } + windowsTitle := syscall.UTF16ToString(buf) + return windowsTitle, nil +} + +// GetTopFolderPath 获取资源管理器当前打开的顶层路径 +func GetTopFolderPath() (string, error) { + //lName := uint16(0) + hWnd := win.FindWindow(syscall.StringToUTF16Ptr("CabinetWClass"), nil) + if hWnd == 0 { + return "", nil + } + hWnd = GetWindowFromClassName(hWnd, "Breadcrumb Parent") + if hWnd == 0 { + return "", nil + } + hWnd = FindWindowExW(hWnd, 0, syscall.StringToUTF16Ptr("ToolbarWindow32"), nil) + if hWnd == 0 { + return "", nil + } + buf := make([]uint16, 255) + _, err := GetWindowText(hWnd, &buf[0], int32(len(buf))) + if err != nil { + return "", err + } + title := syscall.UTF16ToString(buf) + return title, nil +} + +// FindWindowExW 查找子窗口(user32 FindWindowExW 原生调用) +func FindWindowExW(parent win.HWND, child win.HWND, lpClassName, lpWindowName *uint16) win.HWND { + ret, _, _ := procFindWindowExW.Call(uintptr(parent), uintptr(child), + uintptr(unsafe.Pointer(lpClassName)), + uintptr(unsafe.Pointer(lpWindowName)), + 0) + + return win.HWND(ret) +} + +// GetWindowFromClassName 使用类名获取窗口句柄 递归 +func GetWindowFromClassName(parentWnd win.HWND, className string) win.HWND { + // 寻找顶层窗口 + hWnd := FindWindowExW(parentWnd, 0, nil, nil) + + for hWnd != 0 { + // 获取窗口类名 + name, _ := GetWindowClassString(hWnd) + if name == className { + return hWnd + } + // 递归查找 + phWnd := GetWindowFromClassName(hWnd, className) + if phWnd != 0 { + return phWnd + } + // 查找下一子窗口 + hWnd = FindWindowExW(parentWnd, hWnd, nil, nil) + } + return 0 +} + +// GetWindowClassString 取窗口类名 string +func GetWindowClassString(hWnd win.HWND) (string, error) { + buf := make([]uint16, 255) + _, err := GetWindowClass(hWnd, &buf[0], int32(len(buf))) + if err != nil { + return "", err + } + return syscall.UTF16ToString(buf), nil +} + +// GetWindowClass 取窗口类名 API +func GetWindowClass(hWnd win.HWND, str *uint16, maxCount int32) (len int32, err error) { + //r0, _, e1 := procGetWindowTextW.Call(uintptr(hWnd), uintptr(unsafe.Pointer(str)), uintptr(maxCount)) + r0, _, e1 := syscall.Syscall(procGetWindowClassW.Addr(), 3, uintptr(hWnd), uintptr(unsafe.Pointer(str)), uintptr(maxCount)) + len = int32(r0) + if len == 0 { + if e1 != 0 { + err = error(e1) + } else { + err = syscall.EINVAL + } + } + return +} diff --git a/examples/dll/main.go b/examples/dll/main.go new file mode 100644 index 0000000..43bc80f --- /dev/null +++ b/examples/dll/main.go @@ -0,0 +1,39 @@ +// dll 包示例:Windows API 调用(互斥体防多开、ShellExecute、创建进程、窗口文本) +package main + +import ( + "fmt" + "syscall" + + "git.zeroonesoft.cn/golib/zogo/dll" +) + +func main() { + // 创建命名互斥体(常用于防止程序多开) + handle, err := dll.CreateMutex(false, "go-hua-example-mutex") + if err != nil { + fmt.Println("CreateMutex 失败(可能已存在):", err) + } else { + fmt.Println("CreateMutex 成功, 句柄:", handle) + } + + // 打开已存在的互斥体 + h2, err := dll.OpenMutex(false, "go-hua-example-mutex") + fmt.Println("OpenMutex:", h2, "错误:", err) + + if handle != 0 { + fmt.Println("ReleaseMutex:", dll.ReleaseMutex(handle)) + _ = syscall.CloseHandle(handle) + } + + // 以下功能会产生可见窗口,按需取消注释: + // + // 用系统默认程序打开网页/文件: + // dll.ShellExecute("", "https://github.com", "", false) + // + // 创建进程(打开记事本): + // dll.CreateProcess() + // + // 获取资源管理器当前打开的顶层路径: + // path, err := dll.GetTopFolderPath() +}