[WPF] 誰說視窗長相一定要一成不變? - 實作自定外觀的視窗

  • 26893
  • 0
  • WPF
  • 2013-07-15

雖然說自Windows Vista採用了AERO之後,Windows作業系統中應用程式預設的視窗就美觀了不少,但是對於喜歡應用程式有整體美的設計師來說,使用預設的視窗外觀對某些WPF應用程式來說,或許還是有可能造成破壞整體設計感的情況發生。所以,把預設的視窗外觀拔掉,讓設計師們更能展現設計的才能,針對不同的應用程式設計出不同的視窗外觀,想像起來是不是蠻美好的一件事呢?

 

雖然說自Windows Vista採用了AERO之後,Windows作業系統中應用程式預設的視窗就美觀了不少,但是對於喜歡應用程式有整體美的設計師來說,使用預設的視窗外觀對某些WPF應用程式來說,或許還是有可能造成破壞整體設計感的情況發生。所以,把預設的視窗外觀拔掉,讓設計師們更能展現設計的才能,針對不同的應用程式設計出不同的視窗外觀,想像起來是不是蠻美好的一件事呢?

以下就來跟各位分享使用WPF實作出自定外觀視窗的方法。這次的目標是做出如下的不規則外框視窗:

image

 

為了方便練習,請先使用Expression Blend建立一個標準的WPF應用程式,接著針對MainWindow.xaml做以下的調整:

  • 勾選AllowsTransparence
  • WindowStyle改為None
  • ResizeMode改為NoResize

image

做完上述三個步驟的話,我們會得到一個透明背景而且沒有外框的視窗。

 

接著就可以來打造我們自己要的視窗長相啦! 一般的視窗的Title區是可以供使用者拖拉進行視窗搬移的區域,還有用來縮小、放大、關閉視窗的按鈕也會放在這邊(當然,也可以發揮創意把按鈕放在別的地方)。

在這邊就順便分享一個簡單就可以做出透明自訂視窗的Title的偷吃步,有興趣又很懶的朋友們可以跟我一樣將MainWindow.xaml的LayoutRoot改為如下:

MainWindow.xaml

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
		xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
		xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
		xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
		xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
		xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
		xmlns:Microsoft_Windows_Themes="clr-namespace:Microsoft.Windows.Themes;assembly=PresentationFramework.Aero"
		mc:Ignorable="d" x:Class="Wpf_CustomWindow.MainWindow" x:Name="Window" Title="MainWindow" Width="640"
		Height="480" WindowStyle="None" Background="{x:Null}" WindowStartupLocation="CenterScreen" ResizeMode="NoResize"
		AllowsTransparency="True" MinWidth="400" MinHeight="300">
	<Grid x:Name="LayoutRoot">
		<Grid.RowDefinitions>
			<!-- 第一個Row使用固定的高度,因為Title列通常不需要隨著視窗大小縮放 -->
			<RowDefinition Height="40" />
			<!-- 第二個Row用來放置視窗的內容,就把剩餘的空間都給它吧 -->
			<RowDefinition Height="*" />
		</Grid.RowDefinitions>
		<!-- 透過設定Border的邊框,就可以做出自訂的TitleBar的感覺啦!! 另外,透過MouseLeftButtonDown的EventHandler去處理視窗的拖拉移動功能 -->
		<Border x:Name="bdrWindowTitle" BorderBrush="Black" BorderThickness="2,2,2,0" CornerRadius="10,10,0,0"
				Margin="0,10,0,0" Background="White" MouseLeftButtonDown="bdrWindowTitle_MouseLeftButtonDown">
			<Grid x:Name="grdTitleContent" Margin="10,10,10,0">
				<StackPanel x:Name="stkTitle" Orientation="Horizontal" d:LayoutOverrides="Height" Margin="0,-20,0,0"
						HorizontalAlignment="Left">
					<TextBlock x:Name="textBlock" TextWrapping="Wrap" Text="W" VerticalAlignment="Bottom" FontSize="48"
							FontWeight="Bold" FontFamily="Arial Black" d:LayoutOverrides="Width" Margin="0,-20,0,-5"
							Foreground="White">
						<TextBlock.Effect>
							<DropShadowEffect ShadowDepth="3" BlurRadius="2" Opacity="0.5" />
						</TextBlock.Effect></TextBlock>
					<TextBlock TextWrapping="Wrap" Text="indowTitle" VerticalAlignment="Bottom" FontSize="26.667"
							FontWeight="Bold" FontFamily="Arial Black">
						<TextBlock.Effect>
							<DropShadowEffect ShadowDepth="3" BlurRadius="2" Opacity="0.5" />
						</TextBlock.Effect></TextBlock>
				</StackPanel>
				<StackPanel x:Name="stkButtonBar" HorizontalAlignment="Right" Orientation="Horizontal" Margin="0,-5,0,0">
					<!-- 最小化按鈕 -->
					<Button x:Name="btnMinimize" Content="-" Margin="10,0,0,0" Width="28" Click="btnMinimize_Click" />
					<!-- 最大化按鈕 -->
					<Button x:Name="btnMaximize" Content="+" Margin="10,0,0,0" Width="28" Click="btnMaximize_Click" />
					<!-- 關閉視窗鈕 -->
					<Button x:Name="btnClose" Content="X" Margin="10,0,0,0" Width="28" Click="btnClose_Click" />
				</StackPanel>
			</Grid>
		</Border>
		<Border x:Name="bdrWindowContent" BorderBrush="Black" BorderThickness="2,0,2,2" Grid.Row="1"
				CornerRadius="0,0,10,10" Background="White">
		</Border>
	</Grid>
