简体   繁体   English

如何在WPF中闪烁RichTextBox边框和背景颜色

[英]How to flash a RichTextBox border and background colors in WPF

Seems like this should be fairly simple, as it is in Winforms, but I'm relatively new to WPF so still trying to change thinking how data and UI interact. 看起来这应该是相当简单的,就像在Winforms中一样,但我对WPF相对较新,所以仍然试图改变思考数据和UI如何交互。

Scenario: User clicks a button on my main form. 场景:用户单击主窗体上的按钮。 The button is used to enter a street address. 该按钮用于输入街道地址。 In the street address form, I do some basic data validation when the user clicks a submit button. 在街道地址表单中,当用户单击提交按钮时,我会进行一些基本数据验证。 Submit() iterates through each of the data entry fields and calls the method below to attempt to alert the user to the offending data field. Submit()遍历每个数据输入字段并调用下面的方法以尝试提醒用户有问题的数据字段。

Here's the code I have that doesn't do anything that I can detect: 这是我的代码,它没有做任何我能检测到的事情:

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

As I've noted in my comment , the primary issue with your code is that you have blocked the UI thread. 正如我在评论中提到的,您的代码的主要问题是您已阻止UI线程。 So, while you are changing the properties of interest to new values in a loop, the actual UI never gets a chance to update the visual representation, ie what's on the screen. 因此,当您在循环中将感兴趣的属性更改为新值时,实际的UI永远不会有机会更新可视化表示,即屏幕上的内容。

Ironically, while you note "Seems like this should be fairly simple, as it is in Winforms" , had you tried to write the same code in a Winforms program, you would have had the exact same problem. 具有讽刺意味的是,虽然你注意到“这样看起来应该相当简单,就像在Winforms中一样” ,如果你试图在Winforms程序中编写相同的代码,你会遇到完全相同的问题。 Both Winforms and WPF (and indeed, most GUI APIs) have exactly this same limitation: there's one thread that handles all of the UI, and after you change one or more data values that should affect how the UI looks, you have to return control to the UI thread that called you, so that it can then update the screen. Winforms和WPF(实际上,大多数GUI API)都有完全相同的限制:有一个线程可以处理所有UI,在您更改一个或多个应该影响UI外观的数据值之后,您必须返回控制权到调用你的UI线程,然后它可以更新屏幕。

Now, you note also that you are "trying to change thinking how data and UI interact" . 现在,您还注意到“正在尝试改变数据和UI交互方式的思考” This is a good thing, and if you are willing to take the time to learn the MVVM concepts WPF was designed to work with, that will help a lot. 这是一件好事,如果您愿意花时间学习WPF旨在使用的MVVM概念,那将会有很大帮助。 Winforms also has a data binding model, and you can in fact write very similar code in Winforms as is strongly encouraged for WPF. Winforms还有一个数据绑定模型,实际上你可以在Winforms中编写非常相似的代码,这是WPF 强烈建议的。 But, WPF's "retained" graphics model as opposed to Winform's "immediate" model — ie WPF keeps track of what your graphics are supposed to look like, while Winform demands that you handle drawing the graphics yourself every time the screen needs updating — lends itself much better to the data binding approach, and WPF's entire design is based on that. 但是,WPF的“保留”图形模型与Winform的“即时”模型相反 - 即WPF跟踪您的图形应该是什么样子,而Winform要求您在每次屏幕需要更新时自己绘制图形 - 适合自己更好的数据绑定方法,WPF的整个设计就是基于此。

This means that you should work hard to keep your data where the data goes, and your UI where the UI goes. 这意味着您应该努力将数据保存在数据所在的位置,以及UI所在的UI。 Ie data is in your code-behind, and UI is in the XAML. 即数据在您的代码隐藏中,而UI在XAML中。 It's a good idea in both APIs, but you're sacrificing a lot more if you fail to do it with WPF. 这两个API都是一个好主意,但如果你没有使用WPF,那么你会牺牲更多。

So where does that leave your question? 那你的问题在哪里呢? Well, lacking a good minimal, complete, and verifiable code example , it's hard to know what your code looks like, and so what would be the best way to fix it. 好吧,缺少一个好的最小,完整和可验证的代码示例 ,很难知道你的代码是什么样的,所以最好的解决方法是什么。 So instead, I'll provide a couple of examples in the hopes that after you reorient your code to fit the WPF paradigm better, you can apply one as you see fit. 相反,我将提供一些示例,希望在您重新调整代码以更好地适应WPF范例之后,您可以根据需要应用一个。 (Unfortunately, one of the things I don't like much about WPF is that in some ways it's too powerful, offering many different ways to accomplish the same result; this can make it really hard sometimes to know what is the best way.) (不幸的是,我对WPF不太满意的一点是,它在某些方面强大了,提供了许多不同的方法来实现相同的结果;这有时会让人很难知道什么是最好的方法。)

