Eine Funktion, mehrere Button Bindings?

Plattformunabhängige GUIs mit wxWidgets.
Antworten
Nebelhom
User
Beiträge: 155
Registriert: Mittwoch 19. Mai 2010, 01:31

Bei unten angehaengtem Beispielcode habe ich zwei Buttons, die ganz genau dasselbe tun ( erst FileDialog und dann einen Pfad an eine TextCtrl schicken), jedoch fuer eine andere TextCtrl. Der zeilensparsamste Code, der mir eingefallen ist, musste immernoch eine separate Funktion zu jedem Button definieren.

Kann man eine Funktion auch so schreiben, dass sie generell genug ist, um sich auf beide Knoepfe zu beziehen, aber dann fuer die zwei verschiedenen TextCtrls jeweils spezifisch einen passenden return Value zurueckgibt?

Der Grund, warum ich frage ist, weil ich in dem eigentlichen Code nicht 2 sonder 15 verschiedene Zeilen habe. Ich war immer der Ansicht, dass (15 mal) copy and paste von code nicht besonders "pythonic" ist. Nur weiss ich mir mal wieder nicht zu helfen.

Danke euch schonmal fuer Feedback und Kritik

NOTE: Bitte beachten, dass dies Beispielcode ist, der auf das Essentielle herunter getrimmt wurde.

Code: Alles auswählen

#!/usr/bin/env python
# -*- coding: iso-8859-15 -*-
# generated by wxGlade 0.6.3 on Sun May 30 21:43:52 2010

import wx
import os

# begin wxGlade: extracode
# end wxGlade

ID_ONE = wx.NewId()
ID_TWO = wx.NewId()

class MyDialog(wx.Dialog):
    def __init__(self, *args, **kwds):
        # begin wxGlade: MyDialog.__init__
        kwds["style"] = wx.DEFAULT_DIALOG_STYLE
        wx.Dialog.__init__(self, *args, **kwds)
        self.PathOneLabel = wx.StaticText(self, -1, "Path 1")
        self.PathOneText = wx.TextCtrl(self, -1, "")
        self.PathOneBtn = wx.Button(self, ID_ONE, " ... ")
        
        self.PathTwoLabel = wx.StaticText(self, -1, "Path 2")
        self.PathTwoText = wx.TextCtrl(self, -1, "")
        self.PathTwoBtn = wx.Button(self, ID_TWO, " ... ")
        
        ##############################################
        #### Button Bindings #########################
        ##############################################
        
        wx.EVT_BUTTON(self, ID_ONE, self.onPathOne)
        wx.EVT_BUTTON(self, ID_TWO, self.onPathTwo)

        self.__set_properties()
        self.__do_layout()
        # end wxGlade

    def __set_properties(self):
        # begin wxGlade: MyDialog.__set_properties
        self.SetTitle("Example")
        # end wxGlade

    def __do_layout(self):
        # begin wxGlade: MyDialog.__do_layout
        grid_sizer_1 = wx.GridSizer(4, 2, 0, 0)
        grid_sizer_1.Add(self.PathOneLabel, 0, wx.ALIGN_CENTER_HORIZONTAL|wx.ALIGN_CENTER_VERTICAL, 0)
        grid_sizer_1.Add((20, 20), 0, wx.ALIGN_CENTER_HORIZONTAL|wx.ALIGN_CENTER_VERTICAL, 0)
        grid_sizer_1.Add(self.PathOneText, 0, wx.ALIGN_CENTER_HORIZONTAL|wx.ALIGN_CENTER_VERTICAL, 0)
        grid_sizer_1.Add(self.PathOneBtn, 0, wx.ALIGN_CENTER_HORIZONTAL|wx.ALIGN_CENTER_VERTICAL, 0)
        grid_sizer_1.Add(self.PathTwoLabel, 0, wx.ALIGN_CENTER_HORIZONTAL|wx.ALIGN_CENTER_VERTICAL, 0)
        grid_sizer_1.Add((20, 20), 0, wx.ALIGN_CENTER_HORIZONTAL|wx.ALIGN_CENTER_VERTICAL, 0)
        grid_sizer_1.Add(self.PathTwoText, 0, wx.ALIGN_CENTER_HORIZONTAL|wx.ALIGN_CENTER_VERTICAL, 0)
        grid_sizer_1.Add(self.PathTwoBtn, 0, wx.ALIGN_CENTER_HORIZONTAL|wx.ALIGN_CENTER_VERTICAL, 0)
        self.SetSizer(grid_sizer_1)
        grid_sizer_1.Fit(self)
        self.Layout()
        # end wxGlade

    #####################################################################
    ########## The code in question #####################################
    #####################################################################
            
    def onSetPath(self):
        "Sends a Path Config Dialog"
        # Create a file-open dialog in the current directory
        
        dlg = wx.FileDialog(self, message="Open a file", defaultDir=os.getcwd(), 
                            defaultFile="", style=wx.OPEN)
        
        # Call the dialog as a model-dialog so we're required to choose Ok or Cancel
        if dlg.ShowModal() == wx.ID_OK:
            # User has selected something, get the path, set the window's title to the path
            #filename = dlg.GetPath()
            #self.townEurText.SetValue(filename)
            return dlg.GetPath()
                        
        dlg.Destroy()
    
    # Geht das hier auch kuerzer? Bei dem wirklich code waeren das 15 verschiedene Funktionen,
    # die immer nur leichte Aenderungen im Variablennamen haetten
    
    def onPathOne(self, event):
        filename = self.onSetPath()
        self.PathOneText.SetValue(filename)
        
    def onPathTwo(self, event):
        filename = self.onSetPath()
        self.PathTwoText.SetValue(filename)
        
    #################################################################
    ################# End of relevant code ##########################
    #################################################################

# end of class MyDialog


if __name__ == "__main__":
    app = wx.PySimpleApp(0)
    wx.InitAllImageHandlers()
    Example = MyDialog(None, -1, "")
    app.SetTopWindow(Example)
    Example.Show()
    app.MainLoop()
BlackJack

@Nebelhom: Du könntest eine Methode schreiben, die ein `TextCtrl` und das `event` als Argumente entgegennimmt und daraus mit `functools.partial()` Funktionen erstellen, bei denen das erste Argument mit dem entsprechenden `TextCtrl` vorbelegt ist, und die dann an die jeweilige Schaltfläche binden.
Nebelhom
User
Beiträge: 155
Registriert: Mittwoch 19. Mai 2010, 01:31

Hi,

ich bin mir nicht sicher, was du meinst. Soweit ich weiss, kann man bei wxpython nur eine Methode an einen Button binden, die die Argumente "self" und "event" hat. Deshalb kann ich eine variable textctrl nicht angeben :(

Wenn das falsch ist, dann waere ich froh wenn du mich aufklaeren wuerdest, wie das geht (oder ein link).

und eine methode schreiben, die dann von der "an den Button gebundenen" Methode gerufen wird, hat das problem, dass ich nicht verstehe, wie ich von einer generellen methode nur mit den argumenten "self" und "event" (die ja immer gleich sind) spezifische textctrls auswaehlen kann.

Uebrigens, kennst du zufaellig eine Seite, die die Benutzung von functools.partial() illustriert. Ich versteh die Anwendung an sich nicht. In der standard doc haben sie ein Beispiel, das mir leider nicht weiterhilft. Ist functools.partial() ein wenig wie das built-in lambda?

Danke nochmal.
BlackJack

@Nebelhom: An Buttons kannst Du nur Funktionen binden die *ein* Argument entgegennehmen, nämlich das `event`. `self.onPathOne` ist eine Funktion die nur *ein* Argument entgegennimmt. Das erste Argument der Methode ist ja bereits an das Exemplar gebunden. Das passiert beim Abruf des Attributs.

`functools.partial()` nimmt eine Funktion entgegen und ein oder mehrere Werte, und liefert eine Funktion die intern die alte Funktion aufruft. Mit den belegten Werten und den Argumenten, die noch "frei" sind und beim Aufruf der neuen Funktion noch angegeben werden müssen.

Aus Dein Beispiel bezogen brauchst Du eine Methode `onPath(self, text_control, event)`. Wenn Du auf die Methode auf einem Exemplar zugreifst, also `self.onPath`, hast Du eine Funtktion die zwei Argumente erwartet: `text_control` und `event`. Mit `partial()` kannst Du das erste Argument jetzt an ein `TextCtrl` binden; z.B. ``partial(self.onPath, self.PathOneText)``. Das Ergebnis davon ist eine Funktion die nur noch ein Argument erwartet: `event`. Und diese Funktion ist genau dass was man an einen Button binden kann.
Dav1d
User
Beiträge: 1437
Registriert: Donnerstag 30. Juli 2009, 12:03
Kontaktdaten:

Du kannst auch ein dict anlegen, die ID des Buttons als key und die TextCtrl als Value. Später wenn das Event ausgelöst wird, bekommst du die Id des Buttons mit event.GetId(), mit dieser Id kannst du dir die TextCtrl aus dem Dict "holen", wobei ich die Lösung nicht für das Problem benutzen würde, da es nur 2 Button sind, bei mehreren in einer Schleife erzeugten ist IMHO diese Lösung besser.
Zu functools.partial hat ja BlackJack schon was geschrieben.
the more they change the more they stay the same
Nebelhom
User
Beiträge: 155
Registriert: Mittwoch 19. Mai 2010, 01:31

Guys,

you two rule! Ich habe interessehalber beide Ansaetze probiert und in beiden Faellen klappt es ohne zu murren :). Ich haenge meinen geaenderten Code mal fuer die Nachwelt unten an, falls noch jemand mal ein aehnliches Problem hat.

Ich bin mir noch nicht ganz schluessig, welchen der beiden Ansaetze ich in meinem richtigen code benutzen soll. Beide scheinen recht nuetzlich zu sein, um auf 15 Knoepfe ausgeweitet zu werden...

Nur so als Frage. Sind solche Ansaetze Standard und tausendmal gesehen fuer Leute, die sich tagtaeglich mit solchen Problemen auseinadersetzen muessen? Weil ich waere auf beide sachen in hundert jahren nicht gekommen, ich oller hobby-pythonista.

@BlackJack: Jetzt weiss ich, dass man mit functools aus einer funktion/methode mit mehreren argumenten, eine mit nur einem einzigen machen kann. Mal von diesem Fall abgesehen, wofuer wird functools.partial() denn noch benutzt?
Ich kann mir nicht vorstellen, dass das mit wxpython im Hinterkopf geschrieben wurde und ich wuerde gerne die Konstellation erkennen, in der ich auf functools.partial zurueckgreifen kann.

Ich danke euch beiden fuer die tolle Hilfestellung. (Falls ihr noch gravierende Fehler seht, dann koennt ihr ruhig schimpfen... oder so ;) )

Code1: BlackJack's Approach (Der rest des Codes kann im ersten Posting gefunden werden)

Code: Alles auswählen

 

import functools

        # Code....

        ##############################################
        #### Button Bindings #########################
        ##############################################
        
        wx.EVT_BUTTON(self, ID_ONE, functools.partial(self.onSetPath, self.PathOneText))
        wx.EVT_BUTTON(self, ID_TWO, functools.partial(self.onSetPath, self.PathTwoText))

#################################################
######### More Code in between #########################
#################################################

    def onSetPath(self, text_control, event):
        "Sends a Path Config Dialog"
        # Create a file-open dialog in the current directory
        
        dlg = wx.FileDialog(self, message="Open a file", defaultDir=os.getcwd(), 
                            defaultFile="", style=wx.OPEN)
        
        # Call the dialog as a model-dialog so we're required to choose Ok or Cancel
        if dlg.ShowModal() == wx.ID_OK:
            # User has selected something, get the path, set the window's title to the path
            filename = dlg.GetPath()
            text_control.SetValue(filename)
            #return dlg.GetPath()
                        
        dlg.Destroy()
