c# - 如何在WPF中闪烁RichTextBox边框和背景色

标签 c# wpf windows

像Winforms中一样,这似乎应该很简单,但是我对WPF还是比较陌生,因此仍在尝试改变对数据和UI交互方式的看法。

场景:用户在我的主窗体上单击一个按钮。该按钮用于输入街道地址。在街道地址表单中,当用户单击提交按钮时,我会进行一些基本数据验证。 Submit()遍历每个数据输入字段,并调用下面的方法来尝试提醒用户注意有问题的数据字段。

这是我没有执行任何可以检测到的代码:

    private void FlashTextBox(RichTextBox box)
    {
        var currentBorderColor = box.BorderBrush;
        var currentBackgroundColor = box.Background;

        Task.Factory.StartNew(() =>
        {
            for (int x = 0; x < 5; x++)
            {
                this.Dispatcher.Invoke(() =>
                {

                    box.Background = Brushes.Red;
                    box.BorderBrush = Brushes.IndianRed;
                    box.InvalidateVisual();

                    System.Threading.Thread.Sleep(100);

                    box.BorderBrush = currentBorderColor;
                    box.Background = currentBackgroundColor;
                    box.InvalidateVisual();

                    System.Threading.Thread.Sleep(100);
                });
            }
        });
    }

最佳答案

正如我提到的in my comment,代码的主要问题是您已阻止UI线程。因此,当您循环将感兴趣的属性更改为新值时,实际的UI永远不会有机会更新视觉表示,即屏幕上的内容。

具有讽刺意味的是,当您注意到“似乎在Winforms中应该很简单,就像在Winforms中一样”,如果您尝试在Winforms程序中编写相同的代码,那么您将遇到完全相同的问题。 Winforms和WPF(实际上,大多数GUI API)都具有完全相同的限制:只有一个线程可以处理所有UI,并且在更改一个或多个应该影响UI外观的数据值之后,您必须返回控件到调用您的UI线程,以便随后可以更新屏幕。

现在,您还注意到您正在“试图改变对数据和UI交互方式的看法”。这是一件好事,如果您愿意花时间学习WPF旨在使用的MVVM概念,那将会有很大帮助。 Winforms还具有数据绑定模型,实际上,您可以在Winforms中编写非常相似的代码,这是WPF强烈建议的。但是,WPF的“保留”图形模型与Winform的“立即”模型相反-即WPF跟踪图形的外观,而Winform要求您每次需要更新屏幕时都要自己处理图形-这很适合自己数据绑定方法要好得多,WPF的整个设计都基于此。

这意味着您应该努力将数据保留在数据所在的位置,将UI保留在UI所在的位置。即数据在您的代码隐藏中,而UI在XAML中。在这两个API中都是一个好主意,但是如果您无法使用WPF做到这一点,您将付出更多。

那么,这又在哪里留下您的问题呢?好吧,缺少一个很好的minimal, complete, and verifiable code example,很难知道您的代码是什么样子,因此什么是修复它的最佳方法。因此,相反,我将提供一些示例,希望您在重新定位代码以使其更好地适合WPF范例之后,可以按自己的喜好应用一个。 (不幸的是,我对WPF不太喜欢的一件事是,它在某些方面过于强大,提供了许多不同的方法来实现相同的结果;这有时有时真的很难知道什么是最好的方法。)

这两个示例在背后需要多少代码方面彼此不同。第一种将动画逻辑放入C#代码中,作为视图模型的一部分。一方面,这可以说不是“ WPF方式”。但是第二个使用视图代码(即XAML)来定义动画,这需要在视图代码的后面增加一些额外的内容,这使我有些烦恼,因为它模糊了视图和视图模型之间的界限。比我想要的多一点。

那好吧。

这是第一种方法的视图模型类:

class ViewModel : NotifyPropertyChangedBase
{
    private string _text;
    public string Text
    {
        get => _text;
        set => _UpdateField(ref _text, value);
    }

