MAXScript Qt UI

MAXScript Qt UI

Swordslayer
Advisor Advisor
4,925 Views
23 Replies
Message 1 of 24

MAXScript Qt UI

Swordslayer
Advisor
Advisor

In the end, using Qt from MAXScript was more straightforward than I thought, kudos to @denisT.MaxDoctor for setting me on the right track with the QtUiLoader. Here's an edited sample from the max python sample scripts:

 

 

(
	local QFile = (python.import "PySide2.QtCore").QFile
	local QUiLoader = (python.import "PySide2.QtUiTools").QUiLoader
	local GetQMaxMainWindow = (python.import "qtmax").GetQMaxMainWindow

	local ui_file = QFile "QtTest.ui"
	ui_file.open QFile.ReadOnly
	local ui = (QUiLoader()).load ui_file (GetQMaxMainWindow())
	ui_file.close()

	fn hello = messageBox "Hello world!"

	ui.pushButton.clicked.connect hello
	ui.toolButton.clicked.connect getSaveFileName
	ui.show()
)

 

Put it in the same folder as the .ui file from the attached zip and run the ms file. The two buttons are connected to maxscript functions, I guess you get the idea. Have fun 🙂

4,926 Views
23 Replies
Replies (23)
Message 2 of 24

denisT.MaxDoctor
Advisor
Advisor

@Swordslayer wrote:

 

 

(
	local QFile = (python.import "PySide2.QtCore").QFile
	local QUiLoader = (python.import "PySide2.QtUiTools").QUiLoader
	local GetQMaxMainWindow = (python.import "qtmax").GetQMaxMainWindow

	local ui_file = QFile "QtTest.ui"
	ui_file.open QFile.ReadOnly
	local ui = (QUiLoader()).load ui_file (GetQMaxMainWindow())
	ui_file.close()

	fn hello = messageBox "Hello world!"

	ui.pushButton.clicked.connect hello
	ui.toolButton.clicked.connect getSaveFileName
	ui.show()
)

 

Very nice snippet!
I'm pretty far from it now, but I've tried ...

 

local GetQMaxMainWindow = (python.import "qtmax"). GetQMaxMainWindow

 

it doesn't work for me in 2020. no "qtmax".

Also I couldn't connect mxs function like:

fn format_time = (format "time:% \ n" (timestamp ()))

 

it connects but does not printout

 

Another question about UNDO for connected functions.

 

defining them as:

 

fn something = undo on (...)

 

it is not a good practice. any ideas?

  

0 Likes
Message 3 of 24

Swordslayer
Advisor
Advisor

In 2020, it'd be MaxPlus instead of qtmax.

As for printing, I guess you'd have to use python print for the output, this works in 2021:

local py = python.import "builtins"
fn format_time = py.print (py.str.format "time: {0}" (timeStamp()))

In 2020, that'd be python.import "__builtin__" instead.

As for the undo, neither that nor automatic redraw are there so yeah, I guess that would be the way to go.

0 Likes
Message 4 of 24

Swordslayer
Advisor
Advisor

Here's a bit more functional snippet using the same .ui file:

 

(
	local QFile = (python.import "PySide2.QtCore").QFile
	local QUiLoader = (python.import "PySide2.QtUiTools").QUiLoader
	local GetQMaxMainWindow = (python.import "qtmax").GetQMaxMainWindow
	local py = python.import "builtins"
	
	local ui_file = QFile "QtTest.ui"
	ui_file.open QFile.ReadOnly

	if isProperty ::testDialog #close do testDialog.close()
	global testDialog = (QUiLoader()).load ui_file (GetQMaxMainWindow())
	ui_file.close()

	local filename
	fn print_filename = py.print (py.str.format "filename: {0}" filename)
	fn get_filename = filename = getOpenFileName()

	testDialog.pushButton.setText "Print filename"
	testDialog.pushButton.setToolTip "Click the '...' button to pick file."
	testDialog.pushButton.clicked.connect print_filename
	testDialog.toolButton.clicked.connect get_filename
	testDialog.show()
)

The '...' button updates the internal filename var with the picked file and the other button prints it in listener. I've made the testDialog global so that it can be easily closed if it already exists.

0 Likes
Message 5 of 24

Swordslayer
Advisor
Advisor

Creating the UI directly without the help of a .ui file seems to work rather nice as well:

 

(
	local QtWidgets = python.import "PySide2.QtWidgets"
	local GetQMaxMainWindow = (python.import "qtmax").GetQMaxMainWindow

	if isProperty ::testDialog #close do testDialog.close()
	global testDialog = QtWidgets.QDialog(GetQMaxMainWindow())
	testDialog.setWindowTitle "Pyside Qt Window"

	local dialog_layout = QtWidgets.QVBoxLayout()
	local label = QtWidgets.QLabel "Click button to create a cylinder in the scene"
	dialog_layout.addWidget label

	fn make_cylinder = with undo on Cylinder isSelected:on

	local cylinder_btn = QtWidgets.QPushButton "Cylinder"
	cylinder_btn.clicked.connect make_cylinder
	dialog_layout.addWidget cylinder_btn

	testDialog.setLayout dialog_layout
	testDialog.show()
)

 

 

0 Likes
Message 6 of 24

Swordslayer
Advisor
Advisor

It's kinda fun to play with, that's for sure.

 

(
	local QtWidgets = python.import "PySide2.QtWidgets"
	local GetQMaxMainWindow = (python.import "qtmax").GetQMaxMainWindow
	local py = python.import "builtins"

	if isProperty ::teapotRotator #close do teapotRotator.close()
	global teapotRotator = QtWidgets.QDialog(GetQMaxMainWindow())
	local layout = QtWidgets.QGridLayout()

	local dials = for i = 1 to 3 collect
	(
		local btn = QtWidgets.QPushButton ("Zero " + #("X", "Y", "Z")[i] + " Rot")
		layout.addWidget btn 0 i
		btn.clicked.connect (execute ("(fn zeroRot = teapotRotator.zeroDials #{" + i as string + "})"))

		local dial = QtWidgets.QDial()
		layout.addWidget dial 1 i
		dial.setMaximum 360
		dial.setWrapping on
		dial.valueChanged.connect (execute ("(fn rot val = teapotRotator.setPotRot val " + i as string + ")"))
		dial.sliderPressed.connect (fn startHold = theHold.superBegin())
		dial.sliderReleased.connect (fn acceptHold = theHold.superAccept "Teapot Rotate")
		dial
	)
	
	local togglePushButton = QtWidgets.QPushButton "Zero All"
	togglePushButton.clicked.connect (fn zeroAll = teapotRotator.zeroDials #{1..3})
	layout.addWidget togglePushButton 2 2

	delete $TeapotRot*
	local pot = Teapot prefix:"TeapotRot" isSelected:on

	py.setattr teapotRotator "setPotRot" (
		fn setRot val i =
		(
			if isValidNode pot do with undo on pot[3][2][i].value = val
			redrawViews()
		)
	)
	py.setattr teapotRotator "zeroDials" (
		fn zeroDials idx = for i in idx do dials[i].setValue 0
	)
	teapotRotator.setWindowTitle "Teapot Rotator"
	teapotRotator.setLayout layout
	teapotRotator.show()
)

 

0 Likes
Message 7 of 24

denisT.MaxDoctor
Advisor
Advisor

@Swordslayer wrote:

It's kinda fun to play with, that's for sure.

 

 

btn.clicked.connect (execute ("(fn zeroRot = teapotRotator.zeroDials #{" + i as string + "})"))

 

 


so soon you will get to lambdas 😉

0 Likes
Message 8 of 24

denisT.MaxDoctor
Advisor
Advisor

In any case, you are on the right track. The main idea behind using Qt is to create dynamic interfaces, which is a big problem for MXS.
Speaking of some of the issues above ...
I'm pretty sure we can find a workaround for most of the problems (like format, print), but that's not a solution. I need to be aware of all the limitations before starting any big project. In this case, I just want to first understand why the MXS format (print) is not working. Is this a main thread or basic output stream problem? Are they different for mxs and python, Qt(PySide2)? How could this happen?


Another one...
Attempting to UNDO a connected function may result in MAX failure if the function has not been run undoable.

 

0 Likes
Message 9 of 24

Swordslayer
Advisor
Advisor

Okay, that's kinda ugly, would you prefer this from the point of 'nice to read' code? 🙂

 

local partial = (python.import "functools").partial
fn zeroRot arg = teapotRotator.zeroDials #{arg}
...
btn.clicked.connect(partial zeroRot i)

 

0 Likes
Message 10 of 24

Swordslayer
Advisor
Advisor

@denisT.MaxDoctor wrote:

I'm pretty sure we can find a workaround for most of the problems (like format, print), but that's not a solution. I need to be aware of all the limitations before starting any big project. In this case, I just want to first understand why the MXS format (print) is not working. Is this a main thread or basic output stream problem? Are they different for mxs and python, Qt(PySide2)? How could this happen?


Another one...
Attempting to UNDO a connected function may result in MAX failure if the function has not been run undoable.


I'd expect the possible problems are the same as if you were usin pymxs from python i.e. this would apply (scroll down to MAXScript Undo and Redo and Printing to the Listener).

0 Likes
Message 11 of 24

denisT.MaxDoctor
Advisor
Advisor

@Swordslayer wrote:

I'd expect the possible problems are the same as if you were usin pymxs from python i.e. this would apply (scroll down to MAXScript Undo and Redo and Printing to the Listener).


before I start a serious project with MXS - Qt, I will test the entire pipeline ... as far as possible, I will keep you friends informed and updated

Message 12 of 24

Swordslayer
Advisor
Advisor

Sounds great, can't wait to see what you find. If there's anyone who could shed more light on this, it'd probably be @eric.brosseau or @attilaszabo (though with the next version release geting nearer and nearer, I imagine they're pretty busy).

0 Likes
Message 13 of 24

denisT.MaxDoctor
Advisor
Advisor

I have quite a lot of experience with Qt (Maya) user interfaces. As my experience shows, serious tools cannot be created without events. Connections are usually not enough. My guess is that Qt events can be a problem in MXS because additional Qt coding (override, extend) is always required.

0 Likes
Message 14 of 24

Swordslayer
Advisor
Advisor

@denisT.MaxDoctor wrote:

I have quite a lot of experience with Qt (Maya) user interfaces. As my experience shows, serious tools cannot be created without events. Connections are usually not enough. My guess is that Qt events can be a problem in MXS because additional Qt coding (override, extend) is always required.


Hmmm, did I miss something or can't you just a) replace the default event with a custom one, b) store the original if you want to extend it? 

 

 

 

 

(
	local QtWidgets = python.import "PySide2.QtWidgets"
	local GetQMaxMainWindow = (python.import "qtmax").GetQMaxMainWindow

	if isProperty ::testDialog #close do testDialog.close()
	global testDialog = QtWidgets.QDialog(GetQMaxMainWindow())
	testDialog.mousePressEvent = fn mousePressEvent evnt = py.print (py.str.format "dialog click at {}" (evnt.pos()))
	testDialog.setWindowTitle "Pyside Qt Window"
	testDialog.setFixedSize 250 250

	local button = QtWidgets.QPushButton "Button" parent:testDialog
	button.clicked.connect (fn _ = py.print "original click")
	local origEvnt = button.mousePressEvent
	button.mousePressEvent = fn __ evnt = (py.print "extended click"; origEvnt evnt)

	testDialog.show()
)

 

 

 

0 Likes
Message 15 of 24

denisT.MaxDoctor
Advisor
Advisor

I don't like mixing code with different languages in one project. This is why I don't like using .NET in combination with MXS. And I don't use this combination lately, although in the past I have done many projects with mix of MXS and C#.
I usually make the whole interface in MXS and C++ now.

So it's okay for me to use the MXS python interface with access to Qt, but I don't want to do additional coding in Python and then use extended or custom modules.


  

0 Likes
Message 16 of 24

denisT.MaxDoctor
Advisor
Advisor

@Swordslayer wrote:

@denisT.MaxDoctor wrote:

I have quite a lot of experience with Qt (Maya) user interfaces. As my experience shows, serious tools cannot be created without events. Connections are usually not enough. My guess is that Qt events can be a problem in MXS because additional Qt coding (override, extend) is always required.


Hmmm, did I miss something or can't you just a) replace the default event with a custom one, b) store the original if you want to extend it? 


Oh. I didn't make myself clear. I mean a situation where a class (object) doesn't have a specific event and you need to add it.
Qt also has a very interesting mechanism called eventFilter that I may miss.

... looked just now ... hmm ... like you can write event filters in MXS.
It's a pity, but now there is no time to try.

0 Likes
Message 17 of 24

denist.dts
Explorer
Explorer

@denisT.MaxDoctor wrote:

@Swordslayer wrote:

 

 

Another question about UNDO for connected functions.

 

defining them as:

 

fn something = undo on (...)

 

it is not a good practice. any ideas?

  


we can wrap connection function with undo and other contexts like:

partial = (python.import "functools").partial

fn runfn undotext fun = 
(
	undo undotext on
	(
		fun()
	)
)
fn deleteAll = (delete objects)

ww.Button_1.clicked.connect (partial runfn "Select None" clearSelection)
ww.Button_2.clicked.connect (partial runfn "Delete All" deleteAll)
0 Likes
Message 18 of 24

denist.dts
Explorer
Explorer

can we pack with partial optional arguments as well:

fn deleteNodesByClass class: = 
(
	delete (for node in objects where iskindof node class collect node)
)
-- partial this: ???
deleteNodesByClass class:point
0 Likes
Message 19 of 24

Anonymous
Not applicable

Hi,

 

How would I trigger close events using this method ?

Can't find a way...

Thanks !

0 Likes
Message 20 of 24

denisT.MaxDoctor
Advisor
Advisor

@Anonymous wrote:

 

How would I trigger close events using this method ?

Can't find a way...


Please open a new topic with a full and clear title and question.

 

0 Likes