UIAccessor mini tutorial – How to control Make Preview dialog

I have been using jpg sequence for my Make Preview output. It is allways easier and more flexible to deal with image sequence than avi, mov or mp4.

The problem is Make Preview windows is the one of the old window which doesn’t have full exposure to Mmaxscript.  3dsMax dev added more argument for createPreview method in 3dsMax 2020. But, unfortunately some of option in the Make Preview dialog is still not available for Maxscript.

But, that doesn’t mean you can not set Make Preview automatically. 3dsMax has the ultimate hack(?) for controlling any UI component. UIAccessor and DialogMonitorOPS.

This allow you to emulate user interaction with UI like clicking button, choosing dropdown items and pressing Enter with Maxscript.

If you don’t want all these, Download the final template.

Skeleton code of DialogMonitorOPS  

Let’s start with very simple script.

fn setMakePreview = (
	local WindowHandle = DialogMonitorOPS.GetWindowHandle()
	local WindowTitle =  (UIAccessor.GetWindowText WindowHandle)

	if WindowTitle == "Make Preview" then (
		print "Hello"
	)
	True
)

DialogMonitorOPS.enabled = true
DialogMonitorOPS.RegisterNotification setMakePreview id:#setMakePreview  

max preview
	
DialogMonitorOPS.unRegisterNotification id:#setMakePreview 
DialogMonitorOPS.enabled = false 

DialogMonitorOPS.enabled = true

First, you need to turn on DialogMonitorOPS.so 3dsMax can monitor any UI. Of course, you don’t want to turn on this all the time. So, after our job is done, we will turn off.

DialogMonitorOPS.RegisterNotification setMakePreview id:#setMakePreview

Then, resister your function to run(setMakePreview) and give id. Again, after our job is done, make sure to unresister.

max preview

This runs Make Preview

DialogMonitorOPS.enabled = true
DialogMonitorOPS.RegisterNotification setMakePreview id:#setMakePreview

Unresister setMakePreview and turn off DialogMonitorOPS

Now let’s see the setMakePreview fucntion. This function will run all the time while DialogMonitorOPS is running.

The most important thing to know is that this function need to return true at the end. I forgot why. But, you MUST do it. So, just do it.

local WindowHandle = DialogMonitorOPS.GetWindowHandle()

How would you let Maxscript know which UI you want to control? We will use window handle or hwnd which is a unique id of each UI element. The above line will give is the handle of window which DialogMonitorOPS detected.

local WindowTitle = (UIAccessor.GetWindowText WindowHandle)

Then, this above line will give us the title of dialog.

if WindowTitle == “Make Preview” then (
print “Hello”
)
True

DialogMonitorOPS will check if the dialog is “Make Preview” dialog. If so, it will print Hello.

Let’s set custom output path

From now on I’ll only show setMakePreview function.

fn setMakePreview = (
	local WindowHandle = DialogMonitorOPS.GetWindowHandle()
	local WIndowTitle =  (UIAccessor.GetWindowText WindowHandle)

	if WindowTitle == "Make Preview" then (
		for i in (windows.getChildrenHWND WindowHandle) do (format "%\n" i)
		UIAccessor.PressButtonByName WindowHandle "File..."
	)
	True
)

I removed print “Hello” and added UIAccessor.PressButtonByName WindowHandle “File…”. As you can read, this will find a button named “File…” and press it for you.

for i in (windows.getChildrenHWND WindowHandle) do (format “%\n” i)

What does this do? It just printed a bunch of things in Maxscript listener. This is how we sees what kinds of UI element is in the current dialog and fid a way to access each UI element. As I said in the begining, we use windows handle to specify UI element. This line will print out the information of all children of the dialog with given handle, Make Preview dialog. it gives us an array for each UI element. The important ones are first(hwnd of child), forth(UI type) and fifth( displayed text).

fn setMakePreview = (
	local WindowHandle = DialogMonitorOPS.GetWindowHandle()
	local WindowTitle =  (UIAccessor.GetWindowText WindowHandle)

	if WindowTitle == "Make Preview" then (
		UIAccessor.PressButtonByName WindowHandle "File..."
	)
	if WindowTitle == "Create Animated Sequence File..." then (
		-- Set cusom output path
		local edits = for i in (windows.getChildrenHWND WindowHandle) where i[4] == "Edit" collect i[1]
		uiaccessor.setwindowtext edits[1] @"c:\temp\test_.jpg"	
		UIAccessor.PressButtonByName WindowHandle "&Save"
	)		
	True
)

Because we pressed “File…” button. A new dialog pops up, “Create Animated Sequence File…”. In this dialog, we need to these.

  1. Set custom output path
  2. Set Save as Type to jpg
  3. Press Save button

To set custom output path, we need to know hwnd of path input UI. But, if you check fifth item of array. Text input doesn’t have name! What should I do? Other information we have is type of control on fourth item. The type UI you can input text is “Edit”. So, I collected hwnd of “Edit”s. Fortunately 3dsMax seems collecting UI info in the same order from top to bottom. So, let’s try on the first one. You can use uiaccessor.setwindowtext to set value on Spinner of Edit. If you want to use own naming convention. Replace @”c:\temp\test_.jpg” with own function or variable.

