dotnet 职业技术学院

博客

dotnet 职业技术学院

使用 .editorconfig 配置 .NET/C# 项目的代码分析规则的严重程度

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

随着 Visual Studio 2019 更新,在 Visual Studio 中编写代码的时候也带来了基于 Roslyn 的代码质量分析。有一些代码分析严重程度可能与团队约定的不一致,这时就需要配置规则的严重程度。另外如果是个人使用插件安装了分析器,也可以配置一些严重程度满足个人的喜好。

本文介绍使用 .editorconfig 文件来配置 .NET/C# 项目中,代码分析规则的严重性。可以是全局的,也可以每个项目有自己的配置。


生效范围与继承

.editorconfig 文件可以在你的项目中的任何地方,甚至是代码仓库之外。是按照文件夹结构来继承生效的。

比如我的项目结构是这样:

+ Walterlv.Demo
    + Core
        - .editorconfig
        - Foo.cs
    - .editorconfig
    - Program.cs

项目结构

那么 Foo.cs 文件的规则严重性将受 Core 文件夹中的 .editorconfig 文件管理,如果有些规则不在此文件夹的 .editorconfig 里面,就会受外层 .editorconfig 管理。

另外,你甚至可以在整个代码仓库的外部文件夹放一个 .editorconfig 文件,这样,如果项目中没有对应的规则,那么外面文件夹中的 .editorconfig 规则就会生效,这相当于间接做了一个全局生效的规则集。

.editorconfig 中的内容

.editorconfig 中的分析器严重性内容就像下面这样:

[*.cs]

# CC0097: You have missing/unexistent parameters in Xml Docs
dotnet_diagnostic.CC0097.severity = error

# CA1031: Do not catch general exception types
dotnet_diagnostic.CA1031.severity = suggestion

# IDE0051: 删除未使用的私有成员
dotnet_diagnostic.IDE0051.severity = none

对于 C# 语言的规则,在 [*.cs] 区,每个规则格式是 dotnet_diagnostic.{DiagnosticId}.severity = {Severity}

当然,我们不需要手工书写这个文件,了解它的格式只是为了出问题的时候不至于一脸懵逼。

配置严重程度

使用 Visual Studio 2019,配置规则严重性非常简单。当然,16.3 以上版本才这么简单,之前的版本步骤多一点。

配置规则严重性

在提示有问题的代码上按下重构快捷键(默认是 Ctrl + .),可以出现重构菜单,其中就有配置规则严重性的选项,直接选择即可自动添加到 .editorconfig 文件中。如果项目中没有 .editorconfig 文件,则会自动在解决方案同目录下创建一个新的。

对这部分快捷键不了解的话可以阅读:提高使用 Visual Studio 开发效率的键盘快捷键 - walterlv

什么是模态窗口?本文带你了解模态窗口的本质

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

做 Windows 桌面应用开发的小伙伴们对“模态窗口”(Modal Dialog)一定不陌生。如果你希望在模态窗口之上做更多的事情,或者自己实现一套模态窗口类似的机制,那么你可能需要了解模态窗口的本质。

本文不会太深,只是从模态窗口一词出发,抵达大家都熟知的一些知识为止。


开发中的模态窗口

在各种系统、语言和框架中,只要有用户可以看见的界面,都存在模态窗口的概念。从交互层面来说,它的形式是在保留主界面作为环境来显示的情况下,打开一个新的界面,用户只能在新的界面上操作,完成或取消后才能返回主界面。从作用上来说,通常是要求用户必须提供某些信息后才能继续操作,或者单纯只是为了广告。

模态窗口的三个特点

如果你希望自己搞一套模态窗口出来,那么只需要满足这三点即可。你可以随便加戏但那都无关紧要。

  1. 保留主界面显示的同时,禁用主界面的用户交互;
  2. 显示子界面,主界面在子界面操作完成后返回;
  3. 当用户试图跳过子界面的交互的时候进行强提醒。

拿 Windows 系统中的模态对话框为例子,大概就像下面这两张图片这样:

有一个小的子界面盖住了主界面,要求用户必须进行选择。Windows 系统设置因为让背景变暗了,所以用户肯定会看得到需要进行的交互;而任务管理器没有让主界面变暗,所以用户在操作子界面的时候,模态窗口的边框和标题栏闪烁以提醒用户注意。

Windows 系统设置

任务管理器

实现模态窗口

对于 Windows 操作系统来说,模态窗口并不是一个单一的概念,你并不能仅通过一个 API 调用就完成显示模态窗口,你需要在不同的时机调用不同的 API 来完成一个模态窗口。如果要完整实现一个自己的模态窗口,则需要编写实现以上三个特点的代码。

当然,你可能会发现实际上你显示一个模态窗口仅仅一句话调用就够了,那是因为你所用的应用程序框架帮你完成了模态窗口的一系列机制。

关于 WPF 框架是如何实现模态窗口的,可以阅读:直击本质:WPF 框架是如何实现模态窗口的

关于如何自己实现一个跨越线程/进程边界的模态窗口,可以阅读:实现 Windows 系统上跨进程/跨线程的模态窗口

如果你希望定制以上第三个特点中强提醒的动画效果,可以阅读:WPF window 子窗口反馈效果(抖动/阴影渐变) - 唐宋元明清2188 - 博客园

API 调用

为了在 Windows 上实现模态窗口,需要一些 Win32 API 调用(当然,框架够用的话直接用框架就好)。

禁用主窗口

我们需要使用到 BOOL EnableWindow(HWND hWnd, BOOL bEnable); 来启用与禁用某个窗口。

EnableWindow(hWnd, false);
try
{
    // 模态显示一个窗口。
}
finally
{
    EnableWindow(hWnd, true);
}
[DllImport("user32")]
private static extern bool EnableWindow(IntPtr hwnd, bool bEnable);

阻塞代码等待操作完成

因为 async/await 的出现,阻塞其实可以使用 await 来实现。虽然这不是真正的阻塞,但可以真实反应出“异步”这个过程,也就是虽然这里在等待,但实际上依然能够继续在同一个线程响应用户的操作。

UWP 中的新 API 当然已经都是使用 async/await 来实现模态等待了,不过 WPF/Windows Forms 比较早,只能使用 Dispatcher 线程模型来实现模态等待。

于是我们可以考虑直接使用现成的 Dispatcher 线程模型来完成等待,方法是调用下面两个当中的任何一个:

  • Window.ShowDialog 也就是直接使用窗口原生的模态
  • Dispatcher.PushFrame 新开一个消息循环以阻塞当前代码的同时继续响应 UI 交互

上面 Window.ShowDialog 的本质也是在调用 Dispatcher.PushFrame,详见:

关于 PushFrame 新开消息循环阻塞的原理可以参考:

当然,还有其他可以新开消息循环的方法。

进行 UI 强提醒

由于我们一开始禁用了主窗口,所以如果用户试图操作主窗口是不会有效果的。然而如果用户不知道当前显示了一个模态窗口需要操作,那么给出提醒也是必要的。

简单的在 UI 上的提醒是最简单的了,比如:

  • 将主界面变暗(UWP 应用,Web 应用喜欢这么做)
  • 将主界面变模糊(iOS 应用喜欢这么做)
  • 在模态窗口上增加一个很厚重的阴影(Android 应用喜欢这么做)

然而 Windows 和 Mac OS 这些古老的系统由于兼容性负担不能随便那么改,于是需要有其他的提醒方式。

Windows 采用的方式是让标题栏闪烁,让阴影闪烁。

而这些特效的处理,来自于子窗口需要处理一些特定的消息 WM_SETCURSOR

详见:WPF window 子窗口反馈效果(抖动/阴影渐变) - 唐宋元明清2188 - 博客园

通常你不需要手工处理这些消息,但是如果你完全定制了窗口样式,则可能需要自行做一个这样的模态窗口提醒效果。

直击本质:WPF 框架是如何实现模态窗口的

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

想知道你在 WPF 编写 Window.ShowDialog() 之后,WPF 框架是如何帮你实现模态窗口的吗?

本文就带你来了解这一些。


Window.ShowDialog

WPF 显示模态窗口的方法就是 Window.ShowDialog,因此我们直接进入这个方法查看。由于 .NET Core 版本的 WPF 已经开源,我们会使用 .NET Core 版本的 WPF 源代码。

