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):