dotnet 职业技术学院

博客

dotnet 职业技术学院

C# 8.0 可空引用类型中的各项警告/错误的含义和示例代码

dotnet 职业技术学院 发布于 2019-11-24

C# 8.0 引入了可为空引用类型和不可为空引用类型。当你需要给你或者团队更严格的要求时,可能需要定义这部分的警告和错误级别。

本文将介绍 C# 可空引用类型部分的警告和错误提示,便于进行个人项目或者团队项目的配置。


开启可空引用类型以及配置警告和错误

本文的内容本身没什么意义,但如果你试图进行一些团队配置,那么本文的示例可能能带来一些帮助。

警告和错误

CS8600

将 null 文本或可能的 null 值转换为非 null 类型。

string walterlv = null;

CS8600

CS8601

可能的 null 引用赋值。

string Text { get; set; }

void Foo(string? text)
{
    // 将可能为 null 的文本向不可为 null 的类型赋值。
    Text = text;
}

CS8602

null 引用可能的取消引用。

// 当编译器判定 walterlv 可能为 null 时才会有此警告。
var value = walterlv.ToString();

CS8602

CS8603

可能的 null 引用返回。

string Foo()
{
    return null;
}

CS8603

CS8604

将可能为 null 的引用作为参数传递到不可为 null 的方法中:

void Foo()
{
    string text = GetText();;
    Bar(text);
}

string? GetText()
{
    return null;
}

CS8609

返回类型中引用类型的为 Null 性与重写成员不匹配。

比如你的基类中返回值不允许为 null,但是实现中返回值却允许为 null。

protected virtual async Task<string> FooAsync()
{
}
protected override async Task<string?> FooAsync()
{
}

CS8610

参数中引用类型的为 Null 性与重写成员不匹配。

比如你的基类中方法参数值不允许为 null,但是实现中方法参数却允许为 null。

protected virtual void FooAsync(string value)
{
}
protected override void FooAsync(string? value)
{
}

CS8616

接口中定义的成员中的 null 性与实现中成员的 null 型不匹配。

比如你的接口中不允许为 null,但是实现中却允许为 null。

CS8618

未初始化不可以为 null 的字段 “_walterlv”。

如果一个类型中存在不可以为 null 的字段,那么需要在构造函数中初始化,如果没有初始化,则会发出警告或者异常。

CS8619

一个类型与构造这个类型的 null 性不匹配。

例如:

Task<object?> foo = new Task<object>(() => new object());

CS8622

委托定义的参数中引用类型的为 null 性与目标委托不匹配。

比如你定义了一个委托:

void Foo(object? sender, EventArgs e);

然而在订阅事件的时候,使用的函数 null 性不匹配,则会出现警告:

void OnFoo(object sender, EventArgs e)
{
    // 注意到这里的 object 本应该写作 object?
}

CS8625

无法将 null 文本转换为非 null 引用或无约束类型参数。

void Foo(string walterlv = null)
{
}

CS8625

CS8653

对于泛型 T,使用 default 设置其值。如果 T 是引用类型,那么 default 就会将这个泛型类型赋值为 null。然而并没有将泛型 T 的使用写为 T?。

发现电脑屏幕总是不自动关闭?看看你是否打开了这些程序……

dotnet 职业技术学院 发布于 2019-11-21

本文收集一些已知的导致电脑屏幕不关闭的程序。如果你发现无论你设置多短的屏幕关闭超时时间但一直都不关闭,那么可以参考本文检查是否打开了这些程序。


电源和睡眠

先检查一下你系统设置中的电源和睡眠选项,时间不应该太长。一定要先看看这里,别到时候折腾了半天发现是自己设错了就亏了……

电源和睡眠

另外,找程序的时候,不要第一眼看过去没有就忽略它了。因为你可能像我一样有很多个桌面。最好还是用任务管理器找,不会漏掉。

多个桌面

大多数游戏

如果你有游戏没关,你第一个就应该怀疑它!

我不想把我正在玩的游戏列举出来,因为容易过时还会暴露些什么……

应用

  • Microsoft PowerPoint 在演示模式下
  • 金山 WPS 演示 在演示模式下
  • Wallpaper Engine

工具

  • GPU-Z 只要打开就会

为什么我想整理这份名单

因为我总是时不时发现某一天电脑屏幕一直亮着。到了晚上很刺眼的,而且费电……

所以,每发现一个就补充一个好了。如果你有已知的,麻烦在评论区告诉我哟!如果看不到评论区,可以前往这里评论,或者给我发邮件

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

dotnet 职业技术学院 发布于 2019-11-20

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

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


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

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

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

借助 cmd.exe 来启动的方法可以参见我的另一篇博客:

那么我们如何能够借助于 PowerShell 或者 PowerShell 来启动它呢?

PowerShell 的帮助文档

先打开一个 PowerShell。

对于 Windows 自带的基于 .NET Framework 的 PowerShell,使用 powershell 命令可以直接启动 PowerShell。对于基于 .NET Core 版本的 PowerShell Core,使用 pwsh 命令可以直接启动。

关于 .NET Core 版本的 PowerShell Core 可以参见我的另一篇博客:

接下来输入下面三个命令中的任何一个:

  • PowerShell -Help
  • PowerShell -?
  • PowerShell /?

或者对于 PowerShell Core 来说,是下面三个命令中的任何一个:

  • pwsh -Help
  • pwsh -?
  • pwsh /?

你就可以看到 PowerShell 的使用说明:

PowerShell 的使用说明

PowerShell 的启动参数示例

使用 PowerShell 间接启动一个程序并传入参数

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

> pwsh -Command "D:\walterlv\frpc.exe -c ./frpc.ini"

或者简写为:

> pwsh -c "D:\walterlv\frpc.exe -c ./frpc.ini"

实际上使用 PowerShell 来做这些事情简直是用牛刀杀鸡,因为本身 PowerShell 非常强大。我们只是因为一些程序的限制不得不使用这样的方案来启动程序而已。

比如其中之一,执行脚本。

使用 PowerShell 执行命令/脚本后保留窗口不退出

需要加上 -NoExit 参数。

> pwsh -NoExit -c "D:\walterlv\frpc.exe -c ./frpc.ini"

一定要注意,-c 和后面的命令必须放到最末尾,因为 -c 后面的所有字符串都会被解析为需要执行的命令。

使用 PowerShell 执行多条命令/脚本

多条脚本之间使用 ; 作为分隔:

> pwsh -c "D:\walterlv\frpc.exe -c ./frpc.ini"; "D:\walterlv\frps.exe -c ./frps.ini"

如果引号里面还需要写引号,则可以把里面的引号改成单引号 ' 或者把外面的引号改为单引号 '

使用 PowerShell 间接执行一个脚本

# Execute a PowerShell Command in a session
PowerShell -Command "Get-EventLog -LogName security"

# Run a script block in a session
PowerShell -Command {Get-EventLog -LogName security}

# An alternate way to run a command in a new session
PowerShell -Command "& {Get-EventLog -LogName security}"

附 PowerShell 的全部启动参数说明

PowerShell[.exe] [-PSConsoleFile <文件> | -Version <版本>]
    [-NoLogo] [-NoExit] [-Sta] [-Mta] [-NoProfile] [-NonInteractive]
    [-InputFormat {Text | XML}] [-OutputFormat {Text | XML}]
    [-WindowStyle <样式>] [-EncodedCommand <Base64 编码命令>]
    [-ConfigurationName <字符串>]
    [-File <文件路径> <参数>] [-ExecutionPolicy <执行策略>]
    [-Command { - | <脚本块> [-args <参数数组>]
                  | <字符串> [<命令参数>] } ]

PowerShell[.exe] -Help | -? | /?

-PSConsoleFile
    加载指定的 Windows PowerShell 控制台文件。若要创建控制台
    文件,请在 Windows PowerShell 中使用 Export-Console。

-Version
    启动指定版本的 Windows PowerShell。
    使用参数输入版本号,如 "-version 2.0"。

-NoLogo
    启动时隐藏版权标志。

-NoExit
    运行启动命令后不退出。

-Sta
    使用单线程单元启动 shell。
    单线程单元(STA)是默认值。

-Mta
    使用多线程单元启动 shell。

-NoProfile
    不加载 Windows PowerShell 配置文件。

-NonInteractive
    不向用户显示交互式提示。

-InputFormat
    描述发送到 Windows PowerShell 的数据的格式。有效值为
    "Text" (文本字符串)或 "XML" (序列化的 CLIXML 格式)。

-OutputFormat
    确定如何设置 Windows PowerShell 输出内容的格式。有效值
    为 "Text" (文本字符串)或 "XML" (序列化的 CLIXML 格式)。

-WindowStyle
    将窗口样式设置为 Normal、Minimized、Maximized 或 Hidden。

-EncodedCommand
    接受 base-64 编码字符串版本的命令。使用此参数
    向 Windows PowerShell 提交需要复杂引号
    或大括号的命令。

-ConfigurationName
    指定运行 Windows PowerShell 的配置终结点。
    该终结点可以是在本地计算机上注册的任何终结点,包括
    默认的 Windows PowerShell 远程处理终结点或具有特定用户角色功能
    的自定义终结点。

-File
    在本地作用域("dot-sourced")中运行指定的脚本,以便
    脚本创建的函数和变量可以在当前
    会话中使用。输入脚本文件路径和任何参数。
    File 必须是命令中的最后一个参数,因为在 File 参数
    名称后面键入的所有字符都将解释
    为后跟脚本参数的脚本文件路径。

