1. 为什么要自定义Window

对稍微有点规模的桌面软件来说自定义的Window几乎是标配了,一来设计师总是克制不住自己想想软件更个性化,为了UI的和谐修改Window也是必要的;二来多一行的空间可以添加很多功能,尤其是上边缘,因为被屏幕限制住鼠标的移动所以上边缘的按钮很容易选中。做桌面开发总有一天会遇到自定义Window的需求,所以我在控件库中也提供了一个简单的自定义Window。

2. 我想要的功能

我在上一篇文章介绍了标准Window的功能,我想实现一个包含这些基本功能的,窄边框、扁平化的Window,基本上模仿Windows 10 的Window,但要可以方便地自定义样式;阴影、动画效果保留系统默认的就可以了,基本上会很耐看。最后再放置一个FunctionBar方便添加更多功能。

最后成果如下:

这是一个名为ExtendedWindow的自定义Window,源码地址可见文章最后。

3. WindowChrome

3.1 为什么要使用WindowChrome自定义Window

WPF有两种主流的自定义Window的方案,《WPF编程宝典》介绍了使用WindowStyle="None"AllowsTransparency="True"创建无边框的Window然后在里面仿造一个Window,以前也有很多博客详细介绍了这种方式,这里就不再赘述。这种方法的原理是从Window中删除non-client area(即chrome),再由用户自定义Window的所有外观和部分行为。这种方式的自由度很高,但也有不少问题:

  • Window没有阴影导致很难看,但添加自定义的DropShadowEffect又十分影响性能;
  • 没有弹出、关闭、最大化、最小化动画,尤其当启动了大量任务将任务栏堆满的情况下没有最小化动画很容易找不到自己的程序;
  • 没有动画很麻烦,自定义的动画做得不好也十分影响使用;
  • 需要写大量代码实现Window本来的拖动、改变大小、最大化等行为;
  • 各种其它细节的缺失;

大部分自定义Window或多或少都有上面所说的问题,幸好WPF提供了WindowChrome这个类用于创建自定义的Window,这个类本身处理了上面部分问题。

3.2 WindowChrome的基本概念

WindowChrome定义了Window non-client area(即chrome)的外观和行为, 在Window上应用WindowChrome的WindowChrome附加属性即可将Window的non-client area替换为WindowChrome(绕口):

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

然后用Blend生成这个Window的Style,将最外层Border的背景移除并做了些简化后大概是这样:

<Window.Style>
<Style TargetType="{x:Type Window}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Window}">
<Border>
<Grid>
<AdornerDecorator>
<ContentPresenter />
</AdornerDecorator>
<ResizeGrip x:Name="WindowResizeGrip"
HorizontalAlignment="Right"
IsTabStop="false"
Visibility="Collapsed"
VerticalAlignment="Bottom" />
</Grid>
</Border>
<ControlTemplate.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="ResizeMode" Value="CanResizeWithGrip" />
<Condition Property="WindowState" Value="Normal" />
</MultiTrigger.Conditions>
<Setter Property="Visibility" TargetName="WindowResizeGrip" Value="Visible" />
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Style>

这样一个没有Content的Window运行效果如下:

可以看到WindowChrome已经定义好noe-client area的边框、阴影、标题栏、右上角的三个按钮,ControleTemplate里也在右下角放置了一个ResizeGrip,而且拖动、改变大小、最大化最小化、动画等功能都已经做好了。除了Icon和标题外WindowChrome已经把一个标准的Window实现得差不多了。要实现自定义Window,只需要将我们想要的边框、Icon、标题、自定义样式的按钮等放在上面遮挡WindowChrome的各种元素就可以了。原理十分简单,接下来再看看WindowChrome的各个属性。

3.3 UseAeroCaptionButtons

UseAeroCaptionButtons表示标题栏上的那三个默认按钮是否可以命中,因为我们想要自己管理这三个按钮的样式、显示或隐藏,所以设置为False。

3.4 GlassFrameThickness和ResizeBorderThickness