These two examples differ from each other in how much code-behind they require. 这两个示例在它们需要多少代码隐藏方面彼此不同。 The first puts the animation logic into the C# code, as part of the view model. 第一个将动画逻辑放入C#代码中,作为视图模型的一部分。 On the one hand, this is arguably less "the WPF way". 一方面,这可以说是“WPF方式”。 But the second, which uses the view code (ie the XAML) to define the animation, requires a tiny bit of extra plumbing in the view's code-behind, which bugs me a little, as it blurs the line between view and view model a bit more than I'd like. 但第二个,它使用视图代码(即XAML)来定义动画,在视图的代码隐藏中需要一点额外的管道,这让我有点烦恼,因为它模糊了视图和视图模型之间的界限a比我想要的多一点。

Oh well. 那好吧。

Here's the view model class for the first approach: 这是第一种方法的视图模型类:

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

The second class there, NotifyPropertyChangedBase , is just my standard base class for my view models. 那里的第二个类NotifyPropertyChangedBase只是我的视图模型的标准基类。 It contains all the boilerplate to support the INotifyPropertyChanged interface. 它包含支持INotifyPropertyChanged接口的所有样板。 There are WPF frameworks that include such a base class themselves; WPF框架本身包含这样的基类; why WPF doesn't itself provide one, I don't know. 为什么WPF本身不提供一个,我不知道。 But it's handy to have, and between it and a Visual Studio code snippet to paste in the property template, it makes it a lot faster to put together the view models for a program. 但它很方便,并且在它和Visual Studio代码片段之间粘贴属性模板,它使得将程序的视图模型组合在一起要快得多。

