Win10 UWP 开发系列:使用SplitView实现汉堡菜单及页面内导航

在Win10之前,WP平台的App主要有枢轴和全景两种导航模式,我个人更喜欢Pivot即枢轴模式,可以左右切换,非常方便。全景视图因为对设计要求比较高,自己总是做不出好的效果。对于一般的新闻阅读类App来说,Pivot更适合多个频道的展示,因为内容基本都是一样的。

到了Win10,微软模仿其他平台也推出了汉堡菜单,但并没有提供现成的控件,而是需要开发者通过一个名为SplitView的控件来实现。我个人并不觉得左上角的菜单有多么方便,汉堡菜单的使用必然会改变以前的导航模式,比如以前底部的AppBar使用很频繁,现在可以通过汉堡菜单的按钮来切换不同的页面。因此之前的App的导航模式需要重新设计。

假设有A、B、C三个平行的页面,可以在每个页面的左侧都放个汉堡菜单,也可以像web的框架页一样,做一个壳,汉堡菜单只放在外面的框架里,点击不同的按钮,在content里实现不同页面的导航。我比较倾向第二种,之前在做澎湃新闻uwp的时候就使用了这种方式,后来看了下Template10的模板,也是用的这种方式,在主页面外层套了一个Frame,而且还实现 了一个汉堡菜单控件。有兴趣的同学可以参考Template10来快速生成一个带汉堡菜单的基础App,Github地址:https://github.com/Windows-XAML/Template10 ,这个项目还带了很多好东西,比如一些常用的帮助类和一些behavior等,值得uwp开发者好好学习。

我没有直接使用T10的模板,以下介绍的还是当时使用MVVM-Sidekick框架实现的页面内导航。

首先通过MVVM-Sidekick提供的项目模板来新建一个UWP项目,命名为NavDemo。

考虑我们要实现的目的:在主页面放置一个汉堡菜单,在右侧的content中实现不同页面的导航。

先来看一下效果:

PC版:

手机版:

一、创建菜单项类

汉堡菜单每个选项一般是由一个图标和一个文字组成,我还是使用FontAwesomeFont这个字体来显示图标,如何使用这个字体来做图标,可参考我之前的blog。首先建立一个菜单的类NavMenuItem,放在Models目录下,使用provm代码段生成两个属性:


public class NavMenuItem : BindableBase<NavMenuItem>

{

/// <summary>

/// FontAwesomeFontFamily

/// </summary>

public string Glyph

{

get { return _GlyphLocator(this).Value; }

set { _GlyphLocator(this).SetValueAndTryNotify(value); }

}

#region Property string Glyph Setup

protected Property<string> _Glyph = new Property<string> { LocatorFunc = _GlyphLocator };

static Func<BindableBase, ValueContainer<string>> _GlyphLocator = RegisterContainerLocator<string>("Glyph", model => model.Initialize("Glyph", ref model._Glyph, ref _GlyphLocator, _GlyphDefaultValueFactory));

static Func<string> _GlyphDefaultValueFactory = () => { return default(string); };

#endregion

/// <summary>

///文字

/// </summary>

public string Label

{

get { return _LabelLocator(this).Value; }

set { _LabelLocator(this).SetValueAndTryNotify(value); }

}

#region Property string Label Setup

protected Property<string> _Label = new Property<string> { LocatorFunc = _LabelLocator };

static Func<BindableBase, ValueContainer<string>> _LabelLocator = RegisterContainerLocator<string>("Label", model => model.Initialize("Label", ref model._Label, ref _LabelLocator, _LabelDefaultValueFactory));

static Func<string> _LabelDefaultValueFactory = () => { return default(string); };

#endregion

}

打开NavDemo\ViewModels\MainPage_Model.cs,使用propvm代码段生成一个列表:


public ObservableCollection<NavMenuItem> NavMenuItemList

{

get { return _NavMenuItemListLocator(this).Value; }

set { _NavMenuItemListLocator(this).SetValueAndTryNotify(value); }

}

#region Property ObservableCollection<HamburgerMenuItem> NavMenuItemList Setup

protected Property<ObservableCollection<NavMenuItem>> _NavMenuItemList = new Property<ObservableCollection<NavMenuItem>> { LocatorFunc = _NavMenuItemListLocator };