GlassFrameThicknessResizeBorderThickness,这两个属性用于控制边框,及用户可以单击并拖动以调整窗口大小的区域的宽度。如果两个都设置为50效果如下:

可以看到因为边框和ResizeBorder变大了,标题栏也下移了相应的距离(通过可拖动区域和SystemMenu的位置判断)。当然因为外观是我们自己定义的,ResizeBorderThickness也不需要这么宽,所以两个值都保留默认值就可以了。

3.5 CaptionHeight

CaptionHeight指定WindowChrome的标题栏高度。它不影响外观,因为WindowChrome的标题栏范围实际是不可见的,它包括可以拖动窗体、双击最大化窗体、右键打开SystemMenu等行为。

CaptionHeight、GlassFrameThickness和ResizeBorderThickness的默认值都和SystemParameters的对应的值一致。

3.6 IsHitTestVisibleInChrome附加属性

GlassFrameThickness和CaptionHeight定义了Chrome的范围,默认情况下任何在Chrome的范围内的元素都不可以交互,如果需要在标题栏放自己的按钮(或其它交互元素)需要将这个按钮的WindowsChrome.IsHitTestVisibleInChrome附加属性设置为True。

3.7 使用WindowChrome

综上所述,使用WindowChrome只需要设置UseAeroCaptionButtons为False,并且设置CaptionHeight,比较标准的做法是使用SystemParameter的WindowNonClientFrameThickness的Top,在100% DPI下是 27 像素(其它三个边都为4像素,因为我的目标是窄边框的Window,所以不会用这个值)。

<Setter Property="WindowChrome.WindowChrome">
<Setter.Value>
<WindowChrome UseAeroCaptionButtons="False"
CaptionHeight="{Binding Path=(SystemParameters.WindowNonClientFrameThickness).Top}" />
</Setter.Value>
</Setter>

WindowChrome的文档有些旧了,文档中介绍的SystemParameters2在.NET 4.5已经找不到,在Github上还能找到不少它的实现,但没必要勉强用一个旧的API。

4. 自定义Window基本布局

<ControlTemplate TargetType="{x:Type Window}">
<Border BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
x:Name="WindowBorder">
<Grid x:Name="LayoutRoot"
Background="{TemplateBinding Background}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid x:Name="WindowTitlePanel"
Height="{Binding Path=(SystemParameters.WindowNonClientFrameThickness).Top}"
Background="{TemplateBinding BorderBrush}"
Margin="0,-1,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions> <StackPanel Orientation="Horizontal">
<Image Source="{TemplateBinding Icon}"
Height="{x:Static SystemParameters.SmallIconHeight}"
Width="{x:Static SystemParameters.SmallIconWidth}"
WindowChrome.IsHitTestVisibleInChrome="True" />
<ContentControl FontSize="{DynamicResource {x:Static SystemFonts.CaptionFontSize}}"
Content="{TemplateBinding Title}" />
</StackPanel> <StackPanel x:Name="WindowCommandButtonsPanel"
Grid.Column="1"
HorizontalAlignment="Right"
Orientation="Horizontal"
WindowChrome.IsHitTestVisibleInChrome="True"
Margin="0,0,-1,0">
<ContentPresenter Content="{Binding FunctionBar, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}"
Focusable="False" />
<Button x:Name="MinimizeButton" />
<Grid Margin="1,0,1,0">
<Button x:Name="RestoreButton"
Visibility="Collapsed" />
<Button x:Name="MaximizeButton" />
</Grid>
<Button x:Name="CloseButton"
Background="Red" />
</StackPanel>
</Grid>
<AdornerDecorator Grid.Row="1"
KeyboardNavigation.IsTabStop="False">
<ContentPresenter Content="{TemplateBinding Content}"
x:Name="MainContentPresenter"
KeyboardNavigation.TabNavigation="Cycle" />
</AdornerDecorator>
<ResizeGrip x:Name="ResizeGrip"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Grid.Row="1" />
</Grid>
</Border>
</ControlTemplate>

