Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

possible option to remove window wrap need #66

Closed
hannesdelbeke opened this issue Mar 24, 2023 · 15 comments
Closed

possible option to remove window wrap need #66

hannesdelbeke opened this issue Mar 24, 2023 · 15 comments
Labels
enhancement New feature or request

Comments

@hannesdelbeke
Copy link
Collaborator

hannesdelbeke commented Mar 24, 2023

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 #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 #71

@hannesdelbeke
Copy link
Collaborator Author

hannesdelbeke commented Mar 24, 2023

(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
Copy link
Collaborator Author

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
Copy link

juizoi commented May 4, 2023

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
Copy link
Collaborator Author

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

@hannesdelbeke
Copy link
Collaborator Author

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
Copy link

juizoi commented May 4, 2023

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

@hannesdelbeke
Copy link
Collaborator Author

hannesdelbeke commented Jun 4, 2023

⚠️ 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
Copy link
Collaborator Author

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
Copy link
Collaborator Author

hannesdelbeke commented Jun 4, 2023

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
Copy link
Collaborator Author

hannesdelbeke commented Jun 4, 2023

❌ 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
Copy link
Collaborator Author

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
Copy link
Collaborator Author

hannesdelbeke commented Jun 5, 2023

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

  • create empty invisible qt window/widget that's in foreground
  • parent new widget to this QApp.blender_widget
  • start a monitoring loop to check active window.
import ctypes
user32 = ctypes.windll.user32
w = user32.GetActiveWindow()

if blender is active, bring the blender_widget to foreground. set flag always on top?
if qt widget that has rootnode blender_widget is active, same
if any other widget is active, always on top is set to false allowing other windows in front

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
Copy link
Collaborator Author

hannesdelbeke commented Jun 5, 2023

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:

  • register widgets with bqt's widget manager
  • an event loop checks if blender is in focus
    • if yes: set widgets to WindowStaysOnTopHint and show them
    • if no: hide the widget.
      this results in widgets being parented to Blender (they stay in focus, in front of Blender)
      but when Blender is not active, the widgets dissapear.

PROS

  • Since Blender is not wrapped in qt, it wont have various bugs e.g. focus issues, fullscreen not working, ...

CONS

  • every widget needs to be registered, it won't just work like with bqt where we can just do QWidget().show()
    Since for full bqt functionality we already have to do 1 line to parent the widget to QApp.blender_widget, so 1 line to register instead should not be an issue.
  • you can't have a qt widget out of focus in the background, e.g. a loading bar you want to watch while doing something else

Other

  • this doesn't fix operator issues from qt, no change here

@hannesdelbeke hannesdelbeke added the enhancement New feature or request label Jun 5, 2023
@hannesdelbeke
Copy link
Collaborator Author

hannesdelbeke commented Jun 6, 2023

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 #80

@hannesdelbeke
Copy link
Collaborator Author

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants