winform中一个控件如何监视另一个控件的某个属性发生变化?

简单例子

  以一个简单例子来说明问题,现在给一个需求:在Form1窗体中,以Button为基础做一个圆形外观的按钮CircleButton,在窗体中,放置一个CircleButton和一个textbox,当CircleButton改变其直径大小时,textbox的文本能实时显示圆形按钮的直径值,当手动在textbox输入一个数字(1-100)时,CircleButton能实时改变其直径大小。注意,圆形按钮不能通过放置背景图片来做。

问题分析

  这个例子最核心的问题是如何实现圆形按钮,因为需求限定了不能通过背景图片的方式制作,所以基本上只能通过自定义控件的方式来制作圆形按钮了,控件的外观绘制可以通过绘制Region然后赋给圆形按钮的Region属性即可。

  光实现外观还不行,在圆形按钮类中,我们必须要定义一个直径属性,来说明当前直径的大小,同时通过getter提供外部访问该属性的通道,通过setter可以在别的方法里设置圆形按钮实例的直径大小,这个属性也是我们将要监视的属性。

  有了getter和setter的圆形按钮只能实现:textbox主动输入值,然后改变圆形按钮大小的功能;但还是难以实现当圆形按钮主动改变其直径时,textbox实时更新其直径值,那么有什么方法可以让textbox监视CircleButton的直径属性发生变化呢?

  也许你一开始会往线程那个方向想,新开一个线程,轮询监视CircleButton的直径是否发生改变,如果发生改变则更新textbox里的值。但显然这不是理想的方案,如果在项目中存在A对B的监视,B对C的监视等等几十个监视,难道你要开几十个线程吗,显然这是不合理的。其实只有改变值的那一刻触发才是合理的,其他时间的轮询完全是在让CPU做无用功。那么真正解决这个问题的方案是怎样的呢?

发布订阅模式

  要解决这个问题,先了解一下发布订阅模式,那么什么是发布订阅模式呢?

  以生活场景为例,我们生活中遇到的大部分服务是即时服务,比如有一个商家在卖烤肠,我过去用金钱买得一根烤肠。但是有一些服务并不是即时得到结果的,这个结果的出现需要等待一段时间,比如订阅报纸的服务,我们需要到报社跟卖报人说我要订阅什么什么报纸,并告诉他等到报纸到了后送到哪里(或者有些顾客会自己上门取),然后付钱等着报社消息即可。在报社里,卖报人把你的个人信息与订阅的报纸登记下来,只要这个报纸一到,他便会通知你。在这个服务过程中,发布者就是报社,订阅报纸的人就是订阅者。

  现在你已经知道了什么是发布者和订阅者,接下来可以参考这篇文章来了解如何用C#实现发布订阅模式,关键是了解发布者是如何把你的“个人信息与订阅的报纸”登记下来的,以及发布者是如何知道自己的某种报纸发布了?

  现在假设你已经看完了我给出的参考文章,我给出的同时抛出了两个问题,现在先解答一下我对于这两个问题的理解。

  1. 发布者是怎么把你的“个人信息与订阅的报纸”登记下来的?

  可以看到在main函数中,发布者有个自定义事件叫做NumberChange,这个事件我们就可以理解为某种报纸到货了,然后它给该事件绑定了一个订阅者的方法subscriber.OnNumberChange,注意,subscriber可以理解为就是一个订阅者,OnNumberChange是报纸到了之后,这个订阅者要做的事情,比如说叫报社的人送过来,或者我自己来取等,这个动作的定义是由订阅者决定的。

publisher.NumberChange += subscriber.OnNumberChange;

  这段代码的意思是,订阅者subscriber告诉发布者publisher,只要你NumberChange事件一执行,就执行我这个OnNumberChange方法。

  2. 发布者是如何知道自己的报纸发布了?

  那么发布者怎么知道它自己的NumberChange什么时候执行(报纸什么时候发布)呢,这就要看你什么时候调用这个事件了,在参考文章中,可以看到是在Publisher类的MyNumber属性的set方法中进行了调用,所以,每次MyNumber值被改变的时候,就会触发这个事件。这个事件是我们程序员自己定义的,我知道对于新手程序员事件接触得并不多,但是我要告诉你们的是,声明一个事件时需要绑定一个委托,你可以从这个现象中去理解:在我们设置winform中的某个控件的click事件时,通常我们是找到该控件的事件列表,然后双击Click事件,控件代码自动生成一个这个控件对应的click方法体,你有思考过为什么双击这个Click事件会自动生成固定的一个方法体吗,因为声明事件的条件就是要绑定一个固定的方法签名,这样似乎就能解释的通了。

public delegate void NumberChangeDelegate(int newValue);  //声明一个委托NumberChangeDelegate,也就是一个方法签名 
public event NumberChangeDelegate NumberChange; //声明一个事件,并绑定上述的方法签名,类实例该事件注册的方法签名必须与NumberChangeDelegate一致

  如果你能理解透彻上述我说的两个关键问题,基本上就理解订阅发布模式了。接下来我们来逐步实现上述的简单例子。

例子的具体实现

  例子的具体实现我将其分为3步,分别是绘制CircleButton,实现textbox控制CircleButton,实现CircleButton通知textbox。

绘制CircleButton

  在项目中添加新建项->用户控件(windows窗体),新建控件名为CircleButton,其代码修改为如下所示

public partial class CircleButton : Button
    {
        private int diameter = 100;

        public int Diameter
        {
            get { return diameter; }
            set
            {
                diameter = value;
                SetDiameter();
            }
        }
        public CircleButton()
        {
            InitializeComponent();
            ClientSize = new Size(200, 200);
            FlatStyle = FlatStyle.Flat;
            FlatAppearance.BorderSize = 0;
            BackColor = Color.Red;
            SetDiameter();
        }
        private void SetDiameter()
        {
            GraphicsPath grPath = new GraphicsPath();
            grPath.AddEllipse(0, 0, diameter, diameter);
            Region = new Region(grPath);
        }
    }

  在Form1窗体设计器中拖入一个textbox1,然后查看Form1代码,在Form1构造函数中添加如下代码:

public Form1()
{
       InitializeComponent();
       cb = new CircleButton();
       cb.Location = new Point(100, 100);
       this.Controls.Add(cb);
}
    

  点击运行,可以看到窗体如下图所示

 

实现textbox控制CircleButton

  这个很好实现,因为我们已经在CircleButton的setter里设置了改变直径的方法了,所以这里我们直接给textbox1的textchange事件绑定方法就行。找到textbox1的事件列表,双击textchange事件,在该事件对应的方法内写如下代码;

private void textBox1_TextChanged(object sender, EventArgs e)
        {
            int value;
            bool isConverted = int.TryParse(textBox1.Text, out value);
            if (isConverted)
            {
                if (value >0 && value <=100)
                {
                    cb.Diameter = value;
                }
            }
        }

  再次运行,在textbox1输入框中输入1-100的数字,应该可以看到红色圆形按钮大小的即时变化。

实现CircleButton通知textbox

  终于到了重头戏了,这里就要用到我们解释了半天的发布订阅模式了。首先,我们要在CircleButton(发布者)里声明一个委托和一个事件(事件的声明需要一个委托,前面解释过),在直径属性被改变时,我们调用该事件,即可触发与该事件绑定的所有方法(尽管这样的说法不准确,但对于新手来说足够了)。在CircleButton中,修改代码如下:

private int diameter = 100;
        public delegate void OnDiameterChanged(int value);
        public event OnDiameterChanged DiameterChanged;

        public int Diameter
        {
            get { return diameter; }
            set
            {
                if (diameter != value)
                {
                    diameter = value;
                    SetDiameter();
                    DiameterChanged(diameter);
                }
            }
        }

  在Form1的构造函数中,添加一个私有方法,其方法签名必须与CircleButton中声明的委托一致:

 private void UpdateDiameter(int value)
        {
            textBox1.Text = value.ToString();
        }

  修改Form1构造函数,其代码如下所示:

public Form1()
        {
            InitializeComponent();
            cb = new CircleButton();
            cb.Location = new Point(100, 100);
            this.Controls.Add(cb);
            //给CircleButton改变直径的事件添加textbox1更新直径的方法
            cb.DiameterChanged += UpdateDiameter;
        }

验证

  如何验证CircleButton更新直径会通知textbox1呢,很简单,我们再拖个textbox2到Form1中,然后添加textchange事件,令其改变cb的直径,然后观察textbox1中的值是否发生改变

        private void textBox2_TextChanged(object sender, EventArgs e)
        {
            int value;
            bool isConverted = int.TryParse(textBox2.Text, out value);
            if (isConverted)
            {
                if (value > 0 && value <= 100)
                {
                    cb.Diameter = value;
                }
            }
        }

运行效果如图所示(上边为textbox2,下边的为textbox1):

 

posted @ 2022-11-25 00:37  hlz2516  阅读(506)  评论(0编辑  收藏  举报