WinForm问题及解决方法总结

来源:互联网 发布:淘宝网打折耐克女鞋 编辑:程序博客网 时间:2024/06/06 15:42

本文转自http://blog.163.com/da7_1@126/blog/static/104072678201202844831176/

Form问题是最基本的问题,因为编写WinApp程序首先接触的对象就是它,因此在论坛中对它而产生的问题也最常见。

Form相关的常见问题大致分为如下的四类问题。

第一类问题:如何控制窗体的显示顺序;

第二类问题:窗体之间的对象如何相互引用或操作;

第三类问题:如何处理窗体唯一性问题;

最后一个问题:如何合理的关闭窗体或程序。

接下来先说说如何控制窗体的显示顺序。

很多编程者常常会遇到这样的现象,例如,通过一个登录窗体去打开一个主窗体,然后要在主窗体中想关闭这个登录窗体。那么就有人出主意,你可以在打开主窗体的时候把登录窗体自身传进去,然后在主窗体中调用它的Hide方法来隐藏。虽说这样可以暂时达到你所要的效果,但不是最合理的解决办法。因为这样做有如下两个缺陷。

第一个就是,登录窗体已经完成使命,而资源没有得到及时释放;

其次就是,在窗体关闭的时候比较麻烦,需要找到登录窗口,关闭自身的同时要关闭登录窗体。

遇到此问题的时候,首要的是分析窗体打开的顺序以及相互关联的条件,常见的类型无非就是主子或者先后这两种。理解好第一点后,那么要学会合理使用ShowDialogDialogResult这两个好东西,前者属于模式打开窗体,后者属于窗体的返回值。

明白了这两点,就可以很方便的解决类似于登录窗体的问题,这方面的例子可以参看我的这篇文章。

***************************************************

最近,看到网上经常会问如何进行窗口跳转,大多数的问题都是牵扯到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#写程序不难,如何编写正确的程序才是至关重要。此时再回过头看看前面所说的四类问题的解决方法,其实不难发现这些方法并没有用到特别深的技术,都是非常普通的方法。俗话说,平凡中见真知,只要把所学的方法正确应用到编码当中,那么你处理此类问题也能游刃有余。

 

原创粉丝点击