PySide2 widget to drag and drop materials?

PySide2 widget to drag and drop materials?

inquestudios
Enthusiast Enthusiast
4,481 Views
41 Replies
Message 1 of 42

PySide2 widget to drag and drop materials?

inquestudios
Enthusiast
Enthusiast

I'm trying to develop a simple tool that allows loading and saving of material libraries

AND also drag and drop materials from my widget to material editor and reverse.

What QMimeData type should I set to drop INTO material editor?

 

I have set def dropEvent to accept all drops right now, it even triggers when I drop data from external application like files from windows explorer. But when I try to drag from material editor slot, it only activates over material slots and viewport objects. How can I force my widget to accept material drops?

0 Likes
Accepted solutions (1)
4,482 Views
41 Replies
Replies (41)
Message 2 of 42

inquestudios
Enthusiast
Enthusiast

Simplified code:

 

import pymxs, PySide2
from PySide2.QtWidgets import QHBoxLayout, QWidget, QPushButton
from PySide2.QtCore import Qt, QMimeData
from PySide2.QtGui import QDrag

rt = pymxs.runtime
rt.clearListener()

materialArr = []
maxWindow = QWidget.find(rt.windows.getMaxHWnd())

libFileName = rt.getOpenFileName(caption = "Select library to load", types = "Material library(*.mat)|*.mat")
if libFileName != None:
    clib = rt.loadTempMaterialLibrary(libFileName)
    for i in clib:
        materialArr.append(i)

class DragButton(QPushButton):
    def mouseMoveEvent(self, e):
        if e.buttons() == Qt.LeftButton:
            drag = QDrag(self)
            mime = QMimeData()
            mime.setUrls([])
            drag.setMimeData(mime)
            drag.exec_(Qt.MoveAction)

class tWindow(PySide2.QtWidgets.QMainWindow):
    def __init__(self, parent = maxWindow):
        super(tWindow, self).__init__(parent)
        self.setWindowTitle('Drag test')
        self.setMouseTracking(True)
        self.setAcceptDrops(True)
        self.blayout = QHBoxLayout()
        for l in materialArr:
            btn = DragButton(l.name)
            #btn = QPushButton(l)
            self.blayout.addWidget(btn)
        mw = QWidget()
        self.setCentralWidget(mw)
        mw.setLayout(self.blayout)
    
    def dragEnterEvent(self, e):
        e.accept()

    def dropEvent(self, e):
        pos = e.pos()
        widget = e.source()
        print(e.pos())
        e.accept()
tw = tWindow()
tw.show()

 

0 Likes
Message 3 of 42

denisT.MaxDoctor
Advisor
Advisor

The simple answer is, "You can't."

MAX has and uses its own Drag-n-Drop (DAD) mechanism that has nothing to do with Qt, .NET, or Win

 

But there is a complicated way to do it... and it's the only one I see as possible unless you use c++ MAX and Qt SDKs...

The hard way is to add a MAX Rollout control (specifically MaterialButton) to the Qt widget. After that MAX DAD should work as an embedded one

 

0 Likes
Message 4 of 42

inquestudios
Enthusiast
Enthusiast

Another question then - can I embed MAXscript rollout into QT window as layout, or something like that?

0 Likes
Message 5 of 42

denisT.MaxDoctor
Advisor
Advisor

The answer is obvious... of course, you can... Almost the entire MAX user interface is made this way now. 😁

0 Likes
Message 6 of 42

inquestudios
Enthusiast
Enthusiast

Very well, could you supply any example? How can I put simple rollout

 

 

 

rollout mbtest "Matbuttons"
(
	materialButton mb1 "Test" width:100 height:100
)

 

 

 

into 

PySide2.QtWidgets.QMainWindow

 

 

import pymxs, PySide2
rt = pymxs.runtime
mxsstr = 'rollout mbtest "Matbuttons"\n(\n\tmaterialButton mb1 "Test" width:100 height:100\n)'
rt.execute(mxsstr)
maxWindow = PySide2.QtWidgets.QWidget.find(rt.windows.getMaxHWnd())
class CBrowser(PySide2.QtWidgets.QMainWindow):
    def __init__(self, parent = maxWindow):
        super(CBrowser, self).__init__(parent)
        ???
        self.setCentralWidget(???)
        self.resize(850, 600)
cWindow = CBrowser()
cWindow.show()

 

 


 

0 Likes
Message 7 of 42

denisT.MaxDoctor
Advisor
Advisor
Accepted solution

