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


#1

Hello, all!

I am looking for a way to make a splash screen using an animated gif image. I am using the code below.

(
	global rol_splashScreen
	--
	local lab_splashScreen
	local splashscreenGIF = @"D:\process.gif"

	function SetControlPosition posX posY = 
	(
		dotNetObject "System.Drawing.Point" posX posY
	)	
	function SetControlSize width height = 
	(
		dotNetObject "System.Drawing.Size" width height
	)
	function BuildRollout rol width: height:  =
	(		
		rol.ShowIcon=false
		rol.MaximizeBox=false
		rol.MinimizeBox=false
		rol.ControlBox=false
		rol.FormBorderStyle=rol.FormBorderStyle.None
		rol.ClientSize = SetControlSize width height      
		rol.StartPosition = rol.StartPosition.CenterScreen
		rol.AllowTransparency = true
		rol.BackColor=(dotNetClass "system.drawing.color").fromArgb 255 255 255
		rol.TransparencyKey=(dotNetClass "system.drawing.color").fromArgb 255 255 255
	)
	
	function BuildLabel label posX: posY: width: height: =
	(
		label.bounds=(dotNetObject "system.drawing.rectangle" posX posY width height)
	)	
	
	rol_splashScreen = dotnetobject "Form" 
	BuildRollout rol_splashScreen width:200 height:204
	
	lab_splashScreen = dotNetObject "Label"
	BuildLabel lab_splashScreen posX:0 posY:0 width:200 height:204
	lab_splashScreen.image = (dotNetclass "System.Drawing.Image").fromfile splashscreenGIF
	
	rol_splashScreen.Controls.AddRange #(lab_splashScreen)
	
	rol_splashScreen.topMost = true
	
	rol_splashScreen.Show()
)

Everything works well, but one problem remains - along the edges of the animated image, there are pixels of the color specified in the TransparencyKey parameter. I understand that this is a flaw in gif, so I want to use the .NET form Layered Windows features to fix this problem. I found this method for C# here: https://stackoverflow.com/questions/33530623/c-sharp-windows-form-transparent-background-image/33531201#33531201

but I can’t figure out how to adapt this code for MaxScript.
Can someone have any ideas how to do this?


#2

Use WPF.


#3

Thank. If it’s not hard for you, could you show me how to do this in MAXScript using the C# code below?

#region Using directives
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
#endregion
namespace CSWinFormLayeredWindow
{
    public partial class PerPixelAlphaForm : Form
    {
        public PerPixelAlphaForm()
        {
            this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None;
            this.ShowInTaskbar = false;
            this.StartPosition = FormStartPosition.CenterScreen;
            this.Load += PerPixelAlphaForm_Load;
        }