</Window>

接著來看看CodeBehind的部份:

MainWindow.xaml.cs

using System;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;

namespace Wpf_CustomWindow
{
    public partial class MainWindow : Window
    {

        public MainWindow()
        {
            this.InitializeComponent();
        }

        private void bdrWindowTitle_MouseLeftButtonDown( object sender , MouseButtonEventArgs e )
        {
            this.DragMove();
        }

        private void btnClose_Click( object sender , RoutedEventArgs e )
        {
            this.Close();
        }

        private void btnMaximize_Click( object sender , RoutedEventArgs e )
        {
            this.WindowState = ( this.WindowState != WindowState.Maximized ) ? WindowState.Maximized : WindowState.Normal;
        }

        private void btnMinimize_Click( object sender , RoutedEventArgs e )
        {
            this.WindowState = WindowState.Minimized;
        }
    }
}

 

OK!!到這邊為止,我們就有一個可以拖拉移動的視窗啦!!

image

接下來,就是另一個重頭戲了!! 視窗只能移動當然是不夠的啊!! 還得要能任意的放大縮小才行!! 這時候,就要藉助Thumb這個控制項來幫我們完成後面艱巨的任務啦!!

為了實作出和一般的視窗一樣,能讓使用者拖拉視窗的外緣來調整視窗大小的功能,我們要在將八個Thumb分別放置在自訂視窗邊緣的八個位置,並且妥善命名(在CodeBehind會用到),另外,也順便設定每個Thumb所要使用的滑鼠游標

image

八個Thumb放置妥當之後,就可以將它們的Opacity屬性設為0%,因為我們不希望在執行的時候它們會被顯示在畫面上;並且為每個Thumb的完成後的Xaml如下:

