Slicedbread Slicedbread - 1 month ago 27
C# Question

Altering Win10 virtual desktop behavior

I love that windows now has virtual desktops built in, but I have some features that I'd like to add/modify. (force a window to appear on all desktops, launch the task view with a hotkey, have per-monitor desktops, etc)

I have searched for applications and developer references to help me customize my desktops but have had no luck.

Does have any idea where I should start?

Answer

Programmatic access to the virtual desktop feature is very limited, Microsoft has only exposed the IVirtualDesktopManager COM interface. 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 any 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, this code does not necessarily interact well when the user is tinkering with the virtual desktops as well. Always keep in mind that a virtual desktop can appear and disappear at any time, completely out of sync with your 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.