static Func<BindableBase, ValueContainer<ObservableCollection<NavMenuItem>>> _NavMenuItemListLocator = RegisterContainerLocator<ObservableCollection<NavMenuItem>>("NavMenuItemList", model => model.Initialize("NavMenuItemList", ref model._NavMenuItemList, ref _NavMenuItemListLocator, _NavMenuItemListDefaultValueFactory));

static Func<ObservableCollection<NavMenuItem>> _NavMenuItemListDefaultValueFactory = () => default(ObservableCollection<NavMenuItem>);

#endregion

在vm的构造函数里,添加几个项:


public MainPage_Model()

{

if (IsInDesignMode )

{

Title = "Title is a little different in Design mode";

}

NavMenuItemList = new ObservableCollection<NavMenuItem>();

NavMenuItemList.Add(new NavMenuItem { Glyph = "\uf015", Label = "首页" });

NavMenuItemList.Add(new NavMenuItem { Glyph = "\uf002", Label = "搜索" });

NavMenuItemList.Add(new NavMenuItem { Glyph = "\uf05a", Label = "关于" });

}

注意Glyph的赋值方式。

二、显示汉堡菜单

在项目中新建Resources目录,把FontAwesome.otf字体文件放在里面。在项目中新建CustomTheme目录,然后建立自定义的样式资源文件CustomStyles.xaml,代码如下:


<ResourceDictionary

xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

xmlns:local="using:NavDemo">

<FontFamily x:Key="FontAwesomeFontFamily">/Resources/FontAwesome.otf#FontAwesome</FontFamily>

<Style x:Key="SplitViewTogglePaneButtonStyle" TargetType="ToggleButton">

<Setter Property="FontSize" Value="20" />

<Setter Property="FontFamily" Value="{ThemeResource SymbolThemeFontFamily}" />

<Setter Property="MinHeight" Value="48" />

<Setter Property="MinWidth" Value="48" />

<Setter Property="Margin" Value="0" />

<Setter Property="Padding" Value="0" />

<Setter Property="HorizontalAlignment" Value="Left" />

<Setter Property="VerticalAlignment" Value="Top" />

<Setter Property="HorizontalContentAlignment" Value="Center" />

<Setter Property="VerticalContentAlignment" Value="Center" />

<Setter Property="Background" Value="Transparent" />

<Setter Property="Foreground" Value="{ThemeResource SystemControlForegroundBaseHighBrush}" />

<Setter Property="Content" Value="" />

<Setter Property="AutomationProperties.Name" Value="Menu" />

<Setter Property="UseSystemFocusVisuals" Value="True"/>

<Setter Property="Template">

<Setter.Value>

<ControlTemplate TargetType="ToggleButton">

<Grid Background="{TemplateBinding Background}" x:Name="LayoutRoot">

<VisualStateManager.VisualStateGroups>

<VisualStateGroup x:Name="CommonStates">

<VisualState x:Name="Normal" />

<VisualState x:Name="PointerOver">

<Storyboard>

<ObjectAnimationUsingKeyFrames Storyboard.TargetName="LayoutRoot" Storyboard.TargetProperty="(Grid.Background)">

<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlHighlightListLowBrush}"/>

</ObjectAnimationUsingKeyFrames>

<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">

<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlHighlightAltBaseHighBrush}"/>

</ObjectAnimationUsingKeyFrames>

</Storyboard>

</VisualState>

<VisualState x:Name="Pressed">

<Storyboard>

<ObjectAnimationUsingKeyFrames Storyboard.TargetName="LayoutRoot" Storyboard.TargetProperty="(Grid.Background)">

<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlHighlightListMediumBrush}"/>

</ObjectAnimationUsingKeyFrames>

<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">

<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlHighlightAltBaseHighBrush}"/>

</ObjectAnimationUsingKeyFrames>

</Storyboard>

</VisualState>

<VisualState x:Name="Disabled">

<Storyboard>

<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="(TextBlock.Foreground)">

<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlDisabledBaseLowBrush}"/>

</ObjectAnimationUsingKeyFrames>

</Storyboard>

</VisualState>

<VisualState x:Name="Checked"/>

<VisualState x:Name="CheckedPointerOver">

<Storyboard>

