C# Winform学习总结

C# 专栏收录该内容
5 篇文章 0 订阅

C# Winform学习总结

一、串口

1.1 串口停止位设置为1.5位时参数出错

  根据该博文,当数据位为6/7/8位时,停止位只能是1/2位;当数据位为5位时,停止位只能是1/1.5位。在设计串口调试助手时需要进行一下判断和提示。

/* parameters check for data bits and stop bits */
if (cbDataBits.SelectedItem.ToString() == "5" && cbStopBits.SelectedItem.ToString() == "2")
{
    MessageBox.Show("2 stop bits are not allowed with 5 data bits!", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
    return;
}
if ((cbDataBits.SelectedItem.ToString() == "6" || cbDataBits.SelectedItem.ToString() == "7" || cbDataBits.SelectedItem.ToString() == "8") && cbStopBits.SelectedItem.ToString() == "1.5")
{
    MessageBox.Show("1.5 stop bits are not allowed with 6/7/8 data bits!", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
    return;
}

1.2 串口的定时扫描和更新

  一般情况下会先将串口线连好,然后打开上位机,此时可以出现相应的串口。但是如果先打开上位机,再连接好串口线,串口的扫描只在打开上位机后执行一次,那么将不显示相应的调试串口,于是在未成功打开串口前需要进行串口的定时扫描和更新。具体的实现就是使用定时器,可设置扫描间隔为1s。

string[] curPortNames;
string[] prePortNames = { "null" }; // must initialize it, or an error occurs when SequenceEqual() is invoked

private void timSearchPort_Tick(object sender, EventArgs e)
{
        if (!serialPort.IsOpen)
        {
                curPortNames = SerialPort.GetPortNames();
                if (curPortNames != null) // when COM exists
                {
                    btnOpen.Enabled = true; // enable the open button
                    bool isPortEqual = Enumerable.SequenceEqual(curPortNames, prePortNames);

                    if (!isPortEqual)
                    {
                        cbPortName.Items.Clear();
                        cbPortName.Items.AddRange(curPortNames);
                        cbPortName.SelectedIndex = 0; // select the first COM as default COM
                        prePortNames = curPortNames;
                    }
                }
                else // when no COM exists, disable the open button
                    btnOpen.Enabled = false;                                    
            }
        }
}

1.3 串口被占用

  使用try…catch…进行判断

SerialPort serialPort = new SerialPort();
// ...
// initialization of serialPort including baudrate, data bits etc.
// ...
try
{
	serialPort.Open();
}
catch
{
    MessageBox.Show("Cannot open COM! Check if COM has been used!", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
    return;
}

1.4 一个字符串引起的串口的触发次数无法确定

  串口的触发可由ReceivedBytesThreshold属性进行控制,默认情况下它的值是1,也就是如果接收缓冲区中有一个字节就触发事件。要注意的是,实际使用时下位机发送一定长度字符串时,可能会触发多次事件,并不是只触发一次

1.5 通讯过程中拔掉串口线/串口线脱落

  实现思路:利用消息机制检测USB插拔,然后判断已打开的串口是否已经不存在,如果不存在,则关闭串口。
  根据视频可知USB插拔的消息ID号是0x219,在窗体中重载函数WndProc()进行判断即可。代码如下,其中serialPort是添加的串口控件名。
  我有一个疑惑,我并没有在MSDN的文档里找到相应的消息ID号,而该博客提到的消息ID号的网址也不知道是从什么地方收集这些ID号的。

const int WM_DEVICECHANGE = 0x219;	// Message ID for change of device

protected override void WndProc(ref Message m)
{
    if(m.Msg == WM_DEVICECHANGE)
    {
        string[] curPortNames = SerialPort.GetPortNames();
        bool isOffline = false;
        foreach(var curPortName in curPortNames )
        {
            if (curPortName == serialPort.PortName.ToString())
                isOffline = true;
        }
        if (isOffline)
        {
            serialPort.Close();
            // enable comboboxes...
        }
    }
    base.WndProc(ref m);
}

1.6 打开不是调试串口的串口发送数据导致上位机崩溃

在这里插入图片描述

  图1为我电脑上的串口,当打开COM4或者COM5,然后从该串口发送数据给下位机时,上位机会崩溃。目前未找到满意的解决方法。

1.7 暂停接收时应该清除缓冲区

  暂停接收时仅仅将方法解绑是不够的,串口仍然将接收的数据存入缓冲区,这样的话重新绑定方法后,会把这些数据一并读出,但这是我们不想要的。所以正确的方法是重新绑定前先清空缓冲区,或者丢弃第一次读取到的数据(可能包含有效数据)。

二、控件及其属性的使用

2.1 将鼠标放在按钮上,出现提示文字

  添加控件ToolTip,然后打开按钮的属性,可以在最下面找到新添了一个项目叫Misc,里面有一个属性叫做ToolTip on <ToolTip控件的名字>,添加要提示的文字就好了。

2.2 富文本框

  可以使用富文本框进行内容的不同颜色显示,比如对上位机中发送内容和接收内容进行不同颜色的区分。

2.3 dock的优先级

  多个panel都具有相同的dock属性时,需要有优先级,比如都是dock=Left,那么最左是哪个,次之又是哪个需要根据需求进行设计。通过在Designer.cs中修改Add的顺序进行优先级调整,越后面添加优先级越高。

this.grpIO1.Controls.Add(this.pnlIO1Indication);
this.grpIO1.Controls.Add(this.pnlIO1Output);
this.grpIO1.Controls.Add(this.pnlIO1Cust); // 如果都是靠左停靠,那么pnlIO1Cust会在最左边         

2.4 AutoSize属性的使用

  项目中需要根据ComboBox的选项来显示和隐藏一些TextBox和ComboBox,这些内容都在一个GroupBox里。如果希望GroupBox的大小也随之变化,那么可以使用AutoSize属性。

2.5 CheckBox的Button外观

  项目中可能需要设计按钮按下后保持一个按住的状态,再点击一次后恢复原样。这个实现方法有很多,最简单的就是在按钮事件中修改按钮的颜色或者背景图片等。这里说的是CheckBox的方法,把CheckBox的Appearance从Normal改为Button就可以实现按钮外观,按下后按钮会保持按住的状态,再按一次会恢复原状,可以通过FlatStyle、BorderSize等进行进一步的美化。

2.6 FormClosing()和FormClosed()

  顾名思义,FormClosing()就是窗体在关闭时被调用的,可以用来做是否关闭窗口的二次确认(如下代码);而FormClosed()是在窗体关闭后被调用的。至于二者的其他用处,可参考该博文

private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
        DialogResult dialogResult = MessageBox.Show("Are you sure to quit?", "DTU Tool v1.0", MessageBoxButtons.YesNo, MessageBoxIcon.Information);
        if (dialogResult == DialogResult.Yes)
                e.Cancel = false;
        else
                e.Cancel = true;
}

2.7 构造函数还是Load函数?

  每次创建工程时控件的内存空间分配会在构造函数中实现,而诸如给Combox添加Items之类的操作会放在窗体的Load函数里。但这不是一定的。Load函数只有在窗体显示时才会被执行,如果在窗体显示前就需要对控件进行操作,比如根据读取到的数据修改ComboBox的SelectedIndex,那么需要把添加Items的步骤放在构造函数里,否则会报3.5中提到的错误。

2.8 Load()只在第一次Show()的时候调用

  new完一个窗体后,如果之后根据需要对它进行Show()和Hide()操作,那么Load()只在第一次Show()的时候调用。

2.9 定时器只能在UI线程中启用和停用

  我在串口的数据接收方法ReceiveData()中启用了定时器timer,在timer的定时方法中打了断点,然而程序运行后却没有进入断点处,查阅资料,原因是定时器只能在UI线程中启用和停用,串口的ReceiveData()方法是在子线程中运行的,所以定时器没有正常启用。解决方法是使用委托:

this.Invoke((MethodInvoker)delegate { timer.Start(); });

2.10 Visible属性和Show()、Hide()方法

  网上有说法称Hide()会释放内存,而Visible=false不会。但我疑惑我用Hide()的话内存都被释放了,那么我对子窗体用Hide(),那么我子窗体中控件的值不就丢了吗??但实际上并没有丢。也有说法称这两者没区别,Hide()的实现就是使用Visible=false。因为这个辨析对我的项目没有影响,所以不再深究。

2.11 Combox用Text判断还是SelectedItem判断?

  用SelectedItem判断的前提是SelectedIndex不等于-1,如果SelectedIndex=-1,用SelectedItem判断就会报错。而Text不会,在SelectedIndex=-1的情况下,Text=“”;在SelectedIndex不等于-1的情况下,Text=SelectedItem.ToString()。
  此外,Text是Control类的属性,而SelectedIndex仅仅是ComboBox类的属性,所以相比之下Text的通用性更高。

2.12 定时器Stop()或者Enable=false后会重装载计时值

  比如一个interval为1000的定时器,在未计满1000个tick时Stop()或者Enable=false,那么重新Start()或者Enable=true后仍然会从1000个tick开始倒计数。

2.13 同一个方法绑定到多个按钮的事件

  有一种情况,多个控件的方法的实现是相同的,为了代码重用,可以只写一个方法,然后将这个方法绑定到这些控件的事件。具体的做法就是在控件的属性页面中的上面,点击闪电符号(Events),然后在Action类中选择相应的事件名字,点击下拉框,选择想要绑定的方法。
  有时候,在这个方法中需要知道是哪个控件的事件被触发导致这个方法被调用了,可以使用参数sender,比如说这个方法绑定若干个按钮的Click事件,那么按照下面的代码写就可获得调用这个方法的控件变量。

Button btn = (Button)sender;

  目前用过的都是一个方法绑定到若干个按钮、或者若干个文本框,没有尝试过一个方法同时绑定到一个/些按钮和一个/些文本框。

2.14 打开子窗口后,主窗口不能操作

  将Form.Show()用Form.ShowDialog()替代。这个功能可以起到避免打开多个子窗口。

private void btnShowChildForm_Click(object sender, EventArgs e)
{
        SerialPortSetting fSerialPortSetting = new SerialPortSetting();
        fSerialPortSetting.Owner = this;
        //fSerialPortSetting.Show();
        fSerialPortSetting.ShowDialog(); // only the childform can be modified
}

  查阅资料,Show()和ShowDialog()有以下三个区别:
  1、Show()显示窗体后,其他窗体仍然可以操作;ShowDialog()显示窗体后,只能操作该窗体,其他窗体不能操作。
  2、Show()显示窗体后,之后的代码可以继续运行;ShowDialog()显示窗体后,只有当该窗体关闭时,后面的代码才可以执行。
  3、Show()显示窗体后,如果点击关闭按钮,那么Close()方法被调用,窗体资源将被释放,即内存被回收;ShowDialog()显示窗体后,如果点击关闭按钮,窗体资源不会被释放,而是将Visible属性设置未false,并返回一个DialogResult为Cancel的值,如果要释放窗体,需要调用Dispose()方法。
  测试代码如下。button1用于测试Show(),button2用于测试ShowDialog()。点击button1,显示子窗体,再点击button1,隐藏子窗体。再点击button1,子窗体重新显示。此时关闭子窗体,然后点击button1,没有变化,再点击一次,报错“未将对象引用设置到对象的实例”,原因就是窗体资源被释放了,无法再次Show()。对button2进行相同的操作,均没有报错,关闭子窗体后可以在控制台看到打印出来的信息。

namespace Demo
{
    public partial class Form1 : Form
    {
        private Form activeForm;
        private Form2 frm2;
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            frm2 = new Form2();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            if(activeForm == null)
            {
                activeForm = frm2;
                activeForm.Show();
            }
            else
            {
                activeForm.Hide();
                activeForm = null;
            }
        }

        private void button2_Click(object sender, EventArgs e)
        {
            if (activeForm == null)
            {
                activeForm = frm2;
                DialogResult result = activeForm.ShowDialog();
                if (result == DialogResult.Cancel)
                    Console.WriteLine("cancel");
            }
            else
            {
                activeForm.Hide();
                activeForm = null;
            }
        }
    }
}