上面是简化后的ControlTemplate及运行时的VisualTree结构,它包含以下部分:

  • WindowBorder,外层的边框,它的Border颜色即Window的边框颜色。
  • LayoutRoot,分为两行,第一行为标题栏,第二行为Content。
  • 标题栏,里面包含Icon、Title、FunctionBar及WindowCommandButtonsPanel(包含最小化、最大化、还原和关闭等按钮)。
  • MainContentPresenter,即cient area。
  • ResizeGrip。

5. 绑定到SystemCommand

SystemCommands有5个命令CloseWindowCommand、MaximizeWindowCommand、MinimizeWindowCommand、RestoreWindowCommand、ShowSystemMenuCommand,并且还提供了CloseWindow、MaximizeWindow、MinimizeWindow、RestoreWindow、ShowSystemMenu5个静态方法。Window标题栏上的各个按钮需要绑定到这些命名并执行对应的静态方法。写在自定义的Window类里太复杂了而且不能重用,所以我把这个功能做成附加属性,用法如下:

<Setter Property="local:WindowService.IsBindingToSystemCommands"
Value="True" />

具体实现代码很普通,就是IsBindingToSystemCommands属性改变时调用WindowCommandHelper绑定到各个命令:

private class WindowCommandHelper
{
private Window _window; public WindowCommandHelper(Window window)
{
_window = window;
} public void ActiveCommands()
{
_window.CommandBindings.Add(new CommandBinding(SystemCommands.CloseWindowCommand, CloseWindow));
_window.CommandBindings.Add(new CommandBinding(SystemCommands.MaximizeWindowCommand, MaximizeWindow, CanResizeWindow));
_window.CommandBindings.Add(new CommandBinding(SystemCommands.MinimizeWindowCommand, MinimizeWindow, CanMinimizeWindow));
_window.CommandBindings.Add(new CommandBinding(SystemCommands.RestoreWindowCommand, RestoreWindow, CanResizeWindow));
_window.CommandBindings.Add(new CommandBinding(SystemCommands.ShowSystemMenuCommand, ShowSystemMenu));
} /*SOME CODE*/
}

6. UI元素的实现细节

接下来介绍ControlTemplate中各个UI元素的实现细节。

6.1 标题栏

<Grid x:Name="WindowTitlePanel"
VerticalAlignment="Top"
Height="{Binding Path=(SystemParameters.WindowNonClientFrameThickness).Top}"
Background="{TemplateBinding BorderBrush}">

标题栏的高度和WindowChrome的CaptionHeight一致,而Background则和Window的BorderBrush一致。

Icon

<Image Source="{TemplateBinding Icon}"
VerticalAlignment="Center"
Margin="5,0,5,0"
Height="{x:Static SystemParameters.SmallIconHeight}"
Width="{x:Static SystemParameters.SmallIconWidth}"
WindowChrome.IsHitTestVisibleInChrome="True">
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseLeftButtonDown">
<i:InvokeCommandAction Command="{x:Static SystemCommands.ShowSystemMenuCommand}" />
</i:EventTrigger>
<i:EventTrigger EventName="MouseRightButtonDown">
<i:InvokeCommandAction Command="{x:Static SystemCommands.ShowSystemMenuCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Image>

Icon是一张图片,它的大小由SystemParameters.SmallIconHeightSystemParameters.SmallIconWidth决定,通常来说是16 * 16像素。

Icon还绑定到SystemCommands.ShowSystemMenuCommand,点击鼠标左右键都可以打开SystemMenu。

最后记得设置WindowChrome.IsHitTestVisibleInChrome="True"

Title

<ContentControl IsTabStop="False"
Foreground="White"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="{DynamicResource {x:Static SystemFonts.CaptionFontSize}}"
Content="{TemplateBinding Title}" />

标题的字号由SystemFonts.CaptionFontSize决定,但颜色、字体都自己定义。

6.2 按钮

