dotnet 职业技术学院

博客

dotnet 职业技术学院

WPF 程序鼠标在窗口之外的时候,控件拿到的鼠标位置在哪里?

dotnet 职业技术学院 发布于 2019-04-30

在 WPF 程序中,我们有 Mouse.GetPosition(IInputElement relativeTo) 方法可以拿到鼠标当前相对于某个 WPF 控件的位置,也可以通过在 MouseMove 事件中通过 e.GetPosition(IInputElement relativeTo) 方法拿到同样的信息。不过,在任意时刻去获取鼠标位置的时候,如果鼠标在窗口之外,将获取到什么点呢?

本文将介绍鼠标在窗口之外时获取到的鼠标位置。


可用于演示的 DEMO

直接使用 Visual Studio 2019 创建一个空的 WPF 应用程序。默认 .NET Core 版本的 WPF 会带一个文本框和一个按钮。我们现在就用这两个按钮来显示 Mouse.GetPosition 获取到的值。

using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;

namespace Walterlv.Demo
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            CompositionTarget.Rendering += OnRendering; 
        }

        private void OnRendering(object sender, EventArgs e)
        {
            DebugTextBlock.Text = Mouse.GetPosition(DebugTextBlock).ToString();
            DebugButton.Content = Mouse.GetPosition(DebugButton).ToString();
        }
    }
}

观察现象

我们运行这个最简单的 Demo,然后不断移动鼠标,可以观察到一旦鼠标脱离窗口客户区,获取到的坐标点将完全固定。

鼠标在各处时获取到的点坐标

如果不知道客户区是什么,可以阅读下面我的另一篇博客:

在以上图中,我拖动改变了窗口的位置,这时将鼠标移动至离开客户区后,获取到的坐标点又被固定为另一个数值。

推断结论

从上面的动图中以及我实际的测量发现,当鼠标移出窗口的客户区之后,获取鼠标的坐标的时候始终拿到的是屏幕的 (0, 0) 点。如果有多个屏幕,是所有屏幕组合起来的虚拟屏幕的 (0, 0) 点。

验证这一点,我们把窗口移动到屏幕的左上角后,将鼠标移出客户区,左上角的控件其获取到的鼠标位置已经变成了 (0, 31),而这个是窗口标题栏非客户区的高度。

将窗口移至屏幕的左上角

原理

Mouse.GetPosition 获取鼠标相对于控件的坐标点的方法在内部的最终实现是 user32.dll 中的 ClientToScreen

[DllImport("user32.dll")]
static extern bool ClientToScreen(IntPtr hWnd, ref Point lpPoint);

此方法需要使用到一个窗口句柄参数,此参数的含义:

A handle to the window whose client area is used for the conversion.

用于转换坐标点的窗口句柄,坐标会被转换到窗口的客户区部分。

If the function succeeds, the return value is nonzero.
If the function fails, the return value is zero.

如果此方法成功,将返回非零的坐标值;如果失败,将返回 0。

而鼠标在窗口客户区之外的时候,此方法将返回 0,并且经过后面的 ToPoint() 方法转换到控件的坐标下。于是这才得到了我们刚刚观察到的坐标值。

[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);
}

参考资料

如何修改 Visual Studio 新建项目时的默认路径

dotnet 职业技术学院 发布于 2019-04-30

Visual Studio 创建新项目的时候,默认位置在 C:\Users\lvyi\source\repos\ 下。多数时候,我们都希望将其改为一个更适合自己开发习惯的路径。实际上修改默认路径并不是一个麻烦的事情,但是当紧急需要修改的时候,你可能找不到设置项在哪里。

本文介绍如何修改这个默认路径。


默认位置

默认位置在 C:\Users\lvyi\source\repos\ 下。

默认位置

Visual Studio 的设置项

在 Visual Studio 中打开菜单 “工具” -> “选项”;然后找到 “项目和解决方案” -> “位置” 标签。“项目位置” 一栏就是设置新建项目默认路径的地方。

中文版的设置界面

如果是英文本,则打开菜单 “Tools” -> “Options”;然后找到 “Projects and Solutions” -> “Locations” 标签。“Projects location” 一栏就是设置新建项目默认路径的地方。

英文版的设置界面

修改后的默认位置

修改完后,再次新建项目,就可以看到修改后的默认路径了。

修改后的默认位置

使用 dotnet 命令行配合 vscode 完成一个完整 .NET 解决方案的编写和调试

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

如果你是开发个人项目,那就直接用 Visual Studio Community 版本吧,对个人免费,对小团体免费,不需要这么折腾。

如果你是 Mac / Linux 用户,不想用 Visual Studio for Mac 版;或者不想用 Visual Studio for Windows 版那么重磅的 IDE 来开发简单的 .NET Core 程序;或者你就是想像我这么折腾,那我们就开始吧!


安装必要的软件和插件

  1. 点击这里下载正式或者预览版的 .NET Core 然后安装
  2. 点击这里下载 Visual Studio Code 然后安装
  3. 在 Visual Studio Code 里安装 C# for Visual Studio Code 插件(步骤如下图所示)

安装 C# for Visual Studio Code 插件

搜索的时候,推荐使用 OmniSharp 关键字,因为这可以得到唯一的结果,你不会弄混淆。如果你使用 C# 作为关键字,那需要小心,你得找到名字只有 C#,点开之后是 C# for Visual Studio Code 的那款插件。因为可能装错,所以我不推荐这么做。

对于新版的 Visual Studio Code,装完会自动启用,所以你不用担心。我们可以后续步骤了。

创建一个 .NET Core 控制台项目

准备一个空的文件夹,这个文件夹将会成为我们解决方案所在的文件夹,也就是 sln 文件所在的文件夹。在这个空的文件夹中打开 VSCode,然后打开 VSCode 的终端。

在 VSCode 中的终端中输入:

> dotnet new console -o Walterlv.Demo

这样会在当前的文件夹中创建一个 Walterlv.Demo 的子文件夹,并且在此文件夹中新建一个名为 Walterlv.Demo 的控制台项目。

创建一个控制台项目

