无论是在工作和学习中使用WPF时,我们通常都会接触到CustomControl,今天我们就CustomWindow之后的一些边角技术进行探讨和剖析。

  1. 窗口(对话框)模态闪动(Blink)
  2. 自定义窗口的四边拖拽支持
  3. 自定义窗口最大化(位置/大小)

童鞋们在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.}

哎呀,顿时感觉“高大上”起来,可以拿来跟产品经理去吹牛了。(小明:“经理经理!这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 HitTestif ( lParam.ToInt32() == 0x202fffe || lParam.ToInt32() == 0x201fffe ){// if the wnd is not activedif ( !_wndTarget.IsActive ){// we find the actived childwnd in parent's children wnds ,then blink itif ( _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>类.

WPF.UIShell UIFramework之自定义窗口的深度技术相关推荐

  1. WPF中使用WindowChrome自定义窗口中遇到的最大化问题

    FrameWork 4.5 之后,内置了WindowChrome类,官方文档: https://msdn.microsoft.com/en-us/library/system.windows.shel ...

  2. WPF绘制自定义窗口

    原文:WPF绘制自定义窗口 WPF是制作界面的一大利器,下面就用WPF模拟一下360的软件管理界面,360软件管理界面如下: 界面不难,主要有如下几个要素: 窗体的圆角 自定义标题栏及按钮 自定义状态 ...

  3. [WPF疑难] 继承自定义窗口

    [WPF疑难] 继承自定义窗口 原文 [WPF疑难] 继承自定义窗口 [WPF疑难] 继承自定义窗口 周银辉 项目中有不少的弹出窗口,按照美工的设计其外边框(包括最大化,最小化,关闭等按钮)自然不同于 ...

  4. wpf 自定义窗口,最大化时覆盖任务栏解决方案

    原文:wpf 自定义窗口,最大化时覆盖任务栏解决方案 相信很多人使用wpf时会选择自定义美观的窗口,因此会设置WindowStyle="None" 取消自带的标题栏.但这样使用 W ...

  5. WPF的消息机制(二)- WPF内部的5个窗口之隐藏消息窗口

    原文:WPF的消息机制(二)- WPF内部的5个窗口之隐藏消息窗口 版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/powertoolsteam/ar ...

  6. WPF的消息机制(三)- WPF内部的5个窗口之处理激活和关闭的消息窗口以及系统资源通知窗口...

    目录 WPF的消息机制(一)-让应用程序动起来 WPF的消息机制(二)-WPF内部的5个窗口 (1)隐藏消息窗口 (2)处理激活和关闭的消息窗口和系统资源通知窗口 (3)用于用户交互的可见窗口 (4) ...

  7. QT自定义窗口插件在QT Creator的应用

    根据<C++ GUI Programming with Qt 4,Second Edition>中第5章的"在Qt设计师中集成自定义窗口部件"小节,使用插件法生成的窗口 ...

  8. DL之DNN:基于自定义数据集利用深度神经网络(输入层(10个unit)→2个隐藏层(10个unit)→输出层1个unit)实现回归预测实现代码

    DL之DNN:基于自定义数据集利用深度神经网络(输入层(10个unit)→2个隐藏层(10个unit)→输出层1个unit)实现回归预测实现代码 目录 基于自定义数据集利用深度神经网络(输入层(10个 ...

  9. flink 自定义 窗口_《从0到1学习Flink》—— Flink Data transformation(转换)

    前言 在第一篇介绍 Flink 的文章 <<从0到1学习Flink>-- Apache Flink 介绍> 中就说过 Flink 程序的结构 Flink 应用程序结构就是如上图 ...

最新文章

  1. RDIFramework.NET ━ .NET快速信息化系统开发框架 V3.0 版新增系统参数管理
  2. 2018年4月1日 蓝桥杯 C/C++B组答案 递增三元组
  3. 详谈Windows消息循环机制
  4. Android开发常用的插件及工具
  5. 【JUC】第四章 JUC 辅助类、读写锁
  6. Linux系统C/C++通用错误码实现模板
  7. python 读取excel文件,并读成数据框格式输出
  8. 【POJ2069HDU3007】模拟退火算法之最小球/圆覆盖
  9. ubuntu mysql 升级_Ubuntu 升级mysql 之后的一些问题
  10. kubernetes docker Secret
  11. 文件服务器minio
  12. uniapp实现微信公众号支付
  13. 50 链表排序(Sort List)
  14. 微信开放平台和微信公众平台配置流程简介,
  15. 计算机如何添加gust用户,win7系统如何创建安全的Guest账户
  16. RGB颜色与颜色名称对照表
  17. 【Android】【UI】解决DialogFragment反复使用引起的并发问题和状态错误问题
  18. 小飞升值记——(21)
  19. 中秋未到却卖到断货的月饼,究竟有多好吃?
  20. css中flex: initial、flex:0、flex:1、flex:none、flex:auto的区别和使用场景

热门文章

  1. java.lang.NoSuchMethodError的解决办法
  2. 学不下去时坚持的方法
  3. 【Python】基于Python获取链家小区房价信息及其POI数据
  4. 安卓手机在fastboot模式下刷机
  5. java.net.MalformedURLException: no protocol 可能的解决方法
  6. IDEA 神级插件!效率提升 20 倍!
  7. 2022-09-01 网工进阶(二十九) DHCP-概述、工作原理、报文格式、分配IP地址顺序、地址租期与续租、中继(relay)、Snooping
  8. 网易HR告诉你关于网易招聘的那些事(上篇)
  9. python--lintcode109.数字三角形(动态规划)
  10. excel连接mysql插件_Excel插件之连接数据数据库秒数处理,办公轻松化