dotnet 职业技术学院

博客

dotnet 职业技术学院

使用 ProcessMonitor 找到进程所操作的文件的路径

dotnet 职业技术学院 发布于 2019-06-01

很多系统问题都是可以修的,不需要重装系统,但是最近我还是重装了。发现之前正在玩的一款游戏的存档没有了……因为我原有系统的数据并没有删除,所以我还是能找回原来的游戏存档的。但是,我怎么知道这款游戏将存档放在了那个路径下呢?搜索当然是好方法,不过我喜欢玩的游戏大多是冷门游戏,有些搜不到。于是我就用 Process Monitor 找到了存档所在,恢复了我的游戏进度。

本文介绍如何使用 ProcessMonitor 找出进程创建和修改的文件路径。


下载 Process Monitor

Process Monitor 是微软极品工具箱的一部分,你可以在此页面下载:

打开 Process Monitor

当你一开始打开 Process Monitor 的时候,列表中会立刻刷出大量的进程的操作记录。这么多的记录会让我们找到目标进程操作的文件有些吃力,于是我们需要设置规则。

Process Monitor 的工具栏按钮并不多,而且我们这一次的目标只会用到其中的两个:

  • 清除列表(将已经记录的所有数据清空,便于聚焦到我们最关心的数据中)
  • 设置过滤器(防止大量无关的进程操作进入列表中干扰我们的查找)

Process Monitor 的工具栏按钮

设置过滤规则

我启动了我想要玩的游戏,在任务管理器中发现它的进程名称是 RIME.exe。呃……如果你也想玩,给你个链接:

点击设置过滤规则按钮,可以看到下面的界面:

设置过滤器

可以选定 某个名词 与另一个字符串 进行某种操作 之后 引入 (Include)排除 (Exclude)

我希望找到 RIME 这款游戏的游戏存档位置,所以我需要进入游戏,玩到第一个会存档的地方之后观察监视的操作记录。

所以我希望的过滤器规则是:

  1. 将所有不是 RIME.exe 进程的记录全部排除;
  2. 将不是文件操作的记录全部排除;
  3. 将读文件的记录排除(这样剩下的只会是写文件,毕竟游戏读文件很频繁的)。

于是我设置了这些规则:

[ProcessName] is [RIME.exe]      then [Exclude]
[Operation]   is [RegOpenKey]    then [Exclude]
[Operation]   is [RegCloseKey]   then [Exclude]
[Operation]   is [RegQueryKey]   then [Exclude]
[Operation]   is [RegQueryValue] then [Exclude]
[Operation]   is [RegEnumKey]    then [Exclude]
[Operation]   is [RegSetInfoKey] then [Exclude]
[Operation]   is [ReadFile]      then [Exclude]

这样,剩下的记录将主要是文件写入以及一些不常见的操作了。

分析记录

现在,我在游戏里面玩到了第一个存档点,终于在 Process Monitor 的进程列表中看到了创建文件和写入文件相关的操作了。

记录的列表

通过观察 Path 的值,我可以知道 RIME 游戏的存档放在了 %LocalAppData%\SirenGame 文件夹下。

于是我关掉 RIME 游戏,将原来系统中的此文件夹覆盖到新系统中的此文件夹之后,再次打开游戏,我恢复了我的全部游戏存档了。

如何为 Win32 的打开和保存对话框编写文件过滤器(Filter)

dotnet 职业技术学院 发布于 2019-05-31

在使用 Win32 / WPF / Windows Forms 的打开或保存文件对话框的时候,多数情况下我们都会考虑编写文件过滤器。UWP 中有 FileTypeFilter 集合可以添加不同的文件种类,但 Win32 中却是一个按一定规则组合而成的字符串。

因为其包含一定的格式,所以可能写错。本文介绍如何编写 Filter。


编写 Filter

Filter 使用竖线分隔不同种类的过滤器,比如 图片|*.png;*.jpg|文本|*.txt|walterlv 的自定义格式|*.lvyi

var dialog = new OpenFileDialog();
dialog.Filter = "图片|*.png;*.jpg|文本|*.txt|walterlv 的自定义格式|*.lvyi";
dialog.ShowDialog(this);

过滤器的显示效果

有时我们会看到一些程序的过滤器里面显示了过滤器本身,而不止是名称,实际上是因为名称中包含了过滤器:

图片 (png, jpg)|*.png;*.jpg|文本 (txt)|*.txt|walterlv 的自定义格式 (lvyi)|*.lvyi

名称中包含过滤器

你不可以在过滤器中省略名称或者过滤器任何一个部分,否则会抛出异常。

附:如何显示对话框

对于 .NET Core 版本的 WPF 或者 Windows Forms 程序来说,需要安装 Windows 兼容 NuGet 包:

安装后可以使用 Windows Forms 版本的 OpenFileDialog 或者 WPF 版本的 Microsoft.Win32.OpenFileDialog


参考资料

.NET/C# 编译期能确定的字符串会在字符串暂存池中不会被 GC 垃圾回收掉

dotnet 职业技术学院 发布于 2019-05-28

当我们不再使用某个对象的时候,此对象会被 GC 垃圾回收掉。当然前提是你没有写出内存泄漏的代码。我们也知道如果生成了大量的字符串,会对 GC 造成很大的压力。

但是,如果在编译期间能够确定的字符串,就不会被 GC 垃圾回收掉了。


示例代码

下面,我创建了几个字符串,我关心的字符串是 "walterlv""lindexi" 以及一个当前时间。

于是使用下面的代码来验证:

using System;
using System.Linq;
using System.Runtime.CompilerServices;

namespace Walterlv.Demo
{
    class Program
    {
        static void Main(string[] args)
        {
            var table = new ConditionalWeakTable<string, Foo>
            {
                {"walterlv", new Foo("吕毅")},
                {"lindexi", new Foo("林德熙")},
            };
            var time = DateTime.Now.ToString("T");
            table.Add(time, new Foo("时间"));
            time = null;

            Console.WriteLine($"开始个数:{table.Count()}");
            GC.Collect();
            Console.WriteLine($"剩余个数:{table.Count()}");
        }
    }

    public class Foo
    {
        public string Value { get; }
        public Foo(string value) => Value = value;
    }
}

"walterlv""lindexi" 是在编译期间能够完全确定的字符串,而当前时间字符串我们都知道是编译期间不能确定的字符串。

在 GC 收集之前和之后,ConditionalWeakTable 中的对象数量从三个降到了两个。

运行结果

并没有清除成 0 个,说明字符串现在仍然是被引用着的。

那被什么引用着呢?是字符串暂存池。要理解字符串暂存池,可以阅读我的另一篇博客:

另外,即便设置了 CompilationRelaxations.NoStringInterning,编译期间能确定的字符串在上述代码中也是不会被垃圾回收的。


参考资料

.NET/C# 的字符串暂存池

dotnet 职业技术学院 发布于 2019-05-28

本文介绍 .NET 中的字符串暂存池。


字符串暂存池

.NET 的 CLR 运行时会在运行期间管理一个字符串暂存池(string intern pool),在字符串暂存池中的字符串只有一个实例。