如果你观察我们刚刚创建的项目,你会发现里面有一个 csproj 文件和一个 Program.cs 文件。csproj 文件是 Sdk 风格的项目文件,而 Program.cs 里面包含最简单的 Hello World 代码:

using System;

namespace Walterlv.Demo
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}

我们会考虑在一个子文件夹中创建项目,是因为我们会一步步创建一个比较复杂的解决方案,用以演示比较完整的使用 VSCode 开发 .NET 程序的过程。

添加一个解决方案

我们现在创建一个在 Visual Studio 会特别熟悉的解决方案,sln 文件。

使用以下命令创建一个解决方案文件:

> dotnet new sln

现在,这个解决方案文件还是空的,不包含任何项目,于是我们把我们一开始创建的 Walterlv.Demo 项目加入到此 sln 文件中。

使用以下命令添加:

> dotnet sln add .\Walterlv.Demo\Walterlv.Demo.csproj

于是,我们的解决方案中,就存在一个可以运行的控制台项目了。

开始调试最简单的程序

理论上,你按下 F5,选择 .NET Core 后就能自动生成调试所需的 launch.json 和 tasks.json 文件:

如果不能生成所需的文件,你可以使用以下博客中的方法,手动添加这两个文件:

在经过以上两篇博客中的方法之后,你将可以跑起来你的程序。

如果遇到了编译错误……呃这么简单的程序怎么可能遇到编译错误呢?一定是因为之前的操作有问题。可以考虑删除 binobj 文件夹,然后输入以下命令自行编译:

> dotnet build

这个命令会还原 NuGet 包,然后使用 .NET Core 版本的 MSBuild 编译你的解决方案。在此之后,你并不需要总是输入此命令,只需要像 Visual Studio 一样按下 F5 即可调试。

引用项目

现在我们演示如何引用项目。

首先使用以下命令创建一个类库项目:

> dotnet new classlib -o Walterlv.Library

将其添加到 sln 中。

> dotnet sln add .\Walterlv.Library\Walterlv.Library.csproj

于是我们的目录结构现在是这样的(稍微改了一点代码)。

目录结构

然后让我们的 Walterlv.Demo 项目引用这个刚刚创建的项目:

> dotnet add Walterlv.Demo reference .\Walterlv.Library\

现在,我们即可在 Program.cs 中使用到刚刚 Class1.cs 中编写的方法(见上面截图中写的方法)。

不过,当你写下 Class1 后,会没有此名称,但有快速操作提示可以自动添加命名空间(就像没有装 ReSharper 的 Visual Studio 的效果一样)。

有快速操作提示

可添加命名空间

有智能感知提示

这时再按下 F5 运行,可以看到多输出了一个 walterlv is a 逗比 这样的提示,我们成功使用到了刚刚引用的类。

运行的结果

引用 NuGet 包

接下来介绍如何引用 NuGet 包。

> dotnet add Walterlv.Demo package Newtonsoft.Json

这样可以给 Walterlv.Demo 项目引用 Newtonsoft.Json 包。

接下来就像前面一节我们所描述的那样使用这个包里面的类就好了。

Visual Studio 通过修改项目的调试配置文件做到临时调试的时候不要编译(解决大项目编译缓慢问题)

dotnet 职业技术学院 发布于 2019-04-26

.NET 托管程序的编译速度比非托管程序要快非常多,即便是 .NET Core,只要不编译成 Native 程序,编译速度也是很快的。然而总是有一些逗比大项目编译速度非常缓慢(我指的是分钟级别的),而且还没做好差量编译;于是每一次编译都需要等待几十秒到数分钟。这显然是非常影响效率的。

在解决完项目的编译速度问题之前,如何能够临时进行快速调试改错呢?本文将介绍在 Visual Studio 中不进行编译就调试的方法。


我找到了两种临时调试而不用编译的方法:

新建一个普通的类库项目,右击项目,属性,打开属性设置页面。进入“调试”标签:

调试标签

现在,将默认的启动从“项目”改为“可执行文件”,然后将我们本来调试时输出的程序路径贴上去。

现在,如果你不希望编译大项目而直接进行调试,那么将启动项目改为这个小项目即可。

Visual Studio 如何能够不进行编译就调试 .NET/C# 项目(用于解决大项目编译缓慢的问题)

dotnet 职业技术学院 发布于 2019-04-26

.NET 托管程序的编译速度比非托管程序要快非常多,即便是 .NET Core,只要不编译成 Native 程序,编译速度也是很快的。然而总是有一些逗比大项目编译速度非常缓慢(我指的是分钟级别的),而且还没做好差量编译;于是每一次编译都需要等待几十秒到数分钟。这显然是非常影响效率的。

在解决完项目的编译速度问题之前,如何能够临时进行快速调试改错呢?本文将介绍在 Visual Studio 中不进行编译就调试的方法。


我找到了两种临时调试而不用编译的方法:

不编译直接调试

有时候只是为了定位 Bug 不断重复运行以调试程序,并没有修改代码。然而如果 Visual Studio 的差量编译因为逗比项目失效的话,就需要手动告诉 Visual Studio 不需要进行编译,直接进行调试。

在 Visual Studio 中设置编译选项

进入 工具 -> 选项 -> 项目和解决方案 -> 生成并运行

打开选项

生成并运行

“当项目过期时”,选择“从不生成”。

顺便附中文版截图:

中文版生成并运行

这时,你再点击运行你的项目的时候,就不会再编译了,而是直接进入调试状态。

这特别适合用来定位 Bug,因为这时基本不改什么代码,都是在尝试复现问题以及查看各种程序的中间状态。

WPF 获取元素(Visual)相对于屏幕设备的缩放比例,可用于清晰显示图片

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

我们知道,在 WPF 中的坐标单位不是屏幕像素单位,所以如果需要知道某个控件的像素尺寸,以便做一些与屏幕像素尺寸相关的操作,就需要经过一些计算(例如得到屏幕的 DPI)。

更繁琐的是,我们的控件可能外面有一些其他的控件做了 RenderTransform 进行了一些缩放,于是了解到屏幕像素单位就更不容易了。

本文将提供一套计算方法,帮助计算某个 WPF 控件相比于屏幕像素尺寸的缩放比例,用于进行屏幕像素级别的渲染控制。


