利用WPF创建含多种交互特性的无边框窗体
咳咳,标题一口气读下来确实有点累,让我先解释一下。另外文章底部有演示程序的下载。
本文介绍利用WPF创建一个含有以下特性的窗口:
有窗口阴影,比如QQ窗口外围只有几像素的阴影;支持透明且无边框,为了自行美化窗口通常都会想到使用无边框窗口吧;
可正常最大化,WPF无边框窗口直接最大化会直接使窗口全屏即会将任务栏一并盖住;
窗口边缘改变窗口大小,可以拖动窗口边缘改变大小;
支持等同于标题栏的全窗口空白区拖动,这一特性可以参考QQ;
支持多显示器环境。
上述针对无边框窗口的特性均可以独立实现,本文将把这些特性分开叙述。
若本文中代码段无法显示,请换一个浏览器试一下 T T
一、无边框窗口添加窗口阴影
实际上在WPF中添加无边框窗口的窗口阴影十分简单。
首先,设置WindowStyle="None"以及AllowsTransparency="True"使得窗口无边框。并对Window添加DropShadowEffect效果并设定相关参数,在这里我根据设计师的要求设置ShadowDepth="1" BlurRadius="6" Direction="270" Opacity="0.75" Color="#FF211613"。但仅仅设置这些参数是不够的,此时运行程序后无法在窗口边缘看到窗口阴影,为了让阴影显示出来,我们还需要设置BorderThickness,在这里我设置BorderThickness="7"以为阴影提供足够的空间。
看起来还不错,不是吗?不过,可不要高兴太早了……对于有需要通过拖动窗口边缘改变窗口大小和最大化窗口功能的朋友来说,这可是一个大坑,有这两个需要的朋友请继续向下看。
二、使窗口可以正常最大化
对于无边框窗口的最大化,当然就需要我们自己实现最大化和恢复窗口状态的按钮,这些都十分好实现,本文使用一个Toggle按钮来切换窗口状态。
于是现在瞬间就冒出了两个问题需要解决,解决问题的过程是曲折而艰辛的……而且就我这种文笔相信也没人能看得下去,所以我直接介绍我最后使用的处理方法。
首先我们来解决窗口最大化的问题。基本思路是用Win32API接管WM_GETMINMAXINFO消息的处理,为系统提供窗口的最大化参数。
WM_GETMINMAXINFO消息在窗口的位置或大小将要改变时被发送至窗口,消息的lParam指向了一个MINMAXINFO结构体,此结构体中的ptMaxSize和ptMaxPosition提供了窗口最大化时的大小以及位置参数。
WM_GETMINMAXINFO的参考见:http://msdn.microsoft.com/en-us/library/windows/desktop/ms632626(v=vs.85).aspx
MINMAXINFO的参考见:http://msdn.microsoft.com/en-us/library/windows/desktop/ms632605(v=vs.85).aspx
接下来要做的事情就是要想办法计算窗口最大化时的大小参数。我们想要的最大化效果是填满工作区,因此我们需要寻找一种获取工作区大小的方法。
谷歌上有很多解决这个问题的方法,不过相当一部分都是通过SystemParameters.WorkArea属性来获取工作区的大小。不过如果我们在MSDN查看这个属性的参考就会发现,使用这种方式获取的工作区大小仅仅是主显示器的工作区大小(Gets the size of the work area on the primary display monitor)。很显然如果使用这种方式,如果窗口在多屏环境下的非主屏上最大化时,显然会得到一个错误的最大化效果。
简单的方法处理不了,我们就只能再次向Win32API求助。以下是涉及到的函数:
HMONITOR MonitorFromWindow(_In_ HWND hwnd, _In_ DWORD dwFlags);
此函数可以获取一个与指定的窗口相关的显示器句柄,通过第二个参数我们可以指定值为MONITOR_DEFAULTTONEAREST来获取距离窗口最近的显示器的句柄。
参考:http://msdn.microsoft.com/en-us/library/windows/desktop/dd145064(v=vs.85).aspx
BOOL GetMonitorInfo(_In_ HMONITOR hMonitor, _Out_ LPMONITORINFO lpmi);
此函数可以获取制定显示器的相关信息,接受信息的为MONITORINFOEX结构体。MONITORINFOEX结构体中的rcWork提供了该显示器上工作区的矩形。
参考:http://msdn.microsoft.com/en-us/library/windows/desktop/dd144901(v=vs.85).aspx
http://msdn.microsoft.com/en-us/library/windows/desktop/dd145066(v=vs.85).aspx
有了这两个函数,好像我们已经能够正确的在多屏环境下获取工作区大小了。不过其实这里还有一个潜在的问题,假如用户设置过系统的DPI参数,通过这种方式获取到的工作区大小与使用DPI换算过后的工作区尺寸并不相同,这就会导致最大化时再次出现错误。为了解决这个问题,我们还得引入一些方法使得这个尺寸DPI无关。
HwndTarget.TransformFromDevice属性提供了一个矩阵,通过这个矩阵可以将设备坐标变换为渲染坐标。
HwndTarget可以通过HwndSource.CompositionTarget属性获取。
将我们获取到的显示器工作区大小用获取到的矩阵进行变换,我们就可以得到一个DPI无关的工作区大小。
至此,我们解决第一个问题的思路就已经走通了,下面是实现代码。
由于涉及到的Win32函数略多,因此我们将所涉及到的Win32API内容放到一个独立的Win32类中。
Win32.cs
public MainWindow() { InitializeComponent(); this.SourceInitialized += MainWindow_SourceInitialized; this.StateChanged += MainWindow_StateChanged; this.MouseLeftButtonDown += MainWindow_MouseLeftButtonDown; } void MainWindow_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { if (e.OriginalSource is Grid || e.OriginalSource is Border || e.OriginalSource is Window) { WindowInteropHelper wih = new WindowInteropHelper(this); Win32.SendMessage(wih.Handle, Win32.WM_NCLBUTTONDOWN, (int)Win32.HitTest.HTCAPTION, 0); return; } }
至此,我们所有的功能都已经实现。效果图由于不太好截取,暂时先不放啦~个人水平有限,感谢您能读完我的文章,欢迎各位在此交流。
另附代码下载:http://pan.baidu.com/s/1zLn3R
演示程序(.NET 3.5):http://pan.baidu.com/s/1CC0XA