<ObjectAnimationUsingKeyFrames Storyboard.TargetName="LayoutRoot" Storyboard.TargetProperty="(Grid.Background)">

<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlHighlightListLowBrush}"/>

</ObjectAnimationUsingKeyFrames>

<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">

<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlHighlightAltBaseHighBrush}"/>

</ObjectAnimationUsingKeyFrames>

</Storyboard>

</VisualState>

<VisualState x:Name="CheckedPressed">

<Storyboard>

<ObjectAnimationUsingKeyFrames Storyboard.TargetName="LayoutRoot" Storyboard.TargetProperty="(Grid.Background)">

<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlHighlightListMediumBrush}"/>

</ObjectAnimationUsingKeyFrames>

<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">

<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlHighlightAltBaseHighBrush}"/>

</ObjectAnimationUsingKeyFrames>

</Storyboard>

</VisualState>

<VisualState x:Name="CheckedDisabled">

<Storyboard>

<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="(TextBlock.Foreground)">

<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlDisabledBaseLowBrush}"/>

</ObjectAnimationUsingKeyFrames>

</Storyboard>

</VisualState>

</VisualStateGroup>

</VisualStateManager.VisualStateGroups>

<ContentPresenter x:Name="ContentPresenter"

Content="{TemplateBinding Content}"

Margin="{TemplateBinding Padding}"

HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"

VerticalAlignment="{TemplateBinding VerticalContentAlignment}"

AutomationProperties.AccessibilityView="Raw" />

</Grid>

</ControlTemplate>

</Setter.Value>

</Setter>

</Style>

</ResourceDictionary>

然后打开App.xaml文件,把这个资源引用进来:


<Application.Resources>

<ResourceDictionary>

<ResourceDictionary.MergedDictionaries>

<ResourceDictionary Source="CustomTheme/CustomStyles.xaml"/>

</ResourceDictionary.MergedDictionaries>

</ResourceDictionary>

</Application.Resources>

样式资源文件里主要定义了两个样式,一是定义了FontAwesomeFontFamily字体,二是定义了一个针对ToggleButton的按钮样式SplitViewTogglePaneButtonStyle,作为汉堡菜单的开关。这个开关键为什么要设置高度为48呢?参考https://msdn.microsoft.com/zh-cn/library/windows/apps/dn997787.aspx


拆分视图控件具有一个可展开/可折叠的窗格和一个内容区域。内容区域始终可见。窗格可以展开和折叠或停留在打开状态,而且可以从应用窗口的左侧或右侧显示其自身。窗格中有三种模式:

  • 覆盖

    在打开之前隐藏窗格。在打开时,窗格覆盖内容区域。

  • 内联

    窗格始终可见,并且不会覆盖内容区域。窗格和内容区域划分可用的屏幕实际使用面积。

  • 精简

    在此模式下窗格始终可见,它仅足够宽以显示图标(通常 48 epx 宽)。窗格和内容区域划分可用的屏幕实际使用面积。尽管标准精简模式不覆盖内容区域,但它可以转化为更宽的窗格来显示更多内容,这将覆盖该内容区域。

所以我就根据官方文档设置为48了。

修改MainPage.xaml,把根Grid改为以下代码:


<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" DataContext="{StaticResource DesignVM}">

<!-- Top-level navigation menu + app content -->

<SplitView x:Name="RootSplitView" IsPaneOpen="True"

DisplayMode="Inline"

OpenPaneLength="256"

IsTabStop="False">

<SplitView.Pane>

<!-- A custom ListView to display the items in the pane. The automation Name is set in the ContainerContentChanging event. -->

<ListView ItemsSource="{Binding NavMenuItemList}">

</ListView>

</SplitView.Pane>

<SplitView.Content>

<Frame x:Name="mainFrame">

</Frame>

</SplitView.Content>

</SplitView>

<!-- Declared last to have it rendered above everything else, but it needs to be the first item in the tab sequence. -->

<ToggleButton x:Name="TogglePaneButton"

TabIndex="1"

Style="{StaticResource SplitViewTogglePaneButtonStyle}"

IsChecked="{Binding IsPaneOpen, ElementName=RootSplitView, Mode=TwoWay}"

AutomationProperties.Name="Menu"

ToolTipService.ToolTip="Menu" />

</Grid>

