在一个项目中不可避免会有多个窗体的控件布局类似,里面的代码也有好多相同的,可以新建窗体,然后复制粘贴来实现窗体的重复使用,这种方式固然好,可麻烦也随之而来,随着项目的深入,不可避免的要修改窗体布局或代码等,这时就要对多个这样的窗体全部进行修改。正是如此,所以笔者决定采用继承窗体的方式来实现窗体的复用。
窗体继承的官方帮助:Windows 窗体可视化继承
窗体继承有两种方式:编程方式或使用视觉继承选取器。编程方式简单易上手,本文仅介绍以编程方式继承窗体,操作步骤如下:
新一个Windows窗体应用程序,包含被继承的窗体(在这里称基窗体)BaseFrm和继承自基窗体的子窗体Form2。
在基窗体设计界面拉入一个DataGridView控件、一个Button控件和一个TextBox控件,当然啦,你还可以拖入其他控件,只要你需要。如图所示:
打开子窗体的代码界面,我们将会看到自动生成的代码,如下图:
要使Form2窗体继承自基窗体BaseFrm,很简单,只需要添加代码:Inherits BaseFrm
即可。如下图:
是不是很简单?
测试过程中发生很多意想不到的错误和问题,具体如下:
当键入Inherits BaseFrm
后就会报这个错误,如下图所示:
解决办法:
在“解决方案资源管理器”中依次点开“Form2.vb”左侧的三角形。
双击打开“Form2.Designer.vb”代码窗口,将代码Inherits System.Windows.Forms.Form
修改为Inherits BaseFrm
,如下图:
打开子窗体Form2的设计界面,会出现上述错误,如图:
在官方帮助中有这么一段话:为了从窗体进行继承,包含该窗体的文件或命名空间必须生成为可执行文件或 DLL。 若要生成项目,请从“生成”菜单选择“生成”。 此外,对命名空间的引用必须添加到继承窗体的类。 显示的对话框和菜单命令可能会与“帮助”中的描述不同,具体取决于你现用的设置或版本。 若要更改设置,请在 “工具” 菜单上选择 “导入和导出设置” 。
解决办法
根据提示,点击“生成”菜单选择“生成 窗体继承”。
关闭Form2窗体的设计界面再重新打开即可看到设计视图,如图:
如上图,继承来的控件会有一把锁,我们无法移动,也无法在控件属性窗口里修改其属性。
解决办法
在基窗体BaseFrm中把需要在子窗体中修改属性的控件(本文中仅修改DataGridView控件和Button控件)Modifires属性设置为protected或public,如下图所示,注意,只能在属性窗口设置,不能通过代码设置Modifires属性。回到子窗体设计界面,还是不能修改控件属性。
在官方帮助里的看到这样的说明:在应用程序开发过程中,可能经常需要更改基窗体的外观,在设计时,如果生成了包含基窗体的项目,则对基窗体外观所做更改(属性的设置或控件的增减)将反映在继承的窗体上。 仅将更改保存到基窗体是不够的。 若要生成项目,请从“生成”菜单选择“生成”。在运行时对基窗体所做修改不影响已实例化的继承窗体。
根据官方提示,重新生成项目,关闭子窗体设计界面再打开,出现了下图错误提示:值不在预期的范围内。
万般无助,尝试点击“开始调式”,调试过后再打开子窗体界面,居然能正常显示设计界面了,Button控件可以修改属性,但DataGridView控件还是处于锁定状态而无法修改其属性。
从官方帮助中发现这段说明:对于WebBrowser、ToolStrip、ToolStripPanel、TableLayoutPanel、FlowLayoutPanel、DataGridView控件,无论使用何种修饰符(private、protected 或 public),继承的窗体中的这些控件始终为只读。
原来DataGridView控件只能为只读。
分别在基窗体和子窗体里为按钮Button1添加Click事件处理程序:
基窗体:
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
MessageBox.Show("这是基窗体Button1控件Click事件")
End Sub
子窗体:
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
MessageBox.Show("这是子窗体Button1控件Click事件")
End Sub
测试时,这两段代码均被执行,如下图:
可否控制这两段代码的执行呢?比如仅执行子窗体里的Button1按钮Click事件。
解决办法
最简单的办法是删除不需要执行的代码,如果仅执行子窗体里的Button1按钮Click事件,则把基窗体里的Button1按钮Click事件代码删除即可。
当然不删除也是可以的,需把以上代码修改为:
基窗体:
Protected Overridable Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
MessageBox.Show("这是基窗体Button1控件Click事件")
End Sub
子窗体:
Protected Overrides Sub Button1_Click(sender As Object, e As EventArgs)
MessageBox.Show("这是子窗体Button1控件Click事件")
End Sub
可以看出,把访问级别Private修改为Protected或public,基窗体里的代码增加了Overridable修饰符,表示该方法可以被重写,而子窗体里增加Overrides修饰符,表示重写基窗体里的Button1按钮Click事件处理函数。特别注意子窗体里不再需要关联事件,即需要删除这段代码:Handles Button1.Click
。测试结果与预想一样,仅执行子窗体里的Button1按钮Click事件。
如果我想同时执行基窗体里的Button1按钮Click事件,可以实现吗?只需在子窗体里的Button1按钮Click事件处理函数里增加代码:MyBase.Button1_Click(sender, e)
完整代码如下:
Protected Overrides Sub Button1_Click(sender As Object, e As EventArgs)
MessageBox.Show("这是子窗体Button1控件Click事件")
MyBase.Button1_Click(sender, e)
End Sub
或
Protected Overrides Sub Button1_Click(sender As Object, e As EventArgs)
MyBase.Button1_Click(sender, e)
MessageBox.Show("这是子窗体Button1控件Click事件")
End Sub
代码顺序不同执行的顺序也不同,具体你可以亲自测试一下。
经过如此处理,我们就可以随心所欲的控制基窗体与子窗体里的事件的发生。
如果遇到基窗体里的某个方法不能被重写,可以自定义一个方法,称为虚方法。还是以Button1按钮Click事件为例来演示。
基窗体:自定义了一个虚方法BtnClick,然后在Button1按钮Click事件处理函数里调用该方法。
Protected Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
BtnClick()
End Sub
Protected Overridable Sub BtnClick()
MessageBox.Show("这是基窗体Button1控件Click事件")
End Sub
子窗体:重写基窗体里的 BtnClick方法即可,不需要添加Button1按钮Click事件。
Protected Overrides Sub BtnClick()
MessageBox.Show("这是子窗体Button1控件Click事件")
End Sub
如果需要同时调用基窗体和子窗体里的Button1按钮Click事件,子窗体里的代码增加MyBase.BtnClick()
,即:
Protected Overrides Sub BtnClick()
MessageBox.Show("这是子窗体Button1控件Click事件")
MyBase.BtnClick()
End Sub
在基窗体代码窗口里添加New函数和Load事件代码:
Public Sub New()
' 此调用是设计器所必需的。
InitializeComponent()
' 在 InitializeComponent() 调用之后添加任何初始化。
MessageBox.Show("这是基窗体New函数")
End Sub
Private Sub BaseFrm_Load(sender As Object, e As EventArgs) Handles MyBase.Load
MessageBox.Show("这是基窗体Load事件")
End Sub
打开子窗体设计界面时,你会发现这两段代码均被执行。虽然并无大碍,但总感觉不舒服,在其他博文说可以通过Component.DesignMode 属性来阻止以上代码在打开设计界面时执行,即把不需要运行的代码包含在以下IF块中:
If Not DesignMode Then
End If
关于Component.DesignMode 属性,摘抄于其他博文,本人并不是很理解:
“一个控件只有在它自己被拖拽到设计器的时候,其 DesignMode 才是真,如果它被包含在其他控件中被加入到设计器,那么那个控件才是在设计模式,而它不是!换句话说,DesignMode 并不能反映当前环境是否是运行时,它只能告诉你,这个控件当前是不是直接被设计器操作(嵌套的已经不算了) 。”
修改过后的代码如下:
Public Sub New()
' 此调用是设计器所必需的。
InitializeComponent()
' 在 InitializeComponent() 调用之后添加任何初始化。
If Not DesignMode Then
MessageBox.Show("这是基窗体New函数")
End If
End Sub
Private Sub BaseFrm_Load(sender As Object, e As EventArgs) Handles MyBase.Load
If Not DesignMode Then
MessageBox.Show("这是基窗体Load事件")
End If
End Sub
测试发现仅能屏弊Load事件,而New函数里的if块并没有被屏弊。
如果在基窗体里添加的是含参New函数又会如何呢?
我们把原来的无参New函数修改为:
Public Sub New(mystring As String)
' 此调用是设计器所必需的。
InitializeComponent()
' 在 InitializeComponent() 调用之后添加任何初始化。
If Not DesignMode Then
MessageBox.Show("这是基窗体New函数,传入的参数为:" & mystring)
End If
End Sub
会报错:类“Form2”必须声明一个“Sub New”,原因是它的基类“BaseFrm”没有不使用参数就可以调用的可访问“Sub New”。
根据提示为“Form2”必须添加“Sub New”:
Public Sub New()
' 此调用是设计器所必需的。
InitializeComponent()
' 在 InitializeComponent() 调用之后添加任何初始化。
End Sub
又报错:“Form2”的基类“BaseFrm”没有不使用参数就可以调用的可访问“Sub New”,因此该“Sub New”的第一个语句必须是对“MyBase.New”或“MyClass.New”的调用。
再根据提示增加MyBase.New:
Public Sub New()
MyBase.New("ok")
' 此调用是设计器所必需的。
InitializeComponent()
' 在 InitializeComponent() 调用之后添加任何初始化。
End Sub
终于不再报错,调试后再次打开子窗体设计界面,又报错了:未找到类型“窗体继承.BaseFrm”上的构造函数。如图:
估计在设计期间只能调用无参New函数,所以尝试在基窗体里再次添加无参New函数,问题解决,不再报错并正确显示设计界面。
面对对象编程中,窗体也是类,所以我们也可以通过类来继承窗体,具体测试就不再演示了。但我还是建议按本文方式继承窗体,这样会自动生成一个窗体可视设计器对应的Designer类,使窗体初始化代码与逻辑代码分离为两个文件。而如果通过新建一个类的方式,会导致窗体可视化设计器生成的代码在本身的类中。