65.9K
CodeProject 正在变化。 阅读更多。
Home

Silverlight Menu4U

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (58投票s)

2011年7月31日

CPOL

11分钟阅读

viewsIcon

128111

downloadIcon

4131

一个带有样式和模板的新 Silverlight 菜单。

目录

引言

去年,我介绍了一篇介绍 Silverlight Menu 的文章,这是创建社区有用控件的一次尝试。尽管我努力为该控件添加了不错的功能,但我收到的反馈通常抱怨缺乏更灵活的自定义,例如更强大的模板、样式和命令。

现在,新的 Silverlight Menu 4U 已经到来。在浏览了旧代码一段时间(以评估是否可以重用它)后,我意识到代码是多么的整体化。菜单层级是由堆栈面板而不是列表框组成的,使其灵活需要付出巨大的努力。我决定从头开始重写它,以下是结果。

在本文中,我将向您介绍两个主要教程:首先,也是最重要的,是 Silverlight Menu 4U 用户指南,我将解释如何实现各种类型的菜单,从基本到高级用法。第二部分将讨论控件的制作,剖析其各个部分,并展示它们是如何以及为何被制作出来的。

系统要求

运行本文提供的 Silverlight Menu 4U 需要以下软件

此外,您可能还想下载 Microsoft Expression Blend 来为 Silverlight Menu 4U 设置样式和模板。请在安装 Blend 之前注意其系统要求。

Menu 4U 操作指南

一个简单的例子

上图显示了 Silverlight Menu 4U 的默认外观。请注意,它是一个下拉样式,非常像 Visual Studio 2010。让我们看看如何在您的项目中实现它。

首先,将以下命名空间添加到您的 XAML 中

<navigation:Page x:Class="Menu4UDemo.Views.Basic"
    ...
    xmlns:navigation="clr-namespace:System.Windows.Controls;
                      assembly=System.Windows.Controls.Navigation"
    ...
>

接下来,将 SLMenu 添加到您的 XAML 中。请记住,订阅 MenuItemClick 是可选的。

<ctrl:SLMenu x:Name="mnu" MenuItemClick="mnu_MenuItemClick"/>

很简单,不是吗?请记住,如此简单意味着菜单假定了默认设置。

现在我们转到代码隐藏,并提供 DataSource 属性,它是一个包含 Silverlight Menu 4U 所有菜单项的层次结构。

public Basic()
{
    InitializeComponent();
    List<object> menuItems = MenuHelper.CreateSimpleMenu();
    mnu.DataSource = menuItems;
}

对于演示,我将菜单项的创建本身放在一个单独的帮助类中。

public static List<object> CreateSimpleMenu()
{
    List<object> menuItems = new List<object>();

    var i1 = new MenuItem("Item 1");
    var i11 = new MenuItem("Item 1.1");
    var i12 = new MenuItem("Item 1.2");
    var i13 = new MenuItem("Item 1.3");

    i1.Children.Add(i11);
    i1.Children.Add(i12);
    i1.Children.Add(i13);

    var i131 = new MenuItem("Item 1.3.1");
    var i132 = new MenuItem("Item 1.3.2");

    i13.Children.Add(i131);
    i13.Children.Add(i132);

    var i2 = new MenuItem("Item 2");
    var i21 = new MenuItem("Item 2.1");
    var i22 = new MenuItem("Item 2.2");
    var i23 = new MenuItem("Item 2.3");

    i2.Children.Add(i21);
    i2.Children.Add(i22);
    i2.Children.Add(i23);

    var i3 = new MenuItem("Item 3");
    var i31 = new MenuItem("Item 3.1");
    var i32 = new MenuItem("Item 3.2");
    var i33 = new MenuItem("Item 3.3");

    i3.Children.Add(i31);
    i3.Children.Add(i32);
    i3.Children.Add(i33);

    menuItems.Add(i1);
    menuItems.Add(i2);
    menuItems.Add(i3);

    return menuItems;
}

或者,如果您愿意,也可以直接在 XAML 中添加项目,前提是您使用 MenuItem 类型的根项来包含第一级项目(这是该根项的唯一用途)。

<!--The items for this menu are defined directly in XAML-->
<ctrl:SLMenu x:Name="mnu2" Grid.Row="1" MenuItemClick="mnu_MenuItemClick">
    <!--THIS IS THE ROOT ITEM-->
    <ctrl:MenuItem>
        <ctrl:MenuItem Text="File">
            <ctrl:MenuItem Text="New File"/>
            <ctrl:MenuItem Text="Open File">
                <ctrl:MenuItem Text="1. Demo.txt"/>
                <ctrl:MenuItem Text="2. App.config"/>
            </ctrl:MenuItem>
            <ctrl:MenuItem Text="Save File"/>
            <ctrl:MenuItem Text="Exit"/>
        </ctrl:MenuItem>
        <ctrl:MenuItem Text="Edit">
            <ctrl:MenuItem Text="Cut"/>
            <ctrl:MenuItem Text="Copy"/>
            <ctrl:MenuItem Text="Paste"/>
        </ctrl:MenuItem>
        <ctrl:MenuItem Text="View">
            <ctrl:MenuItem Text="Zoom"/>
            <ctrl:MenuItem Text="Full Screen"/>
        </ctrl:MenuItem>
    </ctrl:MenuItem>
</ctrl:SLMenu>

您现在应该已经意识到,MenuItem 类包含菜单项的基本信息。我们将在“幕后”部分进一步处理 MenuItem 类的内部结构。现在,足以指出每个菜单项有两个重要属性:TextChildren,这是一个 MenuItem 实例列表。这描述了一个 Silverlight Menu 4U 将在以后渲染为由父 MenuItem 项链接的独立面板的层次结构。

回到代码隐藏类:现在我们将 MenuItemClick 事件连接到下面的代码,以便我们知道用户何时单击了 MenuItem

private void mnu_MenuItemClick(object sender, MenuItemEventArgs args)
{
    var item = (MenuItem)args.MenuItem;

    if (item.Children.Count() == 0)
        MessageBox.Show(string.Format("You cliked: {0}", item.Text));
}

请注意,行 if (item.Children.Count() == 0) 会忽略所有父菜单项。这是因为您通常不会关心用户是否单击了父菜单项。但如果您愿意,当然可以删除此行。

停靠

为了演示停靠功能,我们将使用一个不同的 DataSource,类似于 Visual Studio 2010 菜单。菜单项的完整列表相当大,因此我们在此跳过。也就是说,我们假设 CreateVSMenu 帮助函数将完成这项工作。

下面的代码显示了四个不同的 SilverlightMenu4U 实例具有相同的 DataSource。这是因为我们想演示相同的菜单数据在不同的停靠情况下是如何表现的。

public partial class Docking : Page
{
    public Docking()
    {
        InitializeComponent();
        mnu1.DataSource =
        mnu2.DataSource =
        mnu3.DataSource =
        mnu4.DataSource = MenuHelper.CreateVSMenu();
    }
    ...

现在让我们看看停靠在页面顶部的菜单。菜单的四个不同实例显示在我们的演示应用程序的同一页面中,因此有一些附加属性,例如 Grid.Row,它们将被设置,只是为了允许四个菜单一起渲染。

<ctrl:SLMenu x:Name="mnu1" 
    Grid.Column="1" Grid.ColumnSpan="1" 
    Grid.Row="0" Grid.RowSpan="3" 
    HorizontalAlignment="Stretch" 
    MenuItemClick="mnu_MenuItemClick"/>

上图显示了我们已经熟悉的菜单。由于 Dock 属性的默认值是 Top,在这种情况下,它不需要被设置。

现在让我们看看将 Dock 属性设置为 Left 的菜单。

<ctrl:SLMenu x:Name="mnu2"
    Grid.Column="0" Grid.ColumnSpan="3" 
    Grid.Row="1" Grid.RowSpan="1" 
    TabTemplate="{StaticResource secondLevelHeaderLeftTemplate}"
    Dock="Left" 
    MenuItemClick="mnu_MenuItemClick"/>

除了 Dock 属性之外,还有一个显著的区别:TabTemplate 属性。

TabTemplate 是第二个菜单层上面的选项卡。在这种情况下,“View”菜单项。如果您想要一个不同于 Top 的停靠,您必须调整此属性以使其与您的菜单配合使用。

<DataTemplate x:Key="secondLevelHeaderLeftTemplate">
    <Grid Margin="0,-2,0,-2" Height="25" Width="59">
        <Grid.Background>
            <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
                <GradientStop Color="#E8EBED" Offset="0"/>
                <GradientStop Color="#E8EBED" Offset="1"/>
            </LinearGradientBrush>
        </Grid.Background>
        <Path Data="M1,0 L0,0 L0,1 L1,1" 
               Stretch="Fill" Stroke="Gray"/>
        <TextBlock Margin="3,0,3,0" VerticalAlignment="Center">
            <ctrl:BindingHelper.Binding>
                <ctrl:BindingProperties TargetProperty="Text" 
                   SourceProperty="SecondLevelHeaderText"
                   RelativeSourceAncestorType="SLMenu" 
                   RelativeSourceAncestorLevel="1"/>
            </ctrl:BindingHelper.Binding>
        </TextBlock>
    </Grid>
</DataTemplate>

或者,如果您根本不在乎使用选项卡,可以像这样将模板留空(它实际上不影响菜单行为,只是外观问题)。

<DataTemplate x:Key="secondLevelHeaderLeftTemplate">
</DataTemplate>

上图显示了一个将 Dock 属性设置为 Right 的菜单。

如您所见,选项卡完美地适合右侧。正如预期的那样,这是 TabTemplate 属性的另一个实现。这就是我们编写 XAML 的方式。

<ctrl:SLMenu x:Name="mnu3" 
    Grid.Column="0" Grid.ColumnSpan="3" 
    Grid.Row="1" Grid.RowSpan="1" 
    TabTemplate="{StaticResource secondLevelHeaderRightTemplate}"
    SecondLevelMenuItemTemplate="{StaticResource secondLevelMenuItemRTLTemplate}"
    Dock="Right" MenuItemClick="mnu_MenuItemClick"/>

这个新实例与左侧的菜单略有不同。我们交换了一些菜单项组件的位置,以便子指示符(黑色小三角形)出现在左侧。

<DataTemplate x:Key="secondLevelMenuItemRTLTemplate">
    <Grid Width="300" Opacity="{Binding IsEnabled, Converter={StaticResource isEnabledToOpacity}}">
        <ctrl:ItemWrapper Text="{Binding Text}" HorizontalAlignment="Stretch">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="12"/>
                <ColumnDefinition Width="28"/>
                <ColumnDefinition/>
                <ColumnDefinition Width="80"/>
            </Grid.ColumnDefinitions>
            <Path Grid.Column="0" Data="M0,1 L1,0 L1,2 L0,1" 
            Width="4" Height="6" Stretch="Fill" Fill="Black"
                  Visibility="{Binding Children, Converter={StaticResource hasItemsToVis}}"/>
            <Image Grid.Column="1" HorizontalAlignment="Left" 
            Source="{Binding IconUrl}" Width="16" Margin="4,0,0,0" 
            Visibility="{Binding IsCheckable, Converter={StaticResource boolToCollapsed}}"/>
            <Image Grid.Column="1" HorizontalAlignment="Left" 
            Source="{Binding IconUrl}" Width="16" Margin="4,0,0,0" 
            Visibility="{Binding IsChecked, Converter={StaticResource boolToVis}}"/>

