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


#41

I can only confirm … yes it works. But I cannot understand and explain why.

Maybe you can run one of my WPF windows too? :wink:


#42

If we can interact with other Process why wouldn’t we be able to interact with other Threads?

I wrongly assumed WPF Windows would work the same as Forms, but they don’t, you need to create the Window in the same Thread that it will run. The simplest would be to setup the Window in .Net.

Perhaps there is a way to create the Window in .Net (in a new Thread), pass a reference back to MXS, set it up and then show it, who knows?


#43

this is what I expected … and for Forms too. But forms sometimes run on a different thread, even they were created in the main thread. I’m sure there are many limitations, but in some implementations this might work.

As for WPF windows … I wrote a simple window in XAML and it runs on a different thread because I can easily create it from String.

For some WPF windows, we can serialize and deserialize some simple windows by doing it like:

xaml = (dotnetclass "System.Windows.Markup.XamlWriter").Save wp 
stringReader = dotnetobject "System.IO.StringReader" xaml
xmlReader = (dotnetclass "System.Xml.XmlReader").Create stringReader
_wp = (dotnetclass "System.Windows.Markup.XamlReader").Load xmlReader

But this method has some limitations too…


#44

After I honestly wrote everything in C#, of course, everything worked. The WPF window must be created on a thread in which it will then live. A NameScope for the animation and all other animation things must be created there as well.

I’m pretty sure you can write a WPF window in XAML, but I’m not a big expert in this area. But fortunately there are many examples on the Internet.


#45

Thank denisT and PolyTools3D!
I tried to make a WPF window according to one of the examples you gave above. It works fine on its own, but when I try to run it in a separate thread using the PolyTools3D method, I get the error

- Runtime error: No 'Show' method found which matched argument list.

Most likely the error occurs because this method only works with the “Form” element, but does not work with the WPF “Window” element. Can you show the C# code snippet to launch a separate thread of a WPF window?

Here is the code I’m trying to run:

(
	
	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()
	
	try(::wp.close())catch()
	
	wp = 
	(
		wp = dotnetobject "System.Windows.Window"
		wp.WindowStartupLocation = wp.WindowStartupLocation.CenterScreen

		wp.WindowStyle = wp.WindowStyle.None
		wp.AllowsTransparency = true

		anim_file = @"D:\snake.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" 
		brush.ImageSource = image

		wp.Background = brush

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

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

		wp.RegisterName "snake_angle" tm
		duration = dotnetobject "System.Windows.Duration" ((dotnetclass "System.TimeSpan").FromMilliseconds 600)
		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 angle_animation
		Storyboard.SetTargetName angle_animation "snake_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" (windows.getMAXHWND())

		wp.Topmost = true	
		story_board.Begin wp	
		wp
	)
	
	xaml = (dotnetclass "System.Windows.Markup.XamlWriter").Save wp
	stringReader = dotnetobject "System.IO.StringReader" xaml
	xmlReader = (dotnetclass "System.Xml.XmlReader").Create stringReader
	_wp = (dotnetclass "System.Windows.Markup.XamlReader").Load xmlReader
	
	-- NOT MAX CHILD
	thread = (dotnetclass "ThreadForm").Show _wp
	
	-- MAX CHILD
-- 	hwnd = dotnetobject "system.intptr" (windows.getMAXHWND())
-- 	thread = (dotnetclass "ThreadForm").ShowAsChild form hwnd

	for j = 1 to 5000 do print j --or any MXS code that needs to run in the background in a separate thread

)

I used this image as source:
snake


#47

Since the Threads are synchronized with the UI Thread, I assumed you could also create the Window in .NET (in a separated Thread) and pass a reference to MXS to set it up there, but I could not manage to get it working.

So, you might need to create the WPF Windows and set it up in .NET. Then just call it from MXS.

You should the able to send the images, paths and other parameters from MXS to .NET before you construct the Window, making it a little more “flexible” to setup.


#48

Yes, I understand. I assigned an image path, set up animation and then created a WPF Window to display it all. Separately, this window will work fine (thanks to denisT). But how to transfer it to .NET now and run it all in a new Thread, I can’t. How to combine WPF Window with .NET?


