2010-01-15 46 views
12

更新:只是總結一下我的問題歸結爲:可以構建在後臺線程的形式,然後在UI線程顯示

我希望構建.NET窗體和控件沒有創建任何窗口句柄 - - 希望該過程被推遲到Form.Show/Form.ShowDialog

任何人都可以確認或否認這是否屬實?


我有一個很大的WinForms窗體與選項卡控件,許多窗體上的許多控件,加載了幾秒鐘後暫停。我將它縮小到InitializeComponent中的設計器生成的代碼,而不是構造函數或OnLoad中的任何邏輯。

我很清楚,我不能嘗試與除主UI線程以外的任何線程上的UI進行交互,但我想要做的是讓應用程序預加載此表單(運行構造函數),所以只要用戶想打開它,就可以立即顯示在UI線程上。然而,當構建在後臺線程,在這條線的設計:

this.cmbComboBox.AutoCompleteMode = System.Windows.Forms.AutoCompleteMode.Suggest; 

,我發現了錯誤

當前線程必須設置爲單 線程單元(STA)模式之前可以進行OLE 調用。確保你的主函數有標記的STAThreadAttribute 。

現在,這是一半,設計師的文件,這給了我希望,一般這個策略會奏效。但是這條線似乎試圖立即啓動某種OLE呼叫。

任何想法?

編輯:

我想我並沒有在這裏說清楚。延遲似乎發生在設計人員生成的代碼中的bazillion控件的構建過程中。

我的希望是,所有這些初始化代碼都是在沒有實際嘗試觸摸任何真正的Win32窗口對象的情況下發生的,因爲窗體尚未實際顯示。

事實上,我可以設置(例如)從這個後臺線程標籤文本和位置給了我希望這是真實的。但是,對於所有房產來說可能並非如此。

+0

是僅針對cmbox或所有控件引發的異常。 因爲如果是這樣,您可以將屬性設置爲最後一件事 – 2010-01-15 16:27:28

+0

只是爲了在組合框上設置AutoCompleteMode。在設計師的這個上面有很多代碼設置文本/名稱/位置/大小/等等。控制屬性。 – Clyde 2010-01-15 16:29:14

+0

雖然它是「預加載」表單,你的應用程序會做什麼?可能顯示「請稍候」的消息? – Codesleuth 2010-01-15 16:29:28

回答

3

答案是否定的。

如果您在GUI線程以外的任何線程上創建窗口句柄,則永遠無法顯示它。

編輯:完全可以創建窗體和控件,並將其顯示在主GUI線程以外的線程中。當然,如果你使用 ,你只能從創建它的線程 訪問多線程的GUI,但這是可能的。 - 阿什利亨德森

你需要在一個BG線程執行任何繁重的工作,然後將數據加載到您的GUI控件

+0

您能否確認一下.NET控件的構造函數實際上是否創建窗口句柄?有沒有關於這方面的任何文件? – Clyde 2010-01-15 17:41:46

+0

沒關係......我親眼看到手柄是在施工後填充的。 – Clyde 2010-01-15 18:58:44

+4

@Jan,這是不正確的。完全可以創建窗體並控制並在主GUI線程以外的線程中顯示它們。當然,如果你這樣做,你只能從創建它的線程訪問多線程的GUI,但它是可能的。 – Ash 2010-01-16 03:49:12

1

通常,表單的屬性需要從運行消息循環的同一個線程訪問。這意味着,爲了在另一個線程上構建表單,您需要編組任何調用以使用BeginInvoke實際設置屬性。如果構造函數最終生成需要處理的消息(就像現在發生的那樣),那麼對於構造函數的屬性集也是如此。

即使你得到這個工作,它會給你買什麼?它會稍微慢一點,總體來說不會更快。

也許只是顯示一個啓動畫面,而這種形式加載?

或者,查看爲什麼你的表單需要這麼長時間來構建。這是不常見的,需要幾秒鐘。

+0

這對我最有意義。查看MethodInvoker委託,瞭解如何執行此操作的示例。 bg線程正在做腿部工作,而GUI線程仍在獨立運行並接受用戶輸入。 – spoulson 2010-01-15 18:59:48

4

我認爲你的理解有點偏離。控件必須從創建它們的線程中觸及,而不是主UI線程。您可以在應用程序中擁有大量的UI線程,每個線程都有自己的一組控件。因此,在不同的線程上創建控件將不允許您在主線程中使用它,而無需封送使用Invoke或BeginInvoke的所有調用。

編輯 多線程UI一些文獻:

MSDN on Message Loops MSDN social discussion Multiple threads in WPF

+0

你能否提供這個說法的參考?我的理解是處理消息循環的相同線程需要更新控件。 – 2010-01-15 16:38:59

+0

正確....我想說的是我不打算更新任何屏幕上的元素。這是所有初始化代碼,表單尚未顯示。我只是試圖儘早讓.NET對象結構不受影響。 – Clyde 2010-01-15 16:46:47

+0

Eric - 您可以擁有N個消息循環,每個線程一個,控件具有線程親和力 Clyde - 創建.Net對象的動作創建基礎Windows對象,該對象綁定到創建它的線程。 – dsolimano 2010-01-15 16:56:55

14

雖然不可能在一個線程中創建一個表單,並使用顯示它另一個線程,它當然可以在非主GUI線程中創建表單。目前接受的答案似乎是說這是不可能的。

Windows窗體強制執行單線程公寓模型。總之,這意味着每個線程只能有一個Window消息循環,反之亦然。另外,例如,如果threadA想要與threadB的消息循環交互,則它必須通過諸如BeginInvoke之類的機制來封送該調用。然而,如果你創建一個新的線程併爲它提供它自己的消息循環,那麼這個線程將會愉快地獨立處理事件,直到它被告知結束消息循環爲止。

所以證明,下面是Windows窗體的代碼,用於創建和非GUI線程顯示錶單:

public partial class Form1 : Form 
{ 
    public Form1() 
    { 
     InitializeComponent(); 
    } 

    private void Form1_Load(object sender, EventArgs e) 
    { 
     label1.Text = Thread.CurrentThread.ManagedThreadId.ToString(); 

    } 

    private void button1_Click(object sender, EventArgs e) 
    { 
     ThreadStart ts = new ThreadStart(OpenForm); 

     Thread t = new Thread(ts); 
     t.IsBackground=false; 

     t.Start(); 
    } 

    private void OpenForm() 
    { 
     Form2 f2 = new Form2(); 

     f2.ShowDialog(); 
    } 
} 


public partial class Form2 : Form 
{ 
    public Form2() 
    { 
     InitializeComponent(); 
    } 

    private void Form2_Load(object sender, EventArgs e) 
    { 
     label1.Text = Thread.CurrentThread.ManagedThreadId.ToString() ; 

    } 
} 

OpenForm方法在一個新的線程中運行,並創建窗體2的實例。

Form2實際上是通過調用ShowDialog()給它自己單獨的消息循環。如果您要調用Show(),則不會提供消息循環,並且Form2會立即關閉。另外,如果您嘗試訪問OpenForm()內的Form1(例如使用'this'),您將在嘗試執行跨線程UI訪問時收到運行時錯誤。

t.IsBackground=false將線程設置爲前景線程。我們需要一個前臺線程,因爲後臺線程在主窗體關閉時沒有首先調用FormClosing或FormClosed事件就會立即終止。

除了這些點外,Form2現在可以像其他任何形式一樣使用。你會注意到Form1仍然像往常一樣運行自己的消息lopp。這意味着您可以單擊該按鈕並創建Form2的多個實例,每個實例都有自己的單獨消息循環和線程。

您確實需要注意跨表單訪問,它現在實際上是跨線程的。您還需要確保您處理關閉主窗體以確保任何非主窗體都能正確關閉。

+0

你對消息循環的理解是強大的,但真正旋轉了第二個消息循環(你怎麼做)通常不是一個好主意,因爲許多事情會變得混亂。像焦點,標籤,鍵盤和鼠標捕獲等。 – 2010-01-18 09:21:09

+0

Jan,是的,如果你想爲視頻,動畫等創建一個單獨的渲染窗口,那麼單獨的消息循環可能更有用。 – Ash 2010-01-18 09:28:39

1

我相信可以將在非UI線程上創建的組件添加到主UI,我已經完成了。

所以有2個線程'NewCompThread'和'MainThread'。

您關閉NewCompThread併爲您創建組件 - 所有準備顯示在MainUI上(在MainThread上創建)。

但是......如果你嘗試NewCompThread這樣的事情,你會得到一個異常: ComponentCreatedOnNewCompTHread.parent = ComponentCreatedOnMainThread;

但你可以補充一點:

if (ComponentCreatedOnMainThread.InvokeRequired) { 
    ComponentCreatedOnMainThread.Invoke(appropriate delegate...); 
} else { 
    ComponentCreatedOnNewCompTHread.parent = ComponentCreatedOnMainThread; 
} 

,它會工作。我已經完成了。
奇怪的事情(對我來說)是,然後ComponentCreatedOnNewCompTHread'認爲'它是在MainThread上創建的。

如果在從NewCompThread如下: ComponentCreatedOnNewCompTHread.InvokeRequired 它將返回TRUE,而你需要創建一個委託並使用調用找回了MainThread。

0

在後臺線程中創建控件是可能的,但只能在STA線程上。

我以與異步使用該創建的擴展方法/等待模式

private async void treeview1_AfterSelect(object sender, TreeViewEventArgs e) 
{ 
    var control = await CreateControlAsync(e.Node); 
    if (e.Node.Equals(treeview1.SelectedNode) 
    { 
     panel1.Controls.Clear(); 
     panel1.Controls.Add(control); 
    } 
    else 
    { 
     control.Dispose(); 
    } 
} 

private async Control CreateControlAsync(TreeNode node) 
{ 
    return await Task.Factory.StartNew(() => CreateControl(node), ApartmentState.STA); 
} 

private Control CreateControl(TreeNode node) 
{ 
    // return some control which takes some time to create 
} 

這是擴展方法。任務不允許設置公寓,所以我在內部使用線程。

public static Task<T> StartNew<T>(this TaskFactory t, Func<T> func, ApartmentState state) 
{ 
    var tcs = new TaskCompletionSource<T>(); 
    var thread = new Thread(() => 
    { 
     try 
     { 
      tcs.SetResult(func()); 
     } 
     catch (Exception e) 
     { 
      tcs.SetException(e); 
     } 
    }); 
    thread.IsBackground = true; 
    thread.SetApartmentState(state); 
    thread.Start(); 
    return tcs.Task; 
}