WPF自定义界面WindowChrome


WPF自定义界面WindowChrome

默认WPF的界面其实也还行,就是满足不了日渐增长的需求,界面还是需要有更高的自定义程度,包括标题栏也要能够塞下更多的操作控件。

默认窗口介绍

新建WPF项目,给里面内容设置一点颜色:

image-20211223154246966

默认创建的界面(Win10上的效果),能够看到两块区域,一块是以颜色#0078D4内容区,一块是顶部白色的非内容区,按照官方的说法它们依次是客户区非客户区

客户区就是我们的主体内容,目前里面有一排文字,这没什么好说的, 你想放什么内容,就往这里放就行了。

非客户区里面的东西稍微有一点多,一点一点来列吧:

左上角的应用图标标题

image-20211223155013699

右上角的最小化最大化关闭按钮:

image-20211223155128648

还有一个就是我们窗体的边框线,黑色的那条。

以上都是我们能够看到的,还有一些看不见的窗口行为

缩放标题栏的拖动以及鼠标拖到窗口到桌面边缘位置,可以对窗口进行一个大小的调整:

动画

点击应用图标之后弹出的窗口操作:

image-20211223160124239

以及在窗口拖动和在拖动的过程中中,窗口会自动扩充到对应的屏幕:

动画1

其实呢, 还有一些行为,这里就不做叙述了,下面我们来看看如何自定义

自定义界面方式

WPF中自定义的界面的方式可以分为两种,一种是使用 AllowsTransparency="True"WindowStyle="None",这种呢就相当于直接把原生非客户区给干掉了,然后我们在内容区域自己去实现非客户区,就会导致窗口自定的行为如:缩放拖动停靠边界放大。。。这些功能全都没有了,如果需要的话,是需要自己手动代码添加的。

第二种呢就是使用我们的WindowChrome来自定义界面, 这种方式保留了一个窗口基本的行为,只需要我们重新规划一下客户区非客户区就行了。

两种方式都不复杂,不过界面客户区非客户区都是需要自己去定义的,但是WindowChrome的方式保留了窗口的一些基本行为,显示性能也会比第一种强。

至于二者窗口性能的对比,可以看下这个大佬的文章:
(walterlv 吕毅)WPF 制作高性能的透明背景异形窗口(使用 WindowChrome 而不要使用 AllowsTransparency=True)

使用WindowChrome

两种方式自定义界面没什么太大区别, 这里就以WindowChrome来进行举例。

使用WindowChrome的方式也很简单,直接声明一下就行了:

    
        
    
    
        
    

得到如下的界面:

image-20211223170623630

这个界面拥有默认窗口的所有窗口行为,就是没有应用图标、标题、操作按钮(最大化、最小化、关闭),其实不是没有,只是被当前Window的默认样式给遮挡了

只需要去除当前Window的样式,就可以看到WindowChrome的样式:

    
        
            
        
    

image-20211227154025409

作为对比看一下默认的Window是什么样子的:

image-20211227154216344

可以看到,仅仅在关闭的地方,WindowChrome的样式就和默认的Window不一致,操作栏都无法靠边界,而且尝试修改这些样式也不好调整到我们想要的样子,所以我们直接抛弃原来的样式,重写WindowChrome,自己定义客户区非客户区,如果对WindowChrome的样式调整有兴趣,可以看一下:

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

除了需要这些东西以外, 还需要添加我们自定义的操作按钮,下面我们就要重写一下窗口的样式,自定义里面的客户区非客户区

自定义WindowChrome说明

在重写样式之前呢,先来介绍几个WindowChrome的几个属性:

CaptionHeight:标题栏的操作高度,一般跟随非客户区高度,设置为0表示界面无法响应鼠标的任何操作(拖动窗口, 双击标题栏的最大化最小化。。。 仅仅是行为区域,并不影响外观)。

GlassFrameThickness:用来设置距离区域的厚度,设置为-1,可以使得区域覆盖整个界面

ResizeBorderThickness:可以设置窗口缩放的边框厚度,不便于设置过大。下图是设置为50的效果:

image-20211227164038836

可以看到侵入内容区这么多,依然可以进行窗口缩放。

