本文转自http://blog.163.com/da7_1@126/blog/static/104072678201202844831176/
Form问题是最基本的问题,因为编写WinApp程序首先接触的对象就是它,因此在论坛中对它而产生的问题也最常见。
与Form相关的常见问题大致分为如下的四类问题。
第一类问题:如何控制窗体的显示顺序;
第二类问题:窗体之间的对象如何相互引用或操作;
第三类问题:如何处理窗体唯一性问题;
最后一个问题:如何合理的关闭窗体或程序。
接下来先说说如何控制窗体的显示顺序。
很多编程者常常会遇到这样的现象,例如,通过一个登录窗体去打开一个主窗体,然后要在主窗体中想关闭这个登录窗体。那么就有人出主意,你可以在打开主窗体的时候把登录窗体自身传进去,然后在主窗体中调用它的Hide方法来隐藏。虽说这样可以暂时达到你所要的效果,但不是最合理的解决办法。因为这样做有如下两个缺陷。
第一个就是,登录窗体已经完成使命,而资源没有得到及时释放;
其次就是,在窗体关闭的时候比较麻烦,需要找到登录窗口,关闭自身的同时要关闭登录窗体。
遇到此问题的时候,首要的是分析窗体打开的顺序以及相互关联的条件,常见的类型无非就是主子或者先后这两种。理解好第一点后,那么要学会合理使用ShowDialog和DialogResult这两个好东西,前者属于模式打开窗体,后者属于窗体的返回值。
明白了这两点,就可以很方便的解决类似于登录窗体的问题,这方面的例子可以参看我的这篇文章。
***************************************************
最近,看到网上经常会问如何进行窗口跳转,大多数的问题都是牵扯到Login窗口。其实,在Visual Studio 6以来,比较正确的做法,是判断Login窗口的返回值,然后决定是否打开主窗体,那么在C#中也是一样的。
具体做法如下:
首先,创建Login窗口,然后添加相应的输入框和按钮,设置窗口的AcceptButton为窗体的确认按钮,而CancelButton为窗体的取消按钮。例如:
this.AcceptButton = this.btnOK;
this.CancelButton = this.btnCancel;
定义确定按钮以及取消按钮事件,如下:
private void btnOK_Click(object sender, System.EventArgs e)
{
// Here is to use fixed username and password
// You can check username and password from DB
if( txtUserName.Text == "Admin" && txtPassword.Text == "nopassword" )
{
// Save login user info
uiLogin.UserName = txtUserName.Text;
uiLogin.Password = txtPassword.Text;
// Set dialog result with OK
this.DialogResult = DialogResult.OK;
}
else
{
// Wrong username or password
nLoginCount++;
if( nLoginCount == MAX_LOGIN_COUNT )
// Over 3 times
this.DialogResult = DialogResult.Cancel;
else
{
MessageBox.Show( "Invalid user name and password!" );
txtUserName.Focus();
}
}
}
private void btnCancel_Click(object sender, System.EventArgs e)
{
// Set dialog result with Cancel
this.DialogResult = DialogResult.Cancel;
}
然后,在Login窗体的Closing事件中,要进行处理,如下:
private void frmLogin_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
// Check whether form is closed with dialog result
if( this.DialogResult != DialogResult.Cancel &&
this.DialogResult != DialogResult.OK )
e.Cancel = true;
}
除此外,Login窗体一些辅助代码如下:
private int nLoginCount = 0;
private const int MAX_LOGIN_COUNT = 3;
private UserInfo uiLogin;
public frmLogin( ref UserInfo ui )
{
//
// Required for Windows Form Designer support
//
InitializeComponent();
// Set login info to class member
uiLogin = ui;
}
调用的时候,要修改程序的Main函数,如下:
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
UserInfo ui = new UserInfo();
frmLogin myLogin = new frmLogin( ref ui );
if( myLogin.ShowDialog() == DialogResult.OK )
{
//Open your main form here
MessageBox.Show( "Logged in successfully!" );
}
else
{
MessageBox.Show( "Failed to logged in!" );
}
}
而附加的UserInfo类如下:
/// <summary>
/// User info class
/// </summary>
public class UserInfo
{
private string strUserName;
private string strPassword;
public string UserName
{
get{ return strUserName;}
set{ strUserName = value; }
}
public string Password
{
get{ return strPassword;}
set{ strPassword = value;}
}
public UserInfo()
{
strUserName = "";
strPassword = "";
}
}
****************************************
文章中修改了一个WinApp的入口函数Main,那么并不意味着这个方法只能在这儿使用,它可以在很多方法中进行使用,基本语法类似,这里我就不多说了。
第二个要说的是窗体之间的对象如何相互引用或者操作。
常见的类似问题有:
1. 如何在子窗体访问到主窗体中的某某数据;
2. 如何在子窗体中调用主窗体中的某某方法;
3. 如何在子窗体关闭的时候去更新主窗体的某某数据;
对于如上的三个问题,完全可以由如下两种方法来完成。
1. 当数据是子窗体显示的必要条件的话,通过修改子窗体的构造函数来进行传递数据;
2. 如果是不定时的访问,则可以通过委托来实现。
对于第一点,我就不多说了,对于第二点,我用如下的例子来说明。
首先在子窗体中,需要如下:
//Define two delegate methods to get or set textbox value in main form
public delegate void SetTextValue( string TextValue );
public delegate string GetTextValue( );
// In sub-form class
// Handler for methods from main form
private SetTextValue SetText = null;
private GetTextValue GetText = null;
// Call methods as follows
string strValue = GetText();
SetText( strValue + DateTime.Now.ToString() );
除了如上一些操作外,还需要修改子窗体的构造函数,来接收这两个delegate方法,这里就不多说了。
至于主窗体,首先要为这两个委托来实现对应函数,例如:
/// <summary>
/// Get textbox's text for other forms
/// </summary>
/// <returns></returns>
private string GetValue()
{
return yourTextBox.Text;
}
/// <summary>
/// Set textbox's text for other forms
/// </summary>
/// <param name="sValue"></param>
private void SetValue( string sValue )
{
yourTextBox.Text = sValue;
}
那么调用子窗体的时候就比较简单了。
// Create subform and show it
yourSubForm myForm = new yourSubForm(
new SetTextValue( SetValue ),
new GetTextValue( GetValue ) );
myForm.ShowDialog();
这样一个通过委托来操纵主窗体的例子就完成了。这里需要注意的一点,如果在子窗体中大量使用到主窗体的数据的话,那我建议你重新考虑窗体架构,这意味着你目前的窗体架构不合理。
有人说了,仅仅为了访问一个成员就需要劳师动众编写委托,多麻烦,直接public成员,或者使用static静态成员多方便,那么对于这两点的坏处,我这里就不多说了,参看我的这篇文章你就会明白。
******************************************************
在程序中,难免要访问某个对象的私有成员。那么以前实现这类功能的方法有两种,第一种方法最简单,就是把成员访问符从“private”改为“public”即可;而另一个就是提供公有的成员访问函数来进行访问。那么现在用C#编写程序,就不再需要采用前面所说的两种方法了,而直接使用属性来完成。
首先来看看三种方法的如何实现以及调用的,这里用一个例子来说明,即访问“EmployeeInfo”类的私有成员strName,具体如下表格所示。
private string strName; | 访问方法 | |
修改成员访问符 | 修改 private string strName; 为 public string strName; | EmployeeInfo empNew...; string strNameValue = empNew.strName; empNew.strName = "me"; |
公有成员函数 | 增加如下两个成员函数 public string getName() { return strName; } public void setName( string Name ) { strName = Name; } | EmployeeInfo empNew...; string strNameValue = empNew.getName(); empNew.setName( "me" ); |
属性 | 增加如下属性 public string Name { get{ return strName;} set{ strName = value; } } | EmployeeInfo empNew...; string strNameValue = empNew.Name; empNew.Name = "me"; |
那么这三种方法有什么区别呢,用如下的表格,可以更好的说明三者的区别。
类的封装性 | 代码安全性 | 代码繁琐性 | 代码效率 | |
修改成员访问符 | 破坏类的封装 | 存在潜在危险 | 简便 | 最高 |
公有成员函数 | 没有破坏 | 安全 | 繁琐,而且调用不直接 | 最低 |
属性 | 没有破坏 | 安全 | 简便 | 仅次于第一种方法 |
(备注:这里用红色表明每一子项中最不好的)
因此可以看出使用属性不但没有破坏类的封装性,没有减弱代码的安全性,而且和第一种方法一样简便,只是在效率方面要略低于第一种方法。但总体看来,在C#中用属性来访问类的私有成员是不二的选择。
不过对于使用属性,以及如上表格所说的,难免会有人产生如下一些疑问。
疑问一:就是用属性是否能达到成员函数那样的效果,即完成复杂的代码操作。
其实属性的底层实现是借助于成员函数,只不过这部分转换是由系统帮忙做的,所以在编写属性的时候,可以像编写成员函数一样,即在成员函数中所能写的代码片断,完全可以在属性中套用。下面就贴出属性所转换的微软中间语言(MSIL)代码。
.property instance string Name()
{
.get instance string NameSpace.EmployeeInfo::get_Name()
.set instance void NameSpace.EmployeeInfo::set_Name(string)
}// end of property EmployeeInfo::Name
.method public hidebysig specialname instance string get_Name() cil managed
{
...
}// end of method EmployeeInfo::get_Name
.method public hidebysig specialname instance void set_Name(string 'value') cil managed
{
...
}// end of method EmployeeInfo::set_Name
如上就是前面EmployeeInfo类的Name属性所转换的中间语言代码(不过省略了函数的具体实现代码,因为这里并不是为了研究中间语言代码,如果需要对这部分有更多地了解,参看中间语言相关书籍)。
疑问二:就是用属性的效率是否仅次于第一种方法。
从上面很容易看出,属性在编译的时候会转换成和成员函数一样的代码,那么它的效率应该和成员函数是一样的。其实并不是这样,因为JIT编译器会把属性所转换成的两个成员函数作为内联函数,这样效率会提高很多。(注:内联函数是代码被插入到调用者代码处的函数,通过避免函数调用所产生的额外开销,从而提高执行效率。不过书中也提到,即使不是内联函数,成员函数相对于方法一的效率损失也是微乎其微的。)
用C#写程序,一提到属性,大家都会编写。其实在属性中,可以产生很多应用,接着来就分别说明。
<!--[if !supportLists]-->1. <!--[endif]-->在属性中使用索引符,例如像“ArrayList[i]”来访问ArrayList某个成员。这里需要注意的是,属性名以及索引参数的编码格式是固定的,如“this […]”。不过索引参数可以是多个,而且不光支持整型参数,还可以使用其他类型参数。例如:
public ReturnValueType this[ ParType1 parValue1, ParType2 parValue2]
{
get{...}
set{...}
}
<!--[if !supportLists]-->2. <!--[endif]-->可以给属性操作加上互斥锁,以防止多线程操作时而产生的并发错误,具体如下。
public string Name
{
get
{
lock(this)
{
return strName;
}
}
set
{
lock(this)
{
strName = value;
}
}
}
<!--[if !supportLists]-->3. <!--[endif]-->书上还提到属性的其他应用,例如:通过接口来实现在一个类中同时提供只读属性以及非只读属性。但是我个人认为,虽然这样可以实现,但是会产生歧义,即在一个类中提供两个不同版本的属性,破坏了类的一致性,所以我并不推荐这么做。
接着,要说说编写属性的时候,需要注意些什么,我个人认为有如下两点大的方面。
第一个就是编写属性get部分的时候,如果当前属性的类型是引用类型的话,且不想通过属性来修改局部成员的话,最好返回局部成员的copy,而不是成员本身。
例如:
public class class1
{
string _data;
public class1( string data )
{
_data = data;
}
public string Data
{
get{ return _data;}
set{ _data = value;}
}
}
public class class2
{
private class1 myClass1 = null;
public class1 Class1
{
get{ return myClass1; }
}
public string Data
{
get{ return myClass1.Data;}
}
public class2( string data )
{
myClass1 = new class1( data );
}
}
如果按照如上所写,那么class2对象可以通过Class1.Data属性访问和修改局部成员myClass1某些值,这样就可以修改了myClass2的私有成员myClass1的值,即会产生潜在错误。
例如:
class1 myClass1 = myClass2.Class1;
myClass1.Data = "test2";
如何避免这类错误呢,那么首先需要修改Class1属性的编写,其次在class1类需要提供Clone函数或者其他copy函数,具体如下:
public class class1:ICloneable
{
string _data;
public class1( string data )
{
_data = data;
}
public string Data
{
get{ return _data;}
set{ _data = value;}
}
#region ICloneable Members
public object Clone()
{
// TODO: Add class1.Clone implementation
return new class1( _data );
}
#endregion
}
public class class2
{
private class1 myClass1 = null;
public class1 Class1
{
get{ return myClass1.Clone() as class1; }
}
public string Data
{
get{ return myClass1.Data;}
}
public class2( string data )
{
myClass1 = new class1( data );
}
}
第二个需要注意的是编写属性set部分的时候,这里需要对参数进行有效性检查。因为属性是外界修改类的私有成员的入口,为了避免因为私有成员不正确而产生的错误,所以在进行属性set的时候要进行有效性检查,从而保证私有成员对于整个类来说是有效的。
很多人都苦恼于如何在子窗体中操作主窗体上的控件,或者在主窗体中操作子窗体上的控件。相比较而言,后面稍微简单一些,只要在主窗体中创建子窗体的时候,保留所创建子窗体对象即可。
下面重点介绍前一种,目前常见的有两种方法,基本上大同小异:
第一种,在主窗体类中定义一个静态成员,来保存当前主窗体对象,例如:
public static yourMainWindow pCurrentWin = null;
然后在主窗体构造函数中,给静态成员初始化,如下:
pCurrentWin = this;
那么在子窗体中调用父窗体,可以通过“主窗体类名. pCurrentWin”来操作当前的主窗体。
第二种,是在子窗体中定义一个私有成员,来保存当前主窗体对象,例如:
private yourMainWindow pParentWin = null;
然后在子窗体构造函数中,加一参数,如下:
public yourChildWindow( yourMainWindow WinMain )
{
pParentWin = WinMain;
//Other code
}
在主窗体创建子窗体的时候,要把this作为参数来构造子窗体,这样在子窗体中调用父窗体,可以直接用“this.pParentWin”就可以了
不过以上所作的,只是让你能够访问当前主窗体对象,那么如何操作控件,很多人直接修改控件的成员访问符,即把“private”改为“public”,我觉得这样破坏了本身类的封装,所以我比较喜欢的做法是增加公有属性或方法来供调用,例如:
public string ButtonText
{
get{ return btn.Text;}
set{ btn.Text = value;}
}
public void Button_Click()
{
this.btnDConvert.PerformClick();//Execute button click
}
*************************************************
第三类问题,窗体的唯一性问题,这个问题我在这儿就不多说了,因为这类问题我在如下的文章已经说得很透彻了。
*****************************
经常看到有人讨论程序运行唯一性或者窗体运行的唯一性问题。我之前也写了一些文章,在此把它进行整理汇总。
如果是程序的唯一性问题,我之前的一篇文章已经写得很全面,可以参看。
http://blog.csdn.net/knight94/archive/2006/03/16/625809.aspx
如果是MDI子窗体的话,那么我最近的一篇文章提到的两种方法都不错,可以参看。
http://blog.csdn.net/knight94/archive/2006/05/17/742324.aspx
如果不是MDI子窗体的话,而是一般窗体的话,其实要做到唯一打开的话,其实也是很简单的,需要在窗体中去做一些简单代码即可了。
如下就用一个名叫“frmUniqueForm”窗体类来说明。
首先,需要在此窗体类中,加一个静态窗体类对象,如下:
// Save the current form object
private static frmUniqueForm pUniqueForm = null;
然后在窗体类的构造函数中,去初始化静态对象,如:
pUniqueForm = this;
在窗体类的Closed事件中,去释放当前静态对象,代码如下:
private void frmUniqueForm_Closed(object sender, System.EventArgs e)
{
pUniqueForm = null;
}
最后,要在此窗体类中创建一个静态函数,来打开唯一窗体,具体如下:
public static void ShowUniqueWindow()
{
// Init static form object
if( pUniqueForm == null )
{
// Create new form
new frmUniqueForm();
// Show the form
pUniqueForm.Show();
}
// Set window focus and topmost attributes
pUniqueForm.Focus();
pUniqueForm.TopMost = true;
}
那么在其他地方去打开此窗口就非常简单了,只需调用这个静态函数即可,如下:
frmUniqueForm.ShowUniqueWindow();
********************************
最后一个问题,如何合理的关闭窗体和程序。很多人关闭了窗体,发现程序进程还在,就不知道如何来操作了。大多数的问题,都是因为第一类问题而产生的连锁反应。所以我不建议使用Application.Exit来关闭程序,虽说C#写的是托管程序,内存的释放可以不用操心,但是好的编码习惯,有利于在编写复杂程序的时候能得心应手。
那么如何正确的关闭一个窗体或者一个程序呢。
如果不能正常关闭的原因是由于第一类问题造成的话,按照第一类的方法去修改窗体显示顺序,来达到合理的步骤。前期的正确,才能保证后期的能通过this.Close进行关闭窗体以及程序。
如果是子窗体要关闭连锁到主窗体关闭的话,这类问题也占一大部分,那么解决此类问题可以采用第二类问题所提到委托方法。
那么还有一些窗体关闭,程序没有正常关闭,是由于子线程没有关闭的问题,这部分留到线程汇总部分再说。
用C#写程序不难,如何编写正确的程序才是至关重要。此时再回过头看看前面所说的四类问题的解决方法,其实不难发现这些方法并没有用到特别深的技术,都是非常普通的方法。俗话说,平凡中见真知,只要把所学的方法正确应用到编码当中,那么你处理此类问题也能游刃有余。