Get all selected modifiers from stack


#7

I’m trying to find the HWND of the layer manager…

print (windows.getChildrenHWND #max)

Not finding it…


#8

root parent is not MAX probably. try to search on child windows.

windows.getChildrenHWND 0

#9

Cheers found it…

print (windows.getChildrenHWND 0)

#(6292968P, 65552P, 477026P, “#32770”, “Layer: 0 (default)”, 477026P, 6292968P, 477026P)

I can get the Hwnd of the SysListView32

(windows.getChildrenHWND 6292968P)[1]
#(1787606P, 6292968P, 6292968P, “SysListView32”, “List1”, 0P, 6292968P, 477026P)

I’m always a bit lost with the dark arts of the windows messages, how do you find out what messages to send???


#10

for SysteListView32 you need another messages… i’ve googled “SysteListView32”.
the first i found http://www.codeproject.com/Questions/238246/Getting-Setting-SysListView-control-items

there i see that another messages have to be used :slight_smile:


#11

It might not be so simple if the object has references or if some sub object levels are expanded.


#12

it’s not easy for sure :slight_smile:

but we can read text of items… and probably filter ‘modifier items’ using some criteria.

references and multiple-node selection case is tricky. it needs full understanding of what the system shows in modifier stack in these cases.

but as i said. i don’t know any other why (including using of sdk) to get a list of selected modifiers


#13

Weird find, if you rename a modifier to “RefObj” it makes its height half.


#14

funny!!! :thumbsup:


#15

That’s probably the (hidden) name of the “modifier” item that gets added when you reference an object - it’s half-height too.

RefObj = Reference Object?


#16

Yes it is. That’s where I got the name from.
ReferenceTarget : DerivedObject


#17

So made progress… but now stuck with getting the selected items.

LVM_GETSELECTEDCOUNT returns 1… always.


layermanagerHWND = (for o in (windows.getChildrenHWND 0) where matchpattern o[5] pattern:"Layer:*" do exit with o)[1]

layerListViewHWND = (windows.getChildrenHWND layermanagerHWND)[1][1]

LVM_GETITEMCOUNT = 0x1004
LVM_GETSELECTEDCOUNT = 0x1050
LVM_GETITEMTEXT = 0x1046
LVM_GETNEXTITEM = 0x1012
 
windows.SendMessage layerListViewHWND LVM_GETSELECTEDCOUNT 0 0

windows.SendMessage layerListViewHWND LVM_GETITEMTEXT 0 0
windows.SendMessage layerListViewHWND LVM_GETNEXTITEM 0 0
windows.SendMessage layerListViewHWND LVM_GETITEMTEXT 0 0


Found a good list of LVM items.
https://docs.omniref.com/ruby/gems/win-user32-ruby/0.1.1/symbols/WinAPISys::LVM_GETSELECTEDCOUNT#line=868


#18

try LVM_GETITEMSTATE to get item’s selection state


#19

Hmm this just crashes max…


layermanagerHWND = (for o in (windows.getChildrenHWND 0) where matchpattern o[5] pattern:"Layer:*" do exit with o)[1]

layerListViewHWND = (windows.getChildrenHWND layermanagerHWND)[1][1]

LVM_GETITEMCOUNT = 0x1004
LVM_GETSELECTEDCOUNT = 0x1050
LVM_GETITEMTEXT = 0x1046
LVM_GETNEXTITEM = 0x1012

LVM_GETITEMSTATE  = 0x1044


windows.SendMessage layerListViewHWND LVM_GETSELECTEDCOUNT 0 0
windows.SendMessage layerListViewHWND LVM_GETNEXTITEM 0 0
windows.SendMessage layerListViewHWND LVM_GETITEMSTATE 0 0



#20

I’m wondering if the ‘selected’ is for which layer is ‘ticked’ as the active layer internally. And maybe the highlighting that we refer to as selected is just a colour trick? It would explain why ‘layermanger.getselectedlayers’ doesn’t exist. Maybe I’m just crazy speaking…


#21

i was thinking similar before i made everything right :slight_smile:
it’s hard to use just windows.sendmessage function in our case because with some messages sendmessage uses some parameters (lParam in our case) as OUT parameter, which is hard to pass via mxs.

so we need c# assembly, and as usually i do it with compiled on-the-fly:

global ListViewAssembly
fn CreateListViewAssembly =
(
source  = ""
source += "using System;
"
source += "using System.Text;
"
source += "using System.Runtime.InteropServices;
"
source += "public class ListViewOps
"
source += "{
"
source += "    private const int LVM_FIRST = 0x1000;
"
source += "    private const int LVM_GETITEMCOUNT = LVM_FIRST + 4;
"
source += "    private const int LVM_GETITEMSTATE = LVM_FIRST + 44;
"
source += "    private const int LVM_GETSELECTEDCOUNT = LVM_FIRST + 50;
"
source += "    private const int LVM_GETITEM = LVM_FIRST + 75;
"
source += "    private const int LVIF_TEXT = 1;
"
source += "    private const int LVIF_STATE = 8;
"
source += "    private const int LVIS_SELECTED = 2;
"
source += "    [DllImport(\"user32.dll\")]
"
source += "    private static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam);
"
source += "    [StructLayoutAttribute(LayoutKind.Sequential)]
"
source += "    private struct LVITEM
"
source += "    {
"
source += "        public uint mask;
"
source += "        public int iItem;
"
source += "        public int iSubItem;
"
source += "        public uint state;
"
source += "        public uint stateMask;
"
source += "        public IntPtr pszText;
"
source += "        public int cchTextMax;
"
source += "        public int iImage;
"
source += "        public IntPtr lParam;
"
source += "    }
"
source += "    public int GetItemsCount(Int32 hWnd)
"
source += "    {
"
source += "        return SendMessage((IntPtr)hWnd, LVM_GETITEMCOUNT, IntPtr.Zero, IntPtr.Zero).ToInt32();
"
source += "    }
"
source += "    public int GetSelectedItemsCount(Int32 hWnd)
"
source += "    {
"
source += "        return SendMessage((IntPtr)hWnd, LVM_GETSELECTEDCOUNT, IntPtr.Zero, IntPtr.Zero).ToInt32();
"
source += "    }
"
source += "    public bool IsItemSelected(Int32 hWnd, int item)
"
source += "    {
"
source += "        return (LVIS_SELECTED & SendMessage((IntPtr)hWnd, LVM_GETITEMSTATE, new IntPtr(item), new IntPtr(LVIS_SELECTED)).ToInt32()) != 0;
"
source += "    }
"
source += "    public string GetItemText(Int32 hWnd, int item, int subItem = 0)
"
source += "    {
"
source += "        // Declare and populate the LVITEM structure
"
source += "        LVITEM lvi = new LVITEM();
"
source += "        lvi.mask = LVIF_TEXT;
"
source += "        lvi.cchTextMax = 512;
"
source += "        lvi.iItem = item;           // the zero-based index of the ListView item
"
source += "        lvi.iSubItem = subItem;     // the one-based index of the subitem, or 0 if this
"
source += "                                    // structure refers to an item rather than a subitem
"
source += "        lvi.pszText = Marshal.AllocHGlobal(512);
"
source += "        // Send the LVM_GETITEM message to fill the LVITEM structure
"
source += "        IntPtr ptrLvi = Marshal.AllocHGlobal(Marshal.SizeOf(lvi));
"
source += "        Marshal.StructureToPtr(lvi, ptrLvi, false);
"
source += "        int result = SendMessage((IntPtr)hWnd, LVM_GETITEM, IntPtr.Zero, ptrLvi).ToInt32();
"
source += "        // Extract the text of the specified item
"
source += "        string text = Marshal.PtrToStringAuto(lvi.pszText);
"
source += "        Marshal.FreeHGlobal(lvi.pszText);
"
source += "        Marshal.FreeHGlobal(ptrLvi);
"
source += "        return (result != 0) ? text : null;
"
source += "    }
"
source += "}
"

	csharpProvider = dotnetobject "Microsoft.CSharp.CSharpCodeProvider"
	compilerParams = dotnetobject "System.CodeDom.Compiler.CompilerParameters"

	compilerParams.ReferencedAssemblies.Add("System.dll");

	compilerParams.GenerateInMemory = true
	compilerResults = csharpProvider.CompileAssemblyFromSource compilerParams #(source)
	
	ListViewAssembly = compilerResults.CompiledAssembly
	ListViewAssembly.CreateInstance "ListViewOps"
)
global ListViewOps = CreateListViewAssembly()

/*
layer_lv_hwnd = -- hwnd to SysListView32 system control

ListViewOps.GetItemsCount layer_lv_hwnd
ListViewOps.GetSelectedItemsCount layer_lv_hwnd
ListViewOps.IsItemSelected layer_lv_hwnd 1
ListViewOps.GetItemText layer_lv_hwnd 1 0
*/  

i’ve tested it for ListController listview. It works as well


#22

Perfect! I started a new thread for this so it’s easier to find. I’m going to look into 3dsmax 2015+ too.


#23

Nice to see a cool spin off :slight_smile:

For the modifier stack one issue remains, if a modifier is expanded, so it’s gizmos/sub modes are visible, the count screws up since each item, including, subitems just counts from the top so there is no match between the ones selected and the modifier index.

I’m trying to get to the listbox items to get their relative x position to get a sense of hierarchy but everyhting returns either zero or false.

btw, this is a nice tool to peek/poke around the UI structure: http://www.catch22.net/software/winspy-17


#24

References will also modify the items indexes #11.

(
  	delete objects
  	obj = box()
  	for j = 1 to 5 do addmodifier obj (edit_poly name:("M-"+(j as string)))
  	select (reference (reference obj))
  	max modify mode
  )

#25

Here’s a nice snippet, after running it x contains an array with all visible items in the stack’s treeview, including gizmos, sub object modes etc, with a bool that indicates if it’s selected or not, like so:

#(item1, selected, item2, selected, … itemN, selected)

And in case anyone is wondering what this is for in the end… I’m making the ultimate trackbar key filter (freeby). It shows only the keys from selected stack items. I’ve already got one but it works for 1 modifier(+gizmos) at a time and due to limited mxs interaction with the stack panel I have to jump trough hoops to get all selected items.

If anyone knows a good way to separate the gizmos from the modifiers let me know! I’m having a hard time to get the indenting/x-position of the listbox items. The item’s rectangle doesn’t show the indenting either…


   source="
   using System;
   using System.Text;
   using System.Runtime.InteropServices;
   
   public class StackReader
   {
   	[DllImport(\"user32.dll\", CharSet = CharSet.Auto, SetLastError = false)]
   	static extern IntPtr SendMessage(int hWnd, uint Msg, IntPtr wParam, StringBuilder sb);
   
   	[DllImport(\"user32.dll\", CharSet = CharSet.Auto, SetLastError = false)]
   	static extern bool SendMessage(int hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
   
   	const int LB_GETCOUNT = 0x018B;
   	const int LB_GETTEXT = 0x0189;
   	const int LB_GETSEL = 0x0187;
   
   	public StackReader()
   	{ }
   
   	public string Read(int listBoxHwnd)
   	{
   		var cnt = (int)SendMessage(listBoxHwnd, LB_GETCOUNT, IntPtr.Zero, null);
 		if (cnt == 0) { return \" #(); \"; }
 
   		var output = \"#(\";
   		for (var i = 0; i < cnt; i++)
   		{
   			var sb = new StringBuilder(256);
   			SendMessage(listBoxHwnd, LB_GETTEXT, (IntPtr)i,  sb);
   
   			var sel = SendMessage(listBoxHwnd, LB_GETSEL, (IntPtr)i,  IntPtr.Zero);
   
   			output += \"\\\"\" + sb + \"\\\",\" + sel + \",\";
   		}
   
   		output = output.Substring(0,output.Length-1)+ \"); \";
   		return output;
   	}
   }
   "
   
   clearlistener()
   csharpProvider = dotnetobject "Microsoft.CSharp.CSharpCodeProvider"
   compilerParams = dotnetobject "System.CodeDom.Compiler.CompilerParameters"
   
   compilerParams.ReferencedAssemblies.AddRange #("System.dll")
   compilerParams.GenerateInMemory = on
   compilerResults = csharpProvider.CompileAssemblyFromSource compilerParams #(source)
    
   hwnd = (windows.getChildHWND #max "Command Panel")[1]
   list_box = for c in windows.getChildrenHWND hwnd   where  c[4] == "ListBox" do exit with c[1]
   
   StackReader=  compilerResults.CompiledAssembly.CreateInstance "StackReader"
   x = execute (StackReader.read list_box ) true;
   

#26

Here’s a ruff update of the modifier trackbar filter, now supporting multiple selected modifiers and sub objects. It will show the keys on the trackbar of the selected modifier(s) for easy manipulation.

A few things:
Selecting a modifier’s gizmo gives the same result as selecting the modifier itself, showing all keys for it and it’s other gizmo’s as well. It’s by-design now and can probably be improved but I’ve run out of spare time :slight_smile:

There is no event that I could find for when multiple items are selected on the stack, only for the first hit, so I’ve resorted to polling the stack 4 times a sec when the filter is active… it’s a light weight thing but not a pretty solution. Maybe a c# hook listening to windows messages could help here?

It’s name based, comparing the stack text to modifier names, duplicate names etc are not an issue but it feels a bit iffy.

And this is more a max thing, but when you right click on a key it will still list all the other keys on that time as well ignoring the filter…

Anyways… here’s the code, I took a snipped of DenisT’s C# from some other thread google came up with when seaching for solutions.

global mf_LB_HWND 
global mf_StackItems =#()
global mf_StackSelection=#()
global mf_tbar= maxops.trackbar;
global mf_filterIndex;
global mf_StackSelCache=#();
global mf_StackItemCount;

hwnd = (windows.getChildHWND #max "Command Panel")[1]
mf_LB_HWND = for c in windows.getChildrenHWND hwnd   where  c[4] == "ListBox" do exit with c[1]
 

--thanks DenisT for the c# part! 
global ListBoxAssembly
fn CreateListBoxAssembly forceRecompile:on =
(
	if forceRecompile or not iskindof ::ListBoxAssembly dotnetobject or (::ListBoxAssembly.GetType()).name != "Assembly" do
	(
		source = ""
		source += "using System;
"
		source += "using System.Text;
"
		source += "using System.Runtime.InteropServices;
"
		source += "public class ListBoxOps
"
		source += "{
"
		source += " [DllImport(\"user32.dll\")]
"
		source += " public static extern int SendMessage(IntPtr window, int message, int wParam, IntPtr lParam);
"
		source += " [DllImport(\"user32.dll\")]
"
		source += " public static extern int SendMessage(IntPtr hWnd, int wMsg, int wParam, StringBuilder lParam);
"
		source += " private const int LB_GETTEXT = 0x0189;
"
		source += " private const int LB_GETTEXTLEN = 0x018A;
"
		source += " public int GetTextLength(IntPtr hWnd, int item)
"
		source += " {
"
		source += " return (SendMessage(hWnd, LB_GETTEXTLEN, item, IntPtr.Zero));
"
		source += " }
"
		source += " public string GetText(IntPtr hWnd, int item)
"
		source += " {
"
		source += " int len = GetTextLength(hWnd, item);
"
		source += " StringBuilder text = new StringBuilder(len+1);
"
		source += " SendMessage(hWnd, LB_GETTEXT, item, text);
"
		source += " return text.ToString();
"
		source += " }
"
		source += "}
"

		csharpProvider = dotnetobject "Microsoft.CSharp.CSharpCodeProvider"
		compilerParams = dotnetobject "System.CodeDom.Compiler.CompilerParameters"

		compilerParams.ReferencedAssemblies.Add("System.dll");

		compilerParams.GenerateInMemory = true
		compilerResults = csharpProvider.CompileAssemblyFromSource compilerParams #(source)
		 
		ListBoxAssembly = compilerResults.CompiledAssembly
		ListBoxAssembly.CreateInstance "ListBoxOps"
	)
)

global ListBoxOps = CreateListBoxAssembly forceRecompile:on


fn testCallbackFilterFunction theAnimatable theParent theSubAnimIndex theGrandParent theNode=
(
	out=false
		
	if (superClassof theAnimatable == FloatController AND mf_StackItems.count > 0 ) then	
	(
		for sel in mf_StackSelection do
		(
			a= mf_StackItems[sel+1]
			b= theGrandParent
			deps=refs.dependents b	
			if (( a==b  or  (findItem deps a)>0) and (isvalidnode a == false) ) then  ( out=true )  
			if ( sel+1 == mf_StackItems.count AND  (theGrandParent as string == a) ) then  ( out=true )  
		)	
	)
	out
)


mf_filterIndex = mf_tbar.registerFilter testCallbackFilterFunction callbackAdditionFunction "ModFilter" 8 active:false stopTraversal:false
 

try(
	stackUpdate.Stop();
	stackUpdate.Dispose();
) catch ()
	
stackUpdate = dotNetObject "System.Windows.Forms.Timer"

fn checkStack =
(
	if ((mf_tbar.isFilterActive mf_filterIndex) AND getCommandPanelTaskMode()==#modify 	) do
	(
		mf_StackItemCount= windows.SendMessage mf_LB_HWND 0x18B 0 0
		mf_StackItems=#();
		curmod=1
		obj=selection[1]
		
		if (obj!=undefined) do
		(
			for i=1 to mf_StackItemCount-1 do
			(
				item = (ListBoxOps.GetText (dotnetobject "System.IntPtr" mf_LB_HWND) (i-1))
				ind=curmod+1
				if (ind> obj.modifiers.count) do ( ind=obj.modifiers.count)
				if (obj.modifiers[ind].name == item AND i!=1 ) do (	  curmod += 1	)   		
				append mf_StackItems (obj.modifiers[curmod])	 	 
			)
		)

		append mf_StackItems (ListBoxOps.GetText (dotnetobject "System.IntPtr" mf_LB_HWND) (mf_StackItemCount-1))
			
		mf_StackSelection = for i=0 to (mf_StackItems.count-1) where (windows.SendMessage  mf_LB_HWND  0x187 i 0) == 1 collect (i)

		if ((mf_StackSelCache as string) != (mf_StackSelection as string)) then
		(
			mf_StackSelCache=deepcopy mf_StackSelection
			if (mf_tbar.isFilterActive mf_filterIndex) do ( mf_tbar.setFilterActive mf_filterIndex true);
		)
	)
	 
)

dotnet.addEventHandler stackUpdate "tick" checkStack
stackUpdate.interval = 250
stackUpdate.start()