MainWindow.xaml

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
		xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
		xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
		xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
		xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
		xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
		xmlns:Microsoft_Windows_Themes="clr-namespace:Microsoft.Windows.Themes;assembly=PresentationFramework.Aero"
		mc:Ignorable="d" x:Class="Wpf_CustomWindow.MainWindow" x:Name="Window" Title="MainWindow" Width="640"
		Height="480" WindowStyle="None" Background="{x:Null}" WindowStartupLocation="CenterScreen" ResizeMode="NoResize"
		AllowsTransparency="True" MinWidth="400" MinHeight="300">
	<Grid x:Name="LayoutRoot">
		<Grid.RowDefinitions>
			<!-- 第一個Row使用固定的高度,因為Title列通常不需要隨著視窗大小縮放 -->
			<RowDefinition Height="40" />
			<!-- 第二個Row用來放置視窗的內容,就把剩餘的空間都給它吧 -->
			<RowDefinition Height="*" />
		</Grid.RowDefinitions>
		<!-- 透過設定Border的邊框,就可以做出自訂的TitleBar的感覺啦!! -->
		<Border x:Name="bdrWindowTitle" BorderBrush="Black" BorderThickness="2,2,2,0" CornerRadius="10,10,0,0"
				Margin="0,10,0,0" Background="White" MouseLeftButtonDown="bdrWindowTitle_MouseLeftButtonDown">
			<Grid x:Name="grdTitleContent" Margin="10,10,10,0">
				<StackPanel x:Name="stkTitle" Orientation="Horizontal" d:LayoutOverrides="Height" Margin="0,-20,0,0"
						HorizontalAlignment="Left">
					<TextBlock x:Name="textBlock" TextWrapping="Wrap" Text="W" VerticalAlignment="Bottom" FontSize="48"
							FontWeight="Bold" FontFamily="Arial Black" d:LayoutOverrides="Width" Margin="0,-20,0,-5"
							Foreground="White">
						<TextBlock.Effect>
							<DropShadowEffect ShadowDepth="3" BlurRadius="2" Opacity="0.5" />
						</TextBlock.Effect></TextBlock>
					<TextBlock TextWrapping="Wrap" Text="indowTitle" VerticalAlignment="Bottom" FontSize="26.667"
							FontWeight="Bold" FontFamily="Arial Black">
						<TextBlock.Effect>
							<DropShadowEffect ShadowDepth="3" BlurRadius="2" Opacity="0.5" />
						</TextBlock.Effect></TextBlock>
				</StackPanel>
				<StackPanel x:Name="stkButtonBar" HorizontalAlignment="Right" Orientation="Horizontal" Margin="0,-5,0,0">
					<!-- 最小化按鈕 -->
					<Button x:Name="btnMinimize" Content="-" Margin="10,0,0,0" Width="28" Click="btnMinimize_Click" />
					<!-- 最大化按鈕 -->
					<Button x:Name="btnMaximize" Content="+" Margin="10,0,0,0" Width="28" Click="btnMaximize_Click" />
					<!-- 關閉視窗鈕 -->
					<Button x:Name="btnClose" Content="X" Margin="10,0,0,0" Width="28" Click="btnClose_Click" />
				</StackPanel>
			</Grid>
		</Border>
		<Border x:Name="bdrWindowContent" BorderBrush="Black" BorderThickness="2,0,2,2" Grid.Row="1"
				CornerRadius="0,0,10,10" Background="White">
		</Border>
		<Thumb x:Name="thumbTop" Height="5" Margin="15,10,15,0" VerticalAlignment="Top" Background="Red"
				Grid.RowSpan="2" Cursor="SizeNS" Opacity="0" />
		<Thumb x:Name="thumbTopRight" HorizontalAlignment="Right" Height="15" Margin="0,10,0,0" VerticalAlignment="Top"
				Width="15" Background="Green" Grid.RowSpan="2" Cursor="SizeNESW" Opacity="0" />
		<Thumb x:Name="thumbRight" HorizontalAlignment="Right" Margin="0,25,0,15" Width="5" Background="Red"
				Grid.RowSpan="2" Cursor="SizeWE" Opacity="0" />
		<Thumb x:Name="thumbBottomRight" HorizontalAlignment="Right" Height="15" Margin="0" VerticalAlignment="Bottom"
				Width="15" Background="Green" Grid.RowSpan="2" Cursor="SizeNWSE" Opacity="0" />
		<Thumb x:Name="thumbBottom" Height="5" Margin="15,0" VerticalAlignment="Bottom" Background="Red"
				Grid.RowSpan="2" Cursor="SizeNS" Opacity="0" />
		<Thumb x:Name="thumbBottomLeft" HorizontalAlignment="Left" Height="15" Margin="0" VerticalAlignment="Bottom"
				Width="15" Background="Green" Grid.RowSpan="2" Cursor="SizeNESW" Opacity="0" />
		<Thumb x:Name="thumbLeft" HorizontalAlignment="Left" Margin="0,25,0,15" Width="5" Background="Red"
				Grid.RowSpan="2" Cursor="SizeWE" Opacity="0" />
		<Thumb x:Name="thumbTopLeft" HorizontalAlignment="Left" Height="15" Margin="0,10,0,0" VerticalAlignment="Top"
				Width="15" Background="Green" Grid.RowSpan="2" Cursor="SizeNWSE" Opacity="0" />
	</Grid>