<Style x:Key="MinimizeButtonStyle"
TargetType="Button"
BasedOn="{StaticResource WindowTitleBarButtonStyle}">
<Setter Property="ToolTip"
Value="Minimize" />
<Setter Property="ContentTemplate"
Value="{StaticResource MinimizeWhite}" />
<Setter Property="Command"
Value="{Binding Source={x:Static SystemCommands.MinimizeWindowCommand}}" />
</Style> <!--OTHER BUTTON STYLES--> <Button x:Name="MinimizeButton"
Style="{StaticResource MinimizeButtonStyle}" />
<Grid Margin="1,0,1,0">
<Button x:Name="RestoreButton"
Style="{StaticResource RestoreButtonStyle}"
Visibility="Collapsed" />
<Button x:Name="MaximizeButton"
Style="{StaticResource MaximizeButtonStyle}" />
</Grid>
<Button x:Name="CloseButton"
Background="Red"
Style="{StaticResource CloseButtonStyle}" />

按钮基本上使用相同的样式,不过CloseButton的背景是红色。按钮的图标参考Windows 10(具体来说是Segoe MDL2里的ChromeMinimize、ChromeMaximize、ChromeRestore、ChromeClose,不过没有在项目中引入Segoe MDL2字体,而是把它们转换成Path来使用)。各个按钮绑定了对应的SystemCommand。

6.3 FunctionBar

<ContentPresenter Content="{Binding FunctionBar, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}"
Focusable="False" />

这篇文章中介绍了FunctionBar的实现及应用,这段XAML即在标题栏为FunctionBar留一个占位符。

6.4 ClientArea

<AdornerDecorator Grid.Row="1"
KeyboardNavigation.IsTabStop="False">
<ContentPresenter Content="{TemplateBinding Content}"
x:Name="MainContentPresenter"
KeyboardNavigation.TabNavigation="Cycle" />
</AdornerDecorator>

这是Client Area部分的内容。一个Window中只有client area中的内容可以获得键盘焦点,而且tab键只会让键盘焦点在Window的内容中循环。当一个Window从非激活状态会到激活状态,之前获得键盘焦点的元素将重新获得键盘焦点。所以AdornerDecorator不要让它获得焦点,而MainContentPresenter则要设置为KeyboardNavigation.TabNavigation="Cycle"

AdornerDecorator 为可视化树中的子元素提供 AdornerLayer,如果没有它的话一些装饰效果不能显示(例如下图Button控件的Focus效果),Window的 ContentPresenter 外面套个 AdornerDecorator 是 必不能忘的。

6.5 ResizeGrip

<ResizeGrip x:Name="ResizeGrip"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Grid.Row="1"
IsTabStop="False"
Visibility="Hidden"
WindowChrome.ResizeGripDirection="BottomRight" />

ResizeGrip是当ResizeMode = ResizeMode.CanResizeWithGrip;并且WindowState = Normal时时出现的Window右下角的大小调整手柄,外观为组成三角形的一些点。除了让可以操作的区域变大一些,还可以用来提示Window是可以调整大小的。

7. 处理Triggers

虽然我平时喜欢用VisualState的方式实现模板化控件UI再状态之间的转变,但有时还是Trigger方便快捷,尤其是不需要做动画的时候。自定义Window有以下几组需要处理的Trigger:

7.1 IsNonClientActive

<Trigger Property="IsNonClientActive"
Value="False">
<Setter Property="BorderBrush"
Value="#FF6F7785" />
</Trigger>

这个属性是我自定义的,用于代替IsActive,在它为False的时候边框和标题栏变成灰色。

7.2 ResizeGrip

<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="ResizeMode"
Value="CanResizeWithGrip" />
<Condition Property="WindowState"
Value="Normal" />
</MultiTrigger.Conditions>
<Setter TargetName="ResizeGrip"
Property="Visibility"
Value="Visible" />
</MultiTrigger>

上面这段XAML控制ResizeGrip是否显示。

7.3 Buttons

<Trigger Property="WindowState"
Value="Normal">
<Setter TargetName="MaximizeButton"
Property="Visibility"
Value="Visible" />
<Setter TargetName="RestoreButton"
Property="Visibility"
Value="Collapsed" />
</Trigger>
<Trigger Property="ResizeMode"
Value="NoResize">
<Setter TargetName="MinimizeButton"
Property="Visibility"
Value="Collapsed" />
<Setter TargetName="MaximizeButton"
Property="Visibility"
Value="Collapsed" />
<Setter TargetName="RestoreButton"
Property="Visibility"
Value="Collapsed" />
</Trigger>