例如,在下面的代码中,变量 abc 都是同一个实例:

var a = "walterlv";
var b = "walterlv";
var c = "walterlv";

我有另一篇博客说到了此问题,可以参见:

字符串暂存池的出现是为了避免分配大量的字符串对象造成的过多的内存空间浪费。

编译期间确定

默认进入字符串暂存池中的字符串是那些写程序的时候直接声明或者直接写入代码中的字符串。上一节中列举的三个变量中的字符串就是直接写到代码中的字符串。

默认情况下编译期间能确定出来的字符串会写入到程序集中,运行时能直接将其放入字符串暂存池。

从暂存池中获取字符串

现在,我们要制造出编译期间不能确定出来的字符串,以便进行一些试验。

我们当然不能使用简单的 "walter" + "lv" 这样简单的字符串拼接的方式来生成字符串,因为实际上这样的字符串依然可以在编译期间完全确定。

所以这里使用 StringBuilder 来在运行期间生成字符串。

var a = "walterlv";
var b = new StringBuilder("walter").Append("lv").ToString();
var c = string.Intern(b);

Console.WriteLine(ReferenceEquals(a, b));
Console.WriteLine(ReferenceEquals(a, c));

在这段代码中,虽然 abc 三个字符串的值都是相等的,但 ab 两个字符串是不同的实例,而 ac 两个字符串是相同的实例。

我们使用了 string.Intern 方法从字符串池中取出了一个字符串的实例。

另外,string 类型还提供了 string.IsInterned 来判断一个字符串是否在字符串暂存池中。

不要池化

你可以在程序集中标记 CompilationRelaxations.NoStringInterning,这样,此程序集中的字符串就不会被池化。即便是在编译期间写下的字符串也会在运行时生成新的实例。

方法是在一个 C# 代码文件中添加特性标记。

[assembly: CompilationRelaxations(CompilationRelaxations.NoStringInterning)]

垃圾回收

在字符串暂存池中的字符串不会被垃圾回收,你可以阅读另一篇博客:


参考资料

.NET/C# 避免调试器不小心提前计算本应延迟计算的值

dotnet 职业技术学院 发布于 2019-05-28

延迟计算属性的值,应该很多小伙伴都经常使用。比如在属性的 get 方法中判断是否已初始化,如果没有初始化则立即开始初始化。

但这样的写法存在一个很大的问题——如果你使用 Visual Studio 调试,当你把鼠标划到对象的实例上的时候,属性就会立刻开始进行初始化。而此时对你的代码来说可能就过早初始化了。我们不应该让调试器非预期地影响到我们程序的执行结果。

本文介绍如何避免调试器不小心提前计算本应延迟计算的值。


方法是在属性上添加一个特性 DebuggerBrowsableAttribute

private Walterlv _foo;

[DebuggerBrowsable(DebuggerBrowsableState.Never)]
public Walterlv Walterlv => _foo ?? (_foo = new Walterlv());

public bool IsInitialized => !(_foo is null);

当指定为不再显示的话,在调试器中查看此实例的属性的时候就看不到这个属性了,也就不会因为鼠标划过导致提前计算了值。

当然,如果你希望为你的类型定制更多的调试器显示方式,可以参考我的另一篇博客:


参考资料

cmd.exe 的命令行启动参数(可用于执行命令、传参或进行环境配置)

dotnet 职业技术学院 发布于 2019-05-25

有一些程序不支持被直接启动,而要求通过命令行启动。这个时候,你就需要使用 cmd.exe 来启动这样的程序。我们都知道如何在 cmd.exe 中启动一个程序,但是当你需要自动启动这个程序的时候,你就需要知道如何通过 cmd.exe 来启动一个程序,而不是手工输入然后回车运行了。

本文就介绍 cmd.exe 的命令行启动参数。利用这些参数,你可以自动化地通过 cmd.exe 程序来完成一些原本需要通过手工执行的操作或者突破一些限制。


一些必须通过命令行启动的程序

一般来说,编译生成的 exe 程序都可以直接启动,即便是命令行程序也是如此。但是有一些程序就是要做一些限制。比如下面的 FRP 反向代理程序:

FRP 反向代理程序限制必须从命令行启动

那么我们如何能够借助于 cmd.exe 来启动它呢?接下来说明。

顺便,使用 PowerShell 来启动的方法可以参见我的另一篇博客:

cmd.exe 的帮助文档

先打开一个 cmd,然后输入:

> cmd /?

你就可以看到 cmd.exe 的使用说明:

cmd.exe 的使用说明

启动 Windows 命令解释器的一个新实例

CMD [/A | /U] [/Q] [/D] [/E:ON | /E:OFF] [/F:ON | /F:OFF] [/V:ON | /V:OFF]
    [[/S] [/C | /K] string]

你可以随时输入上面的 cmd /? 命令来查看这些参数详细说明,所以本文不会非常详细地列举各个参数的含义,只会列出一些常见的使用示例。

cmd.exe 的启动参数示例

使用 cmd.exe 间接启动一个程序并传入参数

下面的命令,使用 cmd 间接启动 frpc.exe 反向代理程序,并给 frpc.exe 程序传入 -c ./frpc.ini 的启动参数:

> cmd /c D:\walterlv\frp\frpc.exe -c ./frpc.ini

关于为什么会用这种方式启动 frpc.exe,则是为了设置 frpc.exe 为开机自动启动。

因为我写了一些 Asp.NET Core 的服务,详见:

另外,间接启动一个程序的时候也可以传入 /k 参数。与 /c 参数不同的是:

  • /c 在执行完程序之后,cmd.exe 也会终止
  • /k 在执行完程序之后,cmd.exe 依然会继续运行

所以 /c 命令会更适用于自动化的脚本,而 /k 命令则更适用于半自动化的脚本。

cmd.exe 启动参数使用中的坑

在上面的例子中,我们的路径中不涉及到空格。我们知道,路径中有空格的话,在命令行中使用需要加上引号。但实际上如果你真的给路径加上了引号,会发现 cmd.exe 就开始不识别你的命令路径了。

这个时候,你需要在整个传给 cmd.exe 的命令外层再加一层引号:

> cmd /c " "D:\walterlv folders\frp\frpc.exe" -c ./frpc.ini "

以上,感谢 林德熙 挥泪踩出来的坑,详见:

附 cmd.exe 的全部启动参数说明

启动 Windows 命令解释器的一个新实例

CMD [/A | /U] [/Q] [/D] [/E:ON | /E:OFF] [/F:ON | /F:OFF] [/V:ON | /V:OFF]
    [[/S] [/C | /K] string]

/C 执行字符串指定的命令然后终止 /K 执行字符串指定的命令但保留 /S 修改 /C 或 /K 之后的字符串处理(见下) /Q 关闭回显 /D 禁止从注册表执行 AutoRun 命令(见下) /A 使向管道或文件的内部命令输出成为 ANSI /U 使向管道或文件的内部命令输出成为 Unicode /T:fg 设置前台/背景颜色(详细信息见 COLOR /?) /E:ON 启用命令扩展(见下) /E:OFF 禁用命令扩展(见下) /F:ON 启用文件和目录名完成字符(见下) /F:OFF 禁用文件和目录名完成字符(见下) /V:ON 使用 ! 作为分隔符启用延迟的环境变量 扩展。例如,/V:ON 会允许 !var! 在执行时 扩展变量 var。var 语法会在输入时 扩展变量,这与在一个 FOR 循环内不同。 /V:OFF 禁用延迟的环境扩展。

