Как изменить цвет у динамически созданной фигуры, добавленной в WrapPanel wpf?

Существует последовательность фигур определённого цвета, которая создаётся динамически при нажатии кнопки, а так же добавляется в WrapPanel.

private void BoxGenerator(int length)
    {
        Path boxpath = new Path();
        PathGeometry boxgeom = new PathGeometry();
        PathFigure boxfigure = new PathFigure();

        LineSegment ln1 = new LineSegment();
        LineSegment ln2 = new LineSegment();
        LineSegment ln3 = new LineSegment();

        
       


        boxfigure.StartPoint = new Point(0, 5);
        ln1.Point = new Point(0, -5 * length);
        ln2.Point = new Point(20, -5 * length);
        ln3.Point = new Point(20, 5);
        boxfigure.IsClosed = true;
        boxfigure.Segments.Add(ln1);
        boxfigure.Segments.Add(ln2);
        boxfigure.Segments.Add(ln3);
        boxgeom.Figures.Add(boxfigure);

        boxpath.Data = boxgeom;
        boxpath.Stroke = Brushes.Green;
        boxpath.StrokeThickness = 5;
        boxpath.Fill = new SolidColorBrush(Color.FromRgb(0, 0, 0));           
        PanelBoxes.Children.Add(boxpath);

    } 
private void InitialBut_Click(object sender, RoutedEventArgs e)
    {

        PanelBoxes.Children.Clear();
        string text = NumberBox.Text.Trim().ToString();
        text = Regex.Replace(text, "[ ]+", " ");

        bool error = false;
        if (text == "")
        {
            ErrorLabel.Content = "Заполните ячейку ввода цифр";
            return;
        }
        foreach (char symbol in text)
        {
            
            if(!Char.IsDigit(symbol) && (symbol != 32) && (symbol != 45))
            {
                error = true;
                
            }
        }
        if (error)
        {
            ErrorLabel.Content = "В наборе содержатся лишние символы, повторите ввод.";
            NumberBox.Text = "";
            return;
        }else
        {
            ErrorLabel.Content = "Полученная последовательность:"+text;
            
        }
        
        
        foreach (int elem in text.Split(' ').Select(int.Parse))
        {
            unsortList.Add(elem);
            BoxGenerator(elem);
        }
        
        
    }

Далее, по нажатию кнопки я пытаюсь изменить цвет некоторых блоков, но мне не удаётся получить к ним доступ, т.к. они находятся в WrapPanel. Вопрос: Как получить к ним доступ и как изменить у определённых элементов цвет? Пытался сделать так, но безуспешно

Path box = PanelBoxes.Children.OfType<Path>().First();
        box.Fill = new SolidColorBrush(Color.FromRgb(100, 100, 0));

Должно быть так: При нажатии кнопки(выделена красным), менялся цвет блока (выделено синим)Изменение цвета блока


Ответы (1 шт):

Автор решения: aepot

Несмотря на то, что вопрос явно не содержит источника проблемы, которую вы пытаетесь победить, задача выглядит интересно, и я покажу ее решение. Проблема скорее всего в том, что в процессе сортировки вы исполняете синхронный код, и интерфейс просто не обновляется до тех пор, пока метод не закончит свою работу. Для исправления данной проблемы лучше использовать асинхронный подход к реализации (см. Асинхронное программирование).

Второй момент заключается в том, что вы используете подход, не являющийся эффективным в WPF. Так как в процессе написания кода вам приходится взаимодействовать напрямую с контролами, а интерфейс контролов не оптимизирован для такого взаимодействия в WPF, он просто неудобен. Такой способ реализации больше подходит к WinForms.

В WPF для максимального удобства при написании кода лучше всего использовать привязки Binding, команды ICommand и отделить логику работы программы от самого интерфейса приложения, оповещая его с помощью реализации интерфейса INotifyPropertyChanged. А арихитектурно эти методики удобнее всего использовать в рамках шаблона программирования MVVM. Этот шаблон программирования очень популярен в WPF и UWP приложениях, и если интересно, вы сможете найти очень много примеров его применения, в том числе здесь на StackOverflow на русском.

Я написал демонстрационное приложение, которое делает примерно то, что вы задумали.

Часть 1. Вспомогательные классы

Эти классы предназначены для удобства при использовании команд и оповещения интерфейса.

NotifyPropertyChanged.cs

public class NotifyPropertyChanged : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

Я буду наследоваться от этого класса там, где мне надо оповещать интерфейс об изменениях свойства, чтобы сам интерфейс себя обновил. Делать это смогу с помощью простого вызова OnPropertyChanged() (см. код ниже).

RelayCommand.cs

public class RelayCommand : ICommand
{
    private readonly Action<object> _execute;
    private readonly Predicate<object> _canExecute;

    public event EventHandler CanExecuteChanged
    {
        add => CommandManager.RequerySuggested += value;
        remove => CommandManager.RequerySuggested -= value;
    }

    public RelayCommand(Action<object> execute, Predicate<object> canExecute = null)
        => (_execute, _canExecute) = (execute, canExecute);

    public bool CanExecute(object parameter)
        => _canExecute == null || _canExecute(parameter);

    public void Execute(object parameter)
        => _execute(parameter);
}

Родной API команд в WPF не то чтобы максимально удобен, и чтобы сделать реализацию команд в коде максимально простой, я буду использовать этот класс для создания экземпляра команды. Если кратко, то команды - это то что я буду использовать вместо обработчиков события Click в кнопках, но их много где в WPF можно использовать.

Часть 2. Реализация квадратика

Square.cs

public class Square : NotifyPropertyChanged
{
    private bool _isActive;

    public int Number { get; } // сюда привязано число внутри квадратика

    public int Size => 20 + Number * 5; // сюда привязан размер квадратика

    public bool IsActive // сюда привязан цвет фона квадратика
    {
        get => _isActive;
        set
        {
            _isActive = value;
            OnPropertyChanged();
        }
    }

    public Square(int number)
    {
        Number = number;
    }
}

Эта модель позволяет интерфейсу нарисовать квадрат нужного размера и вписать в него нужное число.

Часть 3. View Model

MainViewModel.cs

public class MainViewModel : NotifyPropertyChanged
{
    private static readonly Random _rnd = new Random();

    private ObservableCollection<Square> _numbers;
    private ICommand _shuffleItems;
    private ICommand _sortItems;
    private bool _buttonsEnabled = true;

    // задержка анимации квадратиков в миллисекундах
    public int Delay { get; set; } = 250;

    // сюда привязана кнопка, перемешивающая квадратики
    public ICommand ShuffleItems => _shuffleItems ?? (_shuffleItems = new RelayCommand(parameter =>
    {
        ShuffleNumbers();
    }));

    // сюда привязана кнопка, сортирующая квадратики
    public ICommand SortItems => _sortItems ?? (_sortItems = new RelayCommand(parameter =>
    {
        SortNumbers();
    }));

    // коллекция квадратиков
    public ObservableCollection<Square> Numbers
    {
        get => _numbers;
        set
        {
            _numbers = value;
            OnPropertyChanged();
        }
    }

    // сюда привязана доступность кнопок, для защиты от повторного нажатия
    public bool ButtonsEnabled
    {
        get => _buttonsEnabled;
        set
        {
            _buttonsEnabled = value;
            OnPropertyChanged();
        }
    }

    public MainViewModel() // конструктор
    {
        Numbers = new ObservableCollection<Square>(NumberSequence(1, 10));
    }

    // меняет цвет фона перемещаемого квадратика и меняет его местами с другим квадратиком
    private async Task ExchangeNumberAsync(int oldIndex, int newIndex)
    {
        if (oldIndex == newIndex) 
            return;

        Numbers[oldIndex].IsActive = true; // перекрашивает квадратик в красный

        if (Delay > 0) 
            await Task.Delay(Delay);

        Square item = Numbers[oldIndex];
        Numbers[oldIndex] = Numbers[newIndex];
        Numbers[newIndex] = item;

        if (Delay > 0) 
            await Task.Delay(Delay);

        Numbers[newIndex].IsActive = false; // перекрашивает квадратик в черный

        if (Delay > 0) 
            await Task.Delay(Delay);
    }

    // перемешивает квадратики
    private async void ShuffleNumbers()
    {
        ButtonsEnabled = false;
        try
        {
            for (int i = 0; i < Numbers.Count; i++)
            {
                await ExchangeNumberAsync(i, _rnd.Next(0, Numbers.Count));
            }
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.Message);
        }
        finally
        {
            ButtonsEnabled = true;
        }
    }

    // сортирует квадратики пузырьком
    private async void SortNumbers()
    {
        ButtonsEnabled = false;
        try
        {
            for (int i = 0; i < Numbers.Count; i++)
            {
                for (int j = i + 1; j < Numbers.Count; j++)
                {
                    if (Numbers[i].Number > Numbers[j].Number)
                    {
                        await ExchangeNumberAsync(i, j);
                    }
                }
            }
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.Message);
        }
        finally
        {
            ButtonsEnabled = true;
        }
    }

    // если не в курсе, то обязательно поинтересуйтесь, как работают энумераторы
    private IEnumerable<Square> NumberSequence(int start, int length)
    {
        for (int i = start; i < start + length; i++)
        {
            yield return new Square(i);
        }
    }
}

Часть 4. View

А вот и полная разметка интерфейса.

MainWindow.xaml

<Window x:Class="SortTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:SortTest"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <local:MainViewModel/><!-- здесь подключается View Model -->
    </Window.DataContext>
    <Window.Resources>
        <Style TargetType="Button">
            <Setter Property="Margin" Value="5"/>
            <Setter Property="Padding" Value="10,5"/>
        </Style>
    </Window.Resources>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <StackPanel>
            <Button Content="Перемешать" IsEnabled="{Binding ButtonsEnabled}" Command="{Binding ShuffleItems}"/>
            <Button Content="Сортировать" IsEnabled="{Binding ButtonsEnabled}" Command="{Binding SortItems}"/>
        </StackPanel>
        <ItemsControl Grid.Column="1" ItemsSource="{Binding Numbers}">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Border Width="{Binding Size}" Height="{Binding Size}" BorderBrush="Green" BorderThickness="2" Margin="3">
                        <Border.Style>
                            <Style TargetType="Border">
                                <Setter Property="Background" Value="Black"/>
                                <Style.Triggers>
                                    <DataTrigger Binding="{Binding IsActive}" Value="True">
                                        <Setter Property="Background" Value="Red"/>
                                    </DataTrigger>
                                </Style.Triggers>
                            </Style>
                        </Border.Style>
                        <TextBlock Text="{Binding Number}" Foreground="White" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="18" FontWeight="Bold"/>
                    </Border>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <WrapPanel/>
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
        </ItemsControl>
    </Grid>
</Window>

Вот, собственно и всё. В классе MainWindow у меня ничего нет, там пусто.

MainWindows.xaml.cs

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }
}

введите сюда описание изображения

P.S. Опытные разработчики, не ругайте меня за async void, здесь он использован безопасно, использовал его для упрощения примера. Для автора вопроса: в статье по ссылке в начале ответа рассказано, почему async void - плохо. Так же способ указания DataContext здесь самый простой, но не оптимальный, подробнее можно узнать здесь.

→ Ссылка