为了方便查看菜单展开的效果,暂时先把IsPaneOpen属性设置为true,OpenPaneLength设置的是菜单展开后的宽度。在Pane里放一个ListView,ItemSource绑定到之前做好的NavMenuItemList上。SplitView的Content设置为一个Frame,用来展示右侧的页面。

注意,如果当SplitView的Content直接设置为Frame的时候,也就是把外层的<SplitView.Content>去掉后,会报一个错:

这个错误可以不用理会,程序是可以正常运行的。

此外 还要有一个按钮来控制菜单的展开关闭状态,用一个ToggleButton来实现,这个按钮的图标一般是三个横杠,设置其Style为SplitViewTogglePaneButtonStyle即可。

然后,还要设置ListView的项模板,可以使用Blend来设计项模板,但因为这个比较简单,我就直接手写了,在Resources目录下添加一个资源文件CustomDataTemplates.xaml,项目所有的自定义模板都可以写在这里,代码如下:


<ResourceDictionary

xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

xmlns:Interactivity="using:Microsoft.Xaml.Interactivity"

xmlns:Core="using:Microsoft.Xaml.Interactions.Core"

xmlns:Behaviors="using:MVVMSidekick.Behaviors">

<DataTemplate x:Key="NavMenuItemTemplate" >

<Grid>

<Grid.ColumnDefinitions>

<ColumnDefinition MinWidth="48" />

<ColumnDefinition />

</Grid.ColumnDefinitions>

<FontIcon x:Name="Glyph" FontFamily="{StaticResource FontAwesomeFontFamily}" FontSize="16" Margin="0" Glyph="{Binding Glyph}" VerticalAlignment="Center" HorizontalAlignment="Center" ToolTipService.ToolTip="{Binding Label}"/>

<TextBlock x:Name="Text" Grid.Column="1" Text="{Binding Label}" VerticalAlignment="Center"/>

</Grid>

</DataTemplate>

</ResourceDictionary>

在这里定义一个项模板NavMenuItemTemplate,在里面放一个FontIcon,把Glyph属性绑定到NavMenuItem的Glyph属性,当然不要忘了把FontFamily设置为我们在自定义样式里定义好的FontAwesomeFontFamily,不然是不会生效的。

再把这个项模板应用到页面的ListView控件上:


ItemTemplate="{StaticResource NavMenuItemTemplate}"

现在跑一下试试,报错了:

原来忘了把刚才的模板文件引入进来,修改App.xaml,修改为以下的样子:


<Application.Resources>

<ResourceDictionary>

<ResourceDictionary.MergedDictionaries>

<ResourceDictionary Source="CustomTheme/CustomStyles.xaml"/>

<ResourceDictionary Source="Resources/CustomDataTemplates.xaml" />

</ResourceDictionary.MergedDictionaries>

</ResourceDictionary>

</Application.Resources>

现在可以运行了:

貌似左上角的按钮跟ListView重叠了,这样可不好看。

三、调整显示效果

左上角的按钮应用了SplitViewTogglePaneButtonStyle样式,最小高度为48,把ListView往下移动一点,添加一个Margin属性,顶部把开关按钮的空间空出来:


<ListView Margin="0,48,0,0" ItemsSource="{Binding NavMenuItemList}"

ItemTemplate="{StaticResource NavMenuItemTemplate}">

现在列表位置正常了,但图标的位置貌似还是偏右了,那就再给ListView设置ItemContainerStyle样式,在CustomStyles.xaml文件里添加以下代码:


<Style x:Key="NavMenuItemContainerStyle" TargetType="ListViewItem">

<Setter Property="MinWidth" Value="{StaticResource SplitViewCompactPaneThemeLength}"/>

<Setter Property="Height" Value="48"/>

<Setter Property="Padding" Value="0"/>

</Style>

ListView应用此样式:


<ListView Margin="0,48,0,0" ItemsSource="{Binding NavMenuItemList}"

ItemTemplate="{StaticResource NavMenuItemTemplate}"

ItemContainerStyle="{StaticResource NavMenuItemContainerStyle}">

</ListView>

再跑一下:

现在样式正常了。

四、增加新页面

现在MainPage.xaml只是一个壳,右侧内容是空的,下面来添加几个页面。在项目里添加几个页面,比如可以命名为HomePage、SearchPage、AboutPage等:

因为每个页面里已经默认添加了一个TextBlock,并且绑定到了vm的Title属性,这个属性默认取值就是当前页面的Name,所以我们就不用改了,知道当前页面是哪个就行了。

现在的问题是,如何在MainPage载入时,自动在SplitView的Content里显示HomePage呢?

这就需要用到MVVM-Sidekick的一个Behavior了,用Blend打开项目,找到行为:

有一个叫做BaeconBehavior的行为,把它拖到……咦,怎么找不到Content呢?

那就直接手写吧,把Frame部分的代码改成这样:


<SplitView.Content>

<Frame x:Name="mainFrame" mvvm:StageManager.Beacon="frameMain" x:FieldModifier="public">

</Frame>

</SplitView.Content>

StageManager.Beacon属性是用来标识StageManager,MVVM-Sidekick已经把导航的功能封装到了StageManager里,以前我们一般使用this.StageManager.DefaultStage.Show(xxx)的方式来使用,即可实现整个页面的导航,如果要实现页面内某个区域的导航,就需要手动指定是哪个StageManager了,这就需要使用以下属性来标识某个区域:


mvvm:StageManager.Beacon="frameMain"

找到OnBindedViewLoad方法,取消默认的注释,将该方法改为以下的样子:


protected override async Task OnBindedViewLoad(MVVMSidekick.Views.IView view)

{

await base.OnBindedViewLoad(view);

await StageManager["frameMain"].Show(new HomePage_Model());

}

这里要注意,一定要等Bind完成后再Show,不然会显示不出来哦,因为要将整个页面Bind完后,才可以进行后续的动作。

跑一下看看:

很好,默认转到HomePage页了。

五、实现其他页面导航

现在可以处理菜单部分的导航了,点击不同的项导航到不同的页面。看到这里应该也有个大概了,处理不同项的点击事件,将名为frameMain的StageManager使用Show方法展示不同的ViewModel即可。

使用ItemClick事件吗?No,还记得我之前提过的SendToEventRouterAction吗?如果不熟悉的话就翻翻我之前的blog吧,这里我还是用这个Action来实现。

修改项模板为:


<DataTemplate x:Key="NavMenuItemTemplate" >

<Grid>

<Interactivity:Interaction.Behaviors>

<Core:EventTriggerBehavior EventName="Tapped">

<Behaviors:SendToEventRouterAction IsEventFiringToAllBaseClassesChannels="True" EventRoutingName="NavToPage" EventData="{Binding}" />

</Core:EventTriggerBehavior>

</Interactivity:Interaction.Behaviors>

<Grid.ColumnDefinitions>

<ColumnDefinition MinWidth="48" />

<ColumnDefinition />

</Grid.ColumnDefinitions>

<FontIcon x:Name="Glyph" FontFamily="{StaticResource FontAwesomeFontFamily}" FontSize="16" Margin="0" Glyph="{Binding Glyph}" VerticalAlignment="Center" HorizontalAlignment="Center" ToolTipService.ToolTip="{Binding Label}"/>

<TextBlock x:Name="Text" Grid.Column="1" Text="{Binding Label}" VerticalAlignment="Center"/>

</Grid>

</DataTemplate>

然后在MainPage_Model.cs文件中,添加一个方法:


private void RegisterCommand()

{

//一般列表项点击事件

MVVMSidekick.EventRouting.EventRouter.Instance.GetEventChannel<Object>()

.Where(x => x.EventName == "NavToPage")

.Subscribe(

async e =>

{

NavMenuItem item = e.EventData as NavMenuItem;

if (item != null)

{

switch (item.Label)

{

case "首页":

await StageManager["frameMain"].Show(new HomePage_Model());

break;

case "搜索":

await StageManager["frameMain"].Show(new SearchPage_Model());

break;

case "关于":

await StageManager["frameMain"].Show(new AboutPage_Model());

break;

default:

break;

}

}

}

).DisposeWith(this);

}

别忘了在OnBindedViewLoad方法里调用一下:


private bool isLoaded;

/// <summary>

/// This will be invoked by view when the view fires Load event and this viewmodel instance is already in view‘s ViewModel property

/// </summary>

/// <param name="view">View that firing Load event</param>

/// <returns>Task awaiter</returns>

protected override async Task OnBindedViewLoad(MVVMSidekick.Views.IView view)