Window.ShowDialog 的源代码可以在这里查看:

这个方法非常长,所以我只把其中与模态窗口最关键的代码和相关注释留下,其他都删除(这当然是不可编译的):

public Nullable<bool> ShowDialog()
{
    // NOTE:
    // _threadWindowHandles is created here.  This reference is nulled out in EnableThreadWindows
    // when it is called with a true parameter.  Please do not null it out anywhere else.
    // EnableThreadWindow(true) is called when dialog is going away.  Once dialog is closed and
    // thread windows have been enabled, then there no need to keep the array list around.
    // Please see BUG 929740 before making any changes to how _threadWindowHandles works.
    _threadWindowHandles = new ArrayList();
    //Get visible and enabled windows in the thread
    // If the callback function returns true for all windows in the thread, the return value is true.
    // If the callback function returns false on any enumerated window, or if there are no windows
    // found in the thread, the return value is false.
    // No need for use to actually check the return value.
    UnsafeNativeMethods.EnumThreadWindows(SafeNativeMethods.GetCurrentThreadId(),
                                            new NativeMethods.EnumThreadWindowsCallback(ThreadWindowsCallback),
                                            NativeMethods.NullHandleRef);
    //disable those windows
    EnableThreadWindows(false);

    try
    {
        _showingAsDialog = true;
        Show();
    }
    catch
    {
        // NOTE:
        // See BUG 929740.
        // _threadWindowHandles is created before calling ShowDialog and is deleted in
        // EnableThreadWindows (when it's called with true).
        //
        // Window dlg = new Window();
        // Button b = new button();
        // b.OnClick += new ClickHandler(OnClick);
        // dlg.ShowDialog();
        //
        //
        // void OnClick(...)
        // {
        //      dlg.Close();
        //      throw new Exception();
        // }
        //
        //
        // If above code is written, then we get inside this exception handler only after the dialog
        // is closed.  In that case all the windows that we disabled before showing the dialog have already
        // been enabled and _threadWindowHandles set to null in EnableThreadWindows.  Thus, we don't
        // need to do it again.
        //
        // In any other exception cases, we get in this handler before Dialog is closed and thus we do
        // need to enable all the disable windows.
        if (_threadWindowHandles != null)
        {
            // Some exception case. Re-enable the windows that were disabled
            EnableThreadWindows(true);
        }
    }
}

觉得代码还是太长?不要紧,我再简化一下:

  1. EnumThreadWindows 获取当前线程的所有窗口
  2. 把当前线程中的所有窗口都禁用掉(用的是 Win32 API 的禁用哦,这不会导致窗口内控件的样式变为禁用状态)
  3. 将窗口显示出来(如果出现异常,则还原之前禁用的窗口)

可以注意到禁用掉的窗口是“当前线程”的哦。

ShowHelper

接下来的重点方法是 Window.ShowDialog 中的那句 Show()。在 Show() 之前设置了 _showingAsDialogtrue,于是这里会调用 ShowHelper 方法并传入 true

下面的代码也是精简后的 ShowHelper 方法:

private object ShowHelper(object booleanBox)
{
    try
    {
        // tell users we're going modal
        ComponentDispatcher.PushModal();

        _dispatcherFrame = new DispatcherFrame();
        Dispatcher.PushFrame(_dispatcherFrame);
    }
    finally
    {
        // tell users we're going non-modal
        ComponentDispatcher.PopModal();
    }
}

可以看到,重点是 PushModalPopModal 以及 PushFrame

PushFrame 的效果就是让调用 ShowDialog 的代码看起来就像阻塞了一样(实际上就是阻塞了,只不过开了新的消息循环看起来 UI 不卡)。

关于 PushFrame 为什么能够“阻塞”你的代码的同时还能继续响应 UI 操作的原理,可以阅读:

那么 ComponentDispatcher.PushModalComponentDispatcher.PopModal 呢?可以在这里(ComponentDispatcherThread.cs)看它的代码,实际上是为了模态计数以及引发事件的,对模态的效果没有本质上的影响。

.NET/C# 检测电脑上安装的 .NET Framework 的版本

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

如果你希望知道某台计算机上安装了哪些版本的 .NET Framework,那么正好本文可以帮助你解决问题。


如何找到已安装的 .NET Framework

有的电脑的 .NET Framework 是自带的,有的是操作系统自带的。这样,你就不能通过控制面板的“卸载程序”去找到到底安装了哪个版本的 .NET Framework 了。

关于各个版本 Windows 10 上自带的 .NET Framework 版本,可以阅读 各个版本 Windows 10 系统中自带的 .NET Framework 版本 - walterlv

而如果通过代码 Environment.Version 来获取 .NET 版本,实际上获取的是 CLR 的版本,详见 使用 PowerShell 获取 CLR 版本号 - walterlv

这些版本号是不同的,详见 .NET Framework 4.x 程序到底运行在哪个 CLR 版本之上 - walterlv

那么如何获取已安装的 .NET Framework 的版本呢?最靠谱的方法竟然是通过读取注册表。

注册表位置和含义

读取位置在这里:

计算机\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full\2052

注册表位置

而唯一准确能够判定 .NET Framework 版本的,只有里面的 Release 值。但可惜的是,这个值并不能直接看出来到底是 4.5 还是 4.8。我们需要有一张对应表。

我把它整理成了字典和注释,这样会比较容易理解每个编号对应的 .NET Framework 版本代号。

/// <summary>
/// 获取 .NET Framework 4.5 及以上版本的发行号与版本名称的对应关系。
/// 4.5 及以下版本没有这样的对应关系。
/// </summary>
private static readonly Dictionary<int, string> ReleaseToNameDictionary = new Dictionary<int, string>
{
    // .NET Framework 4.5
    { 378389, "4.5" },
    // .NET Framework 4.5.1(Windows 8.1 或 Windows Server 2012 R2 自带)
    { 378675, "4.5.1" },
    // .NET Framework 4.5.1(其他系统安装)
    { 378758, "4.5.1" },
    // .NET Framework 4.5.2
    { 379893, "4.5.2" },
    // .NET Framework 4.6(Windows 10 第一个版本 1507 自带)
    { 393295, "4.6" },
    // .NET Framework 4.6(其他系统安装)
    { 393297, "4.6" },
    // .NET Framework 4.6.1(Windows 10 十一月更新 1511 自带)
    { 394254, "4.6.1" },
    // .NET Framework 4.6.1(其他系统安装)
    { 394271, "4.6.1" },
    // .NET Framework 4.6.2(Windows 10 一周年更新 1607 和 Windows Server 2016 自带)
    { 394802, "4.6.2" },
    // .NET Framework 4.6.2(其他系统安装)
    { 394806, "4.6.2" },
    // .NET Framework 4.7(Windows 10 创造者更新 1703 自带)
    { 460798, "4.7" },
    // .NET Framework 4.7(其他系统安装)
    { 460805, "4.7" },
    // .NET Framework 4.7.1(Windows 10 秋季创造者更新 1709 和 Windows Server 1709 自带)
    { 461308, "4.7.1" },
    // .NET Framework 4.7.1(其他系统安装)
    { 461310, "4.7.1" },
    // .NET Framework 4.7.2(Windows 10 2018年四月更新 1803 和 Windows Server 1803 自带)
    { 461808, "4.7.2" },
    // .NET Framework 4.7.2(其他系统安装)
    { 461814, "4.7.2" },
    // .NET Framework 4.8(Windows 10 2019年五月更新 1903 自带)
    { 528040, "4.8" },
    // .NET Framework 4.8(其他系统安装)
    { 528049, "4.8" },
};

另外,还有一些值也是有意义的(只是不那么精确):

  • 主版本
    • 也就是可以共存的版本,比如 v3.5 系列和 v4 系列就是可以共存的,它们分别是就地更新的保持兼容的版本
  • 发行版本名称
    • 完整版 Full 和精简版 Client
  • 版本号
    • 比如 3.5.30729.4926 或者 4.7.02556
  • 服务包版本
    • 古时候的微软喜欢用 SP1 SP2 来命名同一个版本的多次更新,这也就是那个年代的产物

它们分别在注册表的这些位置:

  • 主版本
    • 计算机\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\NET Framework Setup\NDP 里项的名称
  • 发行版本名称
    • 以上项里子项的名称
  • 版本号
    • 以上项里的 Version
  • 服务包版本
    • 以上项里的 SP

