Изменение фоновых свойств нескольких элементов управления с помощью кнопки
Я работаю над приложением, которое имеет много кнопок в главном окне.
Кнопки были запрограммированы индивидуально для изменения цвета при нажатии и сохранения этих цветов с помощью пользовательских настроек Visual Studio.
Точнее, когда пользователь нажимает кнопку один раз, ее фон меняется на красный, а когда он нажимает ее снова, фон меняется на зеленый.
Отредактировано для mm8:
Вот xaml (образец):
<Window x:Class="test2.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:test2"
xmlns:properties="clr-namespace:test2.Properties"
mc:Ignorable="d"
Title="MainWindow" WindowStartupLocation="CenterScreen" Height="850" Width="925">
<Grid x:Name="theGrid">
<Button x:Name="Button0" HorizontalAlignment="Left" Margin="197,139,0,0" VerticalAlignment="Top" Width="66" Height="26" Focusable="False" Background="{Binding Source={x:Static properties:Settings.Default}, Path=Color0, Mode=TwoWay}" Click="Button0_Click"/>
<Button x:Name="Button1" HorizontalAlignment="Left" Margin="131,139,0,0" VerticalAlignment="Top" Width="66" Height="26" Focusable="False" Background="{Binding Source={x:Static properties:Settings.Default}, Path=Color1, Mode=TwoWay}" Click="Button1_Click"/>
<Button x:Name="Button2" HorizontalAlignment="Left" Margin="263,139,0,0" VerticalAlignment="Top" Width="66" Height="26" Focusable="False" Background="{Binding Source={x:Static properties:Settings.Default}, Path=Color2, Mode=TwoWay}" Click="Button2_Click"/>
<Button x:Name="Reset" Content="Reset" HorizontalAlignment="Left" Margin="832,788,0,0" VerticalAlignment="Top" Width="75" Click="Reset_Click" />
</Grid>
</Window>
И вот код, который я внедрил в событие нажатия каждой кнопки:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.IO;
namespace test2
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void Button0_Click(object sender, RoutedEventArgs e)
{
if (Properties.Settings.Default.Color0 == "Green")
{
Properties.Settings.Default.Color0 = "Red";
Properties.Settings.Default.Save();
}
else
{
Properties.Settings.Default.Color0 = "Green";
Properties.Settings.Default.Save();
}
}
private void Button1_Click(object sender, RoutedEventArgs e)
{
if (Properties.Settings.Default.Color1 == "Green")
{
Properties.Settings.Default.Color1 = "Red";
Properties.Settings.Default.Save();
}
else
{
Properties.Settings.Default.Color1 = "Green";
Properties.Settings.Default.Save();
}
}
private void Button2_Click(object sender, RoutedEventArgs e)
{
if (Properties.Settings.Default.Color2 == "Green")
{
Properties.Settings.Default.Color2 = "Red";
Properties.Settings.Default.Save();
}
else
{
Properties.Settings.Default.Color2 = "Green";
Properties.Settings.Default.Save();
}
}
private void Reset_Click(object sender, RoutedEventArgs e)
{
foreach (Button button in theGrid.Children.OfType<Button>())
}
}
}
Теперь я хочу какую-то кнопку сброса, которая при нажатии меняет фон всех кнопок на значения по умолчанию (не красный и не зеленый).
Я пытался использовать идеи из этой темы и использовать их как событие нажатия кнопки сброса, но всякий раз, когда я это делаю
foreach (Control x in Control.Controls)
или любой другой метод, использующий "Controls" (this.Controls и т. д.). Я подчеркиваю его красным, говоря, что класс Control не имеет определения.
Я делаю что-то неправильно? Ребята, есть ли у вас какие-либо предложения относительно того, как я могу запрограммировать эту кнопку для изменения фона всех кнопок по умолчанию?
2 ответа
Короткая версия: вы делаете это неправильно. Я имею в виду, я подозреваю, что вы уже знали это в некоторой степени, потому что код не работал. Но, глядя на ваш комментарий, в котором говорится, что у вас будет 240 кнопок, вы действительно ошибаетесь.
Этот ответ предназначен для ознакомления с тремя различными вариантами, каждый из которых приближает вас к наилучшему подходу для решения этого сценария.
Начиная с ваших первоначальных усилий, мы можем заставить отправленный вами код работать в основном как есть. Ваша главная проблема в том, что, успешно получив каждый Button
ребенок твой Grid
Вы не можете просто установить Button.Background
имущество. Если вы это сделаете, вы удалите привязку, установленную в XAML.
Вместо этого вам нужно сбросить значения в исходных данных, а затем принудительно обновить цель привязки (поскольку Settings
объект не предоставляет WPF-совместимый механизм уведомления об изменении свойств). Вы можете сделать это, изменив Reset_Click()
способ выглядеть так:
private void Reset_Click(object sender, RoutedEventArgs e)
{
Settings.Default.Color0 = Settings.Default.Color1 = Settings.Default.Color2 = "";
Settings.Default.Save();
foreach (Button button in theGrid.Children.OfType<Button>())
{
BindingOperations.GetBindingExpression(button, Button.BackgroundProperty)?.UpdateTarget();
}
}
Это не идеально. Было бы намного лучше не иметь прямого доступа к состоянию привязки, а вместо этого позволить WPF иметь дело с обновлениями. Кроме того, если вы посмотрите на выходные данные отладки, то каждый раз, когда кнопка устанавливается в состояние "по умолчанию", выдается исключение. Это тоже не очень хорошая ситуация.
Эти проблемы могут быть решены. Первый - переход к реализации в стиле MVVM, в которой состояние программы сохраняется независимо от визуальной части программы, причем визуальная часть реагирует на изменения в этом состоянии. Второе, добавив некоторую логику, чтобы принудить недействительного string
ценить что-то, чем доволен WPF.
Для этого полезно создать пару готовых вспомогательных классов, один для непосредственной поддержки самих классов модели представления, а другой для представления команды (что является лучшим способом обработки ввода пользователя, чем обработка Click
события напрямую). Те выглядят так:
class NotifyPropertyChangedBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void _UpdateField<T>(ref T field, T newValue,
Action<T> onChangedCallback = null,
[CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, newValue))
{
return;
}
T oldValue = field;
field = newValue;
onChangedCallback?.Invoke(oldValue);
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
class DelegateCommand : ICommand
{
private readonly Action _execute;
private readonly Func<bool> _canExecute;
public DelegateCommand(Action execute) : this(execute, null) { }
public DelegateCommand(Action execute, Func<bool> canExecute)
{
_execute = execute;
_canExecute = canExecute;
}
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter)
{
return _canExecute?.Invoke() ?? true;
}
public void Execute(object parameter)
{
_execute();
}
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
Это всего лишь примеры. NotifyPropertyChangedBase
класс в основном идентичен тому, что я использую в повседневной жизни. DelegateCommand
class - это урезанная версия более полнофункциональной реализации, которую я использую (в основном, отсутствует поддержка параметров команды, поскольку они не нужны в данном конкретном сценарии). Существует множество похожих примеров переполнения стека и Интернета, часто встроенных в библиотеку, предназначенную для помощи в разработке WPF.
С их помощью мы можем определить некоторые классы "модели представления", которые будут представлять состояние программы. Обратите внимание, что в этих классах практически нет ничего, что включало бы само представление. Единственным исключением является использование DependencyProperty.UnsetValue
, как уступка простоте. Можно избавиться даже от этого, наряду с "принудительными" методами, поддерживающими этот дизайн, как вы увидите в третьем примере после этого.
Во-первых, модель представления для представления состояния каждой отдельной кнопки:
class ButtonViewModel : NotifyPropertyChangedBase
{
private object _color = DependencyProperty.UnsetValue;
public object Color
{
get { return _color; }
set { _UpdateField(ref _color, value); }
}
public ICommand ToggleCommand { get; }
public ButtonViewModel()
{
ToggleCommand = new DelegateCommand(_Toggle);
}
private void _Toggle()
{
Color = object.Equals(Color, "Green") ? "Red" : "Green";
}
public void Reset()
{
Color = DependencyProperty.UnsetValue;
}
}
Затем модель представления, которая содержит общее состояние программы:
class MainViewModel : NotifyPropertyChangedBase
{
private ButtonViewModel _button0 = new ButtonViewModel();
public ButtonViewModel Button0
{
get { return _button0; }
set { _UpdateField(ref _button0, value); }
}
private ButtonViewModel _button1 = new ButtonViewModel();
public ButtonViewModel Button1
{
get { return _button1; }
set { _UpdateField(ref _button1, value); }
}
private ButtonViewModel _button2 = new ButtonViewModel();
public ButtonViewModel Button2
{
get { return _button2; }
set { _UpdateField(ref _button2, value); }
}
public ICommand ResetCommand { get; }
public MainViewModel()
{
ResetCommand = new DelegateCommand(_Reset);
Button0.Color = _CoerceColorString(Settings.Default.Color0);
Button1.Color = _CoerceColorString(Settings.Default.Color1);
Button2.Color = _CoerceColorString(Settings.Default.Color2);
Button0.PropertyChanged += (s, e) =>
{
Settings.Default.Color0 = _CoercePropertyValue(Button0.Color);
Settings.Default.Save();
};
Button1.PropertyChanged += (s, e) =>
{
Settings.Default.Color1 = _CoercePropertyValue(Button1.Color);
Settings.Default.Save();
};
Button2.PropertyChanged += (s, e) =>
{
Settings.Default.Color2 = _CoercePropertyValue(Button2.Color);
Settings.Default.Save();
};
}
private object _CoerceColorString(string color)
{
return !string.IsNullOrWhiteSpace(color) ? color : DependencyProperty.UnsetValue;
}
private string _CoercePropertyValue(object color)
{
string value = color as string;
return value ?? "";
}
private void _Reset()
{
Button0.Reset();
Button1.Reset();
Button2.Reset();
}
}
Важно отметить, что нигде из вышеперечисленного ничего не пытается напрямую манипулировать объектами пользовательского интерфейса, и все же у вас есть все, что вам нужно для поддержания состояния программы под контролем пользователя.
Имея в виду модели представлений, остается только определить пользовательский интерфейс:
<Window x:Class="WpfApp1.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:l="clr-namespace:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<l:MainViewModel/>
</Window.DataContext>
<Grid>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button Width="66" Height="26" Background="{Binding Button0.Color}" Command="{Binding Button0.ToggleCommand}"/>
<Button Width="66" Height="26" Background="{Binding Button1.Color}" Command="{Binding Button1.ToggleCommand}"/>
<Button Width="66" Height="26" Background="{Binding Button2.Color}" Command="{Binding Button2.ToggleCommand}"/>
</StackPanel>
<Button Content="Reset" Width="75" HorizontalAlignment="Right" VerticalAlignment="Bottom" Command="{Binding ResetCommand}"/>
</Grid>
</Window>
Некоторые вещи, чтобы отметить здесь:
- В файле MainWindow.xaml.cs вообще нет кода. Он полностью не отличается от шаблона по умолчанию, только с помощью конструктора без параметров и вызова
InitializeComponent()
, Переходя к реализации в стиле MVVM, большая часть требуемой внутренней сантехники просто исчезает полностью. - Этот код не кодирует жестко какие-либо местоположения элементов пользовательского интерфейса (например, путем настройки
Margin
ценности). Вместо этого используются функции макета WPF для размещения цветных кнопок в ряду посередине и для размещения кнопки сброса в правом нижнем углу окна (таким образом, это видно независимо от размера окна). MainViewModel
объект устанавливается какWindow.DataContext
значение. Этот контекст данных наследуется любыми элементами в окне, если он не переопределен путем его явной установки или (как вы увидите в третьем примере), поскольку элемент автоматически генерируется в другом контексте. Конечно, все пути привязки относятся к этому объекту.
Теперь это, вероятно, будет правильным решением, если у вас действительно будет только три кнопки. Но с 240 у вас много головной боли при копировании / вставке. Существует множество причин следовать принципу СУХОЙ ("не повторяйся"), в том числе удобство, надежность кода и удобство обслуживания. Это все определенно применимо здесь.
Чтобы улучшить приведенный выше пример MVVM, мы можем сделать несколько вещей:
- Сохраните настройки в коллекции вместо того, чтобы иметь индивидуальное свойство настройки для каждой кнопки.
- Поддерживать коллекцию
ButtonViewModel
объекты вместо того, чтобы иметь явное свойство для каждой кнопки. - Используйте
ItemsControl
представить коллекциюButtonViewModel
объекты вместо того, чтобы объявить отдельныйButton
элемент для каждой кнопки.
Для этого модели представлений придется немного изменить. MainViewModel
заменяет отдельные свойства одним Buttons
Свойство для хранения всех объектов модели вида кнопки:
class MainViewModel : NotifyPropertyChangedBase
{
public ObservableCollection<ButtonViewModel> Buttons { get; } = new ObservableCollection<ButtonViewModel>();
public ICommand ResetCommand { get; }
public MainViewModel()
{
ResetCommand = new DelegateCommand(_Reset);
for (int i = 0; i < Settings.Default.Colors.Count; i++)
{
ButtonViewModel buttonModel = new ButtonViewModel(i) { Color = Settings.Default.Colors[i] };
Buttons.Add(buttonModel);
buttonModel.PropertyChanged += (s, e) =>
{
ButtonViewModel model = (ButtonViewModel)s;
Settings.Default.Colors[model.ButtonIndex] = model.Color;
Settings.Default.Save();
};
}
}
private void _Reset()
{
foreach (ButtonViewModel model in Buttons)
{
model.Reset();
}
}
}
Вы заметите обработку Color
собственность тоже немного другая. Это потому, что в этом примере Color
собственность является актуальной string
тип вместо object
и я использую IValueConverter
реализация для обработки отображения string
значение того, что нужно элементам XAML (подробнее об этом чуть позже).
Новый ButtonViewModel
немного отличается тоже. У него есть новое свойство, указывающее, какая это кнопка (это позволяет модели основного вида знать, с каким элементом коллекции настроек идет модель представления кнопки), и Color
обработка свойств немного проще, потому что теперь мы имеем дело только с string
значения, а не DependencyProperty.UnsetValue
значение также:
class ButtonViewModel : NotifyPropertyChangedBase
{
public int ButtonIndex { get; }
private string _color;
public string Color
{
get { return _color; }
set { _UpdateField(ref _color, value); }
}
public ICommand ToggleCommand { get; }
public ButtonViewModel(int buttonIndex)
{
ButtonIndex = buttonIndex;
ToggleCommand = new DelegateCommand(_Toggle);
}
private void _Toggle()
{
Color = Color == "Green" ? "Red" : "Green";
}
public void Reset()
{
Color = null;
}
}
С нашими новыми моделями представлений их теперь можно подключить в XAML:
<Window x:Class="WpfApp2.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:l="clr-namespace:WpfApp2"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<l:MainViewModel/>
</Window.DataContext>
<Grid>
<ItemsControl ItemsSource="{Binding Buttons}" HorizontalAlignment="Center">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" IsItemsHost="True"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.Resources>
<l:ColorStringConverter x:Key="colorStringConverter1"/>
<DataTemplate DataType="{x:Type l:ButtonViewModel}">
<Button Width="66" Height="26" Command="{Binding ToggleCommand}"
Background="{Binding Color, Converter={StaticResource colorStringConverter1}, Mode=OneWay}"/>
</DataTemplate>
</ItemsControl.Resources>
</ItemsControl>
<Button Content="Reset" Width="75" HorizontalAlignment="Right" VerticalAlignment="Bottom" Command="{Binding ResetCommand}"/>
</Grid>
</Window>
Как и прежде, модель основного вида объявляется как Window.DataContext
значение. Но вместо явного объявления каждого элемента кнопки, я использую ItemsControl
элемент для представления кнопок. Это имеет следующие важные аспекты:
ItemsSource
собственность связана сButtons
коллекция.- Панель по умолчанию, используемая для этого элемента, будет ориентирована вертикально
StackPanel
Таким образом, я переопределил это с горизонтально ориентированным, чтобы достичь того же макета, который использовался в предыдущих примерах. - Я объявил экземпляр моего
IValueConverter
реализация как ресурс, чтобы его можно было использовать в шаблоне. - Я объявил
DataTemplate
как ресурс, сDataType
установить типButtonViewModel
, При представлении личностиButtonViewModel
объекты, WPF будет искать во встроенных ресурсах шаблон, назначенный этому типу, и, поскольку я его здесь объявил, он будет использовать его для представления объекта модели представления. Для каждогоButtonViewModel
объект, WPF создаст экземпляр содержимого вDataTemplate
элемент, и установитDataContext
для корневого объекта этого содержимого в объект модели представления. И наконец, - В шаблоне привязка использует конвертер, который я объявил ранее. Это позволяет мне вставить немного кода C# в привязку свойства, чтобы обеспечить
string
значение обрабатывается соответствующим образом, т. е. когда оно пусто,DependencyProperty.UnsetValue
используется, избегая любых исключений времени выполнения из механизма привязки.
Вот этот конвертер:
class ColorStringConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
string text = (string)value;
return !string.IsNullOrWhiteSpace(text) ? text : DependencyProperty.UnsetValue;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
В этом случае ConvertBack()
метод не реализован, потому что мы будем использовать привязку только в OneWay
Режим. Нам просто нужно проверить string
значение, и если оно нулевое или пустое (или пробел), мы возвращаем DependencyProperty.UnsetValue
вместо.
Некоторые другие замечания по этой реализации:
- Свойство Settings.Colors имеет тип
System.Collections.Specialized.StringCollection
и инициализируется (в конструкторе) тремя пустымиstring
ценности. Длина этой коллекции определяет, сколько кнопок создано. Конечно, вы можете использовать любой механизм, который хотите отслеживать на этой стороне данных, если вы предпочитаете что-то другое. - С 240 кнопками простое расположение их в горизонтальном ряду может работать, а может и не работать (в зависимости от того, насколько большими будут кнопки на самом деле). Вы можете использовать другие объекты панели для
ItemsPanel
имущество; вероятные кандидаты включаютUniformGrid
или жеListView
(сGridView
вид), оба из которых могут расположить элементы в автоматически разнесенной сетке.
Так как Button
элементы расположены в каком-то родителе Panel
такой как, например, StackPanel
Вы можете перебрать его Children
коллекция как это:
foreach(Button button in thePanel.Children.OfType<Button>())
{
//...
}
XAML:
<StackPanel x:Name="thePanel">
<Button x:Name="Button0" HorizontalAlignment="Left" Margin="197,139,0,0" VerticalAlignment="Top" Width="66" Height="26" Focusable="False" Background="{Binding Source={x:Static properties:Settings.Default}, Path=Color0, Mode=TwoWay}" Click="Button0_Click" />
<Button x:Name="Button1" HorizontalAlignment="Left" Margin="131,139,0,0" VerticalAlignment="Top" Width="66" Height="26" Focusable="False" Background="{Binding Source={x:Static properties:Settings.Default}, Path=Color1, Mode=TwoWay}" Click="Button1_Click" />
<Button x:Name="Button0_Copy" HorizontalAlignment="Left" Margin="563,139,0,0" VerticalAlignment="Top" Width="66" Height="26" Focusable="False" Background="{Binding Color_0, Mode=TwoWay, Source={x:Static properties:Settings.Default}}" Click="Button0_Copy_Click"/>
<Button x:Name="Button1_Copy" HorizontalAlignment="Left" Margin="497,139,0,0" VerticalAlignment="Top" Width="66" Height="26" Focusable="False" Background="{Binding Color_1, Mode=TwoWay, Source={x:Static properties:Settings.Default}}" Click="Button1_Copy_Click"/>
</StackPanel>