注意,如果字符串加有引号,可以接受用命令分隔符 “&&” 分隔多个命令。另外,由于兼容性 原因,/X 与 /E:ON 相同,/Y 与 /E:OFF 相同,且 /R 与 /C 相同。任何其他开关都将被忽略。

如果指定了 /C 或 /K,则会将该开关之后的 命令行的剩余部分作为一个命令行处理,其中,会使用下列逻辑 处理引号(“)字符:

1.  如果符合下列所有条件,则会保留
    命令行上的引号字符:

    - 不带 /S 开关
    - 正好两个引号字符
    - 在两个引号字符之间无任何特殊字符,
      特殊字符指下列字符: &<>()@^|
    - 在两个引号字符之间至少有
      一个空格字符
    - 在两个引号字符之间的字符串是某个
      可执行文件的名称。

2.  否则,老办法是看第一个字符
    是否是引号字符,如果是,则去掉首字符并
    删除命令行上最后一个引号,保留
    最后一个引号之后的所有文本。

如果 /D 未在命令行上被指定,当 CMD.EXE 开始时,它会寻找 以下 REG_SZ/REG_EXPAND_SZ 注册表变量。如果其中一个或 两个都存在,这两个变量会先被执行。

HKEY_LOCAL_MACHINE\Software\Microsoft\Command Processor\AutoRun

    和/或

HKEY_CURRENT_USER\Software\Microsoft\Command Processor\AutoRun

命令扩展是按默认值启用的。你也可以使用 /E:OFF ,为某一 特定调用而停用扩展。你 可以在机器上和/或用户登录会话上 启用或停用 CMD.EXE 所有调用的扩展,这要通过设置使用 REGEDIT.EXE 的注册表中的一个或两个 REG_DWORD 值:

HKEY_LOCAL_MACHINE\Software\Microsoft\Command Processor\EnableExtensions

    和/或

HKEY_CURRENT_USER\Software\Microsoft\Command Processor\EnableExtensions

到 0x1 或 0x0。用户特定设置 比机器设置有优先权。命令行 开关比注册表设置有优先权。

在批处理文件中,SETLOCAL ENABLEEXTENSIONS 或 DISABLEEXTENSIONS 参数 比 /E:ON 或 /E:OFF 开关有优先权。请参阅 SETLOCAL /? 获取详细信息。

命令扩展包括对下列命令所做的 更改和/或添加:

DEL or ERASE
COLOR
CD or CHDIR
MD or MKDIR
PROMPT
PUSHD
POPD
SET
SETLOCAL
ENDLOCAL
IF
FOR
CALL
SHIFT
GOTO
START (同时包括对外部命令调用所做的更改)
ASSOC
FTYPE

有关特定详细信息,请键入 commandname /? 查看。

延迟环境变量扩展不按默认值启用。你 可以用/V:ON 或 /V:OFF 开关,为 CMD.EXE 的某个调用而 启用或停用延迟环境变量扩展。你 可以在机器上和/或用户登录会话上启用或停用 CMD.EXE 所有 调用的延迟扩展,这要通过设置使用 REGEDIT.EXE 的注册表中的 一个或两个 REG_DWORD 值:

HKEY_LOCAL_MACHINE\Software\Microsoft\Command Processor\DelayedExpansion

    和/或

HKEY_CURRENT_USER\Software\Microsoft\Command Processor\DelayedExpansion

到 0x1 或 0x0。用户特定设置 比机器设置有优先权。命令行开关 比注册表设置有优先权。

在批处理文件中,SETLOCAL ENABLEDELAYEDEXPANSION 或 DISABLEDELAYEDEXPANSION 参数比 /V:ON 或 /V:OFF 开关有优先权。请参阅 SETLOCAL /? 获取详细信息。

如果延迟环境变量扩展被启用, 惊叹号字符可在执行时间被用来 代替一个环境变量的数值。

你可以用 /F:ON 或 /F:OFF 开关为 CMD.EXE 的某个 调用而启用或禁用文件名完成。你可以在计算上和/或 用户登录会话上启用或禁用 CMD.EXE 所有调用的完成, 这可以通过使用 REGEDIT.EXE 设置注册表中的下列 REG_DWORD 的全部或其中之一:

HKEY_LOCAL_MACHINE\Software\Microsoft\Command Processor\CompletionChar
HKEY_LOCAL_MACHINE\Software\Microsoft\Command Processor\PathCompletionChar

    和/或

HKEY_CURRENT_USER\Software\Microsoft\Command Processor\CompletionChar
HKEY_CURRENT_USER\Software\Microsoft\Command Processor\PathCompletionChar

由一个控制字符的十六进制值作为一个特定参数(例如,0x4 是Ctrl-D,0x6 是 Ctrl-F)。用户特定设置优先于机器设置。 命令行开关优先于注册表设置。

如果完成是用 /F:ON 开关启用的,两个要使用的控制符是: 目录名完成用 Ctrl-D,文件名完成用 Ctrl-F。要停用 注册表中的某个字符,请用空格(0x20)的数值,因为此字符 不是控制字符。

如果键入两个控制字符中的一个,完成会被调用。完成功能将 路径字符串带到光标的左边,如果没有通配符,将通配符附加 到左边,并建立相符的路径列表。然后,显示第一个相符的路 径。如果没有相符的路径,则发出嘟嘟声,不影响显示。之后, 重复按同一个控制字符会循环显示相符路径的列表。将 Shift 键跟控制字符同时按下,会倒着显示列表。如果对该行进行了 任何编辑,并再次按下控制字符,保存的相符路径的列表会被 丢弃,新的会被生成。如果在文件和目录名完成之间切换,会 发生同样现象。两个控制字符之间的唯一区别是文件完成字符 符合文件和目录名,而目录完成字符只符合目录名。如果文件 完成被用于内置式目录命令(CD、MD 或 RD),就会使用目录 完成。 用引号将相符路径括起来,完成代码可以正确处理含有空格 或其他特殊字符的文件名。同时,如果备份,然后从行内调用 文件完成,完成被调用时位于光标右方的文字会被调用。

需要引号的特殊字符是: <space> ()[]{}^=;!'+,~(&()`

WPF 源代码 从零开始写一个 UI 框架

lindexi 发布于 2019-05-24

需要知道 WPF 是一个 UI 框架,作为一个 UI 框架,最主要的就是交互。也就是 UI 框架需要有渲染显示和处理用户输入的功能。 如果直接告诉大家 WPF 里面有哪些类,估计没有几位小伙伴会听下去,要么就是讲的类太简单,看过去我也就知道了,要么就是这个类可能我一直都不会用到他,即使可能会用到也早就忘了。 本文不会直接告诉大家 WPF 的源代码是如何写的,而是从零开始一起来写一个 UI 框架,在写的过程就会了解到为什么 WPF 可以这样写,为什么需要这样写,和 WPF 这样写的好处。 本文适合 WPF 的开发者同样也适合其他语言希望自己写一个 UI 框架的小伙伴。

