WM_PAINT während Destruktor eines Fensters läuft



  • Ich habe hier ein lustiges Problem, wo Foo::OnPaint() aufgerufen wird, während der Destruktor von Foo gerade läuft.

    Wie es zustande kommt ist mir auch halbwegs klar: in Foo::~Foo werden der Reihe nach einige Member (scoped_ptr) resettet, und in einem Destruktor der dabei läuft steht

    #ifdef _DEBUG
        AfxMessageBox(L"blah");
    #endif
    

    Wenn man nun die Message-Box verschiebt, dann kommt ein WM_PAINT daher. Das Foo-Fenster selbst existiert noch, daher bekommt Foo ein OnPaint geschickt, in OnPaint werden dann bestimmte Zeiger dereferenziert (die aber bereits NULL gesetzt wurden), und es knallt.

    Mir fallen jetzt mehrere Möglichkeiten ein das zu handhaben:

    1. Destruktoren "dürfen" per Definition keine modalen Loops fahren (also kein MessageBox()/DoModal() o.ä.).
    2. Im Destruktor immer als erstes das eigene Fenster zerstören.
    3. Im Destruktor als erstes ein going_down Flag setzen, und alle Message-Handler mit if (going_down) return; absichern.

    (1) halte ich für kaum durchsetzbar, da Destruktoren auch gerne Funktionen/Destruktoren von anderen Libraries/Modulen aufrufen, und die halten sich dann u.U. nicht an die Regel.

    (2) ist IMO nicht immer durchführbar, z.B. nicht wenn das Fenster-Handle an andere Objekte übergeben wurde. Die anderen Objekte müssten dann zuerst zerstört werden -- vielleicht greifen sie ja beim Zerstören noch auf das Fenster-Handle zu.

    Bleibt nur noch (3), was für mich aber irgendwie nach Hack riecht.

    Frage: wie handhabt ihr solche Fälle?


  • Mod

    Ist Foo ein Fenster?
    Dann ist das eigentlich (meiner Meinungnach) nicht korrekt wie Du es machst.
    Die MFC sieht vor, dass ein Fenster nicht durch einen Destruktor sondern durch den Aufruf von DestroyWindow zerstört wird.
    Das Objekt selbst wird dann erst im PostNcDestroy (falls es im Heap liegt), oder durch die Zerstörung des Parents vernichtet.

    Wenn explizit DestroyWindow aufgerifen wird, kann das was Du hier schilderst eigentlich nicht passieren.
    Und dann können Destruktoren ruhig solche modalen Loops fahen. Das dürfte in diesem Falle niemanden stören, denn die Fenster sind zu diesem Zeitpunkt zu 100% entsorgt.

    BTW: Entsprechend gibt es einen Trace in CWnd::~CWnd der auf dieses Design hinweist.

    Also würde ich sagen:
    (4) In Destruktoren von Fenstern gehört niemals ein DestroyWindow...



  • Ja, Foo ist ein Fenster.

    Ich habe mir allerdings angewöhnt PostNcDestroy nicht zu verwenden, und statt dessen immer einen Destruktor zu schreiben der DestroyWindow aufruft. Sonst funktioniert die ganze RAII Geschichte nicht wirklich.

    Es ändert IMO auch nicht viel an der eigentlichen Problematik, sondern verschiebt diese nur nach Foo::DestroyWindow() bzw. Foo::OnDestroy() .

    Etwas vereinfacht dargestellt:

    Ich habe mein MainWindow (Foo). Das MainWindow erzeugt in seinem Konstruktor erstmal sich selbst, und danach eine Instanz eines Video-Players. Der Video-Player bekommt das Handle des MainWindow mitgegeben, und erzeugt ein Child-Fenster zum Video reinmalen:

    MainWindow::MainWindow()
    {
        // ...
    
        bool rc = !! CWnd::CreateEx(
            0, className.GetString(), _T("Foo"), m_normalWindowStyle,
            windowRect, 0, 0, 0);
    
        if (!rc)
            throw std::runtime_error(__FUNCTION__ ": Could not create main window.");
    
        // ...
    
        // m_videoPlayer ist ein boost::scoped_ptr
        m_videoPlayer.reset(new VideoPlayer(m_hWnd));
    }
    

    In OnPaint wird dann z.B. eine Member-Funktion des Video-Players aufgerufen:

    void MainWindow::OnPaint()
    {
        // ...
        m_videoPlayer->RefreshVideo(...);
    }
    

    Und im Destruktor steht folgendes:

    MainWindow::~MainWindow()
    {
        // ...
        m_videoPlayer.reset();
        // ...
        DestroyWindow();    
        // ...
    }
    

    Wenn nun der Destruktor von VideoPlayer indirekt MainWindow::OnPaint aufruft, dann ist dort m_videoPlayer bereits NULL und es knallt.

    Ich wüsste aber auch nicht wie ich das mit PostNcDestroy hinbekommen sollte.
    Der Video-Player gehört zerstört, bevor das eigentliche Fenster zerstört werden darf. Also müsste ich m_videoPlayer.reset nach MainWindow::DestroyWindow bzw. MainWindow::OnDestroy verschieben. Ändert aber nix, denn in dem Moment wo VideoPlayer::~VideoPlayer läuft, ist der m_videoPlayer Zeiger bereits NULL. Daher würde ein von VideoPlayer::~VideoPlayer ausgelöstes OnPaint probleme verursachen, egal wo und wann VideoPlayer::~VideoPlayer nun läuft.

    Im Destruktor von MainWindow ist es auf jeden Fall zu spät, denn zu dem Zeitpunkt wäre das Main-Window schon zerstört - ich hätte dann also dem Video-Player sein Parent-Fenster unterm Hintern weg zerstört.

    ps:

    Eine Möglichkeit das "sauber" zu lösen wäre eine Two-Phase Destruction des Video-Players, ala so:

    MainWindow::~MainWindow()
    {
        // ...
        m_videoPlayer->ShutDown(); // m_videoPlayer ist hier noch gültig, d.h. es knallt nicht mehr wenn MainWindow::OnPaint aufgerufen wird
        // ...
        DestroyWindow();    
        m_videoPlayer.reset(); // nachdem das Main-Window jetzt "weg" ist, kann im Destruktor des Video-Player MainWindow::OnPaint nicht mehr aufgerufen werden
    }
    

    Dazu müsste ich allerdings im Video-Player einen Zombie-Zustand einführen, in dem er nicht mehr vollständig initialisiert ist. Im Prinzip hab' ich dadurch das Problem also nur in die Video-Player Klasse verschoben. Dort muss dann ja z.B. die RefreshVideo Funktion auf diesen neuen Zombie-Zustand prüfen.


  • Mod

    Hmmm. So blöd es klingt, aber die CWnd Fenser wurdenicht für RAII gebaut.
    Weder werden Sie im Konstruktor erzeugt, noch im Destruktor zerstört.
    So blöde es klingt: Das Designe sieht das in keiner Weise vor, eben weil in der Zersörungsphase das Parent immer noch Nachrichten bekommt!
    Deshalb sind CWnd Fenster ja Objekte mit 2 Phasen Konstruktion und Destruktion!

    Würde Dein Programm m_videoPlayer DesroyWindow aufrufen (bevor der Zeiger geresettet wird) würde es auch kein Problem geben. Aber das hast Du selbst schon geschrieben.

    Du hast ein Problem, dass Du Zeiger zerstörst die eben übr Umwege noch benötigt werden. Also müsste Dein WM_PAINT dait leben können, dass bestmmte Zeiger in diesem Moment eben schon NULL sind, wenn Du auf RAII bauen willst.
    Das wäre ja auch kein Problem oder?
    Sprich wenn m_videoPointer nix mehr ist, dann wird eben OnPaint gleich verlassen.



  • Martin Richter schrieb:

    Du hast ein Problem, dass Du Zeiger zerstörst die eben übr Umwege noch benötigt werden. Also müsste Dein WM_PAINT dait leben können, dass bestmmte Zeiger in diesem Moment eben schon NULL sind, wenn Du auf RAII bauen willst.
    Das wäre ja auch kein Problem oder?

    Nö, wäre kein Problem.
    Mich hat hauptsächlich interessiert wie andere Leute mit sowas umgehen. Weil ich mir denke dass solche Dinge nicht ganz selten vorkommen. Und dann ist es meist gut, wenn man sich irgendeine Standard-Vorgehensweise zurechtlegt, die man in diesen Fällen anwendet.


  • Mod

    Ich halte mich strickt an die zwei Phasen, sowohl bei der Erzeugung, als auch beim Zerstören.



  • OK.
    Du hast dann aber auch Fälle wie das OnPaint bei mir, wo z.B. der Video-Player schon "halb" zerstört ist, und trotzdem noch eine Memberfunktion drauf aufgerufen wird, oder?
    Würdest du dann im Video-Player in RefreshVideo ein if (!m_hWnd) return; machen? Oder wie gehst du mit diesen Fällen um?


  • Mod

    Nein habe ich nicht.

    Das Childwindow wird zerstört (DestroyWindow). Das wiederum geschieht nur wenn DestroyWindow für das Parent aufgerufen wird.
    WM_DESTROY für das Parent wird ausgeführt.
    Ein Cleanup der Daten erfolgt hier noch nicht, damit komme ich auch noch klar, wenn das Child danach Callbacks machen sollte wenn es zerstört wird.
    Die Freigabe der Daten erfolgt erst in PostNCDestroy, denn erst jetzt kann ich sicher sein, dass alle Childs nichts mehr von mir wolen.

    Alternativ kann man natürlich auch die CHilds explizit zerstören und die Daten danach in OnDestroy abräumen.

    Ein Calback errreicht nun zwar mein OnPaint aber das stört mich nicht, obowohl in in der Zerstörung des Parents bin, weil alle Daten ja noch gültig sind. Muss ich nun auf ein Child zugreiffen (wasich eigentlich in OnPaint immer vermeide) ist natürlich vorher m_hWnd zu prüfen, außer es handelt sich um Daten für die ein m_hWnd auch NULL sein darf.

    EDIT: Habe ich vergessen. Eigentlich vermeideich auch Mesageboxen im OnDestroy und im Destruktor, warum sollte ich auch da welche haben wollen?
    Ist es ein Frame verwende ich WM_CLOSE dafür UIs gegebenenfalls auszuführen.

    Eigentlich wäre das der einfachste Weg solche Porbleme zu vermeiden. Aber man bekommt schon selbst solche eigentümliche Crashes die auf solch ein wiedereintreten in WM_PAINT liegen wenn nur ein ASSERT aufgeht wenn man debuggt und was schief geht in der Zerstörungsphase.



  • Wenn ich dich richtig verstehe, dann geht das nur, wenn auch das Child-Window mit der MFC gemacht ist, und auch von keinen Komponenten verwendet wird die nicht unter deiner Kontrolle stehen.

    In meinem Fall lege ich das Child-Window zwar noch in eigenem Code an, allerdings übergebe ich es (per Window-Handle) an eine andere Komponente, die ich nicht ändern kann (den eigentlichen Video-Player). Dieser verwendet es dann als Target-Fenster für den VMR9 (Windowless Mode).

    Diese andere Komponente (Video-Player) kann ich nur "single phase" zerstören, und ich muss in OnPaint() eine Funktion des Video-Player aufrufen, damit er das Video updaten tut (falls es z.B. gerade pausiert ist).

    D.h. ich werde wohl um ein "if (OK)" in OnPaint nicht herumkommen.

    ps:

    Die Message-Box hab' ich auch nur zu Debugging-Zwecken drinnen. Produktiv gibt's die nicht, aber ich mag nicht unbedingt immer auf Assertions auflaufen oder gar Access-Violations bekommen wenn sie angezeigt wird.


Anmelden zum Antworten