If I told you everything, it wouldn't be interesting!
Besides, Swordslayer and I have already shown how to do it, maybe not on this forum, but on some other forum.

But I'll give you a hint:

rollout mbtest "Matbuttons"
(
	materialButton mb1 "Test" width:100 height:100
)
createdialog mbtest
cui.RegisterDialogBar mbtest 

 

The only thing left to do is to change the Qt parent... 😎

Message 8 of 42

denisT.MaxDoctor
Advisor
Advisor

By the way what MAX version do you use?

0 Likes
Message 9 of 42

inquestudios
Enthusiast
Enthusiast

I'm developing my tool for 2019 and later.


So, looks like after cui.RegisterDialogBar rollout becomes a child widget for main 3ds MAX window.
That's the game changer, thank you for the good lead.

import pymxs, PySide2
rt = pymxs.runtime

rt.clearListener()

mxsstr = 'rollout mbtest "Matbuttons"\n(\n\tmaterialButton mb1 "Test" width:100 height:100\n)\ncreatedialog mbtest\ncui.RegisterDialogBar mbtest'
rt.execute(mxsstr)
maxWindow = PySide2.QtWidgets.QWidget.find(rt.windows.getMaxHWnd())
def procCh(wd):
    chArr = wd.children()
    for cch in chArr:
        if type(cch) == PySide2.QtWidgets.QDockWidget:
            if cch.windowTitle() == 'Matbuttons':
                print(cch)
procCh(maxWindow)
0 Likes
Message 10 of 42

inquestudios
Enthusiast
Enthusiast

Ok, I've managed to find and reparent widget with materialbuttons!

import pymxs, PySide2
rt = pymxs.runtime

rt.clearListener()

mxsstr = 'rollout mbtest "Matbuttons"\n(\n\tmaterialButton mb1 "Test1" width:100 height:100 across:3\n\tmaterialButton mb2 "Test2" width:100 height:100\n\tmaterialButton mb3 "Test3" width:100 height:100\n)\ncreatedialog mbtest width:450\ncui.RegisterDialogBar mbtest'
#print(mxsstr)
rt.execute(mxsstr)
maxWindow = PySide2.QtWidgets.QWidget.find(rt.windows.getMaxHWnd())
def procCh(wd):
    chArr = wd.children()
    for cch in chArr:
        if type(cch) == PySide2.QtWidgets.QDockWidget:
            if cch.windowTitle() == 'Matbuttons':
                return cch
rw = procCh(maxWindow)
stuctArr = rw.children()
class CBrowser(PySide2.QtWidgets.QMainWindow):
    def __init__(self, parent = maxWindow):
        super(CBrowser, self).__init__(parent)
        self.setCentralWidget(stuctArr[5])
        self.resize(850, 600)
cWindow = CBrowser()
cWindow.show()
print(cWindow.centralWidget())

The bad news - I can't find the way to access elements inside the widget.
children() returns some useless QPropertyAnimation
layout() returns None
How can I address individual buttons?

0 Likes
Message 11 of 42

denisT.MaxDoctor
Advisor
Advisor

@inquestudios wrote:


How can I address individual buttons?



by HWHD

Message 12 of 42

inquestudios
Enthusiast
Enthusiast

I have collected
<rollout>.controls[<index>].hwnd
ran
PySide2.QtWidgets.QWidget.find()
with this parameter and got None
What am I doing wrong?
Should I get winId from reparented widget and offset from there?

0 Likes
Message 13 of 42

denisT.MaxDoctor
Advisor
Advisor

The hwnd of the Rollout control is an array... because a control can consist of multiple windows. For example, a spinner has a body control and a caption control... and so on. In short, you usually use the first hwnd in the array.

0 Likes
Message 14 of 42

denisT.MaxDoctor
Advisor
Advisor

Is it some kind of challenge to make a tool in Python?
That's fine. But the implementation of Python in versions 2019 and 2025 is very different. You will have problems supporting all these versions with more or less the same code.

0 Likes
Message 15 of 42

inquestudios
Enthusiast
Enthusiast

Here's the code:

 

import pymxs, PySide2
rt = pymxs.runtime

rt.clearListener()

mxsstr = 'rollout mbtest "Matbuttons"\n(\n\tmaterialButton mb1 "Test1" width:100 height:100 across:3\n\tmaterialButton mb2 "Test2" width:100 height:100\n\tmaterialButton mb3 "Test3" width:100 height:100\n)\ncreatedialog mbtest width:450\ncui.RegisterDialogBar mbtest\n'
print(mxsstr)
rt.execute(mxsstr)
maxWindow = PySide2.QtWidgets.QWidget.find(rt.windows.getMaxHWnd())
def procCh(wd):
    chArr = wd.children()
    for cch in chArr:
        if type(cch) == PySide2.QtWidgets.QDockWidget:
            if cch.windowTitle() == 'Matbuttons':
                return cch
rw = procCh(maxWindow)
for i in rt.mbtest.controls:
    chwnd = i.hwnd[0]
    print(PySide2.QtWidgets.QWidget.find(chwnd))
stuctArr = rw.children()
class CBrowser(PySide2.QtWidgets.QMainWindow):
    def __init__(self, parent = maxWindow):
        super(CBrowser, self).__init__(parent)
        self.setCentralWidget(stuctArr[5])
        self.resize(850, 600)
cWindow = CBrowser()
cWindow.show()

 

And the output:

 

rollout mbtest "Matbuttons"
(
	materialButton mb1 "Test1" width:100 height:100 across:3
	materialButton mb2 "Test2" width:100 height:100
	materialButton mb3 "Test3" width:100 height:100
)
createdialog mbtest width:450
cui.RegisterDialogBar mbtest

None
None
None

 

I'm getting hwnd for every control, but QWidget.find(<hwnd>) returns None
Let's assume I am working with 3ds MAX 2019 only

0 Likes
Message 16 of 42

denisT.MaxDoctor
Advisor
Advisor

What are you searching for? You already have an hwnd for WinAPI methods and UI control to access its properties, such as material.

 

import pymxs, PySide2
rt = pymxs.runtime

rt.clearListener()

mxsstr = '''
rollout mbtest "Matbuttons"
(
	materialButton mb1 "Test1" width:100 height:100 across:3
	materialButton mb2 "Test2" width:100 height:100 
	materialButton mb3 "Test3" width:100 height:100


	on mbtest open do 
	(
		mb1.material = Standard name:#MAT_RED diffuse:red
		mb2.material = Standard name:#MAT_GRN diffuse:green
		mb3.material = Standard name:#MAT_BLU diffuse:blue
	)
)
createdialog mbtest width:450
cui.RegisterDialogBar mbtest
'''


print(mxsstr)
rt.execute(mxsstr)
maxWindow = PySide2.QtWidgets.QWidget.find(rt.windows.getMaxHWnd())
def procCh(wd):
    chArr = wd.children()
    for cch in chArr:
        if type(cch) == PySide2.QtWidgets.QDockWidget:
            if cch.windowTitle() == 'Matbuttons':
                return cch
rw = procCh(maxWindow)
for i in rt.mbtest.controls:
    chwnd = i.hwnd[0]
    print('hwnd:{0} control:{1} (by name >> {2}) mat:{3}'.format(chwnd, i, getattr(rt.mbtest, getattr(i, 'name')) ,getattr(i,'Material')))
    ##print(''.format(chwnd, i, '==', getattr(rt.mbtest, getattr(i, 'name')) , 'mat:', getattr(i,'Material'))
stuctArr = rw.children()
class CBrowser(PySide2.QtWidgets.QMainWindow):
    def __init__(self, parent = maxWindow):
        super(CBrowser, self).__init__(parent)
        self.setCentralWidget(stuctArr[5])
        self.resize(850, 600)
cWindow = CBrowser()
cWindow.show()

 



Message 17 of 42

inquestudios
Enthusiast
Enthusiast

So materialbuttons itself could not be accessed it qt at all, as they are NOT a QT widgets?
And how do I use winAPI with that? Can I override these buttons behavior, so they do not show material browser on left click and just check/uncheck like standard checkButton, but still with material drag and drop?

0 Likes
Message 18 of 42

denisT.MaxDoctor
Advisor
Advisor

they are not Qt... 

0 Likes
Message 19 of 42

inquestudios
Enthusiast
Enthusiast

So can I disable material browser on left click?
On/off functinality could be made by switching button image with on_picked handler.


P.S. in 2019 if you click materialButton and without closing material browser click it again 3ds MAX crashes.
Bravo, Autodesk 👏👏👏

0 Likes
Message 20 of 42

denisT.MaxDoctor
Advisor
Advisor

@inquestudios wrote:

P.S. in 2019 if you click materialButton and without closing material browser click it again 3ds MAX crashes.
Bravo, Autodesk 👏👏👏


Very unlikely... the material browser window that opens is modal, it doesn't allow you to click anything else...

0 Likes