IDesktopWallpaperの使い方

## はじめに

初めての投稿です。

IDesktopWallpaper は Windows で壁紙を設定する API です。
機能的にはシンプルなものですが、C# から使おうとしたところ思いのほか情報が少なく面倒な箇所もあったため、その際に調べた情報を共有します。

:::note warn
インタフェース定義についてコメントをいただきました。
現在は、COM 含め Win32API のネイティブの関数やインタフェース定義は自分で用意しなくても CsWin32 というライブラリを使えば必要な定義を必要な分だけ生成してくれるようです。CsWin32 は Microsoft製で nuget から導入できます。
なので、インタフェース定義は下記で説明しているような自分で定義を書いたりネットで拾ったりするのではなく CsWin32 を使うのが今後の主流となりそうです。
ただ、CsWin32 は現状β版であること、文字列などの受け渡しの方法が独特で呼び出し元のコードの書き換えが必要になるなど、いくつかの課題もあり、移行コストもかかるようです。
CsWin32を使用する場合、以下で説明するコード自体も修正が必要になるので注意してください。
:::

## インタフェース定義

API の仕様自体は [IDesktopWallpaper インターフェイス | Microsoft Learn](https://learn.microsoft.com/ja-jp/windows/win32/api/shobjidl_core/nn-shobjidl_core-idesktopwallpaper) に説明があります。

しかし、C# から使用する場合には C# 用のインタフェース定義を自分で用意しなければなりません。
インタフェース定義は公式情報としては存在しないのでネットで検索して入手します。

```DesktopWallpaper.cs
using System;
using System.Runtime.InteropServices;

namespace DesktopWallpaper
{
    public enum DesktopSlideshowOptions
    {
        ShuffleImages = 0x01,
    }
    public enum DesktopSlideshowState
    {
        Enabled = 0x01,
        Slideshow = 0x02,
        DisabledByRemoteSession = 0x04,
    }
    public enum DesktopSlideshowDirection
    {
        Forward = 0,
        Backward = 1,
    }
    public enum DesktopWallpaperPosition
    {
        Center = 0,
        Tile = 1,
        Stretch = 2,
        Fit = 3,
        Fill = 4,
        Span = 5,
    }

    [ComImport, Guid("B92B56A9-8B55-4E14-9A89-0199BBB6F93B"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    [CoClass(typeof(DesktopWallpaperClass))]
    public interface IDesktopWallpaper
    {
        void SetWallpaper([MarshalAs(UnmanagedType.LPWStr)] string monitorID, [MarshalAs(UnmanagedType.LPWStr)] string wallpaper);
        [return: MarshalAs(UnmanagedType.LPWStr)]
        string GetWallpaper([MarshalAs(UnmanagedType.LPWStr)] string monitorID);

        [return: MarshalAs(UnmanagedType.LPWStr)]
        string GetMonitorDevicePathAt(uint monitorIndex);
        [return: MarshalAs(UnmanagedType.U4)]
        uint GetMonitorDevicePathCount();

        [return: MarshalAs(UnmanagedType.Struct)]
        Win32API.Rect GetMonitorRECT([MarshalAs(UnmanagedType.LPWStr)] string monitorID);

        void SetBackgroundColor([MarshalAs(UnmanagedType.U4)] uint color);
        [return: MarshalAs(UnmanagedType.U4)]
        uint GetBackgroundColor();

        void SetPosition([MarshalAs(UnmanagedType.I4)] DesktopWallpaperPosition position);
        [return: MarshalAs(UnmanagedType.I4)]
        DesktopWallpaperPosition GetPosition();

        void SetSlideshow([MarshalAs(UnmanagedType.Interface)] Win32API.IShellItemArray ppsiItemArray);
        [return: MarshalAs(UnmanagedType.Interface)]
        Win32API.IShellItemArray GetSlideshow();

        void SetSlideshowOptions(DesktopSlideshowOptions options, uint slideshowTick);
        void GetSlideshowOptions(out DesktopSlideshowOptions options, out uint slideshowTick);

        void AdvanceSlideshow([MarshalAs(UnmanagedType.LPWStr)] string monitorID, [MarshalAs(UnmanagedType.I4)] DesktopSlideshowDirection direction);

        [return: MarshalAs(UnmanagedType.U4)]
        DesktopSlideshowState GetStatus();

        void Enable([MarshalAs(UnmanagedType.Bool)]bool enable);
    }

    [ComImport, Guid("C2CF3110-460E-4fc1-B9D0-8A1C0C9CC4BD")]
    public class DesktopWallpaperClass
    {
    }
}
```

加えて IShellItemArray など、関連する定義も必要です。

```Win32API.cs
using System;
using System.Text;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;

public class Win32API
{
    [StructLayout(LayoutKind.Sequential)]
    public struct Rect
    {
        public int Left;
        public int Top;
        public int Right;
        public int Bottom;
    }

    [DllImport("shell32.dll")]
    public static extern int SHGetDesktopFolder(
        ref IShellFolder ppshf);

    [DllImport("shell32.dll")]
    public static extern Int32 SHCreateShellItemArrayFromIDLists(
        uint cidl,
        IntPtr rgpidl,
        out IShellItemArray ppsiItemArray);

    [StructLayout(LayoutKind.Explicit, Size=520)]
    public struct STRRETinternal
    {
        [FieldOffset(0)]
        public IntPtr pOleStr;

        [FieldOffset(0)]
        public IntPtr pStr;  // LPSTR pStr;   NOT USED

        [FieldOffset(0)]
        public uint  uOffset;
    }
    [StructLayout(LayoutKind.Sequential )]
    public struct STRRET
    {
        public uint uType;
        public STRRETinternal data;
    }

    public enum ESFGAO : uint
    {
        SFGAO_CANCOPY = 0x00000001,
        SFGAO_CANMOVE = 0x00000002,
        SFGAO_CANLINK = 0x00000004,
        SFGAO_LINK = 0x00010000,
        SFGAO_SHARE = 0x00020000,
        SFGAO_READONLY = 0x00040000,
        SFGAO_HIDDEN = 0x00080000,
        SFGAO_FOLDER = 0x20000000,
        SFGAO_FILESYSTEM = 0x40000000,
        SFGAO_HASSUBFOLDER = 0x80000000,
    }
    public enum ESHCONTF
    {
        SHCONTF_FOLDERS = 0x0020,
        SHCONTF_NONFOLDERS = 0x0040,
        SHCONTF_INCLUDEHIDDEN = 0x0080,
        SHCONTF_INIT_ON_FIRST_NEXT = 0x0100,
        SHCONTF_NETPRINTERSRCH = 0x0200,
        SHCONTF_SHAREABLE = 0x0400,
        SHCONTF_STORAGE = 0x0800
    }
    public enum ESHGDN
    {
        SHGDN_NORMAL = 0x0000,
        SHGDN_INFOLDER = 0x0001,
        SHGDN_FOREDITING = 0x1000,
        SHGDN_FORADDRESSBAR = 0x4000,
        SHGDN_FORPARSING = 0x8000,
    }

    [ComImport]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    [Guid("000214E6-0000-0000-C000-000000000046")]
    public interface IShellFolder
    {
        void ParseDisplayName(IntPtr hwnd, IntPtr pbc, String pszDisplayName, IntPtr pchEaten, out IntPtr ppidl, IntPtr pdwAttributes);
        void EnumObjects(IntPtr hwnd, ESHCONTF grfFlags, out IntPtr ppenumIDList);
        void BindToObject(IntPtr pidl, IntPtr pbc, [In]ref Guid riid, out IntPtr ppv);
        void BindToStorage(IntPtr pidl, IntPtr pbc, [In]ref Guid riid, out IntPtr ppv);
        [PreserveSig]
        Int32 CompareIDs(Int32 lParam, IntPtr pidl1, IntPtr pidl2);
        void CreateViewObject(IntPtr hwndOwner, [In] ref Guid riid, out IntPtr ppv);
        void GetAttributesOf(UInt32 cidl, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)]IntPtr[] apidl, ref ESFGAO rgfInOut);
        void GetUIObjectOf(IntPtr hwndOwner, UInt32 cidl, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)]IntPtr[] apidl, [In] ref Guid riid, UInt32 rgfReserved, out IntPtr ppv);
        void GetDisplayNameOf(IntPtr pidl, ESHGDN uFlags, out STRRET pName);
        void SetNameOf(IntPtr hwnd, IntPtr pidl, String pszName, ESHCONTF uFlags, out IntPtr ppidlOut);
    }

    public enum SIGDN : uint {
         NORMALDISPLAY = 0,
         PARENTRELATIVEPARSING = 0x80018001,
         PARENTRELATIVEFORADDRESSBAR = 0x8001c001,
         DESKTOPABSOLUTEPARSING = 0x80028000,
         PARENTRELATIVEEDITING = 0x80031001,
         DESKTOPABSOLUTEEDITING = 0x8004c000,
         FILESYSPATH = 0x80058000,
         URL = 0x80068000,
         PARENTRELATIVE = 0x80080001,
         PARENTRELATIVEFORUI = 0x80094001
    }
    public enum SIATTRIBFLAGS
    {
        SIATTRIBFLAGS_AND = 1,
        SIATTRIBFLAGS_APPCOMPAT = 3,
        SIATTRIBFLAGS_OR = 2
    }
    [StructLayout ( LayoutKind.Sequential, Pack = 4 )]
    public struct PROPERTYKEY
    {
        public Guid fmtid;
        public uint pid;
    }

    [ComImport]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    [Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe")]
    public interface IShellItem {
        void BindToHandler(IntPtr pbc,
            [MarshalAs(UnmanagedType.LPStruct)]Guid bhid,
            [MarshalAs(UnmanagedType.LPStruct)]Guid riid,
            out IntPtr ppv);
        void GetParent(out IShellItem ppsi);
        void GetDisplayName(SIGDN sigdnName, out IntPtr ppszName);
        void GetAttributes(uint sfgaoMask, out uint psfgaoAttribs);
        void Compare(IShellItem psi, uint hint, out int piOrder);
    }

    [ComImport, Guid ( "B63EA76D-1F85-456F-A19C-48159EFA858B" ), InterfaceType ( ComInterfaceType.InterfaceIsIUnknown )]
    public interface IShellItemArray
    {
        // Not supported: IBindCtx
        [MethodImpl ( MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime )]
        void BindToHandler ( [In, MarshalAs ( UnmanagedType.Interface )] IntPtr pbc, [In] ref Guid rbhid,
                     [In] ref Guid riid, out IntPtr ppvOut );
        [MethodImpl ( MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime )]
        void GetPropertyStore ( [In] int Flags, [In] ref Guid riid, out IntPtr ppv );
        [MethodImpl ( MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime )]
        void GetPropertyDescriptionList ( [In] ref PROPERTYKEY keyType, [In] ref Guid riid, out IntPtr ppv );
        [MethodImpl ( MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime )]
        void GetAttributes ( [In] SIATTRIBFLAGS dwAttribFlags, [In] uint sfgaoMask, out uint psfgaoAttribs );
        [MethodImpl ( MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime )]
        void GetCount ( out uint pdwNumItems );
        [MethodImpl ( MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime )]
        void GetItemAt ( [In] uint dwIndex, [MarshalAs ( UnmanagedType.Interface )] out IShellItem ppsi );
        // Not supported: IEnumShellItems (will use GetCount and GetItemAt instead)
        [MethodImpl ( MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime )]
        void EnumItems ( [MarshalAs ( UnmanagedType.Interface )] out IntPtr ppenumShellItems );
    }

    public const Int32 S_OK = 0;
    public const Int32 S_FALSE = 1;
    public static bool SUCCEEDED(Int32 hr)
    {
        return hr >= 0;
    }
    public static bool FAILED(Int32 hr)
    {
        return hr < 0;
    }
}
```

## 基本的な使い方

IDesktopWallpaper は COM という技術が実装のベースになっています。
インスタンスの生成や解放などは、COM の作法に従います。

実際に壁紙を変更してみます。

```cs
// IDesktopWallpaperのインスタンスを生成します。
IDesktopWallpaper pDesktopWallpaper = new IDesktopWallpaper();

// 壁紙を設定します。
string imagePath = @"C:\image\wp01.jpg";
pDesktopWallpaper.SetWallpaper(null, imagePath);

// インスタンスを解放します。
Marshal.ReleaseComObject(pDesktopWallpaper);
```

エラー処理などを加えて関数の形にすると以下のようになります。

```cs
void ChangeWallpaper(string imagePath)
{
    IDesktopWallpaper pDesktopWallpaper = null;
    try {
        pDesktopWallpaper = new IDesktopWallpaper();
        pDesktopWallpaper.SetWallpaper(null, imagePath);
    }
    catch (Exception ex) {
        Console.WriteLine(ex.Message);
    }
    finally {
        if (pDesktopWallpaper != null) {
            Marshal.ReleaseComObject(pDesktopWallpaper);
        }
    }
}
```

## スライドショーの設定

スライドショーの設定について説明します。

OS のスライドショーの設定画面では、フォルダを1つしか選択できませんが、
API を使用する場合、フォルダを指定する他に、画像ファイルを直接指定することができます。
いずれの場合も一定の条件があります。

>フォルダ指定:　　　画像ファイルが格納されたフォルダを1つだけ指定可能
> 画像ファイル指定:　複数の画像ファイルを指定可能、ただし、同一フォルダ内の画像に限る

静的イメージの壁紙設定(SetWallpaper())は画像の絶対パス名を文字列のままパラメタにできます。
スライドショーの設定(SetSlideshow())では、ファイル名を文字列として渡すのではなく、
複数のファイル名を IShellItemArray という形式に変換して、それをパラメタとするため割と面倒です。
なお、スライドショーの設定ではフォルダやファイルの指定と同時にスライドショーが有効化されます。

まず、パス名の配列を IShellItemArray の形式に変換するための関数を作成します。
以下のような流れとなります。

  1. パス名を ItemIDList に変換
  2. ItemIDList を複数束ねて ItemIDList の配列作成
  3. ItemIDList の配列から IShellItemArray を作成

これを PathsToShellItemArray() の名前の関数として作成します。
1つのフォルダのみを扱うなら簡単ですが、ここでは任意個のファイルを指定可能とします。

```cs
Win32API.IShellItemArray PathsToShellItemArray(List<string> paths)
{
    Win32API.IShellItemArray pShellItemArray = null;
    Win32API.IShellFolder pDesktopFolder = null;
    IntPtr ptr = IntPtr.Zero;
    int ptrSize = Marshal.SizeOf(typeof(IntPtr));
    try
    {
        Win32API.SHGetDesktopFolder(ref pDesktopFolder);
        ptr = Marshal.AllocCoTaskMem(ptrSize * paths.Count);
        for (int i = 0; i < paths.Count; i++) {
            Marshal.WriteIntPtr(ptr, ptrSize * i, IntPtr.Zero);
        }
        for (int i = 0; i < paths.Count; i++) {
            IntPtr pidl;
            pDesktopFolder.ParseDisplayName(IntPtr.Zero, IntPtr.Zero, Path.GetFullPath(paths[i]), IntPtr.Zero, out pidl, IntPtr.Zero);
            Marshal.WriteIntPtr(ptr, ptrSize * i, pidl);
        }
        Int32 hr = Win32API.SHCreateShellItemArrayFromIDLists((uint)paths.Count, ptr, out pShellItemArray);
        if (Win32API.FAILED(hr)) {
            Console.WriteLine(Marshal.GetExceptionForHR(hr).Message);
            return null;
        }
        return pShellItemArray;
    }
    catch (Exception ex) {
        Console.WriteLine(ex.Message);
    }
    finally {
        if (ptr != IntPtr.Zero) {
            for (int i = 0; i < paths.Count; i++) {
                IntPtr p = Marshal.ReadIntPtr(ptr, ptrSize * i);
                if (p != IntPtr.Zero) {
                    Marshal.FreeCoTaskMem(p);
                }
            }
            Marshal.FreeCoTaskMem(ptr);
        }
        if (pDesktopFolder != null) {
            Marshal.ReleaseComObject(pDesktopFolder);
        }
    }
    return null;
}
```

上記を使用してスライドショー設定の関数を実装します。

```cs
void SetSlideshow(List<string> paths)
{
    IDesktopWallpaper pDesktopWallpaper = null;
    Win32API.IShellItemArray pShellItemArray = null;

    try {
        pDesktopWallpaper = new IDesktopWallpaper();
        pShellItemArray = PathsToShellItemArray(paths);
        if (pShellItemArray == null) {
            return;
        }

        pDesktopWallpaper.SetSlideshow(pShellItemArray);
    }
    catch (Exception e) {
        Console.WriteLine(e.Message);
    }
    finally {
        if (pShellItemArray != null) {
            Marshal.ReleaseComObject(pShellItemArray);
        }
        if (pDesktopWallpaper != null) {
            Marshal.ReleaseComObject(pDesktopWallpaper);
        }
    }
}
```

## スライドショーの設定パス名を取得

パス名の取得は GetSlideshow() メソッドを使用します。
SetSlideshow() 同様、このメソッド自体は簡単ですが、IShellItemArray として返されるパス名を解析するのが少々面倒です。

まず、IShellItemArray からパス名の文字列を取得する関数を作成します。

```cs
List<string> ShellItemArrayToPaths(Win32API.IShellItemArray pShellItemArray)
{
    List<string> paths = new List<string>();
    uint cnt = 0;
    pShellItemArray.GetCount(out cnt);
    if (cnt == 0) {
        Console.WriteLine("(nothing)");
        return paths;
    }
    for (int i = 0; i < (int)cnt; i++) {
        Win32API.IShellItem pShellItem = null;
        IntPtr nameptr = IntPtr.Zero;
        try {
            pShellItemArray.GetItemAt((uint)i, out pShellItem);
            pShellItem.GetDisplayName(Win32API.SIGDN.FILESYSPATH, out nameptr);
            string path = Marshal.PtrToStringUni(nameptr);
            paths.Add(path);
        }
        catch (Exception ex) {
            Console.WriteLine(ex.Message);
            return null;
        }
        finally {
            if (pShellItem != null) {
                Marshal.ReleaseComObject(pShellItem);
            }
            if (nameptr != IntPtr.Zero) {
                Marshal.FreeCoTaskMem(nameptr);
            }
        }
    }
    return paths;
}
```

上記を使用してスライドショーの設定パス名を表示する関数を実装します。

```cs
void ShowSlideshow()
{
    IDesktopWallpaper pDesktopWallpaper = null;
    Win32API.IShellItemArray pShellItemArray = null;
    try {
        pDesktopWallpaper = new IDesktopWallpaper();
        DesktopSlideshowState ssstate = pDesktopWallpaper.GetStatus();
        if ((ssstate & DesktopSlideshowState.Slideshow) != 0) {
            pShellItemArray = pDesktopWallpaper.GetSlideshow();
            List<string> paths = ShellItemArrayToPaths(pShellItemArray);
            if (paths != null) {
                foreach (string path in paths) {
                    Console.WriteLine(path);
                }
            }
        }
    }
    catch (Exception e) {
        Console.WriteLine(e.Message);
    }
    finally {
        if (pShellItemArray != null) {
            Marshal.ReleaseComObject(pShellItemArray);
        }
        if (pDesktopWallpaper != null) {
            Marshal.ReleaseComObject(pDesktopWallpaper);
        }
    }
}
```

## 現在の動作モードについて

OS の壁紙設定画面では、「背景」として3つの動作モード(画像/単色/スライドショー)を指定できます。

APIではモードを選択するのではなく、画像やフォルダを指定することで自動的にモードが変わります。

|モード|説明|
|----|----|
|画像|SetWallpaper()で画像を指定することで画像モードに変更される|
|スライドショー|SetSlideshow()でフォルダ(または任意個の画像)を指定することでスライドショーモードに変更される|
|単色|Enable()でパラメタ false を指定し、画像を無効にすることで単色モードに変更される|

しかし、現在のモードを取得する明示的なAPIが見当たりません。
調べてみると、GetStatus() の公式ドキュメントの説明が怪しそうです。

[IDesktopWallpaper::GetStatus メソッド | Microsoft Lean](https://learn.microsoft.com/ja-jp/windows/win32/api/shobjidl_core/nf-shobjidl_core-idesktopwallpaper-getstatus)

>パラメーター
>[out] state
>
>このメソッドが正常に返されたときに、次のフラグの 1 つ以上を受け取るDESKTOP_SLIDESHOW_STATE値へのポインター。
>
>DSS_ENABLED (0x01)
>スライドショーが有効になっています。
>
>DSS_SLIDESHOW (0x02)
>スライドショーは現在構成されています。

公式では戻り値の説明が上記のように記載されています。
しかし、名称的に GetStatus() メソッドの DSS_ENABLED フラグが、何故スライドショーの有効/無効なのかよくわかりませんし、「スライドショーが有効になっています。」と「スライドショーは現在構成されています。」の区別も分かりにくいです。

Enable() はデスクトップの背景を有効または無効にするメソッドですが、DSS_ENABLED フラグが Enable() メソッドと対になり、デスクトップの背景を有効かを識別するもの、と仮定すると個人的にしっくりきます。
そうであれば、現在のモードは以下のように判定できそうです。

```cs
DesktopSlideshowState state = pDesktopWallpaper.GetStatus();
if ((state & DesktopSlideshowState.Slideshow) != 0) {
    // スライドショー
}
else if ((state & DesktopSlideshowState.Enabled) != 0) {
    // 画像
}
else  {
    // 単色
}
```

## 注意点など

SetWallpaper()やSetSlideshowなど、ファイル名/フォルダ名をパラメタとするメソッドは必ず絶対パスを指定します。

スライドショーはフォルダまたは画像ファイルを指定できますが、それぞれ制限があります。

>フォルダ指定:　　　画像ファイルが格納されたフォルダを1つだけ指定可能
>画像ファイル指定:　複数の画像ファイルを指定可能、ただし、同一フォルダ内の画像に限る

この制限を外れると正常に動作しませんが、API 自体はエラーにならないので注意が必要です。

AdvanceSlideshow() はスライドショーモード時に即時画像切り替えを行うメソッドです。
第一パラメタに対象モニタを指定できることになっていますが未実装であり、現状は null のみ指定できます。

AdvanceSlideshow() メソッドはスライドショーモード時のみ使用可能です。
スライドショー以外のモードで呼ぶとエラーになります。

IDesktopWallpaper での画像切り替えは非常に重い処理で10秒程度かかります。
SetSlideshowOptions で、スライドショーの切り替え間隔をミリ秒単位で指定できますが、
15秒以下に設定するのは止めた方がよいでしょう。