Dav1d's approach:

Code: Alles auswählen

# .... Code

        ##############################################
        #### Button Bindings #########################
        ##############################################
        
        self.idDict = {ID_ONE:self.PathOneText, ID_TWO:self.PathTwoText}
        
        wx.EVT_BUTTON(self, ID_ONE, self.onSetPath)
        wx.EVT_BUTTON(self, ID_TWO, self.onSetPath)

###################################################
### More code in between #################################
###################################################

    def onSetPath(self, event):
        "Sends a Path Config Dialog"
        # Create a file-open dialog in the current directory
        
        dlg = wx.FileDialog(self, message="Open a file", defaultDir=os.getcwd(), 
                            defaultFile="", style=wx.OPEN)
        
        # Call the dialog as a model-dialog so we're required to choose Ok or Cancel
        if dlg.ShowModal() == wx.ID_OK:
            # User has selected something, get the path, set the window's title to the path
            filename = dlg.GetPath()
            btnId = event.GetId()
            self.idDict[btnId].SetValue(filename)
            #return dlg.GetPath()
                        
        dlg.Destroy()
BlackJack

@Nebelhom: Aus Funktionen durch binden von Argumenten neue Funktionen zu erstellen ist in der funktionalen Programmierung eine übliche Technik. Bei Callback-APIs generell oft sehr praktisch. Also nicht nur bei wxPython, sondern auch bei allen anderen gängigen GUI-Toolkits.

Allgemeiner ist die Funktion nützlich wenn man `map()`/`itertools.imap()` oder auch `filter()`/`itertools.ifilter()` mit Funktionen aufrufen möchte bei denen feste Argumente für alle Elemente des "iterable" festgelegt werden sollen. Eben typische funktionale Programmierung.

Vor `functools.partial()` musste man an solchen Stellen mit ``lambda`` arbeiten. Und vor ``lambda`` mit "closures".
Nebelhom
User
Beiträge: 155
Registriert: Mittwoch 19. Mai 2010, 01:31

Ja und was war da der Nachteil an "lambda", wenn das eine aehnliche schnittmenge an anwendungen hat zu functools.partial()? wird lambda einfach unuebersichtlich bei groesseren ansaetzen?
BlackJack

@Nebelhom: Also ich persönlich find's nicht so schön dass man da Defaultwerte binden muss wenn man den Inhalt einer Laufvariable verwenden möchte. Und man muss in der ``lambda``-Signatur die freien Argumente auch noch einmal aufführen. Wenn das mehr als nur ein `event` ist, artet das in unnötiger Schreibarbeit aus.
Nebelhom
User
Beiträge: 155
Registriert: Mittwoch 19. Mai 2010, 01:31

Hmmm... Ok, das macht Sinn.

Danke, meine Neugierde ist jetzt endgueltig befriedigt :D
Dav1d
User
Beiträge: 1437
Registriert: Donnerstag 30. Juli 2009, 12:03
Kontaktdaten:

Es gibt da noch was :D

Code: Alles auswählen

# .... Code

        ##############################################
        #### Button Bindings #########################
        ##############################################
       
        self.Bind(wx.EVT_BUTTON, self.onSetPath, btn1)
        self.Bind(wx.EVT_BUTTON, self.onSetPath, btn2)

###################################################
### More code in between #################################
###################################################

    def onSetPath(self, event):
        "Sends a Path Config Dialog"
        # Create a file-open dialog in the current directory
       
        dlg = wx.FileDialog(self, message="Open a file", defaultDir=os.getcwd(),
                            defaultFile="", style=wx.OPEN)
       
        # Call the dialog as a model-dialog so we're required to choose Ok or Cancel
        if dlg.ShowModal() == wx.ID_OK:
            # User has selected something, get the path, set the window's title to the path
            filename = dlg.GetPath()
            btn = event.GetEventObject()
            btn.SetValue(filename)
            #return dlg.GetPath()
                       
        dlg.Destroy()
Also event.GetEventObject()
the more they change the more they stay the same
Antworten