1

я новичок в wpf, мне нужен простой пример "drop menu" при наведении на неё курсора (необязательно с анимацией). Пока что я находил только триггеры с изменениями стилей. Заранее спасибо =)

ekrosz
  • 13

2 Answers2

5

WPF, это в первую очередь привязки и стили, если углубиться чуть сильнее в это все, то вы узнаете про такое понятие, как MVVM, это подход, где все разделено на слои и все цвета, анимации, размеры и так далее располагаются в View слое (то есть XAML).

Для начала давайте подготовим проект, добавив туда все необходимое:

  1. Зададим DataContext
    DataContext - это источник основных данных, из которого приложение должно брать все свойства для привязки. Тут советую почитать этот ответ, я же буду делать все прям в окне.

    • Заходим в MainWindow.xaml.cs и добавляем туда после InitializeComponent(); строку:

      DataContext = this;
      

      Так мы указали приложению, что источником данных будет окно MainWindow.

  2. Реализуем INotifyPropertyChanged
    Он позволяет оповестить интерфейс о том, что свойство в коде изменило свое значение. Его реализаций в интернете полно, я возьму самую простую, после которой класс окна будет выглядеть так:

    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        public MainWindow()
        {
            InitializeComponent();
            DataContext = this;
        }
    
        public event PropertyChangedEventHandler PropertyChanged;
        public void OnPropertyChanged([CallerMemberName]string prop = default) 
            => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
    }
    
  3. Последняя реализация, которая нам понадобиться, это ICommand.
    Данный интерфейс позволяет не использовать события напрямую, а сделать свойство, которое обработает нам необходимый клик по кнопке. Реализаций тоже уйма, я возьму самую простейшую:

    public class RelayCommand : ICommand
    {
        private Action action;
        public RelayCommand(Action action) => this.action = action;
        public bool CanExecute(object parameter) => true;
        #pragma warning disable CS0067
        public event EventHandler CanExecuteChanged;
        #pragma warning restore CS0067
        public void Execute(object parameter) => action();
    }
    

Собственно с подготовительными работами мы завершили. Теперь давайте сделаем само меню:

  • Для начала в классе, который установлен как DataContext создаем свойство. Это bool значение, которое будет говорить открыто меню или нет. Так, как это свойство меняется через код, оно должно вызвать INPC для применения изменений в интерфейсе.

    private bool isOpen;
    public bool IsOpen
    {
        get => isOpen;
        set
        {
            isOpen = value;
            OnPropertyChanged();
        }
    }
    
  • Далее реализуем команду, которая будет просто менять это bool значение.

    • Создаем свойство:

      public ICommand MenuCommand { get; }
      
    • В конструкторе окна инициализируем его:

      MenuCommand = new RelayCommand(() => IsOpen = !IsOpen);
      

      () => IsOpen = !IsOpen эту логику можно вынести в отдельный void метод и тут прописать лишь его имя.

Все, с кодом мы закончили. Конечный результат у нас такой:

public class RelayCommand : ICommand
{
    private Action action;
    public RelayCommand(Action action) => this.action = action;
    public bool CanExecute(object parameter) => true;
    #pragma warning disable CS0067
    public event EventHandler CanExecuteChanged;
    #pragma warning restore CS0067
    public void Execute(object parameter) => action();
}

public partial class MainWindow : Window, INotifyPropertyChanged
{
    private bool isOpen;
    public bool IsOpen
    {
        get => isOpen;
        set
        {
            isOpen = value;
            OnPropertyChanged();
        }
    }

    public ICommand MenuCommand { get; }

    public MainWindow()
    {
        InitializeComponent();
        DataContext = this;
        MenuCommand = new RelayCommand(() => IsOpen = !IsOpen);
    }

    public event PropertyChangedEventHandler PropertyChanged;
    public void OnPropertyChanged([CallerMemberName]string prop = default)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
}

Как видите тут нет каких либо анимаций, нет каких либо размеров, цветов или еще чего, лишь логика изменения bool значения, все. INPC и ICommand обычно выносятся в отдельные классы, которые раз создаются и дальше про них забывают.

Теперь нам нужен интерфейс, переходим в MainWindow.xaml и делаем необходимый нам дизайн.
Я сделаю допустим такой:

<Grid>
    <!--#region Основной контент-->
    <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
        <TextBlock FontSize="30" Text="Основной контент" />
        <Button Content="Открыть/Закрыть меню" />
    </StackPanel>
    <!--#endregion-->

    <!--#region Меню-->
    <Border
        Padding="15"
        HorizontalAlignment="Left"
        Background="#FF1AB98E">
        <StackPanel>
            <TextBlock FontSize="17" Text="Элемент 1" />
            <TextBlock FontSize="17" Text="Элемент 2" />
            <TextBlock FontSize="17" Text="Элемент 3" />
        </StackPanel>
    </Border>
    <!--#endregion-->
</Grid>

Это даст нам такой результат:

UI

Осталось дело за малым, скрываем/показываем меню. Для этого есть триггеры, которые позволяют взять значение свойства и на его основе изменить что либо в интерфейсе.

  • Прячем меню. Основной подход тут простой - унести его за пределы окна. Если вы будете менять размер панели, то весь контент будет у вас ездить, что не очень хорошо скажется как на производительности, так и на эстетике. По этой причине задаем нашему меню Margin с минусовым значением в нужною сторону.

    Margin="-150 0 0 0"
    

    Как видите, панель в дизайнере есть, но она не видна пользователю при запущенном приложение.

    UI Out

  • Задаем меню триггер: Тут все просто, если свойство IsOpen будет True, то убираем Margin. Стоит учесть, что все триггеры, кроме EventTrigger задаются в стиле объекта. Также не забывает про уровни в WPF проекте, то есть все, что мы хотим изменить триггером должно быть внутри стиля. Исходя из всего мы получает такое:

    <Border.Style>
        <Style TargetType="Border">
            <Setter Property="Margin" Value="-150 0 0 0"/>
            <Style.Triggers>
                <DataTrigger Binding="{Binding IsOpen}" Value="True">
                    <Setter Property="Margin" Value="0" />
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </Border.Style>
    
  • Осталось кнопке задать команду, просто пишем у нее Command="{Binding MenuCommand}".

Запускаем и смотрим результат:

Result (no anim)


Анимации и другие навороты.

Имея все это мы можем делать дальше что захотим, например добавить анимацию:

  • Создаем в ресурсах (например окна) анимации закрытия и открытия. Для простоты просто сделаем плавное изменение свойства Margin:

    <Window.Resources>
        <Storyboard x:Key="OpenMenuAnimation">
            <ThicknessAnimation
                Storyboard.TargetProperty="Margin"
                To="0"
                Duration="0:0:0.3" />
        </Storyboard>
        <Storyboard x:Key="CloseMenuAnimation">
            <ThicknessAnimation
                Storyboard.TargetProperty="Margin"
                To="-150 0 0 0"
                Duration="0:0:0.3" />
        </Storyboard>
    </Window.Resources>
    
  • В триггере меню замени Setter на

    <DataTrigger.EnterActions>
        <BeginStoryboard Storyboard="{StaticResource OpenMenuAnimation}" />
    </DataTrigger.EnterActions>
    <DataTrigger.ExitActions>
        <BeginStoryboard Storyboard="{StaticResource CloseMenuAnimation}" />
    </DataTrigger.ExitActions>
    

Получаем в итоге плавную анимацию появления меню.

UI (Anim)

По такому же принципу мы можем например изменять текст на кнопке, задав нужный триггер, кнопка будет тогда такой:

<Button Command="{Binding MenuCommand}">
    <Button.Style>
        <Style TargetType="Button">
            <Setter Property="Content" Value="Открыть меню" />
            <Style.Triggers>
                <DataTrigger Binding="{Binding IsOpen}" Value="True">
                    <Setter Property="Content" Value="Закрыть меню" />
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </Button.Style>
</Button>

Button Trigger

Как видите, создав всего одно маленькое свойство в коде мы полностью все перенесли на XAML, в стили, а про свойство мы и забыли. Вот это и есть вся мощь WPF, привязки и стили, помните про это! Если же вы будете разрабатывать интерфейс в коде, то вы и ваше приложение начнете в скором времени страдать, как в производительности, так и в удобстве использования и написания кода. Например ответ выше, Dispatcher, если у вас грамотно построенный проект, то он вам не понадобиться, если же нет, то придется использовать такие вот "костыли", которые будут перекидывать контекст с одного потока в другой.

