MAXScript: How to create layered .NET window in Windows Forms


#21

Since the UI Thread might be locked while you initialize your script, you’ll probably need to handle either the animation or the script initialization in a different Thread or update the images inside your initialization procedure.


#22

Yes, I understand that I need to use another thread to process the animation, but I don’t know how to do this.
If it’s not difficult for you, can you show an example of splitting threads to process two scenarios at the same time?

I would also like to load image frames not into a screenshot (without use the “CopyFromScreen” method of the “graphics” element), and into the “PictureBox” or “Label” element for example. Is it possible to implement it in your way?

Thank you!


#23
try(wp.close()) catch()

wp = dotnetobject "System.Windows.Window"
wp.WindowStartupLocation = wp.WindowStartupLocation.Manual

wp.Title = "AminWin"
wp.ShowInTaskbar = off

wp.WindowStyle = wp.WindowStyle.None
wp.AllowsTransparency = on

theGif = @"C:\temp\bell.png"

image = dotnetobject "System.Windows.Media.Imaging.BitmapImage" (dotnetobject "System.Uri" theGif)

frames = 44
w = image.PixelWidth / frames
h = image.PixelHeight
index = 0

rect32 = dotnetobject "System.Windows.Int32Rect" (w*index) 0 w h
cropped = dotnetobject "System.Windows.Media.Imaging.CroppedBitmap" image rect32

wp.width = cropped.PixelWidth 
wp.height = image.PixelHeight

brush = dotnetobject "System.Windows.Media.ImageBrush" 
brush.ImageSource = cropped

wp.Background = brush

wih = dotnetobject "System.Windows.Interop.WindowInteropHelper" wp
wih.owner = dotnetobject "IntPtr" (windows.getmaxhwnd())

wp.WindowStartupLocation = wp.WindowStartupLocation.CenterOwner
	
wp.Show()


	
	
try	(dispatcherTimer.IsEnabled = off) catch()
	
dispatcherTimer = dotnetobject "System.Windows.Threading.DispatcherTimer"
dispatcherTimer.tag = wp
wp.tag = 0	
dispatcherTimer.Interval = (dotnetclass "System.TimeSpan").FromMilliseconds 60

fn playBitmap s a = 
(
	failed = false
	try
	(
		_image = s.tag.Background.ImageSource
		_brush = s.tag.Background
		_source = _image.Source
		
		width = _image.PixelWidth
		height = _image.PixelHeight
		frames = _source.PixelWidth / width
		index = s.tag.tag
		
		rect = dotnetobject "System.Windows.Int32Rect" (width * index) 0 width height	
		_brush.ImageSource = dotnetobject "System.Windows.Media.Imaging.CroppedBitmap" _source rect

		s.tag.tag = mod (index + 1) frames
	)
	catch
	(
		format "Something wrong!\n"
		failed = true
	)

	if failed or keyboard.escpressed do
	(
		s.IsEnabled = off
		s.tag.Close()
	)
)

dotnet.addEventHandler dispatcherTimer "Tick" playBitmap
dispatcherTimer.Start()		
--dispatcherTimer.Stop()

Use BELL.PNG from above but change the path (theGif = @“C:\temp\bell.png”) to yours


#24

you cannot use a different than main thread with MXS.


#25

here is my best… :upside_down_face:

try(wp.close()) catch()

wp = dotnetobject "System.Windows.Window"
wp.WindowStartupLocation = wp.WindowStartupLocation.Manual

wp.Title = "AminWin"
	
wp.WindowStyle = wp.WindowStyle.None
wp.AllowsTransparency = on

anim_file = @"C:\Temp\GIFs\ANIM\spongebob_02.004.png"
image = dotnetobject "System.Windows.Media.Imaging.BitmapImage" (dotnetobject "System.Uri" anim_file)
rect32 = dotnetobject "System.Windows.Int32Rect" 200 200 128 128
image = dotnetobject "System.Windows.Media.Imaging.CroppedBitmap" image rect32

wp.width = image.PixelWidth 
wp.height = image.PixelHeight

brush = dotnetobject "System.Windows.Media.ImageBrush" 
brush.ImageSource = image

wp.Background = brush

NameScope = dotnetclass "System.Windows.NameScope"
name_scope = dotnetobject NameScope
NameScope.SetNameScope wp name_scope

wp.RegisterName "spongebob" brush

duration = dotnetobject "System.Windows.Duration" ((dotnetclass "System.TimeSpan").FromMilliseconds 650)
double_animation = dotnetobject "System.Windows.Media.Animation.DoubleAnimation" 2 -1 duration

double_animation.AutoReverse = true
double_animation.RepeatBehavior = double_animation.RepeatBehavior.Forever

Storyboard = dotnetclass "System.Windows.Media.Animation.Storyboard"
story_board = dotnetobject Storyboard
story_board.Children.Add double_animation
Storyboard.SetTargetName double_animation "spongebob"
property_path = dotnetobject "System.Windows.PropertyPath" (dotnetclass "System.Windows.Media.ImageBrush").OpacityProperty
Storyboard.SetTargetProperty double_animation property_path

wih = dotnetobject "System.Windows.Interop.WindowInteropHelper" wp
wih.owner = dotnetobject "IntPtr" (windows.getmaxhwnd())

wp.WindowStartupLocation = wp.WindowStartupLocation.CenterOwner
wp.Topmost = true
	
wp.Show()
	
story_board.Begin wp	


(use this guy and set the right path)

then go for yourself … I’m sure you can animate Crop values as well


#26

the multi animation:

try(wp.close()) catch()
wp = 
(
	wp = dotnetobject "System.Windows.Window"
	wp.WindowStartupLocation = wp.WindowStartupLocation.Manual

	wp.Title = "AminWin"
		
	wp.WindowStyle = wp.WindowStyle.None
	wp.AllowsTransparency = on

	anim_file = @"C:\Temp\GIFs\ANIM\spongebob_02.004.png"
	image = dotnetobject "System.Windows.Media.Imaging.BitmapImage" (dotnetobject "System.Uri" anim_file)
	rect32 = dotnetobject "System.Windows.Int32Rect" 200 200 128 128
	image = dotnetobject "System.Windows.Media.Imaging.CroppedBitmap" image rect32

	wp.width = image.PixelWidth 
	wp.height = image.PixelHeight

	brush = dotnetobject "System.Windows.Media.ImageBrush" 
	brush.ImageSource = image

	wp.Background = brush

	NameScope = dotnetclass "System.Windows.NameScope"
	name_scope = dotnetobject NameScope
	NameScope.SetNameScope wp name_scope

	wp.RegisterName "spongebob_opacity" brush
	duration = dotnetobject "System.Windows.Duration" ((dotnetclass "System.TimeSpan").FromMilliseconds 300)
	opacity_animation = dotnetobject "System.Windows.Media.Animation.DoubleAnimation" 0.2 0.9 duration
	opacity_animation.AutoReverse = true
	opacity_animation.RepeatBehavior = opacity_animation.RepeatBehavior.Forever

	tm = dotnetobject "System.Windows.Media.RotateTransform"
	tm.CenterX = image.width/3
	tm.CenterY = image.height/3
	tm.Angle = 0
	
	brush.Transform = tm		

	wp.RegisterName "spongebob_angle" tm
	duration = dotnetobject "System.Windows.Duration" ((dotnetclass "System.TimeSpan").FromMilliseconds 800)
	angle_animation = dotnetobject "System.Windows.Media.Animation.DoubleAnimation" 0 360 duration
	angle_animation.AutoReverse = false
	angle_animation.RepeatBehavior = angle_animation.RepeatBehavior.Forever

	Storyboard = dotnetclass "System.Windows.Media.Animation.Storyboard"
	story_board = dotnetobject Storyboard

	story_board.Children.Add opacity_animation
	Storyboard.SetTargetName opacity_animation "spongebob_opacity"

	property_path = dotnetobject "System.Windows.PropertyPath" (dotnetclass "System.Windows.Media.ImageBrush").OpacityProperty
	Storyboard.SetTargetProperty opacity_animation property_path

	story_board.Children.Add angle_animation
	Storyboard.SetTargetName angle_animation "spongebob_angle"

	property_path = dotnetobject "System.Windows.PropertyPath" (dotnetclass "System.Windows.Media.RotateTransform").AngleProperty
	Storyboard.SetTargetProperty angle_animation property_path

	wih = dotnetobject "System.Windows.Interop.WindowInteropHelper" wp
	wih.owner = dotnetobject "IntPtr" (getmaxhwnd())

	wp.WindowStartupLocation = wp.WindowStartupLocation.CenterOwner
	wp.Topmost = true
		
	wp.Show()
		
	story_board.Begin wp
	
	wp
)

