Virtual Desktop Switching in Windows 10

 

Windows 10 introduces a new concept (for Windows anyway) called Virtual Desktops.  Currently, the guidance for this on MSDN states:

The user can group a collection of windows together to create a virtual desktop. Every window is considered to be part of a virtual desktop. When one virtual desktop is hidden, all of the windows associated with it are also hidden. This enables the user to create multiple working environments and to be able to switch between them. Similarly, when a virtual desktop is selected to be active, the windows associated with that virtual desktop are displayed on the screen.

To support this concept, applications should avoid automatically switching the user from one virtual desktop to another. Only the user should instigate that change. In order to support this, newly created windows should appear on the currently active virtual desktop. In addition, if an application can reuse currently active windows, it should only reuse windows if they are on the currently active virtual desktop. Otherwise, a new window should be created.

That’s good advice as it makes for the best user experience in most cases and as a developer lets you ignore virtual desktops altogether in most simple applications; however, if you have an application or scenario that wants to do something such as always stay on top even when the user changes virtual desktops, what can you do?

IVirtualDesktopManager

To go along with the addition of virtual desktops in Windows 10, a new shell interface was introduced called IVirtualDesktopManager.  It only has three functions, but those allow you to do many things with virtual desktops and your own application.  Attempting to say move a window to another virtual desktop with these functions will not work for windows that your process doesn’t own.  As this isn’t a scenario that should be common or desired behavior for most applications, there’s isn’t a notification that you can subscribe to so that you know that your application window’s virtual desktop is no longer visible or that your application window has been moved to a new virtual desktop.  However, if your window has focus when the user switches to another virtual desktop, you will be told that you’ve lost focus.

IsWindowOnCurrentVirtualDesktop will tell you if your window is on the current virtual desktop.  GetWindowDesktopId will give you the ID of the desktop the specified window is on.  MoveWindowToDesktop will allow you to move a specified window to a specified desktop.

But how do you know what the current desktop ID is if you don’t have any windows on the current desktop?  That one turns out to be pretty simple.  If you create a new window with no parent, it will be placed on the current virtual desktop.

Demonstration

Putting all of the above together, here’s a straightforward C# WinForms app as an example of an always on top window that can move itself between Virtual Desktops (csproj attached at the end):

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

namespace VirtualDesktopSwitch
{
    /// <summary>
    /// Example form
    /// </summary>
    public partial class VDExampleWindow : Form
    {
        public VDExampleWindow()
        {
            InitializeComponent();
        }
        private VirtualDesktopManager vdm;
        private void VDExampleWindow_Load(object sender, EventArgs e)
        {
            //Create IVirtualDesktopManager on load
            vdm = new VirtualDesktopManager();
        }

        private void label1_Click(object sender, EventArgs e)
        {
            //Show details on click
            MessageBox.Show("Virtual Desktop ID: " + vdm.GetWindowDesktopId(Handle).ToString("X") + Environment.NewLine +
                "IsCurrentVirtualDesktop: " + vdm.IsWindowOnCurrentVirtualDesktop(Handle).ToString()
                );
        }
        //Timer tick to check if the window is on the current virtual desktop and change it otherwise
        //A timer does not have to be used, but something has to trigger the check
        //If the window was active before the vd change, it would trigger 
        //the deactivated and lost focus events when the vd changes
        //The timer always gets triggered which makes the example hopefully less confusing
        private void VDCheckTimer_Tick(object sender, EventArgs e)
        {
            try
            {
                if (!vdm.IsWindowOnCurrentVirtualDesktop(Handle))
                {
                    using (NewWindow nw = new NewWindow())
                    {
                        nw.Show(null);
                        vdm.MoveWindowToDesktop(Handle, vdm.GetWindowDesktopId(nw.Handle));
                    }
                }
            }
            catch
            {
                //This will fail due to race conditions as currently written on occassion
            }
        }

        /// <summary>
        /// Required designer variable.
        /// </summary>
        private System.ComponentModel.IContainer components = null;

        /// <summary>
        /// Clean up any resources being used.
        /// </summary>
        /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        #region Windows Form Designer generated code

