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 中的分析器严重性内容就像下面这样:
[*.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)一定不陌生。如果你希望在模态窗口之上做更多的事情,或者自己实现一套模态窗口类似的机制,那么你可能需要了解模态窗口的本质。
本文不会太深,只是从模态窗口一词出发,抵达大家都熟知的一些知识为止。
在各种系统、语言和框架中,只要有用户可以看见的界面,都存在模态窗口的概念。从交互层面来说,它的形式是在保留主界面作为环境来显示的情况下,打开一个新的界面,用户只能在新的界面上操作,完成或取消后才能返回主界面。从作用上来说,通常是要求用户必须提供某些信息后才能继续操作,或者单纯只是为了广告。
如果你希望自己搞一套模态窗口出来,那么只需要满足这三点即可。你可以随便加戏但那都无关紧要。
拿 Windows 系统中的模态对话框为例子,大概就像下面这两张图片这样:
有一个小的子界面盖住了主界面,要求用户必须进行选择。Windows 系统设置因为让背景变暗了,所以用户肯定会看得到需要进行的交互;而任务管理器没有让主界面变暗,所以用户在操作子界面的时候,模态窗口的边框和标题栏闪烁以提醒用户注意。
对于 Windows 操作系统来说,模态窗口并不是一个单一的概念,你并不能仅通过一个 API 调用就完成显示模态窗口,你需要在不同的时机调用不同的 API 来完成一个模态窗口。如果要完整实现一个自己的模态窗口,则需要编写实现以上三个特点的代码。
当然,你可能会发现实际上你显示一个模态窗口仅仅一句话调用就够了,那是因为你所用的应用程序框架帮你完成了模态窗口的一系列机制。
关于 WPF 框架是如何实现模态窗口的,可以阅读:直击本质:WPF 框架是如何实现模态窗口的
关于如何自己实现一个跨越线程/进程边界的模态窗口,可以阅读:实现 Windows 系统上跨进程/跨线程的模态窗口
如果你希望定制以上第三个特点中强提醒的动画效果,可以阅读:WPF window 子窗口反馈效果(抖动/阴影渐变) - 唐宋元明清2188 - 博客园。
为了在 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 上的提醒是最简单的了,比如:
然而 Windows 和 Mac OS 这些古老的系统由于兼容性负担不能随便那么改,于是需要有其他的提醒方式。
Windows 采用的方式是让标题栏闪烁,让阴影闪烁。
而这些特效的处理,来自于子窗口需要处理一些特定的消息 WM_SETCURSOR
。
详见:WPF window 子窗口反馈效果(抖动/阴影渐变) - 唐宋元明清2188 - 博客园
通常你不需要手工处理这些消息,但是如果你完全定制了窗口样式,则可能需要自行做一个这样的模态窗口提醒效果。
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);
}
}
}
觉得代码还是太长?不要紧,我再简化一下:
EnumThreadWindows
获取当前线程的所有窗口可以注意到禁用掉的窗口是“当前线程”的哦。
ShowHelper
接下来的重点方法是 Window.ShowDialog
中的那句 Show()
。在 Show()
之前设置了 _showingAsDialog
为 true
,于是这里会调用 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();
}
}
可以看到,重点是 PushModal
、PopModal
以及 PushFrame
。
PushFrame
的效果就是让调用 ShowDialog
的代码看起来就像阻塞了一样(实际上就是阻塞了,只不过开了新的消息循环看起来 UI 不卡)。
关于 PushFrame
为什么能够“阻塞”你的代码的同时还能继续响应 UI 操作的原理,可以阅读:
那么 ComponentDispatcher.PushModal
和 ComponentDispatcher.PopModal
呢?可以在这里(ComponentDispatcherThread.cs)看它的代码,实际上是为了模态计数以及引发事件的,对模态的效果没有本质上的影响。
dotnet 职业技术学院 发布于 2019-10-10
如果你希望知道某台计算机上安装了哪些版本的 .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" },
};
另外,还有一些值也是有意义的(只是不那么精确):
它们分别在注册表的这些位置:
计算机\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
如下:
字典里 Key 是不能共存的主版本,Value 是这个主版本里当前已经安装的具体版本信息。
如果直接使用 ToString()
,是可以生成我们平时经常在各大文档或者社区使用的 .NET Framework 的名称。
第二种,获取当前已安装的最新的 .NET Framework 版本名称:
var currentVersion = NdpInfo.GetCurrentVersionName();
这可以直接获取到一个字符串,比如 .NET Framework 4.8
。对于只是简单获取一下已安装名称而不用做更多处理的程序来说会比较方便。
dotnet 职业技术学院 发布于 2019-10-10
在 Windows 应用开发中,如果需要操作其他的窗口,那么可以使用 EnumWindows
这个 API 来枚举这些窗口。
你可以使用本文编写的一个类型,查找到所有窗口中你关心的信息。
枚举所有窗口仅需要使用到 EnumWindows
,其中需要定义一个委托 WndEnumProc
作为传入参数的类型。
剩下的我们需要其他各种方法用于获取窗口的其他属性。
GetParent
获取窗口的父窗口,这可以确认找到的窗口是否是顶层窗口。(关于顶层窗口,可以延伸 使用 SetParent 跨进程设置父子窗口时的一些问题(小心卡死) - walterlv。)IsWindowVisible
判断窗口是否可见GetWindowText
获取窗口标题GetClassName
获取窗口类名GetWindowRect
获取窗口位置和尺寸,为此我们还需要定义一个结构体 LPRECT
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;
}
}
dotnet 职业技术学院 发布于 2019-10-10
从 Windows 10 (1803) 开始,Win32 应用也可以有 API 来实现原生的亚克力效果了。不过相比于 UWP 来说,可定制性会差很多。
本文介绍如何在 WPF 程序中应用 Windows 10 真•亚克力效果。(而不是一些流行的项目里面自己绘制的亚克力效果。)
需要使用的 API 是微软的文档中并未公开的 SetWindowCompositionAttribute
。
我在另一篇博客中有介绍此 API 各种用法的效果,详见:
当然,使用此 API 也可以做 Windows 10 早期的模糊效果,比如:
为了方便地让你的窗口获得亚克力效果,我做了两层不同的 API:
AcrylicBrush
当然,受到 Win32 启用亚克力效果的限制,只能在窗口上设置此属性WindowAccentCompositor
用于更多地控制窗口与系统的叠加组合效果代码请参见:
要使得亚克力效果可以生效,需要:
GradientColor
参考资料
dotnet 职业技术学院 发布于 2019-10-10
Windows 系统中有一个没什么文档的 API,SetWindowCompositionAttribute
,可以允许应用的开发者将自己窗口中的内容渲染与窗口进行组合。这可以实现很多系统中预设的窗口特效,比如 Windows 7 的毛玻璃特效,Windows 8/10 的前景色特效,Windows 10 的模糊特效,以及 Windows 10 1709 的亚克力(Acrylic)特效。而且这些组合都发生在 dwm 进程中,不会额外占用应用程序的渲染性能。
本文介绍 SetWindowCompositionAttribute
可以实现的所有效果。你可以通过阅读本文了解到与系统窗口可以组合渲染到哪些程度。
本文将创建一个简单的 WPF 程序来验证 SetWindowCompositionAttribute
能达到的各种效果。你也可以不使用 WPF,得到类似的效果。
简单的项目文件结构是这样的:
其中,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
让客户区覆盖非客户区使用 WindowChrome
,你可以用你自己的 UI 覆盖掉系统的 UI 窗口样式。关于 WindowChrome
让客户区覆盖非客户区的知识,可以阅读:
需要注意的是,WindowChrome
的 GlassFrameThickness
属性可以设置窗口边框的粗细,设置为 0
将导致窗口没有阴影,设置为负数将使得整个窗口都是边框。
我们依次来看看效果。
使用 ACCENT_DISABLED
时,GradientColor
叠加色没有任何影响,唯一影响渲染的是 WindowChrome
和操作系统。
不使用 WindowChrome
,在 Windows 10 上:
不使用 WindowChrome
在 Windows 7 上:
在 Windows 10 上,使用 WindowChrome
:
<WindowChrome.WindowChrome>
<WindowChrome />
</WindowChrome.WindowChrome>
在 Windows 7 上,使用 WindowChrome
:
当然,以上边框比较细,跟系统不搭,可以设置成其他值:
在 Windows 10 上,使用 WindowChrome
并且 GlassFrameThickness
设置为 -1
:
<WindowChrome.WindowChrome>
<WindowChrome GlassFrameThickness="-1" />
</WindowChrome.WindowChrome>
而在 Windows 7 上,这就是非常绚丽的全窗口的 Aero 毛玻璃特效:
使用 ACCENT_DISABLED
时,GradientColor
叠加色会影响到最终的渲染效果。
还记得我们前面叠加的颜色是什么吗?
接下来别忘了然后把它误以为是我系统的主题色哦!
不使用 WindowChrome
,在 Windows 10 上:
另外,你会注意到左、下、右三个方向上边框会深一些。那是 Windows 10 的窗口阴影效果,因为实际上 Windows 10 叠加的阴影也是窗口区域的一部分,只是一般人看不出来而已。我们叠加了颜色之后,这里就露馅儿了。
另外,这个颜色并不是我们自己的进程绘制的哦,是 dwm 绘制的颜色。
如果不指定 GradientColor
也就是保持为 0
,你将看到上面绿色的部分全是黑色的;嗯,包括阴影的部分……
不使用 WindowChrome
在 Windows 7 上:
可以看出,在 Windows 7 上,GradientColor
被无视了。
而使用 WindowChrome
在 Windows 10 上,则可以得到整个窗口的叠加色:
<WindowChrome.WindowChrome>
<WindowChrome GlassFrameThickness="16 48 16 16" />
</WindowChrome.WindowChrome>
可以注意到,窗口获得焦点的时候,整个窗口都是叠加色;而窗口失去焦点的时候,指定了边框的部分颜色会更深(换其他颜色叠加可以看出来是叠加了半透明黑色)。
如果你希望失去焦点的时候,边框部分不要变深,请将边框设置为 -1
:
<WindowChrome.WindowChrome>
<WindowChrome GlassFrameThickness="-1" />
</WindowChrome.WindowChrome>
使用 WindowChrome
在 Windows 7 上,依然没有任何叠加色的效果:
使用 ACCENT_ENABLE_TRANSPARENTGRADIENT
时,GradientColor
叠加色没有任何影响,唯一影响渲染的是 WindowChrome
和操作系统。
不使用 WindowChrome
,在 Windows 10 上:
依然左、下、右三个方向上边框会深一些,那是 Windows 10 的窗口阴影效果。
不使用 WindowChrome
在 Windows 7 上:
GradientColor
也是被无视的,而且效果跟之前一样。
使用 WindowChrome
在 Windows 10 上,在获得焦点的时候整个背景是系统主题色;而失去焦点的时候是灰色,但边框部分是深色。
依然可以将边框设置为 -1
使得边框不会变深:
使用 WindowChrome
在 Windows 7 上,依然是老样子:
ACCENT_ENABLE_BLURBEHIND
可以在 Windows 10 上做出模糊效果,就跟 Windows 10 早期版本的模糊效果是一样的。你可以看我之前的一篇博客,那时亚克力效果还没出来:
使用 ACCENT_ENABLE_BLURBEHIND
时,GradientColor
叠加色没有任何影响,唯一影响渲染的是 WindowChrome
和操作系统。
在 Windows 10 上,没有使用 WindowChrome
:
你可能需要留意一下那个“诡异”的模糊范围,你会发现窗口的阴影外侧也是有模糊的!!!你能忍吗?肯定不能忍,所以还是乖乖使用 WindowChrome
吧!
在 Windows 7 上,没有使用 WindowChrome
,效果跟其他值一样,依然没有变化:
在 Windows 10 上,使用 WindowChrome
:
使用 WindowChrome
在 Windows 7 上,依然是老样子:
从 Windows 10 (1803) 开始,Win32 程序也能添加亚克力效果了,因为 SetWindowCompositionAttribute
的参数枚举新增了 ACCENT_ENABLE_ACRYLICBLURBEHIND
。
亚克力效果相信大家不陌生,那么在 Win32 应用程序里面使用的效果是什么呢?
不使用 WindowChrome
,在 Windows 10 上:
咦!等等!这不是跟之前一样吗?
嗯,下面就是不同了,亚克力效果支持与半透明的 GradientColor
叠加,所以我们需要将传入的颜色修改为半透明:
var compositor = new WindowAccentCompositor(this);
-- compositor.Composite(Color.FromRgb(0x18, 0xa0, 0x5e));
++ compositor.Composite(Color.FromArgb(0x3f, 0x18, 0xa0, 0x5e));
那么如果改为全透明会怎么样呢?
不幸的是,完全没有效果!!!
var compositor = new WindowAccentCompositor(this);
-- compositor.Composite(Color.FromRgb(0x18, 0xa0, 0x5e));
++ compositor.Composite(Color.FromArgb(0x00, 0x18, 0xa0, 0x5e));
接下来是使用 WindowChrome
时:
<WindowChrome.WindowChrome>
<WindowChrome GlassFrameThickness="16 48 16 16" />
</WindowChrome.WindowChrome>
然而周围有一圈偏白色的渐变是什么呢?那个其实是 WindowChrome
设置的边框白,被亚克力效果模糊后得到的混合效果。
所以,如果要获得全窗口的亚克力效果,请将边框设置成比较小的值:
<WindowChrome.WindowChrome>
<WindowChrome GlassFrameThickness="0 1 0 0" />
</WindowChrome.WindowChrome>
记得不要像前面的那些效果一样,如果设置成 -1
,你将获得纯白色与设置的 Gradient
叠加色的亚克力特效,是个纯色:
你可以将叠加色的透明度设置得小一些,这样可以看出叠加的颜色:
var compositor = new WindowAccentCompositor(this);
-- compositor.Composite(Color.FromRgb(0x18, 0xa0, 0x5e));
++ compositor.Composite(Color.FromArgb(0xa0, 0x18, 0xa0, 0x5e));
那么可以设置为全透明吗?
var compositor = new WindowAccentCompositor(this);
-- compositor.Composite(Color.FromRgb(0x18, 0xa0, 0x5e));
++ compositor.Composite(Color.FromArgb(0x00, 0x18, 0xa0, 0x5e));
很不幸,最终你会完全看不到亚克力效果,而变成了毫无特效的透明窗口:
最上面那根白线,是我面前面设置边框为 0 1 0 0
导致的。
如果在这种情况下,将边框设置为 0
会怎样呢?记得前面我们说过的吗,会导致阴影消失哦!
呃……你将看到……这个……
什么都没有……
是不是找到了一条新的背景透明异形窗口的方法?
还是省点心吧,亚克力效果在 Win32 应用上的性能还是比较堪忧的……
想要背景透明,请参见:
不用考虑 Windows 7,因为大家都知道不支持。实际效果会跟前面的一模一样。
这个值其实不用说了,因为 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 版的截图,也是我实际操作分区时截下来的图:
不要吐槽为何我用的是古老的 1709 系统,实际上我的系统盘是下面那个 I 盘。不然为什么我会把系统的版本号放到卷标中呢?
在 PE 系统中找到傲梅分区助手,然后启动。在需要调整位置和大小的分区上右键点击选择“调整/移动分区”:
然后在弹出的详细设置对话框中调整分区的位置和大小。如果是 SSD,建议点击“高级”然后勾选“允许分区对齐以优化SSD或HDD硬盘”,这可以开启 4K 对齐以大幅优化 SSD 的读写性能。
最后点击确定。
注意这个时候还没有开始执行真正的操作!
合并分区功能可以将你一个磁盘中的多个分区无损合并成一个。
选择好将哪个分区合并到哪一个,这时另一个分区中的所有文件会放到目标分区中的一个文件夹里。合并完之后你自己移动好这些文件即可。
因为我的分区在合并过程中的操作没有截图,所以只能看到下面这个提前在磁盘管理中的截图:
在你设置好你的所有操作之后,点击左上角的“提交”按钮,这可以开始依次执行之前所有设置的磁盘最终状态。
在提交界面,你可以看到即将进行的所有操作的简介,以及预计完成这些操作所花的时间。
虽然上图只是示例,但我实际将我在下面这篇博客中删除出来的空余空间全部合并在一起,并且还额外合并了两个都需要保留数据的分区。这个过程傲梅的预计时间是 9小时18分,实际上也刚好在 9 个小时左右!
所以,如果你打算开始进行大量的磁盘调整、对拷或者其他无损分区操作:
这是我实际上在 PE 中操作的截图:
dotnet 职业技术学院 发布于 2019-10-07
Windows 系统在安装的时候,会自动为我们的磁盘划分一个恢复分区和一个 EFI 分区。如果后面不打算再用这些分区的时候,却发现无法删除。
本文将提供解决方法。
因为误操作会导致数据丢失,所以我将两种不同的解决方法分开成两篇文章以避免干扰:
看下图,有两种不同类型的无法删除:
删除方法会略有不同,我会在合适的地方提示你使用正确的方法的。
我的磁盘 2 原本包含两个可见分区,一个是图中黑色色块,原来放的是旧操作系统,一个是图中的 D 盘,放大量文件。因为我新买了一个大容量 SSD 专门用来放操作系统,所以原来操作系统所在的磁盘就可以回收与 D 盘合并。
然而悲剧的是,中间隔着一个 820MB 的恢复分区,导致我没有办法为 D 分区扩容。
更麻烦的是,在磁盘管理中,这三个我不会再使用的恢复分区都不可删除。
PS. 吐槽一下,大版本升级一次 Windows 10 竟然会在后面给我多创建一个恢复分区……
在实操之前,你必须清除地知道你每一步在做什么,否则你需要承担丢失数据的后果:
打开开始菜单,输入 cmd
然后回车确定,我们可以打开命令提示符
在 cmd
中输入 diskpart
然后回车,你会看到一个 UAC 提示弹窗,点击“是”之后会启动一个新的管理员权限启动的命令提示符,这里运行着 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 *
请注意,这里看到的是磁盘,而不是平时在“计算机”中看到的分区——每一个磁盘都可以包含一个到多个分区哦!
你有两种方法来确认我们即将操作的是哪个磁盘:
好的,我们知道要操作的是“磁盘 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 现在是所选分区。
然后更改其 ID:
DISKPART> SET ID=ebd0a0a2-b9e5-4433-87c0-68b6b72699c7
DiskPart 成功设置了分区 ID。
然后操作其他的分区。
完整的截图如下:
这个时候,回到磁盘管理中,F5 刷新,你可以看到原本不可删除的 EFI 分区,现在可以直接使用鼠标删除了。点击一下“删除卷”即可。
恢复分区不能使用上面 4.1 中的方法删除,如果你在 4.1 的操作之后还发现存在不可删除的恢复分区,请尝试使用我的另一篇博客:
参考资料
dotnet 职业技术学院 发布于 2019-10-07
Windows 系统在安装的时候,会自动为我们的磁盘划分一个恢复分区和一个 EFI 分区。如果后面不打算再用这些分区的时候,却发现无法删除。
本文将提供解决方法。
因为误操作会导致数据丢失,所以我将两种不同的解决方法分开成两篇文章以避免干扰:
看下图,有两种不同类型的无法删除:
使用本文提供的方法,你可以删除以上两种不同类型的分区。
我的磁盘 2 原本包含两个可见分区,一个是图中黑色色块,原来放的是旧操作系统,一个是图中的 D 盘,放大量文件。因为我新买了一个大容量 SSD 专门用来放操作系统,所以原来操作系统所在的磁盘就可以回收与 D 盘合并。
然而悲剧的是,中间隔着一个 820MB 的恢复分区,导致我没有办法为 D 分区扩容。
更麻烦的是,在磁盘管理中,这三个我不会再使用的恢复分区都不可删除。
PS. 吐槽一下,大版本升级一次 Windows 10 竟然会在后面给我多创建一个恢复分区……
在实操之前,你必须清除地知道你每一步在做什么,否则你需要承担丢失数据的后果:
打开开始菜单,输入 cmd
然后回车确定,我们可以打开命令提示符
在 cmd
中输入 diskpart
然后回车,你会看到一个 UAC 提示弹窗,点击“是”之后会启动一个新的管理员权限启动的命令提示符,这里运行着 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 *
请注意,这里看到的是磁盘,而不是平时在“计算机”中看到的分区——每一个磁盘都可以包含一个到多个分区哦!
你有两种方法来确认我们即将操作的是哪个磁盘:
好的,我们知道要操作的是“磁盘 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 盘扩展更多空间啦!
参考资料
dotnet 职业技术学院 发布于 2019-09-27
启用转为编程设计的连字字体,可以给你的变成带来不一样的体验。
微软随 Windows Terminal 设计了一款新的字体 Cascadia Code,而这是一款连字字体。
你可以看到,在 Windows Terminal 的终端中,=>
==
!=
符号显示成了更容易理解的连字符号:
在 Cascadia Code 发布之前,Fira Code 是一款特别火的连字字体,下面是 Fira Code 连字字体在 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。
下载解压后,你会看到五个不同的文件夹,这是四种不同的字体类型:
对于 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 中启用连字字体需要用到两个选项:
"editor.fontFamily": "Fira Code Light, Consolas, Microsoft YaHei",
"editor.fontLigatures": true,
然后点击新打开的标签右上角的 {}
图标以打开 json 形式编辑的设置:
然后修改把上面两个设置增加或替换进去即可。下面是我的设置的部分截图:
只需要将字体设置成 Fira Code 即可。
参考资料
dotnet 职业技术学院 发布于 2019-09-24
.NET Core 3 相比于 .NET Core 2 是一个大更新。也正因为如此,即便它长时间处于预览版尚未发布的状态,大家也一直在使用。
Visual Studio 2019 中提供了使用 .NET Core SDK 预览版的开关。但几个更新的版本其开关的位置不同,本文将介绍在各个版本中的位置,方便你找到然后设置。
.NET Core 3.0 已经发布,下载地址:
Visual Studio 16.3 与 .NET Core 3.0 正式版同步发布,因此不再需要 .NET Core 3.0 的预览版设置界面。你只需要安装正式版 .NET Core SDK 即可。
从 Visual Studio 2019 的 16.2 版本,.NET Core 预览版的设置项的位置在:
工具
-> 选项
环境
-> 预览功能
-> Use previews of the .NET Core SDK (需要 restart)
如果你是英文版的 Visual Studio,也可以参考英文版:
Tools
-> Options
Environment
-> Preview Features
-> Use previews of the .NET Core SDK (requires restart)
从 Visual Studio 2019 的 16.1 版本,.NET Core 预览版的设置项的位置在:
工具
-> 选项
环境
-> 预览功能
-> 使用 .NET Core SDK 的预览
如果你是英文版的 Visual Studio,也可以参考英文版:
Tools
-> Options
Environment
-> Preview Features
-> Use previews of the .NET Core SDK
在 Visual Studio 2019 的早期,.NET Core 在设置中是有一个专用的选项的,在这里:
工具
-> 选项
项目和解决方案
-> .NET Core
-> 使用 .NET Core SDK 预览版
如果你是英文版的 Visual Studio,也可以参考英文版:
Tools
-> Options
Projects and solutions
-> .NET Core
-> Use previews of the .NET Core SDK
Visual Studio 2019 中此对于 .NET Core SDK 的预览版的设置是全局生效的。
也就是说,你在 Visual Studio 2019 中进行了此设置,在命令行中使用 MSBuild
或者 dotnet build
命令进行编译也会使用这样的设置项。
那么这个全局的设置项在哪个地方呢?是如何全局生效的呢?可以阅读我的其他博客:
dotnet 职业技术学院 发布于 2019-09-19
WPF 中可以使用 UIElement.Focus()
将焦点设置到某个特定的控件,也可以使用 TraversalRequest
仅仅移动焦点。本文介绍如何在 WPF 程序中控制控件的焦点。
UIElement.Focus
仅仅需要在任何一个控件上调用 Focus()
方法即可将焦点设置到这个控件上。
但是需要注意,要使 Focus()
能够工作,这个元素必须满足两个条件:
Focusable
设置为 true
IsVisible
是 true
TraversalRequest
如果你并不是将焦点设置到某个特定的控件,而是希望将焦点转移,可以考虑使用 TraversalRequest
类。
比如,以下代码是将焦点转移到下一个控件,也就是按下 Tab 键时焦点会去的控件。
var traversalRequest = new TraversalRequest(FocusNavigationDirection.Next);
// view 是可视化树中的一个控件。
view.MoveFocus(traversalRequest);
键盘焦点就是你实际上按键输入和快捷键会生效的焦点,也就是当前正在工作的控件的焦点。
而 WPF 有多个焦点范围(Focus Scope),按下 Tab 键切换焦点的时候只会在当前焦点范围切焦点,不会跨范围。那么一旦跨范围切焦点的时候,焦点会去哪里呢?答案是逻辑焦点。
每个焦点范围内都有一个逻辑焦点,记录如果这个焦点范围一旦获得焦点后应该在哪个控件获得键盘焦点。
比如默认情况下 WPF 每个 Window
就是一个焦点范围,那么每个 Window
中的当前焦点就是逻辑焦点。而一旦这个 Window
激活,那么这个窗口中的逻辑焦点就会成为键盘焦点,另一个窗口当中的逻辑焦点保留,而键盘焦点则丢失。
参见我的另一篇博客:
参考资料
dotnet 职业技术学院 发布于 2019-09-19
制作传统 Win32 程序以及 Windows Forms 程序的时候,一个用户看起来独立的窗口本就是通过各种父子窗口嵌套完成的,有大量窗口句柄,窗口之间形成父子关系。不过,对于 WPF 程序来说,一个独立的窗口实际上只有一个窗口句柄,窗口内的所有内容都是 WPF 绘制的。
如果你不熟悉 Win32 窗口中的父子窗口关系和窗口样式,那么很有可能遇到父子窗口之间“抢夺焦点”的问题,本文介绍如何解决这样的问题。
下图中的上下两个部分是两个不同的窗口,他们之间通过 SetParent
建立了父子关系。
注意看下面的窗口标题栏,当我在这些不同区域间点击的时候,窗口标题栏在黑色和灰色之间切换:
这说明当子窗口获得焦点的时候,父窗口会失去焦点并显示失去焦点的样式。
你可以在这篇博客中找到一个简单的例子:
而原因和解决方法仅有一个,就是子窗口需要有一个子窗口的样式。
具体来说,子窗口必须要有 WS_CHILD
样式。
你可以看看 Spyxx.exe 抓出来的默认普通窗口和子窗口的样式差别:
![默认普通窗口]](/static/posts/2019-09-19-10-21-31.png)
▲ 默认普通窗口
▲ 子窗口
参考资料
dotnet 职业技术学院 发布于 2019-09-18
弱引用是 .NET 引入的概念,可以用来协助解决内存泄漏问题。然而事件也可能带来内存泄漏问题,是否有弱事件机制可以使用呢?.NET 没有自带的弱事件机制,但其中的一个子集 WPF 带了。然而我们不是什么项目都能引用 WPF 框架类库的。
本文介绍 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
,订阅了以上 Foo
的 Bar
事件,那么当 a
脱离作用范围后,将可以被垃圾回收机制回收。而如果不这么做,Foo
将始终保留对 a
实例的引用,这将阻止垃圾回收。