这两个Trigger控制最小化、最大化和还原按钮的状态。最大化、还原两个按钮的IsEnabled状态由绑定的SystemCommand控制。

7.4 Maximized

<Trigger Property="WindowState"
Value="Maximized">
<Setter TargetName="MaximizeButton"
Property="Visibility"
Value="Collapsed" />
<Setter TargetName="RestoreButton"
Property="Visibility"
Value="Visible" />
<Setter TargetName="WindowBorder"
Property="BorderThickness"
Value="0" />
<Setter TargetName="WindowBorder"
Property="Padding"
Value="{x:Static SystemParameters.WindowResizeBorderThickness}" />
<Setter Property="Margin"
TargetName="LayoutRoot"
Value="{x:Static local:WindowParameters.PaddedBorderThickness}" />
</Trigger>

Maximized状态下最大化按钮隐藏,还原按钮出现。并且Window的Margin需要调整,具体留到下一篇文章再说吧。

8. DragMove

有些人喜欢不止标题栏,按住Window的任何空白部分都可以拖动Window,只需要在代码中添加DragMove即可:

protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
if (e.ButtonState == MouseButtonState.Pressed)
DragMove();
}

但这样做不喜欢DragMove功能的人又会有意见,再添加一个属性来开关这个功能又很麻烦,索性就把它做成WindowService.IsDragMoveEnabled附加属性,在DefaultStyle中设置了。

9. 结语

使用WindowChrome自定义Window的基本功能就介绍到这里了,但其实WindowChrome有很多缺陷,下一篇文章将介绍这些陷阱及讲解如何回避(或者为什么不/不能回避)。

ExtendedWindow的做法是尽量成为一个更通用的基类,样式和其它附加属性中的行为和ExtendedWindow的类本身没有必然关联(目前位置只添加了FunctionBar依赖属性)。这样做的好处是为代码和样式解耦,而且一旦为控件添加了属性,以后再想不支持就很难了,反正XAML的自由度很高,都交给XAML去扩展就好了。

我以前也写过一篇文章使用WindowChrome自定义Window Style简单介绍过自定义Window样式的方案,当时的方案有不少问题,这次算是填上以前的坑。

10. 参考

WindowChrome Class (System.Windows.Shell) Microsoft Docs

WPF Windows 概述 _ Microsoft Docs

对话框概述 _ Microsoft Docs

SystemParameters Class (System.Windows) Microsoft Docs

WPF 使用 WindowChrome,在自定义窗口标题栏的同时最大程度保留原生窗口样式(类似 UWP_Chrome) - walterlv

11. 源码

Kino.Toolkit.Wpf_Window at master

