Как по координатам найти объект на Canvas'е и переместить его?
1,00
р.
р.
На интерфейсе располагается в центре кнопка, вокруг него элементы типа Image. С виду это выглядит как квадрат 3 на 3. При клике на кнопку окружающие картинки должны сместится на одну клетку при каждом клике. Например, картинка которая находится (объясняю на языке двухмерного массива), которая располагается на позиции 1 0 от кнопки, должна сместится на позицию 0 0, а другая картинка с позиции 0 0, на позицию 0 1. При помощи такого подхода: image10.BeginAnimation(MarginProperty, new ThicknessAnimation { From = new Thickness(99, 66, 361, 212), To = new Thickness(99, 24, 361, 254), Duration = TimeSpan.FromSeconds(0.85) }) мне удается выполнить такое действие до второго клика. После второго клика картинки возвращаются в начальные положения и повторяют действие первого клика, а нужно чтобы при каждом клике ничего не сбивалось и картинки перемещались по кругу. Чтобы реализовать такой подход, я понял, что строго к элементам обращаться не получится, ибо выходит то, что написал выше. Поэтому вопрос таков, как при помощи координат в коде C# мне обратится к объекту, который располагается в данном месте и анимированно переместить в другое место? Как я прочитал в интернете для поиска и перемещения нужно, чтобы все объекты располагались на канвасе. Однако скажу сразу, что я пытался при помощи такого подхода получить координаты. Canvas.SetLeft(image10, 0) Canvas.SetTop(image10, 0) Но при помощи такого подхода не выходит прицепить анимацию перемещения, а она очень важна. P.S. Буду признателен, если озвучите более просто подход выполнения этой задумки.
Ответ Давайте напишем класс, представляющий "плитку": class Tile : Vm { int x public int X { get => x set => Set(ref x, value) } int y public int Y { get => y set => Set(ref y, value) } string caption public string Caption { get => caption set => Set(ref caption, value) } } Вы в своем классе будете хранить картинку или ссылку на нее, я использую просто строковую надпись string Caption Теперь главная ViewModel: class MainVM : Vm { ObservableCollection tiles public ObservableCollection Tiles { get => tiles set => Set(ref tiles, value) } RelayCommand turnCommand public ICommand TurnCommand => turnCommand public MainVM() { Tiles = new ObservableCollection { new Tile { Caption="1", X = 0, Y = 0 }, new Tile { Caption="2", X = 1, Y = 0 }, new Tile { Caption="3", X = 2, Y = 0 }, new Tile { Caption="4", X = 2, Y = 1 }, new Tile { Caption="5", X = 2, Y = 2 }, new Tile { Caption="6", X = 1, Y = 2 }, new Tile { Caption="7", X = 0, Y = 2 }, new Tile { Caption="8", X = 0, Y = 1 }, } turnCommand = new RelayCommand(_ => TurnTiles()) } void TurnTiles() { // Здесь используются фичи C# 7.0 и .NET Framework 4.7 Dictionary<(int, int), (int, int)> transitions = new Dictionary<(int, int), (int, int)> { [(0, 0)] = (0, 1), [(1, 0)] = (0, 0), [(2, 0)] = (1, 0), [(2, 1)] = (2, 0), [(2, 2)] = (2, 1), [(1, 2)] = (2, 2), [(0, 2)] = (1, 2), [(0, 1)] = (0, 2), } foreach (var tile in Tiles) (tile.X, tile.Y) = transitions[(tile.X, tile.Y)] /* Вариант "по-старинке", для тех, у кого по каким-то причинам не работает вариант выше Dictionary, Tuple> transitions = new Dictionary, Tuple> { [Tuple.Create(0, 0)] = Tuple.Create(0, 1), [Tuple.Create(1, 0)] = Tuple.Create(0, 0), [Tuple.Create(2, 0)] = Tuple.Create(1, 0), [Tuple.Create(2, 1)] = Tuple.Create(2, 0), [Tuple.Create(2, 2)] = Tuple.Create(2, 1), [Tuple.Create(1, 2)] = Tuple.Create(2, 2), [Tuple.Create(0, 2)] = Tuple.Create(1, 2), [Tuple.Create(0, 1)] = Tuple.Create(0, 2), } foreach (var tile in Tiles) { var coords = transitions[Tuple.Create(tile.X, tile.Y)] tile.X = coords.Item1 tile.Y = coords.Item2 } */ } } В общем-то все просто, коллекция с "плитками" и команда для их перемещения по кругу. Причем в этом примере использовать ObservableCollection даже не обязательно, вы можете использовать обычный List. transitions - это словарь переходов, ключ словаря - начальные координаты плитки, значение - конечные Теперь займемся представлением. Напишем конвертеры координат VM => View: class XToLeftConverter : IValueConverter { public double TileWidth { get set } public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { return (int)value * TileWidth } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException() } } И class YToTopConverter : IValueConverter { public double TileHeight { get set } public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { return (int)value * TileHeight } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException() } } В них определено публичное свойство, которое будет устанавливаться из разметки. Теперь сама разметка окна, в качестве примера я использовал этот ответ:
Немного кривовато, но пока так. В принципе это уже работает, но, пока, без анимации. При клике по кнопке в центре плитки перепрыгивают по кругу. Теперь это все дело нужно анимировать, я использовал этот ответ. Я пока не знаю, как использовать этот класс-хелпер для анимации двух свойств, поэтому завел два класса с одинаковым содержимым (как появится информация - обновлю ответ): AnimatableLeftHelper и AnimatableTopHelper. Теперь используем эти хелперы чтобы анимировать свойства:
Готово!
Здесь я использовал "стандартные" классы для MVVM WPF Vm: abstract class Vm : INotifyPropertyChanged { protected bool Set(ref T field, T value, [CallerMemberName] string propertyName = null) { if (EqualityComparer.Default.Equals(field, value)) return false field = value NotifyPropertyChanged(propertyName) return true } protected void NotifyPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)) } public event PropertyChangedEventHandler PropertyChanged } RelayCommand: class RelayCommand : ICommand { protected readonly Predicate _canExecute protected readonly Action _execute public event EventHandler CanExecuteChanged public RelayCommand(Action execute) : this(execute, _ => true) { } public RelayCommand(Action execute, Predicate canExecute) { _execute = execute ?? throw new ArgumentNullException(nameof(execute)) _canExecute = canExecute ?? throw new ArgumentNullException(nameof(canExecute)) } public bool CanExecute(object parameter) => _canExecute(parameter) public void Execute(object parameter) => _execute(parameter) public void RaiseCanExecuteChanged() { CanExecuteChanged?.Invoke(this, EventArgs.Empty) } }
Благодаря помощи @VladD'а удалось переписать пример с использованием PointAnimation: "Плитка": class Tile : Vm { Point coords public Point Coords { get => coords set => Set(ref coords, value) } string caption public string Caption { get => caption set => Set(ref caption, value) } } Главная VM: class MainVM : Vm { ObservableCollection tiles public ObservableCollection Tiles { get => tiles set => Set(ref tiles, value) } public ICommand TurnCommand { get } public MainVM() { Tiles = new ObservableCollection { new Tile { Caption="1", Coords = new Point(0, 0) }, new Tile { Caption="2", Coords = new Point(1, 0) }, new Tile { Caption="3", Coords = new Point(2, 0) }, new Tile { Caption="4", Coords = new Point(2, 1) }, new Tile { Caption="5", Coords = new Point(2, 2) }, new Tile { Caption="6", Coords = new Point(1, 2) }, new Tile { Caption="7", Coords = new Point(0, 2) }, new Tile { Caption="8", Coords = new Point(0, 1) } } TurnCommand = new DelegateCommand(_ => TurnTiles()) } void TurnTiles() { Dictionary transitions = new Dictionary { [new Point(0, 0)] = new Point(0, 1), [new Point(1, 0)] = new Point(0, 0), [new Point(2, 0)] = new Point(1, 0), [new Point(2, 1)] = new Point(2, 0), [new Point(2, 2)] = new Point(2, 1), [new Point(1, 2)] = new Point(2, 2), [new Point(0, 2)] = new Point(1, 2), [new Point(0, 1)] = new Point(0, 2), } foreach (var tile in Tiles) tile.Coords = transitions[tile.Coords] } } Конвертер всего один теперь: class XYToPointConverter : IValueConverter { public double TileWidth { get set } public double TileHeight { get set } public double LeftOffset { get set } public double TopOffset { get set } public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { var typedValue = (Point)value return new Point(typedValue.X * TileWidth + LeftOffset, typedValue.Y * TileHeight + TopOffset) } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException() } } Я добавил в него еще пару свойств со смещениями, чтобы настраивать положение плиток можно было более гибко. Разметка окна:
Ну и класс-хелпер для анимации: static class AnimatablePointHelper { public static Point GetOriginalProperty(DependencyObject obj) => (Point)obj.GetValue(OriginalPropertyProperty) public static void SetOriginalProperty(DependencyObject obj, Point value) => obj.SetValue(OriginalPropertyProperty, value) public static readonly DependencyProperty OriginalPropertyProperty = DependencyProperty.RegisterAttached( "OriginalProperty", typeof(Point), typeof(AnimatablePointHelper), new PropertyMetadata(OnOriginalUpdatedStatic)) public static Point GetAnimatedProperty(DependencyObject obj) => (Point)obj.GetValue(AnimatedPropertyProperty) public static void SetAnimatedProperty(DependencyObject obj, Point value) => obj.SetValue(AnimatedPropertyProperty, value) public static readonly DependencyProperty AnimatedPropertyProperty = DependencyProperty.RegisterAttached( "AnimatedProperty", typeof(Point), typeof(AnimatablePointHelper)) static void OnOriginalUpdatedStatic(DependencyObject o, DependencyPropertyChangedEventArgs e) { Point newValue = (Point)e.NewValue FrameworkElement self = (FrameworkElement)o AnimationTimeline animation = new PointAnimation(newValue, new Duration(TimeSpan.FromSeconds(0.3))) self.BeginAnimation(AnimatedPropertyProperty, animation) } } Результат тот же.