Midnight Commander in Python

Du hast eine Idee für ein Projekt?
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Dateiübertragung auf das Handy war bisher für mich nicht zufriedenstellend. Über USB ging es unter Ubuntu manchmal aber auch oft nicht.
Über Samba Server am WLAN Router und dem ES Datei Explorer ging es, wenn auch nicht besonders schnell.

Mir würde so etwas wie der Midnight Commander am PC vorschweben:
https://de.wikipedia.org/wiki/Midnight_ ... 4.7-de.png

Das ließe sich doch mit einer Listbox links und einer Listbox rechts leicht machen, etwa tkinter Listbox im extended Mode.
Außerdem könnte man da auch eine integrierte Umgebung für QPython machen, etwa .py Datei auf dem Handy mit dem Commander auf dem PC anklicken, dann auf dem PC bearbeiten und wieder auf dem Handy speichern.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Ich denke mal, das Konzept sollte man ändern, nämlich in beliebig viele Fenster und Geräte. Soviel, wie auf den Bildschirm passen und wenn man die Fenster noch dazu minimieren kann, können es auch mehr sein. Man würde dannn ein Pack Left Layout nehmen - oder auch Grid - und für die Fenster und Geräte eine eigene Instanz, so dass eh alles gleich ist.

Pro Instanz braucht man dann einen Gerätedeskriptor und einen Pfaddeskriptor. Letzterer deshalb, weil man nicht den OS Pfad verändern sollte und auch mehrere Fenster und Pfade ermöglichen sollte.
Und wenn beim Kopieren oder Moven beide Gerätedeskriptoren übereinstimmen, ist es lokales Kopieren oder Moven auf dem entsprechenden Gerät. Ansonsten geht es über Netzwerk.

Also man braucht getrennte Instanzen für Geschäftslogik und GUI, weil man das möglichst unabhängig von der GUI schreiben sollte.

Aber ganz unabhängig sind die Instanzen doch nicht. WEnn in der GUI etwas ausgewählt wurde zum Löschen - aber zwei Fenster auf dasselbe Verzeichnis offen sind, dann darf der Löschbefehl nur an eine Instanz der Geschäftslogik gehen. Das GUI update danach aber an beide GUI Instanzen. Für gewisse Kommandos braucht man also auch ein Verzeichnis der Instanzen mit Pad und Gerätedeskriptor und wenn die gleich sind, dann GUI Update für beide. Bei Verzeichniswechsel allerdings nicht.
Zuletzt geändert von Alfons Mittelmeyer am Samstag 19. September 2015, 16:32, insgesamt 1-mal geändert.
Benutzeravatar
cofi
Python-Forum Veteran
Beiträge: 4432
Registriert: Sonntag 30. März 2008, 04:16
Wohnort: RGFybXN0YWR0

Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

@cofi Kann ich da auch auf das Handy übertragen? Und wie lange braucht eine 60 MB Datei? Bei meinem WLAN mit meinem Messagebroker waren es 20 Sekunden.

Und dieser ranger ist doch ziemlich eingeschränkt. Ich stelle mir auswählen und dann control-c control-v vor und das auch an alle teilnehmenden Geräte, etwa PDF Dateien auf die Computer aller Schüler in einer Klasse kopieren und natürlich auch auf Handies, Tablets und dergleichen.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Man sollte sich eher Gedanken machen was die GUI, etwa eine Listbox der Geschäftslogik meldet und was die GUI verwaltet.

Ich komme da auf den Namen des Eintrags und den Typ. Und zu unterscheiden wären da drei Typen:

- directory
- file
- parent_directory (etwa '..')

Dann könnten das diese Methoden sein:

- select, etwa bei Doppelklick auf einen Eintrag.
- - bei Directory dann Wechsel in das Sub Directory
- - bei Parent dann Wechsel in das übergeordnete Verzeichnis
- - bei File je nach Filetyp, anzeigen, öffnen zum Editieren, aber vorerst einmal nichts

- delete etwa bei control-y gedrückt. Die Geschäftslogik erhält dabei die ausgewählten Einträge. Typ Parent zählt dabei nicht.
- rename Bedienung noch festzulegen. Parameter alter Name und Typ und neuer Name. Typ Parent zählt dabei nicht.
- selected_for_copy etwa bei control-c. Die Geschäftslogik erhält dabei die ausgewählten Einträge. Typ Parent zählt dabei nicht.
- selected_for_move etwa bei control-x. Die Geschäftslogik erhält dabei die ausgewählten Einträge. Typ Parent zählt dabei nicht.
- selected_for_insert etwa bei control-v. Die Geschäftslogik erhält dabei lediglich, dass das ausgewählt wurde und kümmert sich dann um das Weitere

Und für die GUI gibt es dann nur eine update Funktion mit einer Liste der Verzeichniseinträge. Für rename braucht man vielleicht keine, und wenn dann sollte die alte Auswahl angezeigt werden.
Für Anderes - das wäre nach 'delete' und 'selected_for_insert' - sollte der Auswahlbalken, wenn überhaupt, dann auf '..' stehen
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Zweite Idee, die Anmeldung.

Es gibt eine Masterapplikation mit einer GUI, mit der man Verzeichnisse bearbeitet. Und es gibt Slave-Applikationen - über LAN oder WLAN verbunden, deren Verzeichnisse von der Masterapplikation bearbeitet werden. Die Masterapplikation kann auch ihre eigenen Verzeichnisse bearbeiten.

Wenn eine Slave Applikation hochfährt, meldet sie sich bei der Masterapplikation an. Das nützt natürlich nichts, wenn die Masterapplikation noch nicht hochgefahren ist. Aber wenn die Masterapplikation hochfährt, schickt sie eine Aufforderung zur Anmeldung an die Slaves. Dadurch ist die Anmeldung der Slaves gewährleistet.

Die Auswirkung der Anmeldung.

Die linke Spalte der GUI Applikation enthält die angemeldeten Slaves und den Master. Der Master steht ganz oben (pack top) darunter die Slaves (pack bottom). Da gibt es pro Applikation (Master und Slaves) einen Button, um ein neues Fenster zu öffnen. Darunter werden die Pfade bereits geöffneter Fenster für die betreffende Applikation angezeigt - eventuell auch Buttons, um minimierte Fenster wieder groß zu machen.

Bei Drücken des Buttons für ein neues Fenster wird die betreffende Applikation aufgefordert, ihr Root Verzeichnis zur Anzeige zur senden. Beim Root Verzeichnis handelt es sich um eine virtuelle Root, welche die für diese Applikation freigegebenen Verzeichnisse enthält. Das könnten etwa virtuelle Laufwerke sein A:,B:,C:,D: etc. In der virtuellen Root dürfen keine Änderungen ausgeführt werden, sondern nur in den freigegebenen Verzeichnissen. Die Pfadangaben bestehen daher aus einem Laufwerksbuchstaben und einem Pfad. Damit ist dann die Bearbeitung anderer Verzeichnisse ausgeschlossen.

Ein neues Fenster aufzumachen soll auch möglich sein von einem bereits geöffneten Fester aus. Da wird dann der Pfad des bereits geöffneten Fensters übernommen. Gut um in ein Unterverzeichnis zu moven oder in das übergeordnete Verzeichnis.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Hat keiner eine Idee, wie man beginnen könnte? Ich würde bei einem Button anfangen. Wenn man den drückt, soll für das betreffende Gerät ein Fenster aufgehen und das Root-Directory (virtuelle Root ) dieses Gerätes angezeigt werden.

Also der Startpunkt wäre:

Code: Alles auswählen

class NewWindowButton:
Und wie könnte oder sollte das aussehen?
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Noch ein kleiner Tipp. Wir setzen dabei einen Event Broker ein. Das heißt, ein Subscriber soll sich darum kümmern.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Keiner einen Tipp? Aber da ist die Lösung:

Code: Alles auswählen

import tkinter as tk
import event_broker
import config

root = tk.Tk()

eventbroker = event_broker.EventBroker()

class NewButton(tk.Button):

    def __init__(self,parent,device):
        self.device = device
        self.master = parent
        tk.Button.__init__(self,parent,text=device,command=lambda: eventbroker.publish(self.device,("GetDirEntry",config.ROOTVOLUME,config.NOPATH,config.NEWWINDOW)))
 
mybutton = NewButton(root,config.MASTER_DEVICE)
mybutton.pack()

root.mainloop()
Und das ist die entscheidende Zeile:

Code: Alles auswählen

 tk.Button.__init__(self,parent,text=device,command=lambda: eventbroker.publish(self.device,("GetDirEntry",config.ROOTVOLUME,config.NOPATH,config.NEWWINDOW)))
Und zwar die lambda expression:

Code: Alles auswählen

command=lambda: eventbroker.publish(self.device,("GetDirEntry",config.ROOTVOLUME,config.NOPATH,config.NEWWINDOW)
Wenn jemand diesen Button drückt, dann geht die Nachricht an das Gerät self.decice, dass es den Directory Eintrag schickt ("GetDirEntry") für die virtuelle Root (ROOTVOLUME) - der Pfad (NOPATH) zählt dabei nicht. Und zurücksenden soll der Device das mit der Window ID NEWWINDOW, denn dann geht ein neues GUI Fenster auf.

Und hier haben wir die Konfiguration config.py:

Code: Alles auswählen

MASTER_DEVICE="MASTER PC"
ROOTVOLUME =""
NOPATH = ""
NEWWINDOW = 0
Und hier der ganz entscheidende Code des event brokers event_broker.py:

Code: Alles auswählen

class EventBroker:
 
    def __init__(self): pass

    # publish within same thread
    def publish(self,msgid,msgdata=None): pass
Leider noch nicht in weit fortgeschrittenem Zustand.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Eine kleine Designänderung: Wir unterscheiden nicht zwischen Volume und Path und ändern entsprechend die config.py:

Code: Alles auswählen

MASTER_DEVICE="MASTER PC"
ROOTPATH ="/"
NEWWINDOW = 0
Und dann können wir die fertige Lösung für unseren NewButton in der GUI Task präsentieren:

Code: Alles auswählen

import tkinter as tk
import config
import public_eventbroker

root = tk.Tk()

class NewButton(tk.Button):

    def __init__(self,parent,device):
        self.device = device
        self.master = parent
        tk.Button.__init__(self,parent,text=device,command=lambda: public_eventbroker.eventbroker.publish_extern(self.device,("GetDirEntry",config.ROOTPATH,config.NEWWINDOW)))
 
mybutton = NewButton(root,config.MASTER_DEVICE)
mybutton.pack()

root.mainloop()
Die Lösung ist: die Lösung liegt gar nicht in der GUI Task sondern irgendwo anders im System. Daher nehmen wir den public_eventbroker. Der arbeitet in einem anderen Thread und routet die Aufgabe weiter.

Und ganz stolz sind wir auf die fertige Lösung für den public event broker.

public_evenbroker.py:

Code: Alles auswählen

# This thread contains the public eventbroker, which other threads - which don't know each other - use for communication between them.
# - For events, that other threads want to receive, the method 'public_subscribtions' of their event broker shall be used.
#   This method redirects events in this public event broker to that threads event broker.
# - For publishing events for other threads to this public event broker the  method 'publish_extern' of this public event broker may be used directly
# - or other threads may use the method 'public_publications' of their event broker for redirecting events of their event broker to this public event broker

import threading
import event_broker

eventbroker = None

class MyThread(threading.Thread):
 
    def run(self):
        global eventbroker
        eventbroker = event_broker.EventBroker()
        eventbroker.loop()

mythread = MyThread()
mythread.daemon = True
mythread.start()
Diese Lösung ist doch einfach tutto perfetto - vollkommen fertig und perfekt oder?

Das Verhalten hängt natürlich vom event_broker ab. Und der ist keineswegs fertig.

event_broker.py:

Code: Alles auswählen

import threading

class EventBroker:
 
    def __init__(self): pass

    # publish within same thread
    def publish(self,msgid,msgdata=None): pass
    
    # publish function called by another thread - use it, if you directly access the event broker of another thread
    def publish_extern(self,msgid,msgdata=None): pass

    # loop for event driven threads - don't use this in a GUI event loop (mainloop) - it will block the GUI
    def loop(self):
        self.event = threading.Event()
        self.event.set()
        self._looping = True
        while self._looping:
            self.event.wait()
            self.event.clear()
            #self.do_work()
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Der public event broker war doch noch nicht ganz perfekt. War erst nach Verzögerungszeit einsetzbar. Aber jetzt ist er vollkommen.

public_eventbroker.py:

Code: Alles auswählen

# This thread contains the public eventbroker, which other threads - which don't know each other - use for communication between them.
# - For events, that other threads want to receive, the method 'public_subscriptions' of their event broker shall be used.
#   This method redirects events in this public event broker to that threads event broker.
# - For publishing events for other threads to this public event broker the  method 'publish_extern' of this public event broker may be used directly
# - or other threads may use the method 'public_publications' of their event broker for redirecting events of their event broker to this public event broker

import threading
import event_broker

eventbroker = event_broker.EventBroker()

_worker_thread = threading.Thread(target=eventbroker.loop)
_worker_thread.daemon = True
_worker_thread.start()
Und auch der event broker hat jetzt alle erforderlichen Schnittstellen. Unsubscribe Funktionen für dynamische Applikationen, die sich verändern und dynamisch nachladen, habe ich mal weggelassen.

event_broker.py:

Code: Alles auswählen

import threading

class EventBroker:
 
    def __init__(self,public_eventbroker=None):
        if public_eventbroker==None: self.public_eventbroker = self
        else: self.public_eventbroker = public_eventbroker
        
         # Configuration for triggers at start, which may be changed by the user
        self.trigger = self.do_work # intern trigger at start is direct call of 'do_work'.
        self.extern_trigger = self._noop # extern trigger at start is set to do nothing. feel free to change this in your GUI application, if the GUI framework has a safe trigger, which may be called by another thread
                                         # otherwise polling via 'do_work' has to be used

        self._initialize() # initialize the event broker
 
     # publish within same thread
    def publish(self,msgid,msgdata=None): pass
    
    # publish function called by another thread - use it, if you directly access the event broker of another thread
    def publish_extern(self,msgid,msgdata=None): pass

    # transmit an event to another thread. Similar to publish_extern, but parameters are different: transmit_extern((msgid,message)) , publish_extern(msgid,message)
    def transmit_extern(self,msgid_message): pass

    # subscribe a callback for an event
    def subscribe(self,msgid,callback,optional_parameter=False): pass

    # subscribe function called by another thread - use it, if you access the event broker of another thread
    def subscribe_extern(self,msgid,callback,optional_parameter=False): pass

    # subscribe a list of events to be received from the public event broker
    def public_subscriptions(self,msgid_list):
        for msgid in msgid_list: self.public_eventbroker.subscribe_extern(msgid,self.transmit_extern,True)

    # subscribe a list of events to be sent to the public event broker
    def public_publications(self,msgid_list):
        for msgid in msgid_list: self.subscribe(msgid,self.public_eventbroker.transmit_extern,True)

    # loop for event driven threads - don't use this in a GUI event loop (mainloop) - it will block the GUI
    def loop(self,execute_at_loop_start = None):

        self.event = threading.Event()
        self.trigger = self.event.set
        self.extern_trigger = self.event.set
        self.event.set()
        self._looping = True
        if execute_at_loop_start != None: execute_at_loop_start()

        while self._looping:
            self.event.wait()
            self.event.clear()
            self.do_work()

    # exit event loop 
    def exit_loop(self,*args):
        self._looping = False
        self.trigger()

    # start function for extern_trigger
    def _noop(self): pass

    # process all events in the queues and block other calls
    def do_work(self,*args):
        if self._running: return
        self._running = True
        while self.work(): pass
        self._running = False

    # process one event in the queues 
    def work(self,*args): return False

    def _initialize(self):
        self._running = False # regulates calls of method 'do_work'- start value False enables call of method 'work'
        self._looping = False # regulates ending event loop. After 'loop' is called, the loop may be ended by calling 'exit_loop'
Die Schnittstellen sind da, wenngleich noch keine Funktionalität. Das heißt aber, man kann schon mal implementieren, sodaß nichts abstürzt, auch wenn dann noch nichts geht.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Und jetzt können wir die erste Implementierung machen mit Antwort des betreffenden Devices.
Wir haben den Button. Wenn wir diesen drücken, dann wird zum betreffenden Device gesendet, dass die Masterapplikation dessen Rootdirectory haben will.
Der Device antwortet dann, indem er sein Rootdirectory an die Masterapplikation schickt und wir lassen das zur Probe printen.

newbutton.py:

Code: Alles auswählen

import tkinter as tk
import config
import public_eventbroker
import job_workers

public_eventbroker.eventbroker.subscribe_extern("DirEntry",lambda message: print(message[4]))

root = tk.Tk()

class NewButton(tk.Button):

    def __init__(self,parent,device):
        self.device = device
        self.master = parent
        tk.Button.__init__(self,parent,text=device,command=lambda: public_eventbroker.eventbroker.publish_extern(self.device,["GetDirEntry",self.device,config.ROOTPATH,config.NEWWINDOW]))
 
mybutton = NewButton(root,config.MASTER_DEVICE)
mybutton.pack()

root.mainloop()
Das Event wird zum betreffenden Device geschickt und landet dort im public_eventbroker. Ein Thread holt es sich von da ab, bearbeitet es und schickt das Directory zur Masterapplikation zurück.

Dieser Thread benutzt den public_eventbroker und einen eigenen. Der eigene wird so konfiguriert, dass dieser auf den public_eventbroker zugreifen kann:

Code: Alles auswählen

        self.public_eventbroker = public_eventbroker.eventbroker
        self.eventbroker = event_broker.EventBroker(self.public_eventbroker)
Dieser Thread holt genau die Events ab, die für das betreffende Gerät hereinkommen. Das sind die Events, die als Event ID die Device ID des betreffenden Gerätes haben. Und diese Events werden dann an den job_distributor dieses Threads geleitet. Dieser verteilt dann die Aufgaben und das haben wir hier:

Code: Alles auswählen

        self.public_eventbroker.subscribe_extern(config.THIS_DEVICE,self.job_distributor)

    # called by the public event broker, so self.eventbroker is the extern event broker
    def job_distributor(message): self.eventbroker.publish_extern(message[0],message)
Und message[0] war in unserem Fall "GetDirEntry". Für diese Message ist ein job worker zu installieren. Das wird erreicht durch:

Code: Alles auswählen

        self.eventbroker.subscribe("GetDirEntry",self.get_dir_entry)
Und diese Funktion tut es dann, vorerst nur einmal für das Rootdirectory implementiert:

Code: Alles auswählen

    def get_dir_entry(message):
        if message[2] == config.ROOTPATH: message.append(config.DEVICE_ROOT)
        else: message.append(config.DEVICE_ROOT) # to do later
        message[0] = "DirEntry"
        self.public_eventbroker.publish_extern(MASTER_DEVICE,message)
Die Directory Information wird an die hereingekommene Message gehängt. Der erste Eintrag wird auf "DirEntry" gesetzt, damit diese Message dann richtig behandelt wird, wenn sie beim Master Device ankommt. Dann wird das Event mit der MASTER_DEVICE ID auf die Reise zum Master Device geschickt.

Bei Master Device angekommen, landet das Event dann dort bei dessen Job Workers. Der Job Distributor holt es sich auch dort wieder ab und gibt dann die Aufgabe "DirEntry" weiter. In diesem Falle soll es allerdings kein Job Worker dieses Moduls tun, sondern das Event soll zur GUI Applikation gehen. Also wird dieses Event nun als ausgepacktes "DirEntry" wieder dem public_eventbroker übergeben. Und das wird dadurch ereicht, indem es in die public_publications Liste aufgenommen wird:

Code: Alles auswählen

        # neccessary if this device is the master device
        self.eventbroker.public_publications(("DirEntry",))
Und das ist dann das Modul.

job_workers.py:

Code: Alles auswählen

import config
import threading
import event_broker
import public_eventbroker

class Main:

    def __init__(self):

        self.public_eventbroker = public_eventbroker.eventbroker
        self.eventbroker = event_broker.EventBroker(self.public_eventbroker)
   
        self.public_eventbroker.subscribe_extern(config.THIS_DEVICE,self.job_distributor)
        self.eventbroker.subscribe("GetDirEntry",self.get_dir_entry)
        
        # neccessary if this device is the master device
        self.eventbroker.public_publications(("DirEntry",))

        worker_thread = threading.Thread(target=self.eventbroker.loop)
        worker_thread.daemon = True
        worker_thread.start()

    # called by the public event broker, so self.eventbroker is the extern event broker
    def job_distributor(message): self.eventbroker.publish_extern(message[0],message)

    def get_dir_entry(message):
        if message[2] == config.ROOTPATH: message.append(config.DEVICE_ROOT)
        else: message.append(config.DEVICE_ROOT) # to do later
        message[0] = "DirEntry"
        self.public_eventbroker.publish_extern(MASTER_DEVICE,message)
    
Main()
Und die config Datei dazu sieht so aus:

config.py:

Code: Alles auswählen

MASTER_DEVICE="MASTER PC"
THIS_DEVICE = "MASTER_PC"

DIR_TYPE = 0
ROOTPATH ="/"
DEVICE_ROOT = (("Documents",DIR_TYPE),("Music",DIR_TYPE),("Pictures",DIR_TYPE),("Videos",DIR_TYPE))

NEWWINDOW = 0
Und wenn der event broker funktionsfähig wäre, sollte es das tun.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Den Event Broker zum Laufen zu bringen, ist relativ einfach. Das sind lediglich:

- die publish Funktionen
- die subscribe Funktion
- und work

und für die publish Funktionen nehmen wir eine Queue:

Code: Alles auswählen

import queue

     # publish within same thread
    def publish(self,msgid,msgdata=None):
        self._Queue.put((msgid,msgdata))
        self.trigger()
    
    # publish function called by another thread - use it, if you directly access the event broker of another thread
    def publish_extern(self,msgid,msgdata=None):
        self._Queue.put((msgid,msgdata))
        self.extern_trigger()

    # transmit an event to another thread. Similar to publish_extern, but parameters are different: transmit_extern((msgid,message)) , publish_extern(msgid,message)
    def transmit_extern(self,msgid_message):
        self._Queue.put(msgid_message)
        self.extern_trigger()

    def _initialize(self):
        self._running = False # regulates calls of method 'do_work'- start value False enables call of method 'work'
        self._looping = False # regulates ending event loop. After 'loop' is called, the loop may be ended by calling 'exit_loop'
        self._Queue = queue.Queue() # Queue for publishing
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Für Subscribe benutzen wir ein Dictionary:

Code: Alles auswählen

    # subscribe a callback for an event
    def subscribe(self,msgid,callback,optional_parameter=False):
        if msgid not in self._Dictionary: self._Dictionary[msgid] = {}
        self._Dictionary[msgid][callback] = optional_parameter

    def _initialize(self):
        self._running = False # regulates calls of method 'do_work'- start value False enables call of method 'work'
        self._looping = False # regulates ending event loop. After 'loop' is called, the loop may be ended by calling 'exit_loop'
        self._Queue = queue.Queue() # Queue for publishing
        self._Dictionary = {} # contains registered message ids and registered callback functions for these message ids
Für subscribe_extern müssen wir uns allerdings etwas einfallen lassen, denn es darf ein externer Thread das Dictionary nicht verändern, während es benutzt wird.
Benutzeravatar
sparrow
User
Beiträge: 4187
Registriert: Freitag 17. April 2009, 10:28

Ich glaube nicht, dass jemand deiner Art der Codepräsentation auf diese Weise folgen kann/will.
Klick dir einen Account github oder bitbucket und lad den funktionierenden Code dort hoch.
Hier müsste man, wenn man deinem Monolog folgen will, anfangen Codeschnippsel ineinander zu kopieren. Ich befürchte, das tut niemand.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Der Trick für subscribe_extern besteht aus zwei Komponenten:

- einer Callback Funktion für die event ID "execute_function"
- und einer HighPrio Queue

Code: Alles auswählen

    # subscribe function called by another thread - use it, if you access the event broker of another thread
    def subscribe_extern(self,msgid,callback,optional_parameter=False):
        self._Queue_HighPrio.put(("execute_function",lambda: self.subscribe(msgid,callback,optional_parameter)))
        self.extern_trigger()

    def _initialize(self):
        self._running = False # regulates calls of method 'do_work'- start value False enables call of method 'work'
        self._looping = False # regulates ending event loop. After 'loop' is called, the loop may be ended by calling 'exit_loop'
        self._Queue = queue.Queue() # Queue for publishing
        self._Dictionary = {} # contains registered message ids and registered callback functions for these message ids
        self.subscribe("execute_function",lambda message: message()) # very useful callback function: executes messages, which are lambda expressions
        self._Queue_HighPrio = queue.Queue() # Queue for register extern callback functions
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Der Event Broker ist fertig. Und er hatte auf Anhieb keinen Fehler. Doch in config.py einen Unterstrich vergessen und in job_workers.py zweimal self und einmal config.

Damit haben wir es.

newbutton.py:

Code: Alles auswählen

import tkinter as tk
import config
import public_eventbroker
import job_workers

public_eventbroker.eventbroker.subscribe_extern("DirEntry",lambda message: print(message[4]))

root = tk.Tk()

class NewButton(tk.Button):

    def __init__(self,parent,device):
        self.device = device
        self.master = parent
        tk.Button.__init__(self,parent,text=device,command=lambda: public_eventbroker.eventbroker.publish_extern(self.device,["GetDirEntry",self.device,config.ROOTPATH,config.NEWWINDOW]))
 
mybutton = NewButton(root,config.MASTER_DEVICE)
mybutton.pack()

root.mainloop()
job_workers.py:

Code: Alles auswählen

import config
import threading
import event_broker
import public_eventbroker

class Main:

    def __init__(self):

        self.public_eventbroker = public_eventbroker.eventbroker
        self.eventbroker = event_broker.EventBroker(self.public_eventbroker)
   
        self.public_eventbroker.subscribe_extern(config.THIS_DEVICE,self.job_distributor)
        self.eventbroker.subscribe("GetDirEntry",self.get_dir_entry)
        
        # neccessary if this device is the master device
        self.eventbroker.public_publications(("DirEntry",))

        worker_thread = threading.Thread(target=self.eventbroker.loop)
        worker_thread.daemon = True
        worker_thread.start()

    # called by the public event broker, so self.eventbroker is the extern event broker
    def job_distributor(self,message): self.eventbroker.publish_extern(message[0],message)

    def get_dir_entry(self,message):
        if message[2] == config.ROOTPATH: message.append(config.DEVICE_ROOT)
        else: message.append(config.DEVICE_ROOT) # to do later
        message[0] = "DirEntry"
        self.public_eventbroker.publish_extern(config.MASTER_DEVICE,message)
    
Main()
public_eventbroker.py:

Code: Alles auswählen

# This thread contains the public eventbroker, which other threads - which don't know each other - use for communication between them.
# - For events, that other threads want to receive, the method 'public_subscriptions' of their event broker shall be used.
#   This method redirects events in this public event broker to that threads event broker.
# - For publishing events for other threads to this public event broker the  method 'publish_extern' of this public event broker may be used directly
# - or other threads may use the method 'public_publications' of their event broker for redirecting events of their event broker to this public event broker

import threading
import event_broker

eventbroker = event_broker.EventBroker()

_worker_thread = threading.Thread(target=eventbroker.loop)
_worker_thread.daemon = True
_worker_thread.start()
event_broker.py:

Code: Alles auswählen

import threading
import queue

class EventBroker:
 
    def __init__(self,public_eventbroker=None):
        if public_eventbroker==None: self.public_eventbroker = self
        else: self.public_eventbroker = public_eventbroker
        
         # Configuration for triggers at start, which may be changed by the user
        self.trigger = self.do_work # intern trigger at start is direct call of 'do_work'.
        self.extern_trigger = self._noop # extern trigger at start is set to do nothing. feel free to change this in your GUI application, if the GUI framework has a safe trigger, which may be called by another thread
                                         # otherwise polling via 'do_work' has to be used

        self._initialize() # initialize the event broker
 
     # publish within same thread
    def publish(self,msgid,msgdata=None):
        self._Queue.put((msgid,msgdata))
        self.trigger()
    
    # publish function called by another thread - use it, if you directly access the event broker of another thread
    def publish_extern(self,msgid,msgdata=None):
        self._Queue.put((msgid,msgdata))
        self.extern_trigger()

    # transmit an event to another thread. Similar to publish_extern, but parameters are different: transmit_extern((msgid,message)) , publish_extern(msgid,message)
    def transmit_extern(self,msgid_message):
        self._Queue.put(msgid_message)
        self.extern_trigger()

    # subscribe a callback for an event
    def subscribe(self,msgid,callback,optional_parameter=False):
        if msgid not in self._Dictionary: self._Dictionary[msgid] = {}
        self._Dictionary[msgid][callback] = optional_parameter

    # subscribe function called by another thread - use it, if you access the event broker of another thread
    def subscribe_extern(self,msgid,callback,optional_parameter=False):
        self._Queue_HighPrio.put(("execute_function",lambda msgid=msgid, callback=callback,optional_parameter=optional_parameter: self.subscribe(msgid,callback,optional_parameter)))
        self.extern_trigger()

    # subscribe a list of events to be received from the public event broker
    def public_subscriptions(self,msgid_list):
        for msgid in msgid_list: self.public_eventbroker.subscribe_extern(msgid,self.transmit_extern,True)

    # subscribe a list of events to be sent to the public event broker
    def public_publications(self,msgid_list):
        for msgid in msgid_list: self.subscribe(msgid,self.public_eventbroker.transmit_extern,True)

    # loop for event driven threads - don't use this in a GUI event loop (mainloop) - it will block the GUI
    def loop(self,execute_at_loop_start = None):

        self.event = threading.Event()
        self.trigger = self.event.set
        self.extern_trigger = self.event.set
        self.event.set()
        self._looping = True
        if execute_at_loop_start != None: execute_at_loop_start()

        while self._looping:
            self.event.wait()
            self.event.clear()
            self.do_work()

    # exit event loop 
    def exit_loop(self,*args):
        self._looping = False
        self.trigger()

    # start function for extern_trigger
    def _noop(self): pass

    # process all events in the queues and block other calls
    def do_work(self,*args):
        if self._running: return
        self._running = True
        while self.work(): pass
        self._running = False

    # process one event in the queues 
    def work(self,*args):

        # get message from Queue
        if not self._Queue_HighPrio.empty(): data = self._Queue_HighPrio.get()
        elif not self._Queue.empty(): data = self._Queue.get()
        else: return False

        # look up message id and registered callbacks for it and call callback
        msgid = data[0]
        msgdata = data[1]
        if msgid in self._Dictionary:
            receivers = dict(self._Dictionary[msgid]).items() # # items contain callback function and optional_parameter and we need a copy (via dict), because the entry could change via subscribe
            for callback,optional_parameter in receivers:
                if type(optional_parameter) is bool:
                    if optional_parameter: callback((msgid,msgdata))
                    else: callback(msgdata)
                else: callback((optional_parameter,(msgid,msgdata)))
        return True

    def _initialize(self):
        self._running = False # regulates calls of method 'do_work'- start value False enables call of method 'work'
        self._looping = False # regulates ending event loop. After 'loop' is called, the loop may be ended by calling 'exit_loop'
        self._Queue = queue.Queue() # Queue for publishing
        self._Dictionary = {} # contains registered message ids and registered callback functions for these message ids
        self.subscribe("execute_function",lambda message: message()) # very useful callback function: executes messages, which are lambda expressions
        self._Queue_HighPrio = queue.Queue() # Queue for register extern callback functions
Natürlich können wir die Events via TCP auch zu anderen Geräten schicken, aber zum Entwickeln reicht das vorerst.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Der GUI Window Manager ist für die ersten zwei Spalten der Applikation zuständig.
Die erste Spalte würde bei zwei Geräten so aussehen, wenn noch keine Fenster geöffnet sind:

Code: Alles auswählen

import tkinter as tk

root = tk.Tk()

a = tk.LabelFrame(root,text="Devices")
a.grid(row='0')

def sub(parent):
   a = tk.Frame(parent,bd=1,relief='solid')
   a.pack(pady='1',fill='x')
   
   tk.Button(a,text="MASTER PC").pack(fill='x')

sub(a)

def sub(parent):
   a = tk.Frame(parent,bd=1,relief='solid')
   a.pack(pady='1',fill='x')
   
   tk.Button(a,text="Mein Handy").pack(fill='x')

sub(a)

root.mainloop()
Ich würde vorschlagen, wir implementieren den Start des GUI Managers und lassen dann etwas später ein Handy dazu kommen.
Danach lassen wir das Handy zuerst hochfahren und dann die Master Applikation.

Was meint Ihr dazu?
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Hab mal den NewButton abgespeckt - ohne event broker.

Und mit dieser Gliederung kann man schon einmal Geräte konnekten und un-konnekten:

Code: Alles auswählen

import tkinter as tk
import config

class NewButton(tk.Button):

    def __init__(self,parent,device_name):
        self.device_name = device_name
        tk.Button.__init__(self,parent,text=device_name)

class DeviceManager(tk.Frame):
    def __init__(self,parent,device_name):
        tk.Frame.__init__(self,parent,bd=1,relief='solid')
        self.pack(side='bottom',pady='1',fill='x')
        NewButton(self,device_name).pack(fill='x')

class GuiManager:

    def __init__(self,parent):
        self.device_frame = tk.LabelFrame(parent,text="Devices")
        self.device_frame.grid(row='0')
        self.device_dict = {}
        
        master = self.connect_device(config.MASTER_DEVICE)
        master.pack(side='top',pady='1',fill='x') # the master shall be on top

    def connect_device(self,name):
        manager = DeviceManager(self.device_frame,name)
        self.device_dict[name] = manager
        return manager

    def unconnect_device(self,name):
        try: self.device_dict.pop(name).destroy()
        except: pass

root = tk.Tk()

gui_manager=GuiManager(root)
gui_manager.connect_device("Mein kaputtes Handy")
gui_manager.connect_device("Mein Tablet")
gui_manager.connect_device("Mein neues Handy")
gui_manager.unconnect_device("Mein kaputtes Handy")

root.mainloop()
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

DirEntry Messages für neues Window und Pfad update:

Code: Alles auswählen

import tkinter as tk
import config
import time

testmessage = (["DirEntry","Mein Tablet",config.ROOTPATH,config.NEWWINDOW,config.DEVICE_ROOT],
["DirEntry","Mein Tablet","/Music",config.NEWWINDOW,config.DEVICE_ROOT],
["DirEntry","Mein Tablet","/Video",100,config.DEVICE_ROOT],
["DirEntry","Mein Tablet","/Foto",101,config.DEVICE_ROOT])

class NewButton(tk.Button):

    def __init__(self,parent,device_name):
        self.device_name = device_name
        tk.Button.__init__(self,parent,text=device_name)

class DeviceManager(tk.Frame):
    def __init__(self,parent,device_name):
        tk.Frame.__init__(self,parent,bd=1,relief='solid')
        self.pack(side='bottom',pady='1',fill='x')
        NewButton(self,device_name).pack(fill='x')
        self.win_dict={}

    def new_window(self,win_id,message):
        b = tk.Button(self,text=message[2],pady='0',bg='#fff9e9',relief='flat',anchor='w')
        b.pack(side='bottom',fill='x')
        self.win_dict[win_id] = [b,]

    def update_window(self,win_id,message):
        try: self.win_dict[win_id][0]['text'] = message[2]
        except: pass
            

class GuiManager:

    def __init__(self,parent):
        self.device_frame = tk.LabelFrame(parent,text="Devices")
        self.device_frame.grid(row='0')
        self.device_dict = {}
        self.START_WINDOW_ID = 100
        self.next_window_id = self.START_WINDOW_ID
        
        master = self.connect_device(config.MASTER_DEVICE)
        master.pack(side='top',pady='1',fill='x') # the master shall be on top

    def connect_device(self,name):
        manager = DeviceManager(self.device_frame,name)
        self.device_dict[name] = manager
        return manager

    def unconnect_device(self,name):
        try: self.device_dict.pop(name).destroy()
        except: pass

    def dir_entry(self,message):
        if message[1] in self.device_dict:
            if message[3] == config.NEWWINDOW:
                win_id = self.next_window_id
                self.next_window_id += 1
                self.device_dict[message[1]].new_window(win_id,message)
            elif message[3] >= self.START_WINDOW_ID:
                self.device_dict[message[1]].update_window(message[3],message)
            
root = tk.Tk()

gui_manager=GuiManager(root)
gui_manager.connect_device("Mein kaputtes Handy")
gui_manager.connect_device("Mein Tablet")
gui_manager.connect_device("Mein neues Handy")
gui_manager.unconnect_device("Mein kaputtes Handy")

index = 0
def loop():
    global index
    gui_manager.dir_entry(testmessage[index])
    index += 1
    if index < 4: root.after(2000,loop)

root.after(2000,loop)

root.mainloop()
Damit sollte Spalte eins vorerst einmal fertig sein. In Verbindung mit Spalte zwei gibt es dann freilich noch ein paar kleine Features.
Antworten