在上一篇《[原创]Silverlight的弹出窗口--展示》中,仅仅是将我制作的基于Silverlight的弹出窗 口作了一个功能性的展示,并提供了一个非常基础的版本的源码,
Silverlight的弹出窗口设计
。确实这个版本非常基础,其中存在着众多未经优化的代码,同时结构也存在着一定程度的混乱,因此 如果不从整体上对其进行梳理,仅仅通过阅读代码恐怕是很难了解其整个工作过程的。
此篇的目的就是,从设计的结构上,对弹出窗口这一功能作一个大致的介绍,将贯穿于整个作品的设 计思想描绘出来,而这思想也必然是整作项目中最为稳定的部分,在将来不会产生太大的改动。
源码已经有了小部分的更新,如果上一次有下载的园友,请重新下载
整体结构
首先应当从整体上对这个项目的结构有一个鸟瞰的概念,因此附上架构图一份,当然这并不是正规的 UML图,但应当足以说明不少问题
可以看到,在这个结构中,存在着6个非常重要的组件,他们分别是:
PopupService:核心组件,用于提供弹出窗口的功能,所有弹出窗口都由其进行控制
LayoutMask:位于弹出窗口下部的遮罩层,同时也将提供模态对话框打开时屏蔽下层控件的功能
PopupBox:弹出窗口的基类,与LayoutMask之间具有一定的通信和交互能力,同时提供特效等功能
BoxPage/MessagePage:PopupBox的实现,分别对应不同的功能,但基于此设计,并不存在太多与实现 相关的代码,因此不会作为重点进行讲述
DragService:管理窗口的鼠标拖动功能,从外部引入拖动功能有利于展现与功能的解耦,同时所有与 鼠标拖动相关的实现集中于一个类中,方便了BUG出现之后的定位与修改
PopupService
在我的设计中,我们不能通过直接的新建对象,如BoxPage box = new BoxPage();来生成一个弹出窗 口。
当然最初的设计确实是可以使用new来创建的,但在日后的测试中发现,这样的创建会产生很多问题, 在下面例举一二:
1. 每一个弹出窗口会拥有一个遮罩层,因此后开的弹出窗口一定比之前的在更上层,这就导致先打开 的弹出窗口永远不可能被移到最上方,而在日常操作中,点击后面的窗口应当可以将这个窗口移到最前方
2. 过多的遮罩层导致了资源大量的占用,当打开5个以上弹出窗口时,应用程序的响应将出现明显变 慢的现象
这种种缺陷,都将矛头指向了一处,即我们需要一个统一的环境来管理弹出窗口及其遮罩层,保证多 个弹出窗口所拥有的遮罩层的“单例性”,因此就产生了通过PopupService这样一个“服务”来管理的策 略,其管理方案如下:
1. 一个PopupService对应一个Control,此Control就是弹出窗口的拥有者
2. PopupService会产生一个遮罩层与此Control联系,并且在此后有弹出窗口时都会使用这唯一的一 个遮罩层
3. 无论何时,对于同一个Control,将只会有一个PopupService进行服务
看到上面的方案后,我想大多数人都会发现,PopupService变成了“伪单例”或“局部单例”的类, 因此就着“单例模式”的设计,我们肯定不能简单地通过new的构造来使用这个类,因此就引出了该类的 一个静态方法GetServiceFor,此方法接收一个参数即PopupService的拥有者,当然为了能够将遮罩层铺 在控件上,我们要求控件是一个布局控件,即Panel类型。
为了保证PopupService的伪单例的特性,必须将生成的对象保存起来,这里简单地用到了Dictionary 进行保存,当调用GetServiceFor方法时,首先在缓存池中寻找,如果找不到则调用new进行实例化,随后 放入缓存池中进行持久的保存,以保证日后不会再发生重新构造的问题,其具体代码如下:
public static PopupService GetServiceFor(Panel owner)
{
if (! Cache.ContainsKey(owner))
{
PopupService service = new PopupService (owner);
Cache[owner] = service;
}
return Cache[owner];
}
在这里并没有显式地对Cache对象(Dictionary
上面讲了PopupService的产生过程,下面例举一下PopupService提供的方法,由于方法十分简单,就 不展开讨论,其方法主要有3个:
1. GetBoxPage:获取在此PopupService管理下的BoxPage的一个对象,有重载
2. GetMessagePage:获取在此PopupService管理下的MessagePage的一个对象,有重载
3. RegisterPopupBox:对已经创建好的但没有纳入PopupService管理的PopupBox对象进行注册,注册 后的对象将由此PopupService进行管理,即分配一个LayoutMask
LayoutMask
LayoutMask听起来就叫“遮罩层”,但其实他不是一个控件,其地位类似于PopupService,是一个“ 管理者”的角色,他将管理多个PopupBox,从而将弹出窗口于PopupService分享开来,起到解耦的作用, 尽可能地减少PopupService的负担,从而使程序结构更加清晰。
而从界面的展现角度来讲,又可以认为LayoutMask确实是一个“控件”,因为他会生成一个Canvas平 铺于其所有者(Panel类型)之上,此Canvas就是真正的“遮罩层”,对Canvas设定背景色就会产生模态 对话框的效果,同时所有的弹出窗口都将作为这个Canvas的子元素,通过ZIndex的改变来确定哪一个对话 框处在最前端,从而得以模仿我们日常使用中“点击在后面的窗口之后窗口会被置于最前端”的效果,
电脑资料
《Silverlight的弹出窗口设计》(https://www.unjs.com)。LayoutMask提供了与弹出窗口的管理相关的若干方法,其主要对外的方法如下:
1. AddBox:将一个弹出窗口添加进来,当然在调用窗口的Show方法之前,窗口是不会显示出来的, AddBox只是将窗口与本体进行联系。
2. RemoveBox:与AddBox相反,将一个窗口从本容器中除去,此后即使调用窗口的Show方法,窗口也 不会再显示出来了,因此被移除的Box在XAML树中已经是一个孤岛,与XAML根没有联系的元素是不可能被 渲染的。
3. PositionBox:在上一个版本中称为CenterBox,在此版本中加入了新的功能,即连续打开窗口时, 窗口不会叠在一起,而会按一定的偏移量相互错开,因此方法也被改名为PositionBox,其作用就是找到 一个合适的位置来放置弹出窗口。
除了公开的方法之外,其部分内部方法也有着举足轻重的作用:
1. CheckModal:每当AddBox或RemoveBox调用时,都会重新检查是否有弹出窗口是模态的,如果在这 个LayoutMask管理的弹出对话框中有一个或多个是模态的,则需要将作为遮罩层使用的Canvas改为模态以 屏蔽下面的其他控件,其方式是简单地给Canvas加上背景色。
2. MaxZIndex:返回管理的所有弹出对话框的ZIndex中的最大值,这方便了在后层的对话框移到前层 ,只需要设定其ZIndex为MaxZIndex + 1即可。
3. ReorderZIndex:当然ZIndex是有最大值的,一个很熟悉的数字65535,所以如果不断地给窗口增加 ZIndex,必将导致ZIndex超出范围,这当然不是我们所希望的结果。因此就有了一个方法,当ZIndex已经 过大的时候,将所有控件的ZIndex进行重新排列,按照现有的窗口叠放次序,从1开始重新排列ZIndex, 保证ZIndex永远不会超过最大值(当然你硬要连续打开65536个窗口我也真没办法……)
4. RenderMask:当所有弹出窗口关闭时,遮罩层也应该相应消失,而当遮罩层未打开时,也不能打开 新的窗口,所以这里就有一个流程,在第一个窗口打开时需要先将遮罩层打开,因此有了RenderMask方法 ,负责将遮罩层加入到所有者的子元素中并显示出来。
DragService:
DragService是一个辅助类,他将提供窗口的鼠标拖动功能,这个类的结构也非常简单,在构造的时候 将需要窗口的对象传递给他,随后便可通过设定IsDraggable属性来打开/关闭鼠标拖动的功能,对于鼠标 拖动的实现,在网上无论是FLEX,JS,WINFORM还是Silverlight都有很多的实现了,我使用的也与这些大 同小异,主要就是通过监听MouseDown,MouseUp,MouseMove这3个事件来实现,可以看一下IsDraggable 的实现:
public bool IsDraggable
{
get
{
return m_IsDraggable;
}
set
{
if (m_IsDraggable == value)
{
return;
}
if (value)
{
EnableDrag();
}
else
{
DisableDrag ();
}
}
}
当IsDraggable被设定为true时,会调用EnableDrag方法,此方法如下:
private void EnableDrag()
{
PopupBox.DragMouseCaptureArea.MouseLeftButtonDown +=
new MouseButtonEventHandler(Drag_MouseLeftButtonDown);
PopupBox.DragMouseCaptureArea.MouseMove +=
new MouseEventHandler (Drag_MouseMove);
PopupBox.DragMouseCaptureArea.MouseLeftButtonUp +=
new MouseButtonEventHandler(Drag_MouseLeftButtonUp);
}
此方法监听了3个事件,对于拖动的逻辑,是在MouseDown的时候设定“开始拖动”,在MouseMove的时 候,如果已经“开始拖动”,则计算出鼠标移动的距离,使窗口移动同样的距离,最后在MouseUp的时候 “停止拖动”,具体代码可以在源文件中找到,不再浪费此处的空间了~~
而当IsDraggable设为false的时候,则调用DisableDrag方法,方法如下:
private void DisableDrag()
{
PopupBox.DragMouseCaptureArea.MouseLeftButtonDown -= Drag_MouseLeftButtonDown;
PopupBox.DragMouseCaptureArea.MouseMove -= Drag_MouseMove;
PopupBox.DragMouseCaptureArea.MouseLeftButtonUp -= Drag_MouseLeftButtonUp;
}
通过注销3个事件,自然也就停止了拖动的功能。
以后
下一篇将会比较详细地去讲述一下LayoutMask中的一些方法的实现,同时讲述诸如动画效果等复杂内 容的实现,最后介绍如何扩展这个框架来实现自定义的PopupBox。
本文配套源码