上一篇文章介紹了使用WindowChrome自定義Window,實際使用下來總有各類各樣的問題,這些問題大部分都不影響使用,可能正是由於不影響使用因此一直沒獲得修復(也有可能別人根本不以爲這些是問題)。html
這篇文章我總結了一些實際遇到的問題及其解決方案。git
上一篇文章提到有幾個值用於計算Chrome的尺寸:github
屬性 | 值(像素) | 描述 |
---|---|---|
SM_CXFRAME/SM_CYFRAME | 4 | The thickness of the sizing border around the perimeter of a window that can be resized, in pixels. SM_CXSIZEFRAME is the width of the horizontal border, and SM_CYSIZEFRAME is the height of the vertical border.This value is the same as SM_CXFRAME. |
SM_CXPADDEDBORDER | 4 | The amount of border padding for captioned windows, in pixels.Windows XP/2000: This value is not supported. |
SM_CYCAPTION | 23 | The height of a caption area, in pixels. |
在有標題的標準Window,chrome的頂部尺寸爲SM_CYFRAME + SM_CXPADDEDBORDER + SM_CYCAPTION = 31,左右兩邊尺寸爲SM_CXFRAME + SM_CXPADDEDBORDER = 8,底部尺寸爲SM_CYFRAME + SM_CXPADDEDBORDER = 8。chrome
具體的計算方式能夠參考Firefox的源碼:shell
// mCaptionHeight is the default size of the NC area at // the top of the window. If the window has a caption, // the size is calculated as the sum of: // SM_CYFRAME - The thickness of the sizing border // around a resizable window // SM_CXPADDEDBORDER - The amount of border padding // for captioned windows // SM_CYCAPTION - The height of the caption area // // If the window does not have a caption, mCaptionHeight will be equal to // `GetSystemMetrics(SM_CYFRAME)` mCaptionHeight = GetSystemMetrics(SM_CYFRAME) + (hasCaption ? GetSystemMetrics(SM_CYCAPTION) + GetSystemMetrics(SM_CXPADDEDBORDER) : 0); // mHorResizeMargin is the size of the default NC areas on the // left and right sides of our window. It is calculated as // the sum of: // SM_CXFRAME - The thickness of the sizing border // SM_CXPADDEDBORDER - The amount of border padding // for captioned windows // // If the window does not have a caption, mHorResizeMargin will be equal to // `GetSystemMetrics(SM_CXFRAME)` mHorResizeMargin = GetSystemMetrics(SM_CXFRAME) + (hasCaption ? GetSystemMetrics(SM_CXPADDEDBORDER) : 0); // mVertResizeMargin is the size of the default NC area at the // bottom of the window. It is calculated as the sum of: // SM_CYFRAME - The thickness of the sizing border // SM_CXPADDEDBORDER - The amount of border padding // for captioned windows. // // If the window does not have a caption, mVertResizeMargin will be equal to // `GetSystemMetrics(SM_CYFRAME)` mVertResizeMargin = GetSystemMetrics(SM_CYFRAME) + (hasCaption ? GetSystemMetrics(SM_CXPADDEDBORDER) : 0);
在WPF中這幾個值分別映射到SystemParameters的相關屬性:編程
系統值 | SystemParameters屬性 | 值 |
---|---|---|
SM_CXFRAME/SM_CYFRAME | WindowResizeBorderThickness | 4,4,4,4 |
SM_CXPADDEDBORDER | 無 | 4 |
SM_CYCAPTION | WindowCaptionHeight | 23 |
另外還有WindowNonClientFrameThickness,至關於WindowResizeBorderThickness的基礎上,Top+=WindowCaptionHeight,值爲 4,27,4,4。windows
SM_CXPADDEDBORDER在WPF裏沒有對應的值,我寫了個WindowParameters的類,添加了這個屬性:api
/// <summary> /// returns the border thickness padding around captioned windows,in pixels. Windows XP/2000: This value is not supported. /// </summary> public static Thickness PaddedBorderThickness { [SecurityCritical] get { if (_paddedBorderThickness == null) { var paddedBorder = NativeMethods.GetSystemMetrics(SM.CXPADDEDBORDER); var dpi = GetDpi(); Size frameSize = new Size(paddedBorder, paddedBorder); Size frameSizeInDips = DpiHelper.DeviceSizeToLogical(frameSize, dpi / 96.0, dpi / 96.0); _paddedBorderThickness = new Thickness(frameSizeInDips.Width, frameSizeInDips.Height, frameSizeInDips.Width, frameSizeInDips.Height); } return _paddedBorderThickness.Value; } }
先說說個人環境,WIndows 10,1920 * 1080 分辨率,100% DPI。app
<WindowChrome.WindowChrome> <WindowChrome /> </WindowChrome.WindowChrome> <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>
按上一篇文章介紹的方法打開一個使用WindowChrome的Window(大小爲800 * 600),在VisualStudio的實時可視化樹能夠看到AdornerDecorator的實際大小和Window的實際大小都是800 * 600(畢竟邊WindowChrome裏的Border、Grid等都沒設Margin或Padding)。而後用Inspect觀察它的邊框。能夠看到Window實際上的範圍沒什麼問題。但和標準Window的對比就能夠看出有區別,我在以前的文章中介紹過標準Window的實際範圍和用戶看到的並不同。ide
上面兩張圖分別是經過Inspect觀察的標準Window(上圖)和使用WindowChrome的Window(下圖),能夠看到標準Window左右下三個方向有些空白位置,和邊框加起來是8個像素。WindowChrome則沒有這個問題。
WindowChrome最大化時狀態如上圖所示,大小也變爲1936 * 1066,這個大小沒問題,有問題的是它不會計算好client-area的尺寸,只是簡單地加大non-client的尺寸,致使client-area的尺寸也成了1936 * 1066。標準Window在最大化時non-client area的尺寸爲1936 * 1066,client-area的尺寸爲1920 * 1027。
結合Window(窗體)的UI元素及行爲這篇文章,WindowChrome最大化時的client-area的尺寸就是Window尺寸(1936 * 1066)減去WindowNonClientFrameThickness(4,27,4,4)再減去PaddedBorderThickness(4,4,4,4)。這樣就準確地計算出client-area在最大化狀態下的尺寸爲1920 * 1027。
在自定義Window的ControlTempalte中我使用Trigger在最大化狀態下將邊框改成0,而後加上WindowResizeBorderThickness的Padding和PaddedBorderThickness的Margin:
<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>
之前我還試過讓BorderThickness保持爲1,Margin改成7,但後來發現運行在高於100% DPI的環境下出了問題,因此改成綁定到屬性。
在不一樣DPI下這幾個屬性值以下:
DPI | non-client area 尺寸 | client area 尺寸 | WindowNonClientFrameThickness | PaddedBorderThickness |
---|---|---|---|---|
100 | 1936 * 1066 | 1920 * 1027 | 4,4,4,4 | 4,4,4,4 |
125 | 1550.4 | 1536 | 3.2,3.2,3.2,3.2 | 4,4,4,4 |
150 | 1294.66666666667 | 280 | 3.3333,3.3333,3.3333,3.3333 | 4,4,4,4 |
175 | 1110.85714285714 | 1097.14285714286 | 2.8571428,2.8571428,2.8571428,2.8571428 | 4,4,4,4 |
200 | 973 | 960 | 2.5,2.5,2.5,2.5 | 4,4,4,4 |
能夠看到PaddedBorderThickness老是等於4,因此也可使用不綁定PaddedBorderThickness的方案:
<Border x:Name="WindowBorder" BorderThickness="3" BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}" > <Border.Style> <Style TargetType="{x:Type Border}"> <Style.Triggers> <DataTrigger Binding="{Binding WindowState, RelativeSource={RelativeSource TemplatedParent}}" Value="Maximized"> <Setter Property="Margin" Value="{x:Static SystemParameters.WindowResizeBorderThickness}"/> <Setter Property="Padding" Value="1"/> </DataTrigger> </Style.Triggers> </Style> </Border.Style>
但我仍是更喜歡PaddedBorderThickness,這是心情上的問題(我都寫了這麼多代碼了,你告訴我直接用4這個神奇的數字就行了,我斷然不能接受)。並且有可能未來Windows的窗體設計會改變,綁定系統的屬性比較保險。
最後,其實應該監視SystemParameters的StaticPropertyChanged事件而後修改PaddedBorderThickness,由於WindowNonClientFrameThickness和WindowResizeBorderThickness會在系統主題改變時改變,但不想爲了這小几率事件多寫代碼就偷懶了。
SizeToContent屬性用於指示Window是否自動調整它的大小,但當設置'SizeToContent="WidthAndHeight"'時就會出問題:
上圖左面時一個沒內容的自定義Window,右邊是一個沒內容的系統Window,兩個都設置了SizeToContent="WidthAndHeight"
。能夠看到自定義WindowChorme多出了一些黑色的區域,仔細觀察這些黑色區域,發覺它的尺寸大概就是non-client area的尺寸,並且內容就是WindowChrome本來的內容。
SizeToContent="WidthAndHeight"
時Window須要計算ClientArea的尺寸而後再肯定Window的尺寸,但使用WindowChrome自定義Window時程序覺得整個ControlTempalte的內容都是ClientArea,把它看成了ClientArea的尺寸,再加上non-client的尺寸就得出了錯誤的Window尺寸。ControleTemplate的內容沒辦法遮住整個WindowChrome的內容,因而就出現了這些黑色的區域。
解決方案是在OnSourceInitialized時簡單粗暴地要求再計算一次尺寸:
protected override void OnSourceInitialized(EventArgs e) { base.OnSourceInitialized(e); if (SizeToContent == SizeToContent.WidthAndHeight && WindowChrome.GetWindowChrome(this) != null) { InvalidateMeasure(); } }
之前我曾建議在OnContentRendered
中執行這段代碼,但後來發現調試模式,或者性能比較差的場合會有些問題,因此改成在OnSourceInitialized
中執行了。
若是一個Window設置了Owner而且以ShowDialog的方式打開,點擊它的Owner將對這個Window調用FlashWindowEx功能,即閃爍幾下,而且還有提示音。除了這種方式還能夠用編程的方式調用FlashWindow功能。
WindowChrome提供通知FlashWindow發生的事件,FlashWindow發生時雖然Window看上去在Active/Inactive 狀態間切換,但IsActive屬性並不會改變。
要處理這個問題,能夠監聽WM_NCACTIVATE消息,它通知Window的non-client area是否須要切換Active/Inactive狀態。
IntPtr handle = new WindowInteropHelper(this).Handle; HwndSource.FromHwnd(handle).AddHook(new HwndSourceHook(WndProc)); protected override void OnActivated(EventArgs e) { base.OnActivated(e); SetValue(IsNonClientActivePropertyKey, true); } protected override void OnDeactivated(EventArgs e) { base.OnDeactivated(e); SetValue(IsNonClientActivePropertyKey, false); } private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { if (msg == WindowNotifications.WM_NCACTIVATE) SetValue(IsNonClientActivePropertyKey, wParam == _trueValue); return IntPtr.Zero; }
須要添加一個只讀的IsNonClientActive依賴屬性,ControlTemplate經過Trigger使邊框置灰:
<Trigger Property="IsNonClientActive" Value="False"> <Setter Property="BorderBrush" Value="#FF6F7785" /> </Trigger>
標準Window能夠單擊並拖動以調整窗口大小的區域爲8像素(能夠理解爲SM_CXFRAME的4像素加上SM_CXPADDEDBORDER的4像素)。
WindowChrome實際大小就是看起來的大小,默認的ResizeBorderThickness是4像素,就是從Chrome的邊框向內的4像素範圍,再多就會影響client-area裏各元素的正常使用。
因爲標準Window的課拖動區域幾乎在Window的外側,並且有8個像素,而WindowChrome只能有4個像素,因此WindowChrome拖動起來手感沒那麼好。
最後提一下WindowChrome的性能問題,正常操做我以爲應該沒什麼問題,只有拖動左右邊緣尤爲是左邊緣改變Window大小的時候右邊的邊緣會很不和諧。其實這個問題不是什麼大問題,看看這個空的什麼都沒有的Skype窗體都會這樣,因此不須要特別在乎。
在Kino.Toolkit.Wpf裏我只提供了最簡單的使用WindowChrome的方案,這個方案只能建立沒有圓角的Window,並且不能自定義邊框陰影顏色。若是真的須要更高的自由度能夠試試參考其它方案。
VisualStudio固然沒有開源,但並不妨礙咱們去參考它的源碼。能夠在如下DLL找到Microsoft.VisualStudio.PlatformUI.MainWindow
:
X:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\Common7\IDE\Microsoft.VisualStudio.Shell.UI.Internal.dll
Modern UI for WPF (MUI),A set of controls and styles converting your WPF application into a great looking Modern UI app.
MahApps.Metro,A framework that allows developers to cobble together a Metro or Modern UI for their own WPF applications with minimal effort.
Fluent.Ribbon is a library that implements an Office-like user interface for the Windows Presentation Foundation (WPF).
HandyControlHandyControl是一套WPF控件庫,它幾乎重寫了全部原生樣式,同時包含50多款額外的控件,還提供了一些好看的Window。
WindowChrome Class (System.Windows.Shell) Microsoft Docs
SystemParameters Class (System.Windows) Microsoft Docs
WPF Windows 概述 _ Microsoft Docs
GetSystemMetrics function Microsoft Docs
FlashWindowEx function Microsoft Docs
Window Class (System.Windows) Microsoft Docs
Inspect - Windows applications Microsoft Docs