• 【原创】随心所欲:自己动手打造XP自动桌面

    2009-11-22

    分类:界面编程

    版权声明:转载时请以超链接形式标明文章原始出处和作者信息及本声明
    http://nokyo.blogbus.com/logs/51950704.html

          最近火爆上市的Windows 7很是华丽精美啊,而且它还自带了一个非常有意思的功能:自动变换桌面。
          这个功能很有意思,但可惜不是每个人现在都能换到Win7的,至少我现在还是不敢用Win7来作为工作机器。
          许多网友都会在闲暇时刻去搜索下载大量的桌面背景图片,总感觉哪个都很好看,都不舍得删除,要是能够有一个程序自动帮我们换桌面多好啊。
          事实上,早有一些小工具能够达到这样的要求了,但作为程序员,这种小工具自然还是自己动手比较好。
          我们的目标是制作一个这样的小工具:可以由用户自己指定一个目录,用户只要把他喜欢的图片放入这个目录中,这些图片便会被轮流采用作为桌面背景。我采用的方法是每次启动系统后自动变换桌面,当然你也可以改成定时变换。

    一、怎么设置桌面图片?
          在进行一切工作之前,我们必须知道怎么通过程序自动设置桌面,网上流传最广的方法是使用SystemParametersInfo函数。
          这个函数的用途很广,使用它来设置桌面图片的方法如下所示:
          SystemParametersInfo(SPI_SETDESKWALLPAPER, 0, "图片路径", 1);
          于是立即动手编写一个程序来进行测试,结果桌面变成了蓝色背景,但并不是我们设置的图片,没有达到要求。
          在调试中发现虽然该函数返回错误,但通过GetLastError()得到的错误号却是0(操作成功完成),如此奇怪的错误!
          利用Google大肆求解后终于发现了一条线索:图片格式。似乎该函数只支持bmp格式,于是把程序简单修改后进行测试,果然如此。
          这样显然是不行的,因为现在jpg和gif等格式的图片如此之多,很多人都不愿意再去使用“臃肿”的bmp格式图片。
          另外,我们直接在资源管理器中右键点击一个jpg格式的图片,选择“设为桌面背景”就可以成功设置桌面,这说明Windows是支持其他格式图片作为背景的。
          其实,使用活动桌面就可以达到我们的要求,不废话了,直接上代码:
    VOID SetDeskTop(char *szFile)
    {
        // 判断文件是否存在
        if (GetFileAttributes(szFile) == -1)
        {
            return;
        }
        
        // 将szFile转换为WCHAR
        int   len = MultiByteToWideChar(CP_ACP, 0, szFile, -1, NULL, 0);
        WCHAR  *wszBuffer = new WCHAR[len + 1];
        memset(wszBuffer, 0, len + 1);
        MultiByteToWideChar(CP_ACP, 0, szFile, -1, wszBuffer, len + 1);

        // 设置桌面的代码实现
        IActiveDesktop    *pDeskTop;
        HRESULT            hr;
        COMPONENTSOPT    comps;

        CoInitialize(NULL);
        hr = CoCreateInstance(CLSID_ActiveDesktop, NULL, CLSCTX_INPROC_SERVER, IID_IActiveDesktop, (void **)&pDeskTop);
       
        comps.dwSize            = sizeof(comps);
        comps.fEnableComponents    = TRUE;
        comps.fActiveDesktop    = TRUE;

        pDeskTop->SetDesktopItemOptions(&comps, 0);
        if (FAILED(pDeskTop->SetWallpaper(wszBuffer, 0)))
        {
            return;
        }

        pDeskTop->ApplyChanges(AD_APPLY_ALL | AD_APPLY_FORCE);
        pDeskTop->Release();

        CoUninitialize();
    }


    二、怎样对图片排序?
          好,现在我们知道了该怎么把指定的图片设置为桌面背景,它支持常见的bmp、jpg和gif等格式图片,可以说关键难点已经解决了。
          现在我们面临着第二个难题:我们需要设计一种算法对目录中的图片进行排序,能够根据一定的规则计算出这次该将哪个图片设置为桌面,不能出现重复和漏掉的情况。
          这就有点儿令人感到头痛了,当然最简单的方法是让用户把文件都命名为“数字.jpg”这样的形式。不过你要真敢这样做的话,可以想象到,用户将会毫不犹豫地对你的程序执行“Shife+Del”操作。

          提醒一个小小的知识点,当我们调用FindFirstFile和FindNextFile遍历文件的时候,得到的结果是有序的。
          于是马上想到了解决方法:建立一个配置文件,遍历所有图片文件,然后检查该文件名是否已经存在于配置文件中,若存在而继续遍历,否则就将该图片设置为桌面,同时将该文件名写入配置文件。
          这个方案可行吗?当然可行。
          但有没有更具效率的方法呢?显然会有。

          苦心冥想半天未果,中午吃饭的时候突然冒出了灵感:我们可以在指定的图片目录中建立一个子目录,每次枚举到第一个文件就将其设置为桌面并同时将该图片移动到子目录中;如果遍历不到任何图片文件时就将子目录中的图片全部移动出来。
          这个方案的好处在于避免了需要再设计一种算法对配置文件的写入和检查,同时也避免了每次设置图片时都需要遍历一次目录。
          假如图片数量为n,则第一种方法的平均时间复杂度为O(n/2),而第二种方法的时间复杂度无论在什么情况下都是O(1),效率更高。
          好,写到这里,我们也基本没啥困难了,后面将会扯一些其他的,下面请看代码:
    VOID GetDeskPic()
    {
        char szTempFile[MAX_PATH];
        char TempBuffer[MAX_PATH];
        char FileBuffer[MAX_PATH];

        // 路径不存在或不是目录
        if ((GetFileAttributes(szPicPath)) != FILE_ATTRIBUTE_DIRECTORY)
        {
            return;
        }
       
        sprintf(TempBuffer, "%s\\backup", szPicPath);
        if ((GetFileAttributes(TempBuffer)) == -1)
        {
            CreateDirectory(TempBuffer, NULL);
            return;
        }

        sprintf(FileBuffer, "%s\\*.*", szPicPath);

        WIN32_FIND_DATA dat;
        HANDLE            hFind;
        BOOL            bRet = FALSE;
       
        hFind = ::FindFirstFile(FileBuffer, &dat);
        if (hFind == INVALID_HANDLE_VALUE)
        {
            return;
        }

        do
        {
            if (dat.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
            {
                // 跳过目录,不递归
                continue;
            }
            else
            {
                // 判断是否图片文件
                if (!IsPicFile(dat.cFileName))
                {
                    continue;
                }
                else
                {
                    // 设置成桌面
                    sprintf(szTempFile, "%s\\%s", szPicPath, dat.cFileName);
                    SetDeskTop(szTempFile);

                    // 移动文件
                    sprintf(TempBuffer, "%s\\backup\\%s", szPicPath, dat.cFileName);
                    MoveFile(szTempFile, TempBuffer);

                    bRet = TRUE;
                    return;
                }
            }

        }while(::FindNextFile(hFind, &dat));

        if (!bRet)
        {
            MoveAllFile();
        }
    }

    三、怎样自启动?
          现在我们考虑一个问题:怎样让程序高效地自启动?
          什么?修改注册表的Run项?都什么年代了还用这一招,再说这个项的启动顺序其实很晚,我们需要寻找一个比较好的方法。
          我用的是WinLogon通知事件,如果你还不知道这是什么东西,那就需要请教google大叔了,下面我简单说一下我们可以利用的事件。
          Logon(登录事件)、StartShell(启动shell事件)、Startup(系统开机事件)这三个都可以利用,相对来说最好的应该是StartShell,不过我个人比较懒,套用了以前写的一个测试程序,是用的Startup事件,大家可以自己决定。
          假如我们编写了Startup的响应函数EventStartup并将该DLL通过注册表进行了安装,那么在下次开机时便会调用我们提供的处理函数。这个DLL将会被WinLogon进程加载,由于该进程是系统关键进程,不用担心被结束,因此我们还可以添加其他功能,如实现定时变换桌面的功能,而且不会增加冗余进程。
          在相应的事件处理函数中,我们直接调用上述设置编写好的函数即可,如下:
    VOID WINAPI EventStartup(DWORD Parameter)
    {
         GetDeskPic();
         return;
    }

    四、怎样编写配置程序?
          在上述编写好的DLL中,我将目标文件夹的路径设置为一个预定义好的全局变量,但需要允许用户自定义。
          假如我们的定义如下:
          const CHAR szPicPath[MAX_PATH] = "D:\\我的文档\\My Pictures\\桌面";
          我们用UltraEdit等十六进制编辑器打开编译连接生成的DLL,搜索“D:\\”这个字符串,找到起始地址后(记住这个地址,后面要用)便可进行修改了。
          配置程序的功能是:将生成的原DLL作为资源链接入自身,在需要安装的时候就将其释放到system32目录;紧接着通过文件读写API打开该dll并调整文件指针到上述字符串的偏移,然后写入新的路径即可(注意长度不要超过MAX_PATH)。
          当然,事情还没结束,还需要修改注册表来安装WinLogon通知,需要动的注册表路径是“HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\Notify\xxxx”,剩下的我就不多说了。
          至于如何卸载就更不用我废话了,删除注册表,删除文件即可,注意这时候dll通常无法被删除,可以用MoveFileEx执行重启后删除。
          最后贴一张图片,我用C++ Builder 2009写的配置器,代码就不提供了,有上面那几个函数已经够你用了。





    评论

  • 果然是高人,我可以做有一个自动更换壁纸的程序,和您比起来差远了.