读取注册表

在上面已经梳理了读取注册表的位置之后,相信你可以很容易写出读取已安装 .NET Framework 版本的代码出来。

我已经将其做成了 NuGet 源代码包(使用 SourceYard 打包),你可以安装 NuGet 包来获得读取已安装 .NET Framework 版本的功能:

或者在 GitHub 查看源代码:

只有一个类型——NdpInfo

使用方法有两种。

第一种,获取当前计算机上所有已经安装的 .NET Framework 版本:

var allVersions = await NdpInfo.ReadFromRegistryAsync();

执行完成之后看看得到的字典 allVersions 如下:

已安装的全部 .NET Framework

字典里 Key 是不能共存的主版本,Value 是这个主版本里当前已经安装的具体版本信息。

如果直接使用 ToString(),是可以生成我们平时经常在各大文档或者社区使用的 .NET Framework 的名称。

第二种,获取当前已安装的最新的 .NET Framework 版本名称:

var currentVersion = NdpInfo.GetCurrentVersionName();

这可以直接获取到一个字符串,比如 .NET Framework 4.8。对于只是简单获取一下已安装名称而不用做更多处理的程序来说会比较方便。

Windows 系统上用 .NET/C# 查找所有窗口,并获得窗口的标题、位置、尺寸、最小化、可见性等各种状态

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

在 Windows 应用开发中,如果需要操作其他的窗口,那么可以使用 EnumWindows 这个 API 来枚举这些窗口。

你可以使用本文编写的一个类型,查找到所有窗口中你关心的信息。


需要使用的 API

枚举所有窗口仅需要使用到 EnumWindows,其中需要定义一个委托 WndEnumProc 作为传入参数的类型。

剩下的我们需要其他各种方法用于获取窗口的其他属性。

private delegate bool WndEnumProc(IntPtr hWnd, int lParam);

[DllImport("user32")]
private static extern bool EnumWindows(WndEnumProc lpEnumFunc, int lParam);

[DllImport("user32")]
private static extern IntPtr GetParent(IntPtr hWnd);

[DllImport("user32")]
private static extern bool IsWindowVisible(IntPtr hWnd);

[DllImport("user32")]
private static extern int GetWindowText(IntPtr hWnd, StringBuilder lptrString, int nMaxCount);

[DllImport("user32")]
private static extern int GetClassName(IntPtr hWnd, StringBuilder lpString, int nMaxCount);

[DllImport("user32")]
private static extern bool GetWindowRect(IntPtr hWnd, ref LPRECT rect);

[StructLayout(LayoutKind.Sequential)]
private readonly struct LPRECT
{
    public readonly int Left;
    public readonly int Top;
    public readonly int Right;
    public readonly int Bottom;
}

枚举所有窗口

我将以上 API 封装成 FindAll 函数,并提供过滤器可以给大家过滤众多的窗口使用。

比如,我写了下面一个简单的示例,可以输出当前可见的所有窗口以及其位置和尺寸:

using System;

namespace Walterlv.WindowDetector
{
    class Program
    {
        static void Main(string[] args)
        {
            var windows = WindowEnumerator.FindAll();
            for (int i = 0; i < windows.Count; i++)
            {
                var window = windows[i];
                Console.WriteLine($@"{i.ToString().PadLeft(3, ' ')}. {window.Title}
     {window.Bounds.X}, {window.Bounds.Y}, {window.Bounds.Width}, {window.Bounds.Height}");
            }
            Console.ReadLine();
        }
    }
}

获取所有窗口以及其位置和尺寸

这里的 FindAll 方法,我提供了一个默认参数,可以指定如何过滤所有枚举到的窗口。如果不指定,则会找可见的,包含标题的,没有最小化的窗口。如果你希望找一些看不见的窗口,可以自己写过滤条件。

什么都不要过滤的话,就传入 _ => true,意味着所有的窗口都会被枚举出来。

附源码

因为源代码会经常更新,所以建议在这里查看:

无法访问的话,可以看下面:

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Text;

namespace Walterlv.WindowDetector
{
    /// <summary>
    /// 包含枚举当前用户空间下所有窗口的方法。
    /// </summary>
    public class WindowEnumerator
    {
        /// <summary>
        /// 查找当前用户空间下所有符合条件的窗口。如果不指定条件,将仅查找可见窗口。
        /// </summary>
        /// <param name="match">过滤窗口的条件。如果设置为 null,将仅查找可见窗口。</param>
        /// <returns>找到的所有窗口信息。</returns>
        public static IReadOnlyList<WindowInfo> FindAll(Predicate<WindowInfo> match = null)
        {
            var windowList = new List<WindowInfo>();
            EnumWindows(OnWindowEnum, 0);
            return windowList.FindAll(match ?? DefaultPredicate);

            bool OnWindowEnum(IntPtr hWnd, int lparam)
            {
                // 仅查找顶层窗口。
                if (GetParent(hWnd) == IntPtr.Zero)
                {
                    // 获取窗口类名。
                    var lpString = new StringBuilder(512);
                    GetClassName(hWnd, lpString, lpString.Capacity);
                    var className = lpString.ToString();

                    // 获取窗口标题。
                    var lptrString = new StringBuilder(512);
                    GetWindowText(hWnd, lptrString, lptrString.Capacity);
                    var title = lptrString.ToString().Trim();

                    // 获取窗口可见性。
                    var isVisible = IsWindowVisible(hWnd);

                    // 获取窗口位置和尺寸。
                    LPRECT rect = default;
                    GetWindowRect(hWnd, ref rect);
                    var bounds = new Rectangle(rect.Left, rect.Top, rect.Right - rect.Left, rect.Bottom - rect.Top);

                    // 添加到已找到的窗口列表。
                    windowList.Add(new WindowInfo(hWnd, className, title, isVisible, bounds));
                }

                return true;
            }
        }

        /// <summary>
        /// 默认的查找窗口的过滤条件。可见 + 非最小化 + 包含窗口标题。
        /// </summary>
        private static readonly Predicate<WindowInfo> DefaultPredicate = x => x.IsVisible && !x.IsMinimized && x.Title.Length > 0;

        private delegate bool WndEnumProc(IntPtr hWnd, int lParam);

        [DllImport("user32")]
        private static extern bool EnumWindows(WndEnumProc lpEnumFunc, int lParam);

        [DllImport("user32")]
        private static extern IntPtr GetParent(IntPtr hWnd);

        [DllImport("user32")]
        private static extern bool IsWindowVisible(IntPtr hWnd);

        [DllImport("user32")]
        private static extern int GetWindowText(IntPtr hWnd, StringBuilder lptrString, int nMaxCount);

        [DllImport("user32")]
        private static extern int GetClassName(IntPtr hWnd, StringBuilder lpString, int nMaxCount);

        [DllImport("user32")]
        private static extern void SwitchToThisWindow(IntPtr hWnd, bool fAltTab);

        [DllImport("user32")]
        private static extern bool GetWindowRect(IntPtr hWnd, ref LPRECT rect);

        [StructLayout(LayoutKind.Sequential)]
        private readonly struct LPRECT
        {
            public readonly int Left;
            public readonly int Top;
            public readonly int Right;
            public readonly int Bottom;
        }
    }

    /// <summary>
    /// 获取 Win32 窗口的一些基本信息。
    /// </summary>
    public readonly struct WindowInfo
    {
        public WindowInfo(IntPtr hWnd, string className, string title, bool isVisible, Rectangle bounds) : this()
        {
            Hwnd = hWnd;
            ClassName = className;
            Title = title;
            IsVisible = isVisible;
            Bounds = bounds;
        }

        /// <summary>
        /// 获取窗口句柄。
        /// </summary>
        public IntPtr Hwnd { get; }

        /// <summary>
        /// 获取窗口类名。
        /// </summary>
        public string ClassName { get; }

        /// <summary>
        /// 获取窗口标题。
        /// </summary>
        public string Title { get; }

        /// <summary>
        /// 获取当前窗口是否可见。
        /// </summary>
        public bool IsVisible { get; }

        /// <summary>
        /// 获取窗口当前的位置和尺寸。
        /// </summary>
        public Rectangle Bounds { get; }

        /// <summary>
        /// 获取窗口当前是否是最小化的。
        /// </summary>
        public bool IsMinimized => Bounds.Left == -32000 && Bounds.Top == -32000;
    }
}

在 WPF 程序中应用 Windows 10 真•亚克力效果

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

从 Windows 10 (1803) 开始,Win32 应用也可以有 API 来实现原生的亚克力效果了。不过相比于 UWP 来说,可定制性会差很多。

本文介绍如何在 WPF 程序中应用 Windows 10 真•亚克力效果。(而不是一些流行的项目里面自己绘制的亚克力效果。)


API

需要使用的 API 是微软的文档中并未公开的 SetWindowCompositionAttribute

我在另一篇博客中有介绍此 API 各种用法的效果,详见:

当然,使用此 API 也可以做 Windows 10 早期的模糊效果,比如:

如何使用

为了方便地让你的窗口获得亚克力效果,我做了两层不同的 API:

  1. AcrylicBrush 当然,受到 Win32 启用亚克力效果的限制,只能在窗口上设置此属性
  2. WindowAccentCompositor 用于更多地控制窗口与系统的叠加组合效果

代码请参见:

亚克力效果在 WPF 程序中

注意事项

要使得亚克力效果可以生效,需要:

  1. 设置一个混合色 GradientColor
  2. 混合色不能是全透明(如果全透明,窗口的亚克力部分就全透明穿透了),当然也不能全不透明,这样就看不到亚克力效果了。

参考资料

使用 SetWindowCompositionAttribute 来控制程序的窗口边框和背景(可以做 Acrylic 亚克力效果、模糊效果、主题色效果等)

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

Windows 系统中有一个没什么文档的 API,SetWindowCompositionAttribute,可以允许应用的开发者将自己窗口中的内容渲染与窗口进行组合。这可以实现很多系统中预设的窗口特效,比如 Windows 7 的毛玻璃特效,Windows 8/10 的前景色特效,Windows 10 的模糊特效,以及 Windows 10 1709 的亚克力(Acrylic)特效。而且这些组合都发生在 dwm 进程中,不会额外占用应用程序的渲染性能。

本文介绍 SetWindowCompositionAttribute 可以实现的所有效果。你可以通过阅读本文了解到与系统窗口可以组合渲染到哪些程度。


试验用的源代码

本文将创建一个简单的 WPF 程序来验证 SetWindowCompositionAttribute 能达到的各种效果。你也可以不使用 WPF,得到类似的效果。

简单的项目文件结构是这样的:

  • [项目] Walterlv.WindowComposition
    • App.xaml
    • App.xaml.cs
    • MainWindow.xaml
    • MainWindow.xaml.cs
    • WindowAccentCompositor

其中,App.xaml 和 App.xaml.cs 保持默认生成的不动。

为了验证此 API 的效果,我需要将 WPF 主窗口的背景色设置为纯透明或者 null,而设置 ControlTemplate 才能彻彻底底确保所有的样式一定是受我们自己控制的,我们在 ControlTemplate 中没有指定任何可以显示的内容。MainWindow.xaml 的全部代码如下:

<Window x:Class="Walterlv.WindowComposition.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="欢迎访问吕毅的博客:blog.walterlv.com" Height="450" Width="800">
    <Window.Template>
        <ControlTemplate TargetType="Window">
            <AdornerDecorator>
                <ContentPresenter />
            </AdornerDecorator>
        </ControlTemplate>
    </Window.Template>
    <!-- 我们注释掉 WindowChrome,是因为即将验证 WindowChrome 带来的影响。 -->
    <!--<WindowChrome.WindowChrome>
        <WindowChrome GlassFrameThickness="-1" />
    </WindowChrome.WindowChrome>-->
    <Grid>
    </Grid>
</Window>

而 MainWindow.xaml.cs 中,我们简单调用一下我们即将写的调用 SetWindowCompositionAttribute 的类型。

using System.Windows;
using System.Windows.Media;
using Walterlv.Windows.Effects;

namespace Walterlv.WindowComposition
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            var compositor = new WindowAccentCompositor(this);
            compositor.Composite(Color.FromRgb(0x18, 0xa0, 0x5e));
        }
    }
}

还剩下一个 WindowAccentCompositor.cs 文件,因为比较长放到博客里影响阅读,所以建议前往这里查看:

而其中对我们最终渲染效果有影响的就是 AccentPolicy 类型的几个属性。其中 AccentState 属性是下面这个枚举,而 GradientColor 将决定窗口渲染时叠加的颜色。

private enum AccentState
{
    ACCENT_DISABLED = 0,
    ACCENT_ENABLE_GRADIENT = 1,
    ACCENT_ENABLE_TRANSPARENTGRADIENT = 2,
    ACCENT_ENABLE_BLURBEHIND = 3,
    ACCENT_ENABLE_ACRYLICBLURBEHIND = 4,
    ACCENT_INVALID_STATE = 5,
}

影响因素

经过试验,对最终显示效果有影响的有这些:

  • 选择的 AccentState 枚举值
  • 使用的 GradientColor 叠加色
  • 是否使用 WindowChrome 让客户区覆盖非客户区
  • 目标操作系统(Windows 7/8/8.1/10)

使用 WindowChrome,你可以用你自己的 UI 覆盖掉系统的 UI 窗口样式。关于 WindowChrome 让客户区覆盖非客户区的知识,可以阅读:

需要注意的是,WindowChromeGlassFrameThickness 属性可以设置窗口边框的粗细,设置为 0 将导致窗口没有阴影,设置为负数将使得整个窗口都是边框。

排列组合

我们依次来看看效果。

AccentState=ACCENT_DISABLED

使用 ACCENT_DISABLED 时,GradientColor 叠加色没有任何影响,唯一影响渲染的是 WindowChrome 和操作系统。


不使用 WindowChrome,在 Windows 10 上:

without WindowChrome in Windows 10


不使用 WindowChrome 在 Windows 7 上:

without WindowChrome in Windows 7


在 Windows 10 上,使用 WindowChrome

<WindowChrome.WindowChrome>
    <WindowChrome />
</WindowChrome.WindowChrome>

with WindowChrome in Windows 10


在 Windows 7 上,使用 WindowChrome

with WindowChrome in Windows 7

当然,以上边框比较细,跟系统不搭,可以设置成其他值:

bold thickness WindowChrome in Windows 7


在 Windows 10 上,使用 WindowChrome 并且 GlassFrameThickness 设置为 -1

<WindowChrome.WindowChrome>
    <WindowChrome GlassFrameThickness="-1" />
</WindowChrome.WindowChrome>

-1 glass frame in Windows 10


而在 Windows 7 上,这就是非常绚丽的全窗口的 Aero 毛玻璃特效:

-1 glass frame in Windows 7

AccentState=ACCENT_ENABLE_GRADIENT

使用 ACCENT_DISABLED 时,GradientColor 叠加色会影响到最终的渲染效果。

还记得我们前面叠加的颜色是什么吗?

叠加的颜色

接下来别忘了然后把它误以为是我系统的主题色哦!


不使用 WindowChrome,在 Windows 10 上:

without WindowChrome in Windows 10

另外,你会注意到左、下、右三个方向上边框会深一些。那是 Windows 10 的窗口阴影效果,因为实际上 Windows 10 叠加的阴影也是窗口区域的一部分,只是一般人看不出来而已。我们叠加了颜色之后,这里就露馅儿了。

另外,这个颜色并不是我们自己的进程绘制的哦,是 dwm 绘制的颜色。

如果不指定 GradientColor 也就是保持为 0,你将看到上面绿色的部分全是黑色的;嗯,包括阴影的部分……

without WindowChrome in Windows 10 - default gradient color


不使用 WindowChrome 在 Windows 7 上:

without WindowChrome in Windows 7

可以看出,在 Windows 7 上,GradientColor 被无视了。


而使用 WindowChrome 在 Windows 10 上,则可以得到整个窗口的叠加色:

<WindowChrome.WindowChrome>
    <WindowChrome GlassFrameThickness="16 48 16 16" />
</WindowChrome.WindowChrome>

with WindowChrome in Windows 10

可以注意到,窗口获得焦点的时候,整个窗口都是叠加色;而窗口失去焦点的时候,指定了边框的部分颜色会更深(换其他颜色叠加可以看出来是叠加了半透明黑色)。

