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?
Solved! Go to Solution.
Solved by denisT.MaxDoctor. Go to Solution.
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()
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
Another question then - can I embed MAXscript rollout into QT window as layout, or something like that?
The answer is obvious... of course, you can... Almost the entire MAX user interface is made this way now. 😁
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()
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... 😎
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)
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?
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?
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.
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.
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
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()
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?
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 👏👏👏
@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...
Can't find what you're looking for? Ask the community or share your knowledge.