WPF实现仿模态对话框,可复用,可模态叠加。


系统模态对话框的限制

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异步调用方式。如缺运行出错少代码请留言。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值