hoffstadt / DearPyGui

Dear PyGui: A fast and powerful Graphical User Interface Toolkit for Python with minimal dependencies
https://dearpygui.readthedocs.io/en/latest/
MIT License
12.6k stars 667 forks source link

OS filedialog using Tkinter #2338

Open Catalyst-42 opened 1 month ago

Catalyst-42 commented 1 month ago

Version of Dear PyGui

Version: 1.11.1 (on python 3.12.2, dpg installed via pip) Operating System: macOS Sonoma 14.5

My Issue/Question

I wanted to create a system file dialog (instead of the file dialog provided by dpg.file_dialog), using Tkinter. When creating the dialog, various very strange errors occurred, which are impossible to handle and understand the reason for their occurrence. Over time I have found a working solution, but I don't think it's obvious or correct.

I also saw a similar problem in #1141, but by rewriting the solution I got a different error

To Reproduce

copy the code from the examples into the files first_example.py, example_1141.py and third_example.py and get_file.py

running python3 first_example.py gives zsh: trace trap python3 first_example.py running python3 example_1141.py gives zsh: segmentation fault python3 example_1141.py running python3 subprocess_demo.py works fine

Expected behavior

Filedialog, working in one file without need to call subprocess.

Screenshots/Video

Fine working example with subprocess:

subprocess_demo

Standalone, minimal, complete and verifiable example

The first obvious but unworkable example:

# first_example.py
import dearpygui.dearpygui as dpg
from tkinter import filedialog

dpg.create_context()

def open_file():
    file = filedialog.askopenfilename()  # Gives trace trap
    print(f"{file=}")

with dpg.window(tag="Demo"):
    dpg.add_button(label="Open file", callback=open_file)

dpg.create_viewport(title="Demo", width=600, height=412)
dpg.setup_dearpygui()
dpg.show_viewport()
dpg.set_primary_window("Demo", True)
dpg.start_dearpygui()
dpg.destroy_context()

Example from #1141, updated to last dpg version, unworkable, gives segfault:

# example_1141.py
from dearpygui.dearpygui import *

from tkinter import Tk
from tkinter import filedialog
import os

def new():
    Tk().withdraw()  # Gives segmentation fault
    file_path = filedialog.askdirectory(initialdir=os.getcwd())  
    print(file_path)
    DirString = "Chosen Directory: "+file_path
    set_value(dirpath,DirString)

with window(label="Tutorial") as main_window:
    add_button(label="Choose Directory",callback=new)
    dirpath = add_text(default_value="Chosen Directory: None Selected")

set_primary_window(main_window, True)
start_dearpygui()

Third, fine working (but weird looking) example:

# get_file.py
from tkinter import filedialog

file = filedialog.askopenfilename()
print(file, end="")
# third_example.py
import dearpygui.dearpygui as dpg
from tkinter import filedialog

from sys import executable  # To run subprocess
from subprocess import check_output

dpg.create_context()

def open_file():
    file = check_output([executable, "get_file.py"])  # Works fine, but looks weird
    print(f"{file=}")

with dpg.window(tag="Demo"):
    dpg.add_button(label="Open file", callback=open_file)

dpg.create_viewport(title="Demo", width=600, height=412)
dpg.setup_dearpygui()
dpg.show_viewport()
dpg.set_primary_window("Demo", True)
dpg.start_dearpygui()
dpg.destroy_context()
v-ein commented 1 month ago

Disclaimer: I don't have any experience with Tkinter, and what's worse, I don't have tkinter itself (on Windows, it's optional), so can't really test your code. Nevertheless, here are some thoughts on the topic.

I believe that to successfully use Tkinter with DPG, one needs to understand events (and the event loop) and threads in both libraries. Here's a piece of doc describing threads in Tkinter:

https://docs.python.org/3/library/tkinter.html#threading-model

In DPG, you have two threads: the main thread runs the rendering loop, and a secondary thread (created by DPG internally) runs callbacks and handlers. When you click a button, its callback is run on that secondary "handlers thread".

Now, what happens in your 1st and 2nd example is Tk is run on that secondary thread, and somehow it doesn't like that. In 3rd example, you're running it in a separate process, and therefore in the main thread (whereas DPG's rendering loop runs in the main thread of another process).

Somehow Tkinter doc suggests that it can be used from any thread, but googling it up shows a number of discussions saying that it should be run on the main thread (albeit most of them dating back at 2010-2012). So it's probably a matter of properly initializing Tkinter and using it in the right context. Unfortunately, like I said, I can't play with it and therefore can't provide any ready-to-use solutions.

tobyclh commented 1 month ago

Got the same problem and I think I figured out the solution albeit not being very clean either. The gist is that you have to

  1. setup a root tk object before dpg.show_viewport
  2. call Tk from the main thread.

Alternative, the original code works perfectly on Windows for those who are interested. Functional example:

import dearpygui.dearpygui as dpg
from tkinter import filedialog
from tkinter import Tk
root = Tk()
root.withdraw()

dpg.create_context()

def open_file():
    file = filedialog.askopenfilename()  # Gives trace trap
    print(f"{file}")

with dpg.window(tag="Demo"):
    dpg.add_checkbox(tag='should_open_file', default_value=False, show=False)
    dpg.add_button(label="Open file", callback=lambda: dpg.set_value('should_open_file', True))

dpg.create_viewport(title="Demo", width=600, height=412)

dpg.setup_dearpygui()
dpg.show_viewport()

dpg.set_primary_window("Demo", True)

while dpg.is_dearpygui_running():
    if dpg.get_value('should_open_file'):
        dpg.set_value('should_open_file', False)
        open_file()
    dpg.render_dearpygui_frame()
    pass

dpg.destroy_context()

and if you can afford another external library, xdialog works pretty well on my system

import xdialog
import dearpygui.dearpygui as dpg
dpg.create_context()

def open_file():
    # file = filedialog.askopenfilename()  # Gives trace trap
    file = xdialog.open_file("Title Here", filetypes=[("Text Files", "*.txt")], multiple=True)
    print(f"{file}")

with dpg.window(tag="Demo"):
    dpg.add_button(label="Open file", callback=open_file)
    dpg.add_loading_indicator()

dpg.create_viewport(title="Demo", width=600, height=412)

dpg.setup_dearpygui()
dpg.show_viewport()

dpg.set_primary_window("Demo", True)
dpg.start_dearpygui()

dpg.destroy_context()