2.15 Tag属性

  根据该博文,Tag属性是Object类型,也就是说可以将任何类型的对象赋值给Tag,这个特点方便我们对控件进行自定义的标记。比如在我的应用中,我需要为每一个控件添加一个序号,每个控件的序号是唯一的,那么我可以在属性页直接将Tag属性赋值为一个整型。
  注意,控件一开始的Tag属性为null,如果在属性页进行了修改,然后又把修改删了,这时Tag属性变为“”,不再是null!可以右键Tag属性,点击Reset,让Tag属性重新变为null。

2.16 窗体继承

  如果若干个类都有相同的方法(名字相同,参数相同,实现也相同),为了提高代码复用率,
  方案一:声明一个抽象类,把这个方法的实现写在抽象类中,然后让这些类继承自该抽象类。
  但是,如果这些类已经是继承自某个/些父类的,那么就行不通了。因为C#不支持多重继承。在这种情况下接口也无法解决问题,因为接口中不可以写方法的具体实现,也就是说接口更适用于若干个类都有一个相同名字的方法,但具体实现因类而异的情况。
  方案二:方案一行不通,可以使用多级继承。比如原本Child1类和Child2类有一个相同的方法Func(),它们都继承自Base1类,那么可以声明一个Base2类,让Base2类继承自Base1类,然后让Child1类和Child2类都继承自Base2类,在Base2类中实现Func()即可。一个例子就是窗体继承。
  下面是我的项目中使用的窗体继承,BaseTabForm类继承自Form类,FGlobalParam类继承自BaseTabForm类。BaseTabForm类中实现了子类共有的方法MapControl()和FindChangedCtrl()。

public partial class BaseTabForm : Form
{
    protected MainForm parentForm;
    public BaseTabForm()
    {
        InitializeComponent();
    }
    public BaseTabForm(Form parentForm)
    {
        InitializeComponent();
        this.parentForm = new MainForm();
        this.parentForm = (MainForm)parentForm;
    }
    protected virtual void ComboBoxInit() {; }
    protected void MapControl()
		...
    }
    public void FindChangedCtrl()
    {
		...
    }
}
public partial class FGlobalParam : BaseTabForm
{
    public FGlobalParam(MainForm parentForm) : base(parentForm)
    {
        InitializeComponent();
        ComboBoxInit();
        MapControl();
    }

    protected override void ComboBoxInit()
    {
        cbAction.Items.Add("Re-dial");
        cbAction.Items.Add("Reconnect");
        cbAction.Items.Add("Reboot");
    }
}

  方案三:使用拓展方法。拓展方法能够向现有类型添加方法,而无需创建新的类型。
  关于窗体继承的其他博客:
  1、C# WinForm窗体继承时,需要注意的问题l
  2、Winform窗体继承

2.17 基类窗体的InitializeComponent()只初始化基类窗体的控件

  基类窗体中的InitializeCompnent()只初始化基类窗体的控件。如果子类窗体另外添加了自己的控件,那么子类窗体的构造函数中必须再次调用InitializeComponent()。为了统一,子类窗体的构造函数一律调用InitializeComponent()。

2.18 子类控件无法绑定到基类方法

  多个窗体中的控件的方法一摸一样,于是我让这些窗体继承自基类窗体,然后在基类窗体中实现该方法,让子类窗体的控件绑定到这个方法,然后报错,大意就是子类控件无法绑定到基类实现的方法(所谓“实现”,就是写了函数体)。

在这里插入图片描述
  所以这种情况下窗体继承并不能解决问题,我想到的一个解决方法是:仍然在基类窗体中实现这个方法,比如叫BaseFunc(),然后在各个子类中各自写一个方法叫ChildFunc(),这个方法就一句话,调用BaseFunc()。这样做勉强也实现了代码重用,毕竟BaseFunc()的函数体可能很长。

2.19 子类调用基类构造函数

  关于继承和构造函数需要知道:
  1、构造函数是不能被继承的。
  2、子类的构造函数在调用前会先调用父类的无参构造函数,如果父类没有无参构造函数,就会报错。
  3、对于第二点,参考该博文,如果父类没有无参构造函数,可以通过在子类构造函数后添加:base()指明调用父类的哪个构造函数。但是,设计窗口会报错,但可以正常运行。所以,还是养成习惯,父类写一个无参构造函数。代码在这。下面是我在项目中的调用例子。

public partial class BaseTabForm : Form
{
    public BaseTabForm()
    {
        InitializeComponent();
    }

    public BaseTabForm(Form parentForm)
    {
        InitializeComponent();
        this.parentForm = new MainForm();
        this.parentForm = (MainForm)parentForm;
    }
    ...
}
public partial class FGlobalParam : BaseTabForm
{
    public FGlobalParam(MainForm parentForm) : base(parentForm)
    {
        InitializeComponent();
        ComboBoxInit();
        MapControl();
    }

    protected override void ComboBoxInit()
    {
        ...
    }
}

在这里插入图片描述

2.20 自定义控件

  在解决窗体继承问题时了解到自定义控件的存在,我认为这是对窗体继承的一个拓展,因为窗体也是控件的一种。

2.21 在子窗体没有show()之前,在父窗体中修改子窗体中的控件,仍然会触发控件的事件

  代码如下:Form1有button1和button2,Form2有文本框。Form1中button1用于控制Form2的显示和隐藏,button2用于给Form2中的文本框赋值。具体操作就是运行,点击Form1的button2,可以看到命令窗口输出“Text changed!”,然后点击button1,显示Form2。

namespace demo
{
    public partial class Form1 : Form
    {
        private Form2 frm2;
        private Form activeForm = null;
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            frm2 = new Form2();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            if(activeForm == null)
            {
                activeForm = frm2;
                activeForm.Show();
            }
            else
            {
                activeForm.Hide();
                activeForm = null;
            }
        }

        private void button2_Click(object sender, EventArgs e)
        {
            frm2.textBox1.Text = "hello";
        }
    }
}

namespace demo
{
    public partial class Form2 : Form
    {
        public Form2()
        {
            InitializeComponent();
        }

        private void textBox1_TextChanged(object sender, EventArgs e)
        {
            Console.WriteLine("Text changed!");
        }
    }
}

2.22 自定义标题栏

  默认的标题栏是白色的,跟我设计的颜色主题不相符,于是尝试修改标题栏。按照该博文,遇到了同样的问题:默认的标题栏并无法完全去除,总是会在最上方出现一个窄细的白色区域,就像一个白色的向上停靠的panel一样。最后我选择放弃,因为不好看。

2.23 自制Tab

  思路就是Button+Panel+多个Form。每个Button就是一个Tab,一个Tab对应一个Form,Form在Panel上进行显示。效果图如下:

在这里插入图片描述
  代码如下。主要包括两个功能,一个是根据选中的Tab显示相应的Form,另一个是颜色控制。对于前者,实现思路就是让Tab的名字和Form的名字有一个相同的部分,我是让所有的Tab都是btn+XXX,所有的Form都是f+XXX,比如btnDataCenter和fDataCenter,这样的话就可以根据点击的Tab名字对应到要打开的Form名字,对每个Tab都是如此。知道Form名字后就可以通过分支结构打开相应的Form。对于后者,思路就是记录当前按下的Tab和之前按下的Tab,然后进行相应的颜色更改。更细节的东西还有:
  1、第一次按下时只有activeTab没有inactiveTab,所以需要再重载一个颜色设置函数TabColorCtrl(ref Button activeTab)。
  2、避免重复打开同一个Tab,思路就是将要打开的Form的名字和已打开的Form的名字进行比较,一样的话就不操作;
  3、对于2,同样需要进行跟1一样的null判断。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Drawing;

namespace DTU_Tool_v2_1.Classes
{
    public class TabController
    {
        private Form activeForm;    // form opened according to active tab
        private Button activeTab;   // chosen tab
        public FWorkMode fWorkMode { get; set; }    // all 7 tab forms
        public FDataCenter fDataCenter { get; set; }
        public FSerialPort fSerialPort { get; set; }
        public FIO fIO { get; set; }
        public FDial fDial { get; set; }
        public FGlobalParam fGlobalParam { get; set; }
        public FDeviceMgt fDeviceMgt { get; set; }

        /// <summary>
        /// Class construction function
        /// </summary>
        /// <param name="parentForm"></param>
        public TabController(MainForm parentForm)
        {
            this.activeForm = null;
            this.activeTab = null;
            fWorkMode = new FWorkMode(parentForm);
            fDataCenter = new FDataCenter(parentForm);
            fSerialPort = new FSerialPort(parentForm);
            fIO = new FIO(parentForm);
            fDial = new FDial(parentForm);
            fGlobalParam = new FGlobalParam(parentForm);
            fDeviceMgt = new FDeviceMgt(parentForm);
        }

        public void Init()
        {
            this.activeForm = null;
            this.activeTab = null;
        }

