Open spectereye opened 1 year ago
You can do that via asyncio, but that would require you to set up asyncio in your program. If it's only one message box, I'd say go with callbacks.
BTW "nested popus" are not that easy in DPG. When you open a popup, earlier popups close. See #2183, #1393, #1019, #990. As far as I understand, it is possible to "fix" DPG so that a new popup does not close the old one; however, this is quite a bit of work, and requires certain architectural changes. Don't expect that to happen any time soon - or happen at all, for that matter. Currently, DPG renders all windows at the same level, whereas for popups to keep open, one popup needs to be nested into another as seen by Dear ImGui. That's quite a change.
Thanks @v-ein, i tried asyncio below, seems still not work? (I have little experience of asyncio)
import asyncio
import dearpygui.dearpygui as dpg
dpg.create_context()
async def msgbox():
def on_msgbox_btn_click(sender, data, user_data):
nonlocal resp
resp = dpg.get_item_label(sender)
# dpg.hide_item(popup)
dpg.delete_item(popup)
with dpg.popup(parent=dpg.last_item(), modal=True) as popup:
dpg.add_text('goto step 2?')
with dpg.group(horizontal=True):
dpg.add_button(label='Yes', callback=on_msgbox_btn_click)
dpg.add_button(label='No', callback=on_msgbox_btn_click)
resp = None
dpg.show_item(popup)
while not resp:
print('waiting for response ...')
await asyncio.sleep(0.5)
return resp
async def on_btn_click(sender, data, user_data):
print('do sth step 1 ...')
# resp = asyncio.run(msgbox()) # use this without `async def` doesn't work either, no action from Yes/No button click
resp = await msgbox()
if resp == 'Yes':
print('do sth step 2 ...')
async def main():
with dpg.window(tag='prim'):
dpg.add_button(label='Submit', callback=on_btn_click)
dpg.create_viewport(title='test msgbox', width=600, height=300)
# start_dpg()
dpg.setup_dearpygui()
dpg.set_primary_window('prim', True)
dpg.show_viewport()
dpg.start_dearpygui()
dpg.destroy_context()
if __name__ == '__main__':
asyncio.run(main())
The problem here is that dpg.start_dearpygui()
is not async. Moreover, on_btn_click
is called as a regular (synchronous) function, and not even in asyncio thread. To do that properly, you'd need to implement your own rendering loop, set manual_callback_management
, and handle callbacks within that loop.
More about manual_callback_management
:
https://dearpygui.readthedocs.io/en/latest/documentation/item-callbacks.html?highlight=manual_callbacks#debugging-callbacks-new-in-1-2
Unfortunately there's no ready-to-use asyncio example in DPG docs (yet). This topic has seen some discussions on Discord so you might want to have a look - here's a link to Discord (copied from DPG readme): https://discord.gg/tyE7Gu4
@v-ein thanks for your great guide, finally i work out the solution below, hope it can help somebody looking for this too.
target: pythonic popup messagebox in dearpygui
do_sth_step1(...)
user_response = msgbox('Confirm?') # Yes/No
if user_response == 'Yes':
do_sth_step2(...)
solution:
import time
import inspect
import dearpygui.dearpygui as dpg
dpg.create_context()
dpg_callback_queue = []
def msgbox():
def on_msgbox_btn_click(sender, data, user_data):
nonlocal resp
resp = dpg.get_item_label(sender)
dpg.hide_item(popup)
# dpg.delete_item(popup)
with dpg.popup(parent=dpg.last_item(), modal=True) as popup:
dpg.add_text('goto step 2?')
with dpg.group(horizontal=True):
dpg.add_button(label='Yes', callback=on_msgbox_btn_click)
dpg.add_button(label='No', callback=on_msgbox_btn_click)
dpg.show_item(popup)
resp = None
while not resp:
# print('waiting for response ...')
# time.sleep(0.01) # NOT needed
handle_callbacks_and_render_one_frame() # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
return resp
def on_btn_click(sender, data, user_data):
print('do sth step 1 ...')
resp = msgbox()
if resp == 'Yes':
print('do sth step 2 ...')
print('done!')
def main():
with dpg.window(tag='prim'):
dpg.add_button(label='Submit', callback=on_btn_click)
dpg.create_viewport(title='test msgbox', width=600, height=300)
# start_dpg()
dpg.setup_dearpygui()
dpg.set_primary_window('prim', True)
dpg.show_viewport()
dpg.configure_app(manual_callback_management=True)
while dpg.is_dearpygui_running():
handle_callbacks_and_render_one_frame()
dpg.destroy_context()
def run_callbacks():
global dpg_callback_queue
while dpg_callback_queue:
job = dpg_callback_queue.pop(0)
if job[0] is None:
continue
sig = inspect.signature(job[0])
args = []
for i in range(len(sig.parameters)):
args.append(job[i+1])
result = job[0](*args)
def handle_callbacks_and_render_one_frame():
global dpg_callback_queue
dpg_callback_queue += dpg.get_callback_queue() or [] # retrieves and clears queue
# print(f'jobs: {jobs}')
run_callbacks()
dpg.render_dearpygui_frame()
if __name__ == '__main__':
main()
and asyncio way: (revised acc. to @v-ein's guide below)
import asyncio
import inspect
import dearpygui.dearpygui as dpg
dpg.create_context()
async def msgbox():
def on_msgbox_btn_click(sender, data, user_data):
# print('clicked msgbox button')
nonlocal resp
resp = dpg.get_item_label(sender)
dpg.hide_item(popup)
# dpg.delete_item(popup)
ev.set()
with dpg.popup(parent=dpg.last_item(), modal=True) as popup:
dpg.add_text('goto step 2?')
with dpg.group(horizontal=True):
dpg.add_button(label='Yes', callback=on_msgbox_btn_click)
dpg.add_button(label='No', callback=on_msgbox_btn_click)
dpg.show_item(popup)
resp = None
# while not resp: # use `asyncio.Event` instead
# print('waiting for response ...')
# await asyncio.sleep(0.01)
ev = asyncio.Event()
await ev.wait()
return resp
async def on_btn_click_(sender, data, user_data): # IMPORTANT this callback endswith "_"
print('do sth step 1 ...')
resp = await msgbox()
if resp == 'Yes':
print('do sth step 2 ...')
print('done')
def on_input_change_sync(sender, data, user_data):
dpg.set_value(user_data, data)
async def on_input_change(sender, data, user_data):
dpg.set_value(user_data, data)
async def on_input_change_(sender, data, user_data):
dpg.set_value(user_data, data)
async def main():
with dpg.window(tag='prim'):
dpg.add_button(label='Submit', callback=on_btn_click_)
dpg.add_input_text(callback=on_input_change_sync, user_data='lable0', label='sync')
dpg.add_text(tag='lable0')
dpg.add_input_text(callback=on_input_change_, user_data='lable1', label='create_task')
dpg.add_text(tag='lable1')
dpg.add_input_text(callback=on_input_change, user_data='lable2', label='await')
dpg.add_text(tag='lable2')
dpg.create_viewport(title='test msgbox', width=600, height=300)
# start_dpg()
dpg.setup_dearpygui()
dpg.set_primary_window('prim', True)
dpg.show_viewport()
dpg.configure_app(manual_callback_management=True)
while dpg.is_dearpygui_running():
dpg.render_dearpygui_frame() # placed before or after handle callback? seem same result
jobs = dpg.get_callback_queue() # retrieves and clears queue
await run_callbacks(jobs)
await asyncio.sleep(0.01)
dpg.destroy_context()
async def run_callbacks(jobs):
if jobs is None:
pass
else:
for job in jobs:
if job[0] is None:
continue
sig = inspect.signature(job[0])
args = []
for i in range(len(sig.parameters)):
args.append(job[i+1])
result = job[0](*args)
# diff. callback function naming style for diff. handle method
if inspect.isawaitable(result):
if result.__name__.endswith('_'):
print(f'creat task for: {result.__name__}')
# start corotine without waiting for its result - not follow register sequence
asyncio.create_task(result)
else:
print(f'await for: {result.__name__}')
await result # run callback in sequence of registered
if __name__ == '__main__':
asyncio.run(main())
Good job! A couple of notes here.
Regarding your synchronous solution: it might be better to call run_callbacks
after dpg.render_dearpygui_frame()
, not before it. The reason is, dpg.render_dearpygui_frame()
is what generates callback events. By handling them after the frame, you'll be exiting handle_callbacks_and_render_one_frame
with the callbacks queue empty. Otherwise (as in the current implementation) you're leaving some events in the queue without guarantees as to when they will be handled... which might as well make UI elements unresponsive.
Also, why do you need time.sleep(0.01)
? DPG should be able to do some sleeping inside of dpg.render_dearpygui_frame()
according to FPS and input events. That is, the while-loop should not be eating 100% of CPU even without that time.sleep
.
Re asyncio version:
(1) Instead of a while-loop on asyncio.sleep
, create an asyncio.Event
and do await event.wait()
. After you've assigned the desired value to resp
, do event.set()
- this will unblock event.wait()
and let it process resp
.
(2) By doing asyncio.create_task
for every callback job and not waiting for it to end, you're allowing callbacks to be scheduled concurrently. That is, callbacks can be executed in a different order than they originated in dpg.render_dearpygui_frame()
. If a callback contains await
inside, it even gives another callback a chance to run interleaved with it.
While this might not be a problem to your code, DPG generally expects callbacks to be run in the order in which they were born. Take an input_text
callback for example. As you type text in the input field, callback is invoked for every typed character. If these callbacks are run out-of-order, your program might get wrong understanding of what was really typed into the field (depending on what you actually do in the callback). Like this:
You type: Your callback gets in app_data:
a "a"
b "abc"
c "ab"
thanks again @v-ein, i revised code above:
besides remove time.sleep
from sync way, for aysncio version:
(1) asyncio.Event
looks more pythnoic'
(2) create_task
or await
in handleing callbacks - depends on naming style of the callback :-) easy way for me before see a formal implmentation from dearpygui. BTW rare change to see the input_text issue you described, i guess because of too slow manual interactive on GUI like typing some words in inputbox
(3) place render_dearpygui_frame
before or after callback handle, seems same effect as in a loop, orignal codes follow example below:
https://dearpygui.readthedocs.io/en/latest/documentation/item-callbacks.html#debugging-callbacks-new-in-1-2
AND issue found is that it's obvious (though still works) slower after use manual_callback_management
when quickly tying in inputbox
Does it make sense to have:
async def run_callbacks(jobs): if jobs is None: pass else:
since we could remove if jobs is None: and just keep if jobs: and write what's in the else block inside the if block? If there's some unknown significance to this that I'm missing, I'd like to know it.
Overall, I think the implementations described above are overly complicated. There's a simpler way, like this:
class Popup:
def __init__(self, parent: str, message: str, task):
self.parent = parent
self.message = message
self.task = task
self.dpg_callback_queue = []
self.confirm: Optional[bool] = None
with dpg.mutex():
viewport_width = dpg.get_viewport_client_width()
viewport_height = dpg.get_viewport_client_height()
with dpg.window(label=self.message, modal=True, no_close=True) as modal_id:
dpg.add_text(message)
with dpg.group(horizontal=True):
dpg.add_button(label="Да", width=75, user_data=(modal_id, True), callback=self.confirmation)
dpg.add_button(label="Отмена", width=75, user_data=(modal_id, False), callback=self.cancellation)
dpg.split_frame()
width = dpg.get_item_width(modal_id)
height = dpg.get_item_height(modal_id)
dpg.set_item_pos(modal_id, [viewport_width // 2 - width // 2, viewport_height // 2 - height // 2])
def confirmation(self, sender, unused, user_data):
self.task()
dpg.delete_item(user_data[0])
def cancellation(self, sender, unused, user_data):
dpg.delete_item(user_data[0])
Here we execute the provided function upon confirmation, which should be sufficient in most cases.
Here we execute the provided function upon confirmation, which should be sufficient in most cases.
Yeah, but (1) it's not what the OP asked for (it's not going to be a linear piece of code), and (2) synchronization (and general understanding of thread contexts in this case) is all yours ;).
Oops, I was a bit wrong about synchronization. It's sufficient to understand that the callback will be run in the handlers thread, as usual in DPG. The first point still stands true though - it's not going to be a regular if msgbox=="yes" then...
Here we execute the provided function upon confirmation, which should be sufficient in most cases.
Yeah, but (1) it's not what the OP asked for (it's not going to be a linear piece of code), and (2) synchronization (and general understanding of thread contexts in this case) is all yours ;).
Yep, not exactly that, but as much simpler)
Hi, any pythonic way to use messagebox (popup) as below?
Currently i have to move
do_sth_step2
to callback to msgbox (Yes/No buttons on popup window), like callback chain or "callback hell" if nest popups ... Thanks for any guide!