В общем, удачи в изучение могущего WPF и помните, это не WinForms!

EvgeniyZ
  • 15,694
  • Отличный ответ, но советую не флудить, ибо посмотрев это https://youtu.be/X3yTH0i3jBA , вы увидите, что запоздали. Но факт того, что я подал вам тонус на отличные ответы меня радует – GromWolf May 02 '20 at 12:00
  • 2
    @GromWolf, предлагаете пересмотреть весь ютуб и никогда больше не писать развернутых ответов, потому что наверняка кто-то уже сделал это раньше? Ну и ответы, которые показывают ход мыслей полезны не менее чем ответ с готовым решением (а то и более, помните поговорку про кормление голодных рыбами?) – Андрей NOP May 03 '20 at 08:01
  • 1
    @GromWolf на SO приветствуются полноценные ответы, а вот ответы-ссылки нет. – Suvitruf - Andrei Apanasik May 03 '20 at 08:38
  • Спасибо большое, я все прочитал и попробовал реализовать на примере, всё получилось. Для меня это послужило уроком. Спасибо большое =) – ekrosz May 04 '20 at 10:49
  • А как можно сделать выдвижную панель, которая будет выходить слева? Т.е за окно – Aarnihauta Nov 15 '21 at 04:49
  • @AlAvenger В WPF нельзя рисовать за пределами окна. Так что, стандартными способами, наверно, это невозможно. Выходом может быть реализация прозрачного окна (AllowsTransparency = true), где будет нарисован свой стиль окна с указанием отступов по бокам, тогда в видимой области будет некий объект окна, занимающий не всю площать, а остальное место под выдвижное меню. Но это чревато проблемами (например растягивание на весь экран). Другим выходом может быть PopUp, он рисуется изначально за пределами области видимости, но там тоже свои тонкости. Вообщем, я бы вам не рекомендовал это делать) – EvgeniyZ Nov 15 '21 at 12:09
  • @AlAvenger Ну а так, это стоит задать в виде отдельного вопроса, может есть решение, о котором я не знаю. – EvgeniyZ Nov 15 '21 at 12:10
  • @EvgeniyZ спасибо, я принял решение просто через прозрачное окно делать – Aarnihauta Nov 15 '21 at 12:15
-3

Доброе время суток.

Решений, как всегда, масса. Вы можете воспользоваться стандартным ComboBox`ом, либо написать свой кастомный контрол.

С учётом уточнений из комментариев предлагаю следующее решение:

 public void PanelOpen()
        {
            this.Dispatcher.Invoke(() =>
            {
                DoubleAnimation menuAnimation = new DoubleAnimation();
                menuAnimation.From = 0;
                menuAnimation.To = 100;
                menuAnimation.Duration = TimeSpan.FromSeconds(0.5);
                menuAnimation.EasingFunction = new QuadraticEase();
                ControlMenu.BeginAnimation(HeightProperty, menuAnimation);
            });
        }
        public void PanelClose()
        {
            this.Dispatcher.Invoke(() =>
            {
                DoubleAnimation menuAnimation = new DoubleAnimation();
                menuAnimation.From = ControlMenu.ActualHeight;
                menuAnimation.To = 0;
                menuAnimation.Duration = TimeSpan.FromSeconds(0.5);
                menuAnimation.EasingFunction = new QuadraticEase();
                ControlMenu.BeginAnimation(HeightProperty, menuAnimation);
            });
        }

В XAML`е создаёте Panel с названием ControlMenu, в Event MouseEnter присвоите ему функцию PanelOpen и на MouseLeave, соответственно, PanelClose, и попробуйте запустить такую функцию. Вы увидите, как работает такая анимация, и под свой случай уже будите вольны переделать величины анимации и разные триггеры присвоить, короче говоря, адаптировать под себя.

Вот Как и обещал ссылка на анимации

С уважением и удачи.

GromWolf
  • 180
  • 11