{

if (!isLoaded)

{

this.RegisterCommand();

this.isLoaded = true;

}

await base.OnBindedViewLoad(view);

await StageManager["frameMain"].Show(new HomePage_Model());

}

添加一个isLoaded属性是避免重复调用。

跑一下看看,咦,有时候好用,有时候不好用,点击图标和文字的时候好用,点击不到图标和文字就不好用,这是什么原因?

熟悉ListView的同学可能会想到,ListViewItem默认是没有横向撑满的,所以虽然点击了项,但因为项模板里的Grid没有横向撑满,所以并没有触发Grid的Tapped事件,那我们可以设置ListItemStyle,让ListViewItem都横向撑满。在NavMenuItemContainerStyle里添加以下代码:


<Setter Property="HorizontalContentAlignment" Value="Stretch"/>

<Setter Property="VerticalContentAlignment" Value="Stretch"/>

这样就可以横向纵向撑满了,再跑下:

又乱套了,再改哪里呢,修改项模板NavMenuItemTemplate,设置左侧列宽为Auto:


<DataTemplate x:Key="NavMenuItemTemplate" >

<Grid >

<Interactivity:Interaction.Behaviors>

<Core:EventTriggerBehavior EventName="Tapped">

<Behaviors:SendToEventRouterAction IsEventFiringToAllBaseClassesChannels="True" EventRoutingName="NavToPage" EventData="{Binding}" />

</Core:EventTriggerBehavior>

</Interactivity:Interaction.Behaviors>

<Grid.ColumnDefinitions>

<ColumnDefinition MinWidth="48" Width="Auto" />

<ColumnDefinition />

</Grid.ColumnDefinitions>

<FontIcon x:Name="Glyph" FontFamily="{StaticResource FontAwesomeFontFamily}" FontSize="16" Margin="0" Glyph="{Binding Glyph}" VerticalAlignment="Center" HorizontalAlignment="Center" ToolTipService.ToolTip="{Binding Label}"/>

<TextBlock x:Name="Text" Grid.Column="1" Text="{Binding Label}" VerticalAlignment="Center" />

</Grid>

</DataTemplate>

再运行一下:

现在正常了。

看一下手机上的样子:

六、其他细节调整

使用了一下感觉还是有点细节需要改进,比如菜单弹出后,点击项后应该让菜单自动缩回去,现在改一下吧。

在MainPage的vm里添加一个属性:


/// <summary>

///是否展开菜单

/// </summary>

public bool IsPaneOpen

{

get { return _IsPaneOpenLocator(this).Value; }

set { _IsPaneOpenLocator(this).SetValueAndTryNotify(value); }

}

#region Property bool IsPaneOpen Setup

protected Property<bool> _IsPaneOpen = new Property<bool> { LocatorFunc = _IsPaneOpenLocator };

static Func<BindableBase, ValueContainer<bool>> _IsPaneOpenLocator = RegisterContainerLocator<bool>("IsPaneOpen", model => model.Initialize("IsPaneOpen", ref model._IsPaneOpen, ref _IsPaneOpenLocator, _IsPaneOpenDefaultValueFactory));

static Func<bool> _IsPaneOpenDefaultValueFactory = () => default(bool);

#endregion

在vm的构造函数里将此值设置为false,默认为关闭。

然后将SplitView的IsPaneOpen属性绑定到上面:


<SplitView x:Name="RootSplitView" IsPaneOpen="{Binding IsPaneOpen,Mode=TwoWay}"

DisplayMode="Inline"

OpenPaneLength="256"

IsTabStop="False">

修改RegisterCommand方法,在点击每个项的部分,添加以下代码,关闭菜单:


this.IsPaneOpen = false;

现在点击菜单项后可以自动关闭菜单面板了。

还可以继续针对PC版和手机版调整一下细节,PC版屏幕大,可以让菜单收起时留下图标的部分,这就需要调整PC版的DisplayMode属性为CompactInline,需要请StateTriggers出马了。

在根Grid里添加以下代码:


<!-- Adaptive triggers -->

<VisualStateManager.VisualStateGroups>

<VisualStateGroup>

<VisualState>

<VisualState.StateTriggers>

<AdaptiveTrigger MinWindowWidth="720" />