在 Visual Studio 中重新将高级保存功能放出来,便于强制指定文件编码格式

dotnet 职业技术学院 发布于 2019-05-23

Visual Studio 的早期版本中有一个高级保存功能,但是升级到 Visual Studio 2019 之后这个功能就不在菜单项里面了。

本文将带你把它找出来继续使用。


第一步:工具 -> 自定义

打开 Visual Studio 2019,然后进入“工具 -> 自定义”菜单项。对于英文版本,是“Tools -> Customize”菜单项。

工具 -> 自定义

第二步:自定义命令

按照下图一个个点击,把“高级保存选项”放出来:

放出高级保存选项

当刚刚添加出来的时候,位置可能不太正确,但是我们可以点击窗口旁边的“上移”和“下移”按钮将其放在合适的位置。

为了照顾英文版,我也放出英文版的界面:

English Save Options

WPF 使用 AppBar 将窗口停靠在桌面上,让其他程序不占用此窗口的空间(附我封装的附加属性)

dotnet 职业技术学院 发布于 2019-05-23

本文介绍如何使用 Windows 的 AppBar 相关 API 实现固定停靠在桌面上的特殊窗口。


停靠窗口

你可能并不明白停靠窗口是什么意思。

看下图,你可能使用过 OneNote 的停靠窗口功能。当打开一个新的 OneNote 停靠窗口之后,这个新的 OneNote 窗口将固定显示在桌面的右侧,其他的窗口就算最大化也只会占据剩余的空间。

OneNote 的这种功能可以让你在一边浏览网页或做其他事情的时候,以便能够做笔记。同时又不用担心其他窗口最大化的时候会占据记笔记的一部分空间。

OneNote 的停靠窗口

这其实也是 Windows 任务栏所使用的方法。

OneNote 中给出的名称叫做“停靠窗口”,于是这可以代表微软希望用户对这个概念的理解名词。

只是,这个概念在 Windows API 中的名称叫做 AppBar。

AppBar

要做出停靠窗口的效果,最核心的 API 是 SHAppBarMessage,用于发送 AppBar 消息给操作系统,以便让操作系统开始处理此窗口已形成一个 AppBar 窗口。也就是我们在用户交互上所说的“停靠窗口”。

虽然说要让一个窗口变成 AppBar 只需要一点点代码,但是要让整个停靠窗口工作得真的像一个停靠窗口,依然需要大量的辅助代码。所以我将其封装成了一个 DesktopAppBar 类,方便 WPF 程序来调用。

如何使用

以下使用,你需要先获取我封装的源码才可以编译通过:

你可以在 XAML 中使用:

<Window x:Class="Walterlv.Demo.DesktopDocking.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:dock="clr-namespace:Walterlv.Demo.DesktopDocking"
        mc:Ignorable="d" Title="Walterlv 的停靠窗口" Height="450" Width="500"
        dock:DesktopAppBar.AppBar="Right">
    <StackPanel Background="#ffcd42">
        <TextBlock FontSize="64" Margin="64" TextAlignment="Center" Text="walterlv 的停靠窗口" />
        <Button Content="再停靠一个 - blog.walterlv.com" FontSize="32" Padding="32" Margin="32" Background="#f9d77b" BorderThickness="0"
                Click="Button_Click"/>
    </StackPanel>
</Window>

核心代码是其中的一处属性赋值 dock:DesktopAppBar.AppBar="Right",以及前面的命名空间声明 xmlns:dock="clr-namespace:Walterlv.Demo.DesktopDocking"

你也可以在 C# 代码中使用:

using System;
using System.Windows;

namespace Walterlv.Demo.DesktopDocking
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        protected override void OnSourceInitialized(EventArgs e)
        {
            base.OnSourceInitialized(e);
            DesktopAppBar.SetAppBar(this, AppBarEdge.Right);
        }
    }
}

使用以上代码中的任何一种方式,你就可以让你的窗口在右边停靠了。

停靠的窗口

从图中我们可以发现,我们的示例窗口停靠在了右边,其宽度就是我们在 XAML 中设置的窗口宽度(当然这是我封装的逻辑,而不是 AppBar 的原生逻辑)。

同时我们还能注意到,Visual Studio 的窗口是处于最大化的状态的——这是停靠窗口的最大优势——可以让其他窗口的工作区缩小,在最大化的时候不会覆盖到停靠窗口的内容。

另外,如果设置了第二个停靠窗口,那么第二个停靠窗口会挤下第一个窗口的位置。

两个停靠窗口

如何还原

Windows AppBar 的 API 有一个很不好的设定,如果进程退出了,那么 AppBar 所占用的空间 并不会还原!!!

不过不用担心,我在封装的代码里面加入了窗口关闭时还原空间的代码,如果你正常关闭窗口,那么停靠窗口占用的空间就会及时还原回来。

当然,你也可以适时调用下面的代码:

DesktopAppBar.SetAppBar(this, AppBarEdge.None);

附源码

由于源码一直在持续改进,所以本文中贴的源代码可能不是最新的。你可以在以下仓库找到这段源码的最新版本:

using System;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Interop;

// ReSharper disable IdentifierTypo
// ReSharper disable InconsistentNaming
// ReSharper disable EnumUnderlyingTypeIsInt
// ReSharper disable MemberCanBePrivate.Local
// ReSharper disable UnusedMember.Local
// ReSharper disable UnusedMember.Global

namespace Walterlv.Demo.DesktopDocking
{
    /// <summary>
    /// 表示窗口停靠到桌面上时的边缘方向。
    /// </summary>
    public enum AppBarEdge
    {
        /// <summary>
        /// 窗口停靠到桌面的左边。
        /// </summary>
        Left = 0,

        /// <summary>
        /// 窗口停靠到桌面的顶部。
        /// </summary>
        Top,

        /// <summary>
        /// 窗口停靠到桌面的右边。
        /// </summary>
        Right,

        /// <summary>
        /// 窗口停靠到桌面的底部。
        /// </summary>
        Bottom,

        /// <summary>
        /// 窗口不停靠到任何方向,而是成为一个普通窗口占用剩余的可用空间(工作区)。
        /// </summary>
        None
    }

    /// <summary>
    /// 提供将窗口停靠到桌面某个方向的能力。
    /// </summary>
    public class DesktopAppBar
    {
        /// <summary>
        /// 标识 Window.AppBar 的附加属性。
        /// </summary>
        public static readonly DependencyProperty AppBarProperty = DependencyProperty.RegisterAttached(
            "AppBar", typeof(AppBarEdge), typeof(DesktopAppBar),
            new PropertyMetadata(AppBarEdge.None, OnAppBarEdgeChanged));

        /// <summary>
        /// 获取 <paramref name="window"/> 当前的停靠边缘。
        /// </summary>
        /// <param name="window">要获取停靠边缘的窗口。</param>
        /// <returns>停靠边缘。</returns>
        public static AppBarEdge GetAppBar(Window window) => (AppBarEdge)window.GetValue(AppBarProperty);

