techartorg / bqt

A Blender add-on to support & manage Qt Widgets in Blender (PySide2)
https://github.com/techartorg/bqt/wiki
Mozilla Public License 2.0
162 stars 23 forks source link

possible option to remove window wrap need #66

Closed hannesdelbeke closed 1 year ago

hannesdelbeke commented 1 year ago

right now we wrap blender in a qt window. this is the source of all our current issues.

unsure if context overrider for operators run from qt would be solved if not wrapped investigated, it doesn't solve https://github.com/techartorg/bqt/issues/62

parent widgets to blender with ctypes

using ctypes on our widget windows might solve the parenting instead https://blenderartists.org/t/floating-windows/1163493/317

wrap widgets in a window managed by blender

Can we either extend Blender source code, creating an official PR in the blender repo to support empty windows. Or investigate what we already can do with the current exposed Python code.

auto parent widgets

but ideally bqt works with 3rd party widgets and their show function. not requiring altering the show function. Can we find a way to do this automatically?

Moved this to it's own issue https://github.com/techartorg/bqt/issues/71

hannesdelbeke commented 1 year ago

(testing in krita to have access to lots of qt widgets setup) can't seem to get windows only, also get tons of other widgets. closest is with if windowtitle() but windows can not have titles. .window() also returns tons of widgets.

from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtCore import Qt

for widget in QApplication.topLevelWidgets():
    if widget.windowTitle() and widget.windowFlags() & Qt.Window:
        print(widget, widget.windowTitle(), widget.window().window())
hannesdelbeke commented 1 year ago

possible option 2: use empty window as parent. make it transparent and frameless. but do children inherit this transparent style? that'd be undesired.

juizoi commented 1 year ago

Would love to hear more about this, getting a bunch of bugs because of the wrapping. From what I can see (although I may be wrong) the only reason we do it is to allow the Qt window to be parented to the Blender window, allowing us to minimise the Blender window and the widget at the same time.

hannesdelbeke commented 1 year ago

Could you make a separate issue for each bug? Would help a lot addressing them

hannesdelbeke commented 1 year ago

also for your awareness. you can set the sys env var BQT_DISABLE_WRAP to 1 to disable wrapping blender in a QWindow. only disadvantage is tools wont stay in focus when clicking blender. but a lot of small bugs will dissapear

juizoi commented 1 year ago

That's good to know, although we're looking to keep that focus unfortunately

hannesdelbeke commented 1 year ago

❌⚠️ didn't get this to work, feels this could potentially work if we figure out how to prevent win from dissapearing. ctypes parenting proved hacky when testing with native blender windows though

I can show the qwidget in a sep window. not parented to blender. But as soon as i use user32.SetParent the widget instantly dissapears.

import sys
import ctypes
# import multiprocessing as mp
from PySide2.QtWidgets import QApplication, QWidget

def run_widget(handle):
    app = QApplication.instance()
    exec = False
    if not app:
        app = QApplication(sys.argv)#
        exec = True
    widget = QWidget()
    widget.show()

    if exec:
        app.exec_()

    user32 = ctypes.windll.user32

    # def set_tool_window(hwnd):
#    GWL_EXSTYLE=-20
#    WS_EX_TOOLWINDOW=0x00000080
#    user32.SetWindowLongPtrW(widget.winId(), GWL_EXSTYLE, WS_EX_TOOLWINDOW)

    # this line makes the widget dissapear instantly.
    user32.SetParent(widget.winId(), handle)

    return widget

def do():
    import bqt.blender_applications.win32_blender_application as win
    blender_hwnd = win.get_blender_window()
    #p = mp.Process(target=run_widget, args=(blender_hwnd,))
    #p.start()
    return run_widget(blender_hwnd)
hannesdelbeke commented 1 year ago

the floatingwindows.py script contains various ctypes methods that might prove usefull.

#dupplicate into new windows 
bpy.ops.screen.area_dupli('INVOKE_DEFAULT')
new_window = bpy.context.window_manager.windows[-1]
area = new_window.screen.areas[-1]
space_data = bpy.context.space_data

FloatingWindows.zip

hannesdelbeke commented 1 year ago

bpy.ops.wm.window_new() creates a new window that doesn't stay in foreground, and shows up in alt tab. this means it's the same as not parenting our qt widgets to blender.

