C#WinForm多窗口取串口的数据的问题。

litemanc 2017-02-06 09:20:51
现在是做一个Mdi多窗口程序, 一个父界面,两个子界面,子1,子2。
不管是哪个界面,都需要从串口中接收数据或者发送数据,并且要在接收到数据之后,实时的显示到子1或者子2上,

在以前做的东西只有一个界面的时候, 我把对串口的操作都放在这一个winform对应的.cs中,不存在跨窗体的问题。

问题 :现在因为是跨窗体了,不明白怎么实现这两个子窗体都可以从串口取数据的过程。

补充: 看了一本书有讲到MDI窗口之间的传值问题,但是只是讲到了在打开新窗体的时候对构造函数进行赋值, 这样只是可以实现一次性赋值,但是实现不了实时的刷新。

请有经验的指点一下方向,实在是找不到方向。
...全文
1219 29 打赏 收藏 转发到动态 举报
写回复
用AI写文章
29 条回复
切换为时间正序
请发表友善的回复…
发表回复
litemanc 2017-03-04
  • 打赏
  • 举报
回复
本帖结贴。 采用@以专业开发人员为伍 的事件委托模式, 研究了好久,终于明白了, 非常感谢专业的指点。 也非常感谢其他各位的回答。 再次感谢!
booksell 2017-02-15
  • 打赏
  • 举报
回复
委托、事件;
crazyerer 2017-02-07
  • 打赏
  • 举报
回复
感觉很厉害,学习了
t729468926 2017-02-07
  • 打赏
  • 举报
回复
引用 23 楼 SjzEboy 的回复:
[quote=引用 18 楼 t729468926 的回复:] 比较好的解决办法是@sp1234 所说的,子窗体通过注册事件来处理数据。首先主窗体类初始化一个数据处理类,这个数据处理类里面定义一个通知事件,在这个数据处理类中接收到数据后就发起通知。然后主界面在实例化一个子窗体的同时也注册这个事件。详细的事件的用法可以参考 http://blog.csdn.net/jamestaosh/article/details/4372172 ,里面的 【加热器达到一定温度后通知报警器报警和显示器显示】 的这个例子我觉得挺合适的。 还有一个不那么优雅的办法,你可以在主窗体持有子窗体的引用,这样串口接收到数据后可以直接通过子窗体的引用调用子窗体的public方法。
您好,感谢您的回复。我已经看了这个例子了。也能理解。 有一个问题就是 Heater heater = new Heater(); Alarm alarm = new Alarm(); heater.BoilEvent += alarm.MakeAlert; //注册方法 heater.BoilEvent += (new Alarm()).MakeAlert; //给匿名对象注册方法 heater.BoilEvent += Display.ShowMsg; //注册静态方法 heater.BoilWater(); //烧水,会自动调用注册过对象的方法 第一行, 是先new了一个heater(小写),然后才可以注册, 如果不new,直接用Heater(大写)是不行的, 那我在打开子form1的时候,可以按照这个操作,没有问题, 如果再打开子form2, 我还得New一个heater吗? 新手,请谅解。。急需您的指点。。[/quote] 多个报警器: Heater heater = new Heater(); Alarm alarm1 = new Alarm(); Alarm alarm2 = new Alarm(); heater.BoilEvent += alarm1.MakeAlert; //注册方法 heater.BoilEvent += alarm2.MakeAlert; //注册方法 多个form的同理,不知道这样你能理解不。 如果直接用Heater(大写)也是可以的,不过这样的话Heater就是一个静态类,所涉及的委托和方法都得是静态的。你可以看看静态类,静态方法相关知识,同时你也可以就这个例子自己试着改一下,也不难的。
litemanc 2017-02-07
  • 打赏
  • 举报