    private bool _isHighlighted;
    public bool IsHighlighted
    {
        get => _isHighlighted;
        set => _UpdateField(ref _isHighlighted, value);
    }

    private bool _isAnimating;
    public bool IsAnimating
    {
        get => _isAnimating;
        set => _UpdateField(ref _isAnimating, value, _OnIsAnimatingChanged);
    }

    private void _OnIsAnimatingChanged(bool oldValue)
    {
        _toggleIsHighlightedCommand.RaiseCanExecuteChanged();
        _animateIsHighlightedCommand.RaiseCanExecuteChanged();
    }

    private readonly DelegateCommand _toggleIsHighlightedCommand;
    private readonly DelegateCommand _animateIsHighlightedCommand;

    public ICommand ToggleIsHighlightedCommand => _toggleIsHighlightedCommand;
    public ICommand AnimateIsHighlightedCommand => _animateIsHighlightedCommand;

    public ViewModel()
    {
        _toggleIsHighlightedCommand = new DelegateCommand(() => IsHighlighted = !IsHighlighted, () => !IsAnimating);
        _animateIsHighlightedCommand = new DelegateCommand(() => _FlashIsHighlighted(this), () => !IsAnimating);
    }

    private static async void _FlashIsHighlighted(ViewModel viewModel)
    {
        viewModel.IsAnimating = true;

        for (int i = 0; i < 10; i++)
        {
            viewModel.IsHighlighted = !viewModel.IsHighlighted;
            await Task.Delay(200);
        }

        viewModel.IsAnimating = false;
    }
}

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, Func<bool> canExecute)
    {
        _execute = execute;
        _canExecute = canExecute;
    }

    public DelegateCommand(Action execute) : this(execute, null) { }

    public event EventHandler CanExecuteChanged;
    public bool CanExecute(object parameter) => _canExecute?.Invoke() != false;
    public void Execute(object parameter) => _execute?.Invoke();
    public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}


那里的第二个类NotifyPropertyChangedBase只是我的视图模型的标准基类。它包含所有支持INotifyPropertyChanged接口的样板。有一些WPF框架本身包括此类基类。我不知道为什么WPF本身不提供一个。但是,这样做很方便,并且在它和粘贴到属性模板中的Visual Studio代码段之间,可以更快地将程序的视图模型放在一起。