        /// <summary>
        /// 设置 <paramref name="window"/> 的停靠边缘方向。
        /// </summary>
        /// <param name="window">要设置停靠的窗口。</param>
        /// <param name="value">要设置的停靠边缘方向。</param>
        public static void SetAppBar(Window window, AppBarEdge value) => window.SetValue(AppBarProperty, value);

        private static readonly DependencyProperty AppBarProcessorProperty = DependencyProperty.RegisterAttached(
            "AppBarProcessor", typeof(AppBarWindowProcessor), typeof(DesktopAppBar), new PropertyMetadata(null));

        [SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalse")]
        private static void OnAppBarEdgeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (DesignerProperties.GetIsInDesignMode(d))
            {
                return;
            }

            var oldValue = (AppBarEdge) e.OldValue;
            var newValue = (AppBarEdge) e.NewValue;
            var oldEnabled = oldValue is AppBarEdge.Left
                             || oldValue is AppBarEdge.Top
                             || oldValue is AppBarEdge.Right
                             || oldValue is AppBarEdge.Bottom;
            var newEnabled = newValue is AppBarEdge.Left
                             || newValue is AppBarEdge.Top
                             || newValue is AppBarEdge.Right
                             || newValue is AppBarEdge.Bottom;
            if (oldEnabled && !newEnabled)
            {
                var processor = (AppBarWindowProcessor) d.GetValue(AppBarProcessorProperty);
                processor.Detach();
            }
            else if (!oldEnabled && newEnabled)
            {
                var processor = new AppBarWindowProcessor((Window) d);
                d.SetValue(AppBarProcessorProperty, processor);
                processor.Attach(newValue);
            }
            else if (oldEnabled && newEnabled)
            {
                var processor = (AppBarWindowProcessor) d.GetValue(AppBarProcessorProperty);
                processor.Update(newValue);
            }
        }

        /// <summary>
        /// 包含对 <see cref="Window"/> 进行操作以便使其成为一个桌面停靠窗口的能力。
        /// </summary>
        private class AppBarWindowProcessor
        {
            /// <summary>
            /// 创建 <see cref="AppBarWindowProcessor"/> 的新实例。
            /// </summary>
            /// <param name="window">需要成为停靠窗口的 <see cref="Window"/> 的实例。</param>
            public AppBarWindowProcessor(Window window)
            {
                _window = window;
                _callbackId = RegisterWindowMessage("AppBarMessage");
                _hwndSourceTask = new TaskCompletionSource<HwndSource>();

                var source = (HwndSource) PresentationSource.FromVisual(window);
                if (source == null)
                {
                    window.SourceInitialized += OnSourceInitialized;
                }
                else
                {
                    _hwndSourceTask.SetResult(source);
                }

                _window.Closed += OnClosed;
            }

            private readonly Window _window;
            private readonly TaskCompletionSource<HwndSource> _hwndSourceTask;
            private readonly int _callbackId;

            private WindowStyle _restoreStyle;
            private Rect _restoreBounds;
            private ResizeMode _restoreResizeMode;
            private bool _restoreTopmost;

            private AppBarEdge Edge { get; set; }

            /// <summary>
            /// 在可以获取到窗口句柄的时候,给窗口句柄设置值。
            /// </summary>
            private void OnSourceInitialized(object sender, EventArgs e)
            {
                _window.SourceInitialized -= OnSourceInitialized;
                var source = (HwndSource) PresentationSource.FromVisual(_window);
                _hwndSourceTask.SetResult(source);
            }

            /// <summary>
            /// 在窗口关闭之后,需要恢复窗口设置过的停靠属性。
            /// </summary>
            private void OnClosed(object sender, EventArgs e)
            {
                _window.Closed -= OnClosed;
                _window.ClearValue(AppBarProperty);
            }

            /// <summary>
            /// 将窗口属性设置为停靠所需的属性。
            /// </summary>
            private void ForceWindowProperties()
            {
                _window.WindowStyle = WindowStyle.None;
                _window.ResizeMode = ResizeMode.NoResize;
                _window.Topmost = true;
            }

            /// <summary>
            /// 备份窗口在成为停靠窗口之前的属性。
            /// </summary>
            private void BackupWindowProperties()
            {
                _restoreStyle = _window.WindowStyle;
                _restoreBounds = _window.RestoreBounds;
                _restoreResizeMode = _window.ResizeMode;
                _restoreTopmost = _window.Topmost;
            }

            /// <summary>
            /// 使一个窗口开始成为桌面停靠窗口,并开始处理窗口停靠消息。
            /// </summary>
            /// <param name="value">停靠方向。</param>
            public async void Attach(AppBarEdge value)
            {
                var hwndSource = await _hwndSourceTask.Task;

                BackupWindowProperties();

                var data = new APPBARDATA();
                data.cbSize = Marshal.SizeOf(data);
                data.hWnd = hwndSource.Handle;

                data.uCallbackMessage = _callbackId;
                SHAppBarMessage((int) ABMsg.ABM_NEW, ref data);
                hwndSource.AddHook(WndProc);

                Update(value);
            }

            /// <summary>
            /// 更新一个窗口的停靠方向。
            /// </summary>
            /// <param name="value">停靠方向。</param>
            public async void Update(AppBarEdge value)
            {
                var hwndSource = await _hwndSourceTask.Task;

                Edge = value;


                var bounds = TransformToAppBar(hwndSource.Handle, _window.RestoreBounds, value);
                ForceWindowProperties();
                Resize(_window, bounds);
            }

            /// <summary>
            /// 使一个窗口从桌面停靠窗口恢复成普通窗口。
            /// </summary>
            public async void Detach()
            {
                var hwndSource = await _hwndSourceTask.Task;

                var data = new APPBARDATA();
                data.cbSize = Marshal.SizeOf(data);
                data.hWnd = hwndSource.Handle;

                SHAppBarMessage((int) ABMsg.ABM_REMOVE, ref data);

                _window.WindowStyle = _restoreStyle;
                _window.ResizeMode = _restoreResizeMode;
                _window.Topmost = _restoreTopmost;

                Resize(_window, _restoreBounds);
            }

            private IntPtr WndProc(IntPtr hwnd, int msg,
                IntPtr wParam, IntPtr lParam, ref bool handled)
            {
                if (msg == _callbackId)
                {
                    if (wParam.ToInt32() == (int) ABNotify.ABN_POSCHANGED)
                    {
                        var hwndSource = _hwndSourceTask.Task.Result;
                        var bounds = TransformToAppBar(hwndSource.Handle, _window.RestoreBounds, Edge);
                        Resize(_window, bounds);
                        handled = true;
                    }
                }

                return IntPtr.Zero;
            }

            private static void Resize(Window window, Rect bounds)
            {
                window.Left = bounds.Left;
                window.Top = bounds.Top;
                window.Width = bounds.Width;
                window.Height = bounds.Height;
            }