回复
引用 18 楼 t729468926 的回复:
比较好的解决办法是@sp1234 所说的,子窗体通过注册事件来处理数据。首先主窗体类初始化一个数据处理类,这个数据处理类里面定义一个通知事件,在这个数据处理类中接收到数据后就发起通知。然后主界面在实例化一个子窗体的同时也注册这个事件。详细的事件的用法可以参考 http://blog.csdn.net/jamestaosh/article/details/4372172 ,里面的 【加热器达到一定温度后通知报警器报警和显示器显示】 的这个例子我觉得挺合适的。 还有一个不那么优雅的办法,你可以在主窗体持有子窗体的引用,这样串口接收到数据后可以直接通过子窗体的引用调用子窗体的public方法。
您好,感谢您的回复。我已经看了这个例子了。也能理解。 有一个问题就是 Heater heater = new Heater(); Alarm alarm = new Alarm(); heater.BoilEvent += alarm.MakeAlert; //注册方法 heater.BoilEvent += (new Alarm()).MakeAlert; //给匿名对象注册方法 heater.BoilEvent += Display.ShowMsg; //注册静态方法 heater.BoilWater(); //烧水,会自动调用注册过对象的方法 第一行, 是先new了一个heater(小写),然后才可以注册, 如果不new,直接用Heater(大写)是不行的, 那我在打开子form1的时候,可以按照这个操作,没有问题, 如果再打开子form2, 我还得New一个heater吗? 新手,请谅解。。急需您的指点。。
绿领巾童鞋 2017-02-07
  • 打赏
  • 举报
回复
定义好串口封装,然后在程序的界面主线程 中定义 并调用。
sunwen51 2017-02-07
  • 打赏
  • 举报
回复
是不是用委托去处理的,都是新手,别喷我,因为我自己只想到了这个。。。。
wangyongchao880622 2017-02-07
  • 打赏
  • 举报
回复
看来半天,都挺厉害。委托 事件处理是不是可以呢。
xiaoliang_hai 2017-02-06
  • 打赏
  • 举报
回复
分三楼写不好意思,我也是个新手,csdn我也是个新手 我主要的思路就是 (初始化)的时候把系统内所有可用串口存入串口对象集合,然后需要哪个取哪个用 (读取)在事件触发时把数据都处理好了放到字典里key为串口名,然后需要的时候去读取字典里的(根据串口名) (写入)的时候检验好发送
引用 12 楼 sp1234 的回复:
[quote=引用 10 楼 xiaoliang_hai 的回复:] 这是我曾经写的东西,如果能帮到你我很高兴. 这个前面一段获取控件信任的无视就好了,.......
分享代码是好事。 但 lz 的问题实际是进一步的问题,他是想知道如何设计,当有多个(而且是动态增加的)对象都来同时监听串口有消息的事件时,如何让每一个动态对象知道何时调用 GetResult 方法呢? 关键是“何时调用 GetResult” 这个设计,而不是 GetResult 这个方法本身。[/quote] 用timer应该可以解决
caoqijun 2017-02-06
  • 打赏
  • 举报
回复
sendmessage不行吗
  • 打赏
  • 举报
回复
引用 10 楼 xiaoliang_hai 的回复:
这是我曾经写的东西,如果能帮到你我很高兴. 这个前面一段获取控件信任的无视就好了,.......
分享代码是好事。 但 lz 的问题实际是进一步的问题,他是想知道如何设计,当有多个(而且是动态增加的)对象都来同时监听串口有消息的事件时,如何让每一个动态对象知道何时调用 GetResult 方法呢? 关键是“何时调用 GetResult” 这个设计,而不是 GetResult 这个方法本身。
xiaoliang_hai 2017-02-06
  • 打赏
  • 举报
回复
还有一些细微的方法
        /// <summary>
        /// 获取串口参数,该结果返回Json格式的字符串
        /// </summary>
        /// <param name="portname">获取的串口名</param>
        /// <returns>返回string类型结果</returns>
        public string GetPortParam(string portname)
        {
            //返回Json,结果为:"{串口名":{"BaudRate":"波特率","StopBits":"停止位","Parity":"校验位","DataBits":"数据位"}}
            return "{\"" + spList[portname].PortName +
                "\":{\"BaudRate\":\"" + spList[portname].BaudRate +
                "\",\"StopBits\":\"" + spList[portname].StopBits.ToString() +
                "\",\"Parity\":\"" + spList[portname].Parity.ToString() +
                "\",\"DataBits\":\"" + spList[portname].DataBits + "\"}}";
        }
        /// <summary>
        /// 根据提供的串口名设置该串口参数
        /// </summary>
        /// <param name="portname">串口名</param>
        /// <param name="BaudRate">波特率</param>
        /// <param name="StopBits">停止位</param>
        /// <param name="Parity">校验位</param>
        /// <param name="DataBits">数据位</param>
        /// <returns></returns>
        public string SetPortParam(string portname, int BaudRate, string StopBits, string Parity, int DataBits)
        {
            //把相应的数值赋值到对应的接口中,其中,枚举值则进行解析
            //枚举解析方法:(强转需要解析成的枚举)Enum.parse(typeof(解析成的枚举),该枚举的指定字符串【可以为该枚举的数字值,也可以是该枚举的表示字符串】);
            spList[portname].BaudRate = BaudRate;
            spList[portname].StopBits = (StopBits)Enum.Parse(typeof(StopBits), StopBits);
            spList[portname].Parity = (Parity)Enum.Parse(typeof(Parity), Parity);
            spList[portname].DataBits = DataBits;
            //返回提示字符串,并把修改后的参数罗列出来以便确认修改完成
            return string.Format(
                "修改完成!\n串口名:{0}\n波特率:{1}\n停止位:{2}\n校验位:{3}\n数据位:{4}"
                , spList[portname].PortName, spList[portname].BaudRate
                , spList[portname].StopBits, spList[portname].Parity
                , spList[portname].DataBits);
        }

        /// <summary>
        /// 返回已有的串口名
        /// </summary>
        /// <returns>返回已有的串口名</returns>
        public string getAllPortParam() 
        {
            string result = "{\"PortNames\":[";
            foreach (string item in spList.Keys)
            {
                result += "\"" + item + "\",";
            }
            result = result.Substring(0,result.Length-1)+"]}";
            return result;
        }
xiaoliang_hai 2017-02-06
  • 打赏
  • 举报
回复
这是我曾经写的东西,如果能帮到你我很高兴. 这个前面一段获取控件信任的无视就好了,当时想要编写一个控件的发现怎么布置都不行,就没用,但是底下方法用虚拟串口测试过没问题,里边有一些校验位和一些剪切数据的方法,如果用不上就无视好了
using System;
using System.Collections.Generic;
using System.Text;
using System.IO.Ports;
using System.Runtime.InteropServices;

