golang圆阵列图记:天灵灵地灵灵图标排圆形

21 阅读10分钟

夫 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()
}

七、结语

此术虽小,然融汇窗口枚举、消息发送、注册表操作、三角函数布阵于一体,可谓"麻雀虽小,五脏俱全"。非为炫技,实乃悦目养心耳。

若君试之,见图标环列如日月绕辰,必莞尔曰:"此乐何极!"

注:运行此程序,宜以管理员身份,否则或被系统拒之门外,如叩关不得入,徒叹奈何。

庚子年秋月 于虚拟案前

往期部分文章列表