Leertaste registrieren während anderer Process läuft

Plattformunabhängige GUIs mit wxWidgets.
Antworten
mintoxis
User
Beiträge: 1
Registriert: Dienstag 6. Januar 2015, 21:14

Hallo zusammen,

ich möchte folgendes erreichen: ein Hauptfenster (mit wx) mit einem Button. Klickt man darauf, soll sich ein neues Fenster öffnen und eine Funktion Background.DoSomething() gestartet werden. Während diese Funktion läuft, soll registriert werden, ob der Nutzer die Leertaste drückt und falls ja eine globale Variable confirmed = True gesetzt werden.

Im Prinzip funktioniert das Programm auch fast, aber Background.DoSomething() muss immer erst zu Ende laufen, bevor die Leertaste registriert werden kann. :? Außerdem reagiert das Programm nicht mehr ("Not Responding..."), bis die Funktion zu zu Ende gelaufen ist. Wie mache ich es richtig?

Besten Dank im Voraus für die Hilfe!
mintoxis

-------

Und hier mein Versuch:

Code: Alles auswählen

import time
import wx
    
class Background:
    def DoSomething(self):
        # While background proess runs, show a small window:
        gui.WhileTesting()
        print "This should run in background."
        time.sleep(20)
        print "Background process done."
        

#### GUI ####
class MainFrame(wx.Frame):
    def __init__(self, parent, id, title):
        wx.Frame.__init__(self, parent, id, title,size=(800,600))
 
        self.panel = wx.Panel(self, wx.ID_ANY, style=wx.NO_BORDER)  
        self.panel.SetFocus()
        
        button = wx.Button(self.panel, id=wx.ID_ANY, label="Start Test")
        button.Bind(wx.EVT_BUTTON, self.onButton)
        
    def onButton(self, event):
        print "Starting background process."
        myBackground = Background()
        myBackground.DoSomething()


class CommunicatingFrame(wx.Frame):
    __frequency = 0
    def __init__(self, parent, id, title):
        wx.Frame.__init__(self, parent, id, title,size=(400,200))
  
        self.panel = wx.Panel(self, wx.ID_ANY, style=wx.NO_BORDER)
        self.panel.SetFocus()

        self.panel.Bind(wx.EVT_CHAR, self.OnSpace)

    def OnSpace(self,event):
        if(event.GetKeyCode() == wx.WXK_SPACE or event.GetKeyCode() == wx.WXK_RETURN):
            self.SetTitle("Confirmed!")
            confirmed = True
            print confirmed
            

class MyApp(wx.App):
    def OnInit(self):
        mainframe = MainFrame(None, -1, "Example")
        mainframe.Show(True)
        self.SetTopWindow(mainframe)
        return True
    def WhileTesting(self):
        testframe = CommunicatingFrame(None, -1, "Background process running, press SPACE to set the variable confirmed to 'True'...")
        testframe.Show(True)
        self.SetTopWindow(testframe)
        return True
#### END GUI ####

# global variable
confirmed = False
gui = MyApp(0)
gui.MainLoop()
BlackJack

@mintoxis: Als erstes solltest Du von dem Vorhaben Abstand nehmen eine globale Variable verwenden zu wollen. Das tust Du in dem gezeigten Quelltext mit `confirmed` auch gar nicht, denn `CommunicatingFrame.OnSpace()` verändert `confirmed` auf Modulebene gar nicht sondern hat einen eigene lokalen Namen der nichts mit dem auf Modulebene zu tun hat.

Und statt das jetzt tatsächlich global zu machen, sollte eher die ebenfalls modulglobale `gui` in einer Hauptfunktion verschwinden. Und nicht `gui` heissen denn das `MyApp`-Exemplar selbst ist keine GUI.

Wenn das `MyApp`-Objekt in einer Funktion verschwunden ist, fällt auf das `Background.DoSomething()` einfach so aus dem Nichts darauf zugreifen will. Werte die in Funktionen oder Methoden verwendet werden sollten immer als Argumente in die Funktion/Methode gelangen, ausser es handelt sich um Konstanten. Sonst hat man sehr schnell ein unüberschaubares Geflecht von Abhängigkeiten die den Code schwer nachvollziebar, schlecht testbar, und irgendwann unwartbar machen.

Da das konkrete `wx.App`-Exemplar ein Singleton ist und das von `wx` auch durchgesetzt wird, gibt es die Funktion `wx.GetApp()` um (fast) jederzeit und überall an dieses Objekt zu kommen.

Die Klasse `Background` ist überflüssig. Eine Klasse die keine `__init__()` besitzt, also weder direkt noch vererbt, ist schon ein recht starker „code smell”. Aber auch Klassen mit `__init__()` die nur eine weitere Methode besitzen sind in der Regel nur aufgeblasene Funktionen.

Wenn Code im Hintergrund, also parallel zur GUI-Hauptschleife ausgeführt werden soll, dann braucht man Threads, und all die Probleme die man sich damit einhandelt. Zum Beispiel das man nur vom Hauptthread aus die GUI verändern darf, weil GUI-Rahmenwerke in der Regel nicht „thread safe” sind. Das ist auch bei `wx` so. Dort sind Ereignisse „thread safe”, dass heisst die können zur Kommunikation zwischen Threads verwendet werden. Und man kann `wx.CallAfter()` und `wx.CallLater()` verwenden um Aufrufe aus anderen Threads über die GUI-Hauptschleife indirekt abzusetzen. Aus der Funktion die im Hintergrund läuft direkt so etwas wie Fenster erstellen ist nicht möglich. Das muss man alles in dem Thread mit der GUI-Hauptschleife erledigen.

`wx` bietet ein paar Konstrukte zur Unterstützung an. Zum Beispiel das `wx.lib.delayedresult`-Modul das es auf einfache Weise erlaubt eine Funktion asynchron auszuführen an deren Ende dann eine Rückruffunktion mit dem Ergebnis aufgerufen wird.

Um Bedingungen bei ``if`` & Co gehören keine Klammern. Und statt zweimal den Tastencode vom `Event`-Exemplar abzufragen könnte man das auch nur einmal tun und dann mit dem ``in``-Operator testen ob der Tastencode in einer Liste enthalten ist.

Man sollte keine literalen Zahlen als Wahrheitswerte verwenden. `wx.App` erwartet als erstes Argument einen Wahrheitswert, was man deutlich besser sieht wenn dort `False` statt 0 übergeben wird. Da `False` der Defaultwert ist, kann man sich das aber auch komplett sparen.

Die Vorsilbe `my` ist in 99,9% der Fälle unsinniges Beiwerk was keinerlei Mehrwert hat und deshalb weggelassen werden sollte.

Das `__frequency`-Klassenattribut hat dort nichts zu suchen und sieht auch nach so einer halbglobalen Geschichte aus die man gar nicht erst versuchen sollte. Doppelte führende Unterstriche machen dort auch keinen Sinn.

Ich komme dann ungefähr bei so etwas heraus (nur oberflächlich getestet):

Code: Alles auswählen

#!/usr/bin/env python
# coding: utf8
from __future__ import absolute_import, division, print_function
import time
import wx
from wx.lib.delayedresult import startWorker


def do_something():
    print('This should run in background.')
    time.sleep(5)
    print('Background process done.')


class ConfirmationFrame(wx.Frame):

    def __init__(self, parent, id, title):
        wx.Frame.__init__(self, parent, id, title, size=(400, 200))

        panel = wx.Panel(self, wx.ID_ANY, style=wx.NO_BORDER)
        panel.SetFocus()

        panel.Bind(wx.EVT_CHAR, self.on_key)

        self.confirmed = False

    def on_key(self, event):
        if event.GetKeyCode() in [wx.WXK_SPACE, wx.WXK_RETURN]:
            self.SetTitle('Confirmed!')
            self.confirmed = True
            print(self.confirmed)


class MainFrame(wx.Frame):

    def __init__(self, parent, id, title):
        wx.Frame.__init__(self, parent, id, title, size=(800, 600))

        panel = wx.Panel(self, wx.ID_ANY, style=wx.NO_BORDER)
        panel.SetFocus()

        self.start_button = wx.Button(panel, id=wx.ID_ANY, label='Start Test')
        self.start_button.Bind(wx.EVT_BUTTON, self.on_button)
        self.confirmation_frame = None

    def on_button(self, _event):
        print('Starting background process.')
        self.start_button.Enable(False)
        
        assert self.confirmation_frame is None
        self.confirmation_frame = ConfirmationFrame(
            None,
            wx.ID_ANY,
            'Background process running, press SPACE to set the variable'
            " confirmed to 'True'..."
        )
        self.confirmation_frame.Show(True)
        wx.GetApp().SetTopWindow(self.confirmation_frame)

        startWorker(self.handle_result, do_something)

    def handle_result(self, result):
        if self.confirmation_frame:
            self.confirmation_frame.Destroy()
        self.confirmation_frame = None

        self.start_button.Enable(True)
        
        print(result.get())


def main():
    app = wx.PySimpleApp()
    frame = MainFrame(None, wx.ID_ANY, 'Example')
    frame.Show(True)
    app.SetTopWindow(frame)
    app.MainLoop()


if __name__ == '__main__':
    main()
Piet Lotus
User
Beiträge: 80
Registriert: Dienstag 14. November 2006, 10:40

Hallo mintoxis,
ohne deinen Quellcode genauer "untersucht" zu haben. Wenn ich mich richtig an die Methode "wx.Yield()" erinnere, könntest du mal versuchen diese Methode in deine langlaufenden "Berechungen" unterzubringen. Mittels "wx.Yield()" innerhalb langer "Berechnungen" kannst du wxPython die Gelegenheit geben z.B. eine "Progress Bar" (Gauge) zu aktualisieren. Wahrscheinlich kannst du darüber auch Key-Events in deiner Anwendung verarbeiten.
Viele Grüße
Piet Lotus
BlackJack

Aus der Dokumentation zu `wx.App.Yield()`:
:warning: This function is dangerous as it can lead to unexpected reentrancies (i.e. when called from an event handler it may result in calling the same event handler again), use with extreme care or, better, don't use at all!
Würde ich ja die Finger von lassen.
Piet Lotus
User
Beiträge: 80
Registriert: Dienstag 14. November 2006, 10:40

Hallo BlackJack,
da hatte ich bisher wohl Glück. :)
Wieder was gelernt, das wusste ich noch nicht. Besten Dank für die Info.
@ mintoxis: Sorry für den falschen Hinweis
Viele Grüße
Piet Lotus
Antworten