一个 WPF 控件会经历哪些缩放?

如下图,我画了一个屏幕,屏幕里面有一个 WPF 窗口,WPF 窗口里面有一个或者多个 ViewBox 或者设置了 RenderTransform 这样的缩放的控件,一层层嵌套下有我们的最终控件。

这些缩放

于是,我们的控件如何得知此时相比于屏幕像素的缩放比呢?换句话说,如何得知此时此控件的显示占了多少个屏幕像素的宽高呢?

分别计算所有的缩放

从上面的图中,我们可以得知,有两种不同种类的缩放:

  1. 屏幕到 WPF 窗口的缩放
  2. WPF 窗口内部的缩放

屏幕到 WPF 窗口的缩放

我们知道 WPF 的单位叫做 DIP 设备无关单位。不过,我更希望引入 UWP 中的有效像素单位。实际上 WPF 和 UWP 的像素单位含义是一样的,只是 WPF 使用了一个画饼式的叫法,而 UWP 中的叫法就显得现实得多。

你可以阅读我的另一篇博客了解到有效像素单位:

有效像素主要就是考虑了 DPI 缩放。于是实际上我们就是在计算 DPI 缩放。

// visual 是我们准备找到缩放量的控件。
var ct = PresentationSource.FromVisual(visual)?.CompositionTarget;
var matrix = ct == null ? Matrix.Identity : ct.TransformToDevice;

这里,我们使用的是 PresentationSource.FromVisual(visual)?.CompositionTarget 因为不同屏幕可能存在不同的 DPI。

WPF 窗口内部的缩放

WPF 窗口内部的缩放,肯定不会是一层层自己去叠加。

实际上 WPF 提供了方法 TransformToAncestor 可以计算一个两个具有父子关系的控件的相对变换量。

于是我们需要找到 WPF 窗口中的根元素,可以通过不断查找可视化树的父级来找到根。

// VisualRoot 方法用于查找 visual 当前的可视化树的根,如果 visual 已经显示,则根会是窗口中的根元素。
var root = VisualRoot(visual);
var transform = ((MatrixTransform)visual.TransformToAncestor(root)).Value;

我封装的源码

为了方便使用,我进行了一些封装。

要获取某个 Visual 相比于屏幕的缩放量,则调用 GetScalingRatioToDevice 方法即可。

代码已经上传至 gits:https://gist.github.com/walterlv/6015ea19c9338b9e45ca053b102cf456

using System;
using System.Windows;
using System.Windows.Media;

namespace Walterlv
{
    public static class VisualScalingExtensions
    {
        /// <summary>
        /// 获取一个 <paramref name="visual"/> 在显示设备上的尺寸相对于自身尺寸的缩放比。
        /// </summary>
        public static Size GetScalingRatioToDevice(this Visual visual)
        {
            return visual.GetTransformInfoToDevice().size;
        }

        /// <summary>
        /// 获取一个 <paramref name="visual"/> 在显示设备上的尺寸相对于自身尺寸的缩放比和旋转角度(顺时针为正角度)。
        /// </summary>
        public static (Size size, double angle) GetTransformInfoToDevice(this Visual visual)
        {
            if (visual == null) throw new ArgumentNullException(nameof(visual));

            // 计算此 Visual 在 WPF 窗口内部的缩放(含 ScaleTransform 等)。
            var root = VisualRoot(visual);
            var transform = ((MatrixTransform)visual.TransformToAncestor(root)).Value;
            // 计算此 WPF 窗口相比于设备的外部缩放(含 DPI 缩放等)。
            var ct = PresentationSource.FromVisual(visual)?.CompositionTarget;
            if (ct != null)
            {
                transform.Append(ct.TransformToDevice);
            }
            // 如果元素有旋转,则计算旋转分量。
            var unitVector = new Vector(1, 0);
            var vector = transform.Transform(unitVector);
            var angle = Vector.AngleBetween(unitVector, vector);
            transform.Rotate(-angle);
            // 计算考虑了旋转的综合缩放比。
            var rect = new Rect(new Size(1, 1));
            rect.Transform(transform);

            return (rect.Size, angle);
        }

        /// <summary>
        /// 寻找一个 <see cref="Visual"/> 连接着的视觉树的根。
        /// 通常,如果这个 <see cref="Visual"/> 显示在窗口中,则根为 <see cref="Window"/>;
        /// 不过,如果此 <see cref="Visual"/> 没有显示出来,则根为某一个包含它的 <see cref="Visual"/>。
        /// 如果此 <see cref="Visual"/> 未连接到任何其它 <see cref="Visual"/>,则根为它自身。
        /// </summary>
        private static Visual VisualRoot(Visual visual)
        {
            if (visual == null) throw new ArgumentNullException(nameof(visual));

            var root = visual;
            var parent = VisualTreeHelper.GetParent(visual);
            while (parent != null)
            {
                if (parent is Visual r)
                {
                    root = r;
                }
                parent = VisualTreeHelper.GetParent(parent);
            }
            return root;
        }
    }
}

MSBuild 中的特殊字符($ @ % 等):含义、用法以及转义

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

在 MSBuild 中有一些特殊字符,如 $ @ % ' 等,本文介绍他们的含义,如何使用他们,以及你真的需要这些字符的时候如何编写他们。


特殊字符

MSBuild 中有这些特殊字符:

  • $
  • @
  • %
  • '
  • ;
  • ?
  • *

含义和用法

$

引用一个属性或者环境变量。

<Project>
  <ItemGroup>
    <TargetFramework>netcoreapp3.0</TargetFramework>
    <OutputPath>bin\$(Configuration)</OutputPath>
  </ItemGroup>
</Project>

比如以下两篇博客列出了一些最典型的使用场景。

@

引用一个集合。

<Target Name="WalterlvDemoTarget" BeforeTargets="CoreCompile">
    <Message Text="References:" />
    <Message Text="@(Reference)" />
</Target>

比如以下两篇博客列出了一些最典型的使用场景:

%

引用集合中某一个项的某个属性。

<Target Name="Xxx" AfterTargets="AfterBuild">
    <ItemGroup>
        <Walterlv Include="@(Compile)=%(Compile.CopyToOutputDirectory)" />
    </ItemGroup>
    <Warning Text="@(Walterlv)" />