同样,第三类DelegateCommand使定义ICommand对象更加容易。同样,此类类型的类也可在第三方WPF框架中使用。 (我还有一个类的版本,该类具有通用的类型参数,该类型参数指定传递给CanExecute()Execute()方法的命令参数的类型,但是由于在这里我们不需要它,所以我不必理会包括它。

如您所见,一旦经过样板,代码就非常简单。它具有形式Text属性,因此我在UI中有一些绑定到TextBox的内容。它还具有与bool的视觉状态相关的几个TextBox属性。一个确定实际的视觉状态,而另一个确定当前是否处于动画状态。

有两个ICommand实例提供用户与视图模型的交互。一个只是切换视觉状态,而另一个则导致想要发生的动画。

最后,有一种方法可以真正完成工作。它首先设置IsAnimating属性,然后循环十次以切换IsHighlighted属性。此方法使用async。在Winforms程序中,这是必不可少的,以便UI属性更新发生在UI线程中。但是在此WPF程序中,它是可选的。我喜欢async / await编程模型,但是对于简单的属性更改通知,WPF会根据需要将绑定更新封送回UI线程,因此您实际上可以在线程池中创建后台任务或专用线程来创建该任务。处理动画。

(对于动画,我在帧之间使用200毫秒而不是您的代码使用100毫秒,只是因为我认为它看起来更好,并且在任何情况下都可以更轻松地查看动画的效果。)

请注意,视图模型本身并不知道会涉及UI。它只是具有一个指示文本框是否应突出显示的属性。由UI确定如何执行此操作。

那看起来像这样:

<Window x:Class="TestSO57403045FlashBorderBackground.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:p="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:TestSO57403045FlashBorderBackground"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
  <Window.DataContext>
    <l:ViewModel/>
  </Window.DataContext>
  <StackPanel>
    <Button Command="{Binding ToggleIsHighlightedCommand}" Content="Toggle Control" HorizontalAlignment="Left"/>
    <Button Command="{Binding AnimateIsHighlightedCommand}" Content="Flash Control" HorizontalAlignment="Left"/>
    <TextBox x:Name="textBox1" Width="100" Text="{Binding Text}" HorizontalAlignment="Left">
      <TextBox.Style>
        <p:Style TargetType="TextBox">
          <Setter Property="BorderBrush" Value="Black"/>
          <Setter Property="BorderThickness" Value="2"/>
          <Setter Property="Background" Value="WhiteSmoke"/>
          <p:Style.Triggers>
            <DataTrigger Binding="{Binding IsHighlighted}" Value="True">
              <Setter Property="BorderBrush" Value="IndianRed"/>
              <Setter Property="Background" Value="Red"/>
            </DataTrigger>
          </p:Style.Triggers>
        </p:Style>
      </TextBox.Style>
    </TextBox>
  </StackPanel>
</Window>


这只是为边框和背景色设置了一些默认值。然后,重要的是,它定义了一个数据触发器,该触发器将在数据触发器中的条件为真时临时覆盖这些默认值。也就是说,声明的绑定的计算结果为给定的声明值(在我上面的示例中,实际上是booltrue值)。

您在每个地方看到的元素属性都设置为类似于{Binding}的内容,这是对当前数据上下文的引用,在这种情况下,该上下文设置为我的视图模型类。

现在,WPF具有非常丰富的动画功能集,可以代替上面的功能来处理闪烁的动画。如果我们要这样做,则视图模型可以更简单,因为我们不需要突出显示状态的显式属性。我们仍然需要IsAnimating属性,但是这次不是使用“ animate”命令来调用方法(该方法将此属性设置为副作用),而是直接设置了该属性,而没有执行其他操作(该属性现在动画的主要控制器仍然充当标志,以便可以根据需要启用/禁用按钮的命令):

class ViewModel : NotifyPropertyChangedBase
{
    private string _text;
    public string Text
    {
        get => _text;
        set => _UpdateField(ref _text, value);
    }

    private bool _isAnimating;
    public bool IsAnimating
    {
        get => _isAnimating;
        set => _UpdateField(ref _isAnimating, value, _OnIsAnimatingChanged);
    }

    private void _OnIsAnimatingChanged(bool oldValue)
    {
        _animateIsHighlightedCommand.RaiseCanExecuteChanged();
    }

    private readonly DelegateCommand _animateIsHighlightedCommand;

    public ICommand AnimateIsHighlightedCommand => _animateIsHighlightedCommand;

    public ViewModel()
    {
        _animateIsHighlightedCommand = new DelegateCommand(() => IsAnimating = true, () => !IsAnimating);
    }
}


重要的是,您会注意到,现在视图模型不包含任何实际运行动画的代码。可以在XAML中找到:

<Window x:Class="TestSO57403045FlashBorderBackground.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:p="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:TestSO57403045FlashBorderBackground"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
  <Window.DataContext>
    <l:ViewModel/>
  </Window.DataContext>
  <Window.Resources>
    <Storyboard x:Key="flashBorder" RepeatBehavior="5x"
                Completed="flashStoryboard_Completed">
      <ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(Background).(SolidColorBrush.Color)"
                                    Duration="0:0:0.4">
        <DiscreteColorKeyFrame KeyTime="0:0:0" Value="IndianRed"/>
        <DiscreteColorKeyFrame KeyTime="0:0:0.2" Value="WhiteSmoke"/>
      </ColorAnimationUsingKeyFrames>
      <ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(BorderBrush).(SolidColorBrush.Color)"
                                    Duration="0:0:0.4">
        <DiscreteColorKeyFrame KeyTime="0:0:0" Value="Red"/>
        <DiscreteColorKeyFrame KeyTime="0:0:0.2" Value="Black"/>
      </ColorAnimationUsingKeyFrames>
    </Storyboard>
  </Window.Resources>
  <StackPanel>
    <Button Command="{Binding AnimateIsHighlightedCommand}" Content="Flash Control" HorizontalAlignment="Left"/>
    <TextBox x:Name="textBox1" Width="100" Text="{Binding Text}" HorizontalAlignment="Left">
      <TextBox.Style>
        <p:Style TargetType="TextBox">
          <Setter Property="BorderBrush" Value="Black"/>
          <Setter Property="BorderThickness" Value="2"/>
          <Setter Property="Background" Value="WhiteSmoke"/>
          <p:Style.Triggers>
            <DataTrigger Binding="{Binding IsAnimating}" Value="True">
              <DataTrigger.EnterActions>
                <BeginStoryboard Storyboard="{StaticResource flashBorder}" Name="flashBorderBegin"/>
              </DataTrigger.EnterActions>
              <DataTrigger.ExitActions>
                <StopStoryboard BeginStoryboardName="flashBorderBegin"/>
              </DataTrigger.ExitActions>
            </DataTrigger>
          </p:Style.Triggers>
        </p:Style>
      </TextBox.Style>
    </TextBox>
  </StackPanel>
</Window>


在这种情况下,有一个Storyboard对象,其中包含两个动画序列(两个动画序列均同时启动),它们实际上是控件的闪烁。故事板本身使您可以指定应重复多少次(在这种情况下为"5x"五次),然后在每个动画序列中指定整个序列的持续时间(400毫秒,因为一个序列涉及两个状态,每个状态(在200毫秒内显示),然后是“关键帧”,该关键帧指示动画期间实际发生的情况,每个关键帧都指定动画在什么时间生效。

然后,以文本框的样式而不是触发属性设置器,而是根据触发状态(进入或退出)来启动和停止情节提要。

请注意,在情节提要中,预订了Completed事件。在上一个示例中,默认的MainWindow.xaml.cs文件没有更改,但是对于此版本,有一些代码:

public partial class MainWindow : Window
{
    private readonly ViewModel _viewModel;

    public MainWindow()
    {
        InitializeComponent();
        _viewModel = (ViewModel)DataContext;
    }

    private void flashStoryboard_Completed(object sender, EventArgs e)
    {
        _viewModel.IsAnimating = false;
    }
}


这具有Storyboard.Completed事件的事件处理程序的实现。而且由于该处理程序将需要修改视图模型状态,因此现在有代码从DataContext属性检索视图模型并将其保存在字段中,以便事件处理程序可以获取它。

动画完成后,可以使用此事件处理程序将IsAnimating属性设置回false

所以,你去了。可能有更好的方法可以执行此操作,但是我认为这两个示例应该为您提供一个良好的起点,让他们了解在WPF中应如何“完成”事情。

(我承认,真正让我烦恼的是动画方法的一件事是,我宁愿不必在情节提要中明确指出文本框的原始颜色;但是我不知道有什么方法可以指定<ColorAnimationUsingKeyFrame/>元素中的一个关键帧,而不是实际设置新颜色,只是删除了动画已应用的所有更改。)

关于c# - 如何在WPF中闪烁RichTextBox边框和背景色,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57403045/

相关文章:

wpf - WPF RichTextBox和'\t'

c# - 数据模型中没有为 TextBox Text 属性调用 Setter

node.js - 使用 Firefox 从 Nightwatch.js 自动运行 Selenium 时出错

android - 如何在没有 Android Studio 的情况下仅使用 SDK 工具为 React-Native 设置 Android?

c# - C# 中的 Math.Round 可以用于整数值吗?

c# - 在 App_Web_*.dll 中找到了 index.cshtml 的副本,但正确的源代码与 App_Web_*.dll 中内置的版本不同,

C# 有效地替换字典中的 -Infinity 值

c# - 如何在 C# 中创建类 A,其中 A 只能由 B、C、D 继承?

c# - 根据ViewModel属性更改更改 View 元素属性

c# - 使用 ProcessID 或窗口句柄获取子窗口?