实现一个桌面管理工具
最近换了Windows后,开始怀念起曾经用过的壁纸管理软件。大名鼎鼎的fences价格一百多,臭名昭著的小鸟壁纸倒是免费,但我可不敢把这位菩萨请到我电脑里来了。一时间,竟找不到合格的替代品,唯一看着有希望的Portals也是收费的,免费只能创建两个文件夹。所以我开始思考,一个这样的软件是如何实现的,我能否实现个简单的?

最近换了Windows后,开始怀念起曾经用过的桌面管理软件。大名鼎鼎的fences价格一百多,臭名昭著的小鸟壁纸倒是免费,但我可不敢把这位菩萨请到我电脑里来了。

一时间,竟找不到合格的替代品,唯一看着有希望的Portals也是收费的,免费只能创建两个文件夹。所以我开始思考,一个这样的软件是如何实现的,我能否实现个简单的?

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