Is it possible to hide hourglass during Process Start?

183 views Asked by At

Background

I am working on a application that make extensive use of SetParent functionality in order to play several video players into a single application while keeping memory of main applicaton under control. Everytime the user requests to see a video, a new player.exe is executed and attached to the main window. This is working fine for most use case scenarios.

But there is one I am struggling with. In this scenario the user is playing lots of videos in a fast sequence, which means that main application is constantly killing and creating new players.

Everytime a player.exe is executed, a small hourglass icon appears on the mouse icon, and given that in this case scenario those players are created pretty fast, then the hourglass icon keeps playing constantly.

Motivation

I guess this is possible as for example google chrome makes use of this for each tab and you can add multiple taps without the busy hourglass icon to appear on each tab creation.

Details

I am in control of both main application and player applications just to note that I could do any change to both.

I have made a small windows form application as example of this behaviour, with 2 buttons and 1 panel.

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace SetParentTest
{
    public partial class Form1 : Form
    {
        private Process _childProcessPlayer;

        public Form1()
        {
            InitializeComponent();

            this.Closing += (sender, args) =>
            {
                Clear();
            };
        }

        public const UInt32 WS_POPUP = 0x80000000;
        public const UInt32 WS_CHILD = 0x40000000;

        [DllImport("user32.dll", SetLastError = true)]
        public static extern IntPtr SetParent(
            IntPtr windowChildHandle,
            IntPtr windowNewParentHandle);

        [DllImport("user32.dll", SetLastError = true)]
        public static extern UInt32 GetWindowLong(IntPtr hWnd, int nIndex);

        [DllImport ( "user32.dll" )]
        public static extern int SetWindowLong ( IntPtr hWnd, int nIndex, uint dwNewLong );

        public enum WindowLongFlags : int
        {
            GWL_EXSTYLE = -20,
            GWLP_HINSTANCE = -6,
            GWLP_HWNDPARENT = -8,
            GWL_ID = -12,
            GWL_STYLE = -16,
            GWL_USERDATA = -21,
            GWL_WNDPROC = -4,
            DWLP_USER = 0x8,
            DWLP_MSGRESULT = 0x0,
            DWLP_DLGPROC = 0x4
        }

        [DllImport("user32.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool MoveWindow(
            IntPtr windowHandle,
            int x,
            int y,
            int width,
            int height,
            [MarshalAs(UnmanagedType.Bool)] bool repaint);
        
        [DllImport("User32.dll", ExactSpelling = true, CharSet = CharSet.Auto)]
        public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);

        /// <summary>Enumeration of the different ways of showing a window using
        /// ShowWindow</summary>
        private enum WindowShowStyle : uint
        {
            /// <summary>Hides the window and activates another window.</summary>
            /// <remarks>See SW_HIDE</remarks>
            Hide = 0,
            /// <summary>Activates and displays a window. If the window is minimized
            /// or maximized, the system restores it to its original size and
            /// position. An application should specify this flag when displaying
            /// the window for the first time.</summary>
            /// <remarks>See SW_SHOWNORMAL</remarks>
            ShowNormal = 1,
            /// <summary>Activates the window and displays it as a minimized window.</summary>
            /// <remarks>See SW_SHOWMINIMIZED</remarks>
            ShowMinimized = 2,
            /// <summary>Activates the window and displays it as a maximized window.</summary>
            /// <remarks>See SW_SHOWMAXIMIZED</remarks>
            ShowMaximized = 3,
            /// <summary>Maximizes the specified window.</summary>
            /// <remarks>See SW_MAXIMIZE</remarks>
            Maximize = 3,
            /// <summary>Displays a window in its most recent size and position.
            /// This value is similar to "ShowNormal", except the window is not
            /// actived.</summary>
            /// <remarks>See SW_SHOWNOACTIVATE</remarks>
            ShowNormalNoActivate = 4,
            /// <summary>Activates the window and displays it in its current size
            /// and position.</summary>
            /// <remarks>See SW_SHOW</remarks>
            Show = 5,
            /// <summary>Minimizes the specified window and activates the next
            /// top-level window in the Z order.</summary>
            /// <remarks>See SW_MINIMIZE</remarks>
            Minimize = 6,
              /// <summary>Displays the window as a minimized window. This value is
              /// similar to "ShowMinimized", except the window is not activated.</summary>
            /// <remarks>See SW_SHOWMINNOACTIVE</remarks>
            ShowMinNoActivate = 7,
            /// <summary>Displays the window in its current size and position. This
            /// value is similar to "Show", except the window is not activated.</summary>
            /// <remarks>See SW_SHOWNA</remarks>
            ShowNoActivate = 8,
            /// <summary>Activates and displays the window. If the window is
            /// minimized or maximized, the system restores it to its original size
            /// and position. An application should specify this flag when restoring
            /// a minimized window.</summary>
            /// <remarks>See SW_RESTORE</remarks>
            Restore = 9,
            /// <summary>Sets the show state based on the SW_ value specified in the
            /// STARTUPINFO structure passed to the CreateProcess function by the
            /// program that started the application.</summary>
            /// <remarks>See SW_SHOWDEFAULT</remarks>
            ShowDefault = 10,
            /// <summary>Windows 2000/XP: Minimizes a window, even if the thread
            /// that owns the window is hung. This flag should only be used when
            /// minimizing windows from a different thread.</summary>
            /// <remarks>See SW_FORCEMINIMIZE</remarks>
            ForceMinimized = 11
        }

        /// <summary>
        /// Handles the Click event of the button1 control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="EventArgs"/> instance containing the event data.</param>
        private void button1_Click(object sender, EventArgs e)
        {
            AttachWindow();
        }

        /// <summary>
        /// Handles the Click event of the button1 control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="EventArgs"/> instance containing the event data.</param>
        private void button2_Click(object sender, EventArgs e)
        {
            Clear();
        }

        private void Clear()
        {
            if (_childProcessPlayer == null)
                return;

            if (_childProcessPlayer.HasExited)
            {
                _childProcessPlayer = null;
                return;
            }

            _childProcessPlayer.Kill();
            _childProcessPlayer = null;
        }

        private void AttachWindow()
        {
            // do it only once per test.
            if (_childProcessPlayer != null)
                return;

            // Instance of the remote process to start.
            //_childProcessPlayer = Process.GetProcessesByName("notepad").FirstOrDefault();
            _childProcessPlayer = new Process
            {
                StartInfo =
                {
                    FileName = @"C:\Windows\System32\notepad.exe",
                    //CreateNoWindow = true,
                    UseShellExecute = true,
                    WindowStyle = ProcessWindowStyle.Minimized
                },
                EnableRaisingEvents = true
            };

            Cursor.Current = Cursors.Default;
            _childProcessPlayer.Start();
            Cursor.Current = Cursors.Default;
            _childProcessPlayer.WaitForInputIdle();
            Cursor.Current = Cursors.Default;

            ShowWindow(_childProcessPlayer.MainWindowHandle, (int)WindowShowStyle.Hide);
            
            // Get process window handle.
            var mainWindowHandle = _childProcessPlayer.MainWindowHandle;
                
            // To prevent focus steal when SetParent is called I need to add WS_CHILD to the style.
            uint windowLong = GetWindowLong(
                mainWindowHandle,
                (int) WindowLongFlags.GWL_STYLE);

            // add ws_child
            windowLong |= WS_CHILD;

            // remove pop_up (most cases this is not necessary as it is already unset)
            windowLong &= ~WS_POPUP;

            // modify the style.
            SetWindowLong(
                mainWindowHandle,
                (int)WindowLongFlags.GWL_STYLE,
                windowLong);

            // Disable panel to prevent focus being stolen. (necessary in some cases)
            panel1.Enabled = false;

            // Execute Set parent.
            SetParent(mainWindowHandle, panel1.Handle);

            // Restore child state in order to allow editing in the notepad.
            windowLong &= ~WS_CHILD;
            SetWindowLong(
                mainWindowHandle,
                (int)WindowLongFlags.GWL_STYLE,
                windowLong);

            // Hide panel while notepad is resized.
            panel1.Visible = false;
            
            // Show notepad so resizing work
            ShowWindow(_childProcessPlayer.MainWindowHandle, (int)WindowShowStyle.ShowNormal);

            // Resize and move the window to the panel size.
            MoveWindow(mainWindowHandle, 0, 0, panel1.Width, panel1.Height, true);

            panel1.Visible = true;
            panel1.Enabled = true;
        }
    }
}