</Target>

比如下面两篇博客列出了此字符的一些使用:

'

在形成一个字符串的时候,会使用到此字符。

下面这篇博客列出了此字符的一些使用:

;

如果存在分号,那么在形成一个集合的时候,会被识别为集合中的各个项之间的分隔符。

有时候你真的需要分号而不是作为分隔符的时候,需要进行转义:

?*

作为通配符使用。一个 * 表示文件或者文件夹通配符,而 ** 则表示任意层级的文件或文件夹。

下面这篇博客虽然古老,却也说明了其用法:

转义

在 MSBuild 中,由于这些特殊字符其实非常常见,所以与一些已有的值很容易冲突,所以需要转义。

转义可以使用 ASCII 编码:

  • $ - %24
  • @ - %40
  • % - %25
  • ' - %27
  • ; - %3B
  • ? - %3F
  • * - %2A

转义方法一:

<Compile Include="Walterlv1%3BWalterlv2.cs"/>

这样得到的将是一个名字为 Walterlv1;Walterlv2.cs 的文件,而不是两个文件。

转义方法二:

<Compile Include="$([MSBuild]::Escape('Walterlv1;Walterlv2.cs'))" />

详细方法可参见:


参考资料

在项目文件 csproj 中或者 MSBuild 的 Target 中使用 % 引用集合中每一项的属性

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

在编写项目文件或者 MSBuild Target 文件的时候,我们经常会使用 <Foo Include="Identity" /> 来定义集合中的一项。在定义的同时,我们也会额外指定一些属性。

然而这些属性如何拿到并且使用呢?本文将介绍使用方法。


将下面的代码放到你项目文件的末尾,最后一个 </Project> 的前面,可以在编译的时候看到两个新的警告。

<Target Name="Xxx" AfterTargets="AfterBuild">
    <ItemGroup>
        <WalterlvX Include="@(Compile)" />
        <WalterlvY Include="%(Compile.FileName)" />
    </ItemGroup>
    <Warning Text="@(WalterlvX)" />
    <Warning Text="@(WalterlvY)" />
</Target>

新增的警告

在定义 WalterlvX 集合的时候,我们使用了 @(Compile) 来获取所有需要编译的文件。

在定义 WalterlvY 集合的时候,我们使用了 %(Compile.FileName) 来获取编译文件的文件名。

于是,你在警告信息中看到的两个警告信息里面,一个输出了 Compile 集合中每一项的标识符(通常是相对于项目文件的路径),另一个输出了每一个 Compile 项中的 FileName 属性。FileName 属性是 Compile 会被 Microsoft.NET.Sdk 自动填充。

需要注意,如果 % 得到的项中某个属性为空,那么这一项在最终形成的新集合中是不存在的。

所以,如果存在可能不存在的属性,那么建议先进行拼接再统一处理拼接后的值:

<Target Name="Xxx" AfterTargets="AfterBuild">
    <ItemGroup>
        <Walterlv Include="@(Compile)=%(Compile.CopyToOutputDirectory)" />
    </ItemGroup>
    <Warning Text="@(Walterlv)" />
</Target>

这里的 CopyToOutputDirectory 不是一个总是会设置的属性。

使用 7-Zip 的命令行版本来压缩和解压文件

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

7-Zip 也有一个简短的名称 7z。它的原生 UI 确实不怎么好看,非常有年代感;不过 7-Zip 的强大之处不在于 UI,而在于其算法和各种文件的支持情况。不过,7-Zip 提供了命令行的版本,让你摒除一切杂念,专心处理压缩文件的工作。

本文介绍如何通过命令行来使用 7-Zip。因为使用命令行,所以你甚至可以自动化地完成压缩文件的各种处理。


如何找到 7-Zip 的命令行版本

请前往官方网站下载 7-Zip:

下载安装完去其安装目录下可以找到 7-Zip 的命令行版本:

7-Zip 的安装目录

这些文件作用分别是:

  • 7zFM.exe 7-Zip 文件管理器的主 UI,直接从开始菜单打开 7-Zip 时的 UI 界面。依赖 7z.dll
  • 7zG.exe 7-Zip 的 GUI 模块,需要通过命令行指定参数调用。依赖 7z.dll
  • 7-zip.dll 与 Windows Shell 以及 7zFM.exe 集成。
  • 7z.exe 7-Zip 的命令行版本,需要通过命令行指定参数调用。
  • 7z.dll 7-Zip 的核心执行引擎。
  • 7z.sfx SFX 模块(Windows 版本)。
  • 7zCon.sfx SFX 模块(控制台版本)。
  • 7-zip.chm 7-Zip 的帮助说明文件。

命令行版本的 7z.exe 不依赖与其他 dll,所以我们将 7z.exe 文件拷出来即可使用完整的命令行版本的 7z。

使用命令行操作 7z.exe

如果你希望使用 .NET/C# 代码来自动化地调用 7z.exe,可以参考我的另一篇博客:

本文直接介绍 7z.exe 的命令行使用,你可以将其无缝地迁移至上面这篇博客中编写的 .NET/C# 代码中。

解压一个文件

> 7z x {fileName} -o{outputDirectory}

以上:

  • x 表示解压一个文件
  • {fileName} 是文件名称或者文件路径的占位符
  • {outputDirectory} 是解压后文件夹的占位符,必须是一个不存在的文件夹。
  • -o 表示指定输出路径

特别注意:-o{outputDirectory} 之间是 没有空格 的。

一个例子:

> 7z x C:\Users\walterlv\demo.7z -oC:\Users\walterlv\demo

7z 的强大之处还有一点就是可以解压各种文件——包括解压安装包:

> 7z x C:\Users\walterlv\nsis_installer_1.0.0.0.exe -oC:\Users\walterlv\nsis

这也是为什么我们考虑使用 7z 来解压缩,而不是使用相关的 NuGet 包来调用。

其他命令行操作

运行 7z.exe 后可以看到命令行中列出了可用的命令行命令:

a:将文件添加到压缩档案中
b:测试压缩或解压算法执行时的 CPU 占用
d:从压缩档案中删除文件
e:将压缩档案中的所有文件解压到指定路径,所有文件将输出到同一个目录中
h:计算文件的哈希值
i:显示有关支持格式的信息
l:列出压缩档案的内容
rn:重命名压缩档案中的文件
t:测试压缩档案的完整性
u:更新要进入压缩档案中的文件
x:将压缩档案中的所有文件解压到指定路径,并包含所有文件的完整路径

下面列出几个常用的命令。

a 添加文件

如果你需要压缩文件,或者将文件添加到现有的压缩档案中,则使用此命令。

将 subdir\ 文件夹中的所有文件加入到 walterlv.zip 文件中,所有的子文件和文件夹将会在压缩档案的 subdir 文件夹中:

7z a walterlv.zip subdir\

将 subdir\ 文件夹中的所有文件加入到 walterlv.zip 文件中,所有的子文件和文件夹路径不会包含 subdir 前缀:

7z a walterlv.zip .\subdir\*

d 删除文件

删除压缩档案 walterlv.zip 中的所有扩展名为 bak 的文件:

7z d walterlv.zip *.bak -r

e 解压文件

相比于 x,此命令会将压缩档案中的所有文件输出到同一个目录中。

ClearType 的原理:Windows 上文本的亚像素控制

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

有位小伙伴问我为什么他电脑上的文本看起来比较虚。我去看了下,发现他电脑上关掉了 ClearType。

微软的 ClearType 技术通过控制亚像素来使得文本显示更为清晰。本文代理了解 Windows 系统上的文本是如何通过亚像素控制使得显示更为清晰的。


ClearType 打开和关闭之后的效果

看下图!同样的文本,在不同大小下以及开关 ClearType 下的显示效果:

文本效果预览

你应该能注意到,第 0x00 行,第 0x02 行是比较模糊的,第 0x01 行和第 0x03 行会更清晰一些。

如果你看不出来我说的效果,那么你需要调整你看图的姿势:

  1. 请确保以 100% 比例显示此图片,正在在电脑上看我博客的时候,就会以 100% 比例显示;
  2. 如果你看博客的显示器 DPI 不是 100%,那么也看不出效果,建议在一个 100% DPI 的显示器设备上看。

如果依然看不出来,至少你能感受到第 0x01 行和第 0x03 行的文本会更亮一些。

现在,我们将图片放大。就像下面这张图片一样,左边一半是没有启用 ClearType 的文本,右边是启用了 ClearType 的文本。我将他们放到了一张图片上以便更容易比较效果。

放大后的文本预览

可以注意到,没有开启 ClearType 的文本,其发虚的边框周围是灰色;而开启了 ClearType 的文本,其发虚的边框周围是彩色。

如何显示清晰的线条

像素内的 RGB

在开始显示线条之前,我们来看看显示器如何显示一个像素。下图是我放大的一个像素内的灯管。这是一种主流显示器上像素内的 RGB 排列。这三个灯管同时以规定的最大值亮起,我们将看到白色。当然,我放大这么大你是看不出来白色的,需要足够小才行。

一个像素

现在,我们缩小一点,观察 4×4 个像素:

4×4 个像素

清晰显示 1px 线条

我在另一篇博客中说过如何清晰显示一个线条:

要清晰显示 1 像素宽度的竖线,我们需要对齐像素显示,即在整数像素上显示这根线条。于是,我们需要点亮这一列像素中的所有 RGB:

亮起一列像素中的全部 RGB

嗯,最终看起来会像这样:

清晰显示的白色线条

清晰显示 1.3 px 线条

那么接下来,如何清晰显示 1.33 像素宽度的竖线呢?

传统方法是借用旁边像素,点亮旁边像素 33% 的亮度,于是线条大概是这样的:

传统 1.33 像素的线条

对应到灯管,大概是这样:

传统 1.33 像素亮起的灯管

但是,这样显示 1.33 像素使用了 2 个像素的宽度,用了 6 个灯管。

然而如果亮起的灯管是这样的:

亚像素控制的 1.33 像素灯管

因为现在依然是 RGB 三个灯管紧挨着一起量的,所以人类依然会看出白色来。由于此时灯管亮起的依然是硬边缘,所以依然清晰。

要控制这样亮起灯管,我们需要在左边像素显示白色,右边像素显示红色。

亚像素控制的 1.33 像素的线条

在这个线条中,右边的线条因为是红色,所以只会亮起红色灯管,而这是最靠近左边像素的灯管。

清晰显示 1.7 px 线条

同样的,如果要清晰显示 1.67 像素宽度的竖线,我们需要使用 5 列灯管:

亚像素控制的 1.67 像素灯管

这时,我们不止借用了右边像素显示红色,还借用了左边像素显示蓝色:

亚像素控制的 1.67 像素的线条

当然,也可以是在右边借用一个黄色的像素,也就是亮起 RG 两列灯管。借用哪一边取决于需要从像素的哪个位置开始显示。

文本的亚像素控制

由于文本的显示不像简单图形显示可以随意选取起点,文本因为图形非常复杂,为了保持文本形状不至于变形太多,任何位置开始显示一个像素的起点都是可能的,所以文本需要更多地选择借用左右像素的相邻灯管。

使用了 ClearType 效果的单个文字

在这张图中,果字最中间的竖线,借用了左侧像素的蓝色灯管,借用了右侧像素的红色和绿色灯管。横线的最右边,借用了右侧像素的红色灯管。其他像素以此类推。

ClearType

实际上,本文使用的显示器是 RGB 排列的,其他显示器还有更多像素排列方式,Windows 系统会自动根据像素排列方式选择合适的 ClearType 借用临近灯管的方式。

不过,识别错也是常态,你需要在 Windows 10 搜索框中输入 ClearType 打开 ClearType 的设置界面,选择最清晰的显示文字来调整这样的错误识别。

ClearType 设置 1

ClearType 设置 2

C#/.NET 使用 git 命令行来操作 git 仓库

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

我们可以在命令行中操作 git,但是作为一名程序员,如果在大量重复的时候还手动敲命令行,那就太笨了。

本文介绍使用 C# 编写一个 .NET 程序来自动化地使用 git 命令行来操作 git 仓库。

这是一篇很基础的入门文章。


