2017-06-30 35 views
0

我正在研究主窗口上有很多按鈕的應用程序。使用按鈕更改多個控件的背景屬性

單擊這些按鈕可以在按下時更改顏色,並使用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> 

這是我到每個按鈕的Click事件的實現代碼:

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>()) 
     } 
    } 
} 

現在,我想要某種重置按鈕,當按下按鈕時,將所有按鈕的背景變爲默認(不是紅色,也不是綠色)。

我試圖做的是從this線程中使用的想法,並以此作爲對復位按鈕點擊事件,但每當我做

foreach (Control x in Control.Controls) 

,或者使用「控制」任何其他方法(這.Controls等)我用紅色標出下劃線,說Control類沒有定義。

我做錯了什麼?你們對我如何編程該按鈕將所有按鈕的背景更改爲默認值有任何建議嗎?

回答

1

短版:你做錯了。我的意思是,我懷疑你在某種程度上已經知道,因爲代碼不起作用。但看看你的評論說,你會有240個按鈕,你是真的去錯誤的方式。

此答案旨在引導您瞭解三種不同的選項,每種選項都會讓您更接近處理這種情況的最佳方法。

從你原來的努力開始,我們可以獲得你發佈的代碼大部分是按原樣工作的。您的主要問題是,您成功獲得Grid的每個Button孩子後,不能只設置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類是我使用的更全功能實現的簡化版本(主要是缺少對命令參數的支持,因爲在此特定場景中不需要它們)。 Stack Overflow和Internet上有很多類似的例子,通常內置於一個旨在幫助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(); 
    } 
} 

需要注意的重要一點是,在上面的任何地方都沒有嘗試直接操作UI對象,但是您仍然需要在用戶的控制下維護程序的狀態。

在手,所有剩下的視圖模型是定義UI:

<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> 

有些東西這裏要注意:

  1. 沒有代碼都在主窗口.xaml.cs文件。它與默認模板完全相同,僅使用無參數構造函數並調用InitializeComponent()。通過轉向MVVM風格的實現,否則所需的很多內部管道都會完全消失。
  2. 此代碼不對任何UI元素位置進行硬編碼(例如,通過設置Margin值)。相反,它利用WPF的佈局功能將顏色按鈕放置在中間的一行中,並將重置按鈕放置在窗口的右下方(這樣無論窗口大小如何都可以看到)。
  3. MainViewModel對象被設置爲Window.DataContext的值。這個數據上下文被窗口內的任何元素繼承,除非通過顯式設置來重寫,或者(如第三個例子中所見),因爲元素是在不同的上下文中自動生成的。當然,綁定路徑都是相對於這個對象的。

現在,如果你真的只有三個按鈕,這可能是一個好方法。但是隨着240,你有很多複製/粘貼的麻煩。遵循DRY(「不要重複自己」)原則有許多原因,包括便利性和代碼可靠性和可維護性。這一切肯定會適用於這裏。

爲了提高對MVVM上面的例子,我們可以做一些事情:

  1. 保存,而不必爲每個按鈕的單獨設置屬性集合中的設置。
  2. 維護一個ButtonViewModel對象的集合,而不是每個按鈕的顯式屬性。
  3. 使用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也有點不同。它有一個新屬性,用於指示它是哪個按鈕(這允許主視圖模型知道按鈕視圖模型的設置集合中包含哪個元素),而屬性處理更簡單一些,因爲現在我們只有string值處理,而不是DependencyProperty.UnsetValue值也一樣:前

<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> 

,主視圖:

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.DataContext值。但是,我沒有明確聲明每個按鈕元素,而是使用ItemsControl元素來呈現按鈕。它具有以下關鍵方面:

  1. ItemsSource屬性綁定到Buttons集合。
  2. 這個元素使用的默認面板是一個垂直定向的StackPanel,所以我重寫了一個水平定向的面板,以實現前面例子中使用的相同佈局。
  3. 我已經將我的IValueConverter實現的一個實例聲明爲資源,以便它可以在模板中使用。
  4. 我已聲明DataTemplate作爲資源,DataType設置爲ButtonViewModel的類型。當呈現個體ButtonViewModel對象時,WPF將在範圍內資源中查找指定給該類型的模板,並且由於我在此處聲明瞭一個模板,因此它將使用該模板呈現視圖模型對象。對於每個ButtonViewModel對象,WPF將在DataTemplate元素中創建內容的實例,並將該內容的根對象的DataContext設置爲視圖模型對象。最後,
  5. 在模板中,綁定使用了前面聲明的轉換器。這允許我在屬性綁定中插入一點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

在這個實施其他一些注意事項:

  1. 的Settings.Colors屬性設置爲輸入System.Collections.Specialized.StringCollection,並初始化(在設計器)與三個空string值。該集合的長度決定了創建了多少個按鈕。當然,如果你更喜歡別的東西,你可以使用任何你想跟蹤數據這一端的機制。
  2. 240按鈕,簡單地安排他們在一個水平行可能會或可能不會爲你工作(取決於按鈕真的會有多大)。您可以使用ItemsPanel屬性的其他面板對象;可能的候選人包括UniformGridListView(與GridView視圖),兩者都可以將元素排列在自動間隔網格中。
0

由於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> 
+0

這些按鈕直接添加在主WPF ..內。我嘗試將所有這些(240個按鈕)放置在一個堆疊面板中,但是它們的位置很混亂。 – PaperClip

+0

我嘗試命名網格,如你的例子,並使用foreach方法,但我產生一個錯誤,說「網格名稱」不存在於當前的情況下 – PaperClip

+0

你真的給網格一個x:名稱和實現的foreach循環在同一窗口的代碼隱藏?那麼它應該工作。請發佈您的所有示例代碼以提供您的觀點。 – mm8