</Window>

 

最後,我們就要在CodeBehind來撰寫處理視這八個Thumb被拖放時的EventHandler啦!! 而在這邊,我們得藉助Windows API和Com元件來完成,所以請記得加入System.Runtime.InteropServicesSystem.Windows.Interop這兩個Namespace的引用。

接著請服用以下的程式碼:

MainWindow.xaml.cs

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;

namespace Wpf_CustomWindow
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private const int WM_SYSCOMMAND = 0x112;
        private HwndSource hwndSource;
        IntPtr retInt = IntPtr.Zero;

        public MainWindow()
        {
            this.InitializeComponent();

            this.SourceInitialized += new System.EventHandler( MainWindow_SourceInitialized );
        }

        void MainWindow_SourceInitialized( object sender , System.EventArgs e )
        {
            hwndSource = PresentationSource.FromVisual( ( Visual ) sender ) as HwndSource;
            hwndSource.AddHook( new HwndSourceHook( WndProc ) );
        }

        private IntPtr WndProc( IntPtr hwnd , int msg , IntPtr wParam , IntPtr lParam , ref bool handled )
        {
            Debug.WriteLine( "WndProc messages: " + msg.ToString() );

            if( msg == WM_SYSCOMMAND )
            {
                Debug.WriteLine( "WndProc messages: " + msg.ToString() );
            }

            return IntPtr.Zero;
        }

        public enum ResizeDirection
        {
            Left = 1 ,
            Right = 2 ,
            Top = 3 ,
            TopLeft = 4 ,
            TopRight = 5 ,
            Bottom = 6 ,
            BottomLeft = 7 ,
            BottomRight = 8 ,
        }

        [DllImport( "user32.dll" , CharSet = CharSet.Auto )]
        private static extern IntPtr SendMessage( IntPtr hWnd , uint Msg , IntPtr wParam , IntPtr lParam );

        private void ResizeWindow( ResizeDirection direction )
        {
            SendMessage( hwndSource.Handle , WM_SYSCOMMAND , ( IntPtr ) ( 61440 + direction ) , IntPtr.Zero );
        }

        private void ResetCursor( object sender , MouseEventArgs e )
        {
            if( Mouse.LeftButton != MouseButtonState.Pressed )
            {
                this.Cursor = Cursors.Arrow;
            }
        }

        private void thumb_PreviewMouseLeftButtonDown( object sender , MouseButtonEventArgs e )
        {
            Thumb thumb = sender as Thumb;

            //這邊要配合Thumb的命名來取處理,如果Thumb的命名和我的不同,請自行修改下面的程式內容。
            switch( thumb.Name.Substring( 5 ) )
            {
                case "Top":
                    ResizeWindow( ResizeDirection.Top );
                    break;
                case "Bottom":
                    ResizeWindow( ResizeDirection.Bottom );
                    break;
                case "Left":
                    ResizeWindow( ResizeDirection.Left );
                    break;
                case "Right":
                    ResizeWindow( ResizeDirection.Right );
                    break;
                case "TopLeft":
                    ResizeWindow( ResizeDirection.TopLeft );
                    break;
                case "TopRight":
                    ResizeWindow( ResizeDirection.TopRight );
                    break;
                case "BottomLeft":
                    ResizeWindow( ResizeDirection.BottomLeft );
                    break;
                case "BottomRight":
                    ResizeWindow( ResizeDirection.BottomRight );
                    break;
                default:
                    break;
            }

        }

        private void bdrWindowTitle_MouseLeftButtonDown( object sender , MouseButtonEventArgs e )
        {
            this.DragMove();
        }

        private void btnClose_Click( object sender , RoutedEventArgs e )
        {
            this.Close();
        }

        private void btnMaximize_Click( object sender , RoutedEventArgs e )
        {
            this.WindowState = ( this.WindowState != WindowState.Maximized ) ? WindowState.Maximized : WindowState.Normal;
        }

        private void btnMinimize_Click( object sender , RoutedEventArgs e )
        {
            this.WindowState = WindowState.Minimized;
        }

    }
}