namespace SerialPortActiveX
{
    public class SPActiveX
    {
        //定义字典集合装取需要到的数据
        /// <summary>
        /// 该集合装有所有系统检测到的串口,实例化并以串口名为key值存储,实例化过程在无参构造函数中
        /// </summary>
        public Dictionary<string, SerialPort> spList = new Dictionary<string, SerialPort>();
        /// <summary>
        /// 该集合装取所有读取出来的数据根据sp_DataReceived(sender,e)方法进行逻辑处理
        /// key值代表该value的值属于哪个串口,key值为该串口名
        /// </summary>
        public Dictionary<string, string> read_string = new Dictionary<string, string>();
        /// <summary>
        /// 该集合装取读取出来的数据经过逻辑处理后的最终结果,处理过程在sp_DataReceived(sender,e)方法
        /// key值为串口名
        /// </summary>
        public Dictionary<string, string> read_result = new Dictionary<string, string>();
        /// <summary>
        /// 该集合装取经过处理过后再处理出一串根据既定位置截取的数字字符串,属于计数(根据取出的数据截取想要的那部分,以闭合次数为基准的逻辑处理)
        /// key值为串口名
        /// </summary>
        public Dictionary<string, string> read_count = new Dictionary<string, string>();
        /// <summary>
        /// 初始化方法,方法中与实例化所有系统检测到的串口,并绑定方法和打开
        /// 绑定的事件为DataReceived(接受到数据时触发),绑定的方法为sp_DataReceived(sender,e)
        /// 打开串口后,以串口名为key值储存到spList集合中
        /// </summary>
        public SPActiveX()
        {
            //SerialPort中的GetPortName可以获取当前系统所能检测到的所有串口的名字,通过foreach一个一个取出来用
            foreach (string portname in SerialPort.GetPortNames())
            {
                //**创建一个临时SerialPort类,进行初始化操作并存到集合**\\
                //以串口名实例化一个临时SerialPort
                SerialPort sp_emp = new SerialPort(portname);
                //绑定事件,事件触发时执行方法,事件触发条件为接收到数据,触发时执行的方法为sp_DataReceived
                sp_emp.DataReceived += new SerialDataReceivedEventHandler(sp_DataReceived);
                //打开该串口
                sp_emp.Open();
                //经过上面的处理后,以串口名为key值添加到spList集合中
                spList.Add(sp_emp.PortName, sp_emp);
            }
        }
        /// <summary>
        /// 该方法将会绑定到DataReceived事件中,接受数据时触发,所以该方法在时对数据进行处理的。
        /// 把完整的数据根据串口名无处理存储到read_string中
        /// 把read_string根据逻辑处理成一句目前所接受的结果中可以分辨出来的有用的一串字符
        /// 根据串口名存储到read_result中
        /// 把read_result中处理过的数据处理成一个结果根据串口名存储到read_count,逻辑基准为闭合计数
        /// 注:该方法名非固定,但是参数是固定的
        /// </summary>
        /// <param name="sender">该属性未发送者(即触发该事件的本体,在该方法为,触发事件执行方法的那个串口,理解为他就是接受到数据的串口)</param>
        /// <param name="e">事件数据类,该方法中没有用到</param>
        public void sp_DataReceived(object sender, SerialDataReceivedEventArgs e)
        {
            //把发送者转为SerialPort并使用,sender为触发事件的串口
            SerialPort sp = (SerialPort)sender;
            //根据SerialPort提供的方法Read(byte[],int,int)的需求,首先根据接收缓冲区的字节数作为长度实例化一个byte数据
            byte[] data = new byte[sp.BytesToRead];
            //调用Read方法,第一个参数为byte[]类型,会把数据传到该byte[]里面,第二参数为数据偏移量,第三参数为获取的数据的长度
            //第二和第三的参数可以理解为截取
            sp.Read(data, 0, data.Length);
            //第一个逻辑处理start\\
            //接受到数据后,无处理存储到read_string中,此处注意的是,read方法返回的字符是实时返回的,
            //也就是说数据时不确定完整性的,这里我会无差别全部拼接,read_string为字典集合,首先要判断该集合是否已存
            //在该数据,如果有则覆盖,没有则新添加,字典集合提供了ContainsKey方法,该方法通过提供key值判断该key值
            //是否已经存在返回bool值,如果存在则为true,不存在反之
            if (read_string.ContainsKey(sp.PortName))
                //如果存在的话,直接拼接数据,System.Text.Encoding.ASCII.GetString(byte[])方法作用是
                //把对应的byte数据根据ascii码转为相应的字符串
                read_string[sp.PortName] += System.Text.Encoding.ASCII.GetString(data);
            else
                //如果没有则以串口名新添加
                read_string.Add(sp.PortName, System.Text.Encoding.ASCII.GetString(data));
            //第一个逻辑处理end\\
            //第二个逻辑处理start\\
            //该逻辑是根据取出来的数据编辑的,完整的返回数据为@开头,*结尾(事实上是以\r\n结尾),如果IndexOf查找
            //不到这两个字符,则可以判断为,数据不完整,数据不完整的时候则全部存入read_result,如果完整则要通过处理
            //多余的数据并把需要的数据正确截取出来
            //注:IndexOf(string)方法作用为:在指定字符串中查找提供的字符并返回其下标,如果没找到则返回-1
            if (read_string[sp.PortName].IndexOf("@") >= 0 && read_string[sp.PortName].IndexOf("*") >= 0)
            {
                //这部分处理处计数字符串,根据返回的规律截取数据的从@字符开始的后19位开始,截取8位
                string sp_stringemp = read_string[sp.PortName].Substring(read_string[sp.PortName].IndexOf("@") + 19, 8);
                //声明一个int变量存储该字符串有几个零
                int i = 0;
                //foreach循环计数该数字字符串前面的零有几个,从最大位开始,遇到非零跳出循环
                foreach (char c in sp_stringemp)
                {
                    if (c == '0')
                    {
                        i++;
                    }
                    else
                    {
                        break;
                    }
                }
                //这里的if和存储read_string时的判断一样,判断是否存在
                if (read_count.ContainsKey(sp.PortName))
                    //这里的?字符是简写的if,变量 = 条件?如果ture时赋值:如果false赋值;,这里的使用逻辑是
                    //入股前面的零个数与字符长度相等了(00000000),则赋值0(字符串),否则剪掉前面的0,赋值对应
                    //的数字字符串(比如00011200会处理为11200)
                    read_count[sp.PortName] = i == sp_stringemp.Length ? "0" : sp_stringemp.Substring(i > 0 ? i : 0, sp_stringemp.Length - i);
                else
                    //不存在则新建
                    read_count.Add(sp.PortName, i == sp_stringemp.Length ? "0" : sp_stringemp.Substring(i > 0 ? i : 0, sp_stringemp.Length - i));
                //第二个逻辑处理end\\
                //第三个逻辑处理start\\
                //判断同上
                if (read_count.ContainsKey(sp.PortName))
                    //把read_string处理,使用Substring(开始下标,截取长度)方法进行截取,开始下标为@字符所在的位置,,长度为结束字符*减去@字符的位置
                    read_result[sp.PortName] = read_string[sp.PortName].Substring(read_string[sp.PortName].IndexOf("@"), read_string[sp.PortName].IndexOf("*") - read_string[sp.PortName].IndexOf("@") + 1);
                else
                    //无则新建
                    read_result.Add(sp.PortName, read_string[sp.PortName].Substring(read_string[sp.PortName].IndexOf("@"), read_string[sp.PortName].IndexOf("*") - read_string[sp.PortName].IndexOf("@") + 1));
                //第三个逻辑处理end\\

                //经过上面的处理之后,read_string要清空,否则会影响正确结果
                //如:第一次取出数据为@123*,第二次为@abc*,如果不清空根据拼接原理结果为@123*@abc*,
                //上面处理出来的结果就一直为@123*,而不是后面最新读取出来的正确结果
                read_string[sp.PortName] = "";
            }
            else
            {
                //数据不完整,数据不完整的时候则全部存入read_result
                read_result[sp.PortName] = read_string[sp.PortName];
            }
        }

