Mouse button hold timer


#1

What would be the cleanest way to detect when the (left) mousebutton has been continuously pressed for x milliseconds without movement?

like a “pressed-and-held-in-place” event…


#2

within a rollout/form you have control over or just anywhere in the 3dsmax UI?


#3

Directly in the viewport.

It’s for my ProSelect script (see http://www.scriptspot.com/3ds-max/scripts/proselect) Its now triggers on a selection change, which doesn’t work in a shaded view when the front most object is already selected. Since clicking that doesn’t trigger this event cause it’s already selected.

So I want to invoke the script with this.


#4

I’ve research a little bit and I think you could do this by using Windows Hooks to monitor mouse messages (which I guess would give you access to the mousedown and mouseup event, thus disabling the need for the timer) But I never did nothing with windows hooks and if I could help you it would probably need a separate .dll as I dont know how to dynamically compile stuff in max :smiley:

But one thing for sure, I bet Denis could help out :smiley:

Apart from that, using only maxscript doesnt seem possible because you can only track mouse click…

Here are some links:
http://msdn.microsoft.com/en-us/library/ms632589(v=VS.85).aspx
http://msdn.microsoft.com/en-us/library/ms645601(v=vs.85).aspx


#5

the ultimate solution is to use the low-level mouse hook (google for WH_MOUSE_LL).
but much easier (i used it for many years) is timer + virtual keys… something like that:


global MouseOps
fn CreateMouseOps forceRecompile:on =
(
	if forceRecompile or not iskindof MouseOps dotnetobject or (MouseOps.GetType()).name != "Assembly" do
	(

source = ""
source += "using System;
"
source += "using System.Runtime.InteropServices;
"
source += "public class VMouse
"
source += "{
"
source += "	private const int VK_LBUTTON = 0x01;
"
source += "	private const int VK_RBUTTON = 0x02;
"
source += "	private const int VK_MBUTTON = 0x04;
"
source += "	private const int KEY_PRESSED = 0x80;
"
source += "	[DllImport(\"user32.dll\", CharSet = CharSet.Auto, ExactSpelling = true)]
"
source += "	public static extern short GetKeyState(int virtualKeyCode);
"
source += "	public int IsPressed(int key) { return (GetKeyState(key) & KEY_PRESSED); }
"
source += "	public int MouseButtons()
"
source += "	{
"
source += "		int buttons = 0;
"
source += "		buttons += IsPressed(VK_LBUTTON) >> 5;
"
source += "		buttons += IsPressed(VK_MBUTTON) >> 6;
"
source += "		buttons += IsPressed(VK_RBUTTON) >> 7;
"
source += "		return buttons;
"
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)
		
		MouseOps = compilerResults.CompiledAssembly
		MouseOps.CreateInstance "VMouse"
	)
)
global VMouse = CreateMouseOps()
global getMouseButtons = VMouse.MouseButtons

/***********************************************/
 try(mouse_timer.Dispose()) catch()

mouse_timer = dotnetobject "Timer"
mouse_timer.interval = 100
fn mouseTimerTick s e =
(
--	format "tick... buttons: %
" (getMouseButtons())
	if VMouse.MouseButtons() == 5 do 
	(
		mouse_timer.Stop()
		format "stop...
"
	)
)
dotnet.addEventHandler mouse_timer "Tick" mouseTimerTick
mouse_timer.Start()

usually interval ~100ms is enough. The function doesn’t take time or memory.


#6

if you are going with a timer, why not just access mouse.buttonStates on the timer event?


#7

Lol… see? Denis you the man :stuck_out_tongue:

But, does that catch the click or it catches while the button is pressed? One other thing, I thought that when using something like windows hooks you didnt need a timer to listen to the messages. … After turning on my brain a little, this isn’t the window hooks example… can you enlighten us on how to do it? :smiley:


#8

mouse.buttonStates works in max viewport only. it doesn’t work over any dialog, pop-up menu, etc.


#9

Yeah, I think in the case of the code Denis posted, mousetrack() would have the same effect, that’s why I talked about the WM_MOUSE or like Denis said, WH_MOUSE_LL which contains this:

wParam [in]
Type: WPARAM

The identifier of the mouse message. This parameter can be one of the following messages: WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_MOUSEHWHEEL, WM_RBUTTONDOWN, or WM_RBUTTONUP.

Thus enabling the user to track the mousedown, then the mouseup.


#10

