Programmatic control of virtual desktops in Windows 10

Solution 1:

The Windows SDK Support Team Blog posted a C# demo to switch Desktops via IVirtualDesktopManager:

[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
    );
}

[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);
        }
    }
}

it includes the API to detect on which desktop the Window is shown and it can switch and move a Windows the a Desktop.

Solution 2:

Programmatic access to the virtual desktop feature is very limited, as Microsoft has only exposed the IVirtualDesktopManager COM interface. It does provide two key functions:

  • IVirtualDesktopManager::GetWindowDesktopId allows you to retrieve the ID of a virtual desktop, based on a window that is already assigned to that desktop.

  • IVirtualDesktopManager::MoveWindowToDesktop allows you to move a window to a specific virtual desktop.

Unfortunately, this is not nearly enough to accomplish anything useful. I've written some C# code based on the reverse-engineering work done by NickoTin. I can't read much of the Russian in his blog post, but his C++ code was pretty accurate.

I do need to emphasize that this code is not something you want to commit to in a product. Microsoft always feels free to change undocumented APIs whenever they feel like it. And there is a runtime risk as well: this code does not necessarily interact well when the user is tinkering with the virtual desktops. Always keep in mind that a virtual desktop can appear and disappear at any time, completely out of sync with your code.

To use the code, create a new C# class library project. I'll first post ComInterop.cs, it contains the COM interface declarations that match NickoTin's C++ declarations:

using System;
using System.Runtime.InteropServices;

namespace Windows10Interop {
    internal static class Guids {
        public static readonly Guid CLSID_ImmersiveShell = 
            new Guid(0xC2F03A33, 0x21F5, 0x47FA, 0xB4, 0xBB, 0x15, 0x63, 0x62, 0xA2, 0xF2, 0x39);
        public static readonly Guid CLSID_VirtualDesktopManagerInternal = 
            new Guid(0xC5E0CDCA, 0x7B6E, 0x41B2, 0x9F, 0xC4, 0xD9, 0x39, 0x75, 0xCC, 0x46, 0x7B);
        public static readonly Guid CLSID_VirtualDesktopManager = 
            new Guid("AA509086-5CA9-4C25-8F95-589D3C07B48A");
        public static readonly Guid IID_IVirtualDesktopManagerInternal = 
            new Guid("AF8DA486-95BB-4460-B3B7-6E7A6B2962B5");
        public static readonly Guid IID_IVirtualDesktop = 
            new Guid("FF72FFDD-BE7E-43FC-9C03-AD81681E88E4");
    }

    [ComImport]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    [Guid("FF72FFDD-BE7E-43FC-9C03-AD81681E88E4")]
    internal interface IVirtualDesktop {
        void notimpl1(); // void IsViewVisible(IApplicationView view, out int visible);
        Guid GetId();
    }

    [ComImport]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    [Guid("AF8DA486-95BB-4460-B3B7-6E7A6B2962B5")]
    internal interface IVirtualDesktopManagerInternal {
        int GetCount();
        void notimpl1();  // void MoveViewToDesktop(IApplicationView view, IVirtualDesktop desktop);
        void notimpl2();  // void CanViewMoveDesktops(IApplicationView view, out int itcan);
        IVirtualDesktop GetCurrentDesktop();
        void GetDesktops(out IObjectArray desktops);
        [PreserveSig]
        int GetAdjacentDesktop(IVirtualDesktop from, int direction, out IVirtualDesktop desktop);
        void SwitchDesktop(IVirtualDesktop desktop);
        IVirtualDesktop CreateDesktop();
        void RemoveDesktop(IVirtualDesktop desktop, IVirtualDesktop fallback);
        IVirtualDesktop FindDesktop(ref Guid desktopid);
    }

    [ComImport]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    [Guid("a5cd92ff-29be-454c-8d04-d82879fb3f1b")]
    internal interface IVirtualDesktopManager {
        int IsWindowOnCurrentVirtualDesktop(IntPtr topLevelWindow);
        Guid GetWindowDesktopId(IntPtr topLevelWindow);
        void MoveWindowToDesktop(IntPtr topLevelWindow, ref Guid desktopId);
    }

    [ComImport]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    [Guid("92CA9DCD-5622-4bba-A805-5E9F541BD8C9")]
    internal interface IObjectArray {
        void GetCount(out int count);
        void GetAt(int index, ref Guid iid, [MarshalAs(UnmanagedType.Interface)]out object obj);
    }

    [ComImport]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    [Guid("6D5140C1-7436-11CE-8034-00AA006009FA")]
    internal interface IServiceProvider10 {
        [return: MarshalAs(UnmanagedType.IUnknown)]
        object QueryService(ref Guid service, ref Guid riid);
    }

}

Next is Desktop.cs. It contains the friendly C# classes that you can use in your code:

using System;
using System.Runtime.InteropServices;

namespace Windows10Interop
{
    public class Desktop {
        public static int Count {
            // Returns the number of desktops
            get { return DesktopManager.Manager.GetCount(); }
        }

        public static Desktop Current {
            // Returns current desktop
            get { return new Desktop(DesktopManager.Manager.GetCurrentDesktop()); }
        }

        public static Desktop FromIndex(int index) {
            // Create desktop object from index 0..Count-1
            return new Desktop(DesktopManager.GetDesktop(index));
        }