#27

AND FINALLY! I found how to animate a png sequence …

try(wp.close()) catch()
wp = 
(
	wp = dotnetobject "System.Windows.Window"
	wp.WindowStartupLocation = wp.WindowStartupLocation.Manual

	wp.Title = "AminWin"
		
	wp.WindowStyle = wp.WindowStyle.None
	wp.AllowsTransparency = on

	anim_file = @"C:\Temp\GIFs\ANIM\spongebob_02.000.png"
	image = dotnetobject "System.Windows.Media.Imaging.BitmapImage" (dotnetobject "System.Uri" anim_file)

	wp.width = image.PixelWidth 
	wp.height = image.PixelHeight

	brush = dotnetobject "System.Windows.Media.ImageBrush" 

	num = 12
	step = 30
	
	AnimationUsingKeyFrames = dotnetclass "System.Windows.Media.Animation.ObjectAnimationUsingKeyFrames"
	play_animation = dotnetobject AnimationUsingKeyFrames

	img_key = dotnetclass "System.Windows.Media.Animation.DiscreteObjectKeyFrame"
	img_cls = dotnetclass "System.Windows.Media.Imaging.BitmapImage"

	KeyTime = dotnetclass "System.Windows.Media.Animation.KeyTime"
	TimeSpan = dotnetclass "System.TimeSpan"

	for i=0 to num-1 do
	(
		ss = @"C:\Temp\GIFs\ANIM\spongebob_02."
		ss += formattedprint i format:"03d"
		ss += ".png"

		img = dotnetobject img_cls (dotnetobject "System.Uri" ss)
		time = KeyTime.FromTimeSpan (TimeSpan.FromMilliseconds (i * step))
		key = dotnetobject img_key img time

		play_animation.KeyFrames.Add key
	)

	wp.Background = brush
	
	NameScope = dotnetclass "System.Windows.NameScope"
	name_scope = dotnetobject NameScope
	NameScope.SetNameScope wp name_scope

	Storyboard = dotnetclass "System.Windows.Media.Animation.Storyboard"
	story_board = dotnetobject Storyboard
	
	wp.RegisterName "spongebob_play" brush
	play_animation.AutoReverse = true
	play_animation.RepeatBehavior = opacity_animation.RepeatBehavior.Forever

	wp.RegisterName "spongebob_opacity" brush
	duration = dotnetobject "System.Windows.Duration" (TimeSpan.FromMilliseconds ((num - 1) * step))
	opacity_animation = dotnetobject "System.Windows.Media.Animation.DoubleAnimation" 0.1 0.9 duration
	opacity_animation.AutoReverse = true
	opacity_animation.RepeatBehavior = opacity_animation.RepeatBehavior.Forever

	story_board.Children.Add opacity_animation
	Storyboard.SetTargetName opacity_animation "spongebob_opacity"
	property_path = dotnetobject "System.Windows.PropertyPath" (dotnetclass "System.Windows.Media.ImageBrush").OpacityProperty
	Storyboard.SetTargetProperty opacity_animation property_path
	
	story_board.Children.Add play_animation
	Storyboard.SetTargetName play_animation "spongebob_play"
	property_path = dotnetobject "System.Windows.PropertyPath" (dotnetclass "System.Windows.Media.ImageBrush").ImageSourceProperty
	Storyboard.SetTargetProperty play_animation property_path

	wih = dotnetobject "System.Windows.Interop.WindowInteropHelper" wp
	wih.owner = dotnetobject "IntPtr" (windows.getmaxhwnd())

	wp.WindowStartupLocation = wp.WindowStartupLocation.CenterOwner
	wp.Topmost = true
		
	wp.Show()
		
	story_board.Begin wp
	
	wp
)		

