最近换了Windows后,开始怀念起曾经用过的桌面管理软件。大名鼎鼎的fences价格一百多,臭名昭著的小鸟壁纸倒是免费,但我可不敢把这位菩萨请到我电脑里来了。
一时间,竟找不到合格的替代品,唯一看着有希望的Portals也是收费的,免费只能创建两个文件夹。所以我开始思考,一个这样的软件是如何实现的,我能否实现个简单的?
实现路径
图标显示
一个桌面管理工具当然要能显示文件图标。经过简单的搜索知道在java中获取一个文件的图标可以用这个方法:
val fileIcon = FileSystemView.getFileSystemView().getSystemIcon(iconFile, width.value.toInt(), height.value.toInt())
val img = BufferedImage(fileIcon.iconWidth, fileIcon.iconHeight, BufferedImage.TYPE_INT_ARGB)
val g2d = img.createGraphics()
fileIcon.paintIcon(null, g2d, 0, 0)
要在Compose中画出来,只要转换为Compose的Painter即可,即 img.toPainter()
。
然而实测发现,大量的图标,主要是Steam创建的游戏快捷方式,并没有正确显示。原因是这些“快捷方式”并不是普通的快捷方式,而是url后缀的文件。显然我们的工具没有支持这种文件的图标获取。一个典型的url文件如下:
[{000214A0-0000-0000-C000-000000000046}]
Prop3=19,0
[InternetShortcut]
IDList=
IconIndex=0
URL=steam://rungameid/271590
IconFile=C:\Program Files (x86)\Steam\steam\games\06b52e09e284542dd99eea45f9f85f68440dbcaf.ico
所以,只要读出IconFile字段就可以正确识别了。
然而,经测试还是有一些图标无法正确加载,所以我将系统原生API获取图标方式作为兜底。
val hInstance = Kernel32.INSTANCE.GetModuleHandle(null)
val indexMemory = Memory(Long.SIZE_BITS.toLong())
val icon = Shell32Extend.instance.ExtractAssociatedIcon(hInstance, iconFile.absolutePath, indexMemory)
这里得到的是一个HICON,一个图标的句柄,还是没办法直接在Compose中显示。下面这段来自StackOverflow的代码可以转换为Bitmap。
private fun toImage(hicon: HICON?): BufferedImage? {
var bitmapHandle: HBITMAP? = null
val user32 = User32Extend.instance
val gdi32 = GDI32.INSTANCE
try {
val info = ICONINFO()
if (!user32.GetIconInfo(hicon, info)) return null
info.read()
bitmapHandle = Optional.ofNullable(info.hbmColor).orElse(info.hbmMask)
val bitmap = BITMAP()
if (gdi32.GetObject(bitmapHandle, bitmap.size(), bitmap.pointer) > 0) {
bitmap.read()
val width = bitmap.bmWidth.toInt()
val height = bitmap.bmHeight.toInt()
val deviceContext = user32.GetDC(null)
val bitmapInfo = BITMAPINFO()
bitmapInfo.bmiHeader.biSize = bitmapInfo.bmiHeader.size()
require(
gdi32.GetDIBits(
deviceContext, bitmapHandle, 0, 0, Pointer.NULL, bitmapInfo,
DIB_RGB_COLORS
) != 0
) { "GetDIBits should not return 0" }
bitmapInfo.read()
val pixels = Memory(bitmapInfo.bmiHeader.biSizeImage.toLong())
bitmapInfo.bmiHeader.biCompression = BI_RGB
bitmapInfo.bmiHeader.biHeight = -height
require(
gdi32.GetDIBits(
deviceContext, bitmapHandle, 0, bitmapInfo.bmiHeader.biHeight, pixels, bitmapInfo,
DIB_RGB_COLORS
) != 0
) { "GetDIBits should not return 0" }
val colorArray = pixels.getIntArray(0, width * height)
val image = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB)
image.setRGB(0, 0, width, height, colorArray, 0, width)
return image
}
} finally {
gdi32.DeleteObject(hicon)
Optional.ofNullable(bitmapHandle).ifPresent { hObject: HBITMAP? ->
gdi32.DeleteObject(
hObject
)
}
}
return null
}
窗口嵌入桌面
为了在显示桌面时保持我们的窗口显示,最佳办法就是嵌入到桌面中,把桌面窗口设置为我们窗口的父窗口。
private fun insertWindowToDesktop(childHwnd: HWND) {
// find defView in Program
val program: HWND? = User32.INSTANCE.FindWindowEx(HWND(Pointer.NULL), HWND(Pointer.NULL), "Progman", null)
var defView: HWND? = User32.INSTANCE.FindWindowEx(program, HWND(Pointer.NULL), "SHELLDLL_DefView", null)
var container = program.takeIf { program?.pointer != null && defView?.pointer != null }
if (container == null) {
// find defView in WorkerW
val desktopHwnd = User32.INSTANCE.GetDesktopWindow()
var workerW: HWND? = HWND(Pointer.NULL)
do {
workerW = User32.INSTANCE.FindWindowEx(desktopHwnd, workerW, "WorkerW", null)
defView = User32.INSTANCE.FindWindowEx(workerW, HWND(Pointer.NULL), "SHELLDLL_DefView", null);
} while (defView?.pointer == Pointer.NULL && workerW?.pointer != Pointer.NULL)
container = workerW
}
User32.INSTANCE.SetParent(childHwnd, container)
}
文件系统监听
这个比较简单,使用 org.apache.commons.io.monitor.FileAlterationMonitor#FileAlterationMonitor(long, org.apache.commons.io.monitor.FileAlterationObserver...)
就可以了。其原理是启动一个线程定时去检查文件的时间戳。
最终效果
目前虽然还有一些效果没实现,比如和系统资源管理器的双向拖放,目前能算初步可用吧,我是开始用上了。代码开源在Github上。
最后修改于 2024-08-15