Inkonsistentes Verhalten von wxTextCtrl in wxFrame?

Plattformunabhängige GUIs mit wxWidgets.
Antworten
Benutzeravatar
Humbalan
User
Beiträge: 59
Registriert: Mittwoch 2. September 2009, 15:11

Hallo,

ich validiere in einem Dialog Werte in TextCtrls und gebe via MessageDialog eine Fehlermeldung aus, wenn die Validierung fehlschlägt. Dabei bekomme ich das Verhalten der TextCtrls nicht in den Griff. Der Cursor verschwindet nach einer Fehlermeldung und der Focus ist auch nicht da, wo ich glaube, ihn gesetzt zu haben. Habe das Programm auf ein Minimum gestutzt, um nur die Fehlerumgebung testen zu können. Hier der Code (kurze Erklärung dazu weiter unten):

Code: Alles auswählen

# -*- coding: iso-8859-15 -*-
import wx

class CDlgBlahBase ( wx.Dialog ):

    def __init__( self, parent ):
        # Definiert das UI des Dialogs

        wx.Dialog.__init__ ( self, parent, style = wx.CAPTION|wx.DEFAULT_DIALOG_STYLE )

        self.SetSizeHintsSz( wx.DefaultSize, wx.DefaultSize )

        bSizer_blah = wx.BoxSizer( wx.VERTICAL )

        gbSizer_blah = wx.GridBagSizer( 0, 0 )
        gbSizer_blah.SetFlexibleDirection( wx.BOTH )
        gbSizer_blah.SetNonFlexibleGrowMode( wx.FLEX_GROWMODE_SPECIFIED )

        bSizer_test = wx.BoxSizer( wx.HORIZONTAL )

        bSizer_testname1 = wx.BoxSizer( wx.VERTICAL )
        self.m_staticText_testname1 = wx.StaticText( self, wx.ID_ANY, u"Test 1" )
        bSizer_testname1.Add( self.m_staticText_testname1, 0, wx.ALIGN_LEFT|wx.BOTTOM, 3 )
        self.m_textCtrl_testname1 = wx.TextCtrl( self, wx.ID_ANY, u"Test 1", name=u"testname1" )

        bSizer_testname1.Add( self.m_textCtrl_testname1, 0, wx.BOTTOM|wx.LEFT, 3 )
        bSizer_test.Add( bSizer_testname1, 1, 0, 5 )

        bSizer_testname2 = wx.BoxSizer( wx.VERTICAL )
        self.m_staticText_testname2 = wx.StaticText( self, wx.ID_ANY, u"Test 2" )
        bSizer_testname2.Add( self.m_staticText_testname2, 0, wx.ALIGN_LEFT|wx.BOTTOM, 3 )
        self.m_textCtrl_testname2 = wx.TextCtrl( self, wx.ID_ANY, name=u"testname2" )

        bSizer_testname2.Add( self.m_textCtrl_testname2, 0, wx.BOTTOM|wx.LEFT, 3 )
        bSizer_test.Add( bSizer_testname2, 1, wx.EXPAND, 5 )

        gbSizer_blah.Add( bSizer_test, wx.GBPosition( 0, 0 ), wx.GBSpan( 1, 2 ), wx.ALL|wx.EXPAND, 5 )

        bSizerButtons = wx.BoxSizer( wx.HORIZONTAL )
        self.m_button_ok = wx.Button( self, wx.ID_OK, u"OK", wx.DefaultPosition, wx.DefaultSize, 0 )
        bSizerButtons.Add( self.m_button_ok, 0, wx.ALL, 5 )
        self.m_button_cancel = wx.Button( self, wx.ID_CANCEL, u"Abbrechen" )
        bSizerButtons.Add( self.m_button_cancel, 0, wx.ALL, 5 )

        gbSizer_blah.Add( bSizerButtons, wx.GBPosition( 1, 0 ), wx.GBSpan( 1, 1 ), wx.ALIGN_RIGHT|wx.BOTTOM|wx.TOP, 5 )

        bSizer_blah.Add( gbSizer_blah, 1, wx.ALL|wx.EXPAND, 5 )

        self.SetSizer( bSizer_blah )
        bSizer_blah.Fit( self )


class CMyDlg(CDlgBlahBase):

    def __init__( self, parent ) :
        CDlgBlahBase.__init__( self, parent )
        self._defaultBgCol = self.m_textCtrl_testname1.GetBackgroundColour()
        self.m_textCtrl_testname1.Bind( wx.EVT_KILL_FOCUS, self.on_kill_focus_testname )
        self.m_textCtrl_testname2.Bind( wx.EVT_KILL_FOCUS, self.on_kill_focus_testname )

        # Zur Verhinderung einer Fehlermeldung, wenn aufgrund einer Eingabe z. B. das Feld testname1 den Fokus
        # an testname2 abgibt, dann ein Fehler-Popup den Fokus von testname2 killt.
        self._fail_dict = {'test1':False, 'test2':False}


    def set_focus( self, field ):
        field.SetFocus()


    def on_kill_focus_testname( self, event ) :

        field = event.GetEventObject()

        # Vorbereitungen
        if field == self.m_textCtrl_testname1 :
            test_no = 1
            check_fail = u'test2'
        elif field == self.m_textCtrl_testname2 :
            test_no = 2
            check_fail = u'test1'
        else :
            assert False

        if self._fail_dict[check_fail] :
            # Zur Verhinderung von doppelten Fehlermeldungen (siehe Kommentar von self._fail_dict)
            event.Skip()
            return

        if field.GetValue( ) == u'' :
            # Fehler: das Feld darf nicht leer sein
            self._fail_dict[u'test{}'.format(test_no)] = True
            field.SetBackgroundColour( u'pink' ) # Fehler wird sichtbar
            # Leider ohne Quelle: habe irgendwo gelesen, dass SetFocus() nicht innerhalb eine kill_focus()-Methode
            # aufgerufen werden soll:
            wx.CallAfter( self.set_focus, field )
            # Fehlermeldung.
            wx.MessageDialog( self, u'Testname {} fehlt'.format(test_no), u'Fehler', wx.OK|wx.ICON_ERROR ).ShowModal()
        else :
            # Aufräumen, wenn der Fehler beseitigt ist.
            self._fail_dict[u'test{}'.format(test_no)] = False
            field.SetBackgroundColour( self._defaultBgCol )

        self.Refresh()
        event.Skip()

###################################################################################################################
class CTest( wx.Frame )  :
    def __init__( self, parent ) :
        wx.Frame.__init__( self, parent )
        CMyDlg(self).ShowModal()

if __name__ == "__main__":
    app = wx.App( redirect = False )
    CTest( wx.Frame(None) )
Kurze Erklärung: Die Klasse CDlgBlahBase dient nur zur Definition des UI. Sie erzeugt einen Dialog mit zwei TextCtrl und einer OK- und einer Abbrechen-Taste. Der Ablauf ist in CMyDlg. Die Methode on_kill_focus_testname() ist mit beiden Dialogfeldern verknüpft und soll einen Fehler melden, wenn beim Verlassen eines Dialogfeldes das Feld leer ist. Im Prinzip funktioniert das auch, nur gibt es im gezeigten Code nach der Validierung zwei Fehler:

(1) Mißlang die Validierung, weil das Feld beim Fokusverlust leer war, dann ist in diesem Feld nach Ausgabe der Fehlermeldung der Cursor verschwunden :K .
(2) Ist das Feld testname2 bei Fokusverlust leer, dann gilt (1) und zusätzlich ist der Fokus nicht wie vermutet in testname2, sondern testname1 :K .

Punkt (2) scheint mir besonders seltsam, da der Code genau der gleiche ist aber für die beiden Dialogfelder je unterschiedliches Verhalten hervorruft. Streiche ich den Aufruf von MessageDialog(), dann ist alles funzt es (nur kann ich leider auf die Fehlermeldung nciht verzichten).

Ich arbeite mit Python 2.7.2, wxPython 3.0.2.0 und Win 8.1

Kann jemand helfen?

Schönene Dank schon mal und Beste Grüße aus München
Humbalan
Zuletzt geändert von Anonymous am Dienstag 1. Dezember 2015, 15:05, insgesamt 1-mal geändert.
Grund: Quelltext in Python-Codebox-Tags gesetzt.
Piet Lotus
User
Beiträge: 80
Registriert: Dienstag 14. November 2006, 10:40

Hallo Humbalan,
ohne großartig was getestet zu haben, was passiert, wenn du deine Zeilen 95 und 97 vertauscht, also den "SetFocus" nach deiner "Message"-Meldung ausführst, dann sollte der Fokus auf dem "fehlenden" TextCtrl gesetzt sein?
Viele Grüße
Piet
Benutzeravatar
Humbalan
User
Beiträge: 59
Registriert: Mittwoch 2. September 2009, 15:11

Hallo Piet,

vielen Dank für die schnelle Antwort.

Die Idee war gut, habe sie gleich ausprobiert. Doch leider: nichts ist besser geworden. Warum sie trotzdem gut ist, kommt im übernächsten Absatz.

Der Grund ist wohl: der Aufruf CallAfter(set_focus) macht nichts weiter als set_focus in eine Event-Queue zu hängen, die erst dann abgearbeitet wird, wenn mindestens der momentane Event fertig ist. Falls vor dem CallAfter()-Aufruf noch weiter Events aufgetaucht sind, werden auch die vorher abgearbeitet. Also die beiden Anweisungen zu tauschen ändert nichts an der Abarbeitungsreihenfolge.

Dein Vorschlag hat mich auf etwas ähnliches gebracht: den MessageDialog()-Aufruf in die Methode set_focus() zu verschieben. Und siehe da, es geht :D .

Kleiner Wermutstropfen: ich finde es nicht sehr waidmännisch, in eine Methode, die dazu dient, einen Fokus zu setzen, die Fehlerausgabe hineinzubauen :( . Kein gutes Design. Auch wenn ich diese Methode umbenennen würde: es bleibt dabei, dass sie nur deswegen entstanden ist, um bei einem Fokusverlust den SetFocus()-Aufruf nicht in einer kill_focus-Methode zu haben. Außerdem bin ich unzufrieden damit, dass ich nicht weiß, warum die ursprüngliche Lösung nicht funktioniert. Ich wünsche mir, den Code, den ich schreibe auch wirklich zu verstehen, damit ich in Zukunft solch ein Rästelraten vermeiden kann.

Also noch einmal Danke, Piet, für die Anregung.

Falls mir jemand noch etwas beim Verständnis für diese Geschichte helfen könnte, würde ich mich sehr freuen.

Beste Grüße
Humbalan
Piet Lotus
User
Beiträge: 80
Registriert: Dienstag 14. November 2006, 10:40

Hallo Humbalan,
merkwürdig? Ich habe jetzt mal was getestet. Ich habe in deinem geposteten Quelltext die Zeilen wie beschrieben einfach mal vertauscht und bei mir funktioniert es. Folgendermaßen habe ich getestet:
1. Skript starten -> Cursor über die Maus ins TextCtrl2 setzen -> über Maus Okay drücken -> Fehlermeldung erscheint -> über Maus quittieren -> Cursor ist in TextCrtl2 zu sehen
2. Skript starten -> Cursor über die Maus ins TextCtrl2 setzen -> Test in TextCtrl2 schreiben -> über Maus ins TextCtrl1 wechseln -> Text in TextCrtl1 löschen -> über Maus Okay drücken -> Fehlermeldung erscheint -> über Maus quittieren -> Cursor ist in TextCrtl1 zu sehen

Ich nutze allerdings Python 2.7 und ein wxPython 2.8.12 auf einem Ubuntu-System.
Wenn ich mich richtig erinnere wird CallAfter ausgeführt, wenn alle "Events" abgearbeitet worden sind - hattest du ja auch geschrieben. Nachfolgendes ist höchstwahrscheinlich Quatsch, da ich einfach zu wenig Ahnung habe und leider nicht die Zeit habe, es zu untersuchen oder nachzulesen, aber wenn du einen MessageDialog (mit self als Parent-Attribut) nach CallAfter aufrufst, gibt's vielleicht auch irgendwelche Events die in die "Queue" über diesen Dialog wegen des selfs gehängt werden. Beim Vertauschen und da der Dialog modal ist und die Bearbeitung erst nach Beenden des Dialogs weitergeht, müssten auch alle Events "fertig" (inklusive der des MessageDialogs) sein und CallAfter kann loslaufen. Deshalb kam ich auf die Idee mit dem Vertauschen. Merkwürdigerweise scheint es bei mir ja auch zu funktioneren. Aufgrund meiner höchst spekulativen Annahme habe ich jetzt einfach mal in deinem Orginalquellcode, also ohne Vertauschen der erwähnten Zeilen im MessageDialog-Aufruf das "self" durch "None" ersetzt. Dann funktionierte der obige Test auch ohne Vertauschung der Zeilen bei mir. Ohne "None" oder dem Vertauschen der Zeilen habe ich allerdings auch "deine" Fehler.
Ich kann dir leider nicht weiterhelfen, da ich über die beschrieben beiden Wege deine Probleme nicht nachstellen kann. Vielleicht ist es ja auch nur ein Versionsproblem?
Ich hoffe, meine Beschreibung war nicht zu wirr :)

Viele Grüße
Piet
Benutzeravatar
Humbalan
User
Beiträge: 59
Registriert: Mittwoch 2. September 2009, 15:11

Hallo Piet,

habe gleich Deine Tests nachvollzogen.
... die Zeilen wie beschrieben einfach mal vertauscht ...
1. Skript starten -> Cursor über die Maus ins TextCtrl2 setzen -> über Maus Okay drücken -> Fehlermeldung erscheint -> über Maus quittieren -> Cursor ist in TextCrtl2 zu sehen
Ja, bei mir auch :) .
2. Skript starten -> Cursor über die Maus ins TextCtrl2 setzen -> Test in TextCtrl2 schreiben -> über Maus ins TextCtrl1 wechseln -> Text in TextCrtl1 löschen -> über Maus Okay drücken -> Fehlermeldung erscheint -> über Maus quittieren -> Cursor ist in TextCrtl1 zu sehen
Bei mir nicht :K !
Ich nutze allerdings Python 2.7 und ein wxPython 2.8.12 auf einem Ubuntu-System.
Habe bis vor kurzem auch wx2.8 gehabt. Dort trat ein ähnlicher, nicht ein gleicher (!!) Fehler auf. Also liegt es nicht an der Version. Könnte aber am Betriebssystem liegen. Ich erinnere mich an verschiedene Posts, in denen über unlogische Event-Abfolgen bei Windows geklagt wurde.

Habe auch Tests mit None und self als ersten Parameter für MessageDialog gemacht, liefert aber die gleichen Ergebnisse.

Weiter Tests mit einer Methode on_set_focus...:

Code: Alles auswählen

self.m_textCtrl_testname1.Bind( wx.EVT_SET_FOCUS, self.on_set_focus_testname )
self.m_textCtrl_testname2.Bind( wx.EVT_SET_FOCUS, self.on_set_focus_testname )

def on_set_focus_testname( self, event ) :
    print u'Set Fokus für {}'.format( event.GetEventObject().GetName() )
    event.Skip()
Haben ergeben, dass bei Schließen des MessageDialog() immer das erste Feld im Window den Focus bekommt, egal in welchem Feld der Fehler auftrat. Falls CallAfter() nach dem MessageDialog() aufgerufen wird, kam das set_focus zuerst für das erste Feld, dann für ein set_focus für das zweiten. In jedem Fall fehlt aber der Cursor.

Vielen Dank für Deine Mühe, Piet. Vielleicht weiß eine anderes Forumsmitglied einen Rat?

Beste Grüße
Humbalan
Antworten