#49
(
	
	fn Compile =
	(
		src  ="using System;\n"
		src +="using System.Windows.Interop;\n"
		src +="using System.Threading;\n"
		src +="using System.Threading.Tasks;\n"
		src +="using System.Windows;\n"
		src +="using System.Windows.Threading;\n"
		src +="using System.Windows.Media;\n"
		src +="using System.Windows.Media.Imaging;\n"
		src +="using System.Windows.Media.Animation;\n"
		src +="public class SplashScreen\n"
		src +="{\n"
		src +="	private Thread thread;\n"
		src +="	public string imageFilename = \"\";\n"
		src +="	public int duration = 1000;\n"
		src +="	public int owner = 0;\n"
		src +="	public void Show()\n"
		src +="	{\n"
		src +="		thread = new Thread(() => SetupWindow());\n"
		src +="		thread.SetApartmentState(ApartmentState.STA);\n"
		src +="		thread.IsBackground = true;\n"
		src +="		thread.Start();\n"
		src +="		Task.Delay(50).Wait();\n"
		src +="	}\n"
		src +="	public void Close()\n"
		src +="	{\n"
		src +="		thread.Abort();\n"
		src +="	}\n"
		src +="	private void SetupWindow()\n"
		src +="	{\n"
		src +="		BitmapImage image = new BitmapImage (new Uri (imageFilename));\n"
		src +="		RotateTransform tm = new RotateTransform()\n"
		src +="		{\n"
		src +="			CenterX = image.PixelWidth  * 0.5f,\n"
		src +="			CenterY = image.PixelHeight * 0.5f\n"
		src +="		};\n"
		src +="		ImageBrush brush = new ImageBrush(image);\n"
		src +="		brush.Transform = tm;\n"
		src +="		Window win = new Window()\n"
		src +="		{\n"
		src +="			Title                 = \"SplashScreen\",\n"
		src +="			Width                 = image.PixelWidth,\n"
		src +="			Height                = image.PixelHeight,\n"
		src +="			WindowStartupLocation = WindowStartupLocation.CenterScreen,\n"
		src +="			WindowStyle           = WindowStyle.None,\n"
		src +="			AllowsTransparency    = true,\n"
		src +="			//Topmost               = true,\n"
		src +="			ShowInTaskbar         = false,\n"
		src +="			Background            = brush\n"
		src +="		};\n"
		src +="		DoubleAnimation ani = new DoubleAnimation()\n"
		src +="		{\n"
		src +="			From           = 0.0f,\n"
		src +="			To             = 360.0f,\n"
		src +="			Duration       = new Duration(TimeSpan.FromMilliseconds(duration)),\n"
		src +="			AutoReverse    = false,\n"
		src +="			RepeatBehavior = RepeatBehavior.Forever\n"
		src +="		};\n"
		src +="		Storyboard storyboard = new Storyboard();\n"
		src +="		storyboard.Children.Add(ani);\n"
		src +="		Storyboard.SetTargetName(ani, \"loader\");\n"
		src +="		Storyboard.SetTargetProperty(ani, new PropertyPath(RotateTransform.AngleProperty));\n"
		src +="		WindowInteropHelper helper = new WindowInteropHelper(win);\n"
		src +="		helper.Owner = new IntPtr(owner);\n"
		src +="		NameScope.SetNameScope(win, new NameScope());\n"
		src +="		win.RegisterName(\"loader\", tm);\n"
		src +="		win.Show();\n"
		src +="		storyboard.Begin(win);\n"
		src +="		Dispatcher.Run();\n"
		src +="	}\n"
		src +="}\n"
		
		assemblies = #()
		append assemblies "System.dll"
		append assemblies "System.Windows.dll"
		append assemblies "PresentationCore.dll"
		append assemblies "PresentationFramework.dll"
		append assemblies "WindowsBase.dll"
		append assemblies "System.Xaml.dll"
		
		params = dotnetobject "System.CodeDom.Compiler.CompilerParameters"
		for j in assemblies do params.ReferencedAssemblies.Add (dotnet.loadassembly j).Location
		
		result = (dotnetobject "Microsoft.CSharp.CSharpCodeProvider").CompileAssemblyFromSource params #(src)
		
		errors = result.Errors
		if errors.HasErrors then
		(
			msg = stringstream ""
			for j = 1 to errors.Count do
			(
				err = errors.Item[j-1]
				format "Error:% Line:% Column:%\n\t%\n\n" err.ErrorNumber err.Line err.Column err.ErrorText to:msg 
			)
			msg = msg as string
			format "%" msg
			messagebox msg
		) else (
			return result.CompiledAssembly
		)
		
	)
	
	dll = Compile()
	
	global SplashScreen
	
	SplashScreen = dll.CreateInstance "SplashScreen"
	
	SplashScreen.imageFilename = @"C:\snake.png"
	SplashScreen.duration = 600
	SplashScreen.owner = windows.getMAXHWND()
	
	SplashScreen.Show()
	--SplashScreen.Close()	-- Call to close the window
	
)
  • No error handling implemented
  • There are still some issues

#50

Thank you so much, PolyTools3D!
Unfortunately, I could not check and understand the work of the given code, because when start it I immediately get an error:

-- Runtime error: .NET runtime exception: Could not load file or assembly 'file:///C:\Users\******\AppData\Local\Temp\3cretn1a.dll' or one of its dependencies. The system cannot find the file specified.

I will look for information and try to deal with this error.
Thanks!


#51

Probably the assembly files are not loaded. I’ve updated the code so it now loads all the proper assemblies.

It should run out of the box in any Max version.


#52

for j in assemblies do params.ReferencedAssemblies.Add (dotnet.loadassembly j).Location
brilliant! This is the best method to add references!


#53

Thanks a lot, PolyTools3D!
I checked the code in 3ds max 2020 and 2021 on WIndows 10 - everything works fine and no errors!
This is an interesting technique that can be applied to other things that are difficult to implement in MXS.


#54

look at this article

makes sense for me…

based on the article recommendations:

global wpf_thread_win
global wpf_thread_ass

fn create_wpf_thread_win forceRecompile:on =
(
	if forceRecompile or not iskindof ::wpf_thread_ass dotnetobject or (::wpf_thread_ass.GetType()).name != "Assembly" do
	(

source  = ""
source += "using System;\n"
source += "using System.Windows.Interop;\n"
source += "using System.Threading.Tasks;\n"
source += "using System.Windows;\n"
source += "using System.Windows.Media;\n"
source += "using System.Windows.Media.Animation;\n"
source += "using System.Windows.Media.Imaging;\n"
source += "\n"
source += "using System.Threading;\n"
source += "using System.Windows.Threading;\n"
source += "\n"
source += "public static class ThreadRunner\n"
source += "{\n"
source += "    public static Thread MakeWindow(string imgfile, int owner = 0)\n"
source += "    {\n"
source += "        Thread sta = new Thread(delegate()\n"
source += "        {\n"
source += "            SynchronizationContext.SetSynchronizationContext(\n"
source += "                new DispatcherSynchronizationContext(\n"
source += "                    Dispatcher.CurrentDispatcher));\n"
source += "\n"
source += "            var image = new BitmapImage(new System.Uri(imgfile));\n"
source += "            var brush = new ImageBrush(image)\n"
source += "            {\n"
source += "                Transform = new RotateTransform(0, image.PixelWidth / 2, image.PixelHeight / 2),\n"
source += "            };\n"
source += "\n"
source += "            Window w = new Window()\n"
source += "            {\n"
source += "                WindowStyle = WindowStyle.None,\n"
source += "                AllowsTransparency = true,\n"
source += "                WindowStartupLocation = WindowStartupLocation.CenterScreen,\n"
source += "\n"
source += "                Width = image.PixelWidth,\n"
source += "	               Height = image.PixelHeight,\n"
source += "\n"
source += "                Background = brush,\n"
source += "            };\n"
source += "\n"
source += "            // Animate the brush's opacity to 0 and back when\n"
source += "            // the left mouse button is pressed over the rectangle.\n"
source += "            //\n"
source += "            DoubleAnimation opacityAnimation = new DoubleAnimation(0.1, 1.0, TimeSpan.FromMilliseconds(800))\n"
source += "            {\n"
source += "                AutoReverse = true,\n"
source += "                RepeatBehavior = RepeatBehavior.Forever,\n"
source += "            };\n"
source += "            Storyboard.SetTargetName(opacityAnimation, \"anim_opacity\");\n"
source += "            Storyboard.SetTargetProperty(opacityAnimation, new PropertyPath(ImageBrush.OpacityProperty));\n"
source += "\n"
source += "            DoubleAnimation rotationAnimation = new DoubleAnimation(0, 360 - 0.36, TimeSpan.FromMilliseconds(1000))\n"
source += "            {\n"
source += "                AutoReverse = false,\n"
source += "                RepeatBehavior = RepeatBehavior.Forever,\n"
source += "            };\n"
source += "            Storyboard.SetTargetName(rotationAnimation, \"anim_rotation\");\n"
source += "            Storyboard.SetTargetProperty(rotationAnimation, new PropertyPath(RotateTransform.AngleProperty));\n"
source += "\n"
source += "            Storyboard story = new Storyboard();\n"
source += "            story.Children.Add(opacityAnimation);\n"
source += "            story.Children.Add(rotationAnimation);\n"
source += "\n"
source += "            NameScope.SetNameScope(w, new NameScope());\n"
source += "            w.RegisterName(\"anim_opacity\", brush);\n"
source += "            w.RegisterName(\"anim_rotation\", brush.Transform);\n"
source += "\n"
source += "            w.Closed += (s, e) =>\n"
source += "               Dispatcher.CurrentDispatcher.BeginInvokeShutdown(DispatcherPriority.Background);\n"
source += "            w.MouseDown += (s, e) =>\n"
source += "               ((Window)s).Close();\n"
source += "            w.Activated += (s, e) =>\n"
source += "            {\n"
source += "                story.Begin(w);\n"
source += "            };\n"
source += "\n"
source += "            if (owner != 0)\n"
source += "            {\n"
source += "                WindowInteropHelper helper = new WindowInteropHelper(w);\n"
source += "                helper.Owner = new IntPtr(owner);\n"
source += "            }\n"
source += "            w.Show();\n"
source += "\n"
source += "            System.Windows.Threading.Dispatcher.Run();\n"
source += "        });\n"
source += "\n"
source += "        sta.SetApartmentState(ApartmentState.STA);\n"
source += "        sta.IsBackground = true;\n"
source += "        sta.Start();\n"
source += "\n"
source += "        return sta;\n"
source += "    }\n"
source += "}\n"

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

		assemblies = 
		#(
			"System.dll",
			"System.Windows.dll",
			"PresentationCore.dll",
			"PresentationFramework.dll",
			"WindowsBase.dll",
			"System.Xaml.dll"
		)
		
		-- VERY SMART:
		for assembly in assemblies do compilerParams.ReferencedAssemblies.Add (dotnet.loadassembly assembly).Location

		compilerParams.GenerateInMemory = on
		compilerResults = csharpProvider.CompileAssemblyFromSource compilerParams #(source)
		
		if (compilerResults.Errors.Count > 0 ) then
		(
			errs = stringstream ""
			for i = 0 to (compilerResults.Errors.Count-1) do
			(
				err = compilerResults.Errors.Item[i]
				format "Error:% Line:% Column:% %\n" err.ErrorNumber err.Line err.Column err.ErrorText to:errs 
			)
			MessageBox (errs as string) title: "Errors encountered while compiling C# code"
			format "%\n" errs
			undefined
		)
		else
		(
			wpf_thread_ass = compilerResults.CompiledAssembly
		)
	)
)
create_wpf_thread_win()
--thread = (dotnetclass (wpf_thread_ass.GetType "ThreadRunner")).MakeWindow @"C:\temp\snake.png" (windows.getMAXHWND())
thread = (dotnetclass (wpf_thread_ass.GetType "ThreadRunner")).MakeWindow @"C:/Temp/spongebob_128x128.png" (windows.getMAXHWND())