最後一步,將我們的八個Thumb控制項的PreviewMouseLeftButtonDown事件指定給thumb_PreviewMouseLeftButtonDown這個EventHandler來處理,就大功告成啦!!

完成後的Xaml如下:

 

MainWindow.xaml

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
		xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
		xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
		xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
		xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
		xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
		xmlns:Microsoft_Windows_Themes="clr-namespace:Microsoft.Windows.Themes;assembly=PresentationFramework.Aero"
		mc:Ignorable="d" x:Class="Wpf_CustomWindow.MainWindow" x:Name="Window" Title="MainWindow" Width="640"
		Height="480" WindowStyle="None" Background="{x:Null}" WindowStartupLocation="CenterScreen" ResizeMode="NoResize"
		AllowsTransparency="True" MinWidth="400" MinHeight="300">
	<Window.Resources></Window.Resources>
	<Grid x:Name="LayoutRoot">
		<Grid.RowDefinitions>
			<!-- 第一個Row使用固定的高度,因為Title列通常不需要隨著視窗大小縮放 -->
			<RowDefinition Height="40" />
			<!-- 第二個Row用來放置視窗的內容,就把剩餘的空間都給它吧 -->
			<RowDefinition Height="*" />
		</Grid.RowDefinitions>
		<!-- 透過設定Border的邊框,就可以做出自訂的TitleBar的感覺啦!! -->
		<Border x:Name="bdrWindowTitle" BorderBrush="Black" BorderThickness="2,2,2,0" CornerRadius="10,10,0,0"
				Margin="0,10,0,0" Background="White" MouseLeftButtonDown="bdrWindowTitle_MouseLeftButtonDown">
			<Grid x:Name="grdTitleContent" Margin="10,10,10,0">
				<StackPanel x:Name="stkTitle" Orientation="Horizontal" d:LayoutOverrides="Height" Margin="0,-20,0,0"
						HorizontalAlignment="Left">
					<TextBlock x:Name="textBlock" TextWrapping="Wrap" Text="W" VerticalAlignment="Bottom" FontSize="48"
							FontWeight="Bold" FontFamily="Arial Black" d:LayoutOverrides="Width" Margin="0,-20,0,-5"
							Foreground="White">
						<TextBlock.Effect>
							<DropShadowEffect ShadowDepth="3" BlurRadius="2" Opacity="0.5" />
						</TextBlock.Effect></TextBlock>
					<TextBlock TextWrapping="Wrap" Text="indowTitle" VerticalAlignment="Bottom" FontSize="26.667"
							FontWeight="Bold" FontFamily="Arial Black">
						<TextBlock.Effect>
							<DropShadowEffect ShadowDepth="3" BlurRadius="2" Opacity="0.5" />
						</TextBlock.Effect></TextBlock>
				</StackPanel>
				<StackPanel x:Name="stkButtonBar" HorizontalAlignment="Right" Orientation="Horizontal" Margin="0,-5,0,0">
					<!-- 最小化按鈕 -->
					<Button x:Name="btnMinimize" Content="-" Margin="10,0,0,0" Width="28" Click="btnMinimize_Click" />
					<!-- 最大化按鈕 -->
					<Button x:Name="btnMaximize" Content="+" Margin="10,0,0,0" Width="28" Click="btnMaximize_Click" />
					<!-- 關閉視窗鈕 -->
					<Button x:Name="btnClose" Content="X" Margin="10,0,0,0" Width="28" Click="btnClose_Click" />
				</StackPanel>
			</Grid>
		</Border>
		<Border x:Name="bdrWindowContent" BorderBrush="Black" BorderThickness="2,0,2,2" Grid.Row="1"
				CornerRadius="0,0,10,10" Background="White">
			<Grid x:Name="grdWindowContent" Margin="10,5,10,10" Visibility="Collapsed">
				<Grid.RowDefinitions>
					<RowDefinition Height="0.5*" />
					<RowDefinition Height="0.5*" />
				</Grid.RowDefinitions>
				<TextBlock x:Name="textBlock1" TextWrapping="Wrap" Text="TextBlock" HorizontalAlignment="Center"
						VerticalAlignment="Center" />
				<Button x:Name="button" Content="Button" Grid.Row="1" />
			</Grid>
		</Border>
		<Thumb x:Name="thumbTop" Height="5" Margin="15,10,15,0" VerticalAlignment="Top" Background="Red"
				Grid.RowSpan="2" Cursor="SizeNS" PreviewMouseLeftButtonDown="thumb_PreviewMouseLeftButtonDown" Opacity="0" />
		<Thumb x:Name="thumbTopRight" HorizontalAlignment="Right" Height="15" Margin="0,10,0,0" VerticalAlignment="Top"
				Width="15" Background="Green" Grid.RowSpan="2" Cursor="SizeNESW"
				PreviewMouseLeftButtonDown="thumb_PreviewMouseLeftButtonDown" Opacity="0" />
		<Thumb x:Name="thumbRight" HorizontalAlignment="Right" Margin="0,25,0,15" Width="5" Background="Red"
				Grid.RowSpan="2" Cursor="SizeWE"
				PreviewMouseLeftButtonDown="thumb_PreviewMouseLeftButtonDown" Opacity="0" />
		<Thumb x:Name="thumbBottomRight" HorizontalAlignment="Right" Height="15" Margin="0" VerticalAlignment="Bottom"
				Width="15" Background="Green" Grid.RowSpan="2" Cursor="SizeNWSE"
				PreviewMouseLeftButtonDown="thumb_PreviewMouseLeftButtonDown" Opacity="0" />
		<Thumb x:Name="thumbBottom" Height="5" Margin="15,0" VerticalAlignment="Bottom" Background="Red"
				Grid.RowSpan="2" Cursor="SizeNS"
				PreviewMouseLeftButtonDown="thumb_PreviewMouseLeftButtonDown" Opacity="0" />
		<Thumb x:Name="thumbBottomLeft" HorizontalAlignment="Left" Height="15" Margin="0" VerticalAlignment="Bottom"
				Width="15" Background="Green" Grid.RowSpan="2" Cursor="SizeNESW"
				PreviewMouseLeftButtonDown="thumb_PreviewMouseLeftButtonDown" Opacity="0" />
		<Thumb x:Name="thumbLeft" HorizontalAlignment="Left" Margin="0,25,0,15" Width="5" Background="Red"
				Grid.RowSpan="2" Cursor="SizeWE"
				PreviewMouseLeftButtonDown="thumb_PreviewMouseLeftButtonDown" Opacity="0" />
		<Thumb x:Name="thumbTopLeft" HorizontalAlignment="Left" Height="15" Margin="0,10,0,0" VerticalAlignment="Top"
				Width="15" Background="Green" Grid.RowSpan="2" Cursor="SizeNWSE"
				PreviewMouseLeftButtonDown="thumb_PreviewMouseLeftButtonDown" Opacity="0" />
	</Grid>
</Window>

 

OK!!大功告成!! 奉上專案源始碼來和大家一起慶祝: