pywin32 icon indicating copy to clipboard operation
pywin32 copied to clipboard

Fix modeless COM browser dialog

Open CristiFati opened this issue 2 years ago • 2 comments

A follow-up for #1894.
Fixes [SO]: win32com.client combrowse.main() (Python Object Browser) is not responding Python 3.9.

As I mentioned I am neither a COM expert nor an MFC enthusiast, and this part of PyWin32 is new to me, although I've seen some unorthodox (to me) stuff here :).

The actual fix is the 3rd commit. It's the best compromise I found. After the crash fix, ShowWindow wasn't starting the dialog message loop, so calling RunModalLoop was required. But for some reason when dialog was closed with IDCANCEL (Esc or by clicking the X button), the dialog disappeared, but the loop didn't exit, so Ctrl + Break was required in the console in order to kill the process (this didn't happen for IDOK (Enter) - which worked fine).
That's why EndModalLoop is needed in OnCancel (even if [MS.Docs]: CDialog::OnCancel instructs to call DestroyWindow for modeless dialogs, that doesn't work (probably to PyWin32 extra wrapping layer)).
Overriding RunModalLoop would have been the elegant solution, but dialog.Dialog doesn't have it, so the do_modal wrapper was born (its naming style suggests that it doesn't come from MFC).

Modeless dialog works now (as seen in the image from the answer).

I don't know, maybe in the future, this functionality could be moved up to Dialog, but that's a task far from trivial, which would require extensive testing.

CristiFati avatar Jun 09 '22 13:06 CristiFati

I think the problem in that SO question just came from me making the browser default modal in d10d5594. I was not aware (or forgot) that combrowse.py may be run / called from a shell outside PythonWin as a standalone mini GUI. Fixed I hope in #1924 - with some auto switching like in tlbrowse.py.

The low-level RunModalLoop() in a do_modeless() seems like a contradiction - and leads to those artificial follow-up problems. DoModal() ist already the thing which does a consistent modal dialog operation including message loop and proper ending of the dialog etc. - even as standalone GUI as it seems. (The MFC virtuals OnOK OnCancel are not supposed to return a value I think - void.) Modeless operation (without a new message loop) makes only sense inside another (MFC/win32) GUI app - PythonWin - with an outer / main message loop - so e.g. one can continue doing other interaction there while the COM browser is floating as a non-blocking window similar to the object browser.

kxrob avatar Aug 11 '22 17:08 kxrob

Thank you for feedbacks. I didn't explicitly stated it in the description, but do_modeless is a (lame) workaround (gainarie). Will check it, but give me some time. I will post another comment when done.

CristiFati avatar Aug 22 '22 07:08 CristiFati

I completed my tests. This is the current situation (build with latest changes):

  • Modal dialog (which is now the default) works

  • Modeless crashes (as expected since no fix was attempted in this area)

Now regarding the commits:

  1. Yes, CDialog::OnCancel, CDialog::OnOK return void. But browser.dynamic_browser (which extends Dialog) also have return super().OnOK() I thought to make them consistent (especially since in Python return; and return NULL; are equivalent), so I think it's fine

  2. I don't see anything wrong

  3. Although it fixes the crash, it should be probably left out as it's incorrect from design perspective. Maybe a proper fix will arise. Ore temporary disable the modeless behavior?

CristiFati avatar Aug 25 '22 20:08 CristiFati

  • Modeless crashes (as expected since no fix was attempted in this area)

I guess you are trying to run combrowse.main(modal=False) somehow from a console script / interactive, like in SO, without main message loop. That would naturally just flash and immediately close and exit again. Modeless is for running within Pythonwin (or another GUI): from Menu/Tools/COMBrowser usually, or by >>> combrowse.main(modal=False) # alt. mdi=True from its interactive window - and then does not block the Pythonwin GUI and can be used parallel.

For curiosity I run it from a console script "pseudo-modeless" with minial non-MFC "main message loop" in a further MessageBox - works surprisingly:

#!py -3
import win32api
from win32com.client.combrowse import main
main(modal=False)
win32api.MessageBox(0, "waiting!")

kxrob avatar Aug 26 '22 06:08 kxrob

I finally got the modeless purpose. So setting it from a script (like in SO) is not supported. I think it's fine. As a note: changing mdi has no effect.

Yes! the message box trick works!

CristiFati avatar Aug 26 '22 10:08 CristiFati

win32api.MessageBox(0, "waiting!")

Heh - yeah, or a trivial win32gui.PumpWaitingMessages() loop would work. Sounds like we don't need anything here though.

mhammond avatar Aug 28 '22 09:08 mhammond