spongebob_128x128


#55

The article discusses the cases where you manually close the window, so the thread remains running, as expected.

I am not so sure this is the same scenario, since I’ve implemented a method Close() which directly aborts the thread. If you monitor the running threads you’ll see that when you call Close() it effectively gets rid of the thread.

On the contrary, if you decide to close the window manually, without calling Close(), the threads keep running and accumulating.


#56

this is exactly what I want. Because the created thread is only for this animated thing. There is no reason to continue its working after closing the window.


#57

Thanks, denisT!
Also a good method and works better and more consistently. Upon more complete testing of the PolyTools3D method, I found that it sometimes freeze and does not play animation when another script is loaded in a separate thread, but your method works steadily without freezing.
But there is one question - how to close this window automatically (not manually) from the outside using the Close() command?
Inside C#, this directive works:

Dispatcher.CurrentDispatcher.BeginInvoke(new Action(() => w.Close()))

How to accessing it from outside to close this window and abort this thread?

EDIT: I changed the last lines in your code to these:

create_wpf_thread_win()
thread = dotnetclass (wpf_thread_ass.GetType "ThreadRunner")
win = thread.MakeWindow @"C:/Temp/spongebob_128x128.png" (windows.getMAXHWND ())

and close this window from the outside with this command:

win.Abort()

But it seems to me this is a very wrong way out :roll_eyes:


#58

The Abort is absolutely safe method as far as what we are doing in this thread.


#59

This is unlikely. We both handle the threading the same way.


#60

I was also surprised, but nevertheless it is so.
Below is the work of two variants - you and PolyTools3D.
I did the screen capturing with the “Gif Screen Recorder” program (http://gifrecorder.com/)

  1. denisT code sample:

denisT_2

On the left, you can see a fragment of the script being launched after the splash screen has worked. Moreover, the splash screen animation was not interfered with by the simultaneous recording from the screen by the above program.

  1. PolyTools3D code sample:

PolyTools3D

As you can see, the difference is significant. Moreover, in this version, the UI of the main script did not load after splashscreen until I pressed the ESC button and closed the screen recorder program. You can also see some strange overlay of static and animated images. This issue may be due to a conflict with the screen recorder. But the short animation freezes that can be seen in the snapshot also occur when the screen recorder was not used.

Although, when launched separately (without using it in a complex with loading the main script), both code variants work well.:ok_hand:


#61

never really interested in WPF animation before … but I tried and found it interesting … maybe it will come in handy someday :slight_smile:

spongebob_01