Laden...

Waitform in extra Thread

Erstellt von theflyer vor 15 Jahren Letzter Beitrag vor 15 Jahren 10.478 Views
T
theflyer Themenstarter:in
15 Beiträge seit 2007
vor 15 Jahren
Waitform in extra Thread

[EDIT]Abgeteilt von MdiFenster mit unterschiedlichen Threads[EDIT]

Habe just so ein ähnliches Problem

Wir haben eine globale Waitform, also so ein "Bitte Warten" Fenster mit einer netten Animation darin.

Leider machen die meisten Progger bei uns das Fenster auf, führen ihre Berechnungen im selben GUI Thread aus, so das die Animation steht und schließen danach das Fenster wieder.

Wäre es aber nicht möglich, die Waitform in einen eigenen Thread zu erzeugen und keinen Parent zuzuweisen, so das es völlig autark ist. Nur die Show/Hide Methoden müßten dann noch über Invoke aufgerufen oder überschrieben werden.

Oder denke ich da falsch X( ?

Gruss
theflyer

3.430 Beiträge seit 2007
vor 15 Jahren

Hi,

Wäre es aber nicht möglich, die Waitform in einen eigenen Thread zu erzeugen und keinen Parent zuzuweisen, so das es völlig autark ist. Nur die Show/Hide Methoden müßten dann noch über Invoke aufgerufen oder überschrieben werden.

Sofern es sich um ein eigenständiges Form handelt wäre das schon möglich.
Aber das ist doch nicht der Sinn der ganzen Sache.
Denn die GuiElemente sollen immer alle im selben Thread liegen.
Deshalb würde ich dieses Fenster mit dem GUI-Thread erstellen.

Anschliessend führst du deine Berechnungen in einen weiteren Thread aus.
Sobald dieser seine Aufgabe erledigt hat, rufst du eine Methode auf, die das Loading-Fenster über Invoke schliesst.

mfg
michlG

49.485 Beiträge seit 2005
vor 15 Jahren

Hallo theflyer,

im Prinzip könntest du das Waitform komplett in einen Thread auslagern (der dann natürlich selbst Application.Run ausführen muss). Und dann läuft das Wait-Form unabhängig von dem eigentlichen GUI-Thread und muss das auch. Es darf dann (ohne Control.Invoke) keine Zugriffe von dem eigentlichen GUI-Thread auf das Wait-Form geben und umgekehrt. Inbesondere darf der Owner des Wait-Forms nicht auf ein Fenster im eigentlichen GUI-Thread gesetzt werden.

Nur der Effekt der ganzen Aktion ist immer noch begrenzt. Denn es wird nur die Blockierung des Wait-Forms verhindert. Wenn im eigentlichen GUI-Thread langlaufende Aktionen ausgeführt werden, dann blockiert das eigentliche GUI weiterhin. Mit alle Schikanen, also inkl. nicht aktualisierten Fenster, "Keine Rückmeldung" im Taskmanager usw.

Daher ist es dringend zu empfehlen, die "Progger" bei dir anzuhalten, die Regeln der GUI-Programmierung einzuhalten. Also inbesondere keine langlaufenden Aktionen im GUI-Thread auszuführen. Siehe [FAQ] Warum blockiert mein GUI?

herbivore

T
theflyer Themenstarter:in
15 Beiträge seit 2007
vor 15 Jahren

Hi,

Aber das ist doch nicht der Sinn der ganzen Sache.
Denn die GuiElemente sollen immer alle im selben Thread liegen.

das ist natürlich richtig, das Problem ist, das das Fenster an 50 verschiedenen Stellen aufgerufen wird.
Das Auslagern in eigene Threads hat natürlich auch einige Änderungen in der Codelogik zur Folge. Und dann kommt man in Erklärungsnöte, wenn ein Programm was sonst einigermaßen läuft, haufenweise Fehler produziert, nur weil du ein nettes Gimmick einbauen wolltest....

Gruss
theflyer

3.430 Beiträge seit 2007
vor 15 Jahren

Hi,

Und dann kommt man in Erklärungsnöte, wenn ein Programm was sonst einigermaßen läuft, haufenweise Fehler produziert, nur weil du ein nettes Gimmick einbauen wolltest....

ja, das stimmt. Denn wenn ein Programm nicht von Anfang an gut strukturiert wird, dann wird es sehr schwierig.
Es würde zwar einiges an Aufwand bedeuten, das alles umzubauen, das würde sich aber auf jeden Fall lohnen.

Denn momentan wird das Programm wahrscheinlich auch nicht wirklich stabil laufen. Versuche mal, wenn das Ladefenster geöffnet ist darauf zu klicken. Wenn sich das Programm dabei aufhängt (was ich vermute) dann kannst du die anderen Programmierer sicher von deinem Vorhaben überzeugen.

mfg
michlG

915 Beiträge seit 2006
vor 15 Jahren

Hrm, einen eigenen GUI Thread kannst du z.B. wie folgt erzeugen:


private static WPopUp m_wndPopUp;

        private WPopUp()
        {
            // register components
            this.InitializeComponent();
        }

        private static void Init()
        {
            if (m_wndPopUp == null)
            {
                m_wndPopUp = new WPopUp();
                Application.Run();
            }
        }
        /// <summary>
        /// 
        /// </summary>
        public static void ShowAsGUIThread()
        {
            new Thread(new ThreadStart(Init)).Start();
        }

Hier wird verhindert das ausser über die statische Methode ShowAsGUIThread das Popup (WaitForm) nicht initalisiert werden kann und ebenso wird damit überprüft ob das Form bereits schon instanziert ist.


public static IPopUp Instance
        {
            get { return m_wndPopUp; }
        }

Sollte man das WaitForm von aussen ansprechen wollen, so kann man auf nur die Threadsicheren Methoden zugreifen indem man die Instanz über eine Schnittstelle anspricht.

Denke so solltest die meisten überzeugen können das nichts schief geht. Ansonsten muss nur noch das von herbivore angesprochene beachtet werden. Man kann aber auch etwas schummeln sollte Quellcode vorhanden sein den man absolut nicht verändenr darf / kann der blockiert indem man folgendes ausprobiert:


RECT rec = new RECT();
					GetWindowRect(hWnd, ref rec);
					RedrawWindow(hWnd, ref rec, IntPtr.Zero, (uint)RDW_VALIDATE | RDW_UPDATENOW);

Via Win API kann man das entsprechende Fenster im WaitForm mit RedrawWindow neu Zeichnen lassen - selbst wenn dort im GUI Thread ein längerer Prozess am rumwerkeln ist. Allerdings ist das nen ziemlich schlechter stiel 😉

Wie vernichtet stand Andreas unter den flammenden Augen seiner Kunden.
Ihm war's, als stünde des Schicksals dunkle Wetterwolke über seinem Haupte X(

R
206 Beiträge seit 2007
vor 15 Jahren

Guten Tag,

schau dir mal [gelöst] Datenabruf aus DB soll in Thread ausgelagert werden an...

Gruß,
Max

T
theflyer Themenstarter:in
15 Beiträge seit 2007
vor 15 Jahren
Es geht, aber....

.... es ist aber ziemlich mühselig...

Bin mit Andreas Beispiel angefangen, und landete gleich auf den Bauch:
Ich konnte das Application.Run nicht einbauen, da es schon an anderer Stelle gestartet war, also erzeugte der Thread das Fenster und stoppte am Ende der Init-Funktion. Dadurch zeichnete sich das Fenster nicht mehr neu und ließ sich auch nicht mehr ansprechen.

Also hielt ich den Thread in einer while-Schleife am Leben.
Nach dem Umbau sah der Code dann so aus:


private void InternalShow()
{
lblDescription.Text = Message;
Visible = true;

while (Visible)
{
Refresh();
Thread.Sleep(50);
}
}


/// <summary>
/// Zeigt das Wartefenster mit einer Meldung an
/// </summary>
/// <param name="Text">Der Text, der angezeigt werden soll.</param>
public static void Show( string Text )
{
if (_Instance == null)
{
_Instance = new StatusForm();
_Instance.Message = Text;
new Thread(new ThreadStart(_Instance.InternalShow)).Start();
}
}

public new static void Hide()
{
MethodInvoker UIDelegate = new delegate
{
Close();
}
Invoke( UIDelegate);
}

Die Eingangsroutine ist die statische Methode Show.
Die prüft zunächst, ob schon eine Instanz dieses Fensters erzeugt ist ( wollte sichergehen, das es immer nur ein Fenster gibt ), speichert den zukünftigen Text in der Variable Message. Den Text konnte ich erst innerhalb des Threads zuweisen, da sonst möglicherweise schon ein Fensterhandle im falschen Thread erzeugt wird. Danach wird der Thread gestartet.
Die Threadmethode macht das Fenster sichtbar, weist den Text zu und kurvt in einer Endlosschleife, bis von außen die Methode Hide kommt und das Fenster schließt.
Das ganze dann kompiliert und ausgeführt, das Fenster wurde sichtbar und eine Animation ( die hier nicht weiter von belang ist ) liefen prima. Also Fenster wieder schließen und .... nichts tat sich mehr, das Programm blieb hängen.
Ich fand heraus, das es beim Invoke hängenblieb. Genau weiß ich nicht warum, ich vermute das der Thread gerade schläft, wenn ich das Invoke aufrufe.
Also kam ich auf eine nicht sonderlich elegante Methode: Ich baute Hilfsvariablen wie ShouldClose und ShouldVisible ein, die dann Problemlos aus anderen Threads gesetzt werden konnten und dann in der Threadschleife behandelt wurden. Die Methode Hide sah dann so aus:


/// <summary>
/// Schließt das Statusfenster wieder
/// </summary>
public static new void Hide()
{
if( _Instance!=null )
_Instance._ShouldClose = true;
}

Die Show Routine blieb wie sie war, nur meine Internalshow änderte sich drastisch


private void InternalShow()
{
lblDescription.Text = Message;
Visible = true;

while (_ShouldClose==false)
{
if (lblDescription.Text != Message)
lblDescription.Text = Message;

if (_ShouldTopMost != TopMost)
TopMost = _ShouldTopMost;

if (_ShouldVisible != Visible)
Visible = _ShouldVisible;

Refresh();
Thread.Sleep(50);
}
_Instance = null;
Close();
Dispose();
}

Bei jedem Schleifendurchlauf wurde geprüft, ob sich der Text verändert hatte. TopMost gibt an, das das Fenster immer in Vordergrund bleibt und das ShouldVisible gibt an, ob das Fenster vorübergebend unsichtbar gemacht werden soll. Warum ShouldVisible und TopMost ? Beim Erzeugen des Fensters wird dem Wait-Fenster kein Parent mitgegeben, da es für sich alleine laufen muss und nichts mit der UI-Thread zu tun haben durfte.
Leider hat die Sache nur einen Schönheitsfehler: Da die Fenster nichts miteinander zu tun haben, überdecken sie sich gegenseitig, je nachdem wer den Focus bekommt. Ich konnte das WaitFenster zwar TopMost machen, dadurch war es aber immer sichtbar, auch wenn ein ganz anderes Programm im Vordergrund ist.
Kurzum, so völlig autark konnte das Fenster nun auch wieder nicht laufen, aber den Parent konnte ich dem Fenster ja auch nicht zuweisen.
Die Lösung war dann, das ich die HauptMessage-Schleife des Elternfenster auf relevante Ereignisse abhöre. Das sieht dann so aus


// <summary>
/// Zeigt das Wartefenster mit einer Meldung an
/// </summary>
/// <param name="Text">Der Text, der angezeigt werden soll.</param>
public static void Show( Form Parent, string Text )
{
if (_Instance == null)
{
_Instance = new StatusForm();
_Instance.Message = Text;
_Instance.SetParent(Parent);
new Thread(new ThreadStart(_Instance.InternalShow)).Start();
}
}

Die Methode Show unterscheidet sich zu vorher nur in der zusätzlichen Methode SetParent


private void SetParent(Form NewParent)
{
_ParentWndProc = new WndProcCallBack(Parent_WndProc);
AssignWndProc( NewParent);

_Parent = NewParent;
}

protected virtual void AssignWndProc( Form Parent )
{
_OrgWndProc = Win32.SetWindowLong(Parent.Handle, -4, _ParentWndProc);
}

protected virtual void ReleaseWndProc(Form Parent)
{
Win32.SetWindowLong(Parent.Handle, -4, _OrgWndProc);
}

Die Setparent prüft ruft AssignWndProc auf und speichert den Parent Objektverweis in eine Variable.
AssignWndProc besteht nur aus einer einzigen Zeile, der API Methode SetWindowLong. Diese Methode leitet alle Nachrichten vom Elternfenster in die Methode Parent_WndProc um.


protected IntPtr Parent_WndProc(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam)
{
IntPtr Result = Win32.CallWindowProc(_OrgWndProc, hWnd, Msg, wParam, lParam);

switch (Msg)
{
case Win32.WM_NCACTIVATE:
_ShouldTopMost = (int)wParam == 1 ? true : false;
break;

case Win32.WM_CLOSE:
_ShouldClose = true;
break;

case Win32.WM_WINDOWPOSCHANGED:
Win32.WINDOWPOS wp = new Win32.WINDOWPOS();
wp = ( Win32.WINDOWPOS )Marshal.PtrToStructure(lParam, typeof(Win32.WINDOWPOS));

_ShouldVisible = (wp.x != -32000); // Hauptfenster minimiert

if (_ShouldVisible)
_ShouldTopMost = true;

break;
}

return Result;
}

Diese Funktion horcht nun, ob das Fenster deaktiviert wird ( WM_NCACTIVATE ) und schaltet dann das Topmost ein oder aus.
Wird das Elternfenster geschlossen ( WM_CLOSE ), wird das Flag zum Schließen des Waitfensters gesetzt. Beim Minimieren ( WM_WINDOWPOSCHANGED )
muss natürlich auch das Waitfenster unsichtbar werden ( ShouldVisible )

So, das ganze ist ein bißchen länger geworden. Ich habe das deshalb mal so ausführlich geschrieben, da ich mit dem Design gar nicht zufrieden bin, mir aber auch nichts besseres einfiel. Aber vielleicht hat ja der ein oder andere noch eine Idee...

Gruß
Stefan

49.485 Beiträge seit 2005
vor 15 Jahren

Hallo theflyer,

ich habe nicht alles gelesen, nur den Anfang von mehrfach aufgerufenen Application.Run und while-Schleifen, um Threads am leben zu halten. Alles Mist. Siehe [FAQ] Warum blockiert mein GUI? für eine korrekte Aufteilung der Anwendung in Threads, die den ganzen Mist unnötig macht.

Es ist ganz einfach: Das GUI sollte komplett in einem Thread laufen. Alle langlaufenden Aktionen müssen in einen (oder mehrere) Thread(s) ausgelagert werden.

herbivore

915 Beiträge seit 2006
vor 15 Jahren

Hrm, habe mir den Code mal angesehen und leide rbist immer noch auf dem selben GUI Thread, darum konntest auch kein Application.Run() verwenden. Und deshalb brichst dir auch einen ab bei der gesammten Kommunikation.

Hier mal nen Copy&Paste Beispiel.
Mach dafür einfach mal nen neues Projekt als Windows Forms Application - standardmäßig wird Form1 erstellt und die Klasse Programm, in der Klasse Programm wird in Main via Application.Run(new Form1()); aufgerufen.

Erstell nun einfach eine zusätzliche Form2 und kopiere den Code mal rein.


	public partial class Form2 : Form
	{
		private static Form2 m_frm2 = null;

		private static void Init()
		{
			if (m_frm2 == null)
			{
				Application.Run(new Form2());
			}
		}
		/// <summary>
		///
		/// </summary>
		public static void ShowAsGUIThread()
		{
			new Thread(new ThreadStart(Init)).Start();
		}

		private Form2()
		{
			InitializeComponent();
		}

	}

Geh dann in Form1 und kopiere diesen Code mal rein:


		public Form1()
		{
			InitializeComponent();
			Form2.ShowAsGUIThread();
		}

Und wenn nun das ganze mal debbugst und mit Spy++ ansiehst wirst sehen beide haben andere Threads gehören aber zum selben Prozess.

Wie gesagt der Trick um nun mit Form2 zu Komunizieren besteht darin Form2.Instance ein Statisches Propertie aufzurufen das statt Form2 zurückzugeben, nur ein Interface zurückgibt mit begrenzten Methoden und Properties. Wie die Properties bzw. Methoden dann angesprochen werdne müssen sieht über den Link von herbivore.

Einen eigenen GUI Thread zu erzeugen ist somit ganz einfach, es kommt immer nur darauf an ob dieser einen Sinn hat oder nicht.

Wie vernichtet stand Andreas unter den flammenden Augen seiner Kunden.
Ihm war's, als stünde des Schicksals dunkle Wetterwolke über seinem Haupte X(

49.485 Beiträge seit 2005
vor 15 Jahren

Hallo Andreas.May,

Einen eigenen GUI Thread zu erzeugen ist somit ganz einfach, ...

es ist auch ganz einfach ein Programm mit gotos zu schreiben, viel einfacher als ein Programm nur mit strukturierten Konstrukten wie if, while, ..., weil man eben beliebig springen kann, und damit weniger Einschränkungen unterliegt, ...

... es kommt immer nur darauf an ob dieser einen Sinn hat oder nicht.

... nur macht das keinen Sinn. Genauso ist das mit mehreren GUI-Threads. Mehrere GUI-Threads machen keinen Sinn. Insofern ist es genausowenig hilfreich zu zeigen, wie man mehrere GUI-Threads erstellt, wie zu zeigen, wie man mit goto programmiert. Das ist allenfalls von theoretischem, aber keinesfalls von praktischem Interesse. Zudem zeugt deine Umsetzung mit der statischen Property Form2.Instance, sorry, nicht gerade von guten Programmierstil.

herbivore

F
10.010 Beiträge seit 2004
vor 15 Jahren

@theflyer:

Eine "einfachere" Lösung ist es, dem WaitForm einfach einen delegaten mitzugeben,
der dann in einem extraThread ausgeführt wird.

T
theflyer Themenstarter:in
15 Beiträge seit 2007
vor 15 Jahren

Hallo Andreas,

habe das Programm mal umgebaut, hat prima geklappt. Konnte dadurch die ganzen Hooks und Hilfsvariablen wieder rauswerfen. Allerdings muss die Events vom Parent immer noch extra behandelt werden.

Ich teile nicht die Ansicht von herbivore, es gibt Situationen, da macht es Sinn.
Eine statische Instance finde ich auch keinen schlechten Programmstil, sondern eine Notwendigkeit, wenn ein Objekt oder Fenster nur einmal existieren soll.

Ich habe das Projekt mal mit drangehangen, vielleicht hilft es ja den ein oder anderen.
Bitte beachtet aber, das dies eigentlich nicht der richtige Weg ist. Normalerweise gehören die langandauerende Prozesse in einen extra Thread gepackt und nicht die GUI Aktualisierung. Manchmal fehlt jedoch der Source-Code oder - wie bei uns - ist der Aufwand einer Codeänderung zu hoch.
Im Projekt wird ein AnimationsControl von Codeprojekt benutzt. Wenn jemand deswegen bedenken hat, kann man sich den Quellcode bei Codeprojekt selbst runterladen How to write a loading circle animation.

Gruß
Stefan

Gelöschter Account
vor 15 Jahren

Eine statische Instance finde ich auch keinen schlechten Programmstil, sondern eine Notwendigkeit, wenn ein Objekt oder Fenster nur einmal existieren soll.

eine instanz eines objektes das nur einmal exitieren soll nennt man singleton und ja in seltenen fällen macht es sinn aber bei einer form macht es keinen sinn.

genauso teile ich die ansicht mit herbivore und hunderten von entwicklern, das mehrere gui threads genausowenig sinn machen. unter anderem sollen jegliche userinteraktionen immer nur im gui-thread stattfinden und das ist auch die ansicht von microsoft und wiederum von hunderten von entwicklern.

das was du hier gemcht hast nennt man "dirty hack" weil es das problem nciht beseitigt sondern umgeht und das wiederrum macht meistens zu späteren zeitpunkten probleme ungeahnten außmases.

T
theflyer Themenstarter:in
15 Beiträge seit 2007
vor 15 Jahren

eine instanz eines objektes das nur einmal exitieren soll nennt man singleton und ja in seltenen fällen macht es sinn aber bei einer form macht es keinen sinn.

Danke, ich habe das Buch von Gamma/Vlissides auch. Bei dieser Form macht es schon Sinn. So wird sichergestellt, das es immer nur ein "Warten-Fenster" gibt, egal aus welcher Dll es aufgerufen wird.

das was du hier gemcht hast nennt man "dirty hack" weil es das problem nciht beseitigt sondern umgeht und das wiederrum macht meistens zu späteren zeitpunkten probleme ungeahnten außmases.

Den dirty hack muss ich mir wohl gefallen lassen, aber die konsequenzen an 50 Stellen die Codelogik zu ändern, wiegt weit schwerer. Natürlich fällt es leicht den Zeigefinger zu erheben und zu sagen: "Das habt Ihr aber nicht sauber geplant". Aber die ersten Module sind 2002 entstanden, da hatte kaum einer was von Illegal Cross Threading gehört und uns ging es damals in erster Linie darum, die Geschäftsprozesse sauber hinzubekommen und nicht was in "irgendeinen" Wartenfenster passiert.

49.485 Beiträge seit 2005
vor 15 Jahren

Hallo theflyer,

Aber die ersten Module sind 2002 entstanden, da hatte kaum einer was von Illegal Cross Threading gehört ...

den Einwand würde ich nicht so ohne Weiteres gelten lassen. Schon Win32 war single threaded und schon unter Win32 (also ab Windows 95) waren Threads nötig, um das Blockieren des GUIs sicher zu verhindern. Windows-Forms basiert ja auch Win32 und hat daher die "Einschränkung", dass alle GUI-Zugriffe aus dem GUI-Thread erfolgen müssen, geerbt.

Es bestand also "schon immer" die Notwendigkeit mehrere Threads zu verwenden und gleichzeitig waren "schon immer" threadübergreifende Zugriffe verboten.

Insofern musst du es dir schon gefallen lassen, wenn ich sage, dass ihr euch offensichtlich damals nicht ausreichend informiert habt. Leider passiert das auch heute immer noch, dass Leute einfach anfangen zu programmieren, ohne sich vorher ausreichend mit den Grundlagen zu beschäftigen.

Versteh mich nicht falsch. Ich will dich hier nicht an den Pranger stellen und ich habe Verständnis dafür, dass man in einer verfahrenden Situationen manchmal zu "dirty hacks" greifen muss, um den Aufwand in vertretbaren Grenzen zuhalten.

Nur habe nicht das geringste Verständnis dafür, dass du dein Dein Projekt hier als (gute?) hilfreiche Lösung präsentiert. Dadurch entsteht der Eindruck, es wäre ok, das so zu machen, wie du vorschlägst. Und das ist es eben nur, wenn man so im Schlamassel steckt, wie du das tust.

Wenn man ein neues Projekt angeht, sollte man von vorneherein und konsequent, die Regeln in [FAQ] Warum blockiert mein GUI? und [FAQ] Controls von Thread aktualisieren lassen (Control.Invoke) einhalten. Also insbesondere nur einen GUI-Thread verwenden und langlaufenden Aktionen (und nur die) in extra Threads auszulagern.

herbivore

915 Beiträge seit 2006
vor 15 Jahren

Na ja die Vorposter haben alle sammt schon recht.

Nur ganz ehrlich, greife manchmal lieber zu nen kleinen Trick als das mich dran mache ganze Programme umzuschreiben die ich zumeist nichtmal selber Programmiert habe und teilweise zwischen C++ und C# kunterbunt hin und herwechseln. Ich denke genau das ist halt auch hier der Punkt.

Denke darum war auch die Frage "eigener GUI" Thread, ich beantworte nur Fragen so gut wie ich kann und wie es die Zeit erlaubt 🙂

Wie vernichtet stand Andreas unter den flammenden Augen seiner Kunden.
Ihm war's, als stünde des Schicksals dunkle Wetterwolke über seinem Haupte X(