最简单的运行 git 命令的代码

在 .NET 中,运行一个命令只需要使用 Process.Start 开启一个子进程就好了。于是要运行一个 git 命令,我们其实只需要这句足以:

Process.Start("git", "status");

当然,直接能简写成 git 是因为 git.exe 在我的环境变量里面,一般开发者在安装 Git 客户端的时候,都会自动将此命令加入到环境变量。如果没有,你需要使用完整路径 C:\Program Files\Git\mingw64\bin\git.exe 只是每个人的路径可能不同,所以这是不靠谱的。

允许获得命令的输出

对于上节中写的 Process.Start,你一眼就能看出来这是完全没有用的代码。因为 git status 命令只是获得仓库当前的状态,这个命令完全不影响仓库,只是为了看状态的。

所以,命令最好要能够获得输出。

而要获得输出,你需要使用 ProcessStartInfo 来指定如何启动一个进程。

var info = new ProcessStartInfo(ExecutablePath, arguments)
{
    CreateNoWindow = true,
    RedirectStandardOutput = true,
    UseShellExecute = false,
    WorkingDirectory = WorkingDirectory,
};

需要设置至少这四个属性:

  • CreateNoWindow 表示不要为这个命令单独创建一个控制台窗口
    • 实际上如果使用此代码的程序也是一个控制台程序,这句是没有必要的,因为子进程会共用父进程的控制台窗口;但是对于 GUI 程序来说,这句还是很重要的,这可以避免在执行命令的过程中意外弹出一个黑色的控制台窗口出来。
  • RedirectStandardOutput 进行输出的重定向
    • 这是一定要设置为 true 的属性,因为我们希望拿到命令的输出结果。
  • WorkingDirectory 设置工作路径
    • 本来这是一个可选设置,不过对于 git 命令来说,一般都是对一个已有的 git 仓库进行操作,所以当然要指定一个合理的 git 仓库了。
  • UseShellExecute 设置为 false 表示不要使用 ShellExecute 函数创建进程
    • 此属性的详细说明,请阅读我的另一篇博客:ProcessStartInfo 中的 UseShellExecute - 吕毅
    • 这里我们必须指定为 false,因为要重定向输出的话,这是唯一有效值。顺便一提,此属性如果不设置,默认值是 true

CommandRunner

为了方便起见,我将全部运行一个命令的代码封装到了一个 CommandRunner 的类当中。

using System;
using System.Diagnostics;
using System.IO;

namespace Walterlv.GitDemo
{
    public class CommandRunner
    {
        public string ExecutablePath { get; }
        public string WorkingDirectory { get; }

        public CommandRunner(string executablePath, string? workingDirectory = null)
        {
            ExecutablePath = executablePath ?? throw new ArgumentNullException(nameof(executablePath));
            WorkingDirectory = workingDirectory ?? Path.GetDirectoryName(executablePath);
        }

        public string Run(string arguments)
        {
            var info = new ProcessStartInfo(ExecutablePath, arguments)
            {
                CreateNoWindow = true,
                RedirectStandardOutput = true,
                UseShellExecute = false,
                WorkingDirectory = WorkingDirectory,
            };
            var process = new Process
            {
                StartInfo = info,
            };
            process.Start();
            return process.StandardOutput.ReadToEnd();
        }
    }
}

测试与结果

以上 CommandRunner 命令的使用非常简单,new 出来之后,得到一个可以用来执行命令的实例,然后每次执行调用 Run 方法传入参数即可。

var git = new CommandRunner("git", @"D:\Developments\Blogs\walterlv.github.io");
git.Run("add .");
git.Run(@"commit -m ""这是自动提交的""");

如果需要获得命令的执行结果,直接使用 Run 方法的返回值即可。

比如下面我贴了 Main 函数的完整代码,可以输出我仓库的当前状态:

using System;

namespace Walterlv.GitDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("walterlv 的自动 git 命令");

            var git = new CommandRunner("git", @"D:\Developments\Blogs\walterlv.github.io");
            var status = git.Run("status");

            Console.WriteLine(status);
            Console.WriteLine("按 Enter 退出程序……");
            Console.ReadLine();
        }
    }
}

运行结果

如何快速自定义 Visual Studio 中部分功能的快捷键

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

Visual Studio 中有些自带的快捷键与现有软件有冲突,那么如何修改这些快捷键让这些功能正常工作起来呢?


打开快捷键设置界面

在 Visual Studio 中打开 “工具 -> 选项”,打开选项设置界面。在其中找到 “环境 -> 键盘” 项。我们设置快捷键的地方就在这里。

工具 -> 选项 -> 环境 -> 键盘

修改一个现有功能的快捷键

默认情况下,在 Visual Studio 2019 中快速重构的快捷键是 Ctrl+.。然而,使用中文输入法的各位应该非常清楚,Ctrl+. 是输入法切换中英文符号的快捷键。

于是,当使用中文输入法的时候,实际上是无法通过按下 Ctrl+. 来完成快速重构的。我们需要修改快捷键来避免这样的冲突。

使用 Ctrl+. 来进行快速重构

在“新快捷键”那个框框中,按下 Ctrl+.,正常会在“快捷键的当前使用对象”框中出现此快捷键的功能。不过,如果快捷键已经与输入法冲突,则不会出现,你需要先切换至英文输入法以避免此冲突。

显示此快捷键的当前功能

通过“快捷键的当前使用对象”下拉框,我们可以得知功能的名称,下拉框中的每一项都是此快捷键的功能。

快捷键的当前使用对象

我们需要做的是,搜索这些功能,并为这些功能分配新的快捷键。每一个我们关心的功能都这么设置:

设置快捷键

于是新快捷键就设置好了。

新分配的快捷键

现在,可以使用新的快捷键来操作这些功能了。

可以使用新的快捷键


参考资料

WPF 像素着色器入门:使用 Shazzam Shader Editor 编写 HLSL 像素着色器代码

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

HLSL,High Level Shader Language,高级着色器语言,是 Direct3D 着色器模型所必须的语言。WPF 支持 Direct3D 9,也支持使用 HLSL 来编写着色器。你可以使用任何一款编辑器来编写 HLSL,但 Shazzam Shader Editor 则是专门为 WPF 实现像素着色器而设计的一款编辑器,使用它来编写像素着色器,可以省去像素着色器接入到 WPF 所需的各种手工操作。

