WPF一步步實現完全無邊框自定義Window(附源碼)
在我們設計一個軟件的時候,有很多時候我們需要按照美工的設計來重新設計整個版面,這當然包括主窗體,因為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>
最後,貼出整個工程的源碼,請點擊此處進行下載,文中有表述不當的地方請批評指正,謝謝!
WPF一步步實現完全無邊框自定義Window(附源碼)