WPF SplitButton 的杂七杂八
原文:
http://www.codeproject.com/Articles/20612/A-WPF-SplitButton
SplitButton.cs
using System; using System.ComponentModel; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Markup; namespace Wpf.Controls { /// <summary> /// Implemetation of a Split Button /// </summary> [TemplatePart(Name = "PART_DropDown", Type = typeof (Button))] [ContentProperty("Items")] //不用显示写<Items/>标签 [DefaultProperty("Items")] public class SplitButton : Button { // AddOwner Dependency properties public static readonly DependencyProperty HorizontalOffsetProperty; public static readonly DependencyProperty IsContextMenuOpenProperty; public static readonly DependencyProperty ModeProperty; public static readonly DependencyProperty PlacementProperty; public static readonly DependencyProperty PlacementRectangleProperty; public static readonly DependencyProperty VerticalOffsetProperty; /// <summary> /// Static Constructor /// </summary> static SplitButton() { DefaultStyleKeyProperty.OverrideMetadata(typeof (SplitButton), new FrameworkPropertyMetadata(typeof (SplitButton))); IsContextMenuOpenProperty = DependencyProperty.Register("IsContextMenuOpen", typeof(bool), typeof(SplitButton), new FrameworkPropertyMetadata(false, new PropertyChangedCallback(OnIsContextMenuOpenChanged))); ModeProperty = DependencyProperty.Register("Mode", typeof(SplitButtonMode), typeof(SplitButton), new FrameworkPropertyMetadata(SplitButtonMode.Split)); // AddOwner properties from the ContextMenuService class, we need callbacks from these properties // to update the Buttons ContextMenu properties PlacementProperty = ContextMenuService.PlacementProperty.AddOwner(typeof (SplitButton), new FrameworkPropertyMetadata(PlacementMode.Bottom, new PropertyChangedCallback(OnPlacementChanged))); PlacementRectangleProperty = ContextMenuService.PlacementRectangleProperty.AddOwner(typeof (SplitButton), new FrameworkPropertyMetadata(Rect.Empty, new PropertyChangedCallback(OnPlacementRectangleChanged))); HorizontalOffsetProperty = ContextMenuService.HorizontalOffsetProperty.AddOwner(typeof (SplitButton), new FrameworkPropertyMetadata(0.0, new PropertyChangedCallback(OnHorizontalOffsetChanged))); VerticalOffsetProperty = ContextMenuService.VerticalOffsetProperty.AddOwner(typeof (SplitButton), new FrameworkPropertyMetadata(0.0, new PropertyChangedCallback(OnVerticalOffsetChanged))); } /* * Overrides * */ /// <summary> /// OnApplyTemplate override, set up the click event for the dropdown if present in the template /// </summary> public override void OnApplyTemplate() { base.OnApplyTemplate(); // set up the click event handler for the dropdown button ButtonBase dropDown = this.Template.FindName("PART_DropDown", this) as ButtonBase; if (dropDown != null) dropDown.Click += Dropdown_Click; } /// <summary> /// Handles the Base Buttons OnClick event /// </summary> protected override void OnClick() { switch (Mode) { case SplitButtonMode.Dropdown: OnDropdown(); break; default: base.OnClick(); // forward on the Click event to the user break; } } /* * Properties * */ /// <summary> /// The Split Button's Items property maps to the base classes ContextMenu.Items property /// </summary> public ItemCollection Items { get { EnsureContextMenuIsValid(); return this.ContextMenu.Items; } } /* * DependencyProperty CLR wrappers * */ /// <summary> /// Gets or sets the IsContextMenuOpen property. /// </summary> public bool IsContextMenuOpen { get { return (bool) GetValue(IsContextMenuOpenProperty); } set { SetValue(IsContextMenuOpenProperty, value); } } /// <summary> /// Placement of the Context menu /// </summary> public PlacementMode Placement { get { return (PlacementMode) GetValue(PlacementProperty); } set { SetValue(PlacementProperty, value); } } /// <summary> /// PlacementRectangle of the Context menu /// </summary> public Rect PlacementRectangle { get { return (Rect) GetValue(PlacementRectangleProperty); } set { SetValue(PlacementRectangleProperty, value); } } /// <summary> /// HorizontalOffset of the Context menu /// </summary> public double HorizontalOffset { get { return (double) GetValue(HorizontalOffsetProperty); } set { SetValue(HorizontalOffsetProperty, value); } } /// <summary> /// VerticalOffset of the Context menu /// </summary> public double VerticalOffset { get { return (double) GetValue(VerticalOffsetProperty); } set { SetValue(VerticalOffsetProperty, value); } } /// <summary> /// Defines the Mode of operation of the Button /// </summary> /// <remarks> /// The SplitButton two Modes are /// Split (default), - the button has two parts, a normal button and a dropdown which exposes the ContextMenu /// Dropdown - the button acts like a combobox, clicking anywhere on the button opens the Context Menu /// </remarks> public SplitButtonMode Mode { get { return (SplitButtonMode) GetValue(ModeProperty); } set { SetValue(ModeProperty, value); } } /* * DependencyPropertyChanged callbacks * */ private static void OnIsContextMenuOpenChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { SplitButton s = (SplitButton) d; s.EnsureContextMenuIsValid(); if (!s.ContextMenu.HasItems) return; bool value = (bool) e.NewValue; if (value && !s.ContextMenu.IsOpen) s.ContextMenu.IsOpen = true; else if (!value && s.ContextMenu.IsOpen) s.ContextMenu.IsOpen = false; } /// <summary> /// Placement Property changed callback, pass the value through to the buttons context menu /// </summary> private static void OnPlacementChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { SplitButton s = d as SplitButton; if (s == null) return; s.EnsureContextMenuIsValid(); s.ContextMenu.Placement = (PlacementMode) e.NewValue; } /// <summary> /// PlacementRectangle Property changed callback, pass the value through to the buttons context menu /// </summary> private static void OnPlacementRectangleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { SplitButton s = d as SplitButton; if (s == null) return; s.EnsureContextMenuIsValid(); s.ContextMenu.PlacementRectangle = (Rect) e.NewValue; } /// <summary> /// HorizontalOffset Property changed callback, pass the value through to the buttons context menu /// </summary> private static void OnHorizontalOffsetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { SplitButton s = d as SplitButton; if (s == null) return; s.EnsureContextMenuIsValid(); s.ContextMenu.HorizontalOffset = (double) e.NewValue; } /// <summary> /// VerticalOffset Property changed callback, pass the value through to the buttons context menu /// </summary> private static void OnVerticalOffsetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { SplitButton s = d as SplitButton; if (s == null) return; s.EnsureContextMenuIsValid(); s.ContextMenu.VerticalOffset = (double) e.NewValue; } /* * Helper Methods * */ /// <summary> /// Make sure the Context menu is not null /// </summary> private void EnsureContextMenuIsValid() { if (this.ContextMenu == null) { this.ContextMenu = new ContextMenu(); this.ContextMenu.PlacementTarget = this; this.ContextMenu.Placement = Placement; this.ContextMenu.Opened += ((sender, routedEventArgs) => IsContextMenuOpen = true); this.ContextMenu.Closed += ((sender, routedEventArgs) => IsContextMenuOpen = false); } } private void OnDropdown() { EnsureContextMenuIsValid(); if (!this.ContextMenu.HasItems) return; this.ContextMenu.IsOpen = !IsContextMenuOpen; // open it if closed, close it if open } /* * Events * */ /// <summary> /// Event Handler for the Drop Down Button's Click event /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void Dropdown_Click(object sender, RoutedEventArgs e) { OnDropdown(); e.Handled = true; } } }
SplitButtonMode.cs
namespace Wpf.Controls { public enum SplitButtonMode { Split, Dropdown, Button } }
SplitButtonResources.cs
using System; using System.Collections.Generic; using System.Text; using System.Windows; namespace Wpf.Controls { /// <summary> /// Class used for the ComponentResourceKey /// </summary> public class SplitButtonResources { public static ComponentResourceKey VistaSplitButtonStyleKey { get { return new ComponentResourceKey(typeof(SplitButtonResources), "vistaSplitButtonStyle"); } } } }
自己做的style:
Easy5SplitButtonSplitButtonColor.xaml
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:Wpf.Controls;assembly=Wpf.SplitButton" xmlns:aero="clr-namespace:Microsoft.Windows.Themes;assembly=PresentationFramework.Aero"> <Style x:Key="easy5SplitButton" TargetType="s:SplitButton"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="s:SplitButton"> <Grid VerticalAlignment="Stretch" HorizontalAlignment="Stretch"> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> <ColumnDefinition Width="16"/> </Grid.ColumnDefinitions> <Border x:Name="Bd" Padding="2,3" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" BorderBrush="Black" Background="Red"> <ContentPresenter> </ContentPresenter> </Border> <Path x:Name="path" Data="M0,0L3,3 6,0z" Margin="0,1,0,0" Grid.Column="1" Stroke="{TemplateBinding Foreground}" Fill="{TemplateBinding Foreground}" HorizontalAlignment="Center" VerticalAlignment="Center"/> <Button x:Name="PART_DropDown" Grid.Column="1" Margin="0"> <Path Data="M0,0L3,3 6,0z" Margin="0,1,0,0" Grid.Column="1" Stroke="{TemplateBinding Foreground}" Fill="{TemplateBinding Foreground}" HorizontalAlignment="Center" VerticalAlignment="Center"/> </Button> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style> </ResourceDictionary>
说明的问题:
1.用的CustomerControl自定义控件,而不是UserControl自定义控件。
CustomerControl自定义控件,这种灵活了,不依赖xaml,如果依赖xaml,依照惯例,得标出 :
CustomerControl自定义控件依赖样式中的控件,命名惯例是加上前缀PART_
并在定义控件的时使用标签属性注明:
[TemplatePart(Name = "PART_DropDown", Type = typeof (Button))] public class SplitButton : Button
代码如下:
/// <summary> /// Implemetation of a Split Button /// </summary> [TemplatePart(Name = "PART_DropDown", Type = typeof (Button))] public class SplitButton : Button {
...
}
xaml中的样式中必须提供名为:PART_DropDown的类型为Button的控件,否则,该自定义控件部分功能将肯能有所丢失。
例如:
1 <Style x:Key="easy5SplitButton" 2 TargetType="s:SplitButton"> 3 <Setter Property="Template"> 4 <Setter.Value> 5 <ControlTemplate TargetType="s:SplitButton"> 6 。。。。。。。。。 7 8 <Button x:Name="PART_DropDown" 9 Grid.Column="1" 10 Margin="0"> 11 <Path Data="M0,0L3,3 6,0z" 12 Margin="0,1,0,0" 13 Grid.Column="1" 14 Stroke="{TemplateBinding Foreground}" 15 Fill="{TemplateBinding Foreground}" 16 HorizontalAlignment="Center" 17 VerticalAlignment="Center"/> 18 </Button> 19 20 </Grid> 21 </ControlTemplate> 22 </Setter.Value> 23 </Setter> 24 </Style>
2.
<Style x:Key="easy5SplitButton"
TargetType="s:SplitButton">
。。。。
<Border x:Name="Bd" Padding="2,3"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
BorderBrush="Black"
Background="Red">
<ContentPresenter>
</ContentPresenter>
</Border>
样式 <Style x:Key="easy5SplitButton"
TargetType="s:SplitButton">中的
<ContentPresenter/>等价于:<ContentPresenter Content="{TemplateBinding Content}">
(实验证明:Button类型中的ControlTemplate 包含
</ContentPresenter/>
ContentPresenter的Content属性自动绑定到模板所在的父Button的Content,
其他控件没实验过。
)
3.当单击到SplitButton中的PART_DropDown按钮时,由于:
/// <summary>
/// OnApplyTemplate override, set up the click event for the dropdown if present in the template
/// </summary>
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
// set up the click event handler for the dropdown button
ButtonBase dropDown = this.Template.FindName("PART_DropDown", this) as ButtonBase;
if (dropDown != null)
dropDown.Click += Dropdown_Click;
}
所以引发:
private void Dropdown_Click(object sender, RoutedEventArgs e) { OnDropdown(); e.Handled = true; }
private void OnDropdown() { EnsureContextMenuIsValid(); if (!this.ContextMenu.HasItems) return; this.ContextMenu.IsOpen = !IsContextMenuOpen; // open it if closed, close it if open }
如果没有,即dropDown == null
1 // set up the click event handler for the dropdown button 2 ButtonBase dropDown = this.Template.FindName("PART_DropDown", this) as ButtonBase; 3 if (dropDown != null) 4 dropDown.Click += Dropdown_Click;
会由于路由冒泡到PART_DropDown按钮的父控件SplitButton,被捕捉从而引发Click事件。
但是不会弹出下拉菜单,功能会缺失,即上面已经提到的:
CustomerControl自定义控件,这种灵活了,不依赖xaml,如果依赖xaml,得标出:
/// <summary> /// Implemetation of a Split Button /// </summary> [TemplatePart(Name = "PART_DropDown", Type = typeof (Button))] public class SplitButton : Button {
1 <Style x:Key="easy5SplitButton" 2 TargetType="s:SplitButton"> 3 <Setter Property="Template"> 4 <Setter.Value> 5 <ControlTemplate TargetType="s:SplitButton"> 6 。。。。。。。。。 7 8 <Button x:Name="PART_DropDown" 9 Grid.Column="1" 10 Margin="0"> 11 <Path Data="M0,0L3,3 6,0z" 12 Margin="0,1,0,0" 13 Grid.Column="1" 14 Stroke="{TemplateBinding Foreground}" 15 Fill="{TemplateBinding Foreground}" 16 HorizontalAlignment="Center" 17 VerticalAlignment="Center"/> 18 </Button> 19 20 </Grid> 21 </ControlTemplate> 22 </Setter.Value> 23 </Setter> 24 </Style>
xaml中的样式中必须提供名为:PART_DropDown的类型为Button的控件,否则,该自定义控件部分功能将肯能有所丢失。