还有一个很重要的属性,不是给WindowChrome设置的,是给界面中的元素设置的,如果你的元素在CaptionHeight的高度内,是无法响应鼠标事件的,此时需要给元素设置属性WindowChrome.IsHitTestVisibleInChrome="True",很常见的问题,在自定义标题按钮的时候,会发现自己点击不了标题按钮!

自定义WindowChrome

既然是要自定义客户区非客户区,那么我们稍微设计一下(下面示例所有属性均写死,自行更改)

根元素使用Border,我们设置它的边框和颜色,内部使用Grid元素,进行上下两行分割,第一行客户区, 第二行非客户区,第一行的高度改为Auto,第二行默认使用剩下全部区域。

为了撑开非客户区,这里绑定了一个窗口的标题。

大概代码如下


        
            
                
                    
                    
                        
                        
                    
                    
                    
                        
                    

                    
                    
                        
                    
                
            
        

呈现的效果如下(用装饰器AdornerDecorator包装了一下客户区,为了后期方便添加遮罩层,有文章说明,目前没写,后续弄好了链接再贴过来吧):

image-20211227171937366

客户区没啥说的,你自己想怎么写就怎么写, 主要需要说一下非客户区的图标、标题(已经有了)、操作按钮、自定义按钮,直接贴一下代码,样式这么就不加了,采用自己的样式,主要说明下使用系统的几个命令。

                    
                    
                        
                        
                            
                            
                        
                        
                        
                            
                            
                            
                            
                            
                        
                    

样式出完的效果是这样的:

image-20211228100417631

这里记得需要给按钮的父组件添加WindowChrome.IsHitTestVisibleInChrome="True",也可以给按钮本身添加,看自己的需要,不然会导致按钮无法点击。

虽然添加了属性,但是发现图中依然有按钮是置灰的,那是因为我们给按钮绑定了系统的命令,但是了这个命令并没有初始化,所以需要到后台,进行命令的初始化

        public MainWindow()
        {
            InitializeComponent();

            CommandBindings.Add(new CommandBinding(SystemCommands.CloseWindowCommand, (_, __) => { SystemCommands.CloseWindow(this); }));
            CommandBindings.Add(new CommandBinding(SystemCommands.MinimizeWindowCommand, (_, __) => { SystemCommands.MinimizeWindow(this); }));
            CommandBindings.Add(new CommandBinding(SystemCommands.MaximizeWindowCommand, (_, __) => { SystemCommands.MaximizeWindow(this); }));
            CommandBindings.Add(new CommandBinding(SystemCommands.RestoreWindowCommand, (_, __) => { SystemCommands.RestoreWindow(this); }));
            CommandBindings.Add(new CommandBinding(SystemCommands.ShowSystemMenuCommand, ShowSystemMenu));

        }

        private void ShowSystemMenu(object sender, ExecutedRoutedEventArgs e)
        {
            var element = e.OriginalSource as FrameworkElement;
            if (element == null)
                return;

            var position = WindowState == WindowState.Maximized ? new Point(0, element.ActualHeight)
                : new Point(Left + BorderThickness.Left, element.ActualHeight + Top + BorderThickness.Top);
            position = element.TransformToAncestor(this).Transform(position);
            SystemCommands.ShowSystemMenu(this, position);
        }

现在窗口是正常了,但是放大之后并没有更换放大区域的图标,可以看到,我们摆放按钮的时候, 有一个按钮被隐藏了, 这就是我们的还原界面按钮,所以需要添加一些触发器,让界面在放大之后,可以更换一下图标,并且我们在放大界面的时候, 明显能够感觉到标题栏超出了屏幕外围(窗口最大化的时候设置BorderThickness=8解决此问题):

image-20211228104516111

上面说的这些都可以放在样式和样式的触发器中去实现,具体的步骤就不说明了,直接贴一下完整的代码,供学习参考(因为很多东西都写死了,所以根据自己的需要,自行进行动态更改)

完整代码的效果:

image-20211228113237884

完整代码下载

最后说一句

AllowsTransparency="True"的这种方式依然是适用的,重在开发快速高效,有时候公司的项目,并不需要过多的默认窗口行为,还有就是有些老哥就是喜欢自己添加这些窗口行为,任何一种方式都有有利有弊,性能之战也无终章。

在满足工作要求的时候,我们也要有更高的追求!

没有银弹!

WPF