在我们设计一个软件的时候,有很多时候我们需要按照美工的设计来重新设计整个版面,这当然包括主窗体,因为WPF为我们提供了强大的模板的特性,这就为我们自定义各种空间提供了可能性,这篇博客主要用来介绍如何自定义自己的Window,在介绍整个写作思路之前,我们来看看最终的效果。
图一 自定义窗体主界面
这里面的核心就是重写Window的Template,针对整个开发过程中出现的问题我们再来一步步去剖析,首先要看看我们定义好的样式
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:ui="clr-namespace:CustomWPFWindow.Controls" xmlns:local="clr-namespace:CustomWPFWindow.Themes"> <Style TargetType="{x:Type Window}" x:Key="ShellWindow"> <Setter Property="Background" Value="#2B5A97"></Setter> <Setter Property="WindowStyle" Value="None"></Setter> <Setter Property="AllowsTransparency" Value="False"></Setter> <Setter Property="Template" > <Setter.Value> <ControlTemplate TargetType="Window"> <Border BorderBrush="#333" BorderThickness="1" Background="#eee"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="*"></RowDefinition> </Grid.RowDefinitions> <ui:WindowTopArea Background="#2B579A"> <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> <StackPanel Orientation="Horizontal" Margin="5" HorizontalAlignment="Left" VerticalAlignment="Center"> <Image Source="/CustomWPFWindow;component/Resources/Images/application.png"></Image> <TextBlock Text="标题" Foreground="White" Margin="5 2" HorizontalAlignment="Center" VerticalAlignment="Center"></TextBlock> </StackPanel> <StackPanel Orientation="Horizontal" HorizontalAlignment="Right"> <ui:WindowButtonMin ToolTip="最小化"> <Image Source="/CustomWPFWindow;component/Resources/Images/min.png" Width="16" Height="16" HorizontalAlignment="Center" VerticalAlignment="Center" RenderOptions.BitmapScalingMode="NearestNeighbor"></Image> </ui:WindowButtonMin> <ui:WindowButtonMax x:Name="max" ToolTip="最大化"> <Image Source="/CustomWPFWindow;component/Resources/Images/max.png" Width="16" Height="16" HorizontalAlignment="Center" VerticalAlignment="Center" RenderOptions.BitmapScalingMode="NearestNeighbor"></Image> </ui:WindowButtonMax> <ui:WindowButtonNormal x:Name="normal" ToolTip="向下还原"> <Image Source="/CustomWPFWindow;component/Resources/Images/normal.png" Width="16" Height="16" HorizontalAlignment="Center" VerticalAlignment="Center" RenderOptions.BitmapScalingMode="NearestNeighbor"></Image> </ui:WindowButtonNormal> <ui:WindowButtonClose x:Name="windowclose" ToolTip="关闭"> <Image Source="/CustomWPFWindow;component/Resources/Images/close.png" Width="16" Height="16" HorizontalAlignment="Center" VerticalAlignment="Center" RenderOptions.BitmapScalingMode="NearestNeighbor"> </Image> </ui:WindowButtonClose> </StackPanel> </Grid> </ui:WindowTopArea> <AdornerDecorator Grid.Row="1"> <ContentPresenter></ContentPresenter> </AdornerDecorator> </Grid> </Border> <ControlTemplate.Triggers> <Trigger Property="WindowState" Value="Maximized"> <Setter Property="Visibility" Value="visible" TargetName="normal"></Setter> <Setter Property="Visibility" Value="collapsed" TargetName="max"></Setter> </Trigger> <Trigger Property="WindowState" Value="Normal"> <Setter Property="Visibility" Value="collapsed" TargetName="normal"></Setter> <Setter Property="Visibility" Value="visible" TargetName="max"></Setter> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style> </ResourceDictionary>
这里面要设置几个最核心的属性,第一个就是WindowStyle要设置成None,否则就无法进行自定义的设置按键功能区,另外一个就是是否设置AllowsTransparency属性的问题,我们分别看一下设置和不设置的效果。
1 设置属性为false时:
图二 设置AllowsTransparency=“False”时主窗体样式
我们会发现此时在窗体的正上方出现一块白色的矩形区域,这块区域无论你怎么重写Window的样式,它都是一直存在的,但是此时整个窗体都是允许你进行拉伸的操作,但是此时我们会发现这样不是真正地无边框的Window的样式,整个窗体上面的白色区域都是存在的,查阅相关的资料的时候,我们会发现这一块是整个窗体上面部分的拉伸的区域,是无法通过重写模板去去掉它的,那么该怎样真正地去实现无边框的样式呢?
2 设置属性为true时:
当我们更改窗体的样式,设置窗体可以允许为透明窗体的时候,我们是否可以实现上面的效果呢?当我们设置这个属性为true的时候,我们发现能实现图一所示的效果,但是整个窗体都不能够进行拉伸了,那么这个怎么办呢?其实这个也可以理解,当我们设置Window窗体允许透明的时候,所有和窗体相关的事件都会消失了,这个是必然的,那么我们就不得不去重写所有的这些事件,下面我们将贴出整个窗体重写后的核心代码,并就里面的核心部分来进行深入的分析。
#region 窗体大小变化 public void SetWindowResizer() { Window win = Window.GetWindow(this); ResizePosition ResPosition = ResizePosition.None; int Resizer = 5; win.MouseMove += new MouseEventHandler( delegate (object target, MouseEventArgs args) { try { //do resize if (win.WindowState == WindowState.Normal) { Point MS = args.GetPosition(win); if (args.LeftButton == MouseButtonState.Pressed) { Win32.POINT pos = new Win32.POINT(); Win32.GetCursorPos(out pos); #region 改变窗体大小 switch (ResPosition) { case ResizePosition.Left: //左边 Mouse.SetCursor(Cursors.SizeWE); Point transPointLeft = win.PointToScreen(new Point(0, 0)); win.Left += pos.X - transPointLeft.X; win.Width += transPointLeft.X - pos.X; break; case ResizePosition.Right: //右边 Mouse.SetCursor(Cursors.SizeWE); Point transPointRight = win.PointToScreen(new Point(win.Width, 0)); win.Width += pos.X - transPointRight.X; break; case ResizePosition.Top: //顶部 Mouse.SetCursor(Cursors.SizeNS); Point transPointTop = win.PointToScreen(new Point(0, 0)); win.Top += pos.Y - transPointTop.Y; win.Height += transPointTop.Y - pos.Y; break; case ResizePosition.Bottom: //底部 Mouse.SetCursor(Cursors.SizeNS); Point transPointBottom = win.PointToScreen(new Point(0, win.Height)); win.Height += (pos.Y - transPointBottom.Y); break; case ResizePosition.TopLeft: //左上 Mouse.SetCursor(Cursors.SizeNWSE); Point transPointTopLeft = win.PointToScreen(new Point(0, 0)); win.Left += pos.X - transPointTopLeft.X; win.Top += pos.Y - transPointTopLeft.Y; win.Width += transPointTopLeft.X - pos.X; win.Height += transPointTopLeft.Y - pos.Y; break; case ResizePosition.BottomLeft: //左下 Mouse.SetCursor(Cursors.SizeNESW); Point transPointBottomLeft = win.PointToScreen(new Point(0, win.Height)); win.Left += pos.X - transPointBottomLeft.X; win.Width += transPointBottomLeft.X - pos.X; win.Height += pos.Y - transPointBottomLeft.Y; break; case ResizePosition.TopRight: //右上 Mouse.SetCursor(Cursors.SizeNESW); Point transPointTopRight = win.PointToScreen(new Point(win.Width, 0)); win.Top += pos.Y - transPointTopRight.Y; win.Width = transPointTopRight.Y - pos.X; win.Height = transPointTopRight.Y - pos.Y; break; case ResizePosition.BottomRight: //右下 Mouse.SetCursor(Cursors.SizeNWSE); Point transPointBottomRight = win.PointToScreen(new Point(win.Width, win.Height)); win.Width += pos.X - transPointBottomRight.X; win.Height += pos.Y - transPointBottomRight.Y; break; case ResizePosition.None: default: Mouse.SetCursor(Cursors.Arrow); break; } #endregion } else if (MS.X <= Resizer + 5 && MS.Y <= Resizer + 5) { //左上 (不执行) Mouse.SetCursor(Cursors.SizeNWSE); ResPosition = ResizePosition.TopLeft; } else if (MS.X <= Resizer && MS.Y >= win.ActualHeight - Resizer) { //左下 Mouse.SetCursor(Cursors.SizeNESW); ResPosition = ResizePosition.BottomLeft; } else if (MS.X >= win.ActualWidth - Resizer - 5 && MS.Y <= Resizer + 5) { //右上(不执行) Mouse.SetCursor(Cursors.SizeNESW); ResPosition = ResizePosition.TopRight; } else if (MS.X >= win.ActualWidth - Resizer && MS.Y >= win.ActualHeight - Resizer) { //右下 Mouse.SetCursor(Cursors.SizeNWSE); ResPosition = ResizePosition.BottomRight; } else if (MS.X <= Resizer) { //左边 Mouse.SetCursor(Cursors.SizeWE); ResPosition = ResizePosition.Left; } else if (MS.Y <= Resizer + 5) { //顶部(不执行) Mouse.SetCursor(Cursors.SizeNS); ResPosition = ResizePosition.Top; } else if (MS.X >= win.ActualWidth - Resizer) { //右边 Mouse.SetCursor(Cursors.SizeWE); ResPosition = ResizePosition.Right; } else if (MS.Y >= win.ActualHeight - Resizer) { //底部 Mouse.SetCursor(Cursors.SizeNS); ResPosition = ResizePosition.Bottom; } else { //无 Mouse.SetCursor(Cursors.Arrow); ResPosition = ResizePosition.None; } } } catch { ResPosition = ResizePosition.None; win.ReleaseMouseCapture(); } args.Handled = CaptureMouse; } ); win.MouseLeftButtonDown += new MouseButtonEventHandler( delegate (object target, MouseButtonEventArgs args) { if (win.WindowState == WindowState.Normal) { //获取当前鼠标点击点相对于Dvap.Shell窗体的位置 Point pos = args.GetPosition(win); if (ResPosition != ResizePosition.None) { CaptureMouse = win.CaptureMouse(); } args.Handled = CaptureMouse; } } ); win.MouseLeftButtonUp += new MouseButtonEventHandler( delegate (object target, MouseButtonEventArgs args) { if (win.WindowState == WindowState.Normal) { ResPosition = ResizePosition.None; if (CaptureMouse) { win.ReleaseMouseCapture(); CaptureMouse = false; } args.Handled = CaptureMouse; } } ); } #endregion
这段代码还是很容易理解的,就是为整个窗体添加MouseLeftButtonDown、MouseMove、MouseLeftButtonUp这个事件,这里需要特别注意的就是,我们获取屏幕的坐标的方式是调用Win32的API GetCursorPos来获取当前的屏幕坐标的位置,但是我们在操作的时候获取到的是主窗体的坐标位置,这两个位置该如何进行转化呢?这个是核心,上面的代码是经过反复进行验证后的代码,就具体的拉伸过程我们再来做进一步的分析。
2.1 首先我们要设置一个进行拉伸的区域,这里我们设置Resizer为5个像素,这个距离内作为窗体拉伸的识别区域。
2.2 首先在MouseLeftButtonDown事件中我们需要窗体能够捕获到鼠标的位置,这里我们使用win.CaptureMouse()方法来捕获鼠标的输入。
2.3 最重要的部分都是在MouseMove事件中完成的,首先我们需要通过Point MS = args.GetPosition(win)来获取到当前鼠标相对于主窗体win的位置,记住这个获取到的位置是相对于主窗体的而不是相对于屏幕的坐标位置的。然后我们判断当前鼠标左键是否按下来将整个过程分为两个部分,按下的话进行窗体的拉伸操作,没有按下的话进行窗体的初始化状态操作,通过获取到的MS的坐标位置来初始化操作对象,并且设置当前鼠标的光标的样式,这个是非常重要的一个过程的,只有完成了这个过程才能进行下面的操作。
2.4 当鼠标左键按下时,我们将会看到通过3步骤进行初始化的状态我们来改变窗体的大小以及位置信息,在这一步骤中需要特别注意的是,注意坐标系的转换,比如向右拉伸的时候,我们首先获取到的是通过GetCursorPos来获取到的相对于屏幕的位置信息,那么当我们将窗体向右拉伸时,窗体移动的距离应该是窗体的Width对应的点转化为屏幕坐标点后再与通过GetCursorPos来获取到的相对于屏幕的位置信息做差运算的结果,切不可直接将屏幕坐标位置减去窗体当前的位置,因为窗体获取到的Width以及其它位置信息和屏幕的坐标系是不统一的,无法直接做差运算,这里读者也可以改动代码进行尝试,所以这里就有了PointToScreen和PointFromScreen这两个坐标转换函数的用武之地了,比如我们拉伸窗体的右侧距离时,我们通过 Point transPointRight = win.PointToScreen(new Point(win.Width, 0))这句代码将当前主窗体最右侧的位置首先转成相对于屏幕坐标系的位置,然后用两个相同坐标系的两个坐标位置进行做差运算,从而改变窗体的Left、Top、Width、Height属性,这个是需要我们去一点点分析的,后面的每一个过程都与此类似,这个过程是整个窗体拉伸变换的关键。
2.5 最后一个需要注意的地方就是窗体拉伸的时候,需要考虑窗体是否设置了MinWidt、MinHeight这些属性,当超过这些属性的时候窗体是无法进行拉伸操作的。
3 通过Win32的API函数设置窗体的无边框透明属性。
在进一步分析代码时我们发现可以通过设置SetWindowLong来设置窗体的属性,这个函数可以在设置AllowsTransparency=“False”的状态下仍然改变窗体的无边框样式,下面贴出具体代码,具体每个参数的含义需要参考具体的文档。
private void SetWindowNoBorder() { Window win = Window.GetWindow(this); // 获取窗体句柄 IntPtr hwnd = new System.Windows.Interop.WindowInteropHelper(win).Handle; // 获得窗体的 样式 long oldstyle = Win32.GetWindowLong(hwnd, Win32.GWL_STYLE); // 更改窗体的样式为无边框窗体 Win32.SetWindowLong(hwnd, Win32.GWL_STYLE, (int)(oldstyle & ~Win32.WS_CAPTION)); }
4 最后需要提及的是一个鼠标移入关闭按钮时的一个动画状态,通过这段代码我们可以学习一下该如何在Trigger中设置动画属性。
<Style TargetType="{x:Type controls:WindowButtonClose}"> <Setter Property="Margin" Value="0 0 1 0"></Setter> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type controls:WindowButtonClose}"> <Border x:Name="bg" Background="Transparent" Width="32" Height="32"> <ContentPresenter x:Name="content" HorizontalAlignment="Center" VerticalAlignment="Center" Opacity="0.5" RenderTransformOrigin="0.5 0.5"> <ContentPresenter.RenderTransform> <RotateTransform x:Name="angleRotateTransform" ></RotateTransform> </ContentPresenter.RenderTransform> </ContentPresenter> </Border> <ControlTemplate.Triggers> <Trigger Property="IsMouseOver" Value="true"> <Setter Property="Background" Value="Red" TargetName="bg"></Setter> <Setter Property="Opacity" Value="1" TargetName="content"></Setter> <Trigger.EnterActions> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboard.TargetName="angleRotateTransform" Storyboard.TargetProperty="Angle" From="0" To="90" Duration="0:0:0.5"> <DoubleAnimation.EasingFunction> <BackEase EasingMode="EaseInOut"></BackEase> </DoubleAnimation.EasingFunction> </DoubleAnimation> </Storyboard> </BeginStoryboard> </Trigger.EnterActions> <Trigger.ExitActions> <BeginStoryboard> <Storyboard> <DoubleAnimation Storyboard.TargetName="angleRotateTransform" Storyboard.TargetProperty="Angle" From="90" To="0" Duration="0:0:0.5"> <DoubleAnimation.EasingFunction> <BackEase EasingMode="EaseInOut"></BackEase> </DoubleAnimation.EasingFunction> </DoubleAnimation> </Storyboard> </BeginStoryboard> </Trigger.ExitActions> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style>
最后,贴出整个工程的源码,请点击此处进行下载,文中有表述不当的地方请批评指正,谢谢!