空闲时间研究一个小功能:winform桌面程序如何实现动态更换桌面图标
今天休息在家,由于天气热再加上疫情原因,就在家里呆着,空闲时想着,在很早以前(约3年前),产品人员跟我提了一个需求,那就是winform桌面程序的图标能否根据节日动态更换,这种需求在移动app上还是比较常见,比如:淘宝、天猫、京东、360等,它们在逢节假日时除了app内容有更新,app icon也是都更新了的,但pc端的应用程序(app)则很少见到说有动态更新图标的,故当时我是直接回绝了的,明确表示做不了,但今天我仔细想了一下,其实也是可以实现的,虽然无法直接更新桌面图标,但我们可以更新替换掉桌面的快捷文件呀!(pc端桌面的图标本质都是一个link文件)想到这里我就开始设计,最终还是实现了无感知更新pc端桌面图标的功能。
先看实现方案的流程图如下:
其中:dynamiciconapp【原生真实程序】、applauncher【引导启动程序】 均是我演示的demo程序
如上方案核心实现思路与步骤是:
1.桌面快捷方式连接的程序是启动程序(即:前置程序),而非真实要打开的程序,目的是:如果要替换桌面快捷方式必需是另外进程来执行,如果快捷方式打开的是真实程序,而真实程序又来更新替换桌面快捷方式文件,会被该桌面快捷方式文件被占用; 【当然也可以不用单独搞一个启动程序,可以就是真实程序,但真实程序需支持传入参,根据入参的不同的,可以开启多个进程,也可以达到该目的,我之前就实现过类似功能:程序自己更新自己】
2.桌面快捷方式本质只是一个软连接(linux中也有),故如果真实程序需要更新,只需通过独立的更新程序(程序更新实现原理有很多,在此就不展开说明)来更新真实的程序即可,而桌面的桌面快捷方式却不用动,仍然通过:桌面快捷方式-》启动程序-》最新的真实程序,用户无感知的。
3.更新桌面图标准备工作与步骤:
3.1.创建applauncher【引导启动程序】,在程序内部直接实现:执行启动dynamiciconapp.exe【原生真实程序】,启动时带上额外的参数(告之来自启动程序及自己的进程id,如:fromlauncher:12345),然后关闭自己即可。(其实就是跳板的作用),示例代码如下:
/// <summary> /// 引导启动程序 /// author:zuowenjun /// date:2021-6-19 /// </summary> namespace applauncher { public partial class form1 : form { public form1() { initializecomponent(); } private void form1_load(object sender, eventargs e) { processstartinfo proc = new system.diagnostics.processstartinfo(); proc.useshellexecute = true; proc.filename =path.combine(application.startuppath, "dynamiciconapp.exe"); proc.arguments = "fromlauncher:" + process.getcurrentprocess().id; proc.createnowindow = false; //启动进程 process.start(proc); this.close(); } } }
3.2.创建dynamiciconapp.exe【原生真实程序】,在程序内部实现:在程序启动界面前,通过参数判断是否来自启动引导程序,并判断applauncher【引导启动程序】进程是否已结束,若未结束,则先尝试直接kill,若kill失败则老实等待进程退出。若进程已结束,则再判断是否需要更新桌面快捷方式(这个看具体的情况,可以在db表中或远程配置中心或api中增加可获取是否需要更新桌面快捷方式文件的逻辑),若需要更新,则将当前应用程序目录的指定的桌面快捷方式文件(如:dynamiciconapp.lnk,如果不在,应该从cdn获取最新的桌面快捷方式文件)替换桌面上已有或不存的桌面快捷方式文件,替换ok后,再正常运行显示程序界面即可,这样就能实现桌面app的icon按需动态更换的效果。示例代码如下:
1 /// <summary> 2 /// 原生真实程序 3 /// author:zuowenjun 4 /// date:2021-6-19 5 /// </summary> 6 namespace dynamiciconapp 7 { 8 static class program 9 { 10 /// <summary> 11 /// the main entry point for the application. 12 /// </summary> 13 [stathread] 14 static void main(string[] args) 15 { 16 if (args != null && args.length > 0) 17 { 18 bool fromlauncher = args[0].startswith("fromlauncher:"); 19 if (fromlauncher) 20 { 21 int launcherprocid = int.parse(args[0].substring(args[0].indexof(":") + 1)); 22 //等待applauncher程序完全退出后,再正式运行 23 //messagebox.show("will starting..." + launcherprocid); 24 process proc = null; 25 try 26 { 27 proc = process.getprocessbyid(launcherprocid); 28 messagebox.show(proc.id + "," + proc.processname + "," + proc.hasexited 29 + "," + proc.exittime); 30 } 31 catch (exception e) 32 { 33 //messagebox.show("process.getprocessbyid error:" + e.tostring()); 34 if (!e.message.contains("has exited")) 35 { 36 return; 37 } 38 proc = null; 39 } 40 41 42 bool waitexit = false; 43 if (null != proc) 44 { 45 try 46 { 47 thread.sleep(500); 48 proc.kill(); 49 waitexit = true; 50 } 51 catch (exception e) 52 { 53 messagebox.show("kill process error:" + e.tostring()); 54 proc.waitforexit(); 55 waitexit = true; 56 } 57 } 58 59 //messagebox.show("start run after launcher process exit (waitexit = " + waitexit + ") !"); 60 } 61 } 62 application.sethighdpimode(highdpimode.systemaware); 63 application.enablevisualstyles(); 64 application.setcompatibletextrenderingdefault(false); 65 application.run(new form1()); 66 } 67 } 68 } 69 70 71 72 73 /// <summary> 74 /// 原生真实程序 75 /// author:zuowenjun 76 /// date:2021-6-19 77 /// </summary> 78 namespace dynamiciconapp 79 { 80 public partial class form1 : form 81 { 82 public form1() 83 { 84 initializecomponent(); 85 } 86 87 private void form1_load(object sender, eventargs e) 88 { 89 //todo:这里只是示例,判断是否需要更新桌面快捷方式文件(换图标)取决于远程动态配置 90 bool needupdateapplink = true; 91 //todo:这里只是判断应用程序根目录有没有快捷方式文件,而实际的可能还要增加: 92 //若本地没有,则去cdn下载到本地 93 if (needupdateapplink && file.exists("dynamiciconapp.lnk")) 94 { 95 messagebox.show("will be copy dynamiciconapp.link to desktop dir!"); 96 string desktopfilepath = path.combine(environment.getfolderpath(environment.specialfolder.desktop), "dynamiciconapp.lnk"); 97 if (file.exists(desktopfilepath)) 98 { 99 file.setattributes(desktopfilepath, fileattributes.normal); 100 file.delete(desktopfilepath); 101 } 102 string linkfilepath = path.combine(application.startuppath, "dynamiciconapp.lnk"); 103 file.copy(linkfilepath, desktopfilepath); 104 } 105 } 106 } 107 }
3.3.提前创建快捷方式文件,把图标及链接目标都设置好(当然也可以使用wshshell 组件通过c#来动态创建,这个看需要,我个人觉得没必要),放到cdn或像我示例的放到真实程序根目录即可,注意:dynamiciconapp.lnk 快捷方式的名字虽然叫原生真实程序名,但实际链接执行的是:引导启动程序,目的就是桌面的快捷方式必需是真实程序名,这样对于普通用户来说才是对的。
联想一下,大家有没有发现,原来qq也玩的是这一套,不信你看桌面的快捷方式及实际目标,截图为证:
桌面快捷方式:
qq快捷方式的属性(目标链接的是:qqsclauncher.exe,这个就是qq的引导程序,而本身的程序是qq.exe)
qq真实应用:(它们的关系是:qq快捷方式-》执行qq引导程序-》qq程序,与我们的设计是如出一辙呀!)
qq这样做,除了我说的那个目的(可以动态改快捷图标),也可以在启动qq前做各种前置验证,比如:是否需要升级等。
好了,回到我们的今天的主题上来,上面已讲了实现方案及具体步骤,现在是见证效果的时刻了。
这是原始安装时的桌面快捷方式:(可以看到目标是指向的引导启动程序)
然后我在应用程序根目录把快捷方式更新(更换图标),如下图示:【当然如果是真实的生产环境,应该是将快捷方式文件放到cdn,同时通过远程配置中心或api来返回是否需要更新快捷方式文件的逻辑】
改后效果:
好了,然后我们仍然模拟用户,是在桌面双击原快捷方式图标,最后运行后,桌面的快捷方式图标也自动更新了。如下图示:(原生真实程序运行起来了,桌面的icon也同步更新了,当然想改名也是ok的,甚至改快捷链接目标也是可以的)
文末说一下,这篇文章只是空闲时的小研究而矣,至于技术过不过时还是看需求吧,我最近工作重点是java栈的spring微服务体系各种研究与实战,比如:最近我实现了基于自定义的mybatis拦截器来实现sql语句自动审计功能(即:自动发现sql语句是否合规,是否存在性能问题,若审计不通过,则会报错,这样在开发阶段就能提前发现问题,及时止损),同时也研究了关于oauth2.0+oidc相关内容,后面有机会再分享(为何今天不分享,因为家里的这个笔记本电脑太差,运行vs2019都比较卡,运行ieda估计直接死机),近期工作真的很忙,上班没时间,下班又加班太晚没有精力,生活不易,但学习也不能止。
上一篇: C#下载歌词文件的同步和异步方法
下一篇: C#遍历文件夹及其子目录的完整实现方法