Similarly, the third class, DelegateCommand , makes it easier to define ICommand objects. 同样,第三个类DelegateCommand使定义ICommand对象变得更容易。 Again, this type of class is available in third-party WPF frameworks as well. 同样,这种类也可以在第三方WPF框架中使用。 (I also have a version of the class that is generic with the type parameter specifying the type of the command parameter passed to the CanExecute() and Execute() methods, but since we don't need that here, I didn't bother to include it. (我也有一个通用类的参数,类型参数指定传递给CanExecute()Execute()方法的命令参数的类型,但由于我们不需要在这里,我没有打扰包括它。

As you can see, once you get past the boilerplate, the code's pretty simple. 正如您所看到的,一旦您通过样板,代码就非常简单。 It has a pro-forma Text property just so I have something to bind to the TextBox in my UI. 它有一个形式的Text属性,所以我有一些东西要绑定到我的UI中的TextBox It also has a couple of bool properties that relate to the visual state of the TextBox . 它还有一些与TextBox的可视状态相关的bool属性。 One determines the actual visual state, while the other provides some indication as to whether that state is currently being animated. 一个确定实际的视觉状态,而另一个提供关于该状态当前是否正在动画的一些指示。

There are two ICommand instances providing user interaction with the view model. 有两个ICommand实例提供用户与视图模型的交互。 One just toggles the visual state, while the other causes the animation you want to happen. 一个只是切换视觉状态,而另一个导致你想要发生的动画。

Finally, there's the method that actually does the work. 最后,有实际工作的方法。 It first sets the IsAnimating property, then loops ten times to toggle the IsHighlighted property. 它首先设置IsAnimating属性,然后循环十次以切换IsHighlighted属性。 This method uses async . 此方法使用async In a Winforms program, this would be essential, so that the UI property updates happened in the UI thread. 在Winforms程序中,这是必不可少的,因此UI属性更新发生在UI线程中。 But in this WPF program, it's optional. 但在这个WPF程序中,它是可选的。 I like the async/await programming model, but for simple property-change notifications, WPF will marshal the binding update back to the UI thread as necessary, so you could in fact just create a background task in the thread pool or a dedicated thread to handle the animation. 我喜欢async / await编程模型,但是对于简单的属性更改通知,WPF会根据需要将绑定更新封送回UI线程,因此您实际上只需在线程池或专用线程中创建后台任务。处理动画。

(For the animation, I used 200 ms between frames instead of 100 as your code would've, just because I think it looks better, and in any case makes it easier to see what the animation is doing.) (对于动画,我在帧之间使用200毫秒而不是100代码,因为我认为它看起来更好,并且无论如何都更容易看到动画正在做什么。)

Note that the view model itself has no idea there's a UI involved per se. 请注意,视图模型本身并不知道本身就涉及到UI。 It just has a property that indicates whether the text box should be highlighted or not. 它只有一个属性,指示是否应突出显示文本框。 It's up to the UI to figure out how to do that. 由UI来决定如何做到这一点。

And that, looks like this: 而且,看起来像这样:

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

This just sets some default values for the border and background colors. 这只是为边框和背景颜色设置了一些默认值。 And then, importantly, it defines a data trigger that will temporarily override these defaults any time the condition in the data trigger is true. 然后,重要的是,它定义了一个数据触发器,只要数据触发器中的条件为真,它就会暂时覆盖这些默认值。 That is, the declared binding evaluates to the declared value given (which in my example above is in fact the bool value of true ). 也就是说,声明的绑定计算为给定的声明值(在上面的示例中,实际上是bool值为true )。

Every place you see an element property set to something that looks like {Binding} , that's a reference back to the current data context, which in this case is set to my view model class. 您看到元素属性的每个地方都设置为看起来像{Binding}东西,这是对当前数据上下文的引用,在本例中它被设置为我的视图模型类。

Now, WPF has a very rich animation feature set, and that can be used instead of the above to handle the flashing animation. 现在,WPF具有非常丰富的动画功能集,可以使用它来代替上面的处理闪烁动画。 If we're going to do it that way, then the view model can be simpler, as we don't need the explicit property for the highlighted state. 如果我们要这样做,那么视图模型可以更简单,因为我们不需要突出显示状态的显式属性。 We do still need the IsAnimating property, but this time instead of the "animate" command calling a method, which sets this property as a side-effect, the command sets the property directly and does nothing else (and that property, now the primary controller for the animation, still does serve as the flag so that the button's command can be enabled/disabled as needed): 我们仍然需要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);
    }
}

Importantly, you'll notice that now the view model doesn't contain any code to actually run the animation. 重要的是,您会注意到现在视图模型不包含任何实际运行动画的代码。 That, we'll find in the XAML: 那,我们会在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>

In this case, there's a Storyboard object which contains two animation sequences (both are started simultaneously) that do the actual flashing of the control. 在这种情况下,有一个Storyboard对象,它包含两个动画序列(两个同时启动),用于实际控制闪烁。 The storyboard itself lets you specify how many times it should repeat ( "5x" in this case, for five times), and then within each animation sequence, the duration of the whole sequence (400 ms, since one sequence involves two states, each displayed for 200 ms), and then the "key frames" that dictate what actually happens during the animation, each specifying at what time during the animation it should take effect. 故事板本身允许您指定它应重复的次数(在这种情况下为"5x" ,五次),然后在每个动画序列中,指定整个序列的持续时间(400 ms,因为一个序列涉及两个状态,每个显示200毫秒),然后显示决定动画期间实际发生的事情的“关键帧”,每个关键帧指定在动画期间应该生效的时间。

Then, in the text box's style, instead of triggering property setters, the storyboard is started and stopped according to the trigger state (entered or exited). 然后,在文本框的样式中,不是触发属性设置器,而是根据触发状态(输入或退出)启动和停止故事板。

Note that in the storyboard, the Completed event is subscribed to. 请注意,在故事板中,订阅了Completed事件。 Whereas in the previous example, there was no change to the default MainWindow.xaml.cs file, for this version there's a little bit of code: 而在前面的示例中,默认的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;
    }
}

This has the implementation of the event handler for the Storyboard.Completed event. 这具有Storyboard.Completed事件的事件处理程序的实现。 And since that handler is going to need to modify the view model state, there is now code to retrieve the view model from the DataContext property and save it in a field so that the event handler can get at it. 由于该处理程序将需要修改视图模型状态,现在有代码从DataContext属性中检索视图模型并将其保存在字段中,以便事件处理程序可以获取它。

This event handler is what allows the IsAnimating property to be set back to false once the animation has completed. 此事件处理程序允许在动画完成IsAnimating属性设置回false

So, there you go. 所以,你去吧。 It is possible that there's a better way to do this, but I think these two examples should give you a good place to start in terms of seeing how things "ought to be done" in WPF. 有可能有一个更好的方法来做到这一点,但我认为这两个例子应该为你提供一个好的开始,看看在WPF中“应该如何完成”的事情。

(I'll admit, the one thing that really bugs me about the animation approach is that I'd rather not have to explicitly state in the storyboard the original colors for the text box; but I'm not aware of any way to specify a key frame in the <ColorAnimationUsingKeyFrame/> element that instead of actually setting a new color, just removes whatever changes the animation had already applied.) (我承认,关于动画方法真正让我烦恼的一件事是,我宁愿不必在故事板中明确说出文本框的原始颜色;但我不知道有任何指定的方法<ColorAnimationUsingKeyFrame/>元素中的一个关键帧,它不是实际设置新颜色,而是删除动画已经应用的任何更改。)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM