2

首先,道歉:我是在这个网站上发布问题的新手,所以对于格式或信息错误,我深表歉意。我已经看到了许多从串行端口获取数据并使用它来填充文本框、图表的答案等在主窗体上,使用“调用”,因为串行端口在不同的线程中运行。

我正在尝试将我们一直使用的一些通信内容“概括”到一个班级中(是的,旧的 VB6 程序员正在努力成长:-) 我遇到了问题。如果我在主 program.cs 中强制使用表单名称并为类使用相同的命名空间,我可以做一些事情,但这有点违背了目的。我还尝试在类中的串行端口的“已接收”上添加一个事件,以在主窗体上引发一个事件。该事件试图引发,但发生跨线程异常。

此时的代码相当大,所以我将尝试“概述”它。以简单的形式,假设我有一个名为“Form1”的 for,其中包含一个名为 textbox1 的文本框和一个名为“SerialThing”的类:


表格1:

SerialThing mySerialThing ;

Form1_Load:

mySerialThing = new SerialThing();

显示数据()

Textbox1.Text = "You Got Data!";

串行事物:

Static SerialPort myDevice;

在里面()

myDevice = new SerialPort;
myDevice.DataReceived += new SerialDataReceivedEventHandler(devicePort_DataReceived);

devicePort_DataReceived()

this.Invoke(new EventHandler(DisplayData));

如果将串口放在主窗体上,上述方法将起作用,但如果在类中创建则不会。

再次,如果太复杂或太简单,对不起。我正在寻找一种“简单”的方法来做到这一点,但保持类“通用”(理想情况下不必让工作区名称匹配等)。

-Vin

4

1 回答 1

1

有很多很多方法可以做到这一点。我将介绍使用自定义事件、委托和 Invoke() 的经典方法,因为我认为理解该过程很重要。一旦你明白了这一点,你就可以跳到一些较新的方法。

首先,在您的 SerialThing() 类中,您声明一个自定义事件以在收到数据时传递数据:

class SerialThing
{

    public delegate void DataReceivedDelegate(string data);
    public event DataReceivedDelegate DataReceived;

    static SerialPort myDevice;

    public SerialThing()
    {
        myDevice = new SerialPort();
        myDevice.DataReceived += new SerialDataReceivedEventHandler(myDevice_DataReceived);
    }

    void myDevice_DataReceived(object sender, SerialDataReceivedEventArgs e)
    {
        // ... grab the data and place into a string called "data" ...
        string data = "";

        // raise our custom event:
        if (DataReceived != null)
        {
            DataReceived(data);
        }
    }

}

现在,在 Form1 中,您在创建 SerialThing 实例时订阅该自定义事件。此外,当接收到该事件时,您可以使用 InvokeRequired、Invoke 和委托将来自辅助线程的调用编组到主线程:

public partial class Form1 : Form
{

    SerialThing mySerialThing;

    public Form1()
    {
        InitializeComponent();
    }

    private void Form1_Load(object sender, EventArgs e)
    {
        mySerialThing = new SerialThing();
        mySerialThing.DataReceived += new SerialThing.DataReceivedDelegate(mySerialThing_DataReceived);
    }

    private delegate void DataReceivedDelegate(string data);

    void mySerialThing_DataReceived(string data)
    {
        if (this.InvokeRequired)
        {
            this.Invoke(new DataReceivedDelegate(mySerialThing_DataReceived), new Object[] { data });
        }
        else
        {
            textBox1.Text = data;
        }
    }

}

编辑:针对您的以下评论...

将委托视为简单的“指向方法的指针”。当您执行委托时,相关的方法就会运行。

InvokeRequired() 部分确定代码是否在与创建控件的线程不同的线程中运行。在这种情况下,控件是表单本身 ( this)。如果返回 true,则事件是在不同的线程中接收的。然后我们继续this.Invoke()在块的真实部分内排队If。再次this指Form。因此,表单请求在创建它的线程(主 UI 线程)上调用(“运行”)传递的委托。我们创建了一个委托的实例,它实际上指向了我们已经在导致递归调用的相同方法。第二个参数只是一个 Object 数组,用于与委托一起传递参数。

当 Invoke() 运行时,由于递归调用,我们最终会重新进入该方法。然而,此时 InvokeRequired() 检查将返回 false,因为我们现在正在主 UI 线程中运行。因此,我们下拉到If我们更新 TextBox 的语句的错误部分。在这种模式中,更新语句else块中的GUI 控件是安全的。If

请注意,这里不需要递归调用。这只是一种风格选择。我们本可以使用委托所指向的第二个“帮助”函数,然后调用它。递归方法减少了所需方法的数量。

这可能是解决此类问题的最冗长的方法。不过,我喜欢它,因为它显示了事件和数据的流动,以及线程之间的移动。

我们可以使用匿名委托将所有表单代码缩短到这样:

    private void Form1_Load(object sender, EventArgs e)
    {
        mySerialThing = new SerialThing();
        mySerialThing.DataReceived += delegate (string data)
        {
            this.Invoke((MethodInvoker)(delegate() { textBox1.Text = data; }));
        };
    }

我不了解你,但作为一名前 VB6 程序员,当你第一次看到这种类型的东西时,这看起来很奇怪。

我还使用了我知道在不同线程中运行的组件,但是“表单代码”从来不必使用委托的东西,所以也许有一些东西可以埋在类中?

是的,可以将一些“魔法”烘焙到一个类中,这样它就可以在主 UI 线程上引发事件,因此不需要任何 Invoke() 调用。一种方法是通过使用SynchronizationContext

解决此类问题的另一种可能性是使用 BackgroundWorker() 控件,该控件具有在主 UI 线程中为您引发的诸如 ProgressChanged() 和 RunWorkerCompleted() 之类的事件(它们在后台执行必要的调用类型的东西为你)。

于 2013-11-12T00:10:53.477 回答