文章目录
系统模态对话框的限制
Window系统提供的模态对话框使用范围很广泛,实用性很强。用于用户输入界面数据的逻辑正确性约束有很好的效果,便于模块化分工设计。但系统模态对话框有个严重的限制,就是模态对话框界面的输入不能再弹出模态对话框。
一、突破限制功能的设计思路
要突破模态对话框的功能限制,得先复制模态对话框的运行逻辑。百度搜了一下,系统实现模态对话框是基于底层Win32 API功能,阻塞当前应用程序非模态对话框窗口注册的消息列队,实现界面元素只能响应模态对话框的消息。微软编程从Win32 API,MFC,.NET WinForm,WPF逐步进化框架,我也不清楚WIN32 API的运行基础,重新学习30年前的老框架事倍功半。我们可以利用WPF中丰富多姿的界面设计各种功能的模态对话框。实现功能有几点:(一)阻塞用户操作非模态对话框的UI元素;(二)实现OK按钮的返回机制;(三)实现模态中打开模态对话框的机制。(四)封装模态对话框操作逻辑,方便继承。
二、实现步骤
1.操作逻辑和接口的实现
第一步是操作逻辑的实现,查找当前窗口的最高ZIndex,把模态对话框的遮掩板(Rectangle ModelDialogPanel)的ZIndex+1,表面设置为半透明,调整大小覆盖整个窗体,实现阻塞非模态对话框区域的用户输入。上述功能由子控件ModelDialogPanel实现。
第二步是模态对话框“确定/取消”按钮的调用和回调机制。由Show(...,object param)方法代入调用时参数,用于标识点确定按钮触发OkEvent事件时,明确由哪段代码发出的调用回调(有关委托是ModelDialogBaseCallBackEventHandler,类是 ModelDialogBaseEventArgs) 。派生类不用实现按钮成员和事件的绑定,这工作都交给Show方法来完成。
最后,在第四点功能设计要求下,封装这些逻辑逻辑行为为一个抽象类ModelDialogBase,子控件由派生类实例化,有关抽象类ModelDialogBase的代码如下:
/// <summary>
/// 实现模态对话框事件逻辑。返回具体值由实现(IModelDialogFunctionalRealization)SpecificControl成员
/// </summary>
public abstract class ModelDialogBase : UserControl
{
public event ModelDialogBaseCallBackEventHandler CancelEvent;
public event ModelDialogBaseCallBackEventHandler OkEvent;
#region 遮掩板的颜色
/// <summary>
/// 遮掩板的颜色
/// </summary>
public Color ModelPanelColor { get; set; } = (Color)ColorConverter.ConvertFromString("#33000000");
#endregion
#region 派生类需要实现的方法和属性
/// <summary>
/// 派生类中具体的OK按钮
/// </summary>
protected virtual Button OkButton { get; }
/// <summary>
/// 派生类中具体的Cancel按钮
/// </summary>
protected virtual Button CancelButton { get; }
/// <summary>
/// 遮挡窗体的透明Panle,该属性由子类实现,
/// </summary>
protected virtual Rectangle ModelDialogPanel { get; }
/// <summary>
/// 具体功能的控件
/// </summary>
protected virtual IModelDialogControl SpecificControl { get; }
#endregion
/// <summary>
/// 在Show方法中赋值
/// 在子类或者调用窗体中Loaded事件中赋值,该值只有在Loaded之后才能找到窗体.
/// </summary>
public virtual Window ParentWindow
{
get; private set;
}
/// <summary>
/// 显示模态对话Panel
/// </summary>
/// <param name="param">响应事件Ok Cancel中接收触发位置标记</param>
public virtual void Show(Window parentWindow, object param)
{
if (!this.IsLoaded) return;
SpecificControl.BeforeShow(); //具体功能控件执行界面调整
if (ParentWindow == null) //实现按钮事件和ParentWindow绑定
{
ParentWindow = Common.WpfWindowHelper.GetParentWindow(this);//利用VisualTreeHelper获得父窗体
OkButton.Click += this.ClickOk;
CancelButton.Click += this.ClickCancel;
}
this.showParam = param;
/*
1 设置ModelDialogPanel的大小为父窗体的实际大小
2 设置ModelDialogPanel.Fill带透明灰度SolidColorBrush
3 设置ModelDialogPanel的Panel.ZIndex为窗体中 1+最大,排除this。
4 显示,功能控件和Ok,Cancel按钮提升zindex到ModelDialogPanel之上
*/
//1 设置ModelDialogPanel的大小为父窗体的实际大小
this.Width = this.ModelDialogPanel.Width = ParentWindow.ActualWidth;
this.Height = this.ModelDialogPanel.Height = ParentWindow.ActualHeight;
//2 设置ModelDialogPanel.Fill带透明灰度SolidColorBrush
if (ModelDialogPanel.Fill == null)
ModelDialogPanel.Fill = new SolidColorBrush(ModelPanelColor);
else
((SolidColorBrush)ModelDialogPanel.Fill).Color = ModelPanelColor;
//3 设置ModelDialogPanel的Panel.ZIndex为窗体中 1 + 最大,排除this。
int maxZIndex = (int)WpfWindowHelper.GetMaxAttachedPropertyValue(ParentWindow, Panel.ZIndexProperty, this);
int thisZindex = maxZIndex + 1; //本模态控件的ZIndex须要其他所有控件的最大ZIndex+1
setZIndex(thisZindex);
//调整位置 使具体功能子控件在窗口的中间显示
Point ControlComPoint = new Point();
ControlComPoint.X = (ParentWindow.ActualWidth - SpecificControl.Width) / 2;
ControlComPoint.Y = (ParentWindow.ActualHeight - SpecificControl.Height) / 2;
//功能成员控件的具体位置和 OkButton\CancelButton的位置
VisualTreeHelper.GetParent(SpecificControl).SetValue(Canvas.TopProperty, ControlComPoint.Y);
VisualTreeHelper.GetParent(SpecificControl).SetValue(Canvas.LeftProperty, ControlComPoint.X);
//MessageBox.Show(string.Format("ModelDialogPanel.ZIndex:{0} ;this.ZIndex:{1}", Panel.GetZIndex(ModelDialogPanel), Panel.GetZIndex(this)));
this.Visibility = Visibility.Visible;
}
/// <summary>
/// 设置本控件的ZIndex为zindex 和子控件的ZIndex为zindex+1
/// </summary>
private void setZIndex(int zindex)
{
ModelDialogPanel.SetValue(Panel.ZIndexProperty, zindex);
this.SetValue(Panel.ZIndexProperty, zindex);
OkButton.SetValue(Panel.ZIndexProperty, zindex);
CancelButton.SetValue(Panel.ZIndexProperty, zindex);
//功能控件和Ok,Cancel按钮提升zindex到ModelDialogPanel之上
SpecificControl.SetValue(Panel.ZIndexProperty, zindex + 1);
VisualTreeHelper.GetParent(SpecificControl).SetValue(Panel.ZIndexProperty, zindex + 1);
}
/// <summary>
/// 从show方法传递到OkEvent的参数
/// </summary>
private object showParam;
/// <summary>
/// 实现IModelDialog.Close,触发实现IModelDialog.OK,实现IModelDialog.Cancel,关闭模态对话框
/// </summary>
private void Close()
{
if (!this.IsLoaded) return;
// ZIndex 重置为所有控件中最小
setZIndex(int.MinValue);
this.Visibility = Visibility.Collapsed;
}
/// <summary>
/// 点击Ok按钮时调用,在子类的OKButton.Click调用
/// </summary>
protected void ClickOk(object sender, RoutedEventArgs e)
{
if (!this.SpecificControl.VaildateForOk()) return;
if (OkEvent != null)
{
#region 事件参数
ModelDialogBaseEventArgs eventArg = new ModelDialogBaseEventArgs(e.RoutedEvent)
{
ShowParam = this.showParam,
DialogResult = System.Windows.Forms.DialogResult.OK,
Handled = false,
Source = this,
ReturnValue = SpecificControl.ReturnValue
};
#endregion
this.Dispatcher.Invoke(OkEvent, this, eventArg); //调用窗体类实现的OkEvent事件
this.showParam = null; //重置showParam,阻止下轮触发获得错误信息
eventArg.Handled = true; //模态对话框事件不需要冒泡
}
this.Close();
}
protected void ClickCancel(object sender, RoutedEventArgs e)
{
if (CancelEvent != null)
{
ModelDialogBaseEventArgs eventArg = new ModelDialogBaseEventArgs(e.RoutedEvent)
{
ShowParam = this.showParam,
DialogResult = System.Windows.Forms.DialogResult.Cancel,
Handled = false,
Source = this,
ReturnValue = null,
};
this.Dispatcher.Invoke(OkEvent, this, eventArg);
this.showParam = null; //重置showParam,阻止下轮触发获得错误信息
eventArg.Handled = true;
}
this.Close();
}
}
/// <summary>
/// 自定义模式对话框回调事件委托声明
/// </summary>
public delegate void ModelDialogBaseCallBackEventHandler(object sender, ModelDialogBaseEventArgs e);
/// <summary>
/// 自定义模式对话框回调事件委托参数类
/// </summary>
public class ModelDialogBaseEventArgs : RoutedEventArgs
{
/// <summary>
/// 调用Show时携带的信息,在OkEvent中区分触发该事件的信息
/// </summary>
public object ShowParam;
public System.Windows.Forms.DialogResult DialogResult;
/// <summary>
/// 对话框返回的具体对象
/// </summary>
public object ReturnValue;
public ModelDialogBaseEventArgs(RoutedEvent e)
{
this.RoutedEvent = e;
}
}
public partial class WpfWindowHelper
{
public WpfWindowHelper(Window window)
{ this.Window = window; }
public static T GetParent<T>(object dependencyObject)
{
if (dependencyObject == null) return default(T);
if (!(dependencyObject is DependencyObject)) return default(T);
do
{
dependencyObject = VisualTreeHelper.GetParent(dependencyObject as DependencyObject);
}
while (!(dependencyObject is T)); //如果父容器不是可呈现元素,继续向上查找
return (T)dependencyObject;
}
/// <summary>
/// WpfWindowHelper所依附的Window
/// </summary>
public Window Window { get; set; }
/// <summary>
/// 获得UIElement的父窗体
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public static Window GetParentWindow(UIElement obj)
{
if (!(obj is Window))
{
do
obj = (UIElement)VisualTreeHelper.GetParent(obj);
while (!(obj is Window));
}
return (Window)obj;
}
/// <summary>
/// 取得窗体中某个最大的AttachedProperty值
/// </summary>
/// <param name="obj"></param>
/// <param name="attachedPropertyCode"></param>
/// <param name="withOutObj">排除比较的控件</param>
/// <returns></returns>
public static double GetMaxAttachedPropertyValue(DependencyObject obj, DependencyProperty attachedProperty, DependencyObject withOutObj)
{
double maxValue = 0;
if (obj == withOutObj) //阻止自己比较自己,返回值不断自增,但可以子元素必需轮询。
maxValue = double.MinValue;
else
maxValue = Math.Max(obj.GetValue(attachedProperty) == null ? 0 : (int)obj.GetValue(attachedProperty), maxValue);
int count = VisualTreeHelper.GetChildrenCount(obj);
for (int i = 0; i < count; i++)
{
DependencyObject child = (DependencyObject)VisualTreeHelper.GetChild(obj, i);
if (VisualTreeHelper.GetChildrenCount(child) > 0)
maxValue = Math.Max(maxValue, GetMaxAttachedPropertyValue(child, attachedProperty, withOutObj));
}
return maxValue;
}
public static double GetMaxAttachedPropertyValue(DependencyObject obj, DependencyProperty attachedProperty)
{
return GetMaxAttachedPropertyValue(obj, attachedProperty, null);
}
/// <summary>
/// 取得窗体中某个最大的AttachedProperty值
/// </summary>
/// <param name="attachedPropertyCode"></param>
/// <returns></returns>
public double GetMaxAttachedPropertyValue(DependencyProperty attachedProperty)
{
if (this.Window == null) throw new Exception("GetMaxAttachedPropertyValue:WpfWindowHelper.Window为Null!");
return GetMaxAttachedPropertyValue(this.Window, attachedProperty);
}
/// <summary>
/// 取得窗体中某个最小的AttachedProperty值
/// </summary>
/// <param name="obj"></param>
/// <param name="attachedPropertyCode"></param>
/// <returns></returns>
public static double GetMinAttachedPropertyValue(DependencyObject obj, DependencyProperty attachedProperty, DependencyObject withOutObj)
{
double minValue = 0;
if (obj == withOutObj) //阻止自己比较自己,返回值不断自增,但可以子元素必需轮询。
minValue = double.MaxValue;
else
minValue = Math.Min(obj.GetValue(attachedProperty) == null ? 0 : (int)obj.GetValue(attachedProperty), minValue);
int count = VisualTreeHelper.GetChildrenCount(obj);
for (int i = 0; i < count; i++)
{
DependencyObject child = (DependencyObject)VisualTreeHelper.GetChild(obj, i);
if (VisualTreeHelper.GetChildrenCount(child) > 0)
minValue = Math.Min(minValue, GetMinAttachedPropertyValue(child, attachedProperty, withOutObj));
}
return minValue;
}
public static double GetMinAttachedPropertyValue(DependencyObject obj, DependencyProperty attachedProperty)
{
return GetMinAttachedPropertyValue(obj, attachedProperty, null);
}
/// <summary>
/// 取得窗体中某个最小的AttachedProperty值
/// </summary>
/// <param name="attachedPropertyCode"></param>
/// <returns></returns>
public double GetMinAttachedPropertyValue(DependencyProperty attachedProperty)
{
if (this.Window == null) throw new Exception("GetMaxAttachedPropertyValue:WpfWindowHelper.Window为Null!");
return GetMinAttachedPropertyValue(this.Window, attachedProperty);
}
}
ModelDialogBase的派生类须要把下列成员实例化,就能实现最简单的对话框显示,获得回调信息的功能。WPF中继承自定义的UserControl,需要在派生类实现加载BAML资源(XAML文件编译后)的功能,比较繁琐增加学习门槛,在这里我把ModelDialogBase抽象,实现逻辑动作,但UI成员必须在派生类中实例化,绕过加载BAML的门槛。
#region 派生类需要实现的方法和属性
/// <summary>
/// 派生类中具体的OK按钮
/// </summary>
protected virtual Button OkButton { get; }
/// <summary>
/// 派生类中具体的Cancel按钮
/// </summary>
protected virtual Button CancelButton { get; }
/// <summary>
/// 遮挡窗体的透明Panle,该属性由子类实现,
/// </summary>
protected virtual Rectangle ModelDialogPanel { get; }
/// <summary>
/// 具体功能的控件
/// </summary>
protected virtual IModelDialogControl SpecificControl { get; }
#endregion
2. ColorChooser的实例设计和调用(具体功能用户控件)
上一节实现了模态对话框的基本显示和回调事件机制,接下来就是实现模态对话框的具体功能了。ModelDialogBase中多次调用成员 IModelDialogControl SpecificControl的方法,SpecificControl的派生类必须实现一些方法,那么我们可以把SpecificControl的共同方法抽象为接口IModelDialogControl.但因为ModelDialogBase需要调用到IModelDialogControl的Width,Height,设置Canvas.TopProperty属性,这些使IModelDialogControl必须是一个UserControl对象才行,所以把IModelDialogControl设计为一个继承自UserControl不含XAML资源的抽象类(代码如下):
/// <summary>
/// 实现该接口,作为ModelDialogBase派生类调用的具体功能自定义控件。
/// </summary>
public abstract class IModelDialogControl:UserControl
{
/// <summary>
/// 返回值
/// </summary>
public virtual object ReturnValue { get; }
/// <summary>
/// 点击Ok按钮返回前的界面验证
/// </summary>
/// <returns></returns>
public virtual bool VaildateForOk() { return true; }
/// <summary>
/// Show执行前的界面调整
/// </summary>
public virtual void BeforeShow() { }
}
ColorChooser的具体功能实现颜色选择,我们可以从网上找一段现成的代码。创建自定义用户控件,修改继承和XAML文件中的名字空间映射
/// <summary>
/// ColorChooser.xaml 的交互逻辑
/// </summary>
public partial class ColorChooser :IModelDialogControl
{
#region 覆盖 IModelDialogControl 虚成员
//原来返回Color类型,发现返回颜色值经常要判断被赋值变量是否有画刷,不如直接返回画刷
public override object ReturnValue {get{return Rect.Fill.CloneCurrentValue(); }}
public override bool VaildateForOk()
{
/*不需要对界面进行数据验证,直接返回True*/
return true;
}
public override void BeforeShow()
{
/*不需要调整界面 空函数*/
}
#endregion
public ColorChooser()
{
InitializeComponent();
Rect.Fill = new SolidColorBrush(ColorValue);
}
/// <summary>
/// 控件返回的颜色
/// </summary>
public Color ColorValue;
private void Slider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
Slider s = sender as Slider;
switch (s.Name)
{
case "RedSlider":
ColorValue.R = (byte)s.Value;
break;
case "BlueSlider":
ColorValue.B = (byte)s.Value;
break;
case "GreenSlider":
ColorValue.G = (byte)s.Value;
break;
default: //Alpha
ColorValue.A = (byte)s.Value;
break;
}
SolidColorBrush bursh = Rect.Fill as SolidColorBrush;
bursh.Color = ColorValue;
}
}
ColorChooser.xaml文件如下:
<common:IModelDialogControl x:Class="HelloWorld.MyWPFControls.ColorChooser"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:HelloWorld.MyWPFControls"
xmlns:common="clr-namespace:Common"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="360" Height="300" Width="360">
<UserControl.Resources>
<Style TargetType="Slider">
<Setter Property="Maximum" Value="255"/>
<Setter Property="Minimum" Value="0" />
<Setter Property="Margin" Value="2,2,2,2"/>
</Style>
<Style TargetType="TextBlock">
<Setter Property="HorizontalAlignment" Value="Center"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
</UserControl.Resources>
<Grid Background="White">
<Grid.RowDefinitions>
<RowDefinition MaxHeight="40" MinHeight="30"/>
<RowDefinition MaxHeight="40" MinHeight="30"/>
<RowDefinition MaxHeight="40" MinHeight="30"/>
<RowDefinition MaxHeight="40" MinHeight="30"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition MaxWidth="60" MinWidth="60"/>
<ColumnDefinition MaxWidth="280" MinWidth="280"/>
</Grid.ColumnDefinitions>
<TextBlock Text="红" Grid.Row="0" Grid.Column="0"/>
<TextBlock Text="蓝" Grid.Row="1" Grid.Column="0"/>
<TextBlock Text="绿" Grid.Row="2" Grid.Column="0"/>
<TextBlock Text="透明度" Grid.Row="3" Grid.Column="0"/>
<Slider x:Name="RedSlider" Grid.Row="0" Grid.Column="1" ValueChanged="Slider_ValueChanged"></Slider>
<Slider x:Name="BlueSlider" Grid.Row="1" Grid.Column="1" ValueChanged="Slider_ValueChanged" ></Slider>
<Slider x:Name="GreenSlider" Grid.Row="2" Grid.Column="1" ValueChanged="Slider_ValueChanged"></Slider>
<Slider x:Name="AlphaSlider" Grid.Row="3" Grid.Column="1" ValueChanged="Slider_ValueChanged" ToolTip="0:完全透明,255:完全不透明。"></Slider>
<Rectangle x:Name="Rect" Grid.Row="4" Grid.Column="1" HorizontalAlignment="Center" Width="200" Height="138" Margin="14,1,67,1"/>
</Grid>
</common:IModelDialogControl>
注意XAML文件第一行,需要把原来的UserControl 更改为common:IModelDialogControl,并增加一行映射common命名空间 xmlns:common="clr-namespace:Common",原因是我把抽象类都写在Common名字空间里。如果你把IModelDialogControl,ModelDialogBase放在其它空间名下,请记得修改。
这一节就是把模态对话框的接口功能套上去,各位朋友可以把自己做的自定义的用户控件修改继承类,实现ReturnValue虚成员get。原来控件功能的调用代码根本不用修改,又可进一步当作模态对话框使用。
3.ColorChoserModelDialog的实例设计和调用(封装ColorChooser)
上两节搭好了框架(ModelDialogBase)和组件(IModelDialogControl),这一节就是把它们组合起来,封装成为一个模态对话框。组合的代码很简单。
public partial class ColorChooserModelDialog : ModelDialogBase
{
public ColorChooserModelDialog()
{
InitializeComponent();
}
protected override Rectangle ModelDialogPanel
{ get { return this.modelDialogPanel; } }
protected override Button OkButton
{ get { return this.btnOk; } }
protected override Button CancelButton
{ get { return this.btnCancel;} }
protected override IModelDialogControl SpecificControl
{ get {return this.ColorChooser;} }
}
<common:ModelDialogBase x:Class="HelloWorld.MyWPFControls.ColorChooserModelDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:HelloWorld.MyWPFControls"
xmlns:common="clr-namespace:Common"
mc:Ignorable="d" Height="285" Width="340">
<Canvas Margin="0" Background="{Binding Path=Background,ElementName=ColorChooser}">
<Rectangle x:Name="modelDialogPanel" />
<Grid Width="345">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<local:ColorChooser x:Name="ColorChooser" Grid.Row="0" />
<WrapPanel HorizontalAlignment="Right" Grid.Row="1">
<Button x:Name="btnOk" Content="确定" Margin="0,5,20,5" />
<Button x:Name="btnCancel" Content="取消" Margin="0,5,20,5" />
</WrapPanel>
</Grid>
</Canvas>
</common:ModelDialogBase>
就是在新的用户控件(ColorChooserModelDialog)中,实例化4个ModelDialogBase的虚成员。因为涉及到控件位置调整代码,所以用Canvas作为容器。
调用代码更简单,TestWindow窗体
<Window xmlns:MyWPFControls="clr-namespace:HelloWorld.MyWPFControls" x:Class="HelloWorld.TestWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:HelloWorld"
mc:Ignorable="d"
Title="TestWindow" Height="500" Width="609" AllowsTransparency="True" WindowStyle="None" x:Name="me" Loaded="me_Loaded">
<Window.Resources>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Button Content="颜色选择" Grid.Row="1" Grid.Column="0" Click="Button_Click" Width="100" Height=" 20" ></Button>
<Button Grid.Row="1" Grid.Column="1" Click="Button_Click" Content="画刷选择" Width="100" Height=" 20"></Button>
<!-- Grid.Row="0" Grid.Column="0" Grid.RowSpan="2" Grid.ColumnSpan="2" -->
<Canvas>
<MyWPFControls:ColorChooserModelDialog x:Name="ColorChooserDialog" Visibility="Collapsed" OkEvent="ColorChooserDialog_OkEvent" />
<!--还没实现 BrushChooserModelDialog 先注销-->
<!--MyWPFControls:BrushChooserModelDialog x:Name="BrushChooserDialog" Visibility="Collapsed" OkEvent="BrushChooserDialog_OkEvent"/-->
</Canvas>
</Grid>
</Window>
private void Button_Click(object sender, RoutedEventArgs e)
{
Button btn = (Button)sender;
if (btn.Content.ToString() == "颜色选择")
ColorChooserDialog.Show(this, btn.Content);
/* 还没实现BrushChooserDialog,现把这段注销
else
this.BrushChooserDialog.Show(this, btn.Content);
*/
}
private void ColorChooserDialog_OkEvent(object sender, ModelDialogBaseEventArgs e)
{
/* 还没实现BrushChooserDialog,现把这段注销
//由BrushChooserDialog模式对话框里的ColorTextBox触发
if(e.ShowParam.GetType()==typeof(HelloWorld.MyWPFControls.ColorTextBoxClickArgs))
{
HelloWorld.MyWPFControls.ColorTextBoxClickArgs arg = (HelloWorld.MyWPFControls.ColorTextBoxClickArgs)e.ShowParam;
arg.ColorTextBox.TrySetButtonColorFromTextBox(false);
}
*/
switch(e.ShowParam.ToString())
{
case "颜色选择":
this.Background = (SolidColorBrush)e.ReturnValue;
break;
default: //BrushDialog
break;
}
}
上述代码 Show(...,object param)中的param一直传递到OkEvent事件中的ModelDialogBaseEventArgs e参数.ShowParam成员上。
4.BrushChooser的实例设计和调用(具体功能用户控件)
BrushChooser是一个自定义用户控件,比ColorChooser复杂的多,这里只放上2两个功能界面的代码。
实心画刷标签板调用的是ColorChooser控件,而不是ColorChooserModelDialog 。渐变画刷标签板里的梯度中止颜色 TextBox右边有个小按钮显示TextBox中颜色值的颜色,这两个控件的组合我把它做成用户自定义控件ColorTextBox。执行逻辑是点击小按钮ColorButton弹出ColorChooserModelDialog,选中颜色后,执行OkEvent(在自定义控件BrushChooser里执行),在OkEvent里把ColorTextBox.Text填入颜色值,并把ColorButton的背景上色。
ColorTextBox的代码如下:
<UserControl x:Class="HelloWorld.MyWPFControls.ColorTextBox"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:HelloWorld.MyWPFControls"
mc:Ignorable="d"
d:DesignHeight="26" d:DesignWidth="90">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{Binding Width, ElementName=txtColor }"/>
<ColumnDefinition Width="{Binding Width, ElementName=btnColor }"/>
</Grid.ColumnDefinitions>
<TextBox x:Name="txtColor" Grid.Column="0" Text="" Width="80" LostFocus="txtColor_LostFocus" ></TextBox>
<Button x:Name="btnColor" Grid.Column="1" MinWidth="20" MaxWidth="30" Click="Button_Click" >
</Button>
</Grid>
</UserControl>
public class ColorTextBoxClickArgs: RoutedEventArgs
{
public ColorTextBox ColorTextBox;
public TextBox TextBox;
public Button ColorButton;
}
/// <summary>
/// ColorTextBox内置按钮点击事件
/// </summary>
public delegate void ColorTextBoxClickEventHandler(object sender, ColorTextBoxClickArgs e);
public partial class ColorTextBox : UserControl
{
public string Text
{
get { return txtColor.Text; }
set { txtColor.Text = value; }
}
public ColorTextBox()
{
InitializeComponent();
}
/// <summary>
/// 点击右侧按钮
/// </summary>
public event ColorTextBoxClickEventHandler ColorButtonClick;
private void Button_Click(object sender, RoutedEventArgs e)
{
if (ColorButtonClick != null)
{
ColorTextBoxClickArgs arg = new ColorTextBoxClickArgs();
arg.RoutedEvent = e.RoutedEvent;
arg.TextBox = this.txtColor;
arg.ColorButton = this.btnColor;
arg.ColorTextBox = this;
this.Dispatcher.Invoke(ColorButtonClick,sender,arg);
e.Handled = true; //阻止冒泡
}
}
private void txtColor_LostFocus(object sender, RoutedEventArgs e)
{
TrySetButtonColorFromTextBox(true);
}
/// <summary>
/// 尝试将TextBox.Text的值转为颜色变量,并使Button.Background上色
/// </summary>
/// <param name="throwException">是否抛出错误提示</param>
public void TrySetButtonColorFromTextBox(bool throwException)
{
try
{
if (txtColor.Text.Trim() != string.Empty)
{
Color c = (Color)ColorConverter.ConvertFromString(txtColor.Text);
if (c != null && btnColor != null)
{
//if (btnColor.Background is SolidColorBrush)
// ((SolidColorBrush)btnColor.Background).Color = c; //某些时候会出现冻结状态无法修改
//else
btnColor.Background = new SolidColorBrush(c);
}
}
}
catch (Exception err)
{
if (err.HResult == -2146233033 && throwException) MessageBox.Show("ColorText.Text必须是颜色值。");
Text = string.Empty;
}
}
/// <summary>
/// 将colorString赋给Text属性并尝试更改Button.Background
/// </summary>
/// <param name="colorString"></param>
/// <param name="throwException"></param>
public void TrySetButtonColor(string colorString,bool throwException)
{
this.Text = colorString;
TrySetButtonColorFromTextBox(throwException);
}
}
这个类有个注意的地方就是ColorButton.Click事件触发ColorButtonClick所使用的参数,把ColorTextBox里的两个子控件和自身都公布出去。并把自身动作逻辑都公布出来,就是TrySetButtonColor方法,TrySetButtonColorFromTextBox方法。
下面是BrushChooser的代码
<commonm:IModelDialogControl x:Class="HelloWorld.MyWPFControls.BrushChooser"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:commonm="clr-namespace:Common"
xmlns:local="clr-namespace:HelloWorld.MyWPFControls"
mc:Ignorable="d" Height="370" Width="380" >
<UserControl.Resources>
<Style TargetType="TextBlock">
<Setter Property="HorizontalAlignment" Value="Right" />
<Setter Property="VerticalAlignment" Value="Center" />
<local:ColorChooserModelDialog x:Key="ColorChooserDialogKey" Visibility="Collapsed" x:Name="ColorChooserDialog" OkEvent="ColorChooserDialog_OkEvent"/>
</Style>
</UserControl.Resources>
<Grid Margin="0,0,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="390"/>
<RowDefinition Height="3"/>
</Grid.RowDefinitions>
<TabControl x:Name="Tab" Grid.Row="0" Margin="0,0,0,25" >
<TabItem BorderThickness="1" >
<TabItem.Header>
<TextBlock>
<Run>SolidColorBrush</Run>
<LineBreak/>
<Run>实心画刷</Run>
</TextBlock>
</TabItem.Header>
<local:ColorChooser x:Name="ColorChooser1" Height="310" Width="370" />
</TabItem>
<TabItem BorderThickness="1">
<TabItem.Header>
<TextBlock>
<Run>LinearGradientBrush</Run>
<LineBreak/>
<Run>渐变画刷</Run>
</TextBlock>
</TabItem.Header>
<TabItem.Content>
<Grid Margin="0,0,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="30"/>
<RowDefinition Height="30"/>
<RowDefinition Height="30"/>
<RowDefinition Height="30"/>
<RowDefinition Height="30"/>
<RowDefinition Height="30"/>
<RowDefinition Height="30"/>
<RowDefinition Height="30"/>
<RowDefinition Height="30"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="115"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="115"/>
</Grid.ColumnDefinitions>
<TextBlock Text="循环方式" Grid.Row="0" Grid.Column="0" />
<ComboBox x:Name="cmbGradientSpreadMethod" Grid.Row="0" Grid.Column="1" SelectedIndex="0" SelectionChanged="cmbGradientSpreadMethod_SelectionChanged">
<GradientSpreadMethod>Pad</GradientSpreadMethod>
<GradientSpreadMethod>Reflect</GradientSpreadMethod>
<GradientSpreadMethod>Repeat</GradientSpreadMethod>
</ComboBox>
<TextBlock Text="角度" Grid.Row="0" Grid.Column="2" />
<TextBox x:Name="txtAngle" Width="30" Grid.Row="0" Grid.Column="3" LostFocus="txtAngle_LostFocus" ToolTip="角度必须是数字" Text="0" />
<TextBlock Text="起止点" Grid.Row="1" Grid.Column="0" />
<TextBlock x:Name="txtStartPointX" Text="X" Margin="0,0,0,0" Grid.Row="1" Grid.Column="1" />
<TextBlock x:Name="txtStartPointY" Text="Y" Margin="0,20,0,0" Grid.Row="1" Grid.Column="1" />
<TextBlock x:Name="txtEndPointX" Text="X" TextWrapping="NoWrap" Grid.Row="1" Grid.Column="3" Margin="0,0,30,0"/>
<TextBlock x:Name="txtEndPointY" Text="Y" TextWrapping="NoWrap" Grid.Row="1" Grid.Column="3" Margin="0,20,30,0"/>
<TextBlock Text="梯度中止颜色" Grid.Column="0" Grid.Row="2" />
<local:ColorTextBox x:Name="txtLGBColor1" Width="100" Grid.Column="1" Grid.Row="2" ColorButtonClick="ColorButtonClick" />
<TextBlock Text="偏移位置" Grid.Column="2" Grid.Row="2" />
<TextBox x:Name="txtLGBOffset1" Width="30" Grid.Column="3" Grid.Row="2" />
<TextBlock Text="梯度中止颜色" Grid.Column="0" Grid.Row="3"/>
<local:ColorTextBox x:Name="txtLGBColor2" Width="100" Grid.Column="1" Grid.Row="3" ColorButtonClick="ColorButtonClick" />
<TextBlock Text="偏移位置" Grid.Column="2" Grid.Row="3"/>
<TextBox x:Name="txtLGBOffset2" Width="30" Grid.Column="3" Grid.Row="3"/>
<TextBlock Text="梯度中止颜色" Grid.Column="0" Grid.Row="4"/>
<local:ColorTextBox x:Name="txtLGBColor3" Width="100" Grid.Column="1" Grid.Row="4" ColorButtonClick="ColorButtonClick" />
<TextBlock Text="偏移位置" Grid.Column="2" Grid.Row="4" />
<TextBox x:Name="txtLGBOffset3" Width="30" Grid.Column="3" Grid.Row="4" />
<TextBlock Text="梯度中止颜色" Grid.Column="0" Grid.Row="5"/>
<local:ColorTextBox x:Name="txtLGBColor4" Width="100" Grid.Column="1" Grid.Row="5" ColorButtonClick="ColorButtonClick" />
<TextBlock Text="偏移位置" Grid.Column="2" Grid.Row="5"/>
<TextBox x:Name="txtLGBOffset4" Width="30" Grid.Column="3" Grid.Row="5"/>
<TextBlock Text="梯度中止颜色" Grid.Column="0" Grid.Row="6"/>
<local:ColorTextBox x:Name="txtLGBColor5" Width="100" Grid.Column="1" Grid.Row="6" ColorButtonClick="ColorButtonClick"/>
<TextBlock Text="偏移位置" Grid.Column="2" Grid.Row="6"/>
<TextBox x:Name="txtLGBOffset5" Width="30" Grid.Column="3" Grid.Row="6"/>
<TextBlock Text="梯度中止颜色" Grid.Column="0" Grid.Row="7"/>
<local:ColorTextBox x:Name="txtLGBColor6" Width="100" Grid.Column="1" Grid.Row="7" ColorButtonClick="ColorButtonClick"/>
<TextBlock Text="偏移位置" Grid.Column="2" Grid.Row="7"/>
<TextBox x:Name="txtLGBOffset6" Width="30" Grid.Column="3" Grid.Row="7"/>
<TextBlock x:Name="tbkDes" Grid.Row="8" Grid.Column="0" Grid.ColumnSpan="4" Margin="0,0,20,0"/>
</Grid>
</TabItem.Content>
</TabItem>
<TabItem BorderThickness="1">
<TabItem.Header>
<TextBlock>
<Run>RadialGradientBrush</Run>
<LineBreak/>
<Run>半径向渐变画刷</Run>
</TextBlock>
</TabItem.Header>
<Grid Margin="0,0,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="26"/>
<RowDefinition Height="26"/>
<RowDefinition Height="26"/>
<RowDefinition Height="26"/>
<RowDefinition Height="26"/>
<RowDefinition Height="26"/>
<RowDefinition Height="26"/>
<RowDefinition Height="26"/>
<RowDefinition Height="26"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="115"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="115"/>
</Grid.ColumnDefinitions>
<TextBlock Text="圆半径" Grid.Row="0" Grid.Column="0"/>
<TextBox x:Name="txtRadiusX" Text="0.5" ToolTip="RadiusX 横径" Margin="-40,0,0,0" Width="30" Grid.Row="0" Grid.Column="1"/>
<TextBox x:Name="txtRadiusY" Text="0.5" ToolTip="RadiusY 竖径" Margin="40,0,0,0" Width="30" Grid.Row="0" Grid.Column="1"/>
<TextBlock Text="渐变起始点" Grid.Row="0" Grid.Column="2"/>
<TextBox x:Name="txtOrigin" Text="0.5,0.5" Grid.Row="0" Grid.Column="3" Width="60"/>
<TextBlock Text="循环方式" Grid.Row="1" Grid.Column="0" />
<ComboBox x:Name="cmbRGBGradientSpreadMethod" Grid.Row="1" Grid.Column="1" SelectedIndex="0" Width="60" SelectionChanged="cmbGradientSpreadMethod_SelectionChanged">
<GradientSpreadMethod>Pad</GradientSpreadMethod>
<GradientSpreadMethod>Reflect</GradientSpreadMethod>
<GradientSpreadMethod>Repeat</GradientSpreadMethod>
</ComboBox>
<TextBlock Text="终变(最外圆)圆心" Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" />
<TextBox x:Name="txtCenter" Text="0.5,0.5" Grid.Row="1" Grid.Column="3" Width="60"/>
<TextBlock Text="梯度中止颜色" Grid.Column="0" Grid.Row="2" />
<local:ColorTextBox x:Name="txtRGBColor1" Width="100" Grid.Column="1" Grid.Row="2" ColorButtonClick="ColorButtonClick" />
<TextBlock Text="偏移位置" Grid.Column="2" Grid.Row="2" />
<TextBox x:Name="txtRGBOffset1" Width="30" Grid.Column="3" Grid.Row="2" />
<TextBlock Text="梯度中止颜色" Grid.Column="0" Grid.Row="3"/>
<local:ColorTextBox x:Name="txtRGBColor2" Width="100" Grid.Column="1" Grid.Row="3" ColorButtonClick="ColorButtonClick" />
<TextBlock Text="偏移位置" Grid.Column="2" Grid.Row="3"/>
<TextBox x:Name="txtRGBOffset2" Width="30" Grid.Column="3" Grid.Row="3"/>
<TextBlock Text="梯度中止颜色" Grid.Column="0" Grid.Row="4"/>
<local:ColorTextBox x:Name="txtRGBColor3" Width="100" Grid.Column="1" Grid.Row="4" ColorButtonClick="ColorButtonClick" />
<TextBlock Text="偏移位置" Grid.Column="2" Grid.Row="4" />
<TextBox x:Name="txtRGBOffset3" Width="30" Grid.Column="3" Grid.Row="4" />
<TextBlock Text="梯度中止颜色" Grid.Column="0" Grid.Row="5"/>
<local:ColorTextBox x:Name="txtRGBColor4" Width="100" Grid.Column="1" Grid.Row="5" ColorButtonClick="ColorButtonClick" />
<TextBlock Text="偏移位置" Grid.Column="2" Grid.Row="5"/>
<TextBox x:Name="txtRGBOffset4" Width="30" Grid.Column="3" Grid.Row="5"/>
<TextBlock Text="梯度中止颜色" Grid.Column="0" Grid.Row="6"/>
<local:ColorTextBox x:Name="txtRGBColor5" Width="100" Grid.Column="1" Grid.Row="6" ColorButtonClick="ColorButtonClick"/>
<TextBlock Text="偏移位置" Grid.Column="2" Grid.Row="6"/>
<TextBox x:Name="txtRGBOffset5" Width="30" Grid.Column="3" Grid.Row="6"/>
<TextBlock Text="梯度中止颜色" Grid.Column="0" Grid.Row="7"/>
<local:ColorTextBox x:Name="txtRGBColor6" Width="100" Grid.Column="1" Grid.Row="7" ColorButtonClick="ColorButtonClick"/>
<TextBlock Text="偏移位置" Grid.Column="2" Grid.Row="7"/>
<TextBox x:Name="txtRGBOffset6" Width="30" Grid.Column="3" Grid.Row="7"/>
</Grid>
</TabItem>
<TabItem BorderThickness="1">
<TabItem.Header>
<TextBlock>
<Run>ImageBrush</Run>
<LineBreak/>
<Run>图片画刷</Run>
</TextBlock>
</TabItem.Header>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="30"/>
<RowDefinition Height="30"/>
<RowDefinition/>
<RowDefinition Height="30"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
</TabItem>
<TabItem BorderThickness="1">
<TabItem.Header>
<TextBlock>
<Run>VisualBrush</Run>
<LineBreak/>
<Run>复制画刷</Run>
</TextBlock>
</TabItem.Header>
</TabItem>
<TabItem BorderThickness="1">
<TabItem.Header>
<TextBlock>
<Run>BitmapCacheBrush</Run>
<LineBreak/>
<Run>位图画刷</Run>
</TextBlock>
</TabItem.Header>
</TabItem>
</TabControl>
</Grid>
</commonm:IModelDialogControl>
BrushChooser根据不同面板的设置,生成不同画刷,由函数SolidColorBrush returnSolidColorBrush,LinearGradientBrush returnLinearGradientBrush,RadialGradientBrush returnRadialGradientBrush生成,它们执行检查用户输入界面的数据完整性,数据不规范则抛出异常,规范则返回画刷。见ModelDialogBase接口VaildateForOk的实现
#region 实现接口需函数
/// <summary>
/// 返回值
/// </summary>
public override object ReturnValue { get { return rBrush; } }
/// <summary>
/// 验证界面输入完整性,如通过返回true并设置ReturnValue
/// </summary>
/// <returns></returns>
public override bool VaildateForOk()
{
/*
SolidColorBrush
LinearGradientBrush
RadialGradientBrush
ImageBrush
VisualBrush
BitmapCacheBrush
*/
switch (Tab.SelectedIndex)
{
case 0:
rBrush = returnSolidColorBrush();
return true;
case 1:
try
{
rBrush = returnLinearGradientBrush();
}
catch (Exception err)
{
MessageBox.Show(err.Message);
return false;
}
return true;
case 2:
try
{
rBrush = returnRadialGradientBrush();
}
catch (Exception err)
{
MessageBox.Show(err.Message);
return false;
}
return true;
case 3:
rBrush = returnImageBrush();
return true;
case 4:
rBrush = returnVisualBrush();
return false;
case 5:
rBrush = returnBitmapCacheBrush();
return false;
default: //-1
rBrush = null;
return false;
}
}
/// <summary>
/// ColorChooserDialog实例化,
/// 读出资源里的ColorChooser并附加到本控件的Canvas类型父容器上
/// </summary>
public override void BeforeShow()
{
/* 因为ColorChooserDialog作为本控件的子控件,就会把本控件左上角作为坐标原点,弹出的对话框界面会被剪切。
但又必须使用该控件,只好把它放在资源里,放在BeforeShow实例化。并附加到本控件Canvas类型的父容器里。
*/
if (this.ColorChooserDialog == null)
{
this.ColorChooserDialog = this.Resources["ColorChooserDialogKey"] as ColorChooserModelDialog;
Canvas canvas = Common.WpfWindowHelper.GetParent<Canvas>(this);
if (canvas == null)
{ MessageBox.Show("缺少属于窗体的Canvas层,无法展开ColorChooserDialog。"); }
else
canvas.Children.Add(this.ColorChooserDialog);
}
}
#endregion
//下面两个成员非接口成员,是属于BrushChooser原来的代码
public Brush Brush { get { return rBrush; } } //默认第一个TabItem
protected Brush rBrush;
#region 实心画刷 ColorChooser控件
SolidColorBrush returnSolidColorBrush()
{
//因ColorChooser1不是继承ModelDialogBase,没有Ok按钮事件返回,所以使用ColorValue成员
return new SolidColorBrush(ColorChooser1.ColorValue);
}
#endregion
#region 渐变画刷
/// <summary>
/// 范围外填充方式
/// </summary>
private void cmbGradientSpreadMethod_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (this.IsLoaded)
{
//GradientSpreadMethod.Pad; GradientSpreadMethod.Reflect; GradientSpreadMethod.Repeat
string[] des = new string[] { "默认值。渐变向量末端的颜色值填充剩余的空间。", "按相反方向重复渐变,直至充满空间。", "按原始方向重复渐变,直至充满空间。" };
tbkDes.Text = des[cmbGradientSpreadMethod.SelectedIndex];
}
}
/// <summary>
/// 角度
/// </summary>
private void txtAngle_LostFocus(object sender, RoutedEventArgs e)
{
double angle = 0;
if (!double.TryParse(txtAngle.Text, out angle))
{ txtAngle.Text = ""; return; }
Point[] ps = AngleAndStartEndPoints.AngleToPoints(angle);
txtStartPointX.Text = string.Format("X;{0}", ps[0].X);
txtStartPointY.Text = string.Format("Y;{0}", ps[0].Y);
txtEndPointX.Text = string.Format("X;{0}", ps[1].X);
txtEndPointY.Text = string.Format("Y;{0}", ps[1].Y);
}
LinearGradientBrush returnLinearGradientBrush()
{
double angle = 0;
if(!double.TryParse(txtAngle.Text,out angle))
throw new Exception("角度必须填写");
//检查 LinearGradientBrush(GradientStopCollection gradientStopCollection, double angle);参数输入完整性
double offset = 0;
GradientStopCollection gsc = new GradientStopCollection();
ColorTextBox[] txtLGBColors = new ColorTextBox[6]{ txtLGBColor1,txtLGBColor2,txtLGBColor3,txtLGBColor4,txtLGBColor5,txtLGBColor6 };
TextBox[] txtLGBOffsets = new TextBox[6] { txtLGBOffset1, txtLGBOffset2, txtLGBOffset3, txtLGBOffset4, txtLGBOffset5, txtLGBOffset6 };
for(int i=0;i<6;i++)
{
if (txtLGBColors[i].Text.Trim() != string.Empty && txtLGBOffsets[i].Text.Trim() != string.Empty)
if (double.TryParse(txtLGBOffsets[i].Text, out offset))
gsc.Add(new GradientStop((Color)ColorConverter.ConvertFromString(txtLGBColors[i].Text), offset));
}
if(gsc.Count<2) { throw new Exception("要取得渐变画刷至少得添加2种颜色和偏移位置值。"); }
LinearGradientBrush lgb = new LinearGradientBrush(gsc, angle);
lgb.SpreadMethod = (GradientSpreadMethod)cmbGradientSpreadMethod.SelectedItem;
return lgb;
}
#endregion
#region 径向渐变画刷
RadialGradientBrush returnRadialGradientBrush()
{
double radiusX, radiusY;
if (!double.TryParse(txtRadiusX.Text, out radiusX))
throw new Exception("椭圆横径必须是数字,并且在0到1之间。");
if( radiusX<0 || radiusX>1 )
throw new Exception("椭圆横径必须在0到1之间。");
if (!double.TryParse(txtRadiusY.Text, out radiusY))
throw new Exception("椭圆竖径必须是数字,并且在0到1之间。");
if (radiusY < 0 || radiusY > 1)
throw new Exception("椭圆竖径必须在0到1之间。");
string[] strOrgin = txtOrigin.Text.Split(",").ToArray();
double ox, oy;
if (strOrgin.Length!=2)
throw new Exception("椭圆圆心格式:0.5,0.5。");
else
{
if (!double.TryParse(strOrgin[0], out ox))
throw new Exception("圆心 X 坐标必须是数字");
if (!double.TryParse(strOrgin[1], out oy))
throw new Exception("圆心 Y 坐标必须是数字");
if (ox < 0 || ox> 1)
throw new Exception("圆心 X 坐标必须在0到1之间。");
if (oy < 0 || oy > 1)
throw new Exception("圆心 Y 坐标必须在0到1之间。");
}
double cx, cy;
string[] strCenter = txtCenter.Text.Split(",").ToArray();
if (strCenter.Length != 2)
throw new Exception("最外圆(终变)圆心格式:0.5,0.5。");
else
{
if (!double.TryParse(strCenter[0], out cx))
throw new Exception("最外圆(终变)圆心 X 坐标必须是数字");
if (!double.TryParse(strCenter[1], out cy))
throw new Exception("最外圆(终变)圆心 Y 坐标必须是数字");
if (cx < 0 || cx > 1)
throw new Exception("最外圆(终变)圆心 X 坐标必须在0到1之间。");
if (cy < 0 || cy > 1)
throw new Exception("最外圆(终变)圆心 Y 坐标必须在0到1之间。");
}
double offset = 0;
GradientStopCollection gsc = new GradientStopCollection();
ColorTextBox[] txtRGBColors = new ColorTextBox[6] { txtRGBColor1, txtRGBColor2, txtRGBColor3, txtRGBColor4, txtRGBColor5, txtRGBColor6 };
TextBox[] txtRGBOffsets = new TextBox[6] { txtRGBOffset1, txtRGBOffset2, txtRGBOffset3, txtRGBOffset4, txtRGBOffset5, txtRGBOffset6 };
for (int i = 0; i < 6; i++)
{
if (txtRGBColors[i].Text.Trim() != string.Empty && txtRGBOffsets[i].Text.Trim() != string.Empty)
if (double.TryParse(txtRGBOffsets[i].Text, out offset))
gsc.Add(new GradientStop((Color)ColorConverter.ConvertFromString(txtRGBColors[i].Text), offset));
}
if (gsc.Count < 2) { throw new Exception("要取得径向渐变画刷至少得添加2种颜色和偏移位置值。"); }
RadialGradientBrush rgb = new RadialGradientBrush(gsc);
rgb.SpreadMethod = (GradientSpreadMethod)cmbRGBGradientSpreadMethod.SelectedItem;
rgb.GradientOrigin = new Point(ox, oy);
rgb.Center = new Point(cx, cy);
rgb.RadiusX = radiusX;
rgb.RadiusY = radiusY;
return rgb;
}
#endregion
<local:ColorTextBox x:Name="txtLGBColor1" Width="100" Grid.Column="1" Grid.Row="2" ColorButtonClick="ColorButtonClick" />我们看看ColorButtonClick到底如何再弹出模态对话框,代码如下
#region ColorTextBox事件 调用ColorChooserDialog弹出模态对话框
/// <summary>
/// 空成员
/// </summary>
ColorChooserModelDialog ColorChooserDialog { get; set; }
private void ColorButtonClick(object sender, ColorTextBoxClickArgs e)
{
if(ColorChooserDialog!=null)
ColorChooserDialog.Show(Common.WpfWindowHelper.GetParentWindow(this), e);
//ShowPara 为ColorTextBoxClickArgs,内含触发该事件的ColorText控件的引用
}
private void ColorChooserDialog_OkEvent(object sender, ModelDialogBaseEventArgs e)
{
if (e.ShowParam.GetType() == typeof(ColorTextBoxClickArgs))
{
HelloWorld.MyWPFControls.ColorTextBoxClickArgs arg = (HelloWorld.MyWPFControls.ColorTextBoxClickArgs)e.ShowParam;
arg.ColorTextBox.TrySetButtonColor(((SolidColorBrush)e.ReturnValue).Color.ToString(), false);
}
e.Handled = true;
}
#endregion
因为ColorChooserDialog作为本控件的子控件,就会把本控件左上角作为坐标原点,弹出的对话框界面会被剪切。但又必须使用该控件,只好把它放在资源里,在接口BeforeShow方法中实例化。并附加到本控件Canvas类型的父容器里。到这里就实现了模态中再弹出模态的功能。
BeforwShow在这里就显示出作用 ,重载了接口BeforeShow执行调整界面功能,我们也可以实现展示指定画刷面板,通过先设置Tab.SelectedIndex实现该功能。
5.BrushChooserModelDialog的实例设计和调用(封装BrushChooser)
这个代码更简单,还是实例化ModelDialogBase的4个虚成员
public partial class BrushChooserModelDialog : ModelDialogBase
{
public BrushChooserModelDialog()
{
InitializeComponent();
}
protected override Button CancelButton
{
get
{
return this.btnCancel;
}
}
protected override Button OkButton
{
get
{
return this.btnOk;
}
}
protected override Rectangle ModelDialogPanel
{
get
{
return this.modelDialogPanel;
}
}
protected override IModelDialogControl SpecificControl
{
get
{
return this.BrushChooser;
}
}
}
<common:ModelDialogBase x:Class="HelloWorld.MyWPFControls.BrushChooserModelDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:HelloWorld.MyWPFControls"
xmlns:common="clr-namespace:Common"
mc:Ignorable="d" Width="385">
<Canvas Margin="0" Background="{Binding Path=Background,ElementName=ColorChooser}">
<Rectangle x:Name="modelDialogPanel" />
<Grid >
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<local:BrushChooser x:Name="BrushChooser" Grid.Row="0" Margin="0,0,0,0" />
<WrapPanel HorizontalAlignment="Right" Grid.Row="1" Background="{Binding Background,ElementName=BrushChooser}" >
<Button x:Name="btnOk" Content="确定" Margin="0,2,20,0" />
<Button x:Name="btnCancel" Content="取消" Margin="0,5,20,0" />
</WrapPanel>
</Grid>
</Canvas>
</common:ModelDialogBase>
测试窗体调用代码
public partial class TestWindow : Window
{
public TestWindow()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
Button btn = (Button)sender;
if (btn.Content.ToString() == "颜色选择")
ColorChooserDialog.Show(this, btn.Content);
else
this.BrushChooserDialog.Show(this, btn.Content);
}
private void ColorChooserDialog_OkEvent(object sender, ModelDialogBaseEventArgs e)
{
switch(e.ShowParam.ToString())
{
case "颜色选择":
this.Background = (SolidColorBrush)e.ReturnValue;
break;
default: //BrushDialog
break;
}
}
private void BrushChooserDialog_OkEvent(object sender, ModelDialogBaseEventArgs e)
{
if (e.ReturnValue.GetType() == typeof(SolidColorBrush))
{
this.Background = ((SolidColorBrush)e.ReturnValue);
return;
}
if(e.ReturnValue.GetType()==typeof(LinearGradientBrush))
{
this.Background = ((LinearGradientBrush)e.ReturnValue);
return;
}
if(e.ReturnValue.GetType()==typeof(RadialGradientBrush))
{
this.Background = (RadialGradientBrush)e.ReturnValue;
return;
}
if(e.ReturnValue.GetType()==typeof(ImageBrush))
{
this.Background = (ImageBrush)e.ReturnValue;
return;
}
}
}
<Window xmlns:MyWPFControls="clr-namespace:HelloWorld.MyWPFControls" x:Class="HelloWorld.TestWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:HelloWorld"
mc:Ignorable="d"
Title="TestWindow" Height="500" Width="609" AllowsTransparency="True" WindowStyle="None" x:Name="me" Loaded="me_Loaded">
<Window.Resources>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Button Content="颜色选择" Grid.Row="1" Grid.Column="0" Click="Button_Click" Width="100" Height=" 20" ></Button>
<Button Grid.Row="1" Grid.Column="1" Click="Button_Click" Content="画刷选择" Width="100" Height=" 20"></Button>
<Canvas>
<MyWPFControls:ColorChooserModelDialog x:Name="ColorChooserDialog" Visibility="Collapsed" OkEvent="ColorChooserDialog_OkEvent" />
<MyWPFControls:BrushChooserModelDialog x:Name="BrushChooserDialog" Visibility="Collapsed" OkEvent="BrushChooserDialog_OkEvent"/>
</Canvas>
</Grid>
</Window>
有一点要注意就是放置对话框控件的父容器一定要是Canvas,且不要给Canvas附加Grid.Row,Column属性。
总结
本文综合运用了3个月来的学习成果。复习接口/抽象类的概念,深刻体会接口/抽象类在框架设计上的强大,并学习了WPF图画对象,资源字典,VisualTreeHelper, DependencyObject异步调用方式。如缺运行出错少代码请留言。