【问题标题】:How do I safely populate with data and Refresh() a DataGridView in a multi-threaded application?如何在多线程应用程序中安全地填充数据和 Refresh() DataGridView?
【发布时间】:2008-11-03 16:13:47
【问题描述】:

我的应用有一个 DataGridView 对象和一个 MousePos 类型的列表。 MousePos 是一个自定义类,它保存鼠标 X、Y 坐标(类型为“Point”)和该位置的运行计数。我有一个线程 (System.Timers.Timer) 每秒引发一次事件,检查鼠标位置,添加和/或更新此列表上的鼠标位置计数。

我希望有一个类似的运行线程(同样,我认为 System.Timers.Timer 是一个不错的选择),它会再次每秒引发一次事件以自动 Refresh() DataGridView,以便用户可以看到屏幕上的数据更新。 (就像 TaskManager 一样。)

不幸的是,调用 DataGridView.Refresh() 方法会导致 VS2005 停止执行并注意到我遇到了跨线程情况。

如果我理解正确,我现在有 3 个线程:

  • 主 UI 线程
  • MousePos 列表线程(定时器)
  • DataGridView 刷新线程(定时器)

为了查看是否可以 Refresh() 主线程上的 DataGridView,我向名为 DataGridView.Refresh() 的表单添加了一个按钮,但这(奇怪的是)没有做任何事情。我发现一个主题似乎表明如果我设置 DataGridView.DataSource = null 并返回到我的列表,它将刷新数据网格。这确实有效,但只能通过按钮(在主线程上处理)。


所以这个问题变成了两部分:

  1. 将 DataGridView.DataSource 设置为 null 并返回到我的 List 是一种可接受的刷新数据网格的方法吗? (对我来说似乎效率低下......)
  2. 如何在多线程环境中安全地执行此操作?

这是我目前编写的代码 (C#/.Net 2.0)

public partial class Form1 : Form
{
    private static List<MousePos> mousePositionList = new List<MousePos>();
    private static System.Timers.Timer mouseCheck = new System.Timers.Timer(1000);
    private static System.Timers.Timer refreshWindow = new System.Timers.Timer(1000);

    public Form1()
    {
        InitializeComponent();
        mousePositionList.Add(new MousePos());  // ANSWER! Must have at least 1 entry before binding to DataSource
        dataGridView1.DataSource = mousePositionList;
        mouseCheck.Elapsed += new System.Timers.ElapsedEventHandler(mouseCheck_Elapsed);
        mouseCheck.Start();
        refreshWindow.Elapsed += new System.Timers.ElapsedEventHandler(refreshWindow_Elapsed);
        refreshWindow.Start();
    }

    public void mouseCheck_Elapsed(object source, EventArgs e)
    {
        Point mPnt = Control.MousePosition;
        MousePos mPos = mousePositionList.Find(ByPoint(mPnt));
        if (mPos == null) { mousePositionList.Add(new MousePos(mPnt)); }
        else { mPos.Count++; }
    }

    public void refreshWindow_Elapsed(object source, EventArgs e)
    {
        //dataGridView1.DataSource = null;               // Old way
        //dataGridView1.DataSource = mousePositionList;  // Old way
        dataGridView1.Invalidate();                      // <= ANSWER!!
    }

    private static Predicate<MousePos> ByPoint(Point pnt)
    {
        return delegate(MousePos mPos) { return (mPos.Pnt == pnt); };
    }
}

public class MousePos
{
    private Point position = new Point();
    private int count = 1;

    public Point Pnt { get { return position; } }
    public int X { get { return position.X; } set { position.X = value; } }
    public int Y { get { return position.Y; } set { position.Y = value; } }
    public int Count { get { return count; } set { count = value; } }

    public MousePos() { }
    public MousePos(Point mouse) { position = mouse; }
}

【问题讨论】:

  • 我猜把 [C#] 放在主语里是禁止的吧?

标签: c# .net multithreading datagridview delegates


【解决方案1】:

您必须像所有其他控件一样更新主 UI 线程上的网格。请参阅 control.Invoke 或 Control.BeginInvoke。

【讨论】:

    【解决方案2】:

    更新! -- 我部分在“Pro .NET 2.0 Windows Forms and Customer”一书中找到了part #1的答案C# 中的控件"

    我原本以为 Refresh() 没有做任何事情,我需要调用 Invalidate() 方法来告诉 Windows 重新绘制我的控件这是休闲。 (通常是立即进行,但如果您需要保证现在重绘它,那么立即调用 Update() 方法进行跟进。)

        dataGridView1.Invalidate();
    

    但是,Refresh() 方法只是一个别名:

        dataGridView1.Invalidate(true);
        dataGridView1.Update();             // <== forces immediate redraw
    

    我发现的唯一问题是,如果 dataGridView 中没有数据,那么再多的失效也不会刷新控件。我不得不重新分配数据源。然后它工作得很好。但仅针对行数(或我列表中的项目)——如果添加了新项目,dataGridView 将不知道还有更多行要显示。

    因此,当将数据源(列表或表)绑定到数据源时,dataGridView 会计算项目(行),然后在内部进行设置,从不检查是否有新的行/项目或行/已删除的项目。这就是为什么重复重新绑定数据源之前可以正常工作的原因。

    现在要弄清楚如何更新要在 dataGridView 中显示的行数,而无需重新绑定数据源...有趣、有趣、有趣! :-)


    在做了一些挖掘之后,我想我对我的问题的第 #2 部分有了答案(又名安全多线程):

    我发现我应该使用 System.Windows.Forms.Timer,而不是使用 System.Timers.Timer

    事件的发生使得回调中使用的方法自动发生在主线程上。没有跨线程问题!

    声明如下:

    private static System.Windows.Forms.Timer refreshWindow2;
    refreshWindow2 = new Timer();
    refreshWindow2.Interval = 1000;
    refreshWindow2.Tick += new EventHandler(refreshWindow2_Tick);
    refreshWindow2.Start();
    

    而且方法是这样的:

    private void refreshWindow2_Tick(object sender, EventArgs e)
    {
        dataGridView1.Invalidate();
    }
    

    【讨论】:

    • 重置数据源的更优雅的方法是使用 BindingContext 并刷新它。这意味着您不必每次都将其重置为 null。在自动更新方面,我认为您需要一个实现 INotifyPropertyChanged 的​​数据源。
    【解决方案3】:

    看起来你的答案就在那里! 只是在 cawse 你很好奇如何做跨线程调用回 ui: 所有控件都有一个 Invoke() 方法(或 BEginInvoke() - 如果您想异步执行操作),它用于在主 UI 线程的上下文中调用控件上的任何方法。 因此,如果您要从另一个线程调用您的 datagridview,您需要执行以下操作:

    public void refreshWindow_Elapsed(object source, EventArgs e)
    {
    
       // we use anonymous delgate here as it saves us declaring a named delegate in our class
       // however, as c# type inference sometimes need  a bit of 'help' we need to cast it 
       // to an instance of MethodInvoker
       dataGridView1.Invoke((MethodInvoker)delegate() { dataGridView1.Invalidate(); });
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-07-05
      相关资源
      最近更新 更多