如果你希望失去焦点的时候,边框部分不要变深,请将边框设置为 -1

<WindowChrome.WindowChrome>
    <WindowChrome GlassFrameThickness="-1" />
</WindowChrome.WindowChrome>

使用 WindowChrome 在 Windows 7 上,依然没有任何叠加色的效果:

with WindowChrome in Windows 7

AccentState=ACCENT_ENABLE_TRANSPARENTGRADIENT

使用 ACCENT_ENABLE_TRANSPARENTGRADIENT 时,GradientColor 叠加色没有任何影响,唯一影响渲染的是 WindowChrome 和操作系统。


不使用 WindowChrome,在 Windows 10 上:

without WindowChrome frame in Windows 10

依然左、下、右三个方向上边框会深一些,那是 Windows 10 的窗口阴影效果。


不使用 WindowChrome 在 Windows 7 上:

without WindowChrome in Windows 7

GradientColor 也是被无视的,而且效果跟之前一样。


使用 WindowChrome 在 Windows 10 上,在获得焦点的时候整个背景是系统主题色;而失去焦点的时候是灰色,但边框部分是深色。

with WindowChrome frame in Windows 10

依然可以将边框设置为 -1 使得边框不会变深:

with WindowChrome in Windows 10


使用 WindowChrome 在 Windows 7 上,依然是老样子:

with WindowChrome in Windows 7

AccentState=ACCENT_ENABLE_BLURBEHIND

ACCENT_ENABLE_BLURBEHIND 可以在 Windows 10 上做出模糊效果,就跟 Windows 10 早期版本的模糊效果是一样的。你可以看我之前的一篇博客,那时亚克力效果还没出来:

使用 ACCENT_ENABLE_BLURBEHIND 时,GradientColor 叠加色没有任何影响,唯一影响渲染的是 WindowChrome 和操作系统。


在 Windows 10 上,没有使用 WindowChrome

模糊效果

你可能需要留意一下那个“诡异”的模糊范围,你会发现窗口的阴影外侧也是有模糊的!!!你能忍吗?肯定不能忍,所以还是乖乖使用 WindowChrome 吧!


在 Windows 7 上,没有使用 WindowChrome,效果跟其他值一样,依然没有变化:

without WindowChrome in Windows 7


在 Windows 10 上,使用 WindowChrome

with WindowChrome in Windows 10


使用 WindowChrome 在 Windows 7 上,依然是老样子:

with WindowChrome in Windows 7

AccentState=ACCENT_ENABLE_ACRYLICBLURBEHIND

从 Windows 10 (1803) 开始,Win32 程序也能添加亚克力效果了,因为 SetWindowCompositionAttribute 的参数枚举新增了 ACCENT_ENABLE_ACRYLICBLURBEHIND

亚克力效果相信大家不陌生,那么在 Win32 应用程序里面使用的效果是什么呢?


不使用 WindowChrome,在 Windows 10 上:

without WindowChrome in Windows 10

咦!等等!这不是跟之前一样吗?


嗯,下面就是不同了,亚克力效果支持与半透明的 GradientColor 叠加,所以我们需要将传入的颜色修改为半透明:

    var compositor = new WindowAccentCompositor(this);
--  compositor.Composite(Color.FromRgb(0x18, 0xa0, 0x5e));
++  compositor.Composite(Color.FromArgb(0x3f, 0x18, 0xa0, 0x5e));

acrylic without WindowChrome


那么如果改为全透明会怎么样呢?

不幸的是,完全没有效果!!!

    var compositor = new WindowAccentCompositor(this);
--  compositor.Composite(Color.FromRgb(0x18, 0xa0, 0x5e));
++  compositor.Composite(Color.FromArgb(0x00, 0x18, 0xa0, 0x5e));

no acrylic without WindowChrome


接下来是使用 WindowChrome 时:

<WindowChrome.WindowChrome>
    <WindowChrome GlassFrameThickness="16 48 16 16" />
</WindowChrome.WindowChrome>

acrylic with WindowChrome frame

然而周围有一圈偏白色的渐变是什么呢?那个其实是 WindowChrome 设置的边框白,被亚克力效果模糊后得到的混合效果。


所以,如果要获得全窗口的亚克力效果,请将边框设置成比较小的值:

<WindowChrome.WindowChrome>
    <WindowChrome GlassFrameThickness="0 1 0 0" />
</WindowChrome.WindowChrome>

acrylic with thin WindowChrome frame


记得不要像前面的那些效果一样,如果设置成 -1,你将获得纯白色与设置的 Gradient 叠加色的亚克力特效,是个纯色:

acrylic with WindowChrome -1 frame


你可以将叠加色的透明度设置得小一些,这样可以看出叠加的颜色:

    var compositor = new WindowAccentCompositor(this);
--  compositor.Composite(Color.FromRgb(0x18, 0xa0, 0x5e));
++  compositor.Composite(Color.FromArgb(0xa0, 0x18, 0xa0, 0x5e));

acrylic with darker gradient color


那么可以设置为全透明吗?

    var compositor = new WindowAccentCompositor(this);
--  compositor.Composite(Color.FromRgb(0x18, 0xa0, 0x5e));
++  compositor.Composite(Color.FromArgb(0x00, 0x18, 0xa0, 0x5e));

很不幸,最终你会完全看不到亚克力效果,而变成了毫无特效的透明窗口:

acrylic with transparent gradient color

最上面那根白线,是我面前面设置边框为 0 1 0 0 导致的。


如果在这种情况下,将边框设置为 0 会怎样呢?记得前面我们说过的吗,会导致阴影消失哦!

呃……你将看到……这个……

什么都没有……

acrylic with zero WindowChrome frame thickness

是不是找到了一条新的背景透明异形窗口的方法?

还是省点心吧,亚克力效果在 Win32 应用上的性能还是比较堪忧的……

想要背景透明,请参见:


不用考虑 Windows 7,因为大家都知道不支持。实际效果会跟前面的一模一样。

AccentState=ACCENT_INVALID_STATE

这个值其实不用说了,因为 AccentState 在不同系统中可用的值不同,为了保证向后兼容性,对于新系统中设置的值,旧系统其实就视之为 ACCENT_INVALID_STATE

那么如果系统认为设置的是 ACCENT_INVALID_STATE 会显示成什么样子呢?

答案是,与 ACCENT_DISABLED 完全相同。

总结

由于 Windows 7 上所有的值都是同样的效果,所以下表仅适用于 Windows 10。

  效果
ACCENT_DISABLED 黑色(边框为纯白色)
ACCENT_ENABLE_GRADIENT GradientColor 颜色(失焦后边框为深色)
ACCENT_ENABLE_TRANSPARENTGRADIENT 主题色(失焦后边框为深色)
ACCENT_ENABLE_BLURBEHIND 模糊特效(失焦后边框为灰色)
ACCENT_ENABLE_ACRYLICBLURBEHIND 与 GradientColor 叠加颜色的亚克力特效
ACCENT_INVALID_STATE 黑色(边框为纯白色)

在以上的特效之下,WindowChrome 可以让客户区覆盖非客户区,或者让整个窗口都获得特效,而不只是标题栏。

附源代码

请参见 GitHub 地址以获得最新代码。如果不方便访问,那么就看下面的吧。

using System;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media;

namespace Walterlv.Windows.Effects
{
    /// <summary>
    /// 为窗口提供模糊特效。
    /// </summary>
    public class WindowAccentCompositor
    {
        private readonly Window _window;

        /// <summary>
        /// 创建 <see cref="WindowAccentCompositor"/> 的一个新实例。
        /// </summary>
        /// <param name="window">要创建模糊特效的窗口实例。</param>
        public WindowAccentCompositor(Window window) => _window = window ?? throw new ArgumentNullException(nameof(window));

        public void Composite(Color color)
        {
            Window window = _window;
            var handle = new WindowInteropHelper(window).EnsureHandle();

            var gradientColor =
                // 组装红色分量。
                color.R << 0 |
                // 组装绿色分量。
                color.G << 8 |
                // 组装蓝色分量。
                color.B << 16 |
                // 组装透明分量。
                color.A << 24;

            Composite(handle, gradientColor);
        }

        private void Composite(IntPtr handle, int color)
        {
            // 创建 AccentPolicy 对象。
            var accent = new AccentPolicy
            {
                AccentState = AccentState.ACCENT_ENABLE_ACRYLICBLURBEHIND,
                GradientColor = 0,
            };

            // 将托管结构转换为非托管对象。
            var accentPolicySize = Marshal.SizeOf(accent);
            var accentPtr = Marshal.AllocHGlobal(accentPolicySize);
            Marshal.StructureToPtr(accent, accentPtr, false);

            // 设置窗口组合特性。
            try
            {
                // 设置模糊特效。
                var data = new WindowCompositionAttributeData
                {
                    Attribute = WindowCompositionAttribute.WCA_ACCENT_POLICY,
                    SizeOfData = accentPolicySize,
                    Data = accentPtr,
                };
                SetWindowCompositionAttribute(handle, ref data);
            }
            finally
            {
                // 释放非托管对象。
                Marshal.FreeHGlobal(accentPtr);
            }
        }

        [DllImport("user32.dll")]
        private static extern int SetWindowCompositionAttribute(IntPtr hwnd, ref WindowCompositionAttributeData data);

        private enum AccentState
        {
            ACCENT_DISABLED = 0,
            ACCENT_ENABLE_GRADIENT = 1,
            ACCENT_ENABLE_TRANSPARENTGRADIENT = 2,
            ACCENT_ENABLE_BLURBEHIND = 3,
            ACCENT_ENABLE_ACRYLICBLURBEHIND = 4,
            ACCENT_INVALID_STATE = 5,
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct AccentPolicy
        {
            public AccentState AccentState;
            public int AccentFlags;
            public int GradientColor;
            public int AnimationId;
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct WindowCompositionAttributeData
        {
            public WindowCompositionAttribute Attribute;
            public IntPtr Data;
            public int SizeOfData;
        }

        private enum WindowCompositionAttribute
        {
            // 省略其他未使用的字段
            WCA_ACCENT_POLICY = 19,
            // 省略其他未使用的字段
        }
    }
}

使用傲梅分区助手无损合并分区,无损调整分区大小

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

Windows 本身就提供了强大的磁盘和分区管理工具,一个是操作简单的“磁盘管理”,一个是功能强大的命令行版的“diskpart”。不过这两个都有一些限制,一是不能影响到系统文件,二是其修改的分区不能被应用程序占用(diskpart 可在下次重启时做到)。另外,系统为了管理工具操作的效率和正确性,也有一些功能没有开放。

DiskGenius 是个强大的工具,不过傲梅也很良心。本文介绍使用傲梅分区助手来管理磁盘。


下载

傲梅分区助手有绿色版、专业版和 PE 版。一般我们选择绿色版就好,如果你要改到系统分区,就需要使用集成了傲梅分区助手的 PE 系统。

下面是专业版的截图:

专业版

下面是 PE 版的截图,也是我实际操作分区时截下来的图:

PE 版

不要吐槽为何我用的是古老的 1709 系统,实际上我的系统盘是下面那个 I 盘。不然为什么我会把系统的版本号放到卷标中呢?

调整分区大小

在 PE 系统中找到傲梅分区助手,然后启动。在需要调整位置和大小的分区上右键点击选择“调整/移动分区”:

调整移动分区

然后在弹出的详细设置对话框中调整分区的位置和大小。如果是 SSD,建议点击“高级”然后勾选“允许分区对齐以优化SSD或HDD硬盘”,这可以开启 4K 对齐以大幅优化 SSD 的读写性能。

4K 对齐

最后点击确定。

注意这个时候还没有开始执行真正的操作!

合并分区

合并分区功能可以将你一个磁盘中的多个分区无损合并成一个。

合并分区

选择好将哪个分区合并到哪一个,这时另一个分区中的所有文件会放到目标分区中的一个文件夹里。合并完之后你自己移动好这些文件即可。

因为我的分区在合并过程中的操作没有截图,所以只能看到下面这个提前在磁盘管理中的截图:

无法删除

开始执行真正的操作

在你设置好你的所有操作之后,点击左上角的“提交”按钮,这可以开始依次执行之前所有设置的磁盘最终状态。

提交

在提交界面,你可以看到即将进行的所有操作的简介,以及预计完成这些操作所花的时间。

操作预览

虽然上图只是示例,但我实际将我在下面这篇博客中删除出来的空余空间全部合并在一起,并且还额外合并了两个都需要保留数据的分区。这个过程傲梅的预计时间是 9小时18分,实际上也刚好在 9 个小时左右!

所以,如果你打算开始进行大量的磁盘调整、对拷或者其他无损分区操作:

  • 请提前准备好大量你不用电脑的时间。
  • 请提前准备好大量你不用电脑的时间。
  • 请提前准备好大量你不用电脑的时间。

这是我实际上在 PE 中操作的截图:

分区调整过程

EFI 分区/恢复分区不可删除?你需要使用命令行了(配合鼠标操作)

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

Windows 系统在安装的时候,会自动为我们的磁盘划分一个恢复分区和一个 EFI 分区。如果后面不打算再用这些分区的时候,却发现无法删除。

本文将提供解决方法。


因为误操作会导致数据丢失,所以我将两种不同的解决方法分开成两篇文章以避免干扰:

无法删除

看下图,有两种不同类型的无法删除:

  1. 有完整菜单只是删除按钮不可用的 EFI 分区;
  2. 仅有一个“帮助”菜单的恢复分区。

删除方法会略有不同,我会在合适的地方提示你使用正确的方法的。

无法删除

我的磁盘 2 原本包含两个可见分区,一个是图中黑色色块,原来放的是旧操作系统,一个是图中的 D 盘,放大量文件。因为我新买了一个大容量 SSD 专门用来放操作系统,所以原来操作系统所在的磁盘就可以回收与 D 盘合并。

然而悲剧的是,中间隔着一个 820MB 的恢复分区,导致我没有办法为 D 分区扩容。

更麻烦的是,在磁盘管理中,这三个我不会再使用的恢复分区都不可删除。

PS. 吐槽一下,大版本升级一次 Windows 10 竟然会在后面给我多创建一个恢复分区……

解决办法

在实操之前,你必须清除地知道你每一步在做什么,否则你需要承担丢失数据的后果:

  • 如果你在网上找到一些操作 disk 的命令,请不要相信——因为此命令清除的是整个磁盘而不只是单个分区

第一步:打开命令提示符

打开开始菜单,输入 cmd 然后回车确定,我们可以打开命令提示符

cmd

第二步:打开 diskpart

cmd 中输入 diskpart 然后回车,你会看到一个 UAC 提示弹窗,点击“是”之后会启动一个新的管理员权限启动的命令提示符,这里运行着 Diskpart 程序。

diskpart

第三步:找到要操作的磁盘

输入 list disk 回车,我们可以看到自己计算机中所有已经插入的磁盘。

DISKPART> list disk

  磁盘 ###  状态           大小     可用     Dyn  Gpt
  --------  -------------  -------  -------  ---  ---
  磁盘 0    联机              238 GB      0 B
  磁盘 1    联机              931 GB  2048 KB
  磁盘 2    联机              489 GB   199 GB        *
  磁盘 3    联机              476 GB  1024 KB        *

请注意,这里看到的是磁盘,而不是平时在“计算机”中看到的分区——每一个磁盘都可以包含一个到多个分区哦!

你有两种方法来确认我们即将操作的是哪个磁盘:

  1. 前面我们在磁盘管理中看到的那个界面,也就是本文一开始的那张图,可以直接看出我们要删除的分区在“磁盘 2”上。
  2. 根据分区的大小去猜,相信分区少的时候你可以猜对。

好的,我们知道要操作的是“磁盘 2”,于是我们输入命令 select disk 2(如果你是其他磁盘请换成自己的数字):

DISKPART> select disk 2

磁盘 2 现在是所选磁盘。

紧接着,我们输入 list partition 列出此磁盘上的所有分区:

DISKPART> list partition

  分区 ###       类型              大小     偏移量
  -------------  ----------------  -------  -------
  分区      1    恢复                 499 MB  1024 KB
  分区      2    系统                 100 MB   500 MB
  分区      3    保留                  16 MB   600 MB
  分区      5    恢复                 820 MB   199 GB
  分区      6    主要                 288 GB   200 GB

通过分区,我们也能再次确认我们找到了正确的要操作的磁盘。

截至目前,我们还没有对系统进行任何更改,所以你操作错了也不用担心。但接下来你就需要谨慎一些。

第 4.1 步:删除分区(仅适用于 EFI 分区)

EFI 分区

因为我不再将此磁盘用作系统盘,所以里面除了那个 288GB 的数据部分不能动之外,其他系统生成的部分都是需要删除的,所以接下来我需要对分区 1 2 5 都进行一遍以下操作(你的目的不同可能需要删除的分区也不一样)。

先选中要操作的分区:

DISKPART> select partition 1

分区 1 现在是所选分区。

然后更改其 ID:

DISKPART> SET ID=ebd0a0a2-b9e5-4433-87c0-68b6b72699c7

DiskPart 成功设置了分区 ID

然后操作其他的分区。

完整的截图如下:

完整的截图

这个时候,回到磁盘管理中,F5 刷新,你可以看到原本不可删除的 EFI 分区,现在可以直接使用鼠标删除了。点击一下“删除卷”即可。

在磁盘管理中删除卷

第 4.2 步:删除分区(适用于所有类型的分区)

恢复分区

恢复分区不能使用上面 4.1 中的方法删除,如果你在 4.1 的操作之后还发现存在不可删除的恢复分区,请尝试使用我的另一篇博客:


参考资料

EFI 分区/恢复分区不可删除?你需要使用命令行了(全命令行操作)

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

Windows 系统在安装的时候,会自动为我们的磁盘划分一个恢复分区和一个 EFI 分区。如果后面不打算再用这些分区的时候,却发现无法删除。

本文将提供解决方法。


因为误操作会导致数据丢失,所以我将两种不同的解决方法分开成两篇文章以避免干扰:

无法删除

看下图,有两种不同类型的无法删除:

  1. 有完整菜单只是删除按钮不可用的 EFI 分区;
  2. 仅有一个“帮助”菜单的恢复分区。

使用本文提供的方法,你可以删除以上两种不同类型的分区。

无法删除

我的磁盘 2 原本包含两个可见分区,一个是图中黑色色块,原来放的是旧操作系统,一个是图中的 D 盘,放大量文件。因为我新买了一个大容量 SSD 专门用来放操作系统,所以原来操作系统所在的磁盘就可以回收与 D 盘合并。

然而悲剧的是,中间隔着一个 820MB 的恢复分区,导致我没有办法为 D 分区扩容。

更麻烦的是,在磁盘管理中,这三个我不会再使用的恢复分区都不可删除。

PS. 吐槽一下,大版本升级一次 Windows 10 竟然会在后面给我多创建一个恢复分区……

解决办法

在实操之前,你必须清除地知道你每一步在做什么,否则你需要承担丢失数据的后果:

  • 如果你在网上找到一些操作 disk 的命令,请不要相信——因为此命令清除的是整个磁盘而不只是单个分区

第一步:打开命令提示符

打开开始菜单,输入 cmd 然后回车确定,我们可以打开命令提示符

cmd

第二步:打开 diskpart

cmd 中输入 diskpart 然后回车,你会看到一个 UAC 提示弹窗,点击“是”之后会启动一个新的管理员权限启动的命令提示符,这里运行着 Diskpart 程序。

diskpart

第三步:找到要操作的磁盘

输入 list disk 回车,我们可以看到自己计算机中所有已经插入的磁盘。

DISKPART> list disk

  磁盘 ###  状态           大小     可用     Dyn  Gpt
  --------  -------------  -------  -------  ---  ---
  磁盘 0    联机              238 GB      0 B
  磁盘 1    联机              931 GB  2048 KB
  磁盘 2    联机              489 GB   199 GB        *
  磁盘 3    联机              476 GB  1024 KB        *

请注意,这里看到的是磁盘,而不是平时在“计算机”中看到的分区——每一个磁盘都可以包含一个到多个分区哦!

你有两种方法来确认我们即将操作的是哪个磁盘:

  1. 前面我们在磁盘管理中看到的那个界面,也就是本文一开始的那张图,可以直接看出我们要删除的分区在“磁盘 2”上。
  2. 根据分区的大小去猜,相信分区少的时候你可以猜对。

好的,我们知道要操作的是“磁盘 2”,于是我们输入命令 select disk 2(如果你是其他磁盘请换成自己的数字):

DISKPART> select disk 2

磁盘 2 现在是所选磁盘。

紧接着,我们输入 list partition 列出此磁盘上的所有分区:

DISKPART> list partition

  分区 ###       类型              大小     偏移量
  -------------  ----------------  -------  -------
  分区      1    恢复                 499 MB  1024 KB
  分区      2    系统                 100 MB   500 MB
  分区      3    保留                  16 MB   600 MB
  分区      5    恢复                 820 MB   199 GB
  分区      6    主要                 288 GB   200 GB

通过分区,我们也能再次确认我们找到了正确的要操作的磁盘。

截至目前,我们还没有对系统进行任何更改,所以你操作错了也不用担心。但接下来你就需要谨慎一些。

第四步:删除分区

因为我不再将此磁盘用作系统盘,所以里面除了那个 288GB 的数据部分不能动之外,其他系统生成的部分都是需要删除的,所以接下来我需要对分区 1 2 5 都进行一遍以下操作(你的目的不同可能需要删除的分区也不一样)。

先选中要操作的分区:

DISKPART> select partition 1

分区 1 现在是所选分区。

然后输入 delete part override 删除这个分区:

DISKPART> delete part override

DiskPart 成功地删除了所选分区。

接着,依次删除其他分区。下面是删除其中前两个分区后的截图:

删除分区

所有分区删除完毕之后,可以看到我的整个磁盘现在只剩下我要留下的重要数据分区了。

DISKPART> list partition

  分区 ###       类型              大小     偏移量
  -------------  ----------------  -------  -------
  分区      6    主要                 288 GB   200 GB

这时回到磁盘管理中,可以看到大量已被删除的未分配的空间连在了一起。

未分配

可以将我的 D 盘扩展更多空间啦!


参考资料

推荐几款连字字体,在代码编辑器中启用连字字体(Visual Studio Code)

dotnet 职业技术学院 发布于 2019-09-27

启用转为编程设计的连字字体,可以给你的变成带来不一样的体验。


连字字体

微软随 Windows Terminal 设计了一款新的字体 Cascadia Code,而这是一款连字字体。

你可以看到,在 Windows Terminal 的终端中,=> == != 符号显示成了更容易理解的连字符号:

Cascadia Code

在 Cascadia Code 发布之前,Fira Code 是一款特别火的连字字体,下面是 Fira Code 连字字体在 Visual Studio Code 中的显示效果:

Fira Code in Visual Studio Code

而显示的,其实是下面这一段代码:

x =>
{
    if (x >= 2 || x == 0)
    {
        Console.WriteLine(" >=> 欢迎访问吕毅的博客 ~~> blog.walterlv.com");
    }
}

连字字体推荐

作为微软的粉丝,当然首推 Cascadia Code!不过我喜欢比较细的字体风格,目前 Cascadia Code 还没有提供细体,因此我可能还需要等一些时间才正式入坑。

在这里可以关注 Cascadia Code 的状态:

灵台,你也可以在这里找到其他一些好看的用于编程的连字字体:

相关的开源项目链接:

以 Fira Code 为例安装的话,去它的 GitHub 的 release 页面:

下载最新的发布文件 FiraCode_1.207.zip

下载解压后,你会看到五个不同的文件夹,这是四种不同的字体类型:

  • otf (Open Type)
  • ttf (True Type)
  • variable_ttf (Variable True Type)
  • woff (Web Open Font Format)
  • woff2 (Web Open Font Format)

对于 Open Type 和 True Type 的选择,一般有对应的 Open Type 类型字体的时候就优先选择 Open Type 类型的,因为 True Type 格式是比较早期的,限制比较多,比如字符的数量受到限制,而 Open Type 是基于 Unicode 字符集来设计的新的跨平台的字体格式。

Variable True Type 是可以无极变换的 True Type 字体。

而 Web Open Font Format 主要为网络传输优化,其特点是字体均经过压缩,其大小会比较小。

我们点击进入 otf 文件夹,然后全选所有的字体文件,右键,安装,等待安装完成即可。

在编辑器中启用

在 Visual Studio Code 中启用

在 Visual Studio Code 中启用连字字体需要用到两个选项:

"editor.fontFamily": "Fira Code Light, Consolas, Microsoft YaHei",
"editor.fontLigatures": true,

打开 Visual Studio Code 设置

然后点击新打开的标签右上角的 {} 图标以打开 json 形式编辑的设置:

使用 json 编辑设置

然后修改把上面两个设置增加或替换进去即可。下面是我的设置的部分截图:

设置启用连字字体

在 Visual Studio 或其他 Windows 系统自带软件中启用

只需要将字体设置成 Fira Code 即可。


参考资料

如何在 Visual Studio 2019 中设置使用 .NET Core SDK 的预览版(全局生效)

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

.NET Core 3 相比于 .NET Core 2 是一个大更新。也正因为如此,即便它长时间处于预览版尚未发布的状态,大家也一直在使用。

Visual Studio 2019 中提供了使用 .NET Core SDK 预览版的开关。但几个更新的版本其开关的位置不同,本文将介绍在各个版本中的位置,方便你找到然后设置。


Visual Studio 2019 (16.3 及以上)

.NET Core 3.0 已经发布,下载地址:

Visual Studio 16.3 与 .NET Core 3.0 正式版同步发布,因此不再需要 .NET Core 3.0 的预览版设置界面。你只需要安装正式版 .NET Core SDK 即可。

Visual Studio 2019 (16.2)

从 Visual Studio 2019 的 16.2 版本,.NET Core 预览版的设置项的位置在:

  • 工具 -> 选项
  • 环境 -> 预览功能 -> Use previews of the .NET Core SDK (需要 restart)

Visual Studio 2019 16.2 的设置位置

如果你是英文版的 Visual Studio,也可以参考英文版:

  • Tools -> Options
  • Environment -> Preview Features -> Use previews of the .NET Core SDK (requires restart)

Option location of Visual Studio 2019 16.2

Visual Studio 2019 (16.1)

从 Visual Studio 2019 的 16.1 版本,.NET Core 预览版的设置项的位置在:

  • 工具 -> 选项
  • 环境 -> 预览功能 -> 使用 .NET Core SDK 的预览

Visual Studio 2019 16.1 的设置位置

如果你是英文版的 Visual Studio,也可以参考英文版:

  • Tools -> Options
  • Environment -> Preview Features -> Use previews of the .NET Core SDK

Option location of Visual Studio 2019 16.1

Visual Studio 2019 (16.0 和早期预览版)

在 Visual Studio 2019 的早期,.NET Core 在设置中是有一个专用的选项的,在这里:

  • 工具 -> 选项
  • 项目和解决方案 -> .NET Core -> 使用 .NET Core SDK 预览版

Visual Studio 2019 16.0 的设置位置

如果你是英文版的 Visual Studio,也可以参考英文版:

  • Tools -> Options
  • Projects and solutions -> .NET Core -> Use previews of the .NET Core SDK

Option location of Visual Studio 2019 16.0

关于全局配置

Visual Studio 2019 中此对于 .NET Core SDK 的预览版的设置是全局生效的。

也就是说,你在 Visual Studio 2019 中进行了此设置,在命令行中使用 MSBuild 或者 dotnet build 命令进行编译也会使用这样的设置项。

那么这个全局的设置项在哪个地方呢?是如何全局生效的呢?可以阅读我的其他博客:

WPF 程序如何移动焦点到其他控件

dotnet 职业技术学院 发布于 2019-09-19

WPF 中可以使用 UIElement.Focus() 将焦点设置到某个特定的控件,也可以使用 TraversalRequest 仅仅移动焦点。本文介绍如何在 WPF 程序中控制控件的焦点。


UIElement.Focus

仅仅需要在任何一个控件上调用 Focus() 方法即可将焦点设置到这个控件上。

但是需要注意,要使 Focus() 能够工作,这个元素必须满足两个条件:

  • Focusable 设置为 true
  • IsVisibletrue

TraversalRequest

如果你并不是将焦点设置到某个特定的控件,而是希望将焦点转移,可以考虑使用 TraversalRequest 类。

比如,以下代码是将焦点转移到下一个控件,也就是按下 Tab 键时焦点会去的控件。

var traversalRequest = new TraversalRequest(FocusNavigationDirection.Next);
// view 是可视化树中的一个控件。
view.MoveFocus(traversalRequest);

关于逻辑焦点和键盘焦点

键盘焦点就是你实际上按键输入和快捷键会生效的焦点,也就是当前正在工作的控件的焦点。

而 WPF 有多个焦点范围(Focus Scope),按下 Tab 键切换焦点的时候只会在当前焦点范围切焦点,不会跨范围。那么一旦跨范围切焦点的时候,焦点会去哪里呢?答案是逻辑焦点。

每个焦点范围内都有一个逻辑焦点,记录如果这个焦点范围一旦获得焦点后应该在哪个控件获得键盘焦点。

比如默认情况下 WPF 每个 Window 就是一个焦点范围,那么每个 Window 中的当前焦点就是逻辑焦点。而一旦这个 Window 激活,那么这个窗口中的逻辑焦点就会成为键盘焦点,另一个窗口当中的逻辑焦点保留,而键盘焦点则丢失。

跨窗口/跨进程切换焦点

参见我的另一篇博客:


参考资料

使用 SetParent 制作父子窗口的时候,如何设置子窗口的窗口样式以避免抢走父窗口的焦点

dotnet 职业技术学院 发布于 2019-09-19

制作传统 Win32 程序以及 Windows Forms 程序的时候,一个用户看起来独立的窗口本就是通过各种父子窗口嵌套完成的,有大量窗口句柄,窗口之间形成父子关系。不过,对于 WPF 程序来说,一个独立的窗口实际上只有一个窗口句柄,窗口内的所有内容都是 WPF 绘制的。

如果你不熟悉 Win32 窗口中的父子窗口关系和窗口样式,那么很有可能遇到父子窗口之间“抢夺焦点”的问题,本文介绍如何解决这样的问题。


“抢夺焦点”

下图中的上下两个部分是两个不同的窗口,他们之间通过 SetParent 建立了父子关系。

注意看下面的窗口标题栏,当我在这些不同区域间点击的时候,窗口标题栏在黑色和灰色之间切换:

抢夺焦点

这说明当子窗口获得焦点的时候,父窗口会失去焦点并显示失去焦点的样式。

你可以在这篇博客中找到一个简单的例子:

解决办法

而原因和解决方法仅有一个,就是子窗口需要有一个子窗口的样式。

具体来说,子窗口必须要有 WS_CHILD 样式。

你可以看看 Spyxx.exe 抓出来的默认普通窗口和子窗口的样式差别:

![默认普通窗口]](/static/posts/2019-09-19-10-21-31.png)

▲ 默认普通窗口

子窗口

▲ 子窗口


参考资料

.NET/C# 利用 Walterlv.WeakEvents 高性能地定义和使用弱事件

dotnet 职业技术学院 发布于 2019-09-18

弱引用是 .NET 引入的概念,可以用来协助解决内存泄漏问题。然而事件也可能带来内存泄漏问题,是否有弱事件机制可以使用呢?.NET 没有自带的弱事件机制,但其中的一个子集 WPF 带了。然而我们不是什么项目都能引用 WPF 框架类库的。

本文介绍 Walterlv.WeakEvents 库来定义和使用弱事件。


系列博客:

下载安装 Walterlv.WeakEvents

在你需要做弱事件的项目中安装 NuGet 包:

定义弱事件

现在,定义弱事件就不能直接写 event EventHandler Bar 了,要像下面这样写:

using System;
using Walterlv.WeakEvents;

namespace Walterlv.Demo
{
    public class Foo
    {
        private readonly WeakEvent<EventArgs> _bar = new WeakEvent<EventArgs>();

        public event EventHandler Bar
        {
            add => _bar.Add(value, value.Invoke);
            remove => _bar.Remove(value);
        }

        private void OnBar() => _bar.Invoke(this, EventArgs.Empty);
    }
}

使用弱事件

对于弱事件的使用,就跟以前任何其他正常事件一样了,直接 +=-=

这样,如果我有一个 A 类的实例 a,订阅了以上 FooBar 事件,那么当 a 脱离作用范围后,将可以被垃圾回收机制回收。而如果不这么做,Foo 将始终保留对 a 实例的引用,这将阻止垃圾回收。