簡體   English   中英

如何在WPF中閃爍RichTextBox邊框和背景顏色

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

看起來這應該是相當簡單的,就像在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);
                });
            }
        });
    }

正如我在評論中提到的,您的代碼的主要問題是您已阻止UI線程。 因此,當您在循環中將感興趣的屬性更改為新值時,實際的UI永遠不會有機會更新可視化表示,即屏幕上的內容。

具有諷刺意味的是,雖然你注意到“這樣看起來應該相當簡單,就像在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,那么你會犧牲更多。

那你的問題在哪里呢? 好吧,缺少一個好的最小,完整和可驗證的代碼示例 ,很難知道你的代碼是什么樣的,所以最好的解決方法是什么。 相反,我將提供一些示例,希望在您重新調整代碼以更好地適應WPF范例之后,您可以根據需要應用一個。 (不幸的是,我對WPF不太滿意的一點是,它在某些方面強大了,提供了許多不同的方法來實現相同的結果;這有時會讓人很難知道什么是最好的方法。)

這兩個示例在它們需要多少代碼隱藏方面彼此不同。 第一個將動畫邏輯放入C#代碼中,作為視圖模型的一部分。 一方面,這可以說是“WPF方式”。 但第二個,它使用視圖代碼(即XAML)來定義動畫,在視圖的代碼隱藏中需要一點額外的管道,這讓我有點煩惱,因為它模糊了視圖和視圖模型之間的界限a比我想要的多一點。

那好吧。

這是第一種方法的視圖模型類:

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 它還有一些與TextBox的可視狀態相關的bool屬性。 一個確定實際的視覺狀態,而另一個提供關於該狀態當前是否正在動畫的一些指示。

有兩個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>

這只是為邊框和背景顏色設置了一些默認值。 然后,重要的是,它定義了一個數據觸發器,只要數據觸發器中的條件為真,它就會暫時覆蓋這些默認值。 也就是說,聲明的綁定計算為給定的聲明值(在上面的示例中,實際上是bool值為true )。

您看到元素屬性的每個地方都設置為看起來像{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 ms,因為一個序列涉及兩個狀態,每個顯示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/>元素中的一個關鍵幀,它不是實際設置新顏色,而是刪除動畫已經應用的任何更改。)

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM