如何刷新循环内设置的可视控件属性 (TextBlock.text)?

Posted

技术标签:

【中文标题】如何刷新循环内设置的可视控件属性 (TextBlock.text)?【英文标题】:How do I refresh visual control properties (TextBlock.text) set inside a loop? 【发布时间】:2011-07-27 03:07:32 【问题描述】:

每次循环迭代时,我都想直观地更新文本块的文本。我的问题是 WPF 窗口或控件在循环完成之前不会在视觉上刷新。

for (int i = 0; i < 50; i++)

    System.Threading.Thread.Sleep(100);
    myTextBlock.Text = i.ToString();                

在 VB6 中,我会调用 DoEvents()control.Refresh。目前我只想要一个类似于DoEvents() 的快速而肮脏的解决方案,但我也想知道替代方案或“正确”的方法来做到这一点。我可以添加一个简单的绑定语句吗?语法是什么? 提前致谢。

【问题讨论】:

【参考方案1】:

您可以执行 Dispatcher.BeginInvoke 以执行线程上下文切换,以便渲染线程可以完成其工作。 但是,这不是处理此类事情的正确方法。你应该使用 Animation + Binding 来完成类似的事情,因为这是在 WPF 中做类似事情的免破解方式。

【讨论】:

只有当 UI 更新是纯粹的吸引眼球时,您才能使用动画。如果您确实希望 UI 反映某个进程的状态,则无法做到这一点。 我不同意。动画也可以做到这一点。您唯一需要做的就是在您希望动画发生的地方实现一个 DP。 @EladKatz:当然,您可以绑定到属性并使用适当的转换器来实现 UI 更改。动画从何而来? 您为 DP 制作动画。 TextBox 将绑定到该 DP。 @EladKatz:当你想让它反映进程的状态时,为什么要为它制作动画?【参考方案2】:

这就是你通常的做法:

ThreadPool.QueueUserWorkItem(ignored =>
    
        for (int i = 0; i < 50; i++) 
            System.Threading.Thread.Sleep(100);
            myTextBlock.Dispatcher.BeginInvoke(
                new Action(() => myTextBlock.Text = i.ToString()));
        
    );

这会将操作委托给工作池线程,该线程允许您的 UI 线程处理消息(并防止您的 UI 冻结)。因为工作线程不能直接访问myTextBlock,所以需要使用BeginInvoke

虽然这种方法不是“WPF 方式”做事,但它没有任何问题(事实上,我不相信这个小代码有任何替代方案)。但另一种做事方式是这样的:

    TextBlockText 绑定到实现INotifyPropertyChanged 的某个对象的属性 在循环中,将该属性设置为您想要的值;更改将自动传播到 UI 不需要线程、BeginInvoke 或其他任何东西

如果你已经有一个对象并且 UI 可以访问它,这就像写 Text="Binding MyObject.MyProperty" 一样简单。

更新:例如,假设你有这个:

class Foo : INotifyPropertyChanged // you need to implement the interface

    private int number;

    public int Number 
        get  return this.number; 
        set 
            this.number = value;
            // Raise PropertyChanged here
        
    


class MyWindow : Window

    public Foo PropertyName  get; set; 

绑定将在 XAML 中像这样完成:

<TextBlock Text="Binding PropertyName.Number" />

【讨论】:

唯一实际做的是 Dispatcher.BeginInvoke。我之前已经说过... @EladKatz:将代码移动到另一个线程什么都不做?我想是的,如果将您的 UI 冻结 100 毫秒算作“无”。 +1 表示答案第二部分的 3 个步骤。这完美地工作并且很容易实现。这也是 MVVM 的重要组成部分。 虽然您的代码示例运行良好,但它太复杂了,我无法记住。我可以为 this.MyProperty 使用什么绑定语法?我快速而肮脏的形式只使用背后的代码 - 没有外部对象。我没有得到正确的绑定语法。如何用引用 WPF 窗口上的属性的绑定语句替换您的 MyObject.MyProperty?我不会直接设置 myTextBlock.text,而是更新 Window 属性,该属性又会更新绑定的 textblock.text。【参考方案3】:

如果您真的想要快速而肮脏的实现,并且不关心将来维护产品或用户体验,您可以添加对 System.Windows.Forms 的引用并拨打System.Windows.Forms.Application.DoEvents():

for (int i = 0; i < 50; i++)

    System.Threading.Thread.Sleep(100);
    MyTextBlock.Text = i.ToString();
    System.Windows.Forms.Application.DoEvents();

缺点是它真的很糟糕。您将在 Thread.Sleep() 期间锁定 UI,这会惹恼用户,并且您最终可能会根据程序的复杂性得出不可预测的结果(我见过一个应用程序,其中两个方法在UI线程,每一个都重复调用DoEvents()...)。

应该这样做:

    任何时候您的应用程序必须等待某些事情发生(即磁盘读取、Web 服务调用或 Sleep()),它都应该在一个单独的线程上。 您不应手动设置 TextBlock.Text - 将其绑定到属性并实现 INotifyPropertyChanged。

这是一个示例,展示了您所要求的功能。编写只需要几秒钟的时间,而且使用起来更容易 - 而且它不会锁定 UI。

Xaml:

<StackPanel>
    <TextBlock Name="MyTextBlock" Text="Binding Path=MyValue"></TextBlock>
    <Button Click="Button_Click">OK</Button>
</StackPanel>

代码隐藏:

public partial class MainWindow : Window, INotifyPropertyChanged

    public MainWindow()
    
        InitializeComponent();
        this.DataContext = this;
    

    private void Button_Click(object sender, RoutedEventArgs e)
    
        Task.Factory.StartNew(() =>
        
            for (int i = 0; i < 50; i++)
            
                System.Threading.Thread.Sleep(100);
                MyValue = i.ToString();
            
        );
    

    private string myValue;
    public string MyValue
    
        get  return myValue; 
        set
        
            myValue = value;
            RaisePropertyChanged("MyValue");
        
    

    private void RaisePropertyChanged(string propName)
    
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propName));
    
    public event PropertyChangedEventHandler PropertyChanged;

代码可能看起来有点复杂,但它是 WPF 的基石,并且需要一些练习 - 非常值得学习。

【讨论】:

太棒了!其他答案很好,但我最了解这个答案,它包括我要求的快速而肮脏的解决方案。为了使 Greg 的解决方案发挥作用,我添加了: using System.ComponentModel; (和)使用 System.Threading.Tasks;。不要忘记参考 System.Windows.Forms 以使 DoEvents() 解决方案起作用。我学到了很多东西。谢谢! 这段代码在 .net 3.5 中是什么样的(特别是 Task.Factory.StartNew) Task.Factory.StartNew 是在 .net 4 中引入的。只需搜索 .net 3.5 threading 即可找到遗留机制。【参考方案4】:

我尝试了此处公开的解决方案,但在我添加以下内容之前它对我不起作用:

创建一个扩展方法并确保从您的项目中引用它的包含程序集。

public static void Refresh(this UIElement uiElement)
    uiElement.Dispatcher.Invoke(DispatcherPriority.Background, new Action( () =>  ));

然后在RaisePropertyChanged之后直接调用:

RaisePropertyChanged("MyValue");
myTextBlock.Refresh();

这将强制 UI 线程控制一小段时间并调度 UI 元素上的任何未决更改。

【讨论】:

【参考方案5】:
for (int i = 0; i < 50; i++)

    System.Threading.Thread.Sleep(100);
    myTextBlock.Text = i.ToString();                

在后台工作组件中运行上述代码并使用绑定 updatesourcetrigeer 作为 propertychanged 将立即在 UI 控件中反映更改。

【讨论】:

以上是关于如何刷新循环内设置的可视控件属性 (TextBlock.text)?的主要内容,如果未能解决你的问题,请参考以下文章

asp.net控件treeview如何实现无刷新效果

如何彻底更改 recyclerview 中的 item 布局中的某个控件的某个属性

如何找到datalist 内控件textbox并设置其属性

winform 中如何获取计时器控件的值

如何在给定的时间间隔内自动刷新网页所有控件

创建“非可视”.NET 用户控件