[Silverlight入门系列]右键菜单ContextMenu工具栏Toolbar和SplitButton
Silverlight4 Toolkit提供了ContextMenu和MenuItem控件来实现右键菜单,很方便。另外,Silverlight4 Toolkit提供了PopUp控件来实现弹出式内容,里面可以放任何子控件,利用IsOpen属性来控制显示隐藏。Silverlight工具栏Toolbar制作也很简单,就是用一个StackPanel横向摆一些button即可,button的内容就是图片,或者图片加文字均可。上下文菜单ContextMenu的Silverlight Demo源码下载请点击这儿,SplitButton的Silverlight Demo源码下载请点击这儿,Silverlight工具栏Toolbar的源码在本文末尾。另外微软的一款Silverlight Split Button的源码在这儿下载。各种实现效果如下几幅图:
Silverlight SplitButton

Silverlight ContextMenu
Silverlight工具栏 Toolbar
<splitButton:SplitButton Content="Open" Click="Open_Click">
<splitButton:SplitButton.ButtonMenuItemsSource>
<toolkit:MenuItem Header="Open" Click="Open_Click">
<toolkit:MenuItem.Icon>
<Image Source="/SilverlightApplication3;component/images/search.png" Width="14" Height="14"/>
</toolkit:MenuItem.Icon>
</toolkit:MenuItem>
<toolkit:MenuItem Header="Open read-only" Click="OpenReadOnly_Click"/>
<toolkit:MenuItem Header="Open as copy" Click="OpenCopy_Click"/>
</splitButton:SplitButton.ButtonMenuItemsSource>
</splitButton:SplitButton>
<splitButton:SplitButton.ButtonMenuItemsSource>
<toolkit:MenuItem Header="Open" Click="Open_Click">
<toolkit:MenuItem.Icon>
<Image Source="/SilverlightApplication3;component/images/search.png" Width="14" Height="14"/>
</toolkit:MenuItem.Icon>
</toolkit:MenuItem>
<toolkit:MenuItem Header="Open read-only" Click="OpenReadOnly_Click"/>
<toolkit:MenuItem Header="Open as copy" Click="OpenCopy_Click"/>
</splitButton:SplitButton.ButtonMenuItemsSource>
</splitButton:SplitButton>
Silverlight SplitButton源码
1 using System;
2 using System.Collections.ObjectModel;
3 using System.Windows;
4 using System.Windows.Controls;
5 using System.Windows.Input;
6
7 namespace Delay
8 {
9 /// <summary>
10 /// Implements a "split button" for Silverlight and WPF.
11 /// </summary>
12 [TemplatePart(Name = SplitElementName, Type = typeof(UIElement))]
13 public class SplitButton : Button
14 {
15 /// <summary>
16 /// Stores the public name of the split element.
17 /// </summary>
18 private const string SplitElementName = "SplitElement";
19
20 /// <summary>
21 /// Stores a reference to the split element.
22 /// </summary>
23 private UIElement _splitElement;
24
25 /// <summary>
26 /// Stores a reference to the ContextMenu.
27 /// </summary>
28 private ContextMenu _contextMenu;
29
30
31 /// <summary>
32 /// Stores the initial location of the ContextMenu.
33 /// </summary>
34 private Point _contextMenuInitialOffset;
35
36 /// <summary>
37 /// Stores the backing collection for the ButtonMenuItemsSource property.
38 /// </summary>
39 private ObservableCollection<object> _buttonMenuItemsSource = new ObservableCollection<object>();
40
41 /// <summary>
42 /// Gets the collection of items for the split button's menu.
43 /// </summary>
44 public Collection<object> ButtonMenuItemsSource { get { return _buttonMenuItemsSource; } }
45
46 /// <summary>
47 /// Gets or sets a value indicating whetherthe mouse is over the split element.
48 /// </summary>
49 protected bool IsMouseOverSplitElement { get; private set; }
50
51 /// <summary>
52 /// Initializes a new instance of the SplitButton class.
53 /// </summary>
54 public SplitButton()
55 {
56 DefaultStyleKey = typeof(SplitButton);
57 }
58
59 /// <summary>
60 /// Called when the template is changed.
61 /// </summary>
62 public override void OnApplyTemplate()
63 {
64 // Unhook existing handlers
65 if (null != _splitElement)
66 {
67 _splitElement.MouseEnter -= new MouseEventHandler(SplitElement_MouseEnter);
68 _splitElement.MouseLeave -= new MouseEventHandler(SplitElement_MouseLeave);
69 _splitElement = null;
70 }
71 if (null != _contextMenu)
72 {
73 _contextMenu.Opened -= new RoutedEventHandler(ContextMenu_Opened);
74 _contextMenu.Closed -= new RoutedEventHandler(ContextMenu_Closed);
75 _contextMenu = null;
76 }
77
78 // Apply new template
79 base.OnApplyTemplate();
80
81 // Hook new event handlers
82 _splitElement = GetTemplateChild(SplitElementName) as UIElement;
83 if (null != _splitElement)
84 {
85 _splitElement.MouseEnter += new MouseEventHandler(SplitElement_MouseEnter);
86 _splitElement.MouseLeave += new MouseEventHandler(SplitElement_MouseLeave);
87
88 _contextMenu = ContextMenuService.GetContextMenu(_splitElement);
89 if (null != _contextMenu)
90 {
91
92 _contextMenu.Opened += new RoutedEventHandler(ContextMenu_Opened);
93 _contextMenu.Closed += new RoutedEventHandler(ContextMenu_Closed);
94 }
95 }
96 }
97
98 /// <summary>
99 /// Called when the Button is clicked.
100 /// </summary>
101 protected override void OnClick()
102 {
103 if (IsMouseOverSplitElement)
104 {
105 OpenButtonMenu();
106 }
107 else
108 {
109 base.OnClick();
110 }
111 }
112
113 /// <summary>
114 /// Called when a key is pressed.
115 /// </summary>
116 protected override void OnKeyDown(KeyEventArgs e)
117 {
118 if (null == e)
119 {
120 throw new ArgumentNullException("e");
121 }
122
123 if ((Key.Down == e.Key) || (Key.Up == e.Key))
124 {
125 // WPF requires this to happen via BeginInvoke
126 Dispatcher.BeginInvoke((Action)(() => OpenButtonMenu()));
127 }
128 else
129 {
130 base.OnKeyDown(e);
131 }
132 }
133
134 /// <summary>
135 /// Opens the button menu.
136 /// </summary>
137 protected void OpenButtonMenu()
138 {
139 if ((0 < _buttonMenuItemsSource.Count) && (null != _contextMenu))
140 {
141 _contextMenu.HorizontalOffset = 0;
142 _contextMenu.VerticalOffset = 0;
143 _contextMenu.IsOpen = true;
144 }
145 }
146
147 /// <summary>
148 /// Called when the mouse goes over the split element.
149 /// </summary>
150 /// <param name="sender">Event source.</param>
151 /// <param name="e">Event arguments.</param>
152 private void SplitElement_MouseEnter(object sender, MouseEventArgs e)
153 {
154 IsMouseOverSplitElement = true;
155 }
156
157 /// <summary>
158 /// Called when the mouse goes off the split element.
159 /// </summary>
160 /// <param name="sender">Event source.</param>
161 /// <param name="e">Event arguments.</param>
162 private void SplitElement_MouseLeave(object sender, MouseEventArgs e)
163 {
164 IsMouseOverSplitElement = false;
165 }
166
167 /// <summary>
168 /// Called when the ContextMenu is opened.
169 /// </summary>
170 /// <param name="sender">Event source.</param>
171 /// <param name="e">Event arguments.</param>
172 private void ContextMenu_Opened(object sender, RoutedEventArgs e)
173 {
174 // Offset the ContextMenu correctly
175
176 _contextMenuInitialOffset = _contextMenu.TransformToVisual(null).Transform(new Point());
177
178 UpdateContextMenuOffsets();
179
180 // Hook LayoutUpdated to handle application resize and zoom changes
181 LayoutUpdated += new EventHandler(SplitButton_LayoutUpdated);
182 }
183
184 /// <summary>
185 /// Called when the ContextMenu is closed.
186 /// </summary>
187 /// <param name="sender">Event source.</param>
188 /// <param name="e">Event arguments.</param>
189 private void ContextMenu_Closed(object sender, RoutedEventArgs e)
190 {
191 // No longer need to handle LayoutUpdated
192 LayoutUpdated -= new EventHandler(SplitButton_LayoutUpdated);
193
194 // Restore focus to the Button
195 Focus();
196 }
197
198 /// <summary>
199 /// Called when the ContextMenu is open and layout is updated.
200 /// </summary>
201 /// <param name="sender">Event source.</param>
202 /// <param name="e">Event arguments.</param>
203 private void SplitButton_LayoutUpdated(object sender, EventArgs e)
204 {
205 UpdateContextMenuOffsets();
206 }
207
208 /// <summary>
209 /// Updates the ContextMenu's Horizontal/VerticalOffset properties to keep it under the SplitButton.
210 /// </summary>
211 private void UpdateContextMenuOffsets()
212 {
213 // Calculate desired offset to put the ContextMenu below and left-aligned to the Button
214
215 Point currentOffset = _contextMenuInitialOffset;
216 Point desiredOffset = TransformToVisual(Application.Current.RootVisual).Transform(new Point(0, ActualHeight));
217
218 _contextMenu.HorizontalOffset = desiredOffset.X - currentOffset.X;
219 _contextMenu.VerticalOffset = desiredOffset.Y - currentOffset.Y;
220 // Adjust for RTL
221 if (FlowDirection.RightToLeft == FlowDirection)
222 {
223 _contextMenu.UpdateLayout();
224 _contextMenu.HorizontalOffset -= _contextMenu.ActualWidth;
225
226 }
227 }
228
229 }
230 }
2 using System.Collections.ObjectModel;
3 using System.Windows;
4 using System.Windows.Controls;
5 using System.Windows.Input;
6
7 namespace Delay
8 {
9 /// <summary>
10 /// Implements a "split button" for Silverlight and WPF.
11 /// </summary>
12 [TemplatePart(Name = SplitElementName, Type = typeof(UIElement))]
13 public class SplitButton : Button
14 {
15 /// <summary>
16 /// Stores the public name of the split element.
17 /// </summary>
18 private const string SplitElementName = "SplitElement";
19
20 /// <summary>
21 /// Stores a reference to the split element.
22 /// </summary>
23 private UIElement _splitElement;
24
25 /// <summary>
26 /// Stores a reference to the ContextMenu.
27 /// </summary>
28 private ContextMenu _contextMenu;
29
30
31 /// <summary>
32 /// Stores the initial location of the ContextMenu.
33 /// </summary>
34 private Point _contextMenuInitialOffset;
35
36 /// <summary>
37 /// Stores the backing collection for the ButtonMenuItemsSource property.
38 /// </summary>
39 private ObservableCollection<object> _buttonMenuItemsSource = new ObservableCollection<object>();
40
41 /// <summary>
42 /// Gets the collection of items for the split button's menu.
43 /// </summary>
44 public Collection<object> ButtonMenuItemsSource { get { return _buttonMenuItemsSource; } }
45
46 /// <summary>
47 /// Gets or sets a value indicating whetherthe mouse is over the split element.
48 /// </summary>
49 protected bool IsMouseOverSplitElement { get; private set; }
50
51 /// <summary>
52 /// Initializes a new instance of the SplitButton class.
53 /// </summary>
54 public SplitButton()
55 {
56 DefaultStyleKey = typeof(SplitButton);
57 }
58
59 /// <summary>
60 /// Called when the template is changed.
61 /// </summary>
62 public override void OnApplyTemplate()
63 {
64 // Unhook existing handlers
65 if (null != _splitElement)
66 {
67 _splitElement.MouseEnter -= new MouseEventHandler(SplitElement_MouseEnter);
68 _splitElement.MouseLeave -= new MouseEventHandler(SplitElement_MouseLeave);
69 _splitElement = null;
70 }
71 if (null != _contextMenu)
72 {
73 _contextMenu.Opened -= new RoutedEventHandler(ContextMenu_Opened);
74 _contextMenu.Closed -= new RoutedEventHandler(ContextMenu_Closed);
75 _contextMenu = null;
76 }
77
78 // Apply new template
79 base.OnApplyTemplate();
80
81 // Hook new event handlers
82 _splitElement = GetTemplateChild(SplitElementName) as UIElement;
83 if (null != _splitElement)
84 {
85 _splitElement.MouseEnter += new MouseEventHandler(SplitElement_MouseEnter);
86 _splitElement.MouseLeave += new MouseEventHandler(SplitElement_MouseLeave);
87
88 _contextMenu = ContextMenuService.GetContextMenu(_splitElement);
89 if (null != _contextMenu)
90 {
91
92 _contextMenu.Opened += new RoutedEventHandler(ContextMenu_Opened);
93 _contextMenu.Closed += new RoutedEventHandler(ContextMenu_Closed);
94 }
95 }
96 }
97
98 /// <summary>
99 /// Called when the Button is clicked.
100 /// </summary>
101 protected override void OnClick()
102 {
103 if (IsMouseOverSplitElement)
104 {
105 OpenButtonMenu();
106 }
107 else
108 {
109 base.OnClick();
110 }
111 }
112
113 /// <summary>
114 /// Called when a key is pressed.
115 /// </summary>
116 protected override void OnKeyDown(KeyEventArgs e)
117 {
118 if (null == e)
119 {
120 throw new ArgumentNullException("e");
121 }
122
123 if ((Key.Down == e.Key) || (Key.Up == e.Key))
124 {
125 // WPF requires this to happen via BeginInvoke
126 Dispatcher.BeginInvoke((Action)(() => OpenButtonMenu()));
127 }
128 else
129 {
130 base.OnKeyDown(e);
131 }
132 }
133
134 /// <summary>
135 /// Opens the button menu.
136 /// </summary>
137 protected void OpenButtonMenu()
138 {
139 if ((0 < _buttonMenuItemsSource.Count) && (null != _contextMenu))
140 {
141 _contextMenu.HorizontalOffset = 0;
142 _contextMenu.VerticalOffset = 0;
143 _contextMenu.IsOpen = true;
144 }
145 }
146
147 /// <summary>
148 /// Called when the mouse goes over the split element.
149 /// </summary>
150 /// <param name="sender">Event source.</param>
151 /// <param name="e">Event arguments.</param>
152 private void SplitElement_MouseEnter(object sender, MouseEventArgs e)
153 {
154 IsMouseOverSplitElement = true;
155 }
156
157 /// <summary>
158 /// Called when the mouse goes off the split element.
159 /// </summary>
160 /// <param name="sender">Event source.</param>
161 /// <param name="e">Event arguments.</param>
162 private void SplitElement_MouseLeave(object sender, MouseEventArgs e)
163 {
164 IsMouseOverSplitElement = false;
165 }
166
167 /// <summary>
168 /// Called when the ContextMenu is opened.
169 /// </summary>
170 /// <param name="sender">Event source.</param>
171 /// <param name="e">Event arguments.</param>
172 private void ContextMenu_Opened(object sender, RoutedEventArgs e)
173 {
174 // Offset the ContextMenu correctly
175
176 _contextMenuInitialOffset = _contextMenu.TransformToVisual(null).Transform(new Point());
177
178 UpdateContextMenuOffsets();
179
180 // Hook LayoutUpdated to handle application resize and zoom changes
181 LayoutUpdated += new EventHandler(SplitButton_LayoutUpdated);
182 }
183
184 /// <summary>
185 /// Called when the ContextMenu is closed.
186 /// </summary>
187 /// <param name="sender">Event source.</param>
188 /// <param name="e">Event arguments.</param>
189 private void ContextMenu_Closed(object sender, RoutedEventArgs e)
190 {
191 // No longer need to handle LayoutUpdated
192 LayoutUpdated -= new EventHandler(SplitButton_LayoutUpdated);
193
194 // Restore focus to the Button
195 Focus();
196 }
197
198 /// <summary>
199 /// Called when the ContextMenu is open and layout is updated.
200 /// </summary>
201 /// <param name="sender">Event source.</param>
202 /// <param name="e">Event arguments.</param>
203 private void SplitButton_LayoutUpdated(object sender, EventArgs e)
204 {
205 UpdateContextMenuOffsets();
206 }
207
208 /// <summary>
209 /// Updates the ContextMenu's Horizontal/VerticalOffset properties to keep it under the SplitButton.
210 /// </summary>
211 private void UpdateContextMenuOffsets()
212 {
213 // Calculate desired offset to put the ContextMenu below and left-aligned to the Button
214
215 Point currentOffset = _contextMenuInitialOffset;
216 Point desiredOffset = TransformToVisual(Application.Current.RootVisual).Transform(new Point(0, ActualHeight));
217
218 _contextMenu.HorizontalOffset = desiredOffset.X - currentOffset.X;
219 _contextMenu.VerticalOffset = desiredOffset.Y - currentOffset.Y;
220 // Adjust for RTL
221 if (FlowDirection.RightToLeft == FlowDirection)
222 {
223 _contextMenu.UpdateLayout();
224 _contextMenu.HorizontalOffset -= _contextMenu.ActualWidth;
225
226 }
227 }
228
229 }
230 }
Silverlight工具栏 Toolbar源码
<Grid Height="33" HorizontalAlignment="Stretch" Margin="0,5,0,0" Name="grid1" VerticalAlignment="Top" Width="Auto" >
<Border BorderThickness="0" Background="Transparent" Margin="0" CornerRadius="3" BorderBrush="Gray">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<sdk:DataPager Grid.Column="1" Height="24" HorizontalAlignment="Left" Margin="0,4,10,0" Name="dataPager1" PageSize="10" VerticalAlignment="Top" Width="169" BorderThickness="0" AutoEllipsis="True" AllowDrop="False" DisplayMode="FirstLastPreviousNext" IsTotalItemCountFixed="True" Background="Transparent" />
<StackPanel Grid.Column="0" Margin="0" Name="stackPanel1" VerticalAlignment="Center" HorizontalAlignment="Stretch" Orientation="Horizontal">
<Button Height="30" Padding="5" Cursor="Hand" HorizontalAlignment="Left" Name="tsbAddNew" Margin="0,0,0,0" VerticalAlignment="Top" Width="Auto" BorderThickness="0" Background="Transparent">
<Button.Content>
<StackPanel Orientation="Horizontal">
<Image Opacity="0.7" Margin="-2,-2,0,0" Source="/SilverlightApplication2;component/images/plus.png" Width="16" Height="16" />
<TextBlock Text="Add New Person" Margin="5,-2,0,0" />
</StackPanel>
</Button.Content>
</Button>
<Button Height="30" Padding="5" Cursor="Hand" HorizontalAlignment="Left" Name="tsbTest3" Margin="10,0,0,0" VerticalAlignment="Top" Width="Auto" BorderThickness="0" Background="Transparent">
<Button.Content>
<StackPanel Orientation="Horizontal">
<Image Opacity="0.7" Margin="-2,-2,0,0" Source="/SilverlightApplication2;component/images/zoom.png" Width="16" Height="16" />
<TextBlock Text="Advanced Search" Margin="5,-2,0,0" />
</StackPanel>
</Button.Content>
</Button>
<Button Height="30" Padding="5" Cursor="Hand" HorizontalAlignment="Left" Name="tsbTest" Margin="10,0,0,0" VerticalAlignment="Top" Width="Auto" BorderThickness="0" Background="Transparent">
<Button.Content>
<StackPanel Orientation="Horizontal">
<Image Opacity="0.7" Margin="-2,-2,0,0" Source="/SilverlightApplication2;component/images/refresh2.png" Width="16" Height="16" />
<TextBlock Text="Refresh" Margin="5,-2,0,0" />
</StackPanel>
</Button.Content>
</Button>
</StackPanel>
</Grid>
</Border>
</Grid>
<Border BorderThickness="0" Background="Transparent" Margin="0" CornerRadius="3" BorderBrush="Gray">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<sdk:DataPager Grid.Column="1" Height="24" HorizontalAlignment="Left" Margin="0,4,10,0" Name="dataPager1" PageSize="10" VerticalAlignment="Top" Width="169" BorderThickness="0" AutoEllipsis="True" AllowDrop="False" DisplayMode="FirstLastPreviousNext" IsTotalItemCountFixed="True" Background="Transparent" />
<StackPanel Grid.Column="0" Margin="0" Name="stackPanel1" VerticalAlignment="Center" HorizontalAlignment="Stretch" Orientation="Horizontal">
<Button Height="30" Padding="5" Cursor="Hand" HorizontalAlignment="Left" Name="tsbAddNew" Margin="0,0,0,0" VerticalAlignment="Top" Width="Auto" BorderThickness="0" Background="Transparent">
<Button.Content>
<StackPanel Orientation="Horizontal">
<Image Opacity="0.7" Margin="-2,-2,0,0" Source="/SilverlightApplication2;component/images/plus.png" Width="16" Height="16" />
<TextBlock Text="Add New Person" Margin="5,-2,0,0" />
</StackPanel>
</Button.Content>
</Button>
<Button Height="30" Padding="5" Cursor="Hand" HorizontalAlignment="Left" Name="tsbTest3" Margin="10,0,0,0" VerticalAlignment="Top" Width="Auto" BorderThickness="0" Background="Transparent">
<Button.Content>
<StackPanel Orientation="Horizontal">
<Image Opacity="0.7" Margin="-2,-2,0,0" Source="/SilverlightApplication2;component/images/zoom.png" Width="16" Height="16" />
<TextBlock Text="Advanced Search" Margin="5,-2,0,0" />
</StackPanel>
</Button.Content>
</Button>
<Button Height="30" Padding="5" Cursor="Hand" HorizontalAlignment="Left" Name="tsbTest" Margin="10,0,0,0" VerticalAlignment="Top" Width="Auto" BorderThickness="0" Background="Transparent">
<Button.Content>
<StackPanel Orientation="Horizontal">
<Image Opacity="0.7" Margin="-2,-2,0,0" Source="/SilverlightApplication2;component/images/refresh2.png" Width="16" Height="16" />
<TextBlock Text="Refresh" Margin="5,-2,0,0" />
</StackPanel>
</Button.Content>
</Button>
</StackPanel>
</Grid>
</Border>
</Grid>
菜单、弹出式菜单和鼠标经过按钮出现的下拉菜单
这个开源(LPGL协议)的不错哦,用法是这样的:
<Button Name="Button1" Height="50"/>
<my:PopupMenu RightClickElements="Button1" LeftClickElements="Button1" AccessShortcut="Ctrl+Alt+M">
<ListBox>
<my:PopupMenuItem Header="Menu1" />
<my:PopupMenuSeparator />
<my:PopupMenuItem Header="SubMenuHeader1" ImageRightSource="images/arrow.png">
<my:PopupMenu>
<ListBox>
<my:PopupMenuItem Header="SubMenu1" />
</ListBox>
</my:PopupMenu>
</my:PopupMenuItem>
<my:PopupMenuItem Header="Menu2" />
<my:PopupMenuItem Header="Menu3" />
</ListBox>
</my:PopupMenu>
<my:PopupMenu RightClickElements="Button1" LeftClickElements="Button1" AccessShortcut="Ctrl+Alt+M">
<ListBox>
<my:PopupMenuItem Header="Menu1" />
<my:PopupMenuSeparator />
<my:PopupMenuItem Header="SubMenuHeader1" ImageRightSource="images/arrow.png">
<my:PopupMenu>
<ListBox>
<my:PopupMenuItem Header="SubMenu1" />
</ListBox>
</my:PopupMenu>
</my:PopupMenuItem>
<my:PopupMenuItem Header="Menu2" />
<my:PopupMenuItem Header="Menu3" />
</ListBox>
</my:PopupMenu>
在CodeBehind里面:
this.myMenu.AddTrigger(TriggerTypes.Hover, this.button1);
这样鼠标经过button1就会浮出菜单。同时支持MVVM的Command和CommandParameter,不错。
(这个开源的控件有些小bug,自己修补一下即可,不影响使用。)