        /// <summary>
        /// 发送数据方法,需要指定发送到哪个串口
        /// </summary>
        /// <param name="portname">需要发送的串口名</param>
        /// <param name="SendData">需要发送的内容</param>
        /// <returns></returns>
        public string SendData(string portname, string SendData)
        {
            //trycatch捕捉异常
            try
            {
                //对指定指令进行约定的校验位处理
                //初始化校验数
                int checkcode = 0;
                //拆分字符串为一个一个字符
                foreach (char c in SendData)
                {
                    //把拆分的字符转为ascii码并与校验数进行异或运算,并把结果返回给校验位本身
                    //异或:
                    //异或时指把数字转为二进制,进行异或位运算后,把结果从二进制转为十进制并返回
                    //如:62和78进行异或
                    //62的二进制:0111110
                    //78的二进制:1001110
                    //----------------------
                    //结果为:    1110000
                    //相同为1,不同为零
                    //1110000的十进制为112
                    //则结果为112
                    checkcode = (int)c ^ checkcode;
                }
                //把校验数转为16进制的字符串,并全转为大写(16进制或许有字母)
                string checodestr = checkcode.ToString("x").ToUpper();
                //检验数如果只有一位数(长度为1)时,则补充一个零,如果多于一位则完全赋值
                string checkResult = checodestr.Length == 1 ? "0" + checodestr : checodestr;
                //把结果转为ascii码的byte数组,拼接规律为:SendData(需要传输的数据)+checkReSult(校验数)+“*\r\n”(固定形式)
                byte[] WriteBuffer = Encoding.ASCII.GetBytes(SendData + checkResult + "*\r\n");
                //写入方法Write(byte[]写入数据,偏移量,写入长度)
                spList[portname].Write(WriteBuffer, 0, WriteBuffer.Length);
                //返回结果
                return "写入成功!";
            }
            catch (Exception ex)
            {
                //如果捕捉到异常则返回写入失败并显示错误信息
                return "写入失败!\n" + ex.Message;
            }
        }

        /// <summary>
        /// sp_DataReceived方法已经处理过数据了,需要数据时则根据需要取得串口获取数据
        /// </summary>
        /// <param name="portname">获取的串口名</param>
        /// <returns>返回string类型结果</returns>
        public string GetResult(string portname)
        {
            return read_result[portname];
        }
        /// <summary>
        /// sp_DataReceived方法已经处理过数据了,需要数据时则根据需要取得串口获取数据
        /// </summary>
        /// <param name="portname">获取的串口名</param>
        /// <returns>返回string类型结果</returns>
        public string GetCount(string portname)
        {
            return read_count[portname];
        }
    }
}
xiaoliang_hai 2017-02-06
  • 打赏
  • 举报
