关于.net:给-NET-程序加个设置开机启动

112次阅读

共计 4295 个字符,预计需要花费 11 分钟才能阅读完成。

前几天写了个「干掉微信只读」的程序,用来解决微信更新 3.9 当前收到文件会主动设置为只读的问题。微信这个设计能够无效地保障收到的原始文件安全性,防止被无心改变。但的确有违某些用户的习惯性操作。「干掉微信只读」从技术角度钻研了用 .NET 程序解决问题的伎俩,同时也提供了 Demo 程序。有用户返回 Demo 很好用,就是每次开发须要手工启动不太不便。

作为一个监控类程序,设置开机自启的确是刚需,所以接下来就对这个程序进行一些改良。

一、设置自启动的办法

对于 Windows 来说,设置自启动次要有三个路径:

  1. 批改注册表增加自启动项;
  2. 在开发菜单增加自启动项;
  3. 应用打算工作启动。

对于这三种办法,最简略的是第 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 提供的扩大,相似的还应用了 LetWhenElse 等扩大,能够在源码(后附)中找到。

Assembly.GetExecutingAssembly().Location 有可能失去的是一个 DLL,所以这里间接暴力解决成 .exe 了。

用户体验设计

拿到了可执行文件门路之后,当然能够间接写注册表了。但问题在于,主程序的执行逻辑并不会发生变化,它依然只是弹了一个框出来,期待用户确认 / 批改微信接管文件的门路,再开启「监听」。这一步保留用户干涉会大大降低自启动的用户体验。所以在优化用户体验方面,须要思考两种状况:

  1. 用户本人启动程序的时候,先确认门路,再监听。这就是原来的逻辑,不必扭转。
  2. 自启动的时候,能主动监听。但监听的门路必定不能是 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() 自身,而是在某些状况下,不须要再去猜目录了。

  1. 通过参数传入了门路的状况下,不须要猜
  2. 如果注册表里有启动项设置,也不须要猜。

    这里有个问题:如果有注册自启动,不应该是通过参数传入了门路吗?怎么还须要去查看注册表的启动设置?

    话虽如此,但谁能预测用户行为呢。不论是否自启动,用户都能够手工双击启动,不带参数啊!

这样一来,给 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

正文完
 0