        /// <summary>
        /// Set the color of active tab when first logged in.
        /// </summary>
        /// <param name="activeTab"> the active tab (the pressed button) </param>
        private void TabColorCtrl(ref Button activeTab)
        {
            activeTab.BackColor = Color.FromArgb(0, 122, 204);
            activeTab.ForeColor = Color.FromArgb(255, 255, 255);
        }

        /// <summary>
        /// Set the color of active Tab and inactive tab when switching from tab to tab.
        /// </summary>
        /// <param name="activeTab"> tab switched to (the pressed button) </param>
        /// <param name="inactiveTab"> tab switched from </param>
        private void TabColorCtrl(ref Button activeTab, ref Button inactiveTab)
        {
            activeTab.BackColor = Color.FromArgb(0, 122, 204);
            activeTab.ForeColor = Color.FromArgb(255, 255, 255);
            inactiveTab.BackColor = Color.FromArgb(238, 238, 242);
            inactiveTab.ForeColor = Color.FromArgb(0, 0, 0);
        }

        /// <summary>
        /// Show tab form according to the chosen tab (pressed button). When first logged in, 'activeForm'
        /// is null, and a default tab is shown (in this case the tab is 'Work Mode').
        /// </summary>
        /// <param name="sender"> variable including the information of pressed button </param>
        /// <param name="e"> not used </param>
        /// <param name="ctrl"> control that the tab form belongs to </param>
        public void ShowTabForm(object sender, EventArgs e, Control ctrl)
        {
            Button btn = (Button)sender;
            string tabName = btn.Name.ToString().Substring(3, btn.Name.ToString().Length - 3);
            if (this.activeForm == null)
            {
                TabColorCtrl(ref btn);
                activeTab = btn;
                OpenTabForm(tabName, ctrl);
            }
            else
            {
                string formName = this.activeForm.Name.ToString().Substring(1, this.activeForm.Name.ToString().Length - 1);
                if (formName != tabName) // avoid opening a dulplicated form
                {
                    TabColorCtrl(ref btn, ref activeTab);
                    activeTab = btn;
                    OpenTabForm(tabName, ctrl);
                }
            }
        }

        /// <summary>
        /// Show a form according to its name.
        /// </summary>
        /// <param name="formName"> the name of the form to be opened </param>
        /// <param name="ctrl">  control that the tab form belongs to </param>
        private void OpenTabForm(string formName, Control ctrl)
        {
            if (this.activeForm != null)
                this.activeForm.Hide();
            switch (formName)
            {
                case "WorkMode":
                    this.activeForm = fWorkMode;
                    break;
                case "DataCenter":
                    this.activeForm = fDataCenter;
                    break;
                case "SerialPort":
                    this.activeForm = fSerialPort;
                    break;
                case "IO":
                    this.activeForm = fIO;
                    break;
                case "Dial":
                    this.activeForm = fDial;
                    break;
                case "GlobalParam":
                    this.activeForm = fGlobalParam;
                    break;
                case "DeviceMgt":
                    this.activeForm = fDeviceMgt;
                    break;
                default:
                    this.activeForm = fWorkMode;
                    break;
            }
            this.activeForm.TopLevel = false;
            this.activeForm.Dock = DockStyle.Fill;
            ctrl.Controls.Add(this.activeForm);
            this.activeForm.BringToFront();
            this.activeForm.Show();
        }
        
        /// <summary>
        /// Hide the active form.
        /// </summary>
        public void HideActiveTab()
        {
            if (this.activeForm != null)
                this.activeForm.Hide();
        }
    }
}

2.24 Margin和Padding

  这两个概念在网页设计中也会碰到。MSDN上的示意图如下。Margin是边距,Padding是空白
在这里插入图片描述

  一开始我以为加入我设置了一个控件的Margin为10px,那么当我拖动这个控件靠近其它控件时,它只能达到其他控件外围10px的地方,我错了。Margin是用来辅助你对齐的。如下图,三个Button的Margin依次设置为10、20和30,当拖动它们靠近其他控件(这里是窗体边缘)时,可以看到一条线,这条线叫对齐线,英文名叫snapline。也就是说,设置一个Margin值,就是设置出现对齐线的距离。也仅此而已。

  Padding同理。上面白色Panel的Padding值为10,下面的为50,所以把按钮向上移时,button2先显示了对齐线。我把button3的Marigin值改为10,可以看到button3比button1先显示了对齐线(所以button3靠下些)。

  综上:
  1、一个非容器控件(比如按钮、文本框)靠近另一个非容器控件时,出现对齐线时的距离就是Margin值。
  2、容器控件(比如Panel)中的非容器/容器控件(比如按钮、文本框)靠近容器边缘时,出现对齐线时的距离是容器的Padding值和容器内控件的Margin值。

三、其他

3.1 获取精确到毫秒的时间戳

  前面获取精确到秒的时间戳,后面获取精确到毫秒的时间戳(范围1-999)。

string time  = DateTime.Now.ToString() + "  " + DateTime.Now.Millisecond.ToString();

3.2 使用正则表达式判断字符串是否由英文/数字组成

string str1 = "123";
string str2 = "abc";
string str3 = "123abc";
bool isMatch = false;

// (大小写)字母+数字
System.Text.RegularExpressions.Regex reg1 = new System.Text.RegularExpressions.Regex(@"^[A-Za-z0-9]+$");
isMatch = reg1.IsMatch(str1); // true;
isMatch = reg1.IsMatch(str2); // true;
isMatch = reg1.IsMatch(str3); // true;
// (大小写)字母
System.Text.RegularExpressions.Regex reg2 = new System.Text.RegularExpressions.Regex(@"^[A-Za-z]+$");
isMatch = reg2.IsMatch(str1); // false;
isMatch = reg2.IsMatch(str2); // true;
isMatch = reg2.IsMatch(str3); // false;
// 数字
System.Text.RegularExpressions.Regex reg3 = new System.Text.RegularExpressions.Regex(@"^[0-9]+$");
isMatch = reg3.IsMatch(str1); // true;
isMatch = reg3.IsMatch(str2); // false;
isMatch = reg3.IsMatch(str3); // false;

3.3 将ASCII字符串转换为HEX字符串

  思路就是对每个ASCII字符进行转换,下面是从博客找到的现成函数,使用时只需要传第一个参数就可以了,第二个参数有自己的默认值。

public string ConvertStringToHex(string strASCII, string separator = null)
{
    StringBuilder sbHex = new StringBuilder();
    foreach (char chr in strASCII)
    {
        sbHex.Append(String.Format("{0:X2}", Convert.ToInt32(chr)));
        sbHex.Append(separator ?? string.Empty);
    }
    return sbHex.ToString();
}

3.4 控件的简单分类

  这次项目中只有两种控件作为输入,分别是下拉框和文本框。对此可做简单的分类,一是独立的控件。这些控件的变化不会导致ui的变化,各自互不相关。二是有影响力的空间,这些控件的变化会导致ui随之变化,比如某个/组控件的显示和隐藏。

3.5 报错:ComboBox遇到 INVALIDARGUMENT=“0”的值对于“INDEX”无效 错误

  出现这个问题的原因是ComboBox没有添加Items,可在调试模式下观察ComboBox变量的Items变量中的Count变量值,这个值为0,表示没有任何Item。

3.6 报错:未将对象引用设置到对象的实例

  出现这个错误的常见原因是对象声明之后没有new给它内存空间。调试方法是在给这个对象new的地方打个断点,进入调试模式,观察是否执行到断点处。
  此外当ComboBox的SelectedIndex为-1时,如果对SelectedItem进行判断,也会报这个错误。比如

private void cb1_SelectedIndexChanged(object sender, EventArgs e)
{
	switch(cb1.SelectedItem.ToString())
	{
		...
	}
}

3.7 不要用while(!flag)

  有一个定时器,定时器的方法中将flag置1。flag初始化为0,在一个按钮的方法中启动定时器,接着使用while(!flag)等待flag被定时器置1。但是这样做会死循环,因为只有响应完按钮的事件后才能响应定时器的事件,从中断机制上看,这种情况属于无法嵌套中断。

namespace Demo
{
    public partial class Form1 : Form
    {
        private bool flag;
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            flag = false;   // flag is initialized false
        }

        private void button1_Click(object sender, EventArgs e)
        {
            timer1.Enabled = true;
            timer1.Start(); // start timer
            while (!flag) ; // wait for the timer to set the flag to true
            Console.WriteLine("flag set");  // turn out to be unreachable
        }

        private void timer1_Tick(object sender, EventArgs e)
        {
            flag = true;
        }
    }
}

3.8 for和foreach

  for用的是数组的某个元素的下标,foreach用的是数组的某个元素。foreach可由for替代,但for不可以被foreach完全替代。如下面的代码报错:Cannot assign to ‘i’ because it is a ‘foreach iteration variable’。也就是说使用foreach时无法对迭代的变量进行赋值。

private bool[] flag;
foreach(var i in flag)
	i = true;

  一个正确的foreach使用例子如下:

public void ClearFlag(int[] idxs)
{
    foreach (var i in idxs)
        isChanged[i] = false;
}

3.9 无法将顶级控件添加到控件

  在自制Tab时,我需要在主窗体的一个叫pnlConfig的Panel中显示一个叫activeForm的子窗体,按下面的代码写然后报错“无法将顶级控件添加到控件”。

activeForm = new ChildForm();
pnlConfig.Controls.Add(this.activeForm);

  看到这篇博文,出错原因就是ChildForm默认是顶级控件,只需要将它设置为非顶级控件就好了,如下:

this.activeForm.TopLevel = false; // add this statement
activeForm = new ChildForm();
pnlConfig.Controls.Add(this.activeForm);

3.10 多个panel的显示问题

  情景是这样的:有一个ComboBox和3个Panel,分别为pnlA、pnlB和pnlC。三个panel都是dock=top,而且优先级A>B>C。Combox有三个选项,分别为1、2、3。要求当ComboBox选择1时,只有pnlA显示;选择2时,pnlA、pnlB显示,而且pnlA在上面,pnlB在下方;选择3时,pnlA在上面,pnlB在中间,pnlC在下面。
  这是正确实现的代码

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace Demo1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            comboBox1.Items.Add("1");
            comboBox1.Items.Add("2");
            comboBox1.Items.Add("3");

            pnlA.Hide();
            pnlB.Hide();
            pnlC.Hide();
        }

        private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
        {
            switch(comboBox1.SelectedItem.ToString())
            {
                case "1":
                    pnlA.Show();
                    pnlB.Hide();
                    pnlC.Hide();
                    break;
                case "2":
                    pnlA.Show();
                    pnlB.Show();
                    pnlC.Hide();
                    break;
                case "3":
                    pnlA.Show();
                    pnlB.Show();
                    pnlC.Show();
                    break;
            }
        }
    }
}

  因为设计需要,在执行窗体的Load()函数之前必须已经为ComboBox添加Items,于是把添加ComboBox的Items的操作放在了构造函数中,并索性把panel的显示和隐藏放了进去,然后删除了Load()函数。见如下代码2
  然后问题来了,当ComboBox选择1和2时,显示正常。但是选择3时,发现pnlC跑到了pnlB的上面。也就说现在的显示效果是A-C-B,而不是A-B-C!!!而且这个时候去修改pnlB.Show()和pnlC.Show()的先后顺序,对显示效果毫无影响,仍然是A-C-B!!!如果把panel数目增加到4个,那么ComboBox选择4时,显示效果就是A-C-B-D。可以看到只是将添加Combox的Items的操作和panel的显示和隐藏从Load()函数转移到了构造函数,显示的效果就完全错误且没有规律了!!!

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace Demo2
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();

            comboBox1.Items.Add("1");
            comboBox1.Items.Add("2");
            comboBox1.Items.Add("3");

            pnlA.Hide();
            pnlB.Hide();
            pnlC.Hide();
        }

        private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
        {
            switch (comboBox1.SelectedItem.ToString())
            {
                case "1":
                    pnlA.Show();
                    pnlB.Hide();
                    pnlC.Hide();
                    break;
                case "2":
                    pnlA.Show();
                    pnlB.Show();
                    pnlC.Hide();
                    break;
                case "3":
                    pnlA.Show();
                    pnlB.Show();
                    pnlC.Show();
                    break;
            }
        }
    }
}

  经过调试发现,如果把panel的显示和隐藏仍放在Load()函数,把添加ComboBox的Items的操作放在构造函数中,显示效果就又恢复正常了。但是具体的原因我暂时没有弄明白,也请明白其中道理的读者能分享一下。
  后期添加了pnlD和pnlE,发现问题其实还是存在。也就是说winform并无法简单地通过设置Dock优先级就可以实现这个场景,同其他人交流后,一个可行的解决方法就是动态坐标控制,即通过设置坐标来安排panel的布局。
  我的项目里要么pnlA在上,pnlB在下,要么pnlA在上,pnlC在下,所以我采取了一个比较蠢的方法:在设计窗口中设置pnlB和pnlC的坐标使之完全重合。不过这样会带来显示上的不美观。我的panel都是放在一个GroupBox里的,GroupBox勾选了AutoSize属性,如果是设置Dock,那么可以看到最底下的Panel跟GroupBox的下边沿距离比较小(见ICMP Check区域);如果是直接设置坐标使之重合,那么最底下的Panel跟GroupBox的下边沿距离比较大(见Data Service Center Setting部分)。很明显Dock配合AutoSize看起来会比较美观。

在这里插入图片描述

3.11 将已有窗体添加到新建的工程中

  先将图标文件夹复制到新工程文件夹中。接着在VS属性管理器中将主窗体改名为已有主窗体的名字,然后将已有主窗体的三个文件(.cs,.Designer.cs,.resx)添加到新建工程所在文件夹中进行覆盖。右键工程添加已有项,只选择.cs文件,不要把三个文件都选上,然后添加,成功。对于有多个窗体的工程,每次只能添加一个窗体,也就是每次只能选中一个.cs文件。最后,如果工程名字有变化的话,还需要将添加的窗体中的namespace进行替换处理。
  遇到designer无法显示的问题,先将子类改为继承自Form,然后子类内只保留原始的构造函数,然后右键工程Clean,再Rebuild,成功(必须成功,否则这个方法没用),这时候设计窗口应该可以正常显示了。然后改为继承自基类BaseForm,然后恢复原来类里的内容。
在这里插入图片描述

3.12 树型关系的panel控制

  情景描述:窗体中包含三个Panel,分别为panel1、panel2和panel3。panel1和panel2分别有comboBox1和comboBox2。comboBox1有两个选项,一个是“Show Panel 2”,另一个是“Hide Panel 2”。comboBox2对应的是“Show Panel 3“和“Hide Panel 3”。comboxBox2用于控制panel3的显示,comboBox1用于控制panel2的显示而且当panel2隐藏时panel3也要隐藏。我把这个称为“树型关系的panel控制”,其中panel1控制着panel2和panel3,panel2控制着panel3,这种关系如同树一样,panel1是根节点,panel2是panel1的子节点,panel3是panel2的子节点
  实现思路是:隐藏panel2时,把panel2的所有子节点也都隐藏,在此例中是panel3。显示panel2时,判断panel3是否需要显示(即调用comboBox2绑定的方法去判断是否显示panel3)

//#define CASE1
#define CASE2

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace Demo_Cascade_Panel
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            comboBox1.Items.Add("Show Panel 2");
            comboBox1.Items.Add("Hide Panel 2");
            comboBox1.SelectedIndex = 0;

            comboBox2.Items.Add("Show Panel 3");
            comboBox2.Items.Add("Hide Panel 3");
            comboBox2.SelectedIndex = 0;
        }

        private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
        {
#if CASE1
            switch(comboBox1.Text.ToString())
            {
                case "Show Panel 2":
                    panel2.Show();
                    break;
                case "Hide Panel 2":
                    panel2.Hide();
                    break;
            }
#elif CASE2
            switch(comboBox1.Text.ToString())
            {
                case "Show Panel 2":
                    panel2.Show();
                    comboBox2_SelectedIndexChanged(sender, e);
                    break;
                case "Hide Panel 2":
                    panel2.Hide();
                    panel3.Hide();
                    break;
            }
#endif
        }

        private void comboBox2_SelectedIndexChanged(object sender, EventArgs e)
        {
            switch (comboBox2.Text.ToString())
            {
                case "Show Panel 3":
                    panel3.Show();
                    break;
                case "Hide Panel 3":
                    panel3.Hide();
                    break;
            }
        }
    }
}

  注意!按照上面的写法,有一种情况下会有bug!如果是通过其他窗体修改了当前窗体的控件,要注意一个事情!比如在外面将comboBox1选为“Hide Panel 2”,这时panel2和panel3都隐藏了,接着又修改comboBox2为“Show Panel 3”,那么这时显示的将会是panel1和panel3!!改正方法就是在panel2已经显示的情况下再根据comboBox2的选项去控制panel3,否则comboBox2的变化无效!
  不仅是对Panel,对所有的容器类控件同样存在这样的控制逻辑,比如GroupBox。

3.13 循环选择的判断

  所谓的“循环选择”是指对于下拉框,原本是选择A,接着选择了B,然后又选回了A。
  情景描述:对下位机进行配置,比如有一个下拉框是配置是否清除缓冲区,当前配置是“不清除”。我在配置的时候如果选择了”清除“,那么认为配置发生了变化,当按下”下发配置“时,串口发送数据,下位机将被配置为清除缓冲区;如果我选择了“清除”后,又重新选回了“不清除”,那么认为配置没有变化,按下按钮后串口不会发送数据。
  一个实现的思路是:记录当前的配置,当控件的内容发生了变化时(比如下拉框选项变化),若相比当前配置发生了变化,则认为变化有效(即标志变量置1),反之认为无效。每次下发配置时,更新存储当前配置的变量的值。
  这个问题还可以继续丰富,比如如何获取当前配置等。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace Demo_Loop_Selection
{
    public partial class Form1 : Form
    {
        private bool isChanged;
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            comboBox1.Items.Add("Clear Buffer");
            comboBox1.Items.Add("Don't Clear Buffer");
            comboBox1.SelectedItem = "Don't Clear Buffer";
			// 当前配置:不清除缓冲区
            txtCurrentSetting.Text = "Don't Clear Buffer";	
            isChanged = false;
        }

        private void button1_Click(object sender, EventArgs e)
        {
            if (isChanged)
            {
                textBox1.Text = "Setting changed!";
                txtCurrentSetting.Text = comboBox1.Text.ToString();
                isChanged = false;
            }
            else
                textBox1.Text = "Setting not changed!";
        }

        private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
        {
            if(comboBox1.Text.ToString() == txtCurrentSetting.Text.ToString())
                isChanged = false;
            else
                isChanged = true;
        }
    }
}

  上面的思路其实很局限,陷入“控件发生改变就要触发相应的方法”的思维桎梏里!有一个更好的方法:在下发配置时才去遍历每个控件,判断控件的内容相比当前配置是否发生了变化。这样做的好处是大大减少了代码量:因为按照之前的思路,每个控件都需要绑定一个方法,在方法里判断是否相比当前配置发生了变化,现在只需要一次遍历,统一判断。

3.14 树型关系的panel控制+循环选择的判断

  情景描述:要配置一个IO口的类型,输入或者输出二选一,选择类型后需要配置相应的电流大小。要求先选择IO类型为输入,输入电流大小,然后切换为输出,那么此前在输入模式下配置的参数都认为是无效的,此时按下按钮后配置不更新;如果不按下按钮,接着又切回输入,那么之前在输入模式下做的更改认为是有效的。
  一个实现的思路就是:在隐藏相应的panel时,将该panel下的所有控件的变化都认为无效(即相应的标志变量置零),若该panel还控制着下级panel,则下级panel的所有控件变化也都认为无效;当显示相应的panel时,对每一个控件都判断是否相比当前配置发生了变化(即调用相应的事件方法)

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace Demo_Tree_Panel_Control___Loop_Selection
{
    public partial class Form1 : Form
    {
        private bool[] isChanged;
        private string[] curSetting;
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {          
            isChanged = new bool[2];
            isChanged[0] = false;
            isChanged[1] = false;

            curSetting = new string[2];
            curSetting[0] = "1";
            curSetting[1] = "1";

            txtCurInputCurrent.Text = curSetting[0];
            txtCurOutputCurrent.Text = curSetting[1];

            txtInputCurrent.Text = curSetting[0];
            txtOutputCurrent.Text = curSetting[1];

            cbIOType.Items.Add("Input");
            cbIOType.Items.Add("Output");
            cbIOType.SelectedItem = "Input";
        }

        private void txtInputCurrent_TextChanged(object sender, EventArgs e)
        {
            if (txtInputCurrent.ToString() == curSetting[0])
                isChanged[0] = false;
            else
                isChanged[0] = true;
        }

        private void txtOutputCurrent_TextChanged(object sender, EventArgs e)
        {
            if (txtOutputCurrent.ToString() == curSetting[1])
                isChanged[1] = false;
            else
                isChanged[1] = true;
        }

        private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
        {
            switch(cbIOType.Text.ToString())
            {
                case "Input":
                    panel2.Show();
                    panel3.Hide();
                    txtInputCurrent_TextChanged(sender, e);
                    isChanged[1] = false;
                    break;
                case "Output":
                    panel2.Hide();
                    panel3.Show();
                    txtOutputCurrent_TextChanged(sender, e);
                    isChanged[0] = false;
                    break;
            }
        }

        private void button1_Click(object sender, EventArgs e)
        {
            if (isChanged[0])
            {
                txtCurInputCurrent.Text = txtInputCurrent.Text;
                isChanged[0] = false;
            }
            if(isChanged[1])
            {
                txtCurOutputCurrent.Text = txtOutputCurrent.Text;
                isChanged[1] = false;
            }
        }
    }
}

  用3.13的第二个思路会更好,则整体思路变为:在隐藏相应的panel时,将该panel下的所有控件的变化都认为无效(即相应的标志变量置零),若该panel还控制着下级panel,则下级panel的所有控件变化也都认为无效; 当显示相应的panel时,对每一个控件都判断是否相比当前配置发生了变化(即调用相应的事件方法)。只需要去处理显示panel时的情况,不需要去处理隐藏panel时的情况了!!代码精简了,逻辑耦合也变少了!!!

3.15 多线程问题

  上位机与下位机处于回环测试状态:下位机在收到上位机的数据后会立马发送相同的数据到上位机。在绑定按钮单击事件的方法中先由串口发送数据,然后模拟一个耗时3秒的操作后将标志位置1。在RecevieData中打断点,经过测试,断点并不会被触发。分析后可知,该方法所在线程是主线程,当串口发送数据后,ReceiveData()在子线程中被调用,而此时主线程中按钮方法还没有执行到给标志位赋1的地方,所以断点不会被触发

void btn_Click()
{
	serialPort.Write("test");
	// 5s
	flag = 1;
}

void ReceiveData()
{
	if(flag == 1)
	{
		...// 断点在这里
	}
}

3.16 报错:a property of indexer may not be passed as an out or ref parameter

  窗体A中有类C的对象O,它是自动属性;窗体B的构造函数的参数中有一项是类C的引用,在窗体A中创建窗体B,传入ref O,然后报了如题的错误。StackOverflow上的帖子我没看懂,索性绕开了。

public partial class Form1 : Form
{
	public C O { get; set;}
	...
	
	public btnOpenForm2(ref O)	// error
	{
		...
	}
}
public partial class Form2 : Form
{
	private C cInst;
	public Form2(ref C inst)
    {
        InitializeComponent();
        cInst = new C();
        cInst = inst;
    }
    ...
}

3.17 多个控件控制一个参数值 && 一个控件控制多个参数值

  同一个参数被位于两个不同窗体的下拉框控制,以及一个控件控制多个参数值,下发配置和读取配置时如何准确判断呢?
  场景如下图:IO口的模式受三个控件控制,控制关系如下:
  1、当Work Mode选项卡中触发方式选择I/O时,IO口模式只有Sleep/Wakeup,此时选择相应的IO口(IO1、IO2和IO3),选择了IO1,那么IO选项卡中的IO1组合框将隐藏,其他两个同理;
  2、当Work Mode选项卡中触发方式选择MIXED而且I/O1或I/O2不选择Disable时,IO口的模式可以配置成Sleep/Wakeup和Online/Offline。选择I/O1的模式,则IO选项卡中的IO1组合框将隐藏,IO2同理。
  3、当Work Mode选项卡中触发方式选择MIXED而且I/O1或I/O2选择Disable,或者触发方式不是MIXED时,IO口的模式由IO选项卡中相应IO的组合框配置,有三种可配置方式。
  可以看到共有三个控件:IO选项卡下的I/O1下拉框和Work Mode选项卡下MIXED中的I/O1下拉框属于“多个控件控制同一个参数值”的情况,而Work Mode选项卡下IO中的I/O下拉框属于“一个控件控制多个参数值”的情况。

在这里插入图片描述
  我的思路如下:

if (i == 95) // Tab Work Mode/Tab IO: I/O1
{
    ComboBox cb;
    switch (paramVal)
    {
        case "0":   // Disable
            this.flagIOMode[0] = 0;
            this.flagIOModeN[0] = 2;    // for backup
            cb = (ComboBox)this.frm.tabController.fIO.GetControl("cbIO1");
            cb.SelectedIndex = 0;
            AddMapItem(95, cb);  // needed for backup
            break;
        case "1":   // Input                         
            this.flagIOMode[0] = 1;
            this.flagIOModeN[0] = 2;
            cb = (ComboBox)this.frm.tabController.fIO.GetControl("cbIO1");
            cb.SelectedIndex = 1;
            AddMapItem(95, cb);
            break;
        case "2":   // Output
            this.flagIOMode[0] = 2;
            this.flagIOModeN[0] = 2;
            cb = (ComboBox)this.frm.tabController.fIO.GetControl("cbIO1");
            cb.SelectedIndex = 2;
            AddMapItem(95, cb);
            break;
        case "3":   // Indication
            this.flagIOMode[0] = 3;
            this.flagIOModeN[0] = 2;
            cb = (ComboBox)this.frm.tabController.fIO.GetControl("cbIO1");
            cb.SelectedIndex = 3;
            AddMapItem(95, cb);
            break;
        case "4":   // Sleep/Wakeup Mode
            if (this.frm.dtuParam.GetParamRaw(35) == "MIXD")
            {
                this.flagIOMode[0] = 4;
                this.flagIOModeN[0] = 1;
                cb = (ComboBox)this.frm.tabController.fWorkMode.GetControl("cbDIO1WorkMode");
                cb.SelectedIndex = 1;
                AddMapItem(95, cb);
            }
            else
            {
                this.flagIOMode[0] = 6;
                this.flagIOModeN[0] = 3;
                cb = (ComboBox)this.frm.tabController.fWorkMode.GetControl("cbIOPort");
                cb.SelectedIndex = 0;
                AddMapItem(95, cb);
            }
            break;
        case "5":   // Online/Offline
            this.flagIOMode[0] = 5;
            this.flagIOModeN[0] = 1;
            cb = (ComboBox)this.frm.tabController.fWorkMode.GetControl("cbDIO1WorkMode");
            cb.SelectedIndex = 2;
            AddMapItem(95, cb);
            break;
    }
}

  以IO1为例(第95个参数)。首先读取配置时,有从0到5共六个参数值,对应上面控制关系中提到的七种情况,我用变量 this.flagIOMode[0] 分别标记这七种情况:
  1、0~3:对应IO选项卡中IO1组合框的四种情况;
  2、4~5:对应Work Mode选项卡中当触发方式为MIXED的两种(不是三种)情况。这里需要提一下,触发方式为MIXED时也有Disable选项,我一开始觉得多余,去掉了它,但会出现一个问题:假如我选I/O1为Sleep/Wakeup模式后,我又想改为IO选项卡中的Input模式,此时我并没有办法让IO选项卡中IO1组合框重新显示。也就是说MIXED触发方式下的Disable作用只是为了能够重新显示IO选项卡中的组合框。那么为什么I/O触发方式下就不需要Disable选项呢?因为只需要把触发方式改为其他就可以了,比如I/O触发模式下选择了I/O1,那么想在IO选项卡中配置IO1的话,就把触发方式改为其他就好了。那么又会问,为什么MIXED触发方式不可以这么做?因为MIXED触发方式下有很多个配置项啊,不可能因为一个IO口配置方式想改,就把整个触发方式都改了。
  3、6:对应Work Mode选项卡中触发方式为I/O而且选择I/O1的情况。
  this.flagIOMode[0] 表示IO1当前的配置是属于哪种情况。this.flagIOMode[1]this.flagIOMode[2] 同理。
  接下来就是下发配置前判断应该看哪个控件的值。共有三个控件:IO选项卡下的I/O1下拉框、Work Mode选项卡下MIXED中的I/O1下拉框和Work Mode选项卡下IO中的I/O下拉框、。我用this.flagIOModeN[0]记录,从1到3顺序对应上面提到的三个下拉框。

if(cbWMActvMeth.Text.ToString() == "I/O")
{
    if (cbWMIOAll.Text.ToString() == "I/O1" && this.flagIOMode[0] != 6)
    {
        this.flagIOModeN[0] = 3;
        this.frm.changeRecorder.RecordChange(95);
        AddMapItem(95, this.frm.tabController.fWorkMode.GetControl("cbDIO1WorkMode"));   // needed for backup
    }
    else if (cbWMIOAll.Text.ToString() == "I/O2" && this.flagIOMode[1] != 6)
    {
        ...
    }
    else if (cbWMIOAll.Text.ToString() == "I/O3" && this.flagIOMode[2] != 2)
    {
        ...
    }
}
else
{
    if (cbIO3.SelectedIndex != this.flagIOMode[2])
    {
        ...
    }
}

if (cbWMActvMeth.Text.ToString() == "MIXED" && cbWMIO1.Text.ToString() != "Disable")
{
    if (cbWMIO1.SelectedIndex + 3 != this.flagIOMode[0])
    {
        this.flagIOModeN[0] = 1;
        this.frm.changeRecorder.RecordChange(95);
        AddMapItem(95, this.frm.tabController.fWorkMode.GetControl("cbDIO1WorkMode"));   // needed for backup
    }
}
else if (cbWMActvMeth.Text.ToString() == "MIXED" && cbWMIO1.Text.ToString() == "Disable")
{
    if (cbIO1.SelectedIndex != this.flagIOMode[0])
    {
        this.flagIOModeN[0] = 2;
        this.frm.changeRecorder.RecordChange(95);
        AddMapItem(95, this.frm.tabController.fIO.GetControl("cbIO1"));   // needed for backup
    }
}

  因为我对七种情况进行了标记,所以判断是否变化的思路很简单:如果跟当前配置属于的那种情况不一样,那么肯定发生了变化。因为涉及三个控件,所以我有三处判断。
  综上,对于“多个控件控制一个参数值”和“一个控件控制多个参数值”的情况,处理思路就是:对每种情况进行标号,读取配置时记录当前配置属于哪种情况,下发配置时判断参数是否发生改变,是哪个控件导致的改变,根据导致改变的控件进行下发即可

3.18 减少switch分支的方法

  这篇博客提供了两种思路:表和多态,另一篇博客提到了工厂方法和反射(反射是什么??)。

3.19 富文本框颜色出错问题

  发送的字符串用黑色标记,接收的用蓝色标记,但是有时候就是会出现接收的字符串变成了黑色,出现问题也不好保护现场,所以暂未找到解决方法,也没有记录存底。下面是富文本框的代码以及接收发送时的逻辑。

public void AppendLog(string str, Color color)
{
    txtLog.SelectionColor = color;
    txtLog.AppendText(str);
    /* keep the scroll bar at the bottom of the textobx */
    txtLog.SelectionStart = txtLog.TextLength;
    txtLog.ScrollToCaret();
}
rxBuf = serialPort.ReadExisting();
// ... (other operations)
AppendLog(rxBuf, Color.FromArgb(51, 153, 255));
serialPort.Write(txBuf);
AppendLog(txBuf + "\n", Color.Black);

3.20 子窗口打开关闭的数据保护

  项目中将串口的配置放在一个子窗口中,具体就是:
  点击按钮弹出子窗口,此时主窗口冻结,在子窗口配置好串口参数后,点击打开串口,子窗口自动关闭。或者直接关闭子窗口。要求下次弹出子窗口时,仍然显示之前配置好的参数。这就跟中断有些类似,需要进行现场保护和现场恢复:关闭子窗口时进行现场保护,打开子窗口时进行现场恢复。

3.21 不要对接收信息进行时间显示

  曾经想在显示从下位机接收到的字符串时添加时间显示,于是每次串口事件触发时就在接收到的字符串前添加时间,但是因为一个字符串触发串口接收事件的次数是不固定的,所以就会出现本来一个字符串被分成多个子串,每个子串前都有时间显示。最后我放弃了这个想法。

3.22 关于参数检查

  需要对文本框的输入进行参数检查,比如是否非空,是否为数字,是否为整数,是否溢出(向上向下)。可以把这部分写成通用程序。

四、碎感

4.1 接下来的学习方向

  第一个是多线程。多线程在诸如数据库这样包含大量数据操作的应用场合中使用效果会更明显。
  第二个是消息机制。弄懂消息机制,可能可以实现某些原本以为实现不了的功能。因为Winform的功能最直观地取决于控件,我一开始认为有什么控件就只能实现什么功能,所以想不出如何检测USB插拔,直到看到了消息机制,才实现了这个功能。
  第三个是剩余的一些控件和属性的使用。目前常用的控件都用过了,剩下一些可能是更专门的应用需要用到的,可以学习一下积累积累。同样的,属性页上的一堆属性也只清楚其中一二,知道更多属性的用法同样可以帮助更好地实现某些功能,比如这次看到的Tag属性。

4.2 编程小习惯

  1、成员变量的初始化尽可能放在构造函数中,不要在声明的时候初始化。
  2、主窗口的名字为Mainform.cs,子窗口的名字为F+名字.cs。
  3、文本框的参数检查。
  4、成员变量(包括控件变量)一律加this,局部变量不加。
  5、考虑到程序移植的便利性,尽量不要在属性页做编辑,而是用代码实现。

五、待解决问题

5.1 串口变量在主窗体,子窗体的子窗体要用这个串口收发数据

  我的项目里,串口变量在主窗口,数据接受函数也在主窗口。在二级子窗体中需要用这个串口发数据,我的解决方法是把主窗体变量逐层传递到二级子窗体中,在二级子窗体中调用主窗体的串口变量发送数据。但是,二级子窗体要根据收到的数据来做下一步动作的话,该怎么做呢?只能在主窗体的串口接收函数中做处理了吗?

5.2 程序的发布和更新

  目前程序的发布是通过VS自带的功能。如果想要用户每次打开软件都能够检查更新,那么需要软件联网查询,且需要把更新后的程序放在服务器上。

  • 0
    点赞
  • 0
    评论
  • 5
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值