回复
写一个操作串口类,然后该类对串口数据进行处理,比如编写一个write方法和read方法. 然后呢,不管你什么窗体,只要进行调用read方法获取write方法去写入读取你需要的数据就好了
penglan_ 2017-02-06
  • 打赏
  • 举报
回复
上面说的好高大尚,看不懂 楼主是不是想问,子窗口打开的时候,父窗口更新了变量,子窗口也要同时更新?
  • 打赏
  • 举报
回复
在看各种博客资料时你要注意,这里的重点是“行为”而不是什么存储。如果只是说存储就外道了! 比如说你有窗体 F1、F2、F3 甚至更多的更多的对象要在串口收到一条完整消息之后做出响应(!),那么你的重点就是要知道如何在收到消息之后能让 F1....... 这些对象内部注册的代码被执行。 有些博客说“你有一个存储,S1把消息数据写到那里,然后 F1......就能去取这个消息数据了”,这类说法其实对管理这个流程就毫无意义,你应该忽略那些多余的描述。你要知道如何通知 F1...... 这些对象,那么通知的事件带数据参数、或者不带参数但是通知了之后你的 F1..... 等对象才会去取数据。了解了如何设计事件通知、依赖倒置机制,你才能进一步去设计和实现参数数据的不同形式。你把那些博客文章中仅仅谈存储的部分删掉,看看剩下的是什么呢?
  • 打赏
  • 举报
回复
即使你是用一个 SerialPort 对象编程,你学习到高效率、“不卡程序”的编程模式,我相信也是事件驱动形式的。所以事件驱动是核心的概念,不理解事件那么你肯定是最多只学了一半软件设计技术。 然后,.net 框架串口对象的 DataReceived 事件只是通知你说 .net 的串口对象收到了 bytes,它是非常低级的东西。你需要封装自己的业务层的串口管理对象(类),来处理 DataReceived 事件,判断是否受到了一条完整的消息。如果没收完整了,那就再以后处理 DataReceived 之后才可能判断完整消息;当收到了完整的消息,应该抛出自定义的事件通知。它不依赖任何客户程序,不纠结什么“子窗口”之类的。 现在,假设应用程序中有这样一个 static 的对象就够了,那么这样设计就是最合理的。你用不着把它做为什么参数传来传去的,在程序一个某个 class 中定义全局变量来引用它、其它所有代码都能通过此 class 直接访问这个对象,就够了。 对于各种客户程序来说,虽然可以直接访问到全局的这个对象(根本不需要实例化时传入什么参数),但是如果说“从串口取数据”等于又回到了从来不知道事件驱动概念的时代。串口对象的编程模式,明明是在收到消息时来通知宿主的,而根本不是宿主程序去“取数据”的编程模式。
  • 打赏
  • 举报
回复
引用 楼主 SjzEboy 的回复:
问题 :现在因为是跨窗体了,不明白怎么实现这两个子窗体都可以从串口取数据的过程。
根本不是子窗体取串口数据,而是串口管理对象抛出事件通知子窗体。
exception92 2017-02-06
  • 打赏
  • 举报
回复
http://bbs.csdn.net/topics/390128592 每接受一次数据 ,就调用窗体传值方法。
  • 打赏
  • 举报
回复
你自己设计用来操作串口的封装业务对象(假设叫做S1)是一个服务,而窗口(假设叫做F1、F2、F3)都是客户。服务不应该依赖于客户,客户才是依赖于服务的。 整个系统中只有这一个 S1 实例,因此你应该把S1声明为 static 的、全局的(注意不用纠结什么“单例”概念,声明为 static 即可)。 S1 在上,它在收到一条完整的消息之后,抛出事件通知。事件具有参数,表示收到的消息内容。 客户(F1、F2、F3)窗口创建之后,既可以向这个全局的 S1 注册事件处理委托来捕获串口消息事件。
加载更多回复(9)

110,571

社区成员

发帖
与我相关
我的任务
社区描述
.NET技术 C#
社区管理员
  • C#
  • Web++
  • by_封爱
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告

让您成为最强悍的C#开发者

试试用AI创作助手写篇文章吧