</VisualState.StateTriggers>

<VisualState.Setters>

<Setter Target="RootSplitView.DisplayMode" Value="CompactInline"/>

<Setter Target="RootSplitView.IsPaneOpen" Value="True"/>

<Setter Target="RootSplitView.CompactPaneLength" Value="48" />

</VisualState.Setters>

</VisualState>

<VisualState>

<VisualState.StateTriggers>

<AdaptiveTrigger MinWindowWidth="0" />

</VisualState.StateTriggers>

<VisualState.Setters>

<Setter Target="RootSplitView.DisplayMode" Value="Overlay"/>

</VisualState.Setters>

</VisualState>

</VisualStateGroup>

</VisualStateManager.VisualStateGroups>

这段代码的意思是,如果宽度大于720,就将SplitView的DisplayMode设置为CompactInline,菜单收起的时候可以保留图标部分,这部分图标的宽度通过CompactPaneLength这个值来设定。

还有一点,手机是有硬件返回键的,在菜单弹出的时候,如果用户点击了返回键,应该让菜单缩回去,所以还要额外处理一下手机的返回键。

给项目添加Mobile Extensions引用:

注意我安装了两个版本的SDK,这里需要根据项目的实际版本来选择对应的扩展。

打开MainPage.xaml.cs,添加以下代码:


protected override void OnNavigatedTo(NavigationEventArgs e)

{

if (Windows.Foundation.Metadata.ApiInformation.IsTypePresent("Windows.Phone.UI.Input.HardwareButtons"))

{

HardwareButtons.BackPressed += HardwareButtons_BackPressed;

}

base.OnNavigatedTo(e);

}

protected override void OnNavigatedFrom(NavigationEventArgs e)

{

if (Windows.Foundation.Metadata.ApiInformation.IsTypePresent("Windows.Phone.UI.Input.HardwareButtons"))

{

HardwareButtons.BackPressed -= HardwareButtons_BackPressed;

}

base.OnNavigatedFrom(e);

}

private void HardwareButtons_BackPressed(object sender, BackPressedEventArgs e)

{

//throw new NotImplementedException();

var vm = this.LayoutRoot.DataContext as MainPage_Model;

if (vm != null)

{

if (vm.IsPaneOpen)

{

e.Handled = true;

vm.IsPaneOpen = false;

}

}

}

至此,一个具有基本功能的汉堡菜单就完成了,可以通过修改背景色、前景色等方式再来改善展示效果。再来总结一下主要的知识点:

  1. 使用SplitView来区分菜单面板和内容部分;
  2. 使用FontAwesomeFont字体显示图标;
  3. 为区域使用mvvm:StageManager.Beacon属性来设置StageManager的标识,并通过StageManager["xxx"]的形式来调用;
  4. 通过StateTriggers来为PC和手机端设置不同的菜单效果;
  5. 通过添加Mobile Extensions引用来支持手机硬件返回键;
时间: 2024-09-30 06:46:30

Win10 UWP 开发系列:使用SplitView实现汉堡菜单及页面内导航的相关文章

Win10 UWP开发系列:使用VS2015 Update2+ionic开发第一个Cordova App

安装VS2015 Update2的过程是非常曲折的.还好经过不懈的努力,终于折腾成功了. 如果开发Cordova项目的话,推荐大家用一下ionic这个框架,效果还不错.对于Cordova.PhoneGap.ionic.AngularJS这些框架或库的关系,我个人理解是这样,PhoneGap是一个商业项目,用来实现HTML5式的跨平台开发,后来Adobe公司将其中的核心代码开源,就是Cordova,Cordova只负责实现JavaScript调用原生代码的功能,是一个壳,而壳里具体用什么样式,在H

Win10 UWP开发系列——开源控件库:UWPCommunityToolkit

原文:Win10 UWP开发系列--开源控件库:UWPCommunityToolkit 在开发应用的过程中,不可避免的会使用第三方类库.之前用过一个WinRTXamlToolkit.UWP,现在微软官方发布了一个新的开源控件库—— UWPCommunityToolkit 项目代码托管在Github上:https://github.com/Microsoft/UWPCommunityToolkit 包括以下几个类库: 都可以很方便的从Nuget上安装. NuGet Package Name des

Win10 UWP开发系列:解决Win10不同版本的Style差异导致的兼容性问题