            <TextBlock Grid.Column="2" VerticalAlignment="Center" 
            Text="{Binding Text}"/>
            <TextBlock Grid.Column="3" VerticalAlignment="Center" 
            HorizontalAlignment="Right" Text="{Binding ShortCut}"/>
        </ctrl:ItemWrapper>
        <ctrl:Separator Grid.Column="0" Grid.ColumnSpan="3" Text="{Binding Text}"/>
    </Grid>
</DataTemplate>

上面显示的底部菜单显示了一个与顶部菜单非常相似的菜单。区别在于不同的菜单层级是自下而上渲染而不是自上而下渲染。

样式

重要提示:如果您对直接编写 XAML 代码感到不适,我强烈建议使用 Microsoft Expression Blend

样式在 Silverlight Menu4U 中扮演着重要角色。如果您不喜欢默认的 Visual Studio 式菜单,可以根据需要更改它。我们在下面的示例中将对此更改进行演练。

上图显示了如何设置 FirstLevelItemsPanelBrushSecondLevelItemsPanelBrush 属性以使菜单具有不同的外观和感觉。

<Style x:Key="slMenuStyle" TargetType="ctrl:SLMenu">
    <Setter Property="FirstLevelItemsPanelBrush">
        <Setter.Value>
            <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
                <GradientStop Color="#00ffffff" Offset="0"/>
                <GradientStop Color="#00ffffff" Offset="1"/>
            </LinearGradientBrush>
        </Setter.Value>
    </Setter>
    <Setter Property="SecondLevelItemsPanelBrush">
        <Setter.Value>
            <RadialGradientBrush RadiusX="1.2" 
                       RadiusY="1.2" Center="0,0" 
                       Opacity="0.8">
                <GradientStop Offset="0.0" Color="#00FFFFFA"/>
                <GradientStop Offset="0.7" Color="#00D6EDFE"/>
                <GradientStop Offset="0.8" Color="#806AC8F4"/>
                <GradientStop Offset="0.9" Color="#802590E7"/>
                <GradientStop Offset="1.3" Color="#801E41C3"/>
            </RadialGradientBrush>
        </Setter.Value>
    </Setter>

    <Setter Property="MenuBorderStyle" Value="{StaticResource menuBorderStyle}"/>
    <Setter Property="ItemSelectionStyle" Value="{StaticResource itemSelectionStyle}"/>
    <Setter Property="Foreground" Value="Black"/>
    <Setter Property="FontFamily" Value="Arial Black"/>
</Style>

请注意,我们还覆盖了 MenuBorderStyleItemSelectionStyleFontFamily 属性。菜单现在看起来更“流动”和透明。它可能不是很有吸引力,但毫无疑问,它展示了完全不同的菜单外观。

模板

如果您厌倦了传统的菜单,那么您会喜欢 Silverlight Menu4U 的模板功能。

虽然您可以使用样式来设置菜单的特定属性,但通过模板化菜单,您可以克服经典的图标/文本/快捷方式外观。当您为菜单项定义模板时,您可以完全重构控件结构,以按您想要的方式重构菜单内容:网格、堆栈面板、边框、文本框……模板内允许一切。

在这种情况下,我们将使用模板来为一个与哈利波特相关的网站制作一个完全定制的卷轴式菜单。

看起来很酷,不是吗?首先,我们提供 HeaderTemplateFooterTemplate。这些模板将定义纸卷的顶部和底部。

<DataTemplate x:Key="headerTemplate">
    <Grid Width="380" Height="80">
        <Image Source="/Images/ScrollTop.png" Stretch="UniformToFill"/>
    </Grid>
</DataTemplate>

<DataTemplate x:Key="footerTemplate">
    <Grid Width="380" Height="59">
        <Image Source="/Images/ScrollBottom.png" Stretch="UniformToFill"/>
    </Grid>
</DataTemplate>

现在我们为菜单面板本身定义背景(使用ScrollBody.png 图像)。它必须包含卷纸的拉伸部分,并将作为菜单项的背景。

<Setter Property="SecondLevelItemsPanelBrush">
    <Setter.Value>
        <ImageBrush ImageSource="/Images/ScrollBody.png"/>
    </Setter.Value>
</Setter>

那是容易的部分。现在我们为菜单项定义模板。

<DataTemplate x:Key="secondLevelMenuItemTemplate">
    <Border BorderBrush="Transparent" BorderThickness="1" 
    CornerRadius="2" Width="300" Margin="32,0,32,0">
        <Grid HorizontalAlignment="Stretch">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="50"/>
                <ColumnDefinition/>
                <ColumnDefinition Width="30"/>
            </Grid.ColumnDefinitions>
            <Image Grid.Column="0" HorizontalAlignment="Left" 
            Source="{Binding IconUrl}" VerticalAlignment="Top"/>
            <StackPanel Grid.Column="1">
                <TextBlock Margin="8,0,0,0" Text="{Binding Text}" 
                FontSize="16" FontWeight="Bold" HorizontalAlignment="Left"/>
                <TextBlock Margin="8,0,0,0" Text="{Binding Description}" 
                FontSize="11" TextWrapping="Wrap" HorizontalAlignment="Left" />
            </StackPanel>
            <Path Grid.Column="2" Data="M1,1 L0,0 L0,2 L1,1" 
            Width="10" Height="10" Stretch="Fill" Fill="Black"/>
        </Grid>
    </Border>
</DataTemplate>

从右到左

在我发布旧的 Silverlight 菜单后收到的一项建议是为阿拉伯语和希伯来语等字母创建从右到左的支持。这次,幸运的是我能够拥抱这项功能,以下是如何使用它。您应该覆盖第一级和第二级菜单项的模板。这将搞定。

<DataTemplate x:Key="firstLevelMenuItemTemplate">
    <Grid Width="120" 
           Opacity="{Binding IsEnabled, Converter={StaticResource isEnabledToOpacity}}">
        <ctrl:ItemWrapper Text="{Binding Text}" HorizontalAlignment="Stretch">
            <TextBlock VerticalAlignment="Center" 
            Text="{Binding Text}" TextAlignment="Right"/>
        </ctrl:ItemWrapper>
    </Grid>
</DataTemplate>

<DataTemplate x:Key="secondLevelMenuItemTemplate">
    <Grid Width="160" Opacity="{Binding IsEnabled, 
    Converter={StaticResource isEnabledToOpacity}}">
        <ctrl:ItemWrapper Text="{Binding Text}" HorizontalAlignment="Stretch">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="12"/>
                <ColumnDefinition/>
                <ColumnDefinition Width="28"/>
            </Grid.ColumnDefinitions>
            <Path Grid.Column="0" Data="M0,1 L1,0 L1,2 L0,1" 
            Width="4" Height="6" Stretch="Fill" Fill="Black"
                    Visibility="{Binding Children, 
                    Converter={StaticResource hasItemsToVis}}"/>
            <TextBlock Grid.Column="1" VerticalAlignment="Center" 
            Text="{Binding Text}" TextAlignment="Right"/>
            <Image Grid.Column="2" HorizontalAlignment="Left" 
            Source="{Binding IconUrl}" Width="16" Margin="4,0,0,0" 
            Visibility="{Binding IsCheckable, Converter={StaticResource boolToCollapsed}}"/>
            <Image Grid.Column="2" HorizontalAlignment="Left" 
            Source="{Binding IconUrl}" Width="16" Margin="4,0,0,0" 
            Visibility="{Binding IsChecked, Converter={StaticResource boolToVis}}"/>
        </ctrl:ItemWrapper>
        <ctrl:Separator Grid.Column="0" Grid.ColumnSpan="3" Text="{Binding Text}"/>
    </Grid>
</DataTemplate>

显然,我们必须为从右到左的菜单提供 DataSource。在这种情况下,我们有一个阿拉伯语菜单。

public static List<object> CreateArabicMenu()
{
    var menu = new List<object>();

    var m1 = new MenuItem("إبحار");
    var m11 = new MenuItem("المواضيع", @"/Images/mnuCut.png"); 
    m1.Children.Add(m11);
    var m12 = new MenuItem("أبجدي", @"/Images/mnuCopy.png");
     m1.Children.Add(m12);
    var m13 = new MenuItem("بوابات", @"/Images/mnuPaste.png");
     m1.Children.Add(m13);
    var m14 = new MenuItem("مقالة عشوائية", @"/Images/mnuDelete.png");
     m1.Children.Add(m14);

    var m2 = new MenuItem("المشاركة والمساعدة");
    var m21 = new MenuItem("اتصل بنا", @"/Images/mnuAddClass.png"); 
    m2.Children.Add(m21);
    var m22 = new MenuItem("بوابة المجتمع", @"/Images/mnuAddExistingItem.png");
     m2.Children.Add(m22);

    ...some more menu items...

    menu.Add(m1);
    menu.Add(m2);
    menu.Add(m3);

    return menu;
}

经过一些额外的样式(为简洁起见我跳过了),然后……瞧!这是我们全新的美丽的阿拉伯语菜单。

可勾选项

“可勾选项”是菜单控件中的常见功能。例如,您可能有一个应用程序显示多种面板(例如 Visual Studio 2010)。您还可以让用户单独切换每个面板的可见性。为此,您可以使用 Silverlight Menu4U 提供的“可勾选项”功能。

以下代码显示了客户端应用程序端的 MenuHelper 类如何实现可勾选的菜单项。

...
CreateCheckableMenuItem(mnuToolbars, "Build", true);
CreateCheckableMenuItem(mnuToolbars, "Data Design");
CreateCheckableMenuItem(mnuToolbars, "Database Diagram", true);
CreateCheckableMenuItem(mnuToolbars, "Debug");
CreateCheckableMenuItem(mnuToolbars, "Formatting", true);
CreateCheckableMenuItem(mnuToolbars, "HTML Source Editing", true);
CreateCheckableMenuItem(mnuToolbars, "Layout", true);
CreateCheckableMenuItem(mnuToolbars, "Query Designer");
CreateCheckableMenuItem(mnuToolbars, "Standard", true);
CreateCheckableMenuItem(mnuToolbars, "Style Sheet", true);
CreateCheckableMenuItem(mnuToolbars, "Table Designer");
CreateCheckableMenuItem(mnuToolbars, "Text Editor");
CreateCheckableMenuItem(mnuToolbars, "View Designer", true);
CreateCheckableMenuItem(mnuToolbars, "Web Browser");
CreateCheckableMenuItem(mnuToolbars, "Web One Click Publish");
var mnuSeparator32 = new MenuItem("-"); mnuToolbars.Children.Add(mnuSeparator32);
CreateCheckableMenuItem(mnuToolbars, "Customize");
...

最后,这是 CreateCheckableMenuItem 函数。请注意 ExecuteDelegate 操作如何切换 IsChecked 属性的开与关。

static void CreateCheckableMenuItem(MenuItem parent, string text, bool isChecked = false)
{
    var item = new MenuItem(text);
    item.Command = new SimpleCommand()
    {
        ExecuteDelegate = new Action<object>((o) => { item.IsChecked = !item.IsChecked; })
    };
    item.IsCheckable = true;
    item.IsChecked = isChecked;
    item.IconUrl = @"/Images/Checked.png";
    parent.Children.Add(item);
}

幕后

本节讨论 Silverlight Menu4U 的内部结构。这可能是一个非常广泛的主题,但为了简单起见,这里不涵盖所有细节。

关于 SilverlightMenu4U 的一个有趣问题是,它是用户控件还是自定义控件。根据定义,用户控件是由现有用户控件组成的,就像在 WPF 中创建窗口或在 Silverlight 中创建页面一样。根据定义,用户控件封装了控件演示所需的所有外观和感觉(在控件的 XAML 文件中),并且(通常)无法设置样式。另一方面,自定义控件是从现有控件扩展的(例如,通过派生自 ButtonItemsPanel 等控件),并且默认外观和感觉必须在程序集的 generic.xaml 中提供,并通过代码检索。可以说,自定义控件比用户控件更强大,但也更难开发。

Silverlight Menu4U 属于哪个类别?答案是,它是一个用户控件,因为它是由不同且预先存在的 Silverlight 控件组成的,但它也提供了像自定义控件一样的样式和模板灵活性。

创建层级

第一类内部控件是构成菜单层级的面板。每个菜单层显示菜单中不同组的菜单项兄弟。

不出所料,只有第一级始终可见。其他级别按需显示,当用户将鼠标悬停在上面的菜单级别上时。

层级不仅按需显示,而且按需创建。它们仅在控件的生命周期中创建一次。

如下所示,第一级以不同的方式处理,因为它可以水平或垂直渲染,并且根据用户选择的停靠选项以不同的方式对齐。每个容器实际上是一个 Grid,其中包含将实际渲染菜单项的列表框。