本文是 WPF 编写 HLSL 的入门文章,带大家使用 Shazzam Shader Editor 来编写最简单的像素着色器代码。


下载安装

实际上 Shazzam Shader Editor 有一段时间没有维护了,不过在 WPF 下依然是一个不错的编写 HLSL 的工具。

下载完成之后安装到你的电脑上即可。

Shazzam 是开源的,但是官方开源在 CodePlex 上,https://archive.codeplex.com/?p=shazzam,而 CodePlex 已经关闭。JohanLarsson 将其 Fork 到了 GitHub 上,https://github.com/JohanLarsson/Shazzam,不过几乎只有代码查看功能而不提供维护。

Shazzam Shader Editor

主界面

Shazzam 的主界面

打开 Shazzam,左侧会默认选中 Sample Shaders 即着色器示例,对于不了解像素着色器能够做到什么效果的小伙伴来说,仅浏览这里面的特效就能够学到很多好玩的东西。

旁边是 Tutorial 教程,这里的教程是配合 HLSL and Pixel Shaders for XAML Developers 这本书来食用的,所以如果希望能够系统地学习 HLSL,那么读一读这本书跟着学习里面的代码吧!

左边的另一个标签是 Your Folder,可以放平时学习 HLSL 时的各种代码,也可以是你的项目代码,这里会过滤出 .fx 文件用于编写 HLSL 代码。

如果你打开关于界面,你可以看到这款软件很用心地在关于窗口背后使用了 TelescopicBlur 特效,这是一个 PS_3 特效,后面会解释其含义。

加了特效的关于界面

公共设置

依然在左侧,可以选择 Settings 设置。

Shazzam 设置

目标框架 Target Framework

WPF 自 .NET Framework 4.0 开始支持 PS_3,当然也包括现在的 .NET Core 3.0。如果你不是为了兼容古老的 .NET Framework 3.5 或者更早版本,则建议将默认的 PS_2 修改为 PS_3。因为 PS_2 的限制还是太多了。

关于 PS_3 相比于此前带来的更新可以查看微软的官方文档了解:ps_3_0 - Windows applications - Microsoft Docs

生成的命名空间 Generated Namespace

默认是 Shazzam,实际上在接入到你的项目的时候,这个命名空间肯定是要改的,所以建议改成你项目中需要使用到的命名空间。比如我的是 Walterlv.Effects

改好之后,如果你编译你的 .fx 文件,也就是编写了 HLSL 代码的文件,那么顺便也会生成一份使用 Walterlv.Effects 命名空间的 C# 代码便于你将此特效接入到你的 WPF 应用程序中。

缩进 Indentation

默认的缩进是 Tab,非常不清真,建议改成四个空格。

默认动画时长 Default Animation Length

如果你的特效是为了制作动画(实际上在 Shazzam 中编写的 HLSL,任何一个寄存器(变量)都可以拿来做动画),那么此值将给动画设置一个默认的时长。

相比于前面的所有设置,这个设置不会影响到你的任何代码,只是决定你预览动画效果时的时长,所以设置多少都没有影响。

编写 HLSL 代码

HLSL 代码窗格

实际上本文不会教你编写任何 HLSL 代码,也不会进行任何语法入门之类的,我们只需要了解 Shazzam 是如何帮助我们为 WPF 程序编写像素着色器代码的。

将你的视线移至下方富含代码的窗格,这里标记着 XXX.fx 的标签就是 HLSL 代码了。大致浏览一下,你会觉得这风格就是 C 系列的语言风格,所以从学校里出来的各位应该很有亲切感,上手难度不高。

按下 F5,即可立即编译你的 HLSL 代码,并在界面上方看到预览效果。别说你没有 HLSL 代码,前面我们可是打开了那么多个示例教程呀。

预览调节窗格

确保你刚刚使用 F5 编译了你的 HLSL 代码。这样,你就能在这个窗格看到各种预览调节选项。

预览调节

你可以直接拉动拉杆调节参数范围,也可以直接开启一个动画预览各种值的连续变化效果。

生成的 C# 代码

继续切换一个标签,你可以看到 Shazzam 为你生成的 C# 代码。实际上稍后你就可以直接使用这份代码驱动起你刚刚编写的特效。

代码风格使用了我们刚刚设置的一些全局参数。

生成的 C# 代码

将像素着色器放到 WPF 项目中

将像素着色器放到 WPF 项目中需要经过两个步骤:

  1. 找到生成的像素着色器文件,并放入 WPF 工程中;
  2. 修改像素着色器的生成方式。

将特效放入到你的 WPF 项目中

我们需要将两个文件加入到你的 WPF 程序中:

  1. 一个 .ps 文件,即刚刚的 .fx 文件编译后的像素着色器文件;
  2. 一份用于驱动此像素着色器的 C# 代码。

这些文件都可以使用以下方法找到:

  1. 请前往 %LocalAppData%\Shazzam\GeneratedShaders 文件夹;
  2. 根据名称变化规则找到对应的文件夹:
    • 注意命名,如果你的 .fx 文件命名为 walterlv.fx,那么生成的文件就会在 WalterlvEffect 文件夹下
  3. 进入刚刚找到的 XxxEffect 文件夹,里面有你需要的所有文件:
    • 一个 .ps 文件
    • 一个 C# 文件(以及 VB 文件)

随后,将这两份文件一并加入到你的 WPF 项目工程文件中。

但是,请特别注意路径!留意你的 C# 代码,里面是编写了像素着色器的路径的:

  1. 如果你的程序集名称是其他名称,需要修改下面 Walterlv.Effects 的部分改成你的程序集名称;
  2. 如果你放到了其他的子文件夹中,你也需要在下面 /WalterlvEffect.ps 的前面加上子文件夹。
// 记得修改程序集名称,以及 .ps 文件所在的文件夹路径!切记!
pixelShader.UriSource = new Uri("/Walterlv.Effects;component/WalterlvEffect.ps", UriKind.Relative);

修改像素着色器的生成方式