            private Rect TransformToAppBar(IntPtr hWnd, Rect area, AppBarEdge edge)
            {
                var data = new APPBARDATA();
                data.cbSize = Marshal.SizeOf(data);
                data.hWnd = hWnd;
                data.uEdge = (int) edge;

                if (data.uEdge == (int) AppBarEdge.Left || data.uEdge == (int) AppBarEdge.Right)
                {
                    data.rc.top = 0;
                    data.rc.bottom = (int) SystemParameters.PrimaryScreenHeight;
                    if (data.uEdge == (int) AppBarEdge.Left)
                    {
                        data.rc.left = 0;
                        data.rc.right = (int) Math.Round(area.Width);
                    }
                    else
                    {
                        data.rc.right = (int) SystemParameters.PrimaryScreenWidth;
                        data.rc.left = data.rc.right - (int) Math.Round(area.Width);
                    }
                }
                else
                {
                    data.rc.left = 0;
                    data.rc.right = (int) SystemParameters.PrimaryScreenWidth;
                    if (data.uEdge == (int) AppBarEdge.Top)
                    {
                        data.rc.top = 0;
                        data.rc.bottom = (int) Math.Round(area.Height);
                    }
                    else
                    {
                        data.rc.bottom = (int) SystemParameters.PrimaryScreenHeight;
                        data.rc.top = data.rc.bottom - (int) Math.Round(area.Height);
                    }
                }

                SHAppBarMessage((int) ABMsg.ABM_QUERYPOS, ref data);
                SHAppBarMessage((int) ABMsg.ABM_SETPOS, ref data);

                return new Rect(data.rc.left, data.rc.top,
                    data.rc.right - data.rc.left, data.rc.bottom - data.rc.top);
            }

            [StructLayout(LayoutKind.Sequential)]
            private struct RECT
            {
                public int left;
                public int top;
                public int right;
                public int bottom;
            }

            [StructLayout(LayoutKind.Sequential)]
            private struct APPBARDATA
            {
                public int cbSize;
                public IntPtr hWnd;
                public int uCallbackMessage;
                public int uEdge;
                public RECT rc;
                public readonly IntPtr lParam;
            }

            private enum ABMsg : int
            {
                ABM_NEW = 0,
                ABM_REMOVE,
                ABM_QUERYPOS,
                ABM_SETPOS,
                ABM_GETSTATE,
                ABM_GETTASKBARPOS,
                ABM_ACTIVATE,
                ABM_GETAUTOHIDEBAR,
                ABM_SETAUTOHIDEBAR,
                ABM_WINDOWPOSCHANGED,
                ABM_SETSTATE
            }

            private enum ABNotify : int
            {
                ABN_STATECHANGE = 0,
                ABN_POSCHANGED,
                ABN_FULLSCREENAPP,
                ABN_WINDOWARRANGE
            }

            [DllImport("SHELL32", CallingConvention = CallingConvention.StdCall)]
            private static extern uint SHAppBarMessage(int dwMessage, ref APPBARDATA pData);

            [DllImport("User32.dll", CharSet = CharSet.Auto)]
            private static extern int RegisterWindowMessage(string msg);
        }
    }
}

参考资料

在整个 Git 仓库的历史(包括所有分支和标签)中修改提交作者的信息(姓名和邮箱)

dotnet 职业技术学院 发布于 2019-05-23

一般情况下不建议修改 git 仓库的历史。

但是现在我计划开源我的一个项目,于是自己个人使用的姓名和邮箱就需要在开源的时候改为使用我公开的姓名和邮箱。对于旧仓库,我将废弃,将来所有的精力都将在开源版本的仓库中;而对于开源版本的新仓库,由于此前没有人克隆过,所以也不会因为历史的修改产生问题。所以,我可以很放心地更改全部的 git 仓库历史。


我打算将整个 Git 仓库历史中的名称和邮箱。

第一步:打开 Git Bash

进入本地的 Git 仓库目录,然后打开 Git Bash。

第二步:输入 Git 命令

接下来,我们需要输入一段多行命令。请先复制以下命令到你的临时编辑器中,然后修改这段多行命令中的几个变量的值。

多行命令:

git filter-branch --env-filter '

OLD_EMAIL="your-old-email@example.com"
CORRECT_NAME="Your Correct Name"
CORRECT_EMAIL="your-correct-email@example.com"

if [ "$GIT_COMMITTER_EMAIL" = "$OLD_EMAIL" ]
then
    export GIT_COMMITTER_NAME="$CORRECT_NAME"
    export GIT_COMMITTER_EMAIL="$CORRECT_EMAIL"
fi
if [ "$GIT_AUTHOR_EMAIL" = "$OLD_EMAIL" ]
then
    export GIT_AUTHOR_NAME="$CORRECT_NAME"
    export GIT_AUTHOR_EMAIL="$CORRECT_EMAIL"
fi
' --tag-name-filter cat -- --branches --tags

请注意上面那几个变量:

  • OLD_EMAIL 修改为你的旧邮箱(也就是需要替换掉的 Git 历史中的邮箱)
  • CORRECT_NAME 修改为你的新名称
  • CORRECT_EMAIL 修改为你的新邮箱

对我来说,新名称也就是我在 GitHub 上的名称 walterlv,新邮箱也就是我在 GitHub 上公开使用的提交邮箱。

将以上修改后的命令粘贴到 Git Bash 中,然后按下回车键执行命令:

执行命令

等待命令执行结束,你就能看到你的仓库中所有的分支(Branches)、所有的标签(Tags)中的旧作者信息全部被替换为了新作者信息了。

执行结果

第三步:推送仓库

如果你只是准备开源这个仓库,还没开始推送,那么直接推送即可。使用以下命令推送所有的分支和所有的标签。

git push --force --tags origin 'refs/heads/*'

如果你已经将仓库推送出去了,那么就需要强制推送来覆盖远端的仓库。使用以下命令推送所有的分支和所有的标签。

git push --tags origin 'refs/heads/*'

参考资料

.NET/C# 使用 ConditionalWeakTable 附加字段(CLR 版本的附加属性,也可用用来当作弱引用字典 WeakDictionary)

dotnet 职业技术学院 发布于 2019-05-23

如果你使用过 WPF/UWP 等 XAML UI 框架,那么应该了解到附加属性的概念。那么没有依赖属性支持的时候如何做附加属性的功能呢?你可能会想到弱引用。但这需要做一个弱引用字典,要写的代码还是非常麻烦的。

本文介绍 .NET 的 ConditionalWeakTable<TKey,TValue> 类型,适用于 .NET Framework 4.0 以上和全部 .NET Core 的版本。


这不是字典

现成可用的弱引用字典,即 ConditionalWeakTable<TKey,TValue>。然而实际上这个类的原本作用并不是当作字典使用!

如果你使用过 WPF/UWP 等 XAML UI 框架,那么应该了解到附加属性的概念。这其实是 .NET 为我们提供的一种附加字段的机制。

比如你有一个类:

class Foo
{
    // 请忽略这里公有字段带来的设计问题,只是为了演示。
    public string A;
}

我们希望为它增加一个字段 Bar

class Foo
{
    public string A;
    public Bar Bar;
}