bpy.ops.wm.window_new() creates a new window, can we wrap this in qt. and do that for each widget we show? this would likely work but require devs to manually call a wrap method everytime they show a widget. max and unreal use this setup

played around with some wrapping. but feels a bit hacky

def da():
    app = QApplication.instance()
    exec = False
    if not app:
        app = QApplication(sys.argv)#
        exec = True

    bpy.ops.wm.window_new() 

    user32 = ctypes.windll.user32
    win_h = user32.GetActiveWindow()
    print("new window", win_h)

    # set title, # todo doesnt work reliable
    user32.SetWindowTextW(win_h, "Plugget Qt Manager")

    blender_window = QWindow.fromWinId(win_h)  # also sets flag to Qt.ForeignWindow
    blender_widget = QWidget.createWindowContainer(blender_window)
    blender_widget.show()

    # tool window
    widget = QWidget(blender_widget)
    widget.setWindowFlags(widget.windowFlags() | Qt.Tool)
    widget.show()

    if exec:
        app.exec_()

    return widget, blender_widget, blender_window
hannesdelbeke commented 1 year ago

❌ errors, wrapInstance only works on objects that inherit from QObject

What if we use shiboken wrap? think this might allow to parent qwidgets to blender window without wrapping blender in a qwidget.

Creates a Python wrapper for a C++ object instantiated at a given memory address - the returned object type will be the same given by the user. The type must be a Shiboken type, the C++ object will not be destroyed when the returned Python object reach zero references. If the address is invalid or doesn’t point to a C++ object of given type the behavior is undefined.

asked the AI, not sure how accurate

shiboken.wrapInstance and PySide5.QtWidgets.QWidget.createWindowContainer are both methods for wrapping a C++ object in a Python object, but they have different use cases.

shiboken.wrapInstance is a low-level function provided by the shiboken2 module that allows you to wrap any C++ object in a Python object. It is typically used when working with Qt objects that are not widgets, such as QAbstractItemModel or QAbstractProxyModel.

PySide5.QtWidgets.QWidget.createWindowContainer is a higher-level function provided by the PySide5.QtWidgets module that allows you to create a QWidget container for a QWindow. It is typically used when you want to display a QWindow within a QWidget hierarchy, such as when embedding a 3D view or a video player in a Qt application.

import ctypes
import shiboken2
from PyQt5.QtWidgets import QWidget

win_h = ctypes.windll.user32.GetActiveWindow() # Get the window handle for the active window
win_ptr = ctypes.windll.user32.GetWindowLongPtrW(win_h, ctypes.c_int(0)) # Get the pointer to the window
win_obj = shiboken2.wrapInstance(win_ptr, QWidget)  # Wrap the pointer in a Python object
print(isinstance(win_obj, QWidget))  # Check
hannesdelbeke commented 1 year ago

parenting a new blender window to blender main window with ctypes works, but is really buggy. window also dissapeared in background at some point, and main window was blocked

hannesdelbeke commented 1 year ago

✅ this could be doable: manually manage windows in front or not

event loop could be in blender, but ideally event loop would be in qt. using e.g. qttimers. e.g. anim_bar since Blender operators that run constantly mess with debugging in VS code

hannesdelbeke commented 1 year ago

Using our own manager seems to work quite well. if shows widgets in the foreground when any blender window is active, and hides them when blender looses focus. e.g. by going to another app. see test branch https://github.com/techartorg/bqt/tree/research/focus-no-wrap

we'd need to create some kind of "register/wrap widget" method, to add the widget to our manager.

custom widget focus manager:

PROS

CONS

Other

hannesdelbeke commented 1 year ago

this has gone in with https://github.com/techartorg/bqt/releases/tag/1.1.0 it automatically works if you disabled qt wrapping for bqt by setting the env var BQT_DISABLE_WRAP to 1 it assumes you parented all widgets with bqt.widget_manager.add(my_widget)
update: widgets can now be auto parented.

created new issue to clarify this process in code and docs https://github.com/techartorg/bqt/issues/80

hannesdelbeke commented 1 year ago

interesting to note that this approach would not only work in Blender, but any software that can run python. so we could generalise bqt to work with anything, not just blender