четверг, 28 февраля 2013 г.

Windows Store. Popup AppBar Button

На сайте MSDN есть статья How to add a menu to an app bar, в которой рассказывается как сделать дополнительное меню в AppBar приложения для магазина Windows. Я решил немного улучшить данный вариант и сделал элемент управления, который автоматически показывал бы всплывающее меню с нужным мне контентом.
Собственно что из этого получилось я покажу на скриншоте и уже потом немного опишу весь процесс.



Для начала я наследовал новый класс от класса Button. Учитывая то, что контент будет отображаться либо снизу (в случае если это Page.TopAppBar) либо сверху (если Page.BottomAppBar), я добавил насколько вспомогательных свойств зависимостей:

Code:
    #region DependencyProperty Members

    /// <summary>
    /// Identifies the HorizontalOffset dependency property.
    /// </summary>
    public static readonly DependencyProperty HorizontalOffsetProperty = DependencyProperty.Register("HorizontalOffset", typeof(double), typeof(PopupAppBarButton), new PropertyMetadata(0.0));

    /// <summary>
    /// Get or sets the horizontal distance between the target origin and the popup alignment point.
    /// </summary>
    public double HorizontalOffset
    {
        get { return (double)GetValue(HorizontalOffsetProperty); }
        set { SetValue(HorizontalOffsetProperty, value); }
    }

    /// <summary>
    /// Identifies the VerticalOffset dependency property.
    /// </summary>
    public static readonly DependencyProperty VerticalOffsetProperty = DependencyProperty.Register("VerticalOffset", typeof(double), typeof(PopupAppBarButton), new PropertyMetadata(0.0));

    /// <summary>
    /// Gets or sets the vertical distance between the target origin and the popup alignment point.
    /// </summary>
    public double VerticalOffset
    {
        get { return (double)GetValue(VerticalOffsetProperty); }
        set { SetValue(VerticalOffsetProperty, value); }
    }

    /// <summary>
    /// Identifies the Placement dependency property.
    /// </summary>
    public static readonly DependencyProperty PlacementProperty = DependencyProperty.Register("Placement", typeof(Placement), typeof(PopupAppBarButton), new PropertyMetadata(Placement.Default));

    /// <summary>
    /// Gets or sets the orientation of the Popup control when the control opens, and specifies the behavior of the Popup control when it overlaps screen boundaries.
    /// </summary>
    public Placement Placement
    {
        get { return (Placement)GetValue(PlacementProperty); }
        set { SetValue(PlacementProperty, value); }
    }

    /// <summary>
    /// Identifies the Popup dependency property.
    /// </summary>
    public static readonly DependencyProperty PopupProperty = DependencyProperty.Register("Popup", typeof(FrameworkElement), typeof(PopupAppBarButton), new PropertyMetadata(null, PopupPropertyChangedCallback));

    /// <summary>
    /// Gets or sets a pop-up window that has content.
    /// </summary>
    public FrameworkElement Popup
    {
        get { return (FrameworkElement)GetValue(PopupProperty); }
        set { SetValue(PopupProperty, value); }
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="dependencyObject"></param>
    /// <param name="dependencyPropertyChangedEventArgs"></param>
    private static void PopupPropertyChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
    {
        var target = (PopupAppBarButton)dependencyObject;
        if (target == null) return;
        target.CreatePopup();
    }

    /// <summary>
    /// Identifies the <see cref="IsOpen"/> dependency property.
    /// </summary>
    public static readonly DependencyProperty IsOpenProperty = DependencyProperty.Register("IsOpen", typeof(Boolean), typeof(PopupAppBarButton), new PropertyMetadata(false, IsOpenPropertyChangedCallback));

    /// <summary>
    /// An implementation of <see cref="Boolean"/> designed to be used as is open.
    /// </summary>
    public Boolean IsOpen
    {
        get { return (Boolean)GetValue(IsOpenProperty); }
        set { SetValue(IsOpenProperty, value); }
    }

    /// <summary>
    /// Represents the callback that is invoked when the effective property value of a dependency property changes.
    /// </summary>
    /// <param name="dependencyObject">The DependencyObject on which the property has changed value.</param>
    /// <param name="dependencyPropertyChangedEventArgs">Event data that is issued by any event that tracks changes to the effective value of this property.</param>
    private static void IsOpenPropertyChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
    {
        var target = (PopupAppBarButton)dependencyObject;
        if (target.IsOpen) target.Show();
        else target.Hide();
    }

    #endregion

Так же добавил элемент Popup, который будет содержать сам контент и свойство IsOpen, при изменении которого контент будет открываться или закрываться.
Теперь остается только правильно рассчитать положения всплывающего меню:

Code:
    #region Private Members

    /// <summary>
    /// 
    /// </summary>
    private void CreatePopup()
    {
        popup = new Popup
        {
            IsLightDismissEnabled = true,
            ChildTransitions = new TransitionCollection { new PopupThemeTransition() },
            Child = Popup
        };
        popup.Closed += delegate
        {
            if (IsOpen) IsOpen = false;
            var appBar = this.GetFirstAncestorOfType<AppBar>();
            if (appBar != null) appBar.IsOpen = false;
        };
    }

    /// <summary>
    /// Show the popup on the screen.
    /// </summary>
    private void Show()
    {
        if (popup == null) return;
        popup.DataContext = DataContext;
        Popup.WaitForNonZeroSizeAsync().ContinueWith(task =>
        {
            var point = CalculateOffset();
            popup.HorizontalOffset = point.X;
            popup.VerticalOffset = point.Y;
        }, TaskContinuationOptions.ExecuteSynchronously);
        popup.IsOpen = true;
    }

    /// <summary>
    /// Hide the popup on the screen.
    /// </summary>
    private void Hide()
    {
        if (popup == null) return;
        if (popup.IsOpen) popup.IsOpen = false;
    }

    /// <summary>
    /// 
    /// </summary>
    /// <returns></returns>
    private Point CalculateOffset()
    {
        // Get a practical transform
        var transform = TransformToVisual(Window.Current.Content);
        var offset = transform.TransformPoint(default(Point));
        // Calculate the offset in the X
        var offsetX = offset.X - (Popup.ActualWidth - ActualWidth) / 2;
        if (offsetX < HorizontalOffset) offsetX = HorizontalOffset;
        if (offsetX + Popup.ActualWidth > Window.Current.Bounds.Right - HorizontalOffset)
            offsetX = Window.Current.Bounds.Right - Popup.ActualWidth - HorizontalOffset;
        // Calculate the offset in the Y
        double offsetY = offset.Y;
        switch (Placement)
        {
            case Placement.Above:
                offsetY = offset.Y - Popup.ActualHeight - VerticalOffset;
                break;
            case Placement.Below:
                offsetY = offset.Y + ActualHeight + VerticalOffset;
                break;
            case Placement.Left:
                break;
            case Placement.Right:
                break;
            case Placement.Default:
                break;
        }
        // Return value
        return new Point(offsetX, offsetY);
    }

    #endregion

Остальные классы и методы расширений (такие как WaitForNonZeroSizeAsync и GetFirstAncestorOfType) вы можете найти в исходном коде проекта.
Использовать даннный элемент управления достаточно просто:

Code:
    <Page.TopAppBar>
        <AppBar x:Name="topAppBar" Padding="10,0,10,0">
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="50*"/>
                    <ColumnDefinition Width="50*"/>
                    <ColumnDefinition Width="50*"/>
                </Grid.ColumnDefinitions>
                <StackPanel x:Name="TopLeftPanel" Orientation="Horizontal" Grid.Column="0" HorizontalAlignment="Left">
                    <local:PopupAppBarButton Placement="Below" VerticalOffset="4" HorizontalOffset="4" IsTabStop="False" Style="{StaticResource HomeAppBarButtonStyle}">
                        <local:PopupAppBarButton.Popup>
                            <Border Background="{StaticResource AppBarBackgroundThemeBrush}" Width="250" Height="250">
                                <StackPanel VerticalAlignment="Center">
                                    <TextBlock Text="Placement = Below" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="18"/>
                                    <TextBlock Text="VerticalOffset = 4"  VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="18"/>
                                    <TextBlock Text="HorizontalOffset = 4"  VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="18"/>
                                </StackPanel>
                            </Border>
                        </local:PopupAppBarButton.Popup>
                    </local:PopupAppBarButton>
                </StackPanel>
            </Grid>
        </AppBar>
    </Page.TopAppBar>

Исходный код

Комментариев нет:

Отправить комментарий