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.
MAXScript: How to create layered .NET window in Windows Forms
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!
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
here is my best…
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
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
)
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)
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
And what’s the problem? You can wait 5 seconds without additional entertainment
Just set the Wait Cursor and forget about everything else.
Of course, I can wait. 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
I have already received their displeasure about this.
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.
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!
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
)
theoretically this cannot work. But … it worked once. After that I tried it several times and max freezes.
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.

Perhaps it would be good to create a “SplashScreen” control derived from Form or Window and do all the work .Net side.
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.

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.
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.
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

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
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
(
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
)