        void PerPixelAlphaForm_Load(object sender, EventArgs e)
        {
            this.TopMost = true;
        }
        protected override CreateParams CreateParams
        {
            get
            {
                // Add the layered extended style (WS_EX_LAYERED) to this window.
                CreateParams createParams = base.CreateParams;
                if(!DesignMode)
                    createParams.ExStyle |= WS_EX_LAYERED;
                return createParams;
            }
        }
        /// <summary>
        /// Let Windows drag this window for us (thinks its hitting the title 
        /// bar of the window)
        /// </summary>
        /// <param name="message"></param>
        protected override void WndProc(ref Message message)
        {
            if (message.Msg == WM_NCHITTEST)
            {
                // Tell Windows that the user is on the title bar (caption)
                message.Result = (IntPtr)HTCAPTION;
            }
            else
            {
                base.WndProc(ref message);
            }
        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="bitmap"></param>
        public void SelectBitmap(Bitmap bitmap)
        {
            SelectBitmap(bitmap, 255);
        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="bitmap">
        /// 
        /// </param>
        /// <param name="opacity">
        /// Specifies an alpha transparency value to be used on the entire source 
        /// bitmap. The SourceConstantAlpha value is combined with any per-pixel 
        /// alpha values in the source bitmap. The value ranges from 0 to 255. If 
        /// you set SourceConstantAlpha to 0, it is assumed that your image is 
        /// transparent. When you only want to use per-pixel alpha values, set 
        /// the SourceConstantAlpha value to 255 (opaque).
        /// </param>
        public void SelectBitmap(Bitmap bitmap, int opacity)
        {
            // Does this bitmap contain an alpha channel?
            if (bitmap.PixelFormat != PixelFormat.Format32bppArgb)
            {
                throw new ApplicationException("The bitmap must be 32bpp with alpha-channel.");
            }

            // Get device contexts
            IntPtr screenDc = GetDC(IntPtr.Zero);
            IntPtr memDc = CreateCompatibleDC(screenDc);
            IntPtr hBitmap = IntPtr.Zero;
            IntPtr hOldBitmap = IntPtr.Zero;

            try
            {
                // Get handle to the new bitmap and select it into the current 
                // device context.
                hBitmap = bitmap.GetHbitmap(Color.FromArgb(0));
                hOldBitmap = SelectObject(memDc, hBitmap);

                // Set parameters for layered window update.
                Size newSize = new Size(bitmap.Width, bitmap.Height);
                Point sourceLocation = new Point(0, 0);
                Point newLocation = new Point(this.Left, this.Top);
                BLENDFUNCTION blend = new BLENDFUNCTION();
                blend.BlendOp = AC_SRC_OVER;
                blend.BlendFlags = 0;
                blend.SourceConstantAlpha = (byte)opacity;
                blend.AlphaFormat = AC_SRC_ALPHA;

                // Update the window.
                UpdateLayeredWindow(
                    this.Handle,     // Handle to the layered window
                    screenDc,        // Handle to the screen DC
                    ref newLocation, // New screen position of the layered window
                    ref newSize,     // New size of the layered window
                    memDc,           // Handle to the layered window surface DC
                    ref sourceLocation, // Location of the layer in the DC
                    0,               // Color key of the layered window
                    ref blend,       // Transparency of the layered window
                    ULW_ALPHA        // Use blend as the blend function
                    );
            }
            finally
            {
                // Release device context.
                ReleaseDC(IntPtr.Zero, screenDc);
                if (hBitmap != IntPtr.Zero)
                {
                    SelectObject(memDc, hOldBitmap);
                    DeleteObject(hBitmap);
                }
                DeleteDC(memDc);
            }
        }
        #region Native Methods and Structures

        const Int32 WS_EX_LAYERED = 0x80000;
        const Int32 HTCAPTION = 0x02;
        const Int32 WM_NCHITTEST = 0x84;
        const Int32 ULW_ALPHA = 0x02;
        const byte AC_SRC_OVER = 0x00;
        const byte AC_SRC_ALPHA = 0x01;

        [StructLayout(LayoutKind.Sequential)]
        struct Point
        {
            public Int32 x;
            public Int32 y;

            public Point(Int32 x, Int32 y)
            { this.x = x; this.y = y; }
        }

        [StructLayout(LayoutKind.Sequential)]
        struct Size
        {
            public Int32 cx;
            public Int32 cy;

            public Size(Int32 cx, Int32 cy)
            { this.cx = cx; this.cy = cy; }
        }

        [StructLayout(LayoutKind.Sequential, Pack = 1)]
        struct ARGB
        {
            public byte Blue;
            public byte Green;
            public byte Red;
            public byte Alpha;
        }

        [StructLayout(LayoutKind.Sequential, Pack = 1)]
        struct BLENDFUNCTION
        {
            public byte BlendOp;
            public byte BlendFlags;
            public byte SourceConstantAlpha;
            public byte AlphaFormat;
        }

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        static extern bool UpdateLayeredWindow(IntPtr hwnd, IntPtr hdcDst,
            ref Point pptDst, ref Size psize, IntPtr hdcSrc, ref Point pprSrc,
            Int32 crKey, ref BLENDFUNCTION pblend, Int32 dwFlags);

        [DllImport("gdi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        static extern IntPtr CreateCompatibleDC(IntPtr hDC);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        static extern IntPtr GetDC(IntPtr hWnd);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);

        [DllImport("gdi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        static extern bool DeleteDC(IntPtr hdc);

        [DllImport("gdi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        static extern IntPtr SelectObject(IntPtr hDC, IntPtr hObject);

        [DllImport("gdi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        static extern bool DeleteObject(IntPtr hObject);

        #endregion
    }
}
public partial class Form4 : CSWinFormLayeredWindow.PerPixelAlphaForm
{
    public Form4()
    {
        InitializeComponent();
        this.SelectBitmap(Properties.Resources.splash);
    }
}

#4

I do it like this:

fn CreateImageSource fileName =
(
	src = dotNetObject "System.Windows.Media.Imaging.BitmapImage"
	src.BeginInit()
	src.UriSource = dotNetObject "System.Uri" fileName
	src.EndInit()
	src
)

try(window.close())catch()

window = dotNetObject "System.Windows.Window"
window.WindowStartupLocation = window.WindowStartupLocation.CenterScreen
window.AllowsTransparency = true
window.WindowStyle = window.WindowStyle.None
window.background = dotnetobject "System.Windows.Media.SolidColorBrush" ((dotnetclass "System.Windows.Media.Color").FromArgb 0 0 0 0)
(dotnetobject "System.Windows.Interop.WindowInteropHelper" Window).owner = dotnetobject "IntPtr" (windows.getMAXHWND())
(dotnetclass "ManagedServices.AppSDK").ConfigureWindowForMax Window

image = dotNetObject "System.Windows.Controls.Image"
image.Source = CreateImageSource @"C:\Users\New\Downloads\9xu2u.png"
window.content = image

window.show()

#5

Thanks, your method works well with static images (for example * .png), but unfortunately, this method does not work with animated GIF images.


“When displaying a multiframe image, only the first frame is displayed. The animation of multiframe images is not supported by the Image control.

In the first message I filled up a question about an animated GIF.
I tried to change your code for using MediaElement, but since the animation was played only once, no looping. There is also an old problem with pixels at the edges of the image when using Opacity on the mask.
To fix these problems, I wanted to use the method with Layered Window implemented in C#, which was given in the first post, but unfortunately I do not know how to adapt this code for use in MAXScript.


#6

what MAX version do you use?


#7

From 2013 to 2021:roll_eyes:
I am writing a script that should work on a wide range of 3ds max versions. Before loading the script, there must be a splashscreen with an animated GIF image.


#8
try(form.close()) catch()
form = dotnetobject "MaxCustomControls.Maxform"
form.Text = "Life is getting better!" 
form.size = dotnetobject "System.Drawing.Size" 300 170
form.StartPosition = form.StartPosition.CenterScreen
--form.Location = dotnetobject "System.Drawing.Point" 400 400
	
pbx = dotnetobject "PictureBox" 
pbx.Dock = pbx.Dock.Fill
pbx.SizeMode = pbx.SizeMode.Zoom
form.controls.add pbx

theGif = @"C:\Temp\GIFs\Minions10.gif"	
animatedImage = dotnetobject "System.Drawing.Bitmap" theGif
pbx.Image = animatedImage
	
form.ShowModeless()

Minions10


#9
try(destroydialog AnimGIF_Rollout) catch()
rollout AnimGIF_Rollout "Life is getting better!" width:300
(
	local theGif = @"C:\Temp\GIFs\Minions10.gif"	
	local animatedImage = dotnetobject "System.Drawing.Bitmap" theGif
	local sz = [animatedImage.Width, animatedImage.Height]
	dotnetcontrol panel "UserControl" width:sz.x height:sz.y align:#center
	
	on AnimGIF_Rollout open do
	(
		pbx = dotnetobject "PictureBox" 
		pbx.Dock = pbx.Dock.Fill
		pbx.SizeMode = pbx.SizeMode.Zoom
		panel.controls.add pbx
		pbx.Image = animatedImage
	)
)
createdialog AnimGIF_Rollout

#10

Hello, denisT!

Thanks for the suggested ways. But I actually have no problem creating a window to display an animated GIF.
The problem is quite different!
Probably I did not explain well in the first post of this topic, but I have a problem with the fact that animated GIF images with transparency are displayed dirty, i.e. along the edges of the mask pixels of a different color remain, which are not anti-aliased. You can see this issue in the image below:
_210216222116
In the first post I provided a link that describes a way to get rid of these pixels and get antialiased edges of the transparent image, but this method is written in C# and I would like to know how you can implement this in MAXScript.


#11

The short answer is that what you want to do, in the way you want to do it, can’t be done.

In the link you referenced, the OP asks about using .PNG with ALPHA channel, and so are the solutions.

You are trying to do the same with .GIF with TRANSPARENCY, which is something completely different.

There are some major differences between .GIF and .PNG:

.GIF

  • 8 Bit
  • Can be animated.

.PNG

  • 24-32 Bit
  • Can’t be animated

So, what options do you have?

Using animated .GIF
Have 2 separated animated GIF, one with color and the other with a grayscale mask for the ALPHA channel and compose them at runtime.

Using .PNG sequence
Have a sequence of .PNG files with proper ALPHA channel and compose them at runtime.

Alternatively, you could use .APNG (an extension of .PNG format), which is a mix of both worlds as it supports both animation and ALPHA channel.

You could also look into the .WEBP format

BTW, I don’t think neither .APNG nor .WEBP are natively supported by the .Net framework.


#12

how complicated is your animation? maybe you can do it with WPF and System.Windows.Shapes?


#13

here is I just play animation sequence of PNG files using .NET timer and WPF window:

I found several tools on the internet that create a PNG sequence from an animated GIF. It might require a little cleanup, but all seems like a working pipeline.


#14

Another old “trick” is something like this:

transparent_rollout

(
	
	try (destroyDialog ::RO_SPLASHSCREEN) catch()
	
	rollout RO_SPLASHSCREEN "" width:512 height:256
	(
		timer clock "" interval:30
		
		local current = 1
		local images  = #()
		
		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]
			
			folder = getfilenamepath (getthisscriptfilename())
			files  = getfiles (pathconfig.appendpath folder "splash*.png")
			
			freescenebitmaps()
			gc()
			
			bmp = dotnetobject "system.drawing.bitmap" 512 256
			grp = (dotnetclass "system.drawing.graphics").FromImage bmp
			
			size = dotnetobject "system.drawing.size" 512 256
			grp.CopyFromScreen clienPos[1] clienPos[2] 0 0 size
			
			(dotnetclass "System.windows.forms.clipboard").setImage bmp
			
			bmp = getclipboardbitmap()
			
			images = for j in files collect
			(
				img  = openbitmap j
				back = copy bmp
				pastebitmap img back (box2 0 0 512 256) [0,0] type:#blend
				back
			)
			
			setdialogpos RO_SPLASHSCREEN dialogPos
		)
		
		on RO_SPLASHSCREEN lbuttonup pt do destroyDialog ::RO_SPLASHSCREEN
	)
	
	createdialog RO_SPLASHSCREEN modal:true style:#()
	
)

But I would do it as @MZ showed, in .Net and using a timer, so you don’t need to compose the images with a background.


#15

This would somehow work on some images, the ones that are outlined, but not in those with transparent areas.

And it would depend on how clean the source .GIF is.


#16

The best approach would be to use built-in WPF animation system to swap images.


#17

Hi guys!

Thanks everyone for the great advice and examples provided.
Yes, I understand that there are limitations in GIF that cannot be circumvented. Solving the problem with an animated splash screen using PNG images looks more logical, but the fact is that for this you have to use a multiply of PNG files that are loaded one by one, which is not very good for me. Therefore, I was looking for a solution that would result in only one animated file being used.
The APNG format would be a good fit for this, but I haven’t found a way to load it and render it as an animated image using MAXScript.
I found some information on this here: https://pronama.jp/2020/02/11/wpf-apng/
Unfortunately, all information is in Japanese, and automitic translation does not provide a sufficiently accurate understanding of this information.
I also found an archive with the library: https://archive.codeplex.com/?p=apngwpf, but as far as I understood, this is also written in C # and I could not figure it out.


#18

You could store them as base64 strings right inside the script so you don’t need to load each file from disk separately.


#19

Use an animation strip.
Animation Strip Generator

(
	
	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  = #()
		
		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
		)
		
		on RO_SPLASHSCREEN lbuttonup pt do destroyDialog ::RO_SPLASHSCREEN
	)
	
	createdialog RO_SPLASHSCREEN modal:true style:#()
	
)

#20

Hello, PolyTools3D!

Thanks, it looks like this is what you need!

Only one question I have an animated image is displayed without transparency, although the PNG file itself has a transparent background. How to make the background transparency also taken from the same PNG file?

EDIT #1: I figured it out about transparency! It turns out that if the script is run when the MAXScript Editor window is open, then the background of the MAXScript Editor is taken when generating the image. If you run the script from a file, then everything works fine and transparency is taken into account correctly!

You can use this resource to create animated PNG files: https://icons8.com/preloaders/en/

EDIT #2: While testing this method, I ran into another problem. The fact is that this splash-screen should be played while the main script is loaded into memory. When the script is fully loaded into memory and its interface should start, then the splash-screen should turn off (i.e. the RO_SPLASHSCREEN rollout should close). In this case, the splash screen dialog is modal, and therefore the main script is not loaded at all as long as the splash screen is opened. The main script starts loading only after I manually turn off the splash screen by clicking on it with the mouse. If the dialog with the splash-screen is not created modal (modal: false), then everything works as it should and the main script is loaded into memory at the time when the splash-screen is displayed on the screen, but the splash-screen window remains empty and in it does not load picture frames from striped PNG (into the RO_SPLASHSCREEN dialog). Unfortunately, I don’t know how to fix this :frowning:


But I’m still wondering if it is possible to also implement this using the APNG format, so that you can use one animated file as in the case of GIF, without having to cut each frame from one row of pictures and create a separate virtual image for each frame. Is there a way to read correctly an APNG file as animation using MAXScript and .NET ( or WPF?)?