Leertaste registrieren während anderer Process läuft

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

Leertaste registrieren während anderer Process läuft

Beitragvon mintoxis » Dienstag 6. Januar 2015, 21:24

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=python file=CommunicateWithGUI.py]
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()
[/code]
BlackJack

Re: Leertaste registrieren während anderer Process läuft

Beitragvon BlackJack » Donnerstag 8. Januar 2015, 11:55

@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: 74
Registriert: Dienstag 14. November 2006, 10:40

Re: Leertaste registrieren während anderer Process läuft

Beitragvon Piet Lotus » Donnerstag 8. Januar 2015, 22:30

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

Re: Leertaste registrieren während anderer Process läuft

Beitragvon BlackJack » Donnerstag 8. Januar 2015, 23:24

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: 74
Registriert: Dienstag 14. November 2006, 10:40

Re: Leertaste registrieren während anderer Process läuft

Beitragvon Piet Lotus » Freitag 9. Januar 2015, 22:42

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

Wer ist online?

Mitglieder in diesem Forum: 0 Mitglieder