适用于:.net2.0+ Winform项目
有时候我们需要开一个简单的窗口来做一些事,例如输入一些东西、点选一个item之类的,可能像这样:
完了返回原窗体并获取刚刚的输入,这样做并没有什么问题,但在几天前我突然产生了一些想法:为什么非得有板有眼的弹出一个窗体给用户呢,是不是可以在按钮附近迅速呈现一个层来做这些事呢,类似快捷菜单那样,用户高兴就在里面做一下该做的事,不高兴就在其它地方点一下它就消失,本来很轻便快捷的操作,DUANG~弹出一个窗体来会不会令用户心里咯噔一下呢,感受层面的事情往往是很微妙的,不管怎样,我既然起了这个念头,just try it。
我首先找了一下现成的方案,果然在牛逼的codeproject.com已经有牛人做了这样的事情:
http://www.codeproject.com/Articles/17502/Simple-Popup-Control
简单体验了一下,的确是了不起的创造。原理是利用ToolStripControlHost可以承载自定义控件的这一能力,让下拉式控件ToolStripDropDown将任何自定义控件像右键菜单那样弹出来 (别忘了右键菜单ContextMenuStrip就是继承自ToolStripDropDown) ,这样就等于把菜单作为一个容器,可以弹出任何或简单或复杂的控件组合,同时又具有菜单具有的便捷性,召之即来挥之即去。当时了解到这方案的时候真挺开心,正是我想要的效果,感觉这下好了,不用瞎费劲自己造了。
但很快发现一个在我看来还挺在意的不足,就是ToolStripDropDown只有Show,没有ShowDialog,就是不能以模式化 (Modal,也有叫模态的,鉴于MSDN都称模式,我也随流叫它模式) 的方式弹出,这是由ToolStripDropDown的固有能力决定的,该方案既然基于ToolStripDropDown,自然也受限于此,不能模式化弹出。这样带来的问题是某些情况下的调用体验不好 (体验这种事当然不是用户才有的专利,俺们码农也是人,也要讲体验的说) ,比如弹出的控件是让用户输入一些东西,完了用户点击某个按钮什么的返回原窗体,然后在原窗体获取用户刚刚的输入,然后接着做后面的事。由于非模式的Show不会阻塞代码,所以就不能在Show的下方想当然的获取值、使用值~这是显然的。要想获得值可能就得额外采取一些做法,例如响应弹出控件的关闭事件,或者把原窗体传入弹出控件完了在后者中做原本应该在原窗体中做的事~等等,办法当然有很多,但这都是因为只能Show带来的多余的事,有什么比在一个方法中弹出控件、 等待 返回、继续处理来的爽滑的呢,像这样不是很自然吗:
string s; using (Popup p = new Popup()) { if (p.ShowDialog() != DialogResult.OK) { return; } s = p.InputText; } //go on ...
所以很遗憾,不得不挥别这个优秀的方案,造自己的轮子。不过受该方案的启发,我想到用ContextMenu来做容器 (注意这个菜单类跟上面提到的继承自ToolStripDropDown的ContextMenuStrip大大的不同,前者是OS原生的菜单,就是在桌面、图标以及文本框中右键弹出的那种菜单,.net是通过调API的方式来操作这样的菜单,而后者则完全是.net实现,更多信息请参考 MSDN ,此处不展开) ,因为ContextMenu的Show是阻塞式的,正合我意。但一番尝试之后放弃,它的菜单项MenuItem不像ToolStripItem那样可以通过ToolStripControlHost承载自定义控件,希望是我能力有限,总之我做不到把自定义控件弄到ContextMenu上,也没见过原生菜单上出现过文本框、复选框等奇怪的东西, 如果您知道怎么扩展原生菜单,还望不吝赐教,先行谢过!
我还是打回.net的主意,当中仍然是做了许多不同的尝试,Form、Panel、UserControl、ContainerControl、Control等等看起来适合做容器层的东西都试了个遍,甚至重新在ToolStripDropDown上打主意,最后选用Form,改造一番,自我感觉较理想的实现了我要的东西:一个叫做 FloatLayerBase 的基类,它本身继承自System.Windows.Forms.Form类,而需要作为浮动层显示的应用则继承自FloatLayerBase进行实现,例如下面这个接受用户输入数值的NumInputDemo实现:
不会令父窗口失去焦点(不会抢焦点的层才是好层):
当然,男人不止一面:
还有其它边框样式,有待用户自行体验,最后有demo提供。
这些都只是demo,没那么好看和强大,重点是有了这个FloatLayerBase,就可以实现自己的浮动应用。
using System; using System.ComponentModel; using System.Drawing; using System.Runtime.InteropServices; using System.Windows.Forms; namespace AhDung.WinForm.Controls { /// <summary> /// 浮动层基类 /// </summary> public class FloatLayerBase : Form { /// <summary> /// 鼠标消息筛选器 /// </summary> //由于本窗体为WS_CHILD,所以不会收到在窗体以外点击鼠标的消息 //该消息筛选器的作用就是让本窗体获知鼠标点击情况,进而根据鼠标是否在本窗体以外的区域点击,做出相应处理 readonly AppMouseMessageHandler _mouseMsgFilter; /// <summary> /// 指示本窗体是否已ShowDialog过 /// </summary> //由于多次ShowDialog会使OnLoad/OnShown重入,故需设置此标记以供重入时判断 bool _isShowDialogAgain; //边框相关字段 BorderStyle _borderType; Border3DStyle _border3DStyle; ButtonBorderStyle _borderSingleStyle; Color _borderColor; /// <summary> /// 获取或设置边框类型 /// </summary> [Description("获取或设置边框类型。")] [DefaultValue(BorderStyle.Fixed3D)] public BorderStyle BorderType { get { return _borderType; } set { if (_borderType == value) { return; } _borderType = value; Invalidate(); } } /// <summary> /// 获取或设置三维边框样式 /// </summary> [Description("获取或设置三维边框样式。")] [DefaultValue(Border3DStyle.RaisedInner)] public Border3DStyle Border3DStyle { get { return _border3DStyle; } set { if (_border3DStyle == value) { return; } _border3DStyle = value; Invalidate(); } } /// <summary> /// 获取或设置线型边框样式 /// </summary> [Description("获取或设置线型边框样式。")] [DefaultValue(ButtonBorderStyle.Solid)] public ButtonBorderStyle BorderSingleStyle { get { return _borderSingleStyle; } set { if (_borderSingleStyle == value) { return; } _borderSingleStyle = value; Invalidate(); } } /// <summary> /// 获取或设置边框颜色(仅当边框类型为线型时有效) /// </summary> [Description("获取或设置边框颜色(仅当边框类型为线型时有效)。")] [DefaultValue(typeof(Color), "DarkGray")] public Color BorderColor { get { return _borderColor; } set { if (_borderColor == value) { return; } _borderColor = value; Invalidate(); } } protected override sealed CreateParams CreateParams { get { CreateParams prms = base.CreateParams; //prms.Style = 0; //prms.Style |= -2147483648; //WS_POPUP prms.Style |= 0x40000000; //WS_CHILD 重要,只有CHILD窗体才不会抢父窗体焦点 prms.Style |= 0x4000000; //WS_CLIPSIBLINGS prms.Style |= 0x10000; //WS_TABSTOP prms.Style &= ~0x40000; //WS_SIZEBOX 去除 prms.Style &= ~0x800000; //WS_BORDER 去除 prms.Style &= ~0x400000; //WS_DLGFRAME 去除 //prms.Style &= ~0x20000; //WS_MINIMIZEBOX 去除 //prms.Style &= ~0x10000; //WS_MAXIMIZEBOX 去除 prms.ExStyle = 0; //prms.ExStyle |= 0x1; //WS_EX_DLGMODALFRAME 立体边框 //prms.ExStyle |= 0x8; //WS_EX_TOPMOST prms.ExStyle |= 0x10000; //WS_EX_CONTROLPARENT //prms.ExStyle |= 0x80; //WS_EX_TOOLWINDOW //prms.ExStyle |= 0x100; //WS_EX_WINDOWEDGE //prms.ExStyle |= 0x8000000; //WS_EX_NOACTIVATE //prms.ExStyle |= 0x4; //WS_EX_NOPARENTNOTIFY return prms; } } //构造函数 public FloatLayerBase() { //初始化消息筛选器。添加和移除在显示/隐藏时负责 _mouseMsgFilter = new AppMouseMessageHandler(this); //初始化基类属性 InitBaseProperties(); //初始化边框相关 _borderType = BorderStyle.Fixed3D; _border3DStyle = System.Windows.Forms.Border3DStyle.RaisedInner; _borderSingleStyle = ButtonBorderStyle.Solid; _borderColor = Color.DarkGray; } protected override void OnLoad(EventArgs e) { //防止重入 if (_isShowDialogAgain) { return; } //需得减掉两层边框宽度,运行时尺寸才与设计时完全相符,原因不明 //确定与ControlBox、FormBorderStyle有关,但具体联系不明 if (!DesignMode) { Size size = SystemInformation.FrameBorderSize; this.Size -= size + size;//不可以用ClientSize,后者会根据窗口风格重新调整Size } base.OnLoad(e); } protected override void OnShown(EventArgs e) { //防止重入 if (_isShowDialogAgain) { return; } //在OnShown中为首次ShowDialog设标记 if (Modal) { _isShowDialogAgain = true; } if (!DesignMode) { //激活首控件 Control firstControl; if ((firstControl = GetNextControl(this, true)) != null) { firstControl.Focus(); } } base.OnShown(e); } protected override void WndProc(ref Message m) { //当本窗体作为ShowDialog弹出时,在收到WM_SHOWWINDOW前,Owner会被Disable //故需在收到该消息后立即Enable它,不然Owner窗体和本窗体都将处于无响应状态 if (m.Msg == 0x18 && m.WParam != IntPtr.Zero && m.LParam == IntPtr.Zero && Modal && Owner != null && !Owner.IsDisposed) { if (Owner.IsMdiChild) { //当Owner是MDI子窗体时,被Disable的是MDI主窗体 //并且Parent也会指向MDI主窗体,故需改回为Owner,这样弹出窗体的Location才会相对于Owner而非MDIParent NativeMethods.EnableWindow(Owner.MdiParent.Handle, true); NativeMethods.SetParent(this.Handle, Owner.Handle);//只能用API设置Parent,因为模式窗体是TopLevel,.Net拒绝为顶级窗体设置Parent } else { NativeMethods.EnableWindow(Owner.Handle, true); } } base.WndProc(ref m); } //画边框 protected override void OnPaintBackground(PaintEventArgs e) { base.OnPaintBackground(e); if (_borderType == BorderStyle.Fixed3D)//绘制3D边框 { ControlPaint.DrawBorder3D(e.Graphics, ClientRectangle, Border3DStyle); } else if (_borderType == BorderStyle.FixedSingle)//绘制线型边框 { ControlPaint.DrawBorder(e.Graphics, ClientRectangle, BorderColor, BorderSingleStyle); } } //显示后添加鼠标消息筛选器以开始捕捉,隐藏时则移除筛选器。之所以不放Dispose中是想尽早移除筛选器 protected override void OnVisibleChanged(EventArgs e) { if (!DesignMode) { if (Visible) { Application.AddMessageFilter(_mouseMsgFilter); } else { Application.RemoveMessageFilter(_mouseMsgFilter); } } base.OnVisibleChanged(e); } //实现窗体客户区拖动 //在WndProc中实现这个较麻烦,所以放到这里做 protected override void OnMouseDown(MouseEventArgs e) { //让鼠标点击客户区时达到与点击标题栏一样的效果,以此实现客户区拖动 NativeMethods.ReleaseCapture(); NativeMethods.SendMessage(Handle, 0xA1/*WM_NCLBUTTONDOWN*/, (IntPtr)2/*CAPTION*/, IntPtr.Zero); base.OnMouseDown(e); } /// <summary> /// 显示为模式窗体 /// </summary> /// <param name="control">显示在该控件下方</param> public DialogResult ShowDialog(Control control) { return ShowDialog(control, 0, control.Height); } /// <summary> /// 显示为模式窗体 /// </summary> /// <param name="control">触发弹出窗体的控件</param> /// <param name="offsetX">相对control水平偏移</param> /// <param name="offsetY">相对control垂直偏移</param> public DialogResult ShowDialog(Control control, int offsetX, int offsetY) { return ShowDialog(control, new Point(offsetX, offsetY)); } /// <summary> /// 显示为模式窗体 /// </summary> /// <param name="control">触发弹出窗体的控件</param> /// <param name="offset">相对control偏移</param> public DialogResult ShowDialog(Control control, Point offset) { return this.ShowDialogInternal(control, offset); } /// <summary> /// 显示为模式窗体 /// </summary> /// <param name="item">显示在该工具栏项的下方</param> public DialogResult ShowDialog(ToolStripItem item) { return ShowDialog(item, 0, item.Height); } /// <summary> /// 显示为模式窗体 /// </summary> /// <param name="item">触发弹出窗体的工具栏项</param> /// <param name="offsetX">相对item水平偏移</param> /// <param name="offsetY">相对item垂直偏移</param> public DialogResult ShowDialog(ToolStripItem item, int offsetX, int offsetY) { return ShowDialog(item, new Point(offsetX, offsetY)); } /// <summary> /// 显示为模式窗体 /// </summary> /// <param name="item">触发弹出窗体的工具栏项</param> /// <param name="offset">相对item偏移</param> public DialogResult ShowDialog(ToolStripItem item, Point offset) { return this.ShowDialogInternal(item, offset); } /// <summary> /// 显示窗体 /// </summary> /// <param name="control">显示在该控件下方</param> public void Show(Control control) { Show(control, 0, control.Height); } /// <summary> /// 显示窗体 /// </summary> /// <param name="control">触发弹出窗体的控件</param> /// <param name="offsetX">相对control水平偏移</param> /// <param name="offsetY">相对control垂直偏移</param> public void Show(Control control, int offsetX, int offsetY) { Show(control, new Point(offsetX, offsetY)); } /// <summary> /// 显示窗体 /// </summary> /// <param name="control">触发弹出窗体的控件</param> /// <param name="offset">相对control偏移</param> public void Show(Control control, Point offset) { this.ShowInternal(control, offset); } /// <summary> /// 显示窗体 /// </summary> /// <param name="item">显示在该工具栏下方</param> public void Show(ToolStripItem item) { Show(item, 0, item.Height); } /// <summary> /// 显示窗体 /// </summary> /// <param name="item">触发弹出窗体的工具栏项</param> /// <param name="offsetX">相对item水平偏移</param> /// <param name="offsetY">相对item垂直偏移</param> public void Show(ToolStripItem item, int offsetX, int offsetY) { Show(item, new Point(offsetX, offsetY)); } /// <summary> /// 显示窗体 /// </summary> /// <param name="item">触发弹出窗体的工具栏项</param> /// <param name="offset">相对item偏移</param> public void Show(ToolStripItem item, Point offset) { this.ShowInternal(item, offset); } /// <summary> /// ShowDialog内部方法 /// </summary> private DialogResult ShowDialogInternal(Component controlOrItem, Point offset) { //快速连续弹出本窗体将有可能遇到尚未Hide的情况下再次弹出,这会引发异常,故需做处理 if (this.Visible) { return System.Windows.Forms.DialogResult.None; } this.SetLocationAndOwner(controlOrItem, offset); return base.ShowDialog(); } /// <summary> /// Show内部方法 /// </summary> private void ShowInternal(Component controlOrItem, Point offset) { if (this.Visible) { return; }//原因见ShowDialogInternal this.SetLocationAndOwner(controlOrItem, offset); base.Show(); } /// <summary> /// 设置坐标及所有者 /// </summary> /// <param name="controlOrItem">控件或工具栏项</param> /// <param name="offset">相对偏移</param> private void SetLocationAndOwner(Component controlOrItem, Point offset) { Point pt = Point.Empty; if (controlOrItem is ToolStripItem) { ToolStripItem item = (ToolStripItem)controlOrItem; pt.Offset(item.Bounds.Location); controlOrItem = item.Owner; } Control c = (Control)controlOrItem; pt.Offset(GetControlLocationInForm(c)); pt.Offset(offset); this.Location = pt; //设置Owner属性与Show[Dialog](Owner)有不同,当Owner是MDIChild时,后者会改Owner为MDIParent this.Owner = c.FindForm(); } /// <summary> /// 获取控件在窗体中的坐标 /// </summary> private static Point GetControlLocationInForm(Control c) { Point pt = c.Location; while (!((c = c.Parent) is Form)) { pt.Offset(c.Location); } return pt; } #region 屏蔽对本类影响重大的基类方法和属性 /// <summary> /// 初始化部分基类属性 /// </summary> private void InitBaseProperties() { base.ControlBox = false; //重要 //必须得是SizableToolWindow才能支持调整大小的同时,不受SystemInformation.MinWindowTrackSize的限制 base.FormBorderStyle = System.Windows.Forms.FormBorderStyle.SizableToolWindow; base.Text = string.Empty; //重要 base.HelpButton = false; base.Icon = null; base.IsMdiContainer = false; base.MaximizeBox = false; base.MinimizeBox = false; base.ShowIcon = false; base.ShowInTaskbar = false; base.StartPosition = FormStartPosition.Manual; //重要 base.TopMost = false; base.WindowState = FormWindowState.Normal; } //屏蔽原方法 [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("请使用别的重载!", true)] public new DialogResult ShowDialog() { throw new NotImplementedException(); } [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("请使用别的重载!", true)] public new DialogResult ShowDialog(IWin32Window owner) { throw new NotImplementedException(); } [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("请使用别的重载!", true)] public new void Show() { throw new NotImplementedException(); } [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("请使用别的重载!", true)] public new void Show(IWin32Window owner) { throw new NotImplementedException(); } //屏蔽原属性 [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("禁用该属性!", true)] public new bool ControlBox { get { return false; } set { } } [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("设置边框请使用Border相关属性!", true)] public new FormBorderStyle FormBorderStyle { get { return System.Windows.Forms.FormBorderStyle.SizableToolWindow; } set { } } [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("禁用该属性!", true)] public override sealed string Text { get { return string.Empty; } set { } } [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("禁用该属性!", true)] public new bool HelpButton { get { return false; } set { } } [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("禁用该属性!", true)] public new Image Icon { get { return null; } set { } } [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("禁用该属性!", true)] public new bool IsMdiContainer { get { return false; } set { } } [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("禁用该属性!", true)] public new bool MaximizeBox { get { return false; } set { } } [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("禁用该属性!", true)] public new bool MinimizeBox { get { return false; } set { } } [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("禁用该属性!", true)] public new bool ShowIcon { get { return false; } set { } } [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("禁用该属性!", true)] public new bool ShowInTaskbar { get { return false; } set { } } [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("禁用该属性!", true)] public new FormStartPosition StartPosition { get { return FormStartPosition.Manual; } set { } } [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("禁用该属性!", true)] public new bool TopMost { get { return false; } set { } } [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("禁用该属性!", true)] public new FormWindowState WindowState { get { return FormWindowState.Normal; } set { } } #endregion /// <summary> /// 程序鼠标消息筛选器 /// </summary> private class AppMouseMessageHandler : IMessageFilter { readonly FloatLayerBase _layerForm; public AppMouseMessageHandler(FloatLayerBase layerForm) { _layerForm = layerForm; } public bool PreFilterMessage(ref Message m) { //如果在本窗体以外点击鼠标,隐藏本窗体 //若想在点击标题栏、滚动条等非客户区也要让本窗体消失,取消0xA1的注释即可 //本例是根据坐标判断,亦可以改为根据句柄,但要考虑子孙控件 //之所以用API而不用Form.DesktopBounds是因为后者不可靠 if ((m.Msg == 0x201/*|| m.Msg==0xA1*/) && _layerForm.Visible && !NativeMethods.GetWindowRect(_layerForm.Handle).Contains(MousePosition)) { _layerForm.Hide();//之所以不Close是考虑应该由调用者负责销毁 } return false; } } /// <summary> /// API封装类 /// </summary> private static class NativeMethods { [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool EnableWindow(IntPtr hWnd, bool bEnable); [DllImport("user32.dll", CharSet = CharSet.Auto)] public static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam); [DllImport("user32.dll")] public static extern bool ReleaseCapture(); [DllImport("user32.dll", SetLastError = true)] public static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent); [DllImport("user32.dll", SetLastError = true)] private static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect); [StructLayout(LayoutKind.Sequential)] private struct RECT { public int left; public int top; public int right; public int bottom; public static explicit operator Rectangle(RECT rect) { return new Rectangle(rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top); } } public static Rectangle GetWindowRect(IntPtr hwnd) { RECT rect; GetWindowRect(hwnd, out rect); return (Rectangle)rect; } } } }FloatLayerBase.cs
在需要的地方使用它。关于使用,先看一下FloatLayerBase的部分公开成员:
//属性 public BorderStyle BorderType { get; set; } public Border3DStyle Border3DStyle { get; set; } public ButtonBorderStyle BorderSingleStyle { get; set; } public Color BorderColor { get; set; } //方法 public void Show(Control control); public void Show(Control control, Point offset); public void Show(Control control, int offsetX, int offsetY); public void Show(ToolStripItem item); public void Show(ToolStripItem item, Point offset); public void Show(ToolStripItem item, int offsetX, int offsetY); public DialogResult ShowDialog(Control control); public DialogResult ShowDialog(Control control, Point offset); public DialogResult ShowDialog(Control control, int offsetX, int offsetY); public DialogResult ShowDialog(ToolStripItem item); public DialogResult ShowDialog(ToolStripItem item, Point offset); public DialogResult ShowDialog(ToolStripItem item, int offsetX, int offsetY);
上面4个属性都是跟边框有关的,边框总共有3种形态,三维、线型、无,由BorderType指定;当为三维形态时,由Border3DStyle指定具体样式;为线型时,由BorderSingleStyle和BorderColor分别指定具体线型和颜色。原Form.FormBorderStyle属性已被屏蔽,不允许子类访问,还有若干原Form的属性也已屏蔽,原因都在源码里。另外,原Form.SizeGripStyle照常使用,是否允许调整浮动层大小就靠它了
方法就说一下Show和ShowDialog,显然分别是用来非模式化/模式化显示浮动层的,两者在调用角度的重大区别就是,前者不会阻塞代码,后者则会,实际应用中根据情况选用。每个方法从参数又分Control和ToolStripItem两类,都是代表从什么控件上弹出浮动层的意思,前者接受Button、TextBox等控件(不能传入Form,后果会不愉快),后者接受工具栏上面的项目,例如ToolStripButton、ToolStripTextBox之类的。重载可以指定相对control或item的偏移位置,默认是在control/item的下方弹出浮动层。最后无论是Show还是ShowDialog弹出来的浮动层,都可以像右键菜单那样通过在其它地方点鼠标使之消失,这里需要说明一下:
原Show()/Show(IWin32Window)和ShowDialog()/ShowDialog(IWin32Window)已被屏蔽,原因见源码。
编写期间一直使用PopupFormBase作为类名,发布最后时刻才改为现在的FloatLayerBase,所以demo中可能尚有依据原名起名的子类、方法名等。
http://pan.baidu.com/s/1mgnGPGc
里面有个Tester供您体验。
-文毕-