共计 4295 个字符,预计需要花费 11 分钟才能阅读完成。
前几天写了个「干掉微信只读」的程序,用来解决微信更新 3.9 当前收到文件会主动设置为只读的问题。微信这个设计能够无效地保障收到的原始文件安全性,防止被无心改变。但的确有违某些用户的习惯性操作。「干掉微信只读」从技术角度钻研了用 .NET 程序解决问题的伎俩,同时也提供了 Demo 程序。有用户返回 Demo 很好用,就是每次开发须要手工启动不太不便。
作为一个监控类程序,设置开机自启的确是刚需,所以接下来就对这个程序进行一些改良。
一、设置自启动的办法
对于 Windows 来说,设置自启动次要有三个路径:
- 批改注册表增加自启动项;
- 在开发菜单增加自启动项;
- 应用打算工作启动。
对于这三种办法,最简略的是第 1 种,应用 Microsoft.Win32.Registry
相干 API 写注册表就好。
最洁净的是第 2 种,在开始菜单 程序 \ 启动
增加一个快捷方式,不须要了要删除也好找。在程序里创立快捷方式须要应用 Windows Script Host Object Model,须要增加相应的 COM 组件援用,应用 WshSehll
来实现。
最简单的是第 3 种,因为做打算工作须要的配置内容比拟多。这种形式也须要增加 COM 组件援用(搜寻 TaskScheduler
)。
相对来说,第 1 种形式最为轻量、简略,这里采纳第 1 种形式:批改注册表。
二、技术剖析及处理过程
增加复选框来设置 / 勾销自启动
界面上不必想太简单,加一个复选框控件,勾上就写注册项,去掉勾选就删除注册项。逻辑很简略:
AutoStartup.CheckedChanged += (_, _) => {if (AutoStartup.Checked) {// TODO 增加注册表项} | |
else {// TODO 删除注册表项} | |
}; |
不过须要留神的是,程序启动之后会去查看注册表看是否设置了自启动,如果设置了会将选框勾上。此时如果曾经注册了 CheckedChanged
事件处理函数,那么会再次进入“增加注册表项”的逻辑。为了防止这种事件产生,增加事件处理函数必须在初始化 AutuStartup.Checked
之后。
理解如何写注册表值
注册表项须要增加在 HKEY_CURRENT_USER
下的 SOFTWARE\Microsoft\Windows\CurrentVersion\Run
键中,字符串值 (REG_SZ
)。值的名称任意,个别是应用程序名;值的数据就是一个含参数的命令行。
如果不能确定「数据」该如何设置,能够看看现有的自启动项设置。比方下图中金山文档的启动命令就是一个带参数的命令行。而 EverythingToolbar 的启动命令门路中因为存在空格,还应用了引号。
理解注册表自启动项的设置办法之后,咱们晓得须要找到 执行文件的门路 来组成自启动命令。
获取执行文件的门路
通过 AppContext.BaseDirectory
很容易失去执行文件所在目录,但还须要补文件名才是执行文件门路。与其去找文件名,不如就用 Assembly.GetExecutingAssembly().Location
还间接一些。
在理论开发中,该办法获取执行文件门路的确工作良好,直到 —— 公布。采纳“生成繁多文件 (PublishSingleFile
)”公布进去之后失去的门路是空值,而且这个景象如同是最近才呈现的,它很可能跟更新 SDK 无关(刚更新了 VS2022 和 .NET 6 SDK)。对于这个问题在 Github 上能够找到很多探讨,最终的解决办法是应用 Process.GetCurrentProcess().MainModule.FileName
。
留神到 MainModule
的类型是 ProcessModule?
,也就是说可能为 null
。为了稳当起见,罗唆两个办法都用上。
private static string? executable; | |
public static string Executable => executable ??= (Process.GetCurrentProcess().MainModule?.FileName | |
?? Assembly.GetExecutingAssembly().Location.LetWhenNot(path => path.EndsWith(".exe", true, null), | |
path => $"{path[..^Path.GetExtension(path).Length]}.exe" | |
) | |
); |
注:
LetWhenNot
是 Viyi.Util 提供的扩大,相似的还应用了Let
、When
、Else
等扩大,能够在源码(后附)中找到。
Assembly.GetExecutingAssembly().Location
有可能失去的是一个 DLL,所以这里间接暴力解决成 .exe
了。
用户体验设计
拿到了可执行文件门路之后,当然能够间接写注册表了。但问题在于,主程序的执行逻辑并不会发生变化,它依然只是弹了一个框出来,期待用户确认 / 批改微信接管文件的门路,再开启「监听」。这一步保留用户干涉会大大降低自启动的用户体验。所以在优化用户体验方面,须要思考两种状况:
- 用户本人启动程序的时候,先确认门路,再监听。这就是原来的逻辑,不必扭转。
- 自启动的时候,能主动监听。但监听的门路必定不能是
GuessReceivePath()
失去的,因为它不能保障正确。
这样一来,在用户设置自启动的时候就须要设置监听门路,这个门路依然能够来自 ReceivePath.Text
,但必须保留下来。这个值保留成配置文件或者保留到注册表都是可选的计划。不过我抉择了另一个计划:不保留,而是作为自启动命令的参数传入。
当程序启动查看到有传入参数的时候,就把这个参数作为监听门路,立刻暗藏窗口,开始监听。这部分逻辑:
if (args.length > 0) {ReceivedPath.Text = args[0]; | |
StartWatch().Then(Hide); | |
} |
然而很遗憾,这里又有坑 —— Hide 在窗体的结构和 Load 阶段都不起作用。
这里有两个方法,一个是在 Shown
事件中去暗藏,另一个是在 Load
事件中通过 BeginInvoke(Hide)
来调用暗藏。BeginInvoke()
是一个协调线程间操作的办法,它在肯定水平上会期待主线程(UI 线程)实现某些操作。尽管文档中没有明确的阐明它的运作机制,然而实测无效。
在 Load
中去暗藏窗体绝对简略,因为 Load
事件只会在窗体的生命周期中呈现一次。但 Shown
就不同了,只有显示进去就会执行。如果在 Shown
中暗藏窗口,在用户点击任务栏图标心愿显示窗口的时候,会陷入自动隐藏的死循环,所以这里在第一次暗藏之后就须要把事件处理函数登记掉:
在
Load
事件中解决的形式绝对简略,就不写示例了。
if (args.length > 0) { | |
//... | |
// 定义部分函数作为处理函数,公有实例函数也行 | |
void handle(object? sender, EventArgs e) {StartWatch().Then(Hide); | |
Shown -= handle; // ← 登记处理函数 | |
} | |
Shown += handle; // ← 注册处理函数 | |
} |
总算到了写注册表的环节
所有具备,只差写注册表了,其实很简略,就一句话:
RegistryKey Key = Registry.CurrentUser.CreateSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Run"); | |
Key.SetValue(AppName, $""""{AppHelper.Executable}" "{wxFilePath}" """.Trim()); |
其中 AppHelper.Executable
拿到了执行文件门路,wxFilePath
则是须要监听的目录。
为了不去本义引号,这里应用了 C# 11 的 Raw string literals(原始字符串文本)。这个语法应用至多三个引号作为限界符,分单行和多行两种状况。下面应用了单行语法,间接踩进了坑里 —— 字符串内容是以双引号开始或结尾的,词法剖析会认为那是限界符的一部分,所以只能用多余的空格来分隔,最初再通过 Trim()
把空格去掉。
Key.SetValue(AppName, $""""{AppHelper.Executable}" "{wxFilePath}" """.Trim()); | |
// ^ ^ 须要加空格来分隔 | |
// ^^^ ^^^ 一对限界符 | |
// ^ ^ 内容中的引号 |
当然如果用多行写法就不会呈现这种问题:
Key.SetValue(AppName, $""""{AppHelper.Executable}" "{wxFilePath}" | |
"""); |
在删除这个注册值的时候也须要留神,如果这个值不存在会抛 ArgumentException
。比拟暴力的解决办法是抓住异样,疏忽掉
try {Key.DeleteValue(AppName); } | |
catch (ArgumentException) {// ignore} |
也能够当时判断是否存在。RegisterKey
并没有提供判断值是否存在的 API,但能够通过 GetValue()
来取值,如果取值为 null
则示意不存在(如果是未设置无效字符串数据,取值会失去 ""
)。
还能够优化一下 GuessReceivePath
当然不是优化 GuessReceivePath()
自身,而是在某些状况下,不须要再去猜目录了。
- 通过参数传入了门路的状况下,不须要猜
-
如果注册表里有启动项设置,也不须要猜。
这里有个问题:如果有注册自启动,不应该是通过参数传入了门路吗?怎么还须要去查看注册表的启动设置?
话虽如此,但谁能预测用户行为呢。不论是否自启动,用户都能够手工双击启动,不带参数啊!
这样一来,给 ReceivePath.Text
赋初始值的逻辑就会有一个优先级的解决:
ReceivePath.Text = argPath ?? regPath ?? GuessReceivePath();
argPath
来自程序的启动参数,regPath
则是从注册表值中剖析进去的。这个剖析过程要粗疏的话,不仅须要把参数剖析进去(万一手工设置不带参数呢),还须要兼容解决含引号和不含引号两种状况。当然对于这样一个小程序,就不做这么粗疏了,粗犷地依据程序设置的形式来解析(假如取到的值就是这个程序设置的)。
相干资源
- 相干浏览:写个 .NET 程序解决 Windows 版微信 3.9 收到文件“只读”的问题
- 下载编译好的(可能须要自行装置环境)
-
学习源代码
留神:间接克隆指定分支:git clone -b WxFilesWritable https://gitee.com/jamesfancy/code-for-articles.git