uiaccessor.setwindowtext edits[1] @”c:\temp\test_.jpg”

The, press “Save” button.

UIAccessor.PressButtonByName WindowHandle “&Save”

Wait? why the name iis “&Save”. How do I know I need &? I also don’t know where & come from. But, I know “Save” did not work. So, I printed out all child UI elem data and checked the names.

Did it work? Maybe or Maybe not. Because 3dsmax remembers the format you used last time, if it was not jpg, Make Preview window will automatically switch to the format. So, we need to choose jpg from format dropdown. Now this is real fun!

fn setMakePreview = (
	local WindowHandle = DialogMonitorOPS.GetWindowHandle()
	local WindowTitle =  (UIAccessor.GetWindowText WindowHandle)

	if WindowTitle == "Make Preview" then (
		UIAccessor.PressButtonByName WindowHandle "File..."
	)
	if WindowTitle == "Create Animated Sequence File..." then (

		local edits = for i in (windows.getChildrenHWND WindowHandle) where i[4] == "Edit" collect i[1]
		uiaccessor.setwindowtext edits[1] @"c:\temp\reallyanothertest_.jpg"	

		local comboboxes = for i in (windows.getChildrenHWND WindowHandle) where i[4] == "ComboBox" collect i[1]
		local filetypeHwnd = comboboxes[3] 
		
		local CB_SHOWDROPDOWN = 0x014F
		local CB_SETCURSEL = 0x014E 
		local WM_LBUTTONDOWN = 0x0201
		local WM_LBUTTONUP = 0x0202
		windows.sendMessage filetypeHwnd CB_SHOWDROPDOWN 1 0 -- Open combobox dropdown
		windows.sendMessage filetypeHwnd CB_SETCURSEL 7 0 -- Select 7th item
		windows.sendMessage filetypeHwnd WM_LBUTTONDOWN 0 -1  -- Press left mouse button	
		windows.sendMessage filetypeHwnd WM_LBUTTONUP 0 -1  -- Raise left mouse button
		windows.sendMessage filetypeHwnd CB_SHOWDROPDOWN 0 0    -- Close dropdown
		
		UIAccessor.PressButtonByName WindowHandle "&Save"
	)		
	True
)

I guess you already have figured out what this does. Yes, it collect hwnd of all comboboxes.Them 3rd one was the Save As Type dropdown.

local comboboxes = for i in (windows.getChildrenHWND WindowHandle) where i[4] == “ComboBox” collect i[1]
local filetypeHwnd = comboboxes[3]

All cool. Butn thet the heck is the next lines?

windows.sendMessage Sends a Win32 message to the HWND specified in the first argument. This is how you emulate UI interaction programmatically.

I commented on the code what each lines does. But, you may think how am I suppose to know all the secret code?

CGTalk maxscript forum has a lot of answers for common operations. You can also google windows message reference like this.

Now since you set jpg as a new format, JPEG Image Control windows pops up. This one is easy. We can just press OK button like this.

if WIndowTitle == “JPEG Image Control” then ( UIAccessor.PressButtonByName WindowHandle “OK” )

How about other controls like checkbox?

Since checkbox text usually doesn’t change, we can search the string pattern of fifth item to find hwnd. This is function to get hwnd using UI name. Then you can BM_SETCHECK window message to check the checkbox. if the first argument is 1, the chebox will be checked. If it is 0, the checkbox will be unchecked.

fn getChildHwndByName parent_hwnd childUIname = (
    local child_hwnd = 0
    for i in (windows.getChildrenHWND parent_hwnd) where matchPattern i[5] pattern:childUIname do (child_hwnd = i[1])
    child_hwnd
)
local frameNumHwnd = (getChildHwndByName WindowHandle  "Frame Numbers" )
windows.sendMessage frameNumHwnd BM_SETCHECK 1 0

Runscript after Make Preview is done

If you want to automatically run image sequence player like PDPlayer or RAMPlayer or resister to Shotgun, simple add the code after max preview.

Final template code

Here is the cleaned final template code. If you don’t want to read all this, start from this.

Download the final template.

This is made in 3dsMax 2019. Other version might not work with this if there is UI difference.

3dsMax 2020 Preview Enhancement

3dsMax 2020 has some nice improvement for Make Preview.

  • Much faster. 1.5 – 3x faster creation on local drives
  • Capture size greater than viewport dimensions supported
  • “Quality” setting accessible from Preview UI (Nitrous only)
  • Default preview filename based on current scene filename
  • 100% output resolution on by default
  • MXS snippet can be executed per frame for custom strings
  • Filename and MXS snippet values can be specified from MXS command line of CreatePreview()
  • After executing the preview, the time slider is returned to the original starting frame
  • “Play when done” accessible from Preview UI
  • If running from MXS command line, avoid dialog boxes, output to listener instead

3dsMax 2020 also has the bug fix for “User defined” Per-view preset missing issue. This issue is related to the permission. If you are still on older version. Make sure to open the permission for folders under 3dsMax root to be able to choose “User Defined” Per-view preset in Make Preview. Or, upgrade to 2020.