spongebob.zip (560.0 KB)


#28

Hello, denisT!

Thank you for these few ways!
I tested them and settled on the “the multi animation:” method for now, as it uses a single image file and is most suitable for my purposes. I just removed the opacity animation and tweaked the code a bit so that the image appears in the center of the screen and etc.
But this method also works if running only this script separately, and stops working if the main script is loaded into memory in the background. In this case, the splash screen displays only a static image without animation. Unfortunately, I never found a solution to this problem.


I think that at the moment only one solution is possible, which does not require dividing processes into separate threads (without rendering frame-by-frame animation) - this is using one animated file as a splash screen, which will be loaded at the beginning, and then simply displayed on the splash screen until the main process loads the main script into memory.
This works if an animated GIF is used as an image, but since this format is not suitable due to limitations in display quality, it would be nice if there was still a way to use animated PNG (APNG) for this purpose. I’ve found information on how to do this in C#, but haven’t yet found a way to do it with .NET in MaxScript: https://stackoverflow.com/questions/6216094/implementing-the-apng-render-function


#29

Just curious how long it takes for your script to start?


#30

It depends on the power of the computer, but on average 3-5 seconds.


#31

And what’s the problem? You can wait 5 seconds without additional entertainment :grimacing:

Just set the Wait Cursor and forget about everything else.


#32

Of course, I can wait.:smile: But other users, who do not know that the script may load for some time, depending on the power of their computer, get nervous when they do not see the immediate result after starting the script :slight_smile: I have already received their displeasure about this.:disappointed:
Therefore, at the time of launching the main script, I launch a regular window with something like this “My script is running. Please wait …”. This is enough for the moment, but I just wanted to somehow diversify the splash screen and make it not textual, but graphical, more pleasing to the eye while waiting for the script to run.
Of course, this is not critical and there is no hard need for this. I just became interested in how this problem can be solved, since it can be useful in some other cases.


#33

Here is a MXS Rollout running in a separated thread.

You can do the same in .Net with a Form or a Window and use any technique you want to animate the images. This is just a proof of concept, no fully debugged, not fully tested, and so not fully trusted.

Make sure to set the correct path for “C:\bell.png” and watch out the Listener.

Enjoy! :slight_smile:

EDIT 2: Fixed rollout not being closed properly.