[WPF自定义控件]使用WindowChrome自定义Window Style的更多相关文章

  1. [WPF自定义控件]?使用WindowChrome自定义Window Style

    原文:[WPF自定义控件]?使用WindowChrome自定义Window Style 1. 为什么要自定义Window 对稍微有点规模的桌面软件来说自定义的Window几乎是标配了,一来设计师总是克 ...

  2. [WPF]使用WindowChrome自定义Window Style

    1. 前言 做了WPF开发多年,一直未曾自己实现一个自定义Window Style,无论是<WPF编程宝典>或是各种博客都建议使用WindowStyle="None" ...

  3. [WPF 自定义控件]使用WindowChrome自定义RibbonWindow

    1. 为什么要自定义RibbonWindow 自定义Window有可能是设计或功能上的要求,可以是非必要的,而自定义RibbonWindow则不一样: 如果程序使用了自定义样式的Window,为了统一 ...

  4. WPF中使用WindowChrome自定义窗口中遇到的最大化问题

    FrameWork 4.5 之后,内置了WindowChrome类,官方文档: https://msdn.microsoft.com/en-us/library/system.windows.shel ...

  5. WPF自定义Window样式(1)

    1. 引言 WPF是制作界面的一大利器.最近在做一个项目,用的就是WPF.既然使用了WPF了,那么理所当然的,需要自定义窗体样式.所使用的代码是在网上查到的,遗憾的是,整理完毕后,再找那篇帖子却怎么也 ...

  6. [WPF自定义控件库]使用WindowChrome自定义RibbonWindow

    原文:[WPF自定义控件库]使用WindowChrome自定义RibbonWindow 1. 为什么要自定义RibbonWindow 自定义Window有可能是设计或功能上的要求,可以是非必要的,而自 ...

  7. WPF自定义Window样式(2)

    1. 引言 在上一篇中,介绍了如何建立自定义窗体.接下来,我们需要考虑将该自定义窗体基类放到类库中去,只有放到类库中,我们才能在其他地方去方便的引用该基类. 2. 创建类库 接上一篇的项目,先添加一个 ...

  8. [WPF自定义控件库]为Form和自定义Window添加FunctionBar

    1. 前言 我常常看到同一个应用程序中的表单的按钮----也就是"确定"."取消"那两个按钮----实现得千奇百怪,其实只要使用统一的Style起码就可以统一按 ...

  9. WPF.UIShell UIFramework之自定义窗口的深度技术 - 模态闪动(Blink)、窗口四边拖拽支持(WmNCHitTest)、自定义最大化位置和大小(WmGetMinMaxInfo)

    无论是在工作和学习中使用WPF时,我们通常都会接触到CustomControl,今天我们就CustomWindow之后的一些边角技术进行探讨和剖析. 窗口(对话框)模态闪动(Blink) 自定义窗口的 ...

随机推荐

  1. AtCoder Beginner Contest 057 ABCD题

    A - Remaining Time Time limit : 2sec / Memory limit : 256MB Score : 100 points Problem Statement Dol ...

  2. Linux、Windows 下分割、合并rar文件

    1.分割rar 1.1 linux下分割压缩rar 安装rar和unrar和序 $sudo aptitude install rar unrar 示例,分割压缩temp文件,每个包为1MB $rar ...

  3. popoverController使用注意--转

    一.设置尺寸 提示:不建议,像下面这样吧popover的宽度和高度写死. 1 //1.新建一个内容控制器 2 YYMenuViewController *menuVc=[[YYMenuViewCont ...

  4. P2614 计算器弹琴

    题目描述 总所周知,计算器可以拿来干很多它本不应该干的事情,比如写作文.(参看洛谷P2549) 小A发现了一个计算器的另一个隐藏功能——弹琴. http://www.bilibili.com/vide ...

  5. C# winform 创建快捷方式

    using System;using IWshRuntimeLibrary;using System.IO; namespace UavSystem.Common{    public class S ...

  6. C#中构造函数和析构函数区别

    把对象的初始化工作放在构造函数中,把清除工作放在析构函数中.当对象被创建时,构造函数被自动执行.当对象消亡时,析构函数被自动执行.这样就不用担心忘记对象的初始化和清除工作. 析构函数是由垃圾回收器控制 ...

  7. Java并发——ThreadPoolExecutor线程池解析及Executor创建线程常见四种方式

    前言: 在刚学Java并发的时候基本上第一个demo都会写new Thread来创建线程.但是随着学的深入之后发现基本上都是使用线程池来直接获取线程.那么为什么会有这样的情况发生呢? new Thre ...

  8. re正则表达式2

    1.“字符*” 匹配*前面的字符0次或者多次. 注意:是匹配*前一个字符,只能是*前一个字符多次打印出来.*前面其他的字符相当于前缀会打印出来,但是不会再匹配. *前一个字符前面的其他字符里的首字符先 ...

  9. PHP Deprecated: Function split() is deprecated in /var/www/html/cacti/cmd.php on line 61

    [root@localhost cacti]# php cmd.php PHP Deprecated: Function split() is deprecated in /var/www/html/ ...

  10. std::map插入已存在的key时,key对应的内容不会被更新

    std::map插入已存在的key时,key对应的内容不会被更新,如果不知道这一点,可能会造成运行结果与预期的不一致 “Because element keys in a map are unique ...