WinForm問題及解決方法總結

本文轉自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#寫程序不難,如何編寫正確的程序纔是至關重要。此時再回過頭看看前面所說的四類問題的解決方法,其實不難發現這些方法並沒有用到特別深的技術,都是非常普通的方法。俗話說,平凡中見真知,只要把所學的方法正確應用到編碼當中,那麼你處理此類問題也能遊刃有餘。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章