原文:Win10 UWP开发系列:解决Win10不同版本的Style差异导致的兼容性问题 最近在开发一个项目时,遇到了一个奇怪的问题,项目依赖的最低版本是10586,目标版本是14393,开发完毕发布到商店后,很多用户报无法正常加载页面.经查,有问题的都是Win10 10586版本. 我上篇博客中写到的自定义的AppBar控件,也存在这个问题,10586会报错. 为此特意下载了10586的SDK调试.错误显示,一个样式找不到,名为ListViewItemBackground.因为开发的时候是基于

Win10 UWP开发系列:实现Master/Detail布局

在开发XX新闻的过程中,UI部分使用了Master/Detail(大纲/细节)布局样式.Win10系统中的邮件App就是这种样式,左侧一个列表,右侧是详情页面.关于这种 样式的说明可参看MSDN文档:https://msdn.microsoft.com/zh-cn/library/windows/apps/xaml/dn997765.aspx 样式如下: 在微软官方的Sample里,有这种样式的代码示例,下载地址:https://github.com/Microsoft/Windows-univ

Win10 UWP 开发系列:使用SQLite

在App开发过程中,肯定需要有一些数据要存储在本地,简单的配置可以序列化后存成文件,比如LocalSettings的方式,或保存在独立存储中.但如果数据多的话,还是需要本地数据库的支持.在UWP开发中,可以使用SQLite.本篇文章说一下如何在UWP中使用SQLite.因为SQLite是跨平台的,版本众多,我刚开始用的时候不知道要装哪个,什么WP8的.WP8.1的.Win RT的……简直摸不着头脑.希望这篇文章能让大家少走点弯路. 其实这篇文章写到一半就看到已经有大神写了这个:http://ww

Win10 UWP 开发系列:支持异步的SQLite

上篇文章已经实现了在UWP中使用SQLite作为本地存储,作为移动端的程序,及时响应用户的操作是提高用户体验的重要途径,因此UWP的很多api都是异步的.那么如何使SQLite支持异步呢? 参考SQLite.Net-PCL的github页面:https://github.com/oysteinkrog/SQLite.Net-PCL 可以看到SQLite.Net-PCL是支持异步的,在创建数据库链接的时候,可以创建同步的SQLiteConnection,也可以创建异步的SQliteAsyncCon

Win 10 UWP开发系列:设置AppBarButton的图标

在WP8以前,页面最下面的四个小圆按钮是不支持绑定的,WP8.1 RT之后,系统按钮升级成了AppBarButton,并且支持绑定了.在Win10 UWP开发中,按钮的样式发生了变化,外面的圆圈没有了.不过个人还是更喜欢之前的圆按钮的样子^_^ 很喜欢Metro Studio这个程序,有数百个好看的按钮可以用,并且可以导出成png图片.但现在不建议采取png图片的方式了,主要是因为png在缩放后有可能会失真,最好使用字体.path等矢量的方式来实现.以下介绍几种设置AppBarButton图标的

Win10/UWP开发—使用Cortana语音与App后台Service交互

上篇文章中我们介绍了使用Cortana调用前台App,不熟悉的移步到:Win10/UWP开发—使用Cortana语音指令与App的前台交互,这篇我们讲讲如何使用Cortana调用App的后台任务,相比调用前台的App,调用后台任务有个有点就是App不用被启动即可为用户提供服务. 要想使用Cortana调用App后台任务,首先我们需要定义VCD文件,我们依旧使用上篇中的代码,让它支持Cortana调用后台任务. 创建后台任务 新增一个[Windows运行时组件]项目,暂时起名叫做:XiaoMiBa

Force.com微信开发系列(五)自定义菜单进阶及语音识别

在上文里我们介绍了如何通过Force.com平台里为微信账号添加自定义菜单,本文里我们将进一步介绍如何查询菜单以及删除菜单的相关知识,最后会介绍微信平台如何进行语音识别的相关技术. 查询菜单 与创建菜单类似,查询菜单有自己的接口,其中ACESS_TOKEN需要通过前文介绍到的接口获取,通常2小时后会失效: https://api.weixin.qq.com/cgi-bin/menu/get?access_token=[ACCESS_TOKEN] 为此首先通过前文介绍到的获取Access_Toke