        /// <summary>
        /// Required method for Designer support - do not modify
        /// the contents of this method with the code editor.
        /// </summary>
        private void InitializeComponent()
        {
            this.components = new System.ComponentModel.Container();
            this.label1 = new System.Windows.Forms.Label();
            this.VDCheckTimer = new System.Windows.Forms.Timer(this.components);
            this.SuspendLayout();
            // 
            // label1
            // 
            this.label1.Dock = System.Windows.Forms.DockStyle.Fill;
            this.label1.Font = new System.Drawing.Font("Microsoft Sans Serif", 13.875F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
            this.label1.Location = new System.Drawing.Point(0, 0);
            this.label1.Name = "label1";
            this.label1.Size = new System.Drawing.Size(1112, 368);
            this.label1.TabIndex = 0;
            this.label1.Text = "Example Contents";
            this.label1.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
            this.label1.Click += new System.EventHandler(this.label1_Click);
            // 
            // VDCheckTimer
            // 
            this.VDCheckTimer.Enabled = true;
            this.VDCheckTimer.Interval = 1000;
            this.VDCheckTimer.Tick += new System.EventHandler(this.VDCheckTimer_Tick);
            // 
            // VDExampleWindow
            // 
            this.AutoScaleDimensions = new System.Drawing.SizeF(12F, 25F);
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.ClientSize = new System.Drawing.Size(1112, 368);
            this.Controls.Add(this.label1);
            this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.Fixed3D;
            this.Name = "VDExampleWindow";
            this.Text = "VD Example";
            this.TopMost = true;
            this.Load += new System.EventHandler(this.VDExampleWindow_Load);
            this.ResumeLayout(false);

        }

        #endregion

        private System.Windows.Forms.Label label1;
        private System.Windows.Forms.Timer VDCheckTimer;

        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new VDExampleWindow());
        }
    }
    [ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("a5cd92ff-29be-454c-8d04-d82879fb3f1b")]
    [System.Security.SuppressUnmanagedCodeSecurity]
    public interface IVirtualDesktopManager
    {
        [PreserveSig]
        int IsWindowOnCurrentVirtualDesktop(
            [In] IntPtr TopLevelWindow,
            [Out] out int OnCurrentDesktop
            );
        [PreserveSig]
        int GetWindowDesktopId(
            [In] IntPtr TopLevelWindow,
            [Out] out Guid CurrentDesktop
            );

        [PreserveSig]
        int MoveWindowToDesktop(
            [In] IntPtr TopLevelWindow,
            [MarshalAs(UnmanagedType.LPStruct)]
            [In]Guid CurrentDesktop
            );
    }

    public class NewWindow : Form
    {
    }
    [ComImport, Guid("aa509086-5ca9-4c25-8f95-589d3c07b48a")]
    public class CVirtualDesktopManager
    {

    }
    public class VirtualDesktopManager
    {
        public VirtualDesktopManager()
        {
            cmanager = new CVirtualDesktopManager();
            manager = (IVirtualDesktopManager)cmanager;
        }
        ~VirtualDesktopManager()
        {
            manager = null;
            cmanager = null;
        }
        private CVirtualDesktopManager cmanager = null;
        private IVirtualDesktopManager manager;

        public bool IsWindowOnCurrentVirtualDesktop(IntPtr TopLevelWindow)
        {
            int result;
            int hr;
            if ((hr = manager.IsWindowOnCurrentVirtualDesktop(TopLevelWindow, out result)) != 0)
            {
                Marshal.ThrowExceptionForHR(hr);
            }
            return result != 0;
        }

        public Guid GetWindowDesktopId(IntPtr TopLevelWindow)
        {
            Guid result;
            int hr;
            if ((hr = manager.GetWindowDesktopId(TopLevelWindow, out result)) != 0)
            {
                Marshal.ThrowExceptionForHR(hr);
            }
            return result;
        }

        public void MoveWindowToDesktop(IntPtr TopLevelWindow, Guid CurrentDesktop)
        {
            int hr;
            if ((hr = manager.MoveWindowToDesktop(TopLevelWindow, CurrentDesktop)) != 0)
            {
                Marshal.ThrowExceptionForHR(hr);
            }
        }
    }
}

Follow us on Twitter, www.twitter.com/WindowsSDK.

VirtualDesktopSwitch.zip

Comments

  • Anonymous
    September 25, 2015
    Is there no way to get a collection of desktops to determine how many there are and get there id's? I need to save the layout of an application with multiple windows so a user can restore it. Now with multiple desktops I need to restore to the correct desktop. I can get the desktop id as you show above, and save it. But what if the user reboots then restores? How do I determine what the new desktop id's are so I can correctly lay out the windows?

  • Anonymous
    September 25, 2015
    Hi Kelly, There unfortunately is not an API to enumerate virtual desktops at this time.  However, you shouldn't be restoring your application to multiple virtual desktops (even if there are multiple desktops at the time your app could restore its layout, they're not necessarily going to have the same IDs as they did last time your application was running).  You should restore each window to the current virtual desktop.  

  • Anonymous
    September 27, 2015
    1.Is virtual desktop mentioned above  the same thing as desktops created by using API CreateDesktop? 2.And when  double-click mouse to open a folder, the desktop will switch to the other desktop that the folder has opened. Why?

  • Anonymous
    October 07, 2015
    Is the virtual desktop mentioned above the same as the desktop created by using API createdesktop?

  • Anonymous
    October 08, 2015
    No, those are desktop objects; see here for documentation on those: msdn.microsoft.com/.../ms682573(v=vs.85).aspx.  Typically, most users just have one desktop object, the default input desktop for the interactive window station, which is named winsta0default.

  • Anonymous
    October 08, 2015
    By opening a folder, do you mean with the Windows shell?  I am not able to reproduce that behavior.

  • Anonymous
    October 11, 2015
    Thanks for your answer. In fact, I have encountered a problem when I am using sysinternal tool Desktops.exe (technet.microsoft.com/.../cc817881) in windows 10. First, I create a new desktop object named Desktop1 by using Desktops.exe.So,there are two desktop objects,the default input desktop named winsta0default and the new created desktop Desktop1. Second, I double click on a folder called A on Desktop1, it opens normally. Third, I switchdesktop to the  winsta0default desktop and double-click on the folder A again, but it can't be opened anymore. In my opinion, the result may be caused by virtual desktop mentioned above, Do you kown why that is?