Ahh great input, thx guys! “Works in max viewport only”… that is actually the only place it has to work :slight_smile:

I’m going to try and see what works best, I’ll be back with the results!


#11

using timer and vkey you can check the state of the vkey.

using mouse hook you don’t need timer. you can hook any window mouse event and call any function from mousehook procedure. I don’t like call max functions from c#, so I use events. My MouseHook class has three events: MouseMove, MouseDown, and MouseUp. It’s enough for all my cases.
sample? … hmm… i will think about possibility to show the sample.


#12

it’s hard to stay in viewport all the time … you can start some operation in viewport, drag cursor, and finish the operation out of viewport or over a dialog.


#13

following Denis’s advice, this a very quick and dirty implementation of low level mousehooks, most of which I stole off an MSDN blog, and added event delegates, etc.

(
	str="using System;
"
	str+="using System.Collections.Generic;
"
	str+="using System.Text;
"
	str+="using System.Runtime.InteropServices;
"
	str+="using System.Windows.Forms;
"
	str+="using System.Diagnostics;
"
	str+="using System.Drawing;
"

	str+="class InterceptMouse
"
	str+="{
"
	str+="    private static IntPtr _hookID = IntPtr.Zero;
"

	str+="public delegate void MouseDownHandler(object Sender, MouseEventArgs e);
"
	str+="public event MouseDownHandler MouseDown;
"
	str+="public delegate void MouseUpHandler(object Sender, MouseEventArgs e);
"
	str+="public event MouseUpHandler MouseUp;
"
	str+="public delegate void MouseMoveHandler(object Sender, MouseEventArgs e);
"
	str+="public event MouseMoveHandler MouseMove;
"
		
	str+="public InterceptMouse()
"
	str+="{
"
	str+="    _hookID = SetHook(HookCallback);
"
	str+="}
"

	str+="public void Release()
"
	str+="{
"
	str+="	UnhookWindowsHookEx(_hookID);
"
	str+="}
"
	
	str+="private static IntPtr SetHook(LowLevelMouseProc proc)
"
	str+="{
"
	str+="    using (Process curProcess = Process.GetCurrentProcess())
"
	str+="    using (ProcessModule curModule = curProcess.MainModule)
"
	str+="    {
"
	str+="        return SetWindowsHookEx(WH_MOUSE_LL, proc,
"
	str+="            GetModuleHandle(curModule.ModuleName), 0);
"
	str+="    }
"
	str+="}
"

	str+="private delegate IntPtr LowLevelMouseProc(int nCode, IntPtr wParam, IntPtr lParam);
"

	str+="private IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
"
	str+="{
"
	str+="   if (nCode >= 0)
"
	str+="	{
"
	str+="		MSLLHOOKSTRUCT hookStruct = (MSLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, typeof(MSLLHOOKSTRUCT));
"
	str+="		if (MouseMessages.WM_LBUTTONDOWN == (MouseMessages)wParam)
"
	str+="		{
"
	str+="			MouseEventArgs e = new MouseEventArgs(MouseButtons.Left,1,hookStruct.pt.x,hookStruct.pt.y,0);
"
	str+="         	MouseDown(this, e);
"
	str+="		}
"
	str+="		else if (MouseMessages.WM_LBUTTONUP == (MouseMessages)wParam)
"
	str+="		{			
"
	str+="			MouseEventArgs e = new MouseEventArgs(MouseButtons.Left,1,hookStruct.pt.x,hookStruct.pt.y,0);
"
	str+="			MouseUp(this, e);
"
	str+="		}
"
	str+="		else if (MouseMessages.WM_MOUSEMOVE == (MouseMessages)wParam)
"
	str+="		{
"
	str+="				MouseEventArgs e = new MouseEventArgs(MouseButtons.None,0,hookStruct.pt.x,hookStruct.pt.y,0);
"
	str+="           	MouseMove(this, e);
"
	str+="		}
"
	str+="			else if (MouseMessages.WM_RBUTTONDOWN == (MouseMessages)wParam)
"
	str+="{
"
	str+="MouseEventArgs e = new MouseEventArgs(MouseButtons.Right,1,hookStruct.pt.x,hookStruct.pt.y,0);
"
	str+="            	MouseDown(this, e);
"
	str+="			}
"
	str+="			else if (MouseMessages.WM_RBUTTONUP == (MouseMessages)wParam)
"
	str+="			{			
"
	str+="				MouseEventArgs e = new MouseEventArgs(MouseButtons.Right,1,hookStruct.pt.x,hookStruct.pt.y,0);
"
	str+="				MouseUp(this, e);
"
	str+="			}
"
	str+="		}
"
	str+="       return CallNextHookEx(_hookID, nCode, wParam, lParam);
"
	str+="   }
"

	str+="   private const int WH_MOUSE_LL = 14;
"

	str+="   private enum MouseMessages
"
	str+="  {
"
	str+="      WM_LBUTTONDOWN = 0x0201,
"
	str+="       WM_LBUTTONUP = 0x0202,
"
	str+="      WM_MOUSEMOVE = 0x0200,
"
	str+="      WM_MOUSEWHEEL = 0x020A,
"
	str+="      WM_RBUTTONDOWN = 0x0204,
"
	str+="       WM_RBUTTONUP = 0x0205
"
	str+="   }
"

	str+="    [StructLayout(LayoutKind.Sequential)]
"
	str+="   private struct POINT
"
	str+="   {
"
	str+="       public int x;
"
	str+="        public int y;
"
	str+="    }
"
	str+="   [StructLayout(LayoutKind.Sequential)]
"
	str+="    private struct MSLLHOOKSTRUCT
"
	str+="   {
"
	str+="      public POINT pt;
"
	str+="      public uint mouseData;
"
	str+="     public uint flags;
"
	str+="     public uint time;
"
	str+="     public IntPtr dwExtraInfo;
"
	str+="   }
"

	str+="    [DllImport(\"user32.dll\", CharSet = CharSet.Auto, SetLastError = true)]
"
	str+="    private static extern IntPtr SetWindowsHookEx(int idHook,
"
	str+="       LowLevelMouseProc lpfn, IntPtr hMod, uint dwThreadId);
"

	str+="   [DllImport(\"user32.dll\", CharSet = CharSet.Auto, SetLastError = true)]
"
	str+="   [return: MarshalAs(UnmanagedType.Bool)]
"
	str+="   private static extern bool UnhookWindowsHookEx(IntPtr hhk);
"

	str+="   [DllImport(\"user32.dll\", CharSet = CharSet.Auto, SetLastError = true)]
"
	str+="   private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode,
"
	str+="       IntPtr wParam, IntPtr lParam);
"

	str+="   [DllImport(\"kernel32.dll\", CharSet = CharSet.Auto, SetLastError = true)]
"
	str+="  private static extern IntPtr GetModuleHandle(string lpModuleName);
"
		
	str+="}
"


	global mouseHook
	struct mouseHookStr
	(
		mouseOps,
		
		fn CreateWinAssembly =
		(
			local csharpProvider = dotnetobject "Microsoft.CSharp.CSharpCodeProvider"
			local compilerParams = dotnetobject "System.CodeDom.Compiler.CompilerParameters"
				
			compilerParams.ReferencedAssemblies.addRange #("System.dll","System.Windows.Forms.dll","System.Drawing.dll")
			compilerParams.GenerateInMemory = on
			local compilerResults = csharpProvider.CompileAssemblyFromSource compilerParams #(str)
			
			for er =0 to compilerResults.errors.count-1 do print (compilerResults.errors.item[er].tostring())
			mouseOps = compilerResults.CompiledAssembly.CreateInstance "InterceptMouse"
		),
		
		fn mouseDown s e = print "mouse down",
		fn mouseUp s e = print "mouse up",
		fn mouseMove s e = print "mouse move",

		fn initStruct =
		(
			CreateWinAssembly()
			dotNet.addEventHandler mouseOps "MouseDown" mouseDown
			dotNet.addEventHandler mouseOps "MouseUp" mouseUp
			dotNet.addEventHandler mouseOps "MouseMove" mouseMove
		),
		
		_init = initStruct()	
	)
)
mouseHook = mouseHookStr()

-- call mousehook.mouseops.release() to stop it

#14

Pure awesomeness :smiley:


#15

everything looks right. I would add an option to fire events when mouse in max window only.


#16

I also haven’t implemented mousewheel events. Anyone, feel free to pick it up.


#17

next is to realize CBTHook and forget the DialogMonitorOPS…


#18

And after that rewrite 3dsmax :slight_smile:


#19

Nice catch! :slight_smile:


#20

Nice, it even works outside max’s window :slight_smile: Sometimes it stops working but the idea is great!

I’m suddenly flooded in work so somewhere next week I’ll hope to find the time to play around a bit more…