-ExecutionPolicy
    设置当前会话的默认执行策略,并将其保存
    在 $env:PSExecutionPolicyPreference 环境变量中。
    该参数不会更改在注册表中
    设置的 Windows PowerShell 执行策略。

-Command
    执行指定的命令(和任何参数),就好像它们是
    在 Windows PowerShell 命令提示符下键入的一样,然后退出,除非
    指定了 NoExit。Command 的值可以为 "-"、字符串或
    脚本块。

    如果 Command 的值为 "-",则从标准输入中读取
    命令文本。

    如果 Command 的值为脚本块,则脚本块必须
    用大括号({})括起来。只有在 Windows PowerShell 中运行 PowerShell.exe 时,
    才能指定脚本块。脚本块的结果将作为反序列化的 XML 对象
    (而非活动对象)返回到父 Shell。

    如果 Command 的值为字符串,则 Command 必须是命令中的
    最后一个参数,因为在命令后面键入的所有字符
    都将解释为命令参数。

    若要编写运行 Windows PowerShell 命令的字符串,请使用以下格式:
        "& {<命令>}"
    其中,引号表示一个字符串,调用运算符(&)
    导致执行命令。

-Help, -?, /?
    显示此消息。如果在 Windows PowerShell 中键入 PowerShell.exe
    命令,请在命令参数前面添加连字符(-),而不是添加正
    斜杠(/)。你可以在 Cmd.exe 中使用连字符或正斜杠。

示例
    PowerShell -PSConsoleFile SqlSnapIn.Psc1
    PowerShell -version 2.0 -NoLogo -InputFormat text -OutputFormat XML
    PowerShell -ConfigurationName AdminRoles
    PowerShell -Command {Get-EventLog -LogName security}
    PowerShell -Command "& {Get-EventLog -LogName security}"

    # To use the -EncodedCommand parameter:
    $command = 'dir "c:\program files" '
    $bytes = [System.Text.Encoding]::Unicode.GetBytes($command)
    $encodedCommand = [Convert]::ToBase64String($bytes)
    powershell.exe -encodedCommand $encodedCommand

参考资料

清理 git 仓库太繁琐?试试 bfg!删除敏感信息删除大文件一句命令搞定(比官方文档还详细的使用说明)

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

你可能接触过 git-filter-branch 来清理 git 仓库,不过同时也能体会到这个命令使用的繁琐,以及其超长的执行时间。

现在,你可以考虑使用 bfg 来解决问题了!


安装 bfg

传统方式安装(不推荐)

  1. 下载安装 Java 运行时
  2. 下载安装 bfg.jar

这里并不推荐使用传统方式安装,因为传统方式安装后,bfg 不会成为你计算机的命令。在实际使用工具的时候,你必须为你的每一句命令加上 java -jar bfg.jar 前缀来使用 Java 运行时间接运行。

使用包管理器 scoop 安装

如果你使用包管理器 scoop,那么安装将会非常简单,只需要以下几个命令。

  • scoop install bfg
  • scoop bucket add java
  • scoop install java/openjdk

安装 bfg:

PS C:\Users\lvyi> scoop install bfg
Installing 'bfg' (1.13.0) [64bit]
bfg-1.13.0.jar (12.8 MB) [============================================================================================================================] 100%
Checking hash of bfg-1.13.0.jar ... ok.
Linking ~\scoop\apps\bfg\current => ~\scoop\apps\bfg\1.13.0
Creating shim for 'bfg'.
'bfg' (1.13.0) was installed successfully!
'bfg' suggests installing 'java/oraclejdk' or 'java/openjdk'.

安装 Java 源:

PS C:\Users\lvyi> scoop bucket add java
Checking repo... ok
The java bucket was added successfully.

安装 Jdk:

PS C:\Users\lvyi> scoop install java/openjdk
Installing 'openjdk' (13.0.1-9) [64bit]
openjdk-13.0.1_windows-x64_bin.zip (186.9 MB) [=======================================================================================================] 100%
Checking hash of openjdk-13.0.1_windows-x64_bin.zip ... ok.
Extracting openjdk-13.0.1_windows-x64_bin.zip ... done.
Linking ~\scoop\apps\openjdk\current => ~\scoop\apps\openjdk\13.0.1-9
'openjdk' (13.0.1-9) was installed successfully!

准备工作

当你准备好清理你的仓库的时候,需要进行一些准备。

  1. 克隆一个镜像仓库(git clone 命令加上 --mirror 参数)
    • 这样,当你 git push 的时候,会更新远端仓库的所有引用
  2. cd 到你要清理的仓库路径的根目录
    • 如果你没有前往根目录,那么本文后面的所有命令的最后面你都应该加上路径
  3. 可能需要解除保护
    • 如果本文后面的命令你遇到了受保护的提交,那么需要在所有命令的后面加上 --no-blob-protection 参数

常见用法

使用 bfg 来清理仓库比 git 原生的 git-filter-branch 快得多。官方说法是,10-720 倍:

turning an overnight job into one that takes less than ten minutes.

将一整夜的工作缩减到不到十分钟。

删除误上传的大文件

使用下面的命令,可以将仓库历史中大于 500M 的文件都删除掉。

> bfg --strip-blobs-bigger-than 500M

删除特定的一个或多个文件

删除 walterlv.snk 文件:

> bfg --delete-files walterlv.snk

删除 walterlv.snk 或 lindexi.snk 文件:

> bfg --delete-files {walterlv,lindexi}.snk

比如原来仓库结构是这样的:

- README.md
- Security.md
- walterlv.snk
+ test
    - lindexi.snk

那么删除完后,根目录的 walterlv.snk 和 test 子目录下的 lindexi.snk 就都删除了。

删除文件夹

删除名字为 walterlv 的文件夹:

> bfg --delete-folders walterlv

此命令可以与上面的 --delete-files 放在一起执行:

> bfg --delete-folders walterlv --delete-files walterlv.snk

删除敏感的密码信息

> bfg --replace-text expression-file.txt

注意,这里的 expression-file.txt 名称是随便取的,你可以取其他任何名称,只要在命令里输入正确的名称(可能需要包含路径)就行。

但是 expression-file.txt 里面的内容却是我们需要关注的重点。

此文件中的每一行是一个匹配表达式。默认情况下,每一个表达式被视为一段文本常量,但你可以通过指定 regex: 前缀来说明此表达式是一个正则表达式,或者指定 glob: 前缀。每一个表达式的后面可以加上 ‘==>’ 来指定匹配的文件应该被替换成什么(如果没有指定,就会被替换成默认值 ***REMOVED***

下面这个例子示例将 git 仓库中所有文件中的 密码:123456 字符串替换成 ***REMOVED***

密码:123456

更复杂一点的,下面的例子示例将 git 仓库中所有文件中的 密码:123456 字符串替换成 密码:******

密码:123456 ==> 密码:******

还可以使用正则表达式:

regex:密码:\d+ ==> 密码:******

推回远端仓库

当你在本地操作完镜像仓库之后,可以将其推回原来的远端仓库了。

> git push

最后,有一个不必要的操作。就是回收已经没有引用的旧提交,这可以减小本地仓库的大小:

> git reflog expire --expire=now --all && git gc --prune=now --aggressive

附命令行用法输出

直接在命令行输入 bfg 可以看 bfg 命令行的用法。我贴在下面可以让还没安装的小伙伴感受一下它的功能:

PS C:\Users\lvyi\Desktop\BfgDemoRepo> bfg
bfg 1.13.0
Usage: bfg [options] [<repo>]

  -b, --strip-blobs-bigger-than <size>
                           strip blobs bigger than X (eg '128K', '1M', etc)
  -B, --strip-biggest-blobs NUM
                           strip the top NUM biggest blobs
  -bi, --strip-blobs-with-ids <blob-ids-file>
                           strip blobs with the specified Git object ids
  -D, --delete-files <glob>
                           delete files with the specified names (eg '*.class', '*.{txt,log}' - matches on file name, not path within repo)
  --delete-folders <glob>  delete folders with the specified names (eg '.svn', '*-tmp' - matches on folder name, not path within repo)
  --convert-to-git-lfs <value>
                           extract files with the specified names (eg '*.zip' or '*.mp4') into Git LFS
  -rt, --replace-text <expressions-file>
                           filter content of files, replacing matched text. Match expressions should be listed in the file, one expression per line - by default, each expression is treated as a literal, but 'regex:' & 'glob:' prefixes are supported, with '==>' to specify a replacement string other than the default of '***REMOVED***'.
  -fi, --filter-content-including <glob>
                           do file-content filtering on files that match the specified expression (eg '*.{txt,properties}')
  -fe, --filter-content-excluding <glob>
                           don't do file-content filtering on files that match the specified expression (eg '*.{xml,pdf}')
  -fs, --filter-content-size-threshold <size>
                           only do file-content filtering on files smaller than <size> (default is 1048576 bytes)
  -p, --protect-blobs-from <refs>
                           protect blobs that appear in the most recent versions of the specified refs (default is 'HEAD')
  --no-blob-protection     allow the BFG to modify even your *latest* commit. Not recommended: you should have already ensured your latest commit is clean.
  --private                treat this repo-rewrite as removing private data (for example: omit old commit ids from commit messages)
  --massive-non-file-objects-sized-up-to <size>
                           increase memory usage to handle over-size Commits, Tags, and Trees that are up to X in size (eg '10M')
  <repo>                   file path for Git repository to clean

我觉得你可能需要中文版,于是自己翻译了一下:

PS C:\Users\lvyi\Desktop\BfgDemoRepo> bfg
bfg 1.13.0
用法: bfg [options] [<repo>]

  -b, --strip-blobs-bigger-than <size>
                           移除大于 <size> 大小的文件(<size> 可填写诸如 '128K''1M'
  -B, --strip-biggest-blobs NUM
                           从大到小移除 NUM 数量的文件
  -bi, --strip-blobs-with-ids <blob-ids-file>
                           移除具有指定 git 对象 id 的文件
  -D, --delete-files <glob>
                           移除具有指定名称的文件(例如 '*.class''*.{txt,log}',仅匹配文件名而不能匹配路径)
  --delete-folders <glob>  移除具有指定名称的文件夹(例如 '.svn''*-tmp',仅匹配文件夹名而不能匹配路径)
  --convert-to-git-lfs <value>
                           将指定名称的文件(例如 '*.zip'  '*.mp4')解压到 Git LFS
  -rt, --replace-text <expressions-file>
                           查找文件内容,并替换其中匹配的文本。<expressions-file> 是一个包含一个或多个匹配表达式的文件,文件中每一行是一个匹配表达式。
                           默认情况下,每一个表达式被视为一段文本常量,但你可以通过指定 'regex:' 前缀来说明此表达式是一个正则表达式,或者指定 'glob:' 前缀。
                           每一个表达式的后面可以加上 '==>' 来指定匹配的文件应该被替换成什么(如果没有指定,就会被替换成默认值 '***REMOVED***'
  -fi, --filter-content-including <glob>
                           指定文件名(例如 '*.{txt,properties}'),在进行内容替换的时候只对这些文件进行处理。
  -fe, --filter-content-excluding <glob>
                           指定文件名(例如 '*.{xml,pdf}'),在进行内容替换的时候不对这些文件进行处理。
  -fs, --filter-content-size-threshold <size>
                           仅对小于 <size> 指定的大小的文件替换内容。(默认值为 1048576 字节)
  -p, --protect-blobs-from <refs>
                           protect blobs that appear in the most recent versions of the specified refs (default is 'HEAD')
  --no-blob-protection     allow the BFG to modify even your *latest* commit. Not recommended: you should have already ensured your latest commit is clean.
  --private                仅将本次操作视为个人数据的修改(这样生成的新提交会使用旧提交的 Id,其他人拉取仓库的时候因为这些 Id 已经存在于是不会更新,以至于此更改实际上只影响自己)。
  --massive-non-file-objects-sized-up-to <size>
                           increase memory usage to handle over-size Commits, Tags, and Trees that are up to X in size (eg '10M')
  <repo>                   file path for Git repository to clean

参考资料

C#/.NET 当我们在写事件 += 和 -= 的时候,方法是如何转换成事件处理器的

dotnet 职业技术学院 发布于 2019-10-29

当我们在写 +=-= 事件的时候,我们会在 +=-= 的右边写上事件处理函数。我们可以写很多种不同的事件处理函数的形式,那么这些形式都是一样的吗?如果你不注意,可能出现内存泄漏问题。

本文将讲解事件处理函数的不同形式,理解了这些可以避免编写代码的时候出现内存相关的问题。


典型的事件处理函数

事件处理函数本质上是一个委托,比如 FileSystemWatcherChanged 事件是这样定义的:

// 这是简化的代码。
public event FileSystemEventHandler Changed;

这里的 FileSystemEventHandler 是一个委托类型:

public delegate void FileSystemEventHandler(object sender, FileSystemEventArgs e);

一个典型的事件的 += 会像下面这样:

void Subscribe(FileSystemWatcher watcher)
{
    watcher.Changed += new FileSystemEventHandler(OnChanged);
}

void OnChanged(object sender, FileSystemEventArgs e)
{
}

+= 的右边传入的是一个 new 出来的委托实例。

变种事件处理函数

除了上面直接创建的目标类型的委托之外,还有其他类型可以放到 += 的右边:

// 方法组。
watcher.Changed += OnChanged;
// Lambda 表达式。
watcher.Changed += (sender, e) => Console.WriteLine(e.ChangeType);
// Lambda 表达式。
watcher.Changed += (sender, e) =>
{
    // 事件引发时,代码会在这里执行。
};
// 匿名方法。
watcher.Changed += delegate (object sender, FileSystemEventArgs e)
{
    // 事件引发时,代码会在这里执行。
};
// 委托类型的局部变量(或者字段)。
FileSystemEventHandler onChanged = (sender, e) => Console.WriteLine(e.ChangeType);
watcher.Changed += onChanged;
// 局部方法(或者局部静态方法)。
watcher.Changed += OnChanged;
void OnChanged(object sender, FileSystemEventArgs e)
{
}

因为我们可以通过编写事件的 addremove 方法来观察事件 += -= 传入的 value 是什么类型的什么实例,所以可以很容易验证以上每一种实例最终被加入到事件中的真实实例。

实际上我们发现,无论哪一个,最终传入的都是 FileSystemEventHandler 类型的实例。

都是同一类型的实例

然而我们知道,只有直接 new 出来的那个和局部变量那个真正是 FileSystemEventHandler 类型的实例,其他都不是。

那么中间发生了什么样的转换使得我们所有种类的写法最终都可以 += 呢?

编译器类型转换

具有相同签名的不同委托类型,彼此之前并没有继承关系,因此在运行时是不可以进行类型转换的。

比如:

FileSystemEventHandler onChanged1 = (sender, e) => Console.WriteLine(e.ChangeType);
Action<object, FileSystemEventArgs> onChanged2 = (sender, e) => Console.WriteLine(e.ChangeType);

这里,onChanged1 的实例不可以赋值给 onChanged2,反过来 onChanged2 的实例也不可以赋值给 onChanged1。于是这里只有 onChanged1 才可以作为 Changed 事件 += 的右边,而 onChanged2 放到 += 右边是会出现编译错误的。

不能转换

然而,我们可以放 Lambda 表达式,可以放匿名函数,可以放方法组,也可以放局部函数。因为这些类型可以在编译期间,由编译器帮助进行类型转换。而转换的效果就类似于我们自己编写 new FileSystemEventHandler(xxx) 一样。

不是同一个委托实例

看下面这一段代码,你认为可以 -= 成功吗?

void Subscribe(FileSystemWatcher watcher)
{
    watcher.Changed += new FileSystemEventHandler(OnChanged);
    watcher.Changed -= new FileSystemEventHandler(OnChanged);
}

void OnChanged(object sender, FileSystemEventArgs e)
{
}

实际上这是可以 -= 成功的。

我们平时编写代码的时候,下面的情况可能会多一些,于是自然而然以为 +=-= 可以成功,因为他们“看起来”是同一个实例:

watcher.Changed += OnChanged;
watcher.Changed -= OnChanged;

在读完刚刚那一段之后,我们就可以知道,实际上这一段和上面 new 出来委托的写法在运行时是一模一样的。

如果你想测试,那么在 += 的时候为对象加上一个 Id,在 -= 的时候你就会发现这是一个新对象(因为没有 Id)。

不是同一个对象

然而,你平时众多的编码经验会告诉你,这里的 -= 是一定可以成功的。也就是说,+=-= 时传入的委托实例即便不是同一个,也是可以成功 +=-= 的。

+= -= 是怎么做的

+=-= 到底是怎么做的,可以在不同实例时也能 +=-= 成功呢?

+=-= 实际上是调用了 DelegateCombineRemove 方法,并生成一个新的委托实例赋值给 += -= 的左边。

public event FileSystemEventHandler Changed
{
    add
    {
        onChangedHandler = (FileSystemEventHandler)Delegate.Combine(onChangedHandler, value);
    }
    remove
    {
        onChangedHandler = (FileSystemEventHandler)Delegate.Remove(onChangedHandler, value);
    }
}

而最终的判断也是通过 DelegateEquals 方法来比较委托的实例是否相等的(==!= 也是调用的 Equals):

public override bool Equals(object? obj)
{
    if (obj == null || !InternalEqualTypes(this, obj))
        return false;

    Delegate d = (Delegate)obj;

    // do an optimistic check first. This is hopefully cheap enough to be worth
    if (_target == d._target && _methodPtr == d._methodPtr && _methodPtrAux == d._methodPtrAux)
        return true;

    // even though the fields were not all equals the delegates may still match
    // When target carries the delegate itself the 2 targets (delegates) may be different instances
    // but the delegates are logically the same
    // It may also happen that the method pointer was not jitted when creating one delegate and jitted in the other
    // if that's the case the delegates may still be equals but we need to make a more complicated check

    if (_methodPtrAux == IntPtr.Zero)
    {
        if (d._methodPtrAux != IntPtr.Zero)
            return false; // different delegate kind
        // they are both closed over the first arg
        if (_target != d._target)
            return false;
        // fall through method handle check
    }
    else
    {
        if (d._methodPtrAux == IntPtr.Zero)
            return false; // different delegate kind

        // Ignore the target as it will be the delegate instance, though it may be a different one
        /*
        if (_methodPtr != d._methodPtr)
            return false;
            */

        if (_methodPtrAux == d._methodPtrAux)
            return true;
        // fall through method handle check
    }

    // method ptrs don't match, go down long path
    //
    if (_methodBase == null || d._methodBase == null || !(_methodBase is MethodInfo) || !(d._methodBase is MethodInfo))
        return Delegate.InternalEqualMethodHandles(this, d);
    else
        return _methodBase.Equals(d._methodBase);
}

于是可以看出来,判断相等就是两个关键对象的判断相等:

  1. 方法所在的对象
  2. 方法信息(对应到反射里的 MethodInfo)

继续回到这段代码:

void Subscribe(FileSystemWatcher watcher)
{
    watcher.Changed += new FileSystemEventHandler(OnChanged);
    watcher.Changed -= new FileSystemEventHandler(OnChanged);
}

void OnChanged(object sender, FileSystemEventArgs e)
{
}

这里的对象就是 this,方法信息就是 OnChanged 的信息,也就是:

// this 就是对象,OnChanged 就是方法信息。
this.OnChanged

-=

于是什么样的 -= 才可以把 += 加进去的事件处理函数减掉呢?

  • 必须是同一个对象的同一个方法

所以:

  1. 使用方法组、静态局部函数、委托字段的方式创建的委托实例,在 +=-= 的时候无视哪个委托实例,都是可以减掉的;
  2. 使用局部函数、委托变量,在同一个上下文中,是可以减掉的,如果调用是再次进入此函数,则不能减掉(因为委托方法所在的对象实例不同)
  3. 使用 Lambda 表达式、匿名函数是不能减掉的,因为每次编写的 Lambda 表达式和匿名函数都会创建新的包含此对象的实例。

如何在 .NET 项目中开启不安全代码(以便启用 unsafe fixed 等关键字)

dotnet 职业技术学院 发布于 2019-10-29

有小伙伴希望在 .NET 代码中使用指针,操作非托管资源,于是可能使用到 unsafe fixed 关键字。但使用此关键字的前提是需要在项目中开启不安全代码。

本文介绍如何在项目中开启不安全代码。


入门方法

第一步:在你需要启用不安全代码的项目上点击右键,然后选择属性:

项目 - 属性

第二步:在“生成”标签下,勾选上“允许不安全代码”:

允许不安全代码

第三步:切换到 Release 配置,再勾上一次“允许不安全代码”(确保 Debug 和 Release 都打开)

在 Release 允许不安全代码

方法结束。

如果你一开始选择了“所有配置”,那么就不需要分别在 Debug 和 Release 下打开了,一次打开即可。

高级方法

推荐

如果你使用 .NET Core / .NET Standard 项目,那么你可以修改项目文件来实现,这样项目文件会更加清真。

第一步:在你需要启用不安全代码的项目上点击右键,然后选择编辑项目文件:

编辑项目文件

第二步:在你的项目文件的属性组中添加一行 <AllowUnsafeBlocks>true</AllowUnsafeBlocks>

我已经把需要新增的行高亮出来了

    <Project Sdk="Microsoft.NET.Sdk">

      <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>netcoreapp3.0</TargetFramework>
++      <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
      </PropertyGroup>

    </Project>

临时方法

不推荐

如果你只是临时希望加上不安全代码开关,则可以在编译的时候加入 -unsafe 命令行参数:

csc -unsafe walterlv.cs

注意,不能给 msbuild 或者 dotnet build 加上 -unsafe 参数来编译项目,只能使用 csc 加上 -unsafe 来编译文件。因此使用场景非常受限,不推荐使用。

其他说明

第一种方法(入门方法)和第二种方法(高级方法)最终的修改是有一些区别的。入门方法会使得项目文件中有针对于 Debug 和 Release 的不同配置,代码会显得冗余;而高级方法中只增加了一行,对任何配置均生效。

因此如果可能,尽量使用高级方法呗。

    <Project Sdk="Microsoft.NET.Sdk">

      <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>netcoreapp3.0</TargetFramework>
++      <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
      </PropertyGroup>

--    <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
--      <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
--    </PropertyGroup>

--    <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
--      <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
--    </PropertyGroup>

    </Project>

即使是 .NET Framework 也是可以使用 SDK 风格的项目文件的,详情请阅读:

不要在 C# 代码中写部分命名空间(要么不写,要么写全),否则会有源码兼容性问题

dotnet 职业技术学院 发布于 2019-10-29

我只是增加库的一个 API,比如增加几个类而已,应该不会造成兼容性问题吧。对于编译好的二进制文件来说,不会造成兼容性问题;但——可能造成源码不兼容。

本文介绍可能的源码不兼容问题。


This post is written in multiple languages. Please select yours:

比如我有一个项目 P 引用 A 和 B 两个库。其中使用到了 A 库中的 Walterlv.A.Diagnostics.Foo 类型。

using Walterlv.A;
using Walterlv.B;

namespace Walterlv.Demo
{
    class Hello
    {
        Run(Diagnostics.Foo foo)
        {
        }
    }
}

现在,我们在 B 库中新增一个类型 Walterlv.B.Diagnostics.Bar 类型。

那么上面的代码将无法完成编译,因为 Diagnosis 命名空间将具有不确定的含义,其中的 Foo 类型也将无法在不确定的命名空间中找到。

因此:

  1. 强烈建议遵守 使用类型的时候,要么不写命名空间(完全留给 using),要么写全命名空间(从第一段开始写,不要省略任何部分),否则就容易与其他命名空间冲突;
  2. 可选遵守 在库中新增 API 的时候,可能需要考虑避免将部分命名空间写成过于通用的名称。

是的,即使是单纯的新增 API 也可能会导致使用库的一方在源码级不兼容。当然二进制还是兼容的。

另外,OpportunityLiu 提醒,如果命名空间是 Walterlv.B.Walterlv.A.Diagnostics.Bar,一样可以让写全了的命名空间炸掉。呃……还是不要在库里面折腾这样的命名空间好……不然代码当中到处充斥着 global:: 可是非常难受的。

.NET/C# 在 64 位进程中读取 32 位进程重定向后的注册表

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

我们知道,32 位程序在读取注册表的时候,会自动将注册表的路径映射到 32 位路径下,即在 Wow6432Node 子节点下。但是 64 位程序不会映射到 32 位路径下。那么 64 位程序如何读取到 32 位程序写入的注册表路径呢?


Wow6432Node

Wow6432Node

对于 32 位程序,读取注册表路径的时候,会读到 Wow6432Node 节点下的项:

32 位

这张图读取的就是前面截图中的节点。

那么怎样编译的程序是 32-bit 的程序呢?

x86

AnyCPU 32-bit preferred

对于 64 位程序,读取的时候就不会有 Wow6432Node 路径部分。由于我没有在那个路径放注册表项,所以会得到 null

null

那么怎样编译的程序是 64-bit 的程序呢?

x64

AnyCPU

如何在 64 位程序中读取 32 位注册表路径

前面我们的例子代码是这样的:

var value = RegistryHive.LocalMachine.Read(@"SOFTWARE\Walterlv");

可以看到,相同的代码,在 32 位和 64 位进程下得到的结果是不同的:

  • 32 位进程在 32 位系统上,64 位进程在 64 位系统上,读取的路径会是传入的路径;
  • 32 位进程在 64 位系统上,读取的路径会包含 Wow6432Node

那么如何在 64 位进程中读取 32 位注册表路径呢?

方法是在打开注册表项的时候,传入 RegistryView.Registry32

RegistryKey.OpenBaseKey(root, RegistryView.Registry32);

Walterlv.Win32

可以在我的 GitHub 仓库中查看完整的实现。当然,除了上面那句话,其他都不是关键代码,在哪里都可以找得到的。


参考资料

解决 WPF 嵌套的子窗口在改变窗口大小的时候闪烁的问题

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

因为 Win32 的窗口句柄是可以跨进程传递的,所以可以用来实现跨进程 UI。不过,本文不会谈论跨进程 UI 的具体实现,只会提及其实现中的一个重要缓解,使用子窗口的方式。

你有可能在使用子窗口之后,发现拖拽改变窗口大小的时候,子窗口中的内容不断闪烁。如果你也遇到了这样的问题,那么正好可以阅读本文来解决。


问题

你可以看一下下面的这张动图,感受一下窗口的闪烁:

窗口闪烁

实际上在拖动窗口的时候,是一直都在闪的,只是每次闪烁都非常快,截取 gif 的时候截不到。

如果你希望实际跑一跑项目看看,可以使用下面的代码:

我特地提取了一个提交下的代码,如果你要尝试,不能使用 master 分支,因为 master 分支修复了闪烁的问题。

后来使用 CreateWindowEx 创建了一个纯 Win32 窗口,这种闪烁现象更容易被截图:

Win32 窗口闪烁

Win32 窗口闪烁 - 动图

解决

    public class HwndWrapper : HwndHost
    {
        protected override HandleRef BuildWindowCore(HandleRef hwndParent)
        {
            const int WS_CHILD = 0x40000000;
++          const int WS_CLIPCHILDREN = 0x02000000;
            var owner = ((HwndSource)PresentationSource.FromVisual(this)).Handle;

            var parameters = new HwndSourceParameters("demo")
            {
                ParentWindow = owner,
--              WindowStyle = (int)(WS_CHILD),
++              WindowStyle = (int)(WS_CHILD | WS_CLIPCHILDREN),
            };
            var source = new HwndSource(parameters);
            source.RootVisual = new ChildPage();
            return new HandleRef(this, source.Handle);
        }

        protected override void DestroyWindowCore(HandleRef hwnd)
        {
        }
    }

原因

正在探索……


参考资料

System.ComponentModel.Win32Exception (0x80004005): 无效的窗口句柄。

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

在 WPF 获取鼠标当前坐标的时候,可能会得到一个异常:System.ComponentModel.Win32Exception:“无效的窗口句柄。”

本文解释此异常的原因和解决方法。


异常

获取鼠标当前相对于元素 element 的坐标的代码:

var point = Mouse.GetPosition(element);

或者,还有其他的代码:

var point1 = e.PointFromScreen(new Point());
var point2 = e.PointToScreen(new Point());

如果在按下窗口关闭按钮的时候调用以上代码,则会引发异常:

System.ComponentModel.Win32Exception (0x80004005): 无效的窗口句柄。
   at Point MS.Internal.PointUtil.ClientToScreen(Point pointClient, PresentationSource presentationSource)
   at Point System.Windows.Input.MouseDevice.GetScreenPositionFromSystem()

原因

将窗口上的点转换到控件上的点的方法是这样的:

/// <summary>
///     Convert a point from "client" coordinate space of a window into
///     the coordinate space of the screen.
/// </summary>
/// <SecurityNote>
///     SecurityCritical: This code causes eleveation to unmanaged code via call to GetWindowLong
///     SecurityTreatAsSafe: This data is ok to give out
///     validate all code paths that lead to this.
/// </SecurityNote>
[SecurityCritical, SecurityTreatAsSafe]
public static Point ClientToScreen(Point pointClient, PresentationSource presentationSource)
{
    // For now we only know how to use HwndSource.
    HwndSource inputSource = presentationSource as HwndSource;
    if(inputSource == null)
    {
        return pointClient;
    }
    HandleRef handleRef = new HandleRef(inputSource, inputSource.CriticalHandle);

    NativeMethods.POINT ptClient            = FromPoint(pointClient);
    NativeMethods.POINT ptClientRTLAdjusted = AdjustForRightToLeft(ptClient, handleRef);

    UnsafeNativeMethods.ClientToScreen(handleRef, ptClientRTLAdjusted);

    return ToPoint(ptClientRTLAdjusted);
}

最关键的是 UnsafeNativeMethods.ClientToScreen,此方法要求窗口句柄依然有效,然而此时窗口已经关闭,句柄已经销毁。

解决

使用 Direct3D11 的 OpenSharedResource 方法渲染来自其他进程/设备的共享资源(SharedHandle)

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

如果你得到了一个来自于其他进程或者其他模块的 Direct3D11 的共享资源,即 SharedHandle 句柄,那么可以使用本文提到的方法将其转换成 Direct3D11 的设备和纹理,这样你可以进行后续的其他处理。


SharpDX

本文的代码会使用到 SharpDX 库,因此,你需要在你的项目当中安装这些 NuGet 包:

<!-- 基础,必装 -->
<PackageReference Include="SharpDX" Version="4.2.0" />
<PackageReference Include="SharpDX.D3DCompiler" Version="4.2.0" />
<PackageReference Include="SharpDX.DXGI" Version="4.2.0" />
<PackageReference Include="SharpDX.Mathematics" Version="4.2.0" />
<PackageReference Include="SharpDX.Direct3D11" Version="4.2.0" />

<!-- 其他,可选 -->
<PackageReference Include="SharpDX.Direct2D1" Version="4.2.0" />
<PackageReference Include="SharpDX.Direct3D9" Version="4.2.0" />

SharedHandle

Direct3D 支持在不同的 Direct3D 设备之间共享资源。需要设置 ResourceOptionFlagsShared 的纹理才可以支持共享,当然这不是本文要说的重点。

本文要说的是,如果你拿到了一个来自于其他模块的共享资源句柄的时候,你可以如何使用它。

你的使用可能类似于这样:

public void OnAcceleratedPaint(IntPtr sharedHandle, Int32Rect dirtyRect)
{
    // 通过 sharedHandle 进行后续的处理。
}

OpenSharedResource

DirectX 中用来表示 Direct3D11 的设备类型是 ID3D11Device,它有一个 OpenSharedResource 方法可以用来打开来自于其他设备的共享资源。

对应到 SharpDX 中,用来表示 Direct3D11 的设备的类型是 SharpDX.Direct3D11.Device,其有一个 OpenSharedResource<T> 方法来打开来自于其他设备的共享资源。

我们必须要创建一个自己的 Direct3D11 设备,因为设备是不共享的,代码如下:

var device = new SharpDX.Direct3D11.Device(DriverType.Hardware, DeviceCreationFlags.BgraSupport);
var resource = device.OpenSharedResource<SharpDX.Direct3D11.Resource>(sharedHandle);

后续操作

在得到此共享资源之后,我们可以获得更多关于此资源的描述,以及有限地使用此资源的方法。

获取 Texture2D

可以通过 QueryInterface 获取某个资源相关的 COM 对象的引用。我们拿到的共享资源是 2D 纹理的话,我们可以使用 QueryInterface 获取 SharpDX.Direct3D11.Texture2D COM 对象的引用。

var texture = resource.QueryInterface<SharpDX.Direct3D11.Texture2D>();

获取 Texture2DDescription

可以从 Texture2D 的实例中获取到 Texture2DDescription,这是用来描述此 2D 纹理创建时的各种信息。

// 在 DirectX 的传统代码中,通常使用 desc 来作为 Texture2DDescription 实例命名的后缀。
// 不过 C# 代码通常不这么干,这是 C++ 代码的习惯。在这里这么写是为了在得到 C++ 搜索结果的时候可以与本文所述的 C# 代码对应起来。
var desc = texture.Description;

获取 Surface

或者,我们可以获取到 2D 图面,用于做渲染、绘制等操作。当然,是否能真正进行这些操作取决于 Texture2DDescription 中是否允许相关的操作。

var surface = texture2D.QueryInterface<SharpDX.DXGI.Surface>();

在获取到 SharpDX.DXGI.Surface 的 COM 组件引用之后,可以在内存中映射位图用于调试,可以参见:


参考资料

将 Direct3D11 在 GPU 中的纹理(Texture2D)导出到内存(Map)或导出成图片文件

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

Direct3D11 的使用通常不是应用程序唯一的部分,于是使用 Direct3D11 的代码如何与其他模块正确地组合在一起就是一个需要解决的问题。

本文介绍将 Direct3D11 在 GPU 中绘制的纹理映射到内存中,这样我们可以直接观察到此纹理是否是正确的,而不用担心是否有其他模块影响了最终的渲染过程。


SharpDX

本文的代码会使用到 SharpDX 库,因此,你需要在你的项目当中安装这些 NuGet 包:

<!-- 基础,必装 -->
<PackageReference Include="SharpDX" Version="4.2.0" />
<PackageReference Include="SharpDX.D3DCompiler" Version="4.2.0" />
<PackageReference Include="SharpDX.DXGI" Version="4.2.0" />
<PackageReference Include="SharpDX.Mathematics" Version="4.2.0" />
<PackageReference Include="SharpDX.Direct3D11" Version="4.2.0" />

<!-- 其他,可选 -->
<PackageReference Include="SharpDX.Direct2D1" Version="4.2.0" />
<PackageReference Include="SharpDX.Direct3D9" Version="4.2.0" />

来自于 Direct3D11 的渲染纹理

本文不会说如何创建或者获取来自 Direct3D11 的渲染纹理,不过如果你希望了解,可以:

本文接下来的内容,是在你已经获得了 SharpDX.Direct3D11.Resource 的引用,或者 SharpDX.Direct3D11.Texture2D 的前提之下。当然,如果你获得了其中任何一个实例,可以通过 COM 组件的 QueryInterface 方法获得其他实例。

var texture = resource.QueryInterface<SharpDX.Direct3D11.Texture2D>();
var resource = texture.QueryInterface<SharpDX.Direct3D11.Resource>();

关键代码(SharpDX.DXGI.Surface.Map)

要获得 GPU 中渲染的图片,我们必须要将其映射到内存中才行。而映射到内存中的核心代码是 SharpDX.DXGI.Surface 对象的 Map 方法。

using (var surface = texture2D.QueryInterface<SharpDX.DXGI.Surface>())
{
    var map = surface.Map(SharpDX.DXGI.MapFlags.Read, out DataStream dataStream);
    for (var y = 0; y < surface.Description.Height; y++)
    {
        for (var x = 0; x < surface.Description.Width; x++)
        {
            // 在这里使用位图的像素数据,坐标为 (x, y)。
            // 得到此坐标下的像素指针:
            //     var ptr = ((byte*)map.DataPointer) + y * map.Pitch;
            // 得到此像素的颜色值:
            //     var b = *(ptr + 4 * x);
            //     var g = *(ptr + 4 * x + 1);
            //     var r = *(ptr + 4 * x + 2);
            //     var a = *(ptr + 4 * x + 3);
        }
    }
    dataStream.Dispose();
    surface.Unmap();
}

注意以上代码使用了不安全代码(指针),你需要为你的项目开启不安全代码开关,详见:

你可能需要拷贝资源

实际上,在使用上面的代码时,你可能会遇到错误,错误出现在 Map 方法的调用上,描述为“参数错误”。实际上真正检查这里的两个参数时并不能发现究竟是哪个参数出了问题。

实际上出问题的参数是 surface 的实例。

一段 GPU 中的纹理要能够被映射到内存,必须要具有 CPU 的访问权。而是否具有 CPU 访问权在创建纹理的时候就已经确定下来了。

如果前面你得到的纹理是自己创建的,那么恭喜你,你只需要改一下创建纹理的参数就好了。给 Texture2DDescriptionCpuAccessFlags 属性加上 CpuAccessFlags.Read 标识。

desc.CpuAccessFlags = CpuAccessFlags.Read

但是,如果此纹理不是由你自己创建的,那么就需要拷贝一份新的纹理了。当然,拷贝过程发生在 GPU 中,占用的也是 GPU 专用内存(即显存,如果有的话)。

拷贝需要做到两点:

  1. 创建一个新的 Texture2DDescription(一定要是新的实例,你不能影响原来的实例),然后修改其 CPU 访问权限为 Read
  2. 使用 ImmediateContext 实例的 CopyResource 方法来拷贝资源(此实例可以通过 SharpDX.Direct3D11.Device 来找到)。
var originalDesc = originalTexture.Description;
var desc = new Texture2DDescription
{
    CpuAccessFlags = CpuAccessFlags.Read,
    BindFlags = BindFlags.None,
    Usage = ResourceUsage.Staging,
    Width = originalDesc.Width,
    Height = originalDesc.Height,
    Format = originalDesc.Format,
    MipLevels = 1,
    ArraySize = 1,
    SampleDescription =
    {
        Count = 1,
        Quality = 0
    },
};

var texture2D = new Texture2D(device, desc);
device.ImmediateContext.CopyResource(originalTexture, texture2D);

需要注意,拷贝纹理会额外占用显存,一般不建议这么做,除非你真的有需求一定要 CPU 能够访问到这段纹理。

导出成图片文件

实际上,当你组合起来以上以上方法,你应该能够将纹理导出成图片了。

不过,为了理解更方便一些,我还是将导出成图片的全部代码贴出来:

public static unsafe void MapTexture2DToFile(SharpDX.Direct3D11.Texture2D texture, string fileName)
{
    // 获取 Texture2D 的相关实例。
    var device = texture.Device;
    var originDesc = texture.Description;

    // 创建新的 Texture2D 对象。
    var desc = new Texture2DDescription
    {
        CpuAccessFlags = CpuAccessFlags.Read,
        BindFlags = BindFlags.None,
        Usage = ResourceUsage.Staging,
        Width = originDesc.Width,
        Height = originDesc.Height,
        Format = originDesc.Format,
        MipLevels = 1,
        ArraySize = 1,
        SampleDescription =
        {
            Count = 1,
            Quality = 0
        },
        OptionFlags = ResourceOptionFlags.Shared
    };
    var texture2D = new Texture2D(device, desc);

    // 拷贝资源。
    device.ImmediateContext.CopyResource(texture, texture2D);

    var bitmap = new System.Drawing.Bitmap(desc.Width, desc.Height);
    using (var surface = texture2D.QueryInterface<SharpDX.DXGI.Surface>())
    {
        var map = surface.Map(SharpDX.DXGI.MapFlags.Read, out DataStream dataStream);
        var lines = (int)(dataStream.Length / map.Pitch);
        var actualWidth = surface.Description.Width * 4;
        for (var y = 0; y < desc.Height; y++)
        {
            var h = desc.Height - y;
            var ptr = ((byte*)map.DataPointer) + y * map.Pitch;

            for (var x = 0; x < desc.Width; x++)
            {
                var b = *(ptr + 4 * x);
                var g = *(ptr + 4 * x + 1);
                var r = *(ptr + 4 * x + 2);
                var a = *(ptr + 4 * x + 3);
                bitmap.SetPixel(x, y, System.Drawing.Color.FromArgb(a, r, g, b));
            }
        }
        dataStream.Dispose();
        surface.Unmap();
        bitmap.Save(fileName);
    }
}

如果你是希望以纯软件的方式渲染到 WPF 中(WriteableBitmap),可以参考:

记得打开不安全代码开关哦!详见:


参考资料

.NET 实现 NTFS 文件系统的硬链接 mklink /J(Junction)

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

我们知道 Windows 系统 NTFS 文件系统提供了硬连接功能,可以通过 mklink 命令开启。如果能够通过代码实现,那么我们能够做更多有趣的事情。

本文提供使用 .NET/C# 代码创建 NTFS 文件系统的硬连接功能(目录联接)。


目录联接

以管理员权限启动 CMD(命令提示符),输入 mklink 命令可以得知 mklink 的用法。

C:\WINDOWS\system32>mklink
创建符号链接。

MKLINK [[/D] | [/H] | [/J]] Link Target

        /D      创建目录符号链接。默认为文件
                符号链接。
        /H      创建硬链接而非符号链接。
        /J      创建目录联接。
        Link    指定新的符号链接名称。
        Target  指定新链接引用的路径
                (相对或绝对)

我们本次要用 .NET/C# 代码实现的是 /J 目录联接。实现的效果像这样:

目录联接

这些文件夹带有一个“快捷方式”的角标,似乎是另一些文件夹的快捷方式一样。但这些与快捷方式的区别在于,应用程序读取路径的时候,目录联接会成为路径的一部分。

比如在 D:\Walterlv\NuGet\ 中创建 debug 目录联接,目标设为 D:\Walterlv\DemoRepo\bin\Debug,那么,你在各种应用程序中使用以下两个路径将被视为同一个:

  • D:\Walterlv\NuGet\debug\DemoRepo-1.0.0.nupkg
  • D:\Walterlv\DemoRepo\bin\Debug\DemoRepo-1.0.0.nupkg

或者这种:

  • D:\Walterlv\NuGet\debug\publish\
  • D:\Walterlv\DemoRepo\bin\Debug\publish\

使用 .NET/C# 实现

本文的代码主要参考自 jeff.brownManipulating NTFS Junction Points in .NET - CodeProject 一文中所附带的源代码。

由于随时可能更新,所以你可以前往 GitHub 仓库打开此代码:

使用 JunctionPoint

如果希望在代码中创建目录联接,则直接使用:

JunctionPoint.Create("walterlv.demo", @"D:\Developments", true);

后面的 true 指定如果目录联接存在,则会覆盖掉原来的目录联接。


参考资料

WPF 高性能位图渲染 WriteableBitmap 及其高性能用法示例

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

WPF 渲染框架并没有对外提供多少可以完全控制渲染的部分,目前可以做的有:

  • D3DImage,用来承载使用 DirectX 各个版本渲染内容的控件
  • WriteableBitmap,通过一段内存空间来指定如何渲染一个位图的图片
  • HwndHost,通过承载一个子窗口以便能叠加任何种类渲染的控件

本文将解释如何最大程度压榨 WriteableBitmap 在 WPF 下的性能。


如何使用 WriteableBitmap

创建一个新的 WPF 项目,然后我们在 MainWindow.xaml 中编写一点可以用来显示 WriteableBitmap 的代码:

<Window x:Class="Walterlv.Demo.HighPerformanceBitmap.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:Walterlv.Demo.HighPerformanceBitmap"
        Title="WriteableBitmap - walterlv" SizeToContent="WidthAndHeight">
    <Grid>
        <Image x:Name="Image" Width="1280" Height="720" />
    </Grid>
</Window>

为了评估其性能,我决定绘制和渲染 4K 品质的位图,并通过以下步骤来评估:

  1. 使用 CompositionTarget.Rendering 逐帧渲染以评估其渲染帧率
  2. 使用 Benchmark 基准测试来测试内部各种不同方法的性能差异

于是,在 MainWindow.xaml.cs 中添加一些测试用的修改 WriteableBitmap 的代码:

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace Walterlv.Demo.HighPerformanceBitmap
{
    public partial class MainWindow : Window
    {
        private readonly WriteableBitmap _bitmap;

        public MainWindow()
        {
            InitializeComponent();

            _bitmap = new WriteableBitmap(3840, 2160, 96.0, 96.0, PixelFormats.Pbgra32, null);
            Image.Source = _bitmap;
            CompositionTarget.Rendering += CompositionTarget_Rendering;
        }

        private void CompositionTarget_Rendering(object sender, EventArgs e)
        {
            var width = _bitmap.PixelWidth;
            var height = _bitmap.PixelHeight;

            _bitmap.Lock();

            // 在这里添加绘制位图的逻辑。

            _bitmap.AddDirtyRect(new Int32Rect(0, 0, width, height));
            _bitmap.Unlock();
        }
    }
}

注意,我留了一行注释说即将添加绘制位图的逻辑,接下来我们的主要内容将从此展开。

启用不安全代码

为了获取最佳性能,我们需要开启不安全代码。为此,你需要修改一下你的项目属性。

你可以阅读我的另一篇博客了解如何启用不安全代码:

简单点说就是在你的项目文件中添加下面这一行:

    <Project Sdk="Microsoft.NET.Sdk">
      <PropertyGroup>
        <TargetFramework>netcoreapp3.0</TargetFramework>
++      <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
      </PropertyGroup>
    </Project>

启用帧率测试

接下来,我们需要添加一点点代码来评估 WriteableBitmap 的性能:

++  private readonly byte[] _empty4KBitmapArray = new byte[3840 * 2160 * 4];

--  private void CompositionTarget_Rendering(object sender, EventArgs e)
++  private unsafe void CompositionTarget_Rendering(object sender, EventArgs e)
    {
        var width = _bitmap.PixelWidth;
        var height = _bitmap.PixelHeight;

        _bitmap.Lock();

++      fixed (byte* ptr = _empty4KBitmapArray)
++      {
++          var p = new IntPtr(ptr);
++          Buffer.MemoryCopy(ptr, _bitmap.BackBuffer.ToPointer(), _empty4KBitmapArray.Length, _empty4KBitmapArray.Length);
++      }

        _bitmap.AddDirtyRect(new Int32Rect(0, 0, width, height));
        _bitmap.Unlock();
    }

嗯,就是将一个空的 4K 大小的数组中的内容复制到 WriteableBitmap 的位图缓存中。

4K 脏区

虽然我们看不到任何可变的修改,不过 WriteableBitmap 可不这么认为。因为我们调用了 AddDirtyRect 将整个位图空间都加入到了脏区中,这样 WPF 会重新渲染整幅位图。

Visual Studio 中看到的 CPU 占用率大约维持在 16% 左右(跟具体机器相关);并且除了一开始启动的时候之外,完全没有 GC(这点很重要),内存稳定在一个值上不再变化。

也只有本文一开始提及的三种方法才可能做到渲染任何可能的图形的时候没有 GC

CPU 占用率和内存用量

查看界面渲染帧率可以发现跑满 60 帧没有什么问题(跟具体机器相关)。

帧率

小脏区

现在,我们把脏区的区域缩小为 100*100,同样看性能数据。

--  _bitmap.AddDirtyRect(new Int32Rect(0, 0, width, height));
++  _bitmap.AddDirtyRect(new Int32Rect(0, 0, 100, 100));

可以发现 CPU 占用降低到一半(确实是大幅降低,但是跟像素数量并不成比例);内存没有变化(废话,4K 图像是确定的);帧率没有变化(废话,只要性能够,帧率就是满的)。

小脏区 CPU 占用率和内存用量

小脏区帧率

无脏区

现在,我们将脏区清零。

--  _bitmap.AddDirtyRect(new Int32Rect(0, 0, width, height));
++  _bitmap.AddDirtyRect(new Int32Rect(0, 0, 0, 0));

在完全没有脏区的时候,CPU 占用直接降为 0,这个性能提升还是非常恐怖的。

零脏区 CPU 占用率和内存用量

不渲染

如果我们不把 WriteableBitmap 设置为 ImageSource 属性,那么无论脏区多大,CPU 占用都是 0。

脏区大小与 CPU 占用率之间的关系

从前面的测试中我们可以发现,脏区的大小在 WriteableBitmap 的渲染里占了绝对的耗时。因此,我把脏区大小与 CPU 占用率之间的关系用图表的形式贴出来,这样可以直观地理解其性能差异。

需要注意,CPU 占用率与机器性能强相关,因此其绝对占用没有意义,但相对大小则有参考价值。

脏区大小 CPU 占用率 帧率
0*0 0.0% 60
1*1 5.1% 60
16*9 5.7% 60
160*90 6.0% 60
320*180 6.5% 60
640*360 6.9% 60
1280*720 7.5% 60
1920*1080 10.5% 60
2560*1440 12.3% 60
3840*2160 16.1% 60

根据这张表我么可以得出:

  • 脏区渲染是 CPU 占用的最大瓶颈(因为没有脏区仅剩内存拷贝的时候 CPU 占用为 0%)

但是有一个需要注意的信息是——虽然 CPU 占用率受脏区影响非常大,但主线程却几乎没有消耗 CPU 占用。此占用基本上全是渲染线程的事。

如果我们分析主线程的性能分布,可以发现内存拷贝现在是性能瓶颈:

内存拷贝是性能瓶颈

后面我们会提到 WriteableBitmap 的渲染原理,也会说到这一点。

启用基准测试(Benchmark)

不过,由于内存数据的拷贝和脏区渲染实际上可以分开到两个不同的线程,如果这两者不同步执行(可能执行次数还有差异)的情况下,内存拷贝也可能成为性能瓶颈的一部分。

于是我将不同的内存拷贝方法进行一个基准测试,便于大家评估使用哪种方法来为 WriteableBitmap 提供渲染数据。

使用 CopyMemory 拷贝内存

++  [Benchmark(Description = "CopyMemory")]
++  [Arguments(3840, 2160)]
++  [Arguments(100, 100)]
    public unsafe void CopyMemory(int width, int height)
    {
        _bitmap.Lock();

++      fixed (byte* ptr = _empty4KBitmapArray)
++      {
++          var p = new IntPtr(ptr);
++          CopyMemory(_bitmap.BackBuffer, new IntPtr(ptr), (uint)_empty4KBitmapArray.Length);
++      }

        _bitmap.AddDirtyRect(new Int32Rect(0, 0, width, height));
        _bitmap.Unlock();
    }
[DllImport("kernel32.dll")]
private static extern void CopyMemory(IntPtr destination, IntPtr source, uint length);

使用 MoveMemory 移动内存

++  [Benchmark(Description = "RtlMoveMemory")]
++  [Arguments(3840, 2160)]
++  [Arguments(100, 100)]
    public unsafe void RtlMoveMemory(int width, int height)
    {
        _bitmap.Lock();

++      fixed (byte* ptr = _empty4KBitmapArray)
++      {
++          var p = new IntPtr(ptr);
++          MoveMemory(_bitmap.BackBuffer, new IntPtr(ptr), (uint)_empty4KBitmapArray.Length);
++      }

        _bitmap.AddDirtyRect(new Int32Rect(0, 0, width, height));
        _bitmap.Unlock();
    }
    [DllImport("kernel32.dll", EntryPoint = "RtlMoveMemory")]
    private static extern void MoveMemory(IntPtr dest, IntPtr src, uint count);

使用 Buffer.MemoryCopy 拷贝内存

需要注意,Buffer.MemoryCopy 是 .NET Framework 4.6 才引入的 API,在 .NET Framework 后续版本以及 .NET Core 的所有版本才可以使用,更旧版本的 .NET Framework 没有这个 API。

++  [Benchmark(Baseline = true, Description = "Buffer.MemoryCopy")]
++  [Arguments(3840, 2160)]
++  [Arguments(100, 100)]
    public unsafe void BufferMemoryCopy(int width, int height)
    {
        _bitmap.Lock();

++      fixed (byte* ptr = _empty4KBitmapArray)
++      {
++          var p = new IntPtr(ptr);
++          Buffer.MemoryCopy(ptr, _bitmap.BackBuffer.ToPointer(), _empty4KBitmapArray.Length, _empty4KBitmapArray.Length);
++      }

        _bitmap.AddDirtyRect(new Int32Rect(0, 0, width, height));
        _bitmap.Unlock();
    }

自己写 for 循环

++  [Benchmark(Description = "for for")]
++  [Arguments(3840, 2160)]
++  [Arguments(100, 100)]
    public unsafe void ForForCopy(int width, int height)
    {
        _bitmap.Lock();

++      var buffer = (byte*)_bitmap.BackBuffer.ToPointer();
++      for (var j = 0; j < height; j++)
++      {
++          for (var i = 0; i < width; i++)
++          {
++              var pixel = buffer + j * width * 4 + i * 4;
++              *pixel = 0xff;
++              *(pixel + 1) = 0x7f;
++              *(pixel + 2) = 0x00;
++              *(pixel + 3) = 0xff;
++          }
++      }

        _bitmap.AddDirtyRect(new Int32Rect(0, 0, width, height));
        _bitmap.Unlock();
    }

基准测试数据

我们跑一次基准测试:

Method Mean Error StdDev Median Ratio RatioSD
CopyMemory 2.723 ms 0.0642 ms 0.1881 ms 2.677 ms 0.84 0.08
RtlMoveMemory 2.659 ms 0.0740 ms 0.2158 ms 2.633 ms 0.82 0.08
Buffer.MemoryCopy 3.246 ms 0.0776 ms 0.2250 ms 3.200 ms 1.00 0.00
‘for for’ 10.401 ms 0.1979 ms 0.4964 ms 10.396 ms 3.21 0.25
‘CopyMemory with 100*100 dirty region’ 2.446 ms 0.0757 ms 0.2207 ms 2.368 ms 0.76 0.09
‘RtlMoveMemory with 100*100 dirty region’ 2.415 ms 0.0733 ms 0.2161 ms 2.369 ms 0.75 0.08
‘Buffer.MemoryCopy with 100*100 dirty region’ 3.076 ms 0.0612 ms 0.1523 ms 3.072 ms 0.95 0.08
‘for for with 100*100 dirty region’ 10.014 ms 0.2398 ms 0.6995 ms 9.887 ms 3.10 0.29

可以发现:

  1. CopyMemoryRtMoveMemory 性能是最好的,其性能差不多;
  2. 自己写循环拷贝内存的性能是最差的;
  3. 如果 WriteableBitmap 不渲染,那么无论设置多大的脏区都不会对性能有任何影响。

结论和使用建议

综合前面两者的结论,我们可以发现:

  1. WriteableBitmap 的性能瓶颈源于对脏区的重新渲染
    • 脏区为 0 或者不在可视化树渲染,则不消耗性能
    • 只要有脏区,渲染过程就会开始成为性能瓶颈
      • CPU 占用基础值就很高了
      • 脏区越大,CPU 占用越高,但增幅不大
  2. 内存拷贝不是 WriteableBitmap 的性能瓶颈
    • 建议使用 Windows API 或者 .NET API 来拷贝内存(而不是自己写)

另外,如果你有一些特殊的应用场景,可以适当调整下自己写代码的策略:

  • 如果你希望有较大脏区的情况下降低 CPU 占用,可以考虑降低 WriteableBitmap 脏区的刷新率
  • 如果你希望 WriteableBitmap 有较低的渲染延迟,则考虑减小脏区

WriteableBitmap 渲染原理

在调用 WriteableBitmap 的 AddDirtyRect 方法的时候,实际上是调用 MILSwDoubleBufferedBitmap.AddDirtyRect,这是 WPF 专门为 WriteableBitmap 而提供的非托管代码的双缓冲位图的实现。

在 WriteableBitmap 内部数组修改完毕之后,需要调用 Unlock 来解锁内部缓冲区的访问,这时会提交所有的修改。接下来的渲染都交给了 MediaContext,用来完成双缓冲位图的渲染。

private void SubscribeToCommittingBatch()
{
    // Only subscribe the the CommittingBatch event if we are on-channel.
    if (!_isWaitingForCommit)
    {
        MediaContext mediaContext = MediaContext.From(Dispatcher);
        if (_duceResource.IsOnChannel(mediaContext.Channel))
        {
            mediaContext.CommittingBatch += CommittingBatchHandler;
            _isWaitingForCommit = true;
        }
    }
}

在上面的 CommittingBatchHandler 中,将渲染指令发送到了渲染线程。

channel.SendCommand((byte*)&command, sizeof(DUCE.MILCMD_DOUBLEBUFFEREDBITMAP_COPYFORWARD));

前面我们通过脏区大小可以得出内存拷贝不是 CPU 占用率的瓶颈,脏区大小才是,不过是渲染线程在占用这 CPU 而不是主线程。但是内存拷贝却成为了主线程的瓶颈(当然前面我们给出了数据,实际上非常小)。所以如果试图分析这么高 CPU 的占用,会发现并不能从主线程上调查得出符合预期的结论(因为即便你完全干掉了内存拷贝,CPU 占用依然是这么高)。

内存拷贝是性能瓶颈

WPF 制作高性能的透明背景异形窗口(使用 WindowChrome 而不要使用 AllowsTransparency=True)

dotnet 职业技术学院 发布于 2019-10-17

在 WPF 中,如果想做一个背景透明的异形窗口,基本上都要设置 WindowStyle="None"AllowsTransparency="True" 这两个属性。如果不想自定义窗口样式,还需要设置 Background="Transparent"。这样的设置会让窗口变成 Layered Window,WPF 在这种类型窗口上的渲染性能是非常糟糕的。

本文介绍如何使用 WindowChrome 而不设置 AllowsTransparency="True" 制作背景透明的异形窗口,这可以避免异形窗口导致的低渲染性能。


背景透明的异形窗口

如下是一个背景透明异形窗口的示例:

示例异形窗口

此窗口包含很大的圆角,还包含 DropShadowEffect 制作的阴影效果。对于非透明窗口来说,这是不可能实现的。

如何实现

要实现这种背景透明的异形窗口,需要为窗口设置以下三个属性:

  • WindowStyle="None"
  • ResizeMode="CanMinimize"ResizeMode="NoResize"
  • WindowChrome.GlassFrameThickness="-1" 或设置为其他较大的正数(可自行尝试设置之后的效果)

如下就是一个最简单的例子,最关键的三个属性我已经高亮标记出来了。

    <Window x:Class="Walterlv.Demo.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
++          WindowStyle="None" ResizeMode="CanMinimize"
            Title="walterlv demo" Height="450" Width="800">
++      <WindowChrome.WindowChrome>
++          <WindowChrome GlassFrameThickness="-1" />
++      </WindowChrome.WindowChrome>
        <Window.Template>
            <ControlTemplate TargetType="Window">
                <Border Padding="64" Background="Transparent">
                    <Border CornerRadius="16" Background="White">
                        <Border.Effect>
                            <DropShadowEffect BlurRadius="64" />
                        </Border.Effect>
                        <ContentPresenter ClipToBounds="True" />
                    </Border>
                </Border>
            </ControlTemplate>
        </Window.Template>
        <Grid>
            <TextBlock FontSize="20" Foreground="#0083d0"
                   TextAlignment="Center" VerticalAlignment="Center">
                <Run Text="欢迎访问吕毅的博客" />
                <LineBreak />
                <Run Text="blog.walterlv.com" FontSize="64" FontWeight="Light" />
            </TextBlock>
        </Grid>
    </Window>

网上流传的主流方法

在网上流传的主流方法中,AllowsTransparency="True" 都是一个必不可少的步骤,另外也需要 WindowStyle="None"。但是我一般都会极力反对大家这么做,因为 AllowsTransparency="True" 会造成很严重的性能问题。

如果你有留意到我的其他博客,你会发现我定制窗口样式的时候都在极力避开设置此性能极差的属性:

性能对比

既然特别说到性能,那也是口说无凭,我们要拿出数据来说话。

以下是我用来测试渲染性能所使用的例子:

测试性能所用的程序

相比于上面的例子来说,主要就是加了背景动画效果,这可以用来测试帧率。

    <Window x:Class="Walterlv.Demo.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            WindowStyle="None" ResizeMode="CanMinimize"
            Title="walterlv demo" Height="450" Width="800">
        <WindowChrome.WindowChrome>
            <WindowChrome GlassFrameThickness="-1" />
        </WindowChrome.WindowChrome>
        <Window.Template>
            <ControlTemplate TargetType="Window">
                <Border Padding="64" Background="Transparent">
                    <Border CornerRadius="16" Background="White">
                        <Border.Effect>
                            <DropShadowEffect BlurRadius="64" />
                        </Border.Effect>
                        <ContentPresenter ClipToBounds="True" />
                    </Border>
                </Border>
            </ControlTemplate>
        </Window.Template>
        <Grid>
++          <Rectangle x:Name="BackgroundRectangle" Margin="0 16" Fill="#d0d1d6">
++              <Rectangle.RenderTransform>
++                  <TranslateTransform />
++              </Rectangle.RenderTransform>
++              <Rectangle.Triggers>
++                  <EventTrigger RoutedEvent="FrameworkElement.Loaded">
++                      <BeginStoryboard>
++                          <BeginStoryboard.Storyboard>
++                              <Storyboard RepeatBehavior="Forever">
++                                  <DoubleAnimation Storyboard.TargetName="BackgroundRectangle"
++                                                   Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.X)"
++                                                   From="800" To="-800" />
++                              </Storyboard>
++                          </BeginStoryboard.Storyboard>
++                      </BeginStoryboard>
++                  </EventTrigger>
++              </Rectangle.Triggers>
++          </Rectangle>
            <TextBlock FontSize="20" Foreground="#0083d0"
                   TextAlignment="Center" VerticalAlignment="Center">
                <Run Text="欢迎访问吕毅的博客" />
                <LineBreak />
                <Run Text="blog.walterlv.com" FontSize="64" FontWeight="Light" />
            </TextBlock>
        </Grid>
    </Window>

那么性能数据表现如何呢?我们让这个窗口在 2560×1080 的屏幕上全屏渲染,得出以下数据:

方案 WindowChrome AllowsTransparency
帧率(fps)数值越大越好,60 为最好 59 19
脏区刷新率(rects/s)数值越大越好 117 38
显存占用(MB)数值越小越好 83.31 193.29
帧间目标渲染数(个)数值越大越好 2 1

另外,对于显存的使用,如果我在 7680×2160 的屏幕上全屏渲染,WindowChrome 方案依然保持在 80+MB,而 AllowsTransparency 已经达到惊人的 800+MB 了。

可见,对于渲染性能,使用 WindowChrome 制作的背景透明异形窗口性能完虐使用 AllowsTransparency 制作的背景透明异形窗口,实际上跟完全没有设置透明窗口的性能保持一致。

使用 WindowChrome 制作透明窗口的性能数据

使用 AllowsTransparency 制作透明窗口的性能数据

功能对比

既然 WindowChrome 方法在性能上完虐网上流传的设置 AllowsTransparency 方法,那么功能呢?

值得注意的是,由于在使用 WindowChrome 制作透明窗口的时候设置了 ResizeMode="None",所以你拖动窗口在屏幕顶部和左右两边的时候,Windows 不会再帮助你最大化窗口或者靠边停靠窗口,于是你需要自行处理。不过窗口的标题栏拖动功能依然保留了下来,标题栏上的右键菜单也是可以继续使用的。

方案 WindowChrome AllowsTransparency
拖拽标题栏移动窗口 保留 自行实现
最小化最大化关闭按钮 丢失 丢失
拖拽边缘调整窗口大小 丢失 丢失
移动窗口到顶部可最大化 丢失 自行实现
拖拽最大化窗口标题栏还原窗口 保留 自行实现
移动窗口到屏幕两边可侧边停靠 丢失 自行实现
拖拽摇动窗口以最小化其他窗口 保留 自行实现
窗口打开/关闭/最小化/最大化/还原动画 丢失 丢失

表格中:

  • 保留 表示此功能无需任何处理即可继续支持
  • 自行实现 表示此功能已消失,但仅需要一两行代码即可补回功能
  • 丢失 表示此功能已消失,如需实现需要编写大量代码

另外,以上表格仅针对鼠标操作窗口。如果算上使用触摸来操作窗口,那么所有标记为 自行实现 的都将变为 丢失。因为虽然你可以一句话补回功能,但在触摸操作下各种 Bug,你解不完……

这两种实现的窗口之间还有一些功能上的区别:

方案 WindowChrome AllowsTransparency 说明
点击穿透 在完全透明的部分点击依然点在自己的窗口上 在完全透明的部分点击会穿透到下面的其他窗口 感谢 nocanstillbb (huang bin bin) 提供的信息