golang圆阵列图记:天灵灵地灵灵图标排圆形
夫 Windows 之桌面,图标林立,如市井杂陈,无序可言。或左或右,或上或下,杂然纷呈,观之令人心烦。余尝思:何不使诸图标环列如星,布阵若圆,以彰秩序之美乎?遂作此《圆阵列图记》,以志其事。
一、缘起
昔者,余偶见一友,其桌面图标排列如北斗七星,熠熠生辉,问其故,答曰:"吾以程序御之,使图标绕圆而居。"余闻之大奇,遂求其术。友笑曰:"此非仙术,乃 Windows API 之妙用也。"余归而研之,终得其法,遂著此文,以飨同好。
二、其理何在?
Windows 桌面之图标,实乃"SysListView32"控件所载,藏于"SHELLDLL_DefView"之中,而此窗又匿于"Progman"或"WorkerW"之内。欲驭图标,必先寻其巢穴,如捕龙须先知其穴。
吾程序首务,乃遍历窗口,寻得此"列表视图"之句柄(HWND),而后方可号令图标。其法有三:
- 寻Progman:此乃桌面之祖窗,古称"程序管理器"。
- 探WorkerW:新世Windows,图标多藏于WorkerW子窗之中,须遍历而得。
- 定SysListView32:图标之真身,藏于此列表控件内,如鱼潜渊。
得其句柄,便可调用 LVM_SETITEMPOSITION 之令,指定某图标于某坐标。此即"点兵布阵"之术也。
三、圆阵之法
既得图标之数,复知屏幕之广(GetSystemMetrics 可得),便可布圆阵矣。
设屏幕宽高为 W、H,则圆心居中:
X₀ = W/2,Y₀ = H/2
图标 N 枚,均布于半径 R 之圆周,则第 i 图标之位为:
X = X₀ + R·cos(2πi/N)
Y = Y₀ + R·sin(2πi/N)
为免图标贴边如"临崖勒马",更设边距四十像素,使诸图标进退有度,不至"坠入虚空"。
四、关键一诀:禁"自动排列"
然有一大忌,不可不察!
Windows 有"自动排列图标"之癖,若此癖未除,则吾所设之位,顷刻被系统抹去,如沙上书字,潮至即没。故必先入注册表,改 AutoArrange 为 0,方可成事。
路径:HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced
吾程序虽可自动禁之,然有时系统顽固,需手动右键桌面 → 查看 → 取消"自动排列图标",方保万全。此乃成败之枢机,切记!切记!
五、刷新与重启
布阵既毕,需告之系统:"吾已更易阵型,速速显之!"
可用 WM_SETTINGCHANGE 之信,或干脆重启 explorer.exe,如更衣换甲,焕然一新。
然重启资源管理器,风险稍高,或致桌面暂隐。故吾程序今但发刷新之令,辅以提示:"君可按 F5 自行刷新",更为稳妥。
六、源码
package main
import (
"fmt"
"math"
"os/exec"
"runtime"
"strconv"
"strings"
"syscall"
"time"
"unsafe"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/registry"
)
// Windows API 常量
const (
// 屏幕分辨率获取常量
SM_CXSCREEN = 0
SM_CYSCREEN = 1
// 桌面窗口类名
DESKTOP_CLASS_NAME = "Progman"
// 图标窗口类名
SHELL_DEF_VIEW = "SHELLDLL_DefView"
// 列表视图常量
LVM_GETITEMCOUNT = 0x1004
LVM_SETITEMPOSITION = 0x100F
// 通知常量
SHCNE_ASSOCCHANGED = 0x08000000
SHCNF_IDLIST = 0x0000
WM_SETTINGCHANGE = 0x001A
)
// Windows API 声明
var (
user32 = windows.NewLazySystemDLL("user32.dll")
procGetSystemMetrics = user32.NewProc("GetSystemMetrics")
procFindWindowW = user32.NewProc("FindWindowW")
procFindWindowExW = user32.NewProc("FindWindowExW")
procSendMessageW = user32.NewProc("SendMessageW")
procEnumChildWindows = user32.NewProc("EnumChildWindows")
shell32 = windows.NewLazySystemDLL("shell32.dll")
procSHChangeNotify = shell32.NewProc("SHChangeNotify")
)
// 回调函数类型
type EnumWindowsProc func(hwnd HWND, lParam uintptr) uintptr
// 确保我们使用syscall类型与Windows API兼容
type HWND = uintptr
type WPARAM = uintptr
type LPARAM = uintptr
type POINT struct {
X int32
Y int32
}
type RECT struct {
Left int32
Top int32
Right int32
Bottom int32
}
// 查找桌面列表视图窗口
func findDesktopListView() HWND {
// 查找 Progman 窗口
progman, _, _ := procFindWindowW.Call(
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(DESKTOP_CLASS_NAME))),
0,
)
if progman == 0 {
fmt.Println("无法找到桌面窗口 (Progman)")
return 0
}
// 查找 SHELLDLL_DefView 窗口
shelldllDefView, _, _ := procFindWindowExW.Call(
progman,
0,
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(SHELL_DEF_VIEW))),
0,
)
if shelldllDefView == 0 {
// 在某些Windows版本上,我们可能需要特殊处理
// 向Progman发送消息以获取WorkerW窗口链
user32.NewProc("SendMessageTimeoutW").Call(
progman,
0x052C, // WM_USER + 0x52C
0,
0,
0,
500,
0,
)
// 尝试查找WorkerW窗口
var workerw HWND
workerw = 0
procFindWindowExW.Call(
0,
0,
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr("WorkerW"))),
0,
)
// 遍历所有WorkerW窗口,找到包含SHELLDLL_DefView的那个
for workerw != 0 {
temp, _, _ := procFindWindowExW.Call(
workerw,
0,
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(SHELL_DEF_VIEW))),
0,
)
if temp != 0 {
shelldllDefView = temp
break
}
workerw, _, _ = procFindWindowExW.Call(
0,
workerw,
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr("WorkerW"))),
0,
)
}
}
if shelldllDefView == 0 {
fmt.Println("无法找到SHELLDLL_DefView窗口")
return 0
}
// 查找列表视图窗口
listView, _, _ := procFindWindowExW.Call(
shelldllDefView,
0,
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr("SysListView32"))),
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr("FolderView"))),
)
if listView == 0 {
fmt.Println("无法找到桌面列表视图窗口")
return 0
}
return listView
}
// 获取桌面图标数量
func getDesktopIconCount(listView HWND) int {
count, _, _ := procSendMessageW.Call(
uintptr(listView),
LVM_GETITEMCOUNT,
0,
0,
)
return int(count)
}
// 设置图标位置
func setIconPosition(listView HWND, index int, x, y int32) bool {
result, _, _ := procSendMessageW.Call(
uintptr(listView),
LVM_SETITEMPOSITION,
uintptr(index),
uintptr((int32(y)<<16)|int32(x)),
)
return result != 0
}
// 获取屏幕分辨率
func getScreenResolution() (int, int) {
width, _, _ := procGetSystemMetrics.Call(SM_CXSCREEN)
height, _, _ := procGetSystemMetrics.Call(SM_CYSCREEN)
return int(width), int(height)
}
// 通知系统刷新桌面
func refreshDesktop() {
fmt.Println("正在刷新桌面...")
// 简单的刷新方式,避免复杂的API调用导致卡住
// 直接发送WM_SETTINGCHANGE消息给Program Manager
hwnd, _, _ := procFindWindowW.Call(
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr("Progman"))),
0,
)
if hwnd != 0 {
procSendMessageW.Call(
hwnd,
WM_SETTINGCHANGE,
0,
0,
)
fmt.Println("已发送刷新信号到桌面窗口")
} else {
fmt.Println("桌面窗口未找到,但图标位置已设置")
}
}
// 重启资源管理器
func restartExplorer() {
fmt.Println("正在重启Windows资源管理器以应用更改...")
// 终止资源管理器
fmt.Println(" 终止资源管理器进程...")
cmdKill := exec.Command("taskkill", "/F", "/IM", "explorer.exe")
cmdKill.SysProcAttr = &syscall.SysProcAttr{
HideWindow: true,
}
if err := cmdKill.Run(); err != nil {
fmt.Printf(" 警告: 终止资源管理器时出错: %v\n", err)
}
// 等待资源管理器完全关闭
time.Sleep(2 * time.Second)
// 重启资源管理器
fmt.Println(" 重启资源管理器进程...")
cmdStart := exec.Command("explorer.exe")
cmdStart.SysProcAttr = &syscall.SysProcAttr{
HideWindow: false,
}
if err := cmdStart.Start(); err != nil {
fmt.Printf(" 错误: 重启资源管理器时出错: %v\n", err)
} else {
fmt.Println(" 资源管理器已重启")
}
// 等待资源管理器完全启动
time.Sleep(3 * time.Second)
}
// 最小最大值函数
func min(a, b int) int {
if a < b {
return a
}
return b
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
// 禁用桌面自动排列图标
func disableAutoArrange() bool {
fmt.Println("正在检查并禁用桌面自动排列图标...")
// 注册表路径
keyPath := `Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced`
// 打开注册表键
k, err := registry.OpenKey(registry.CURRENT_USER, keyPath, registry.QUERY_VALUE|registry.SET_VALUE)
if err != nil {
fmt.Printf("警告: 无法打开注册表项: %v\n", err)
return false
}
defer k.Close()
// 检查当前值
currentValue, _, err := k.GetIntegerValue("AutoArrange")
if err != nil && err != registry.ErrNotExist {
fmt.Printf("警告: 读取注册表值时出错: %v\n", err)
} else if currentValue == 0 {
fmt.Println("自动排列图标已经是禁用状态")
return true
}
// 设置为0(禁用)
err = k.SetDWordValue("AutoArrange", 0)
if err != nil {
fmt.Printf("警告: 无法修改自动排列设置: %v\n", err)
return false
}
fmt.Println("已成功禁用自动排列图标")
return true
}
// 圆形排列图标
func arrangeIconsInCircle(listView HWND, iconCount int, screenW, screenH int, radius float64) {
// 计算圆心
centerX := screenW / 2
centerY := screenH / 2
fmt.Printf("开始圆形排列 %d 个图标\n", iconCount)
fmt.Printf("圆心: (%d, %d), 半径: %.0f 像素\n\n", centerX, centerY, radius)
// 为每个图标计算位置
for i := 0; i < iconCount; i++ {
// 计算角度 (弧度)
angle := 2 * math.Pi * float64(i) / float64(iconCount)
// 计算圆形位置
x := centerX + int(radius*math.Cos(angle))
y := centerY + int(radius*math.Sin(angle))
// 确保图标不会超出屏幕边界 (留出边距)
margin := 40
x = max(margin, min(x, screenW-margin))
y = max(margin, min(y, screenH-margin))
// 设置图标位置
success := setIconPosition(listView, i, int32(x), int32(y))
// 输出进度信息
if i < 5 || i == iconCount-1 {
status := "✓"
if !success {
status = "✗"
}
fmt.Printf("图标 %d: (%d, %d) %s\n", i+1, x, y, status)
} else if i == 5 {
fmt.Println("...")
}
}
fmt.Println("\n图标位置设置完成")
}
// 主函数
func main() {
fmt.Println("===== Windows桌面图标圆形排列工具 =====")
fmt.Println("使用Windows API直接操作桌面图标位置\n")
// 检查是否在Windows上运行
if runtime.GOOS != "windows" {
fmt.Println("此程序仅支持Windows操作系统")
fmt.Println("按Enter键退出...")
fmt.Scanln()
return
}
// 找到桌面列表视图窗口
fmt.Println("正在查找桌面窗口...")
listView := findDesktopListView()
if listView == 0 {
fmt.Println("错误: 无法访问桌面窗口")
fmt.Println("请确保您有管理员权限")
fmt.Println("按Enter键退出...")
fmt.Scanln()
return
}
fmt.Println("找到桌面列表视图窗口")
// 禁用自动排列图标
disableAutoArrange()
// 获取实际的桌面图标数量
iconCount := getDesktopIconCount(listView)
fmt.Printf("检测到 %d 个桌面图标\n\n", iconCount)
// 让用户确认或修改图标数量
fmt.Printf("请输入要排列的图标数量 (默认: %d, 按Enter使用默认值): ", iconCount)
var input string
fmt.Scanln(&input)
input = strings.TrimSpace(input)
if input != "" {
customCount, err := strconv.Atoi(input)
if err == nil && customCount > 0 && customCount <= iconCount {
fmt.Printf("使用您输入的图标数量: %d\n", customCount)
iconCount = customCount
} else {
fmt.Printf("输入无效,继续使用检测到的图标数量: %d\n", iconCount)
}
}
// 获取屏幕分辨率
screenW, screenH := getScreenResolution()
if screenW <= 0 || screenH <= 0 {
fmt.Println("错误: 无法获取屏幕分辨率")
fmt.Println("按Enter键退出...")
fmt.Scanln()
return
}
fmt.Printf("屏幕分辨率: %dx%d\n\n", screenW, screenH)
// 建议的默认半径 (屏幕较小边的30%)
defaultRadius := float64(min(screenW, screenH)) * 0.3
// 获取用户指定的半径
fmt.Printf("请输入圆的半径 (默认: %.0f 像素): ", defaultRadius)
fmt.Scanln(&input)
input = strings.TrimSpace(input)
radius := defaultRadius
if input != "" {
customRadius, err := strconv.ParseFloat(input, 64)
if err == nil && customRadius > 0 {
// 限制最大半径
maxRadius := float64(min(screenW, screenH)) * 0.4
if customRadius > maxRadius {
fmt.Printf("半径过大,已调整为最大允许值: %.0f 像素\n", maxRadius)
radius = maxRadius
} else {
radius = customRadius
}
} else {
fmt.Printf("输入无效,使用默认半径: %.0f 像素\n", defaultRadius)
}
}
// 执行圆形排列
arrangeIconsInCircle(listView, iconCount, screenW, screenH, radius)
// 刷新桌面
fmt.Println("\n刷新桌面...")
refreshDesktop()
// 不再询问重启资源管理器,避免可能的卡住问题
fmt.Println("\n💡 提示: 图标位置已成功设置")
fmt.Println(" 如果需要,可以手动按F5刷新桌面以确保更改可见")
fmt.Println("\n🎉 桌面图标圆形排列完成!")
fmt.Println("📋 提示:")
fmt.Println(" 1. 如果图标位置未正确更新,请按F5刷新桌面")
fmt.Println(" 2. 或者手动重启Windows资源管理器")
fmt.Println(" 3. 如有需要,请以管理员权限运行此程序")
fmt.Println("\n按Enter键退出...")
fmt.Scanln()
}
七、结语
此术虽小,然融汇窗口枚举、消息发送、注册表操作、三角函数布阵于一体,可谓"麻雀虽小,五脏俱全"。非为炫技,实乃悦目养心耳。
若君试之,见图标环列如日月绕辰,必莞尔曰:"此乐何极!"
注:运行此程序,宜以管理员身份,否则或被系统拒之门外,如叩关不得入,徒叹奈何。
庚子年秋月 于虚拟案前
往期部分文章列表
- 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 实现
- 用 Go 语言实现《周易》大衍筮法起卦程序
- Go 语言400行代码实现 INI 配置文件解析器:支持注释、转义与类型推断
- 高性能 Go 语言带 TTL 的内存缓存实现:精确过期、自动刷新、并发安全
- Golang + OpenSSL 实现 TLS 安全通信:从私有 CA 到动态证书加载