4

我的应用程序有一个 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; }
}
4

3 回答 3

5

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

于 2008-11-03T16:15:57.070 回答
5

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

我原本以为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();
}
于 2008-11-03T18:17:08.527 回答
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(); });
}
于 2009-04-20T12:58:52.077 回答