【WPF】用CustomControl打造模拟的Window

【重要】代码有误,我已经更新了

 

可能有时候会有这样的需求,我们的应用程序需要弹出一个窗口,或者是包含多个窗口。同时呢,又不想真正的用Window,尤其是当我们写XBAP应用的时候。恰巧WPF里面又没有MDI……

    当然,我们有几种解决办法。

    一种比较简单的办法是,用UserControl仿造一个窗口放在应用程序里面,然后将Visibility设置为隐藏。接着,在我们需要的时候,让它显示。但是,这种方式,一个两个还好说,如果稍微一多,那管理起来就比较麻烦了。

    另外我们还可以启动一个真实的窗口,然后通过调用API,来SetParent,把子窗口放在父窗口中。但是如果是Xbap,可能这些操作都要受到限制。而且这样的窗口要改改样式很困难。

     有没有别的方法来在应用程序内部仿造窗口呢?

     在这篇文章里面,我们就来试试怎么用CustomControl来打造模拟的窗口。

    上图先:

    首先是一般的情况:

   

   

    然后是最大化的情况:

   

    XBAP程序(需要改改安全策略)

 

    最小化没做,因为现在还没想清楚最小化后怎么放这些窗口。

    当然,这些“窗口”都是可以拖来拖去的,而且可以任意改变大小。

【分析】

    其实要模拟一个窗口的外观并不困难。难点在于对窗口的操作上,最主要的包括移动,修改大小,最大最小化,关闭。

    对于最大最小化这样的操作,我们可以用Command来完成。但是鼠标拖拽移动、改变大小,是跟UI关系很紧密的操作,而CustomControl最主要的特点是UI和逻辑的解耦。WPF中提供Thumb来做拖拽的工作,那么算一下总共有多少这东西吧:移动一个,四周的Resize和四角的Resize,总共9个。我可不想我的做的这个控件上标记着一堆TemplatePart。

    所以,我决定从Thumb继承,写两个东西,一个叫Repostioner,用来改变位置;一个叫Resizer,用来改变大小。而是使用它们的时候,我们只需要把这两个控件放到某个控件中,指定一下要操作的对象,然后这个控件就能拖拖拽拽了。

【控件的实现】

Repositioner

在WPF里面,切记没有横坐标、纵坐标这种东西(即使是Canvas,那也是个附加属性),如果我们想改变某个元素的位置,最好的方法是用TranslateTransform。

所以,在指定了Repositioner要操作的对象之后,我们需要给它添加一个TranslateTransform

Code

那么,在我们拖拽Repostioner的时候,就可以通过这个Transform来改变控件的位置了。

Code

    注意,Thumb里面没有OnDragXXX的方法来重载,所以我只能做事件响应,并且以虚方法的方式提供,供子类重载。这会在后面的Resizer中看到。

除此之外,我还让Repostioner实现了ICommandSource接口,这是一个伏笔,因为我们会需要一个“双击最大化/恢复”窗口大小的操作,而这个操作通过Command来触发。

Code

    在上面的代码中,我让Repostioner双击的时候触发Command,而Command的执行是通过CommandHelper来调用的。

CommandHelper

Code

Resizer

Resizer直接从Repostioner继承。这是因为,在改变大小的时候,可能需要改变位置。

比如,向左拖拽着改变大小的时候,我们需要把控件的位置相应地往左移动,才能保证右边沿不动。

Code

     需要注意的是,虽然总共有8个Resize的方向,但是只有上下左右四个方向是最基本的,而左上,右上,左下,右下是这个四个基本方向的组合。所以,我们只需要处理四个方向的逻辑即可。但是在Xaml里面,我们没法写诸如"Top | Left"这样的“或”操作的表达式,因此,ResizerDirection这个枚举类型还是要8个值的。为了简化处理,我将这个枚举类型标记为[Flags],并且附上了初值,组合值恰好等于基本值取或。这样,在处理代码中,我们把Direction的值分别同四个基本方向的值按位求与,只要不等于0,那就表示在这个方向上发生了变化。(一般情况下,Left和Right是不会同时变化,当然,如果使用Surface这种可以多点触发的触摸屏技术,你就可以用手拉着窗口的左右两边拖放了,嘿嘿)

     Resizer的鼠标是在Style中定义,一共写个8个Trigger,虽然用代码会简单一些,但是我认为这是属于UI层的东西,还是放到Style去描述比较好,因为使用者可能想换成别的鼠标样式。

测试Repostioner和Resizer

好,现在我们来试试写好的这两个东东能不能用吧。我们放个Border,在放个Grid来装Repostioner和Resizer。

Code

神奇的事情发生了!在应用的时候,我们没写一句代码,只是简单地将Repostioner和Resizer放到Grid里面,指定他们的目标元素是TestBorder,这个Border就能拖来拖去,并且任意改变大小了!

图中,灰色部分是Repostioner,红色的是四个边角Resizer,黑色的是边线Resizer,蓝色的是Border。怎么样?已经有点Window的样子了吧?

注意,Repostioner和Resizer不一定非得放到目标元素中去,你甚至可以放到外面,只需要指定目标即可。(你可以画一个模拟的笔记本触摸板了,呵呵)

 

VirtualWindow

现在我们着手做模拟的窗口。

首先,我们添加一个自定义控件,添加一些跟Window相关的DependencyProperty,这个过程是很机械化,代码就不粘贴。

比较重要的地方是在VirtualWindow中定义的Command和它状态改变的逻辑。

Code

我在VirtualWindow的内部定义了一个WindowOperationCommand类,实现了ICommand接口,而不是使用RoutedCommand。它通过一个OperationCommand属性暴露给外面。一般轻量的Command这样写也就足够了,我也就懒得再去注册RoutedCommand,然后再注册CommandBinding了。

这个命令是通过传入不同的参数来执行相应的操作的。

最大化,和恢复状态还是比较好做的。有一点麻烦的是怎么对齐。研究了半天发现用VisualTreeHelper.GetOffset能拿到相对位移,我们再给他一个TranslateTranform让它反向移动相应的距离即可。

 

Code

但是,需要特别注意的是,这个代码的结果跟具体的Panel有关系,因为不同的Panel会有不同的Arrange方式来布局Child,可能最后布局的结果跟我们的期望的相去甚远。

同样,父Panel的布局方式还会影像到Resizer。比如说,我们放到Grid中,当我们向右拖动右边沿的Resizer的时候,我们期望的结果是控件不动,宽度向右变宽。然后,由于Grid对于Alignment设置为Stretch的Child,会按照位移加上Margin的最终值来重新布局,因此我们会看到,该控件同时会往左发生移动。同时,由于Panel的Messure方法会影响到Child的RenderSize,所以最后可能会看到VirtualWindow被截掉一部分。所以,虽然理论上VirtualWindow了可以放到任何Panel(甚至任何控件)中,但我还是建议使用的时候大家放到Canvas里面,因为Canvas的定位是绝对定位的。目前就我测试的情况来看,当VirtualWindow的Alignment分别设置为Top和Left之后,可以在StackPanel中和不设置Row和Column的Grid中正常使用。另外,我重载了Alignment的初始值,分别设为了Left和Top。

 

VirtualWindow的模板比较复杂,我就不贴上来了,下载代码后可以自己看。因为是自定义控件,所以它的模板可以随便改,这样,修改样式的工作会很简单。

 

【写在最后】

 

VirtualWindow的最小化功能我还没做,因为没想好应该是个什么效果。我在考虑要不要搞个VirtualDesktop来管理这些窗口,这样还可以提供最小化的支持。当然,这些Window都是跑在一个线程中的,如果一个Window死掉,别的Window也挂了。为此,必须让非UI代码跑在不同的线程中,所以VirtualDesktop会是个相当艰巨的任务。

可能实际项目中这些东东都用不到,不过做这些东西乐趣多多,呵呵。

 

代码下载https://files.cnblogs.com/RMay/RMay.VirtualWindows.rar

原文地址:https://www.cnblogs.com/RMay/p/1278842.html