(
	-- ############################################################################################
	-- COMPILE A LITTLE C# HELPER
	-- ############################################################################################
	
	fn Compile =
	(
		src  = "using System.Threading;\n"
		src += "using System.Threading.Tasks;\n"
		src += "using ManagedServices;\n"
		src += "public static class ThreadRollout\n"
		src += "{\n"
		src += "	public static Thread ShowRollout (string rollout)\n"
		src += "	{\n"
		src += "		Thread thread = new Thread(() => ManagedServices.MaxscriptSDK.ExecuteMaxscriptCommand(\"createdialog ::\" + rollout +\" modal:true style:#()\"));\n"
		src += "		thread.SetApartmentState(ApartmentState.STA);\n"
		src += "		thread.Start();\n"
		src += "		Task.Delay(100).Wait();\n"
		src += "		return (thread);\n"
		src += "	}\n"
		src += "}"
		
		params = dotnetobject "System.CodeDom.Compiler.CompilerParameters"
		params.ReferencedAssemblies.Add ((getdir #maxroot) + "ManagedServices.dll")
		
		result = (dotnetobject "Microsoft.CSharp.CSharpCodeProvider").CompileAssemblyFromSource params #(src)
		result.CompiledAssembly
	)
	
	Compile()
	
	-- ############################################################################################
	-- SETUP THE ROLLOUT
	-- ############################################################################################
	
	freescenebitmaps()
	gc()
	
	local strip = openbitmap @"C:\bell.png"
	
	try (destroyDialog ::RO_SPLASHSCREEN) catch()
	
	rollout RO_SPLASHSCREEN "" width:109 height:75
	(
		timer clock "" interval:30
		
		local current = 1
		local images  = #()
		
		fn Destroy =
		(
			free images
			setdialogpos RO_SPLASHSCREEN [-10000, -10000]
			destroyDialog RO_SPLASHSCREEN
		)
		
		on clock tick do
		(
			setdialogbitmap RO_SPLASHSCREEN images[current]
			if (current += 1) > images.count do current = 1
		)
		
		on RO_SPLASHSCREEN open do
		(
			dialogPos = getdialogpos RO_SPLASHSCREEN
			clienPos  = windows.clienttoscreen RO_SPLASHSCREEN.hwnd 0 0
			setdialogpos RO_SPLASHSCREEN [-10000, -10000]
			
			frames = 44
			height = strip.height
			width  = strip.width / frames
			
			bmp = dotnetobject "system.drawing.bitmap" width height
			grp = (dotnetclass "system.drawing.graphics").FromImage bmp
			
			size = dotnetobject "system.drawing.size" width height
			grp.CopyFromScreen clienPos[1] clienPos[2] 0 0 size
			
			(dotnetclass "System.windows.forms.clipboard").setImage bmp
			
			bmp = getclipboardbitmap()
			
			images = for j = 1 to frames collect
			(
				back = copy bmp
				x = (j-1)*width
				pastebitmap strip back (box2 x 0 width height) [0,0] type:#blend
				back
			)
			
			setdialogpos RO_SPLASHSCREEN dialogPos
		)
	)
	
	-- ############################################################################################
	-- RUN THE MXS ROLLOUT IN A SEPARATED THREAD
	-- ############################################################################################
	
	thread = (dotnetclass "ThreadRollout").ShowRollout "RO_SPLASHSCREEN"
	
	clearlistener()
	format "MXS IS SLEEPING FOR 5 SECONDS BEFORE INTENSIVE TASK\n\n"
	
	sleep 5
	
	for j = 1 to 100000 do
	(
		if mod j 10 == 0 do
		(
			format "MXS is Performing an Intensive Task >> %\%\n" (j*100/100000)
			windows.processPostedMessages()
			setwaitcursor()
		)
	)
	
	format "\nDONE! NOW CLOSING ROLLOUT\n"
	
	RO_SPLASHSCREEN.Destroy()
	thread.Abort()
	
	setarrowcursor()
	
	gc()
	(dotnetclass "system.gc").collect()
	ok
)

#34

:roll_eyes::point_up:


#35

theoretically this cannot work. But … it worked once. After that I tried it several times and max freezes.


#36

It runs well on my end, but yes, I don’t think it is very safe to do that.

“colorman.repaintUI #repaintAll” seems to crash Max in some versions.

But you can run a Form or Window the same way instead of a Rollout. I think that would be safe, as long as there is no interaction between .Net and MXS other than the creation of it.

Perhaps it would be good to create a “SplashScreen” control derived from Form or Window and do all the work .Net side.


#37

You cannot do any Form or Window in another thread. You can… but you must make it all in this thread. You can’t create a form or window in MAX main thread and just show the form in another. Also the “SpashScreen” in case of another thread can’t be a child of MAX window and MAX process.


#38

Yes, you can do both things:

(
	fn Compile =
	(
		src  = "using System;\n"
		src += "using System.Threading;\n"
		src += "using System.Threading.Tasks;\n"
		src += "using System.Windows.Forms;\n"
		src += "public static class ThreadForm\n"
		src += "{\n"
		src += "	public static Thread Show (Form form)\n"
		src += "	{\n"
		src += "		Thread thread = new Thread(() => form.ShowDialog());\n"
		src += "		thread.SetApartmentState(ApartmentState.STA);\n"
		src += "		thread.Start();\n"
		src += "		Task.Delay(100).Wait();\n"
		src += "		return (thread);\n"
		src += "	}\n"
		src += "	public static Thread ShowAsChild (Form form, IntPtr hwnd)\n"
		src += "	{\n"
		src += "		NativeWindow nw = new NativeWindow();\n"
		src += "		nw.AssignHandle (hwnd);\n"
		src += "		Thread thread = new Thread(() => form.ShowDialog(nw));\n"
		src += "		thread.Start();\n"
		src += "		Task.Delay(100).Wait();\n"
		src += "		return (thread);\n"
		src += "	}\n"
		src += "}"
		
		params = dotnetobject "System.CodeDom.Compiler.CompilerParameters"
		params.ReferencedAssemblies.Add "System.dll"
		params.ReferencedAssemblies.Add "System.Windows.Forms.dll"
		
		result = (dotnetobject "Microsoft.CSharp.CSharpCodeProvider").CompileAssemblyFromSource params #(src)
		result.CompiledAssembly
	)
	
	Compile()
	
	form = dotnetobject "System.Windows.Forms.Form"
	form.ShowInTaskbar = false
	
	-- NOT AS MAX CHILD
	thread = (dotnetclass "ThreadForm").Show form
	
	-- AS MAX CHILD
-- 	hwnd = dotnetobject "system.intptr" (windows.getMAXHWND())
-- 	thread = (dotnetclass "ThreadForm").ShowAsChild form hwnd
)

Run one version at a time and close Max (shutdown the process).
You’ll see that one Form is child and the other one is not.


#39

what you show does not contradict anything … your forms do not interact with the process … no data or objects are exchanged. I don’t even know what to say … it’s just that MAX UI is made in one thread and doesn’t allow otherwise. If suddenly something unexpectedly works, then it’s just a coincidence


#40

For the purpose of this discussion, the Form or Window, do not need to interact at all with Max. It could even be an external .EXE.

This discussion is not about “how to interact with Max”.

If it is unexpected or not I can’t tell, all I can say is that you can create a form in Max and run it in a separated Thread with a fluid animated image having alpha channel.

This would serve at least to be used as a Splash Screen, what is the topic of this thread.

I am just showing what I found to be possible, even is unexpected.

PD: Life is full of coincidences :wink:

(
	
	fn Compile =
	(
		src  = "using System;\n"
		src += "using System.Threading;\n"
		src += "using System.Threading.Tasks;\n"
		src += "using System.Windows.Forms;\n"
		src += "public static class ThreadForm\n"
		src += "{\n"
		src += "	public static Thread Show (Form form)\n"
		src += "	{\n"
		src += "		Thread thread = new Thread(() => form.ShowDialog());\n"
		src += "		thread.SetApartmentState(ApartmentState.STA);\n"
		src += "		thread.Start();\n"
		src += "		Task.Delay(100).Wait();\n"
		src += "		return (thread);\n"
		src += "	}\n"
		src += "	public static Thread ShowAsChild (Form form, IntPtr hwnd)\n"
		src += "	{\n"
		src += "		NativeWindow nw = new NativeWindow();\n"
		src += "		nw.AssignHandle (hwnd);\n"
		src += "		Thread thread = new Thread(() => form.ShowDialog(nw));\n"
		src += "		thread.Start();\n"
		src += "		Task.Delay(100).Wait();\n"
		src += "		return (thread);\n"
		src += "	}\n"
		src += "}"
		
		params = dotnetobject "System.CodeDom.Compiler.CompilerParameters"
		params.ReferencedAssemblies.Add "System.dll"
		params.ReferencedAssemblies.Add "System.Windows.Forms.dll"
		
		result = (dotnetobject "Microsoft.CSharp.CSharpCodeProvider").CompileAssemblyFromSource params #(src)
		result.CompiledAssembly
	)
	
	Compile()

	form = dotnetobject "System.Windows.Forms.Form"
	form.size = dotnetobject "System.Drawing.Size" 320 175
	form.StartPosition = form.StartPosition.CenterScreen
	form.ShowInTaskbar = false
	
	pbx          = dotnetobject "PictureBox" 
	pbx.Dock     = pbx.Dock.Fill
	pbx.SizeMode = pbx.SizeMode.Zoom
	pbx.Image    = dotnetobject "System.Drawing.Bitmap" @"C:\minions.gif"
	
	form.controls.add pbx
	
	-- NOT MAX CHILD
	thread = (dotnetclass "ThreadForm").Show form
	
	-- MAX CHILD
-- 	hwnd = dotnetobject "system.intptr" (windows.getMAXHWND())
-- 	thread = (dotnetclass "ThreadForm").ShowAsChild form hwnd
	
	for j = 1 to 5000 do print j
	
)