需要使用 Resource 方式编译此 .ps 文件到 WPF 项目中。

如果你使用的是旧的项目格式,则右键此 .ps 文件的时候选择属性,你可以在 Visual Studio 的属性窗格的生成操作中将其设置为 Resource

右键属性

使用 Resource 编译

如果你使用的是 Sdk 风格的新项目格式,则在属性窗格中无法将其设置为 Resource,这个时候请直接修改 .csproj 文件,加上下面一行:

<Resource Include="**\*.ps" />

如果不知道怎么放,我可以多贴一些 csproj 的代码,用于指示其位置:

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

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>netcoreapp3.0</TargetFramework>
    <UseWPF>true</UseWPF>
    <AssemblyName>Walterlv.Demo</AssemblyName>
  </PropertyGroup>

  <ItemGroup>
    <Resource Include="**\*.ps" />
  </ItemGroup>

</Project>

在 WPF 程序中使用这个特效

要在 WPF 程序中使用这个特效,则设置控件的 Effect 属性,将我们刚刚生成的像素着色器对应 C# 代码的类名写进去即可。当然,需要在前面引入 XAML 命名空间。

<Window x:Class="Walterlv.CloudTyping.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:effects="clr-namespace:Walterlv.Effects"
        Title="walterlv">
    <Grid>
        <Grid.Effect>
            <effects:WalterlvEffect />
        </Grid.Effect>
        <!-- 省略了界面上的各种代码 -->
    </Grid>
</Window>

下面是我将 Underwater 特效加入到我的云键盘窗口中,给整个窗口带来的视觉效果。

云键盘的水下特效

入门总结

本文毕竟是一篇入门文章,没有涉及到任何的技术细节。你可以按照以下问题检查是否入门成功:

  1. 你能否成功安装并打开 Shazzam Shader Editor 软件?
  2. 你能否找到并打开一个示例像素着色器代码,并完成编译预览效果?
  3. 知道如何设置像素着色器使用 PS_3 版本吗?
  4. 尝试将一个示例像素着色器编译完并放入到你的 WPF 项目中。
  5. 尝试将特效应用到你的一个 WPF 控件中查看其效果。

参考资料

如何在 MSBuild 的项目文件 csproj 中获取绝对路径

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

通常我们能够在 csproj 文件中仅仅使用相对路径就完成大多数的编译任务。但是有些外部命令的执行需要用到绝对路径,或者对此外部工具来说,相对路径具有不同的含义。这个时候,就需要将相对路径在 csproj 中转换为绝对路径来使用。

本文介绍如何在项目文件 csproj 中将一个相对路径转换为绝对路径。


在 MSBuild 4.0 中,可以在 csproj 中编写调用 PowerShell 脚本的代码,于是获取一个路径的绝对路径就非常简单:

[System.IO.Path]::GetFullPath('$(WalterlvRelativePath)')

具体到 csproj 的代码中,是这样的:

<Project>
    <PropertyGroup>
        <WalterlvRelativePath>$(OutputPath)</WalterlvRelativePath>
        <_WalterlvAbsolutePath>$([System.IO.Path]::GetFullPath($(WalterlvRelativePath)))</_WalterlvAbsolutePath>
    </PropertyGroup>
</Project>

这样,就可以使用 $(_WalterlvAbsolutePath) 属性来获取绝对路径。

你可以阅读我的其他篇博客了解到 $(OutputPath) 其实最终都会是相对路径:


参考资料

MSBuild 如何编写带条件的属性、集合和任务 Condition?

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

在项目文件 csproj 中,通过编写带条件的属性(PropertyGroup)、集合(ItemGroup)和任务(Target)可以完成更加复杂的项目文件的功能。

本文介绍如何编写带条件的 MSBuild 项。


Condition

如果要给你的 MSBuild 项附加条件,那么加上 Condition 特性即可。

Condition 可以写在任何地方,例如 PropertyGroupItemGroupTarget 或者内部的一个属性或一个项或者一个任务等。

下面这段代码表示在 Debug 配置下计算一个属性的值,而这个逗比属性 DoubiNames 的属性仅在此属性从未被指定过值的时候赋一个值 吕毅

<Project>
    <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
        <DoubiNames Condition=" '$(DoubiNames)' == '' ">吕毅</DoubiNames>
    </PropertyGroup>
</Project>

在单引号的前后,等号这些运算符的前后空格可加可不加,没有影响。

单引号

在上面的例子中,我们给条件中的所有字符串加上了包裹的单引号。

单引号对于简单的字母数字字符串是不必要的,对于布尔值来说也是不必要的。但是,对于空值来说,是必须加上的,即 ''

==!=

== 符号左右两侧的字符串如果相等,则返回 true,否则返回 false

!= 符号左右两侧的字符串如果相等,则返回 false,否则返回 true

Condition=" $(Configuration) == 'Debug' "

<, >, <=, >=

用于比较数值上的大小关系。当然,在项目文件中,用于表示数值的字符串在此操作符下表示的就是数值。

  1. 左右两侧比较的字符串必须是表示数值的字符串,例如 123 或者 0x7b
  2. 只能是十进制或者十六进制字符串,而十六进制字符串必须以 0x 开头;
  3. 由于此比较是写在 XML 文件中的,所以必须转义,即 < 需要写成 &lt;> 需要写成 &gt;

Exists, HasTrailingSlash

Exists 判断文件或者文件夹是否存在。存在则返回 true,否则返回 false

Condition=" Exists('Foo\walterlv.config') "
Condition=" Exists('Foo\WalterlvFolder') "
Condition=" Exists('$(WalterlvFile)') "

HasTrailingSlash 如果字符串的尾部包含 / 或者 \ 字符串,则返回 true,否则返回 false

Condition="!HasTrailingSlash($(OutputPath))"

与或非:And, Or, !

就是计算机中常见的与或非的机制。

<DoubiNames Condition=" '$(DoubiNames)' == '吕毅' Or '$(DoubiNames)' == '林德熙' ">组队逗比</DoubiNames>

组合:()

就是计算机中通常用于修改运算优先级的括号,这可以先计算括号内的布尔结果。

if 条件:$if$

Condition=" $if$ ( %expression% ), $else$, $endif$ "

参考资料