private void CreateMenuLevel(int level)
{
    if (level == 0)
    {
        switch (Dock)
        {
            case Dock.Top:
            case Dock.Bottom:
                firstLevelOrientation = Orientation.Horizontal;
                break;
            case Dock.Left:
            case Dock.Right:
                firstLevelOrientation = Orientation.Vertical;
                break;
        }

        var levelContainer = new Grid();
        levelContainer.Name = 
          string.Format("levelContainer{0}", levelContainers.Count());
        levelContainer.SetValue(Canvas.ZIndexProperty, 100 + level);
        LayoutRoot.Children.Add(levelContainer);
        levelContainers.Add(levelContainer);

        var listBox = CreateListBox(level, "levelListBoxes[0]", 
            "menuItemTemplate14U", "FirstLevelListBoxStyle4U", 
            "listBoxItemStyle4U", "FirstLevelItemsPanelBrush", 
            firstLevelOrientation, true);
        levelListBoxes.Add(listBox);

        var menuPanel = new Border();

        menuPanel.Child = listBox;

        menuPanels.Add(menuPanel);

        levelContainer.Children.Insert(0, menuPanel);

        levelListBoxes[level].CustomSelectionChanged += 
          new CustomListBox.CustomSelectionChangedDelegate(
          levelListBox_CustomSelectionChanged);
        levelListBoxes[level].ItemMouseLeftButtonDown += 
          new CustomListBox.ItemMouseLeftButtonDownDelegate(
          levelListBox_ItemMouseLeftButtonDown);

        switch (Dock)
        {
            case Dock.Top:
                levelListBoxes[level].HorizontalAlignment = 
                         HorizontalAlignment.Stretch;
                levelContainer.SetValue(Grid.ColumnProperty, 0);
                levelContainer.SetValue(Grid.ColumnSpanProperty, 3);
                break;
            case Dock.Left:
                levelListBoxes[level].VerticalAlignment = 
                         VerticalAlignment.Stretch;
                levelContainer.SetValue(Grid.RowProperty, 0);
                levelContainer.SetValue(Grid.RowSpanProperty, 3);
                break;
            case Dock.Bottom:
                levelListBoxes[level].HorizontalAlignment = 
                         HorizontalAlignment.Stretch;
                levelContainer.SetValue(Grid.RowProperty, 2);
                levelContainer.SetValue(Grid.ColumnProperty, 0);
                levelContainer.SetValue(Grid.ColumnSpanProperty, 3);
                levelContainer.VerticalAlignment = VerticalAlignment.Bottom;
                break;
            case Dock.Right:
                levelListBoxes[level].VerticalAlignment = VerticalAlignment.Stretch;
                levelContainer.SetValue(Grid.ColumnProperty, 2);
                levelContainer.SetValue(Grid.RowProperty, 0);
                levelContainer.SetValue(Grid.RowSpanProperty, 3);
                break;
        }
    }

对于第二级及更高级别,容器和列表框始终垂直渲染(尽管我将来可能会更改这一点)。

else
{
    switch (Dock)
    {
        case Dock.Top:
        case Dock.Left:
        case Dock.Bottom:
        case Dock.Right:
            secondLevelOrientation = Orientation.Vertical;
            break;
    }

    var levelContainer = new Grid();
    levelContainer.Name = string.Format("levelContainer{0}", levelContainers.Count());
    levelContainer.SetValue(Canvas.ZIndexProperty, 100 + level);
    LayoutRoot.Children.Add(levelContainer);
    levelContainers.Add(levelContainer);

    var secondLevel = new CustomListBox(level, secondLevelOrientation);
    levelListBoxes.Add(secondLevel);

    var listBox = CreateListBox(level, string.Format("levelListBoxes{0}", level), 
        "menuItemTemplate24U", "SecondLevelListBoxStyle4U", 
        "listBoxItemStyle4U", "SecondLevelItemsPanelBrush", 
        secondLevelOrientation, false);
    levelListBoxes[level] = listBox;

    var menuPanel = new Border();

    menuPanel.Child = listBox;
    menuPanels.Add(menuPanel);

    levelContainer.Children.Insert(0, menuPanel);

    levelListBoxes[level].CustomSelectionChanged += 
      new CustomListBox.CustomSelectionChangedDelegate(levelListBox_CustomSelectionChanged);
    levelListBoxes[level].ItemMouseLeftButtonDown += 
      new CustomListBox.ItemMouseLeftButtonDownDelegate(levelListBox_ItemMouseLeftButtonDown);

    if (level == 1)
    {
        secondLevelHeader.MouseLeftButtonDown += 
          new MouseButtonEventHandler(levelHeader_MouseLeftButtonDown);
        secondLevelHeader2.MouseLeftButtonDown += 
          new MouseButtonEventHandler(levelHeader_MouseLeftButtonDown);
        levelContainer.Visibility = System.Windows.Visibility.Collapsed;
    }

    switch (Dock)
    {
        case Dock.Top:
            levelListBoxes[0].HorizontalAlignment = HorizontalAlignment.Stretch;
            levelContainer.SetValue(Grid.RowProperty, 1);
            levelContainer.SetValue(Grid.ColumnProperty, 0);
            levelContainer.SetValue(Grid.ColumnSpanProperty, 3);
            levelContainer.VerticalAlignment = VerticalAlignment.Top;
            break;
        case Dock.Left:
            levelListBoxes[0].VerticalAlignment = VerticalAlignment.Stretch;
            levelContainer.SetValue(Grid.ColumnProperty, 1);
            levelContainer.SetValue(Grid.RowProperty, 0);
            levelContainer.SetValue(Grid.RowSpanProperty, 3);
            levelContainer.HorizontalAlignment = HorizontalAlignment.Left;
            break;
        case Dock.Bottom:
            levelListBoxes[0].HorizontalAlignment = HorizontalAlignment.Stretch;
            levelContainer.SetValue(Grid.RowProperty, 1);
            levelContainer.SetValue(Grid.ColumnProperty, 0);
            levelContainer.SetValue(Grid.ColumnSpanProperty, 3);
            if (level == 1)
                levelContainer.VerticalAlignment = VerticalAlignment.Bottom;
            else
                levelContainer.VerticalAlignment = VerticalAlignment.Bottom;
            break;
        case Dock.Right:
            levelListBoxes[0].VerticalAlignment = VerticalAlignment.Stretch;
            levelContainer.SetValue(Grid.ColumnProperty, 0);
            levelContainer.SetValue(Grid.RowProperty, 0);
            levelContainer.SetValue(Grid.RowSpanProperty, 3);
            levelContainer.HorizontalAlignment = HorizontalAlignment.Right;
            break;
    }
}

创建列表框

我认为列表框的制作是控件中最重要的一部分。首先,我们提供模板、样式和列表框方向。然后我们创建一个 CustomListBox 实例,然后将这些属性应用于列表框。然后,我们让列表框“继承”一些用户控件属性。最后,我们为 ItemsPanel 创建 ItemsPanelTemplate。这部分是控件模板的核心。

CustomListBox CreateListBox(int level, string name, string 
              itemTemplate, string style, string itemContainerStyle, 
              string itemsPanelBrush, Orientation listBoxOrientation, bool stretch)
{
    var listBox = new CustomListBox(level, listBoxOrientation);
    listBox.Name = string.Format("listBox{0}", levelListBoxes.Count());

    listBox.ItemTemplate = (DataTemplate)this.Resources[itemTemplate];
    listBox.Style = (Style)this.Resources[style];
    listBox.ItemContainerStyle = (Style)this.Resources[itemContainerStyle];
    string orientation = listBoxOrientation.ToString();
    var virtualizingStackPanel = new VirtualizingStackPanel()
    {
        Orientation = listBoxOrientation
    };

    switch (listBoxOrientation)
    {
        case Orientation.Horizontal:
            listBox.VerticalAlignment = System.Windows.VerticalAlignment.Top;
            if (stretch)
            {
                listBox.HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch;
            }
            else
            {
                listBox.HorizontalAlignment = System.Windows.HorizontalAlignment.Left;
            }
            break;
        case Orientation.Vertical:
            listBox.HorizontalAlignment = System.Windows.HorizontalAlignment.Left;
            if (stretch)
            {
                listBox.VerticalAlignment = System.Windows.VerticalAlignment.Stretch;
            }
            else
            {
                listBox.VerticalAlignment = System.Windows.VerticalAlignment.Top;
            }
            break;
    }

    var properties = new BindingProperties()
    {
        SourceProperty = "FirstLevelItemsPanelBrush",
        TargetProperty = "Background",
        RelativeSourceAncestorType = "SLMenu",
        RelativeSourceAncestorLevel = 1
    };

    listBox.Foreground = this.Foreground;
    listBox.FontFamily = this.FontFamily;
    listBox.FontSize = this.FontSize;
    listBox.FontStyle = this.FontStyle;
    listBox.FontWeight = this.FontWeight;
    listBox.Background = this.Background;
    listBox.BorderBrush = this.BorderBrush;
    listBox.BorderThickness = this.BorderThickness;
    listBox.Language = this.Language;

    Controls.BindingHelper.SetBinding(virtualizingStackPanel, properties);

    var strTemplate = new StringBuilder();
    strTemplate.Append("<ItemsPanelTemplate");
    strTemplate.Append(" xmlns=\"http://schemas." + 
       "microsoft.com/winfx/2006/xaml/presentation\"");
    strTemplate.Append(" xmlns:controls=\"clr-namespace:" + 
       "Silverlight.Controls;assembly=Silverlight.Controls\">");
    strTemplate.Append("    <VirtualizingStackPanel Orientation=\"" + 
        orientation + "\" Background=\"{Binding " + 
        itemsPanelBrush + "}\">");
    strTemplate.Append("        <controls:BindingHelper.Binding>");
    strTemplate.Append("            <controls:BindingProperties " + 
       "TargetProperty=\"Background\" SourceProperty=\"" + 
       itemsPanelBrush + "\"");
    strTemplate.Append("                RelativeSourceAncestorType" + 
       "=\"SLMenu\" RelativeSourceAncestorLevel=\"1\"/>");
    strTemplate.Append("        </controls:BindingHelper.Binding>");
    strTemplate.Append("    </VirtualizingStackPanel>");
    strTemplate.Append("</ItemsPanelTemplate>");

    ItemsPanelTemplate itemsPanelTemplate = (ItemsPanelTemplate)XamlReader.Load(strTemplate.ToString());

    listBox.ItemsPanel = itemsPanelTemplate;

    return listBox;
}

处理鼠标事件

我使用了上面提到的 CustomListBox 而不是原始的 ListBox 来封装一些功能,例如鼠标事件处理。每当用户将鼠标悬停在菜单项上时,该列表框项必须自动被选中。这不是列表框的默认功能,所以我们必须自己实现。以下代码显示了我们如何为列表中每个项目的 ContainerMouseEnterMouseMove 事件进行连接。

void CustomListBox_MouseEnter(object sender, MouseEventArgs e)
{
    for (var i = 0; i < this.Items.Count; i++)
    {
        var container = (ListBoxItem)this.ItemContainerGenerator.ContainerFromIndex(i);
        if (container != null)
        {
            container.MouseEnter -= new MouseEventHandler(container_MouseEnter);
            container.MouseEnter += new MouseEventHandler(container_MouseEnter);

            container.MouseMove -= new MouseEventHandler(container_MouseMove);
            container.MouseMove += new MouseEventHandler(container_MouseMove);
        }
    }
}

然后我们选择正在进入的项目。

void container_MouseMove(object sender, MouseEventArgs e)
{
    SelectItem(sender);
}

void container_MouseEnter(object sender, MouseEventArgs e)
{
    SelectItem(sender);
}

然后我们选择列表框中的项目,前提是它没有被禁用也不是项目分隔符。

private void SelectItem(object sender)
{
    var container = (ListBoxItem)sender;
    dynamic item = this.ItemContainerGenerator.ItemFromContainer(container);
    var isEnabled = item.IsEnabled == null ? true : item.IsEnabled;
    var isSeparator = item.IsSeparator == null ? false : item.IsSeparator;

    if (isEnabled && !isSeparator)
    {
        if (this.SelectedItem != item)
            this.SelectedItem = item;
    }
}

定位层级

最后,每当用户将鼠标悬停在特定菜单项上时,我们都必须显示或隐藏所选菜单项的子菜单项的下一个菜单层级。

不仅如此,每个子菜单层级都必须考虑到停靠模式、父项的顶部和左侧以及菜单方向来定位。

void levelListBox_CustomSelectionChanged(CustomListBox parentListBox, 
        IEnumerable itemsSource, double stackedPosition, double width, double height)
{
    var parentLevel = parentListBox.Level;
    var menuLevel = parentLevel + 1;

    for (var i = levelContainers.Count() - 1; i > parentListBox.Level + 1; i--)
    {
        levelContainers[i].Visibility = Visibility.Collapsed;
    }

    if (levelListBoxes.Count() < menuLevel + 1)
    {
        CreateMenuLevel(menuLevel);
    }

    levelListBoxes[menuLevel].ItemsSource = itemsSource;

    var parentMarginLeft = 0.0;
    var parentMarginRight = 0.0;
    var parentMarginTop = 0.0;
    var parentHeight = 0.0;

    var offsetX = 0.0;
    var offsetY = 0.0;

    if (firstLevelOrientation == Orientation.Horizontal)
    {
        if (parentLevel > 0)
        {
            parentMarginLeft = ((Thickness)levelContainers[parentLevel].GetValue(MarginProperty)).Left;
            parentMarginTop = ((Thickness)levelListBoxes[parentLevel].GetValue(MarginProperty)).Top;
            parentHeight = ((double)levelListBoxes[parentLevel].GetValue(ActualHeightProperty));
        }

        switch (Dock)
        {
            case Dock.Top:
                if (menuLevel > 1)
                {
                    offsetX = width;
                    offsetY = parentMarginTop + stackedPosition;
                }
                else if (menuLevel > 0)
                {
                    offsetX = stackedPosition;
                    offsetY = parentMarginTop;
                }
                else
                {
                    offsetX = stackedPosition;
                    offsetY = 0.0;
                }
                levelListBoxes[menuLevel].Margin = new Thickness(0, offsetY, 0, 0);
                levelContainers[menuLevel].Margin = new Thickness(parentMarginLeft + offsetX, 0, 0, 0);
                levelContainers[menuLevel].SetValue(Grid.RowProperty, 1);

                if (parentLevel == 0)
                {
                    secondLevelHeader.HorizontalAlignment = System.Windows.HorizontalAlignment.Left;
                    secondLevelHeader.Margin = new Thickness(offsetX, 0, 0, 0);
                    secondLevelHeader.VerticalAlignment = VerticalAlignment.Top;
                    SecondLevelHeaderText = levelListBoxes[parentLevel].SelectedItem.ToString();
                }
                break;
            case Dock.Bottom:
                if (menuLevel > 1)
                {
                    offsetX = parentMarginLeft + width;
                    offsetY = parentHeight - stackedPosition - height;
                }
                else if (menuLevel > 0)
                {
                    offsetX = stackedPosition;
                    offsetY = parentMarginTop;
                }
                else
                {
                    offsetX = stackedPosition;
                    offsetY = 0.0;
                }
                levelListBoxes[menuLevel].Margin = new Thickness(0, 0, 0, offsetY);
                levelContainers[menuLevel].Margin = new Thickness(offsetX, 0, 0, 0);
                levelContainers[0].HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch;

                if (parentLevel == 0)
                {
                    secondLevelHeader.Margin = new Thickness(offsetX, 0, 0, 0);
                    secondLevelHeader.VerticalAlignment = VerticalAlignment.Bottom;
                    SecondLevelHeaderText = levelListBoxes[parentLevel].SelectedItem.ToString();
                }
                break;
        }
    }
    else
    {
        var secondLevelContainerHeight = 
           (double)levelListBoxes[menuLevel].GetValue(ActualHeightProperty);

        if (parentLevel > 0)
        {
            parentMarginLeft = ((Thickness)levelContainers[parentLevel].GetValue(MarginProperty)).Left;
            parentMarginRight = ((Thickness)levelContainers[parentLevel].GetValue(MarginProperty)).Right;
            parentMarginTop = ((Thickness)levelContainers[parentLevel].GetValue(MarginProperty)).Top;
        }
        switch (Dock)
        {
            case Dock.Left:
                if (menuLevel > 1)
                {
                    offsetX = parentMarginLeft + width;
                    offsetY = parentMarginTop + stackedPosition;
                }
                else if (menuLevel > 0)
                {
                    offsetX = parentMarginLeft;
                    offsetY = stackedPosition;
                }
                else
                {
                    offsetX = 0.0;
                    offsetY = stackedPosition;
                }

                levelContainers[menuLevel].SetValue(Grid.ColumnProperty, 1);
                levelContainers[menuLevel].Margin = new Thickness(offsetX, offsetY, 0, 0);
                levelContainers[menuLevel].HorizontalAlignment = HorizontalAlignment.Left;
                if (parentLevel == 0)
                {
                    secondLevelHeader2.Margin = new Thickness(0, offsetY, 0, 0);
                    secondLevelHeader2.VerticalAlignment = VerticalAlignment.Top;
                    secondLevelHeader2.HorizontalAlignment = HorizontalAlignment.Left;
                    SecondLevelHeaderText = levelListBoxes[parentLevel].SelectedItem.ToString();
                }
                break;
            case Dock.Right:
                if (menuLevel > 1)
                {
                    offsetX = parentMarginRight + width;
                    offsetY = parentMarginTop + stackedPosition;
                }
                else if (menuLevel > 0)
                {
                    offsetX = parentMarginLeft;
                    offsetY = stackedPosition;
                }
                else
                {
                    offsetX = 0.0;
                    offsetY = stackedPosition;
                }

                levelContainers[menuLevel].SetValue(Grid.ColumnProperty, 1);
                levelContainers[menuLevel].Margin = new Thickness(0, offsetY, offsetX, 0);
                levelContainers[menuLevel].HorizontalAlignment = HorizontalAlignment.Right;
                if (parentLevel == 0)
                {
                    secondLevelHeader2.Margin = new Thickness(0, offsetY, 0, 0);
                    secondLevelHeader2.VerticalAlignment = VerticalAlignment.Top;
                    secondLevelHeader2.HorizontalAlignment = HorizontalAlignment.Right;
                    SecondLevelHeaderText = levelListBoxes[parentLevel].SelectedItem.ToString();
                }
                break;
        }
    }

    if (menuLevel > 1)
    {
        levelContainers[menuLevel].Visibility = levelListBoxes[menuLevel].Items.Count() > 0 ? 
        Visibility.Visible : Visibility.Collapsed;
    }
}

致谢

非常感谢 Colin Eberhardt 的文章 Implementing RelativeSource binding in Silverlight。您可能已经看到,我将 Colin 的实现用于整个项目。我这样做的原因是 Silverlight 4(我在这里使用的版本)不原生支持相对源绑定。幸运的是,正如 Kunal Chowdhury 在本文中所指出的,新的 Silverlight 5 现在 提供了这项很棒的功能

最终考虑

我希望您喜欢本文,并发现它对您有用。如果您有任何抱怨、想法、意见,请告诉我。我愿意根据您的反馈修改/增强代码,以便项目能够有机地发展。

历史

  • 2011-07-31:初始版本。
  • 2011-08-03:新的“幕后”部分。
  • 2011-08-05:新功能:直接通过 XAML 填充项目。
  • 2011-08-05:解释可勾选项。
© . All rights reserved.