無論是在工作和學習中使用WPF時,我們通常都會接觸到CustomControl,今天我們就CustomWindow之后的一些邊角技術進行探討和剖析。
童鞋們在WPF開發過程中是否覺得默認的Style太丑,或者是由Balabala的一些原因,使你覺得重寫一個“高大上”的Window來符合項目的UI要求(小明:“我們使用Telerik” 老師:“什么?你說你們使用第三方UI框架?滾出去!”)經過半天的努力我們搞定了一個帥氣的Window! Like this:
[TemplatePart( Name = "PART_RichTitle", Type = typeof( RichTitleChrome ) )] [TemplatePart( Name = "PART_PluginArea", Type = typeof( ScrollItemsContainer ) )] [TemplatePart( Name = "PART_MenuButton", Type = typeof( Button ) )] [TemplatePart( Name = "PART_MinButton", Type = typeof( Button ) )] [TemplatePart( Name = "PART_MaxButton", Type = typeof( Button ) )] [TemplatePart( Name = "PART_CloseButton", Type = typeof( Button ) )] [TemplatePart( Name = "PART_NonWorkArea", Type = typeof( AdornerNonWorkArea ) )] [TemplatePart( Name = "PART_BusyIndicator", Type = typeof( BusyIndicator ) )] [TemplatePart( Name = "PART_ToolbarArea", Type = typeof( ScrollItemsContainer ) )] [TemplatePart( Name = "PART_ResizeGrip", Type = typeof( ResizeGrip ) )] [TemplatePart( Name = "PART_Shadow", Type = typeof( Border ) )] [DefaultProperty( "ToolBarContent" )] public class MultilayerWindowShell : Window, IFrameworkVisual, IWindowNavigationService, IBusyObservableVisual, IVersionComponent { static MultilayerWindowShell() { DefaultStyleKeyProperty.OverrideMetadata( typeof( MultilayerWindowShell ), new FrameworkPropertyMetadata( typeof( MultilayerWindowShell ) ) ); } // // 一大堆依賴屬性啊 事件啊 什么的 // protected override void OnInitialized( EventArgs e ) { // To do sth. } public override void OnApplyTemplate() { // To do sth. } // Other sth. }
然后運行起來 Like this:

哎呀,頓時感覺“高大上”起來,可以拿來跟產品經理去吹牛了。(小明:“經理經理!這UI帥氣吧!符合要求吧!” 經理:“很好很好,看起來不錯嘛,我就說你這娃有創意有思想不會令我失望的!balabalabala... 咦? ” 小明:“...” 經理:“小明啊!做事不能敷衍啊!你這窗口拖拽四邊和頂點不能改變大小啊!小明啊,這最大化位置也不對啊!我們要的最大化是距離屏幕上方有150px啊不要全屏啊!小明啊!你這子窗口彈出來的是模態的嗎?為什么不會Blink Blink的閃爍呀!小明,別忽悠我喲!!” 經理:“小明今晚加班搞定喲!” 小明:“....WQNMLGB....”。
那么為了解決小明的問題,為了滿足我們神聖的產(qu)品(shi)經(ba)理,我們來逐個搞定它!
-
窗口(對話框)模態閃動(Blink)
首先我們說明一下模態閃動為什么沒了? 因為我們自定義Window 將 WindowStyle設置為None了,窗口被隱藏掉了非工作區和邊框,只剩下了工作區,所以我們就看不到閃動了。
先來了解什么叫模態閃動, 當我們在父窗口之上彈出來一個模態的子窗口(比如 彈出另存為對話框),我們都知道模態窗口除非關閉,否則后面的任何窗口都不能接受處理。windows系統為了友好的提醒用戶,所以當用戶點擊或者想要操作除模態窗口之外的區域時,使用Blink來提示用戶,閃動的窗口必須要關閉才可以進行其他操作。
然而我們干掉了系統默認的窗口非客戶區和邊框,導致我們失去了模態閃動,所以我們的工作是恢復它或者是說是重新模擬它!想要模擬Blink,那么我們就需要知道我們需要在什么情況下讓模態窗口閃動和怎么讓它閃動?
第一個問題:模態閃動的觸發時機是什么? 是模態子窗口為關閉期間,欲操作其他窗口(或者說是父窗口)時。那么我們又是怎么個欲操作呢?通常都是鼠標去點的,但是發現沒反應。我們通過使用SPY++來監視父窗口的消息得知,即使模態子窗口未關閉,我們父窗口一樣能接受到系統發送的鼠標指針消息,那么我們的觸發Blink時機就可以確定為接收 WM_SETCURSOR 消息時進行判定和處理。
第二個問題:怎么進行Blink? 曾經有過研究的同學可能就要發表看法了。(小明:“我知道!Win32 API 有提供 FlashWindow 和FlashWindowEx! ”)恩,小明說的對。FlashWindow(Ex)確實是閃爍窗口的API,但是,那只是閃爍有系統窗口邊的窗口和在任務欄中閃爍(類似QQ來消息后的黃色閃爍),很遺憾API對於我們的無邊框自定義窗口無效!(所以,小明!滾出去!),API不好使,那么我們怎么辦呢?別忘了,區區一個閃爍是WPF的強項啊!動畫唄!所以我們可以搞一段閃爍動畫來模擬它的Blink!
So,we try it now!
為了攔截系統消息,我們先給我們的窗口安裝一個鈎子。
HwndSource hwndSource = PresentationSource.FromVisual( _wndTarget ) as HwndSource; if ( hwndSource != null ) { hwndSource.AddHook( new HwndSourceHook( WndProc ) ); } private IntPtr WndProc( IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled ) { switch ( msg ) { case NativeMethods.WM_SETCURSOR: { WmSetCursor( lParam, ref handled ); } break; } return IntPtr.Zero; }
在哪里開始安裝鈎子呢? 隨便 OnApplyTemplate OnInitialized Onloaded 自己喜歡哪就掛哪吧,對於AddHook,這是與Win32互操作的基礎,這里就不做講解了。有童鞋不明白不懂的請去搜索“HwndSource”“PresentationSource”“SourceInitialized”等關鍵字一看便知。對於 “WndProc”方法,其實他是一個回調函數。有C++ 基礎的人都知道。WndProc 這個回調中就是我們攔截消息的地方。
如上面代碼片段所示,我們攔截了WM_SETCURSOR消息。其中的lParam參數就是具體消息攜帶的消息值,我們可以獲取鼠標的狀態信息,比如我們截取在LeftButtonDown/LeftButtonUp時對Blink進行觸發判定。
// 0x202fffe: WM_LBUTTONUP and HitTest // 0x201fffe: WM_LBUTTONDOWN and HitTest
我們再來分析我們的處理方式,我們從下面2點 1) 當父窗口未激活時 2)當父窗口激活時 來分析.
1父窗口未激活時
- 我們循環查找子窗口列表中處於激活狀態的子窗口,然后Blink它,
- 如果父窗口沒有子窗口,那么我們調用 GetActiveWindow 來獲取當前進程中的Actived窗口,然后Blink它.為什么這么做,因為此時的模態窗口可能是MessageBox,或者文件打開/保存等通用對話框,並且沒有設置它的Owner.
- 如果GetActiveWindow沒有找到,我們在使用 Application.Current.Windows來找一找我們自己創建的窗口列表,並且找一找那個是模態的,然后Blink它. 如何判斷某一個Window是否是模態,我們后面將.
2父窗口在上面而模態窗口跑到下面的情況同樣需要找到,blink它.
private void WmSetCursor( IntPtr lParam, ref bool handled ) { // 0x202fffe: WM_LBUTTONUP and HitTest // 0x201fffe: WM_LBUTTONDOWN and HitTest if ( lParam.ToInt32() == 0x202fffe || lParam.ToInt32() == 0x201fffe ) { // if the wnd is not actived if ( !_wndTarget.IsActive ) { // we find the actived childwnd in parent's children wnds ,then blink it if ( _wndTarget.OwnedWindows.Count > 0 ) { foreach ( Window child in _wndTarget.OwnedWindows ) { if ( child.IsActive ) { // FlashWindowEx cann't use for non-border window... child.Blink(); handled = true; return; } } } else { // if target window has 0 children // then , find current active wnd and blink it. // eg: MessageBox.Show("hello!"); the box without // owner, when setcursor to target window , we will // blink this box. IntPtr pWnd = NativeMethods.GetActiveWindow(); if ( pWnd != IntPtr.Zero ) { HwndSource hs = HwndSource.FromHwnd( pWnd ); Window activeWnd = null == hs ? null : hs.RootVisual as Window; if ( null != activeWnd && activeWnd.IsActive ) { activeWnd.Blink(); handled = true; return; } } else { var wnds = Application.Current.Windows; if ( null != wnds && wnds.Count > 1 ) { Window modalWnd = wnds.OfType<Window>().Where( p => p != _wndTarget ).FirstOrDefault( p => p.IsModal() ); if ( null != modalWnd ) { modalWnd.Activate(); modalWnd.Blink(); handled = true; return; } } } } } else {// 父窗口在上面 而模態的在下面的情況 var wnds = Application.Current.Windows; if ( null != wnds && wnds.Count > 1 ) { Window modalWnd = wnds.OfType<Window>().Where( p => p != _wndTarget ).FirstOrDefault( p => p.IsModal() ); if ( null != modalWnd ) { modalWnd.Activate(); modalWnd.Blink(); handled = true; return; } } } } handled = false; }
上面Code中有你沒見過的方法,我們再寫一下.
1) IsModal() 方法是一個擴展方法,用來判斷指定窗口是不是模態的窗口.
public static bool IsModal<TWindow>( this TWindow wnd ) where TWindow : Window { return (bool)typeof( TWindow ).GetField( "_showingAsDialog", BindingFlags.Instance | BindingFlags.NonPublic ).GetValue( wnd ); }
其中的字段_showingAsDialog 為Window類私有的成員變量,用來保存窗口的顯示模式.(小明:"你咋知道?" Me:"調試得來,休得再問,滾出去!") 所以我們只需要得到這個變量的值就知道了窗口是否是以模態形式Show的.
2)Blink() 方法同樣為擴展類方法,用來生產動畫並播放. 大致介紹一下Blinker類:
class DialogBlinker<TWindow> where TWindow : Window { //
//
//
public void Blink() { } }
我就不貼完整的類,就不讓你全看到然后無腦copy,表着急,聽我慢慢白活~~~ :)
在講這個類之前,我們先大致了解一下,我們的動畫該怎么來模仿系統的Blink閃爍.我們通常看到的是系統窗口的邊框在閃爍,忽大忽小,如此反復若干次.其實閃爍的不是Border,而是窗口的陰影.那么好辦了,WPF的UIElement元素都有Effect屬性來設置元素的位圖效果,
我們可以為我們的Window加入DropShadowEffect陰影效果,並控制這個陰影的大小狀態來模擬閃爍.
所以,我們先構造一個靜態的位圖陰影效果並緩存到static變量中,在Blink時使用它.
private static DropShadowEffect InitDropShadowEffect() { DropShadowEffect dropShadowEffect = new DropShadowEffect(); dropShadowEffect.BlurRadius = 8; dropShadowEffect.ShadowDepth = 0; dropShadowEffect.Direction = 0; dropShadowEffect.Color = System.Windows.Media.Colors.Black; return dropShadowEffect; }
至於,DropShadowEffect的BlurRadius / Shadowdepth / Direction 屬性的值,是在經過一萬遍的實驗中得到的一組相對靠譜的數據.如果想陰影再大些或者偏移些,請自行設定.
目前有了待處理的陰影效果,我們還需要一個來處理它的動畫,來模擬系統Blink的具體動作方式.這里我使用了緩動關鍵幀動畫(EasingDoubleKeyFrame)來處理它.然后我們通過動畫來控制點啥呢?當然是控制DropShadowEffect的BlurRadius屬性.
那么我們就讓這個屬性的值在指定時間內反復的變換吧, 再大些再粗些再大些再粗些再大些再粗些,再小些再細些再小些再細些再小些再細些,balabalabala~~~~.
我們來看看具體的Animation code:
Storyboard storyboard = new Storyboard(); DoubleAnimationUsingKeyFrames keyFrames = new DoubleAnimationUsingKeyFrames(); EasingDoubleKeyFrame kt1 = new EasingDoubleKeyFrame( 0, KeyTime.FromTimeSpan( TimeSpan.FromSeconds( 0 ) ) ); EasingDoubleKeyFrame kt2 = new EasingDoubleKeyFrame( 8, KeyTime.FromTimeSpan( TimeSpan.FromSeconds( 0.3 ) ) ); kt1.EasingFunction = new ElasticEase() { EasingMode = EasingMode.EaseOut }; kt2.EasingFunction = new ElasticEase() { EasingMode = EasingMode.EaseOut }; keyFrames.KeyFrames.Add( kt1 ); keyFrames.KeyFrames.Add( kt2 ); storyboard.Children.Add( keyFrames ); Storyboard.SetTargetProperty( keyFrames, new PropertyPath( System.Windows.Media.Effects.DropShadowEffect.BlurRadiusProperty ) ); return storyboard;
哎~這里就有小明問了: WPF動畫從來沒有這么寫過啊,我們都是用Blend拖拽的!!我不認識這些東西.. 那么我們再看一組code:
<Storyboard x:Key="BlinkStory"> <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Effect).(DropShadowEffect.BlurRadius)" Storyboard.TargetName="border"> <EasingDoubleKeyFrame KeyTime="0" Value="8"> <EasingDoubleKeyFrame.EasingFunction> <ElasticEase EasingMode="EaseOut"/> </EasingDoubleKeyFrame.EasingFunction> </EasingDoubleKeyFrame> <EasingDoubleKeyFrame KeyTime="0:0:0.3" Value="26"> <EasingDoubleKeyFrame.EasingFunction> <ElasticEase EasingMode="EaseOut"/> </EasingDoubleKeyFrame.EasingFunction> </EasingDoubleKeyFrame> </DoubleAnimationUsingKeyFrames> </Storyboard>
看懂了嗎? 這倆如出一轍.一個是code behind 一個是xaml端的寫法. 在0.3秒內讓Effect的BlurRadius從0 到8的轉變. 並且伴隨 EasingMode.Easeout的緩動效果.
然后在Blink方法中 使用這個Storyboard來play就可以了. 還有一個技術點這里會涉及到.NameScope 的用法.那么它是個啥? 不過就是WPF對 名稱-UI對象 的鍵值對映射而已.每一個NameScope都有一個識別范圍.如果某個元素你想通過名稱找到它,那么你需要向NameScope來注冊這個名字(其實在XAML端,當你寫出 <XXX x:Name="name" /> 時,Name已經被自動注冊到了它所在的NameScope中). 我們需要使用Effect的名字,那么我需要注冊它.
現在我們來看核心的Blink()
public void Blink() { if ( null != targetWindow ) { if ( null == NameScope.GetNameScope( targetWindow ) ) NameScope.SetNameScope( targetWindow, new NameScope() ); originalEffect = targetWindow.Effect; if ( null == targetWindow.Effect || targetWindow.Effect.GetType() != typeof( DropShadowEffect ) ) targetWindow.Effect = dropShadowEffect; targetWindow.RegisterName( "_blink_effect", targetWindow.Effect ); Storyboard.SetTargetName( blinkStoryboard.Children[0], "_blink_effect" ); targetWindow.FlashWindowEx(); blinkStoryboard.Begin( targetWindow, true ); targetWindow.UnregisterName( "_blink_effect" ); } }
為了保持Window原有的Effect 我們需要在動畫執行完畢后 重新將之前保存起來的originalEffect賦回到Window中.
到此,我們的模態閃動就完成了.
下面展示完整的WindowBlinker<Window>類.
class DialogBlinker<TWindow> where TWindow : Window { static Storyboard blinkStoryboard; static DropShadowEffect dropShadowEffect; Effect originalEffect; TWindow targetWindow = null; static DialogBlinker() { blinkStoryboard = InitBlinkStory(); dropShadowEffect = InitDropShadowEffect(); } internal DialogBlinker( TWindow target ) { targetWindow = target; blinkStoryboard.Completed += blinkStoryboard_Completed; } void blinkStoryboard_Completed( object sender, EventArgs e ) { targetWindow.Effect = originalEffect; blinkStoryboard.Completed -= blinkStoryboard_Completed; } public void Blink() { if ( null != targetWindow ) { if ( null == NameScope.GetNameScope( targetWindow ) ) NameScope.SetNameScope( targetWindow, new NameScope() ); originalEffect = targetWindow.Effect; if ( null == targetWindow.Effect || targetWindow.Effect.GetType() != typeof( DropShadowEffect ) ) targetWindow.Effect = dropShadowEffect; targetWindow.RegisterName( "_blink_effect", targetWindow.Effect ); Storyboard.SetTargetName( blinkStoryboard.Children[0], "_blink_effect" ); targetWindow.FlashWindowEx(); blinkStoryboard.Begin( targetWindow, true ); targetWindow.UnregisterName( "_blink_effect" ); } } private static Storyboard InitBlinkStory() { #region xaml code /* <Storyboard x:Key="BlinkStory"> <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Effect).(DropShadowEffect.BlurRadius)" Storyboard.TargetName="border"> <EasingDoubleKeyFrame KeyTime="0" Value="8"> <EasingDoubleKeyFrame.EasingFunction> <ElasticEase EasingMode="EaseOut"/> </EasingDoubleKeyFrame.EasingFunction> </EasingDoubleKeyFrame> <EasingDoubleKeyFrame KeyTime="0:0:0.3" Value="26"> <EasingDoubleKeyFrame.EasingFunction> <ElasticEase EasingMode="EaseOut"/> </EasingDoubleKeyFrame.EasingFunction> </EasingDoubleKeyFrame> </DoubleAnimationUsingKeyFrames> </Storyboard> */ #endregion // xaml code Storyboard storyboard = new Storyboard(); DoubleAnimationUsingKeyFrames keyFrames = new DoubleAnimationUsingKeyFrames(); EasingDoubleKeyFrame kt1 = new EasingDoubleKeyFrame( 0, KeyTime.FromTimeSpan( TimeSpan.FromSeconds( 0 ) ) ); EasingDoubleKeyFrame kt2 = new EasingDoubleKeyFrame( 8, KeyTime.FromTimeSpan( TimeSpan.FromSeconds( 0.3 ) ) ); kt1.EasingFunction = new ElasticEase() { EasingMode = EasingMode.EaseOut }; kt2.EasingFunction = new ElasticEase() { EasingMode = EasingMode.EaseOut }; keyFrames.KeyFrames.Add( kt1 ); keyFrames.KeyFrames.Add( kt2 ); storyboard.Children.Add( keyFrames ); Storyboard.SetTargetProperty( keyFrames, new PropertyPath( System.Windows.Media.Effects.DropShadowEffect.BlurRadiusProperty ) ); return storyboard; } private static DropShadowEffect InitDropShadowEffect() { DropShadowEffect dropShadowEffect = new DropShadowEffect(); dropShadowEffect.BlurRadius = 8; dropShadowEffect.ShadowDepth = 0; dropShadowEffect.Direction = 0; dropShadowEffect.Color = System.Windows.Media.Colors.Black; return dropShadowEffect; } }
-
自定義窗口的四邊拖拽支持
在我們的自定義窗口中,如果不對四周進行特殊的拖拽處理的話,只能使用 ResizeGrip 組件來實現拖拽右下角實現改變窗口的大小,但是也只能拖拽右下角,而不能隨意拖拽窗口的四邊和角. 為了更完美的實現自定義窗口的功能,合理的人機交互,也為了神聖的產品經理
不對小明失望.我們來搞定它!
我們先來分析一下,或者說是猜測一下系統窗口的四邊/角拖拽是怎么實現的?
1)當鼠標移動到應用程序窗口的四個邊上或者附近時,光標變化為 可拖拽的箭頭樣子.
2)當鼠標移動四個角落或者附近時,光標發生變化.
這是2個拖拽響應前的一個光標位置判斷.當此時鼠標按下並保持移動即可拖拽. 知道了拖拽的時機,那么又是怎么拖的呢?我們通過Spy++ 檢測得到了鼠標命中測試消息(OnNcHitTest),So,我們可以通過手工發送鼠標命中測試枚舉值給操作系統,來欺騙操作系統,讓操作系統認為鼠標真實的Hit到了非客戶區的某個地點.從而乖乖的為我們作出相應.
我大致列一下這些個HitTest枚舉:
下面列出的鼠標擊中測試枚舉值之一。
· HTBORDER 在不具有可變大小邊框的窗口的邊框上。
· HTBOTTOM 在窗口的水平邊框的底部。
· HTBOTTOMLEFT 在窗口邊框的左下角。
· HTBOTTOMRIGHT 在窗口邊框的右下角。
· HTCAPTION 在標題條中。
· HTCLIENT 在客戶區中。
· HTERROR 在屏幕背景或窗口之間的分隔線上(與HTNOWHERE相同,除了Windows的DefWndProc函數產生一個系統響聲以指明錯誤)。
· HTGROWBOX 在尺寸框中。
· HTHSCROLL 在水平滾動條上。
· HTLEFT 在窗口的左邊框上。
· HTMAXBUTTON 在最大化按鈕上。
· HTMENU 在菜單區域。
· HTMINBUTTON 在最小化按鈕上。
· HTNOWHERE 在屏幕背景或窗口之間的分隔線上。
· HTREDUCE 在最小化按鈕上。
· HTRIGHT 在窗口的右邊框上。
· HTSIZE 在尺寸框中。(與HTGROWBOX相同)
· HTSYSMENU 在控制菜單或子窗口的關閉按鈕上。
· HTTOP 在窗口水平邊框的上方。
· HTTOPLEFT 在窗口邊框的左上角。
· HTTOPRIGHT 在窗口邊框的右上角。
· HTTRANSPARENT 在一個被其它窗口覆蓋的窗口中。
· HTVSCROLL 在垂直滾動條中。
· HTZOOM 在最大化按鈕上。
我們需要使用的只有Left/Top/Right/Bottom四邊, TopLeft/BottomLeft/TopRight/BottomRight四角的枚舉.
來看看具體實現:
class WindowResizerImp { private readonly int agWidth = 12; //拐角寬度 private readonly int bThickness = 4; // 邊框寬度 private Point mousePoint = new Point(); //鼠標坐標 private IntPtr WndProc( IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled ) { switch ( msg ) { case NativeMethods.WM_NCHITTEST: { return WmNCHitTest( lParam, ref handled ); } } return IntPtr.Zero; } private IntPtr WmNCHitTest( IntPtr lParam, ref bool handled ) { // Update cursor point // The low-order word specifies the x-coordinate of the cursor. // #define GET_X_LPARAM(lp) ((int)(short)LOWORD(lp)) this.mousePoint.X = (int)(short)( lParam.ToInt32() & 0xFFFF ); // The high-order word specifies the y-coordinate of the cursor. // #define GET_Y_LPARAM(lp) ((int)(short)HIWORD(lp)) this.mousePoint.Y = (int)(short)( lParam.ToInt32() >> 16 ); // Do hit test handled = true; if ( Math.Abs( this.mousePoint.Y - _wndTarget.Top ) <= this.agWidth && Math.Abs( this.mousePoint.X - _wndTarget.Left ) <= this.agWidth ) { // Top-Left return new IntPtr( (int)NativeMethods.HitTest.HTTOPLEFT ); } else if ( Math.Abs( _wndTarget.ActualHeight + _wndTarget.Top - this.mousePoint.Y ) <= this.agWidth && Math.Abs( this.mousePoint.X - _wndTarget.Left ) <= this.agWidth ) { // Bottom-Left return new IntPtr( (int)NativeMethods.HitTest.HTBOTTOMLEFT ); } else if ( Math.Abs( this.mousePoint.Y - _wndTarget.Top ) <= this.agWidth && Math.Abs( _wndTarget.ActualWidth + _wndTarget.Left - this.mousePoint.X ) <= this.agWidth ) { // Top-Right return new IntPtr( (int)NativeMethods.HitTest.HTTOPRIGHT ); } else if ( Math.Abs( _wndTarget.ActualWidth + _wndTarget.Left - this.mousePoint.X ) <= this.agWidth && Math.Abs( _wndTarget.ActualHeight + _wndTarget.Top - this.mousePoint.Y ) <= this.agWidth ) { // Bottom-Right return new IntPtr( (int)NativeMethods.HitTest.HTBOTTOMRIGHT ); } else if ( Math.Abs( this.mousePoint.X - _wndTarget.Left ) <= this.bThickness ) { // Left return new IntPtr( (int)NativeMethods.HitTest.HTLEFT ); } else if ( Math.Abs( _wndTarget.ActualWidth + _wndTarget.Left - this.mousePoint.X ) <= this.bThickness ) { // Right return new IntPtr( (int)NativeMethods.HitTest.HTRIGHT ); } else if ( Math.Abs( this.mousePoint.Y - _wndTarget.Top ) <= this.bThickness ) { // Top return new IntPtr( (int)NativeMethods.HitTest.HTTOP ); } else if ( Math.Abs( _wndTarget.ActualHeight + _wndTarget.Top - this.mousePoint.Y ) <= this.bThickness ) { // Bottom return new IntPtr( (int)NativeMethods.HitTest.HTBOTTOM ); } else { handled = false; return IntPtr.Zero; } } }
定義了 為HitTest使用的拖拽邊框厚度和拐點手柄的大小,當鼠標經過並進入定義的矩形區域內就觸發相應的HitTest.
可拖拽區域的命中范圍參考下圖

Ok, 此項技術的關鍵點只是在於你要知道 一個Windows 窗口, 在某些情況下系統都偷偷的做了些什么.哪些事情是操作系統自動做的,而我們在什么時候的情況下也可以指示OS來為我們干活.
自定義窗口最大化(位置/大小)
有些情況下,我們可能需要應用程序的窗口最大化的時候不要鋪滿全屏,比如我需要窗口的最大化固定到我的屏幕右側,或者距離屏幕頂端150px,這種情況的需求也許不多,但是有一種情況你肯定會遇到過.那么就是你自定義一個Window也實現了最大化功能.
但是你的Window你為其加入了陰影效果(啥是陰影,看上面的Blink) 由於我們的自定義窗口都是先禁用掉了系統邊框(WindowStyle.None),並且支持透明化,然后在其基礎之上進行的,你所有干的活其實都是在Window的客戶區做的.那么你如果想為你的窗
口加入陰影效果,也許你會使用Margin等來為你的陰影讓出一點顯示的空位,否則你的陰影可能看不到.那么你的窗口在最大化的時候可能就不是完全的最大化,四邊全部貼到屏幕顯示屏的工作區,也許會有留白.為了干掉這個留白就用到了我們下面說的技術.
一個窗口在最大化的時候到底最大化到多大,最大化到什么位置其實是有 MINMAXINFO 這個結構體來決定的.這個結構體內包含了最大化需要的缺省數據.
結構體:
typedef struct {
POINT ptReserved;
POINT ptMaxSize;
POINT ptMaxPosition;
POINT ptMinTrackSize;
POINT ptMaxTrackSize;
} MINMAXINFO;參數說明:
ptMaxSize: 設置窗口最大化時的寬度、高度
ptMaxPosition: 設置窗口最大化時x坐標、y坐標
ptMinTrackSize: 設置窗口最小寬度、高度
ptMaxTrackSize:設置窗口最大寬度、高度
我們知道Windows系統是以消息為基礎的系統,任何處理都有相應的WM消息.最大化也不例外 我們可以找到一個叫做 WM_GETMINMAXINFO 的消息(參見MS檔案)
So, 我們知道該怎么做了. 一句話: 抓住 WM_GETMINMAXINFO消息 然后偷偷修改 MINMAXINFO 結構體數據,搞定.
class WindowResizerImp { private const int WND_BORDER_DROPSHADOW_SIZE = 4; private int? _maxiX = null; // 窗口最大化時的左上角坐標X private int? _maxiY = null; // 窗口最大化時的左上角坐標Y private Func<int?> _maxiX_call = null; private Func<int?> _maxiY_call = null; private readonly bool _bUseCall = false; // 使用 xxx_call 還是 field,默認field private bool _bFailed = false; private Window _wndTarget = null; WindowResizerImp( Window wndTarget ) { if ( null == wndTarget ) _bFailed = true; _wndTarget = wndTarget; _wndTarget.SourceInitialized += _wndTarget_SourceInitialized; } public WindowResizerImp( Window wndTarget, Func<int?> maxiX_call = null, Func<int?> maxiY_call = null ) : this( wndTarget ) { this._maxiX_call = maxiX_call ?? ( () => null ); this._maxiY_call = maxiY_call ?? ( () => null ); _bUseCall = true; } void _wndTarget_SourceInitialized( object sender, EventArgs e ) { addHook(); } private void addHook() { if ( _bFailed ) return; HwndSource hwndSource = PresentationSource.FromVisual( _wndTarget ) as HwndSource; if ( hwndSource != null ) { hwndSource.AddHook( new HwndSourceHook( WndProc ) ); } } private IntPtr WndProc( IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled ) { switch ( msg ) { case NativeMethods.WM_GETMINMAXINFO: { WmGetMinMaxInfo( hwnd, lParam ); handled = true; } break; } return IntPtr.Zero; } private void WmGetMinMaxInfo( IntPtr hwnd, IntPtr lParam ) { NativeMethods.MINMAXINFO mmi = (NativeMethods.MINMAXINFO)Marshal.PtrToStructure( lParam, typeof( NativeMethods.MINMAXINFO ) ); IntPtr monitor = NativeMethods.MonitorFromWindow( hwnd, NativeMethods.MONITOR_DEFAULTTONEAREST ); if ( monitor != IntPtr.Zero ) { NativeMethods.MONITORINFOEX monitorInfo = new NativeMethods.MONITORINFOEX(); monitorInfo.cbSize = Marshal.SizeOf( monitorInfo ); NativeMethods.GetMonitorInfo( new HandleRef( this, monitor ), monitorInfo ); NativeMethods.RECT rcWorkArea = monitorInfo.rcWork; NativeMethods.RECT rcMonitorArea = monitorInfo.rcMonitor; //mmi.ptMaxPosition.X = ( null != this.MaxiX ? this.MaxiX.Value : Math.Abs( rcWorkArea.Left - rcMonitorArea.Left ) ) - WND_BORDER_DROPSHADOW_SIZE; //mmi.ptMaxPosition.Y = ( null != this.MaxiY ? this.MaxiY.Value : Math.Abs( rcWorkArea.Top - rcMonitorArea.Top ) ) - WND_BORDER_DROPSHADOW_SIZE; if ( !_bUseCall ) {// use field mmi.ptMaxPosition.X = ( null != this._maxiX ? this._maxiX.Value : Math.Abs( rcWorkArea.Left - rcMonitorArea.Left ) ) - WND_BORDER_DROPSHADOW_SIZE; mmi.ptMaxPosition.Y = ( null != this._maxiY ? this._maxiY.Value : Math.Abs( rcWorkArea.Top - rcMonitorArea.Top ) ) - WND_BORDER_DROPSHADOW_SIZE; } else { if ( null == this._maxiX_call ) this._maxiX_call = () => null; if ( null == this._maxiY_call ) this._maxiY_call = () => null; int? ret_x = this._maxiX_call.Invoke(); int? ret_y = this._maxiY_call.Invoke(); mmi.ptMaxPosition.X = ( null != ret_x ? ret_x.Value : Math.Abs( rcWorkArea.Left - rcMonitorArea.Left ) ) - WND_BORDER_DROPSHADOW_SIZE; mmi.ptMaxPosition.Y = ( null != ret_y ? ret_y.Value : Math.Abs( rcWorkArea.Top - rcMonitorArea.Top ) ) - WND_BORDER_DROPSHADOW_SIZE; } mmi.ptMaxSize.X = Math.Abs( Math.Abs( rcWorkArea.Right - rcWorkArea.Left ) + WND_BORDER_DROPSHADOW_SIZE - mmi.ptMaxPosition.X ); mmi.ptMaxSize.Y = Math.Abs( Math.Abs( rcWorkArea.Bottom - rcWorkArea.Top ) + WND_BORDER_DROPSHADOW_SIZE - mmi.ptMaxPosition.Y ); mmi.ptMinTrackSize.X = (int)this._wndTarget.MinWidth; mmi.ptMinTrackSize.Y = (int)this._wndTarget.MinHeight; } Marshal.StructureToPtr( mmi, lParam, true ); } }
- IntPtr monitor = NativeMethods.MonitorFromWindow( hwnd, NativeMethods.MONITOR_DEFAULTTONEAREST ); 為獲取應用程序所在屏幕的句柄
- NativeMethods.GetMonitorInfo( new HandleRef( this, monitor ), monitorInfo ); 為獲取 monitor屏幕的工作區大小等信息
- Func<int?> maxiX_call = null, Func<int?> maxiY_call = null 為方便在 自定義窗口基類中對這兩個傳入值進行Override. 如果不理解,你就當他是2個傳入的自定義的寬高int值吧.
- 由於 MINMAXINFO結構數據為非托管數據.如果我們想要得到它,我們需要調用 Marshal.PtrToStructure(...) 方法來將非托管內存塊封送到托管對象中,說白了就是從非托管拿數據來,然后存到我自己的變量中.
- 在我們重新修改數據后,怎么拿來的要怎么還回去.因此還回數據是將托管對象數據封送到指定的內存塊中 , Marshal.StructureToPtr(...). marshal 是個牛x的類,記住喲!
到此, RT中的技術點已經介紹完了.
補充一點: "小明! 在Win7+ 的系統中最大化一個窗口后,鼠標無需雙擊標題欄來取消最大化,直接按住拽下來就可以.你知道嘛?" 小明:"啥?這么容易就脫下來了啊?" "滾出去!"
補充一下自定義窗口如何實現類似默認窗口的直接拖拽窗口標題欄就可以取消最大化的功能,一句話攻略: 向OS發送WM_NCLBUTTONDOWN消息,並攜帶HTCAPTION的HitTest值即可.具體,小明你自己玩去吧!
文章中涉及的部分代碼整理, 由於代碼是我自己的UIShell ui框架中的一部分,因此暫時無法提供窗口等Controls的源碼,但是我已經免費奉送了 文章中關於這幾個技術點的類,幾個點我都是直接寫到一個類中的.可以湊合看看.
傳送門(CSDN的),你0分下載我也賺1分辛苦分: http://download.csdn.net/detail/wangye2008/8128519
/*******************************************************/
歡迎轉載!歡迎拍磚!
版權所有 © Vito野子
E-mail: vito2015@live.com
轉載請注明出處 http://www.cnblogs.com/Vito2008/p/WPF-UIShell-UIFramework-Blink-WmNCHitTest-WmGetMinMaxInfo.html
/*******************************************************/
