tkinter.filedialog produziert "RuntimeError: main thread is not in main loop"

Fragen zu Tkinter.
Antworten
Oxyrhynchos
User
Beiträge: 3
Registriert: Montag 6. August 2018, 07:51

Hallo,

ich habe eine Frage zum tkinter.filedialog.
Ich setze den filedialog zur Auswahl eines Verzeichnisses ein, ohne ansonsten mit Tkinter zu arbeiten (oder mich sonderlich damit auszukennen): Der Dialog ist halt super praktisch, um in einer App ohne GUI ein Verzeichnis komfortabel auszuwählen.

Die Auswahl des Verzeichnisses funktioniert beim ersten Aufruf des Dialogs noch.
Der Versuch, den Dialog (zum Wechsel des Verzeichnisses) mittels eines Tastatur-Shortcuts ein zweites Mal aufzurufen, scheitert aber mit der Fehlermeldung: "RuntimeError: main thread is not in main loop".
Leider verstehe ich nicht, was mir das genau sagen soll und wie ich es abstellen könnte.

Im Folgenden ein Auszug des betreffenden Codes:

Code: Alles auswählen

import datetime
import os
from pynput import keyboard
import tkinter as tk
from tkinter import filedialog
import winsound

COMBINATIONS = [
    {keyboard.Key.f9},
    {keyboard.Key.f10},
]

root = tk.Tk()
root.withdraw()

chosen_path = None


def choose_directory():
    global chosen_path
    _chosen_path = filedialog.askdirectory(
        initialdir="D:\\",
        title="Wähle Scan-Verzeichnis ..."
    )
    print(f'Scan-Verzeichnis: {_chosen_path}\n')
    chosen_path = _chosen_path


choose_directory()
current = set()


def delete_last_scan():
    list_of_files = []
    for file in os.listdir(chosen_path):
        if file.endswith('.tif'):
            list_of_files.append(os.path.join(chosen_path, file))

    if len(list_of_files) == 0:
        return False

    latest_file_fullpath = max(list_of_files, key=os.path.getctime)
    filename = os.path.basename(latest_file_fullpath)
    _datetime = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    print(f'({_datetime}) Datei gelöscht: {filename}')
    winsound.PlaySound(sound='trash.wav', flags=winsound.SND_FILENAME)
    os.remove(path=latest_file_fullpath)


def on_press(key):
    if any([key in COMBO for COMBO in COMBINATIONS]):
        current.add(key)
        if any(all(k in current for k in COMBO) for COMBO in COMBINATIONS):
            if key.name == 'f9':
                delete_last_scan()
            elif key.name == 'f10':
                # choose_directory()
                print('Die Funktion zum Wechsel des Verzeichnisses '
                      'produziert immer noch einen Fehler und ist daher '
                      'abgeschaltet.\n')


def on_release(key):
    if any([key in COMBO for COMBO in COMBINATIONS]):
        current.remove(key)


with keyboard.Listener(on_press=on_press, on_release=on_release) as listener:
    listener.join()
Die App erleichtert mir die Arbeit beim Buchscannen.
Über Hilfe würde ich mich sehr freuen.

Mit freundlichem Gruß,


Johannes
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

Benutze keine globalen Variablen; `choose_directory` sollte das Verzeichnis als Rückgabewert haben. Was ist der Sinn, sowohl _chosen_path als auch chosen_path zu benutzen?

In `delete_last_scan` besser glob.glob benutzen, das kann schon nach Pattern filtern. Führende Unterstriche bedeuten, dass man zwar eine Funktion aufruft, die einen Rückgabewert hat, diesen aber eigentlich nicht braucht. Du brauchst aber `_datetime`.

In on_press benutzt Du zwar die Konstante COMBINATIONS, fragst aber dennoch hardkodiert F9 und F10 über den Namen ab.

`keyboard.Listener` setzt offensichtlich Threads ein, um die Events abzuarbeiten. GUIs und Threads arbeiten nicht gut zusammen, bedeutet, nur aus dem Hauptthread heraus darf man etwas mit der GUI machen. Du brauchst also im Hauptthread eine Schleife, die Events abfrägt, die in der on_press-Funktion generiert und in eine Queue gesteckt werden.

Edit: ungetestet sieht das dann ungefähr so aus:

Code: Alles auswählen

import datetime
import os
from functools import partial
from glob import glob
from pynput import keyboard
import tkinter as tk
from tkinter import filedialog
from queue import Queue
import winsound

def choose_directory():
    path = filedialog.askdirectory(
        initialdir="D:\\",
        title="Wähle Scan-Verzeichnis ..."
    )
    print(f'Scan-Verzeichnis: {path}\n')
    return path

def delete_last_scan(scan_path):
    list_of_files = glob(os.path.join(scan_path, '*.tif'))

    if not list_of_files:
        return

    latest_file_fullpath = max(list_of_files, key=os.path.getctime)
    filename = os.path.basename(latest_file_fullpath)
    now = datetime.datetime.now()
    print(f'({now:%Y-%m-%d %H:%M:%S}) Datei gelöscht: {filename}')
    winsound.PlaySound(sound='trash.wav', flags=winsound.SND_FILENAME)
    os.remove(latest_file_fullpath)


def on_press(queue, key):
    queue.put(key)

def main():
    root = tk.Tk()
    root.withdraw()
    queue = Queue()
    scan_path = choose_directory()
    with keyboard.Listener(on_press=partial(on_press, queue)) as listener:
        while True:
            key = queue.get()
            if keyboard.Key.f9:
                delete_last_scan(scan_path)
            elif keyboard.Key.f10:
                scan_path = choose_directory()

if __name__ == '__main__':
    main()
Oxyrhynchos
User
Beiträge: 3
Registriert: Montag 6. August 2018, 07:51

@Sirius3:

Herzlichen Dank für Deine äußerst instruktive Analyse meines Codes!
Kaum zu glauben, was sich der bemühte Laie da so zusammengestümpert hat :cry: ...

Dein Code hat nicht gleich auf Anhieb funktioniert.
Es bedurfte noch der folgenden kleinen Änderungen:

Code: Alles auswählen

def main():
    root = tk.Tk()
    root.withdraw()
    queue = Queue()
    scan_path = choose_directory()
    with keyboard.Listener(on_press=partial(on_press, queue)) as listener:
        while True:
            key = queue.get()
            if key == keyboard.Key.f9:
                delete_last_scan(scan_path)
            elif key == keyboard.Key.f10:
                scan_path = choose_directory()
Vielen Dank für Deine rasche Hilfe! :P
Oxyrhynchos
User
Beiträge: 3
Registriert: Montag 6. August 2018, 07:51

@sirius:

Kurzer Nachtrag:
Sirius3 hat geschrieben: Montag 6. August 2018, 08:45 In `delete_last_scan` besser glob.glob benutzen, das kann schon nach Pattern filtern.
glob hatte ich vorher schon verwendet.
Das Problem ist, dass glob bei längeren Verzeichnisnamen bzw. Verzeichnisnamen mit Sonderzeichen in meinen Fall nicht richtig zu funktionieren scheint.

Gerade bin ich wieder auf einen solchen Fall gestoßen (mit dem von Dir überarbeiteten Code).
Wenn ich als Verzeichnis im FileDialog "D:\Die Geschichte des Christentums 1. Die Zeit des Anfangs (bis 250), 2005 [=2000]" auswähle, folgt anschließend zwar keine Fehlermeldung.
Es wird aber auch nichts gelöscht, wenn ich F9 drücke; so als wäre das Verzeichnis leer.

Ich weiß nicht, woran das liegt; bei obigem Workaround mit os.listdir funktioniert alles.
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

Sonderzeichen, wie [ und ] in einem Verzeichnisnamen sind auch eine ganz schlechte Idee.

Besser wäre es sowieso mit pathlib zu arbeiten

Code: Alles auswählen

def delete_last_scan(scan_path):
    list_of_files = pathlib.Path(scan_path).glob('*.tif')
    try:
        latest_file_fullpath = max(list_of_files, key=lambda p: p.stat().st_ctime)
    except ValueError:
        # no file found
        pass
    else:
        now = datetime.datetime.now()
        print(f'({now:%Y-%m-%d %H:%M:%S}) Datei gelöscht: {filename.name}')
        winsound.PlaySound(sound='trash.wav', flags=winsound.SND_FILENAME)
        os.remove(latest_file_fullpath)
Antworten