I leave the whole code as someone might be interested. Obviously I am not in control of notepad.exe though. This code is the closest I can get to the one I use in the original application. For unknown reasons starting hidden was not working fine with notepad so I had to hide it afterwards so it appears nicely without blinks into the panel.

Already tried

  1. I have noticed that retrieving notepad if it is already started have no issues.
  2. Calling Process.Start in a Task, or a different thread, have no effect.
  3. I have noticed that the hourglass appears even when you execute it directly on windows so the hourglass effect might me something that could be disabled by the child executable. (which means the adhoc test provided would never work as it is using notepad.exe).

Question

Finally the question is: is it possible to call Process Start of a child application avoiding the hourglass to appear on the mouse while doing so?

Worst case scenario would be to disable hourglass icon at windows registry (I guess this is possible although I currenltly don't know) but this would be my last option.

Edit 1:

Another thing I have tried: Forcing the mouse icon before calling Process start. I thought it was going to work as it worked for the hand icon or a different icon than the pointer, but if the icon is the pointer, it does not work.

    Cursor cr = new Cursor(Cursors.Arrow.Handle);
    Icon ico = Icon.FromHandle(cr.Handle);
    cr = new Cursor(ico.Handle);
    
    Cursor.Current = cr;
    _childProcessPlayer.Start();
    _childProcessPlayer.WaitForInputIdle();
    Cursor.Current = Cursors.Default;
1

There are 1 answers

0
Guillermo Alías On

I came up with 2 workarounds although not perfect ones:

1- Disable mouse with hourglass at windows level by configuring a different icon. Obviously that affects everything.

Change mouse icon

2- Forcing the icon before the operation did work if it does not know the icon is the arrow:

    Icon ico = Icon.FromHandle(Cursor.Current.Handle);
    ico = (Icon)ico.Clone();
    Cursor.Current = new Cursor(ico.Handle);
    _childProcessPlayer.Start();
    _childProcessPlayer.BeginOutputReadLine();
    _childProcessPlayer.WaitForInputIdle();

    Cursor.Current = Cursors.Default;

The problem with this other solution is that it only works well if the mouse remains over your application. If the mouse is outside it setting the icon does not work 100% of the times.