        public static Desktop FromWindow(IntPtr hWnd) {
            // Creates desktop object on which window <hWnd> is displayed
            Guid id = DesktopManager.WManager.GetWindowDesktopId(hWnd);
            return new Desktop(DesktopManager.Manager.FindDesktop(ref id));
        }

        public static Desktop Create() {
            // Create a new desktop
            return new Desktop(DesktopManager.Manager.CreateDesktop());
        }

        public void Remove(Desktop fallback = null) {
            // Destroy desktop and switch to <fallback>
            var back = fallback == null ? DesktopManager.GetDesktop(0) : fallback.itf;
            DesktopManager.Manager.RemoveDesktop(itf, back);
        }

        public bool IsVisible {
            // Returns <true> if this desktop is the current displayed one
            get { return object.ReferenceEquals(itf, DesktopManager.Manager.GetCurrentDesktop()); }
        }

        public void MakeVisible() {
            // Make this desktop visible
            DesktopManager.Manager.SwitchDesktop(itf);
        }

        public Desktop Left {
            // Returns desktop at the left of this one, null if none
            get {
                IVirtualDesktop desktop;
                int hr = DesktopManager.Manager.GetAdjacentDesktop(itf, 3, out desktop);
                if (hr == 0) return new Desktop(desktop);
                else return null;

            }
        }

        public Desktop Right {
            // Returns desktop at the right of this one, null if none
            get {
                IVirtualDesktop desktop;
                int hr = DesktopManager.Manager.GetAdjacentDesktop(itf, 4, out desktop);
                if (hr == 0) return new Desktop(desktop);
                else return null;
            }
        }

        public void MoveWindow(IntPtr handle) {
            // Move window <handle> to this desktop
            DesktopManager.WManager.MoveWindowToDesktop(handle, itf.GetId());
        }

        public bool HasWindow(IntPtr handle) {
            // Returns true if window <handle> is on this desktop
            return itf.GetId() == DesktopManager.WManager.GetWindowDesktopId(handle);
        }

        public override int GetHashCode() {
            return itf.GetHashCode();
        }
        public override bool Equals(object obj) {
            var desk = obj as Desktop;
            return desk != null && object.ReferenceEquals(this.itf, desk.itf);
        }

        private IVirtualDesktop itf;
        private Desktop(IVirtualDesktop itf) { this.itf = itf; }
    }

    internal static class DesktopManager {
        static DesktopManager() {
            var shell = (IServiceProvider10)Activator.CreateInstance(Type.GetTypeFromCLSID(Guids.CLSID_ImmersiveShell));
            Manager = (IVirtualDesktopManagerInternal)shell.QueryService(Guids.CLSID_VirtualDesktopManagerInternal, Guids.IID_IVirtualDesktopManagerInternal);
            WManager = (IVirtualDesktopManager)Activator.CreateInstance(Type.GetTypeFromCLSID(Guids.CLSID_VirtualDesktopManager));
        }

        internal static IVirtualDesktop GetDesktop(int index) {
            int count = Manager.GetCount();
            if (index < 0 || index >= count) throw new ArgumentOutOfRangeException("index");
            IObjectArray desktops;
            Manager.GetDesktops(out desktops);
            object objdesk;
            desktops.GetAt(index, Guids.IID_IVirtualDesktop, out objdesk);
            Marshal.ReleaseComObject(desktops);
            return (IVirtualDesktop)objdesk;
        }

        internal static IVirtualDesktopManagerInternal Manager;
        internal static IVirtualDesktopManager WManager;
    }
}

And finally a little test WinForms project that I used to test the code. Just drop 4 buttons on a form and name them buttonLeft/Right/Create/Destroy:

using Windows10Interop;
using System.Diagnostics;
...
    public partial class Form1 : Form {
        public Form1() {
            InitializeComponent();
        }

        private void buttonRight_Click(object sender, EventArgs e) {
            var curr = Desktop.FromWindow(this.Handle);
            Debug.Assert(curr.Equals(Desktop.Current));
            var right = curr.Right;
            if (right == null) right = Desktop.FromIndex(0);
            if (right != null) {
                right.MoveWindow(this.Handle);
                right.MakeVisible();
                this.BringToFront();
                Debug.Assert(right.IsVisible);
            }
        }

        private void buttonLeft_Click(object sender, EventArgs e) {
            var curr = Desktop.FromWindow(this.Handle);
            Debug.Assert(curr.Equals(Desktop.Current));
            var left = curr.Left;
            if (left == null) left = Desktop.FromIndex(Desktop.Count - 1);
            if (left != null) {
                left.MoveWindow(this.Handle);
                left.MakeVisible();
                this.BringToFront();
                Debug.Assert(left.IsVisible);
            } 
        }

        private void buttonCreate_Click(object sender, EventArgs e) {
            var desk = Desktop.Create();
            desk.MoveWindow(this.Handle);
            desk.MakeVisible();
            Debug.Assert(desk.IsVisible);
            Debug.Assert(desk.Equals(Desktop.Current));
        }

        private void buttonDestroy_Click(object sender, EventArgs e) {
            var curr = Desktop.FromWindow(this.Handle);
            var next = curr.Left;
            if (next == null) next = curr.Right;
            if (next != null && next != curr) {
                next.MoveWindow(this.Handle);
                curr.Remove(next);
                Debug.Assert(next.IsVisible);
            }
        }
    }

The only real quirk I noticed while testing this is that moving a window from one desktop to another can move it to the bottom of the Z-order when you first switch the desktop, then move the window. No such problem if you do it the other way around.