选择后如何持续更改ListBox SelectedItem的颜色
How to persistently change color of ListBox SelectedItem after selecting
我有一个列表框,用于加载前景色设置为红色的项目。我想做的是:用鼠标选择一个项目后,将 SelectedItem 的前景色更改为黑色,但要使更改持久化,以便在取消选择该项目后,颜色保持黑色。顺便说一句,我想将此实现为向用户显示 'read items' 的一种方式。
本质上,我想要类似于下面代码的常见 属性 触发器的实现,但在取消选择后不恢复样式。我也玩过事件触发器,但运气不佳。
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Style.Triggers>
<Trigger Property="IsSelected" Value="True" >
<Setter Property="Foreground" Value="Black" /> //make this persist after deselection
</Trigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
提前致谢!
您可以为 Foreground
属性:
设置动画
<ListBox>
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Foreground" Value="Red" />
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<ColorAnimation Storyboard.TargetProperty="(ListBoxItem.Foreground).(SolidColorBrush.Color)"
To="Black" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
</Trigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
这种简单方法的缺点是信息没有存储在某个地方。这是没有任何数据支持的纯可视化。为了保留信息,以便重新启动应用程序显示相同的先前状态,您应该为您的数据模型引入一个专用的 属性,例如 IsMarkedAsRead
。
根据您的要求,您可以覆盖 ListBoxItem.Template
并将 ToggleButton.IsChecked
绑定到 IsMarkedAsRead
或使用 Button
,它使用 ICommand 来设置 IsMarkedAsRead
属性。有很多解决方案,例如实施附加行为。
以下示例覆盖 ListBoxItem.Template
以将 ListBoxItem
变成 Button
。现在,当单击该项目时,数据模型的 IsMarkedAsRead
属性 设置为 true
:
数据模型
(有关 RelayCommand
的实施示例,请参阅 Microsoft Docs: Patterns - WPF Apps With The Model-View-ViewModel Design Pattern。)
public class Notification : INotifyPropertyChanged
{
public string Text { get; set; }
public ICommand MarkAsReadCommand => new RelayCommand(() => this.IsMarkedAsRead = true);
public ICommand MarkAsUnreadCommand => new RelayCommand(() => this.IsMarkedAsRead = false);
private bool isMarkedAsRead;
public bool IsMarkedAsRead
{
get => this.isMarkedAsRead;
set
{
this.isMarkedAsRead = value;
OnPropertyChanged();
}
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
列表框
<ListBox ItemsSource="{Binding Notifications}">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Border Background="{TemplateBinding Background}">
<Button x:Name="ContentPresenter"
ContentTemplate="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=ListBox}, Path=ItemTemplate}"
Content="{TemplateBinding Content}"
Command="{Binding MarkAsReadCommand}"
Foreground="Red">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border>
<ContentPresenter />
</Border>
</ControlTemplate>
</Button.Template>
</Button>
</Border>
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding IsMarkedAsRead}" Value="True">
<Setter TargetName="ContentPresenter" Property="Foreground" Value="Green" />
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type Notification}">
<TextBlock Text="{Binding Text}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
非常感谢@BionicCode 的全面回答。我最终采用了另一种解决方案,这可能是也可能不是好的约定;我是一个爱好者。
首先,我不需要数据备份/持久性。
关于数据模型解决方案和覆盖 ListBoxItem.Template,我使用预定义的 class 'SyndicationItem' 作为数据 class(我的应用程序是 Rss Reader).为了实现您的数据模型解决方案,我想我可以破解一个未使用的 SyndicationItem 属性,或者使用 SyndicationItem 继承来自定义 class(我猜这是最专业的方法?)
我的完整数据模型如下:
ObservableCollection >>> CollectionViewSource >>> ListBox.
无论如何,我最终使用了一些当时并不那么简单的简单代码:
首先是XAML:
<Window.Resources>
<CollectionViewSource x:Key="fooCollectionViewSource" Source="{Binding fooObservableCollection}" >
<CollectionViewSource.SortDescriptions>
<scm:SortDescription PropertyName="PublishDate" Direction="Descending" />
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
<Style x:Key="DeselectedTemplate" TargetType="{x:Type ListBoxItem}">
<Setter Property="Foreground" Value="Gray" />
</Style>
</Window.Resources>
<ListBox x:Name="LB1" ItemsSource="{Binding Source={StaticResource fooCollectionViewSource}}" HorizontalContentAlignment="Stretch" Margin="0,0,0,121" ScrollViewer.HorizontalScrollBarVisibility="Disabled" >
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="80" />
</Grid.ColumnDefinitions>
<TextBlock MouseDown="TextBlock_MouseDown" Grid.Column="0" Text="{Binding Path=Title.Text}" TextWrapping="Wrap" FontWeight="Bold" />
<TextBlock Grid.Column="1" HorizontalAlignment="Right" TextAlignment="Center" FontSize="11" FontWeight="SemiBold"
Text="{Binding Path=PublishDate.LocalDateTime, StringFormat='{}{0:d MMM, HH:mm}'}"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
下面是代码:
解决方案 1:取消选择 listboxitem 时应用新样式。不再使用,因此 LB1_SelectionChanged 事件不存在于 XAML.
中
private void LB1_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.RemovedItems.Count != 0)
{
foreach (var lbItem in e.RemovedItems)
{
//get reference to source listbox item. This was a pain.
int intDeselectedItem = LB1.Items.IndexOf(lbItem);
ListBoxItem lbi = (ListBoxItem)LB1.ItemContainerGenerator.ContainerFromIndex(intDeselectedItem);
/*apply style. Initially, instead of applying a style, I used mylistboxitem.Foreground = Brushes.Gray to set the text color.
Howver I noticed that if I scrolled the ListBox to the bottom, the text color would revert to the XAML default style in my XAML.
I assume this is because of refreshes / redraws (whichever the correct term). Applying a new style resolved.*/
Style style = this.FindResource("DeselectedTemplate") as Style;
lbi.Style = style;
}
}
}
解决方案 2:我选择的那个。在 SelectedItem = true 时发生,效果与您的第一个建议相同。
private void TextBlock_MouseDown(object sender, MouseButtonEventArgs e)
{
TextBlock tb = e.Source as TextBlock;
tb.Foreground = Brushes.Gray;
}
我有一个列表框,用于加载前景色设置为红色的项目。我想做的是:用鼠标选择一个项目后,将 SelectedItem 的前景色更改为黑色,但要使更改持久化,以便在取消选择该项目后,颜色保持黑色。顺便说一句,我想将此实现为向用户显示 'read items' 的一种方式。
本质上,我想要类似于下面代码的常见 属性 触发器的实现,但在取消选择后不恢复样式。我也玩过事件触发器,但运气不佳。
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Style.Triggers>
<Trigger Property="IsSelected" Value="True" >
<Setter Property="Foreground" Value="Black" /> //make this persist after deselection
</Trigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
提前致谢!
您可以为 Foreground
属性:
<ListBox>
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Foreground" Value="Red" />
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<ColorAnimation Storyboard.TargetProperty="(ListBoxItem.Foreground).(SolidColorBrush.Color)"
To="Black" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
</Trigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
这种简单方法的缺点是信息没有存储在某个地方。这是没有任何数据支持的纯可视化。为了保留信息,以便重新启动应用程序显示相同的先前状态,您应该为您的数据模型引入一个专用的 属性,例如 IsMarkedAsRead
。
根据您的要求,您可以覆盖 ListBoxItem.Template
并将 ToggleButton.IsChecked
绑定到 IsMarkedAsRead
或使用 Button
,它使用 ICommand 来设置 IsMarkedAsRead
属性。有很多解决方案,例如实施附加行为。
以下示例覆盖 ListBoxItem.Template
以将 ListBoxItem
变成 Button
。现在,当单击该项目时,数据模型的 IsMarkedAsRead
属性 设置为 true
:
数据模型
(有关 RelayCommand
的实施示例,请参阅 Microsoft Docs: Patterns - WPF Apps With The Model-View-ViewModel Design Pattern。)
public class Notification : INotifyPropertyChanged
{
public string Text { get; set; }
public ICommand MarkAsReadCommand => new RelayCommand(() => this.IsMarkedAsRead = true);
public ICommand MarkAsUnreadCommand => new RelayCommand(() => this.IsMarkedAsRead = false);
private bool isMarkedAsRead;
public bool IsMarkedAsRead
{
get => this.isMarkedAsRead;
set
{
this.isMarkedAsRead = value;
OnPropertyChanged();
}
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
列表框
<ListBox ItemsSource="{Binding Notifications}">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Border Background="{TemplateBinding Background}">
<Button x:Name="ContentPresenter"
ContentTemplate="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=ListBox}, Path=ItemTemplate}"
Content="{TemplateBinding Content}"
Command="{Binding MarkAsReadCommand}"
Foreground="Red">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border>
<ContentPresenter />
</Border>
</ControlTemplate>
</Button.Template>
</Button>
</Border>
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding IsMarkedAsRead}" Value="True">
<Setter TargetName="ContentPresenter" Property="Foreground" Value="Green" />
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type Notification}">
<TextBlock Text="{Binding Text}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
非常感谢@BionicCode 的全面回答。我最终采用了另一种解决方案,这可能是也可能不是好的约定;我是一个爱好者。
首先,我不需要数据备份/持久性。
关于数据模型解决方案和覆盖 ListBoxItem.Template,我使用预定义的 class 'SyndicationItem' 作为数据 class(我的应用程序是 Rss Reader).为了实现您的数据模型解决方案,我想我可以破解一个未使用的 SyndicationItem 属性,或者使用 SyndicationItem 继承来自定义 class(我猜这是最专业的方法?)
我的完整数据模型如下:
ObservableCollection >>> CollectionViewSource >>> ListBox.
无论如何,我最终使用了一些当时并不那么简单的简单代码:
首先是XAML:
<Window.Resources>
<CollectionViewSource x:Key="fooCollectionViewSource" Source="{Binding fooObservableCollection}" >
<CollectionViewSource.SortDescriptions>
<scm:SortDescription PropertyName="PublishDate" Direction="Descending" />
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
<Style x:Key="DeselectedTemplate" TargetType="{x:Type ListBoxItem}">
<Setter Property="Foreground" Value="Gray" />
</Style>
</Window.Resources>
<ListBox x:Name="LB1" ItemsSource="{Binding Source={StaticResource fooCollectionViewSource}}" HorizontalContentAlignment="Stretch" Margin="0,0,0,121" ScrollViewer.HorizontalScrollBarVisibility="Disabled" >
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="80" />
</Grid.ColumnDefinitions>
<TextBlock MouseDown="TextBlock_MouseDown" Grid.Column="0" Text="{Binding Path=Title.Text}" TextWrapping="Wrap" FontWeight="Bold" />
<TextBlock Grid.Column="1" HorizontalAlignment="Right" TextAlignment="Center" FontSize="11" FontWeight="SemiBold"
Text="{Binding Path=PublishDate.LocalDateTime, StringFormat='{}{0:d MMM, HH:mm}'}"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
下面是代码:
解决方案 1:取消选择 listboxitem 时应用新样式。不再使用,因此 LB1_SelectionChanged 事件不存在于 XAML.
中 private void LB1_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.RemovedItems.Count != 0)
{
foreach (var lbItem in e.RemovedItems)
{
//get reference to source listbox item. This was a pain.
int intDeselectedItem = LB1.Items.IndexOf(lbItem);
ListBoxItem lbi = (ListBoxItem)LB1.ItemContainerGenerator.ContainerFromIndex(intDeselectedItem);
/*apply style. Initially, instead of applying a style, I used mylistboxitem.Foreground = Brushes.Gray to set the text color.
Howver I noticed that if I scrolled the ListBox to the bottom, the text color would revert to the XAML default style in my XAML.
I assume this is because of refreshes / redraws (whichever the correct term). Applying a new style resolved.*/
Style style = this.FindResource("DeselectedTemplate") as Style;
lbi.Style = style;
}
}
}
解决方案 2:我选择的那个。在 SelectedItem = true 时发生,效果与您的第一个建议相同。
private void TextBlock_MouseDown(object sender, MouseButtonEventArgs e)
{
TextBlock tb = e.Source as TextBlock;
tb.Foreground = Brushes.Gray;
}