那么我们需要修改类 Foo 本身以实现这个效果;但是这样就使得 Foo 耦合了 Bar,从而破坏了内聚性/依赖倒置原则。典型的情况是 Foo 类表示一个人 Person,它里面不应该包含一个 某行账号 这样的字段,因为很多人是没有那家银行账号的。这个信息让那家银行存起来才是比较符合设计原则的设计。

我们可以通过一个字典 Dictionary<Foo, Bar> 来存储所有 Foo 实例额外增加的 Bar 的值可以避免让 Foo 类中增加 Bar 字段从而获得更好的设计。但这样就引入了一个静态字典从而使得所有的 FooBar 的实例无法得到释放。我们想当然希望拥有一个弱引用字典来解决问题。然而这是一个 X-Y 问题

实际上 .NET 中提供了 ConditionalWeakTable<TKey,TValue> 帮我们解决了最本质的问题——在部分场景下期望为 Foo 类添加一个字段。虽然它不是弱引用字典,但能解决此类问题,同时也能当作一个弱引用字典来使用,仅此而已。

你需要注意的是,ConditionalWeakTable<TKey,TValue> 并不实现 IDictionary<TKey,TValue> 接口,只是里面有一些像 IDictionary<TKey, TValue> 的方法,可以当作字典使用,也可以遍历取出剩下的所有值。

验证

ConditionalWeakTable<TKey,TValue> 中的所有 Key 和所有的 Value 都是弱引用的,并且会在其 Key 被回收或者 Key 和 Value 都被回收之后自动从集合中消失。这意味着当你使用它来为一个类型附加一些字段或者属性的时候完全不用担心内存泄漏的问题。

下面我写了一段代码用于验证其内存泄漏问题:

  1. ConditionalWeakTable<TKey,TValue> 中添加了三个键值对;
  2. 将后两个的 key 设为 null
  3. 进行垃圾回收。
using System;
using System.Linq;
using System.Runtime.CompilerServices;

namespace Walterlv.Demo.Weak
{
    class Program
    {
        public static void Main()
        {
            var key1 = new Key("Key1");
            var key2 = new Key("Key2");
            var key3 = new Key("Key3");

            var table = new ConditionalWeakTable<Key, WalterlvValue>
            {
                {key1, new WalterlvValue()},
                {key2, new WalterlvValue()},
                {key3, new WalterlvValue()}
            };

            var weak2 = new WeakReference(key2);
            key2 = null;
            key3 = null;

            GC.Collect();

            Console.WriteLine($@"key1 = {key1?.ToString() ?? "null"}
key2 = {key2?.ToString() ?? "null"}, weak2 = {weak2.Target ?? "null"}
key3 = {key3?.ToString() ?? "null"}
Table = {{{string.Join(", ", table.Select(x => $"{x.Key} = {x.Value}"))}}}");
        }
    }

    public class Key
    {
        private readonly string _name;
        public Key(string name) => _name = name;
        public override string ToString() => _name;
    }

    public class WalterlvValue
    {
        public DateTime CreationTime = DateTime.Now;
        public override string ToString() => CreationTime.ToShortTimeString();
    }
}

这段代码的运行结果如下图:

运行结果

从中我们可以发现:

  1. 当某个 Key 被回收后,ConditionalWeakTable<TKey,TValue> 中就没有那一项键值对了;
  2. 当 Key 的实例依然在的时候,ConditionalWeakTable<TKey,TValue> 中的 Value 依然还会存在。

另外,我们这里在调查内存泄漏问题,你需要在 Release 配置下执行此代码才能得到最符合预期的结果。


参考资料

WPF 判断一个对象是否是设计时的窗口类型,而不是运行时的窗口

dotnet 职业技术学院 发布于 2019-05-22

当我们对 Window 类型写一个附加属性的时候,在属性变更通知中我们需要判断依赖对象是否是一个窗口。但是,如果直接判断是否是 Window 类型,那么在设计器中这个属性的设置就会直接出现异常。

那么有没有什么方法能够得知这是一个设计时的窗口呢?这样就不会抛出异常,而能够完美支持设计器了。


方法一:判断设计时属性

WPF 原生自带一个附加属性可以判断一个依赖对象是否来源于设计器。而这个属性就是 DesignerProperties.IsInDesignMode

在 WPF 的设计器中,这个属性会被设计器重写元数据,指定其值为 true,而其他默认的情况下,它的默认值都是 false

所以通过判断这个值可以得知此时是否是在设计器中使用此附加属性。

if (DesignerProperties.GetIsInDesignMode(d))
{
    // 通常我们考虑在设计器中不做额外的任何事情是最偷懒不会出问题的代码了。
    return;
}

我在这些博客中使用过这样的判断方法,可以参见源码:

方法二:判断设计时窗口

上面的方法是个通用的判断设计器中的方法。不过,如果我们希望得到更多的设计器支持,而不是像上面那样直接 return 导致此属性在设计器中一点效果都没有的话,我们需要进行更精确的判断。

然而设计器中的类型我们不能直接引用到,所以可以考虑进行类型名称判断的方式。类型名称判断的方式会与 Visual Studio 的版本相关,所以实际上代码并不怎么好看。

我将判断方法整理如下:

public static class WalterlvDesignTime
{
    /// <summary>
    /// 判断一个依赖对象是否是设计时的 <see cref="Window"/>。
    /// </summary>
    /// <param name="window">要被判断设计时的 <see cref="Window"/> 对象。</param>
    /// <returns>如果对象是设计时的 <see cref="Window"/>,则返回 true,否则返回 false。</returns>
    private static bool IsDesignTimeWindow(DependencyObject window)
    {
        const string vs201920172015Window =
            "Microsoft.VisualStudio.DesignTools.WpfDesigner.InstanceBuilders.WindowInstance";
        const string vs2013Window = "Microsoft.Expression.WpfPlatform.InstanceBuilders.WindowInstance";

        if (DesignerProperties.GetIsInDesignMode(window))
        {
            var typeName = window.GetType().FullName;
            if (Equals(vs201920172015Window, typeName) || Equals(vs2013Window, typeName))
            {
                return true;
            }
        }

        return false;
    }
}

于是,只需要调用一下这个方法即可得到此窗口实例是否是设计时的窗口:

if (WalterlvDesignTime.IsDesignTimeWindow(d))
{
    // 检测到如果是设计时的窗口,就跳过一些句柄等等一些真的需要一个窗口的代码调用。
}
else if (d is Window)
{
    // 检测到真的是窗口,做一些真实窗口初始化需要做的事情。
}
else
{
    // 这不是一个窗口,需要抛出异常。
}

通过修改环境变量修改当前进程使用的系统 Temp 文件夹的路径

dotnet 职业技术学院 发布于 2019-05-16

Windows 系统提供了一个在 Windows 单个用户下全局的 Temp 文件夹,用于给各种不同的应用程序提供一个临时目录。但是,直到 Windows 10 推出存储感知功能之前,这个文件夹都一直只归各个应用程序自己管理,应用自己需要删除里面的文件。另外,进程多了,临时文件也会互相影响(例如个数过多、进程读写竞争等等)。

本文介绍将自己当前进程的 Temp 文件夹临时修改到应用程序自己的一个临时目录下,避免与其他程序之间的各种影响,同时也比较容易自行清理。


如何修改 Temp 文件夹的路径

在程序启动的时候,调用如下方法:

var newTempFolder = @"C:\Walterlv\ApplicationTemp";
Environment.SetEnvironmentVariable("TEMP", newTempFolder);
Environment.SetEnvironmentVariable("TMP", newTempFolder);

这样,可以将当前进程的临时文件夹设置到 C:\Walterlv\ApplicationTemp 文件夹下。

上面设置了两个环境变量,实际上 .NET Framework 中主要使用的临时文件夹环境变量是 TMP 那个。

使用临时文件夹中的临时文件

使用 Path.GetTempPath() 可以获取临时文件夹的路径:

var tempPath = Path.GetTempPath();

使用 Path.GetTempFileName() 可以生成一个唯一的临时文件文件名:

var tempPath = Path.GetTempFileName();

不过,使用此方法需要注意,这要求临时文件夹必须存在。如果你使用了前面的方法修改了临时文件夹的地址,请务必确保文件夹存在。

扩展阅读

如果使用 Path.GetTempFileName() 方法创建的临时文件数量达到了 65535 个,而又不及时删除掉创建的文件的话,那么再调用此方法将抛出异常 IOException

需要注意的是,此 API 调用创建的文件数量是当前用户账户下所有程序共同累计的,其他程序用“满”了你的进程也一样会挂。当然,如果你使用的不是 .NET 的 API,而是使用原生 Win32 API,那么你可以指定临时文件名前缀,相同临时文件名前缀的程序会累计数量。而 .NET 中此 API 使用的是 tmp 前缀,所以所有的 .NET 程序会共享这 65535 个文件累计;其他程序使用其他前缀使则分别累计。

另外,如果此方法无法再生成一个唯一的文件名的时候也会抛出异常。

为了解决这些异常,在用户端的解决方案是删除临时文件夹。而在程序端的解决方案是 —— 本文。

本文是为了和 林德熙 一起解决一个光标问题时提出的解决方案的一种。更多关于光标问题的内容可以阅读以下链接:


参考资料

Markdown 如何在内联代码或者代码块中使用代码开始符号反引号(`)

dotnet 职业技术学院 发布于 2019-05-15

我们都知道如何在 Markdown 中使用反引号 ` 来包裹一段代码。无论是内联的代码还是单独的代码块,都需要使用它,只是个数的差别,比如 ````

那么如何能够在代码片中输入反引号(backtick)呢?


方法是:用两个反引号来包裹

内联代码中包含反引号

例如,你想输入这段代码中包含`符号,那么你应该这么输入:

``这段代码中包含`符号``

内联代码中只有反引号

例如,你希望输入`,那么你应该这么输入:

`` ` ``

注意,这里有 5 个 ` 符号,其中前后各两个 `` 是代码块的开始和结束符,中间的 ` 则是代码块中的 ` 符号,代码块和内容之间必须有空格。

内联代码中只有反引号且有多个

如果你读到上面一节,你可能好奇为什么我能打出两个 `` 符号来,是因为我输入了:

``` `` ```

注意,这里有 8 个 ` 符号,其中前后各两个 ``` 是代码块的开始和结束符,中间的 `` 则是代码块中的 `` 符号,代码块和内容之间必须有空格。

更多个,则以此类推。

内联代码中首尾包含反引号

有时候你希望示意 Markdown 的代码块的用法,你需要告诉别人使用 `<code>` 这样的写法。那么,你可以输入:

`` `<code>` ``

由于 ` 符号就在内容的开始和结尾,所以 `` 的开头和结尾也是需要输入一个空格的。

代码块中的反引号

只要代码块中的反引号数量小于三个,就能直接在代码块中使用反引号而不用担心转义问题:

`
``

但是,如果反引号的数量大于或等于三个,那么代码块的包裹就需要更多的反引号了:

```` 四个反引号开始的代码块
` 有一个
`` 有两个
``` 有三个
```` 四个反引号结束的代码块

反正,包括代码块的反引号可以一直重复,比里面用到的多就好了。


参考资料

在编译期间使用 Roslyn/MSBuild 自带的方法/函数判断、计算和修改属性

dotnet 职业技术学院 发布于 2019-05-15

充分利用 MSBuild 自带的方法,可以在编译期间完成大多数常见的属性转换,而不再需要自己专门写库来完成。

本文介绍如何使用 MSBuild 自带的方法,并列举 MSBuild 中各种自带的方法。


如何在编译期间使用 MSBuild 自带的方法

当然,在修改编译期间的代码的时候,你可能需要提前了解项目文件相关的知识:

以下是使用 MSBuild 自带方法的最简单的一个例子,执行 5-1 的数学运算。

<Walterlv>$([MSBuild]::Subtract(5, 1))</Walterlv>

更复杂的,可能是 MSBuild 方法调用的嵌套了:

<WalterlvPath Condition="HasTrailingSlash('$(WalterlvPath)')">$(WalterlvPath.Substring(0, $([MSBuild]::Add($(WalterlvPath.Length), -1))))</WalterlvPath>

以上两段示例分别来自我的另外两篇博客,如果不明白,可以参考这两篇博客的内容:

MSBuild 自带的方法

数学运算

MSBuild 中数学运算的部分可以参考我的另一篇博客:

EnsureTrailingSlash

确保路径结尾有斜杠。

可参考我的另一篇博客:

GetDirectoryNameOfFileAbove & GetPathOfFileAbove

这两个是非常有用却又非常容易被忽视的 API,非常有必要介绍一下。

可以阅读我的另一篇博客了解其用途和用法:

MakeRelative

计算两个路径之间的相对路径表示。

<PropertyGroup>
    <Path1>C:\Walterlv\</Path1>
    <Path2>C:\Walterlv\Demo\</Path2>
    <WalterlvPath1>$([MSBuild]::MakeRelative($(Path1), $(Path2)))</WalterlvPath1>
    <WalterlvPath2>$([MSBuild]::MakeRelative($(Path2), $(Path1)))</WalterlvPath2>
</PropertyGroup>

WalterlvPath1 的值会计算为 Demo\,而 WalterlvPath2 的值会计算为 ..\

ValueOrDefault

如果赋值了,就使用所赋的值;否则使用参数指定的值:

<PropertyGroup>
    <WalterlvValue1>$([MSBuild]::ValueOrDefault('$(FooBar)', 'walterlv'))</WalterlvValue1>
    <WalterlvValue2>$([MSBuild]::ValueOrDefault('$(WalterlvValue1)', 'lindexi'))</WalterlvValue2>
</PropertyGroup>

第一行,因为我们没有定义任何一个名为 FooBar 的属性,所以 WalterlvValue1 属性会计算得到 walterlv 值。第二行,因为 WalterlvValue1 已经得到了一个值,所以 WalterlvValue2 也会得到 WalterlvValue1 的值,也就是 walterlv,不会得到 lindexi

其他

MSBuild 剩下的一些方法使用场景非常有限(不懂就别瞎装懂了),这里做一些简单的介绍。


参考资料