0

There are a number of questions and answers about setting wallpapers programmatically on multi-monitor setups in Windows, but I'm asking specifically for Windows 10 (and maybe Windows 8) because it seems to work differently from all the explanations I found.

Raymond Chen has an article "How do I put a different wallpaper on each monitor?" (https://devblogs.microsoft.com/oldnewthing/?p=25003), also quoted in Monitors position on Windows wallpaper. The core concepts is that Windows places the top-left corner of the provided bitmap at the top-left corner of the primary monitor, and wraps around to fill any desktop space to the left and/or above that. I understand that, I wrote a little program using that knowledge, and it works beautifully in Windows 7.

How it works: I create a bitmap that conceptually covers the whole desktop space, as the user sees it. I draw the contents of each monitor to that bitmap in its appropriate position (the program is written in C++ using VCL, but the principle remains the same in other programming environments):

TRect GetMonitorRect_WallpaperCoords(int MonitorNum)
{
  Forms::TMonitor *PrimaryMonitor = Screen->Monitors[0];
  Forms::TMonitor *Monitor = Screen->Monitors[MonitorNum];
  // Get the rectangle in desktop coordinates
  TRect Rect(Monitor->Left, Monitor->Top, Monitor->Left + Monitor->Width, Monitor->Top + Monitor->Height);
  // Convert to wallpaper coordinates
  Rect.Left += PrimaryMonitor->Left - Screen->DesktopLeft;
  Rect.Top += PrimaryMonitor->Top - Screen->DesktopTop;
  Rect.Right += PrimaryMonitor->Left - Screen->DesktopLeft;
  Rect.Bottom += PrimaryMonitor->Top - Screen->DesktopTop;
  return Rect;
}

std::unique_ptr<Graphics::TBitmap> CreateWallpaperBitmap_WallpaperCoords()
{
  std::unique_ptr<Graphics::TBitmap> Bmp(new Graphics::TBitmap);
  Bmp->PixelFormat = pf24bit;
  Bmp->Width = Screen->DesktopWidth;
  Bmp->Height = Screen->DesktopHeight;

  // Draw background (not that we really need it: it will never be visible)
  Bmp->Canvas->Brush->Style = bsSolid;
  Bmp->Canvas->Brush->Color = clBlack;
  Bmp->Canvas->FillRect(TRect(0, 0, Bmp->Width, Bmp->Height));

  for (int MonitorNum = 0; MonitorNum < Screen->MonitorCount; ++MonitorNum)
    {
    TDrawContext DC(Bmp->Canvas, GetMonitorRect_WallpaperCoords(MonitorNum));
    DrawMonitor(DC);
    }

  return Bmp;
}

(The draw context uses a coordinate translation rect so that the code int DrawMonitor function can draw in a rectangle like (0, 0, 1920, 1080) without having to wonder where in the full bitmap it is drawing, and with a clip rect so that DrawMonitor can not accidentally draw outside of the monitor it's drawing on).

Then I convert that bitmap to an image that will properly wrap around when placed at the top-left corner of the primary monitor (as Raymond Chen describes in his article):

std::unique_ptr<Graphics::TBitmap> ConvertWallpaperToDesktopCoords(std::unique_ptr<Graphics::TBitmap> &Bmp_WallpaperCoords)
{
  std::unique_ptr<Graphics::TBitmap> Bmp_DesktopCoords(new Graphics::TBitmap);
  Bmp_DesktopCoords->PixelFormat = Bmp_WallpaperCoords->PixelFormat;
  Bmp_DesktopCoords->Width = Bmp_WallpaperCoords->Width;
  Bmp_DesktopCoords->Height = Bmp_WallpaperCoords->Height;

  // Draw Bmp_WallpaperCoords to Bmp_DesktopCoords at four different places to account for all
  // possible ways Windows wraps the wallpaper around the left and bottom edges of the desktop
  // space
  Bmp_DesktopCoords->Canvas->Draw(Screen->DesktopLeft, Screen->DesktopTop, Bmp_WallpaperCoords.get());
  Bmp_DesktopCoords->Canvas->Draw(Screen->DesktopLeft + Screen->DesktopWidth, Screen->DesktopTop, Bmp_WallpaperCoords.get());
  Bmp_DesktopCoords->Canvas->Draw(Screen->DesktopLeft, Screen->DesktopTop + Screen->DesktopHeight, Bmp_WallpaperCoords.get());
  Bmp_DesktopCoords->Canvas->Draw(Screen->DesktopLeft + Screen->DesktopWidth, Screen->DesktopTop + Screen->DesktopHeight, Bmp_WallpaperCoords.get());

  return Bmp_DesktopCoords;
}

Then I install that bitmap as a wallpaper by writing the appropriate values in the registry and calling SystemParametersInfo with SPI_SETDESKWALLPAPER:

void InstallWallpaper(const String &Fn)
{
  // Install wallpaper:
  // There are 3 name/data pairs that have an effect on the desktop wallpaper, all under HKCU\Control Panel\Desktop:
  //  - Wallpaper (REG_SZ): file path and name of wallpaper
  //  - WallpaperStyle (REG_SZ):
  //    . 0: Centered
  //    . 1: Tiled
  //    . 2: Stretched
  //  - TileWallpaper (REG_SZ):
  //    . 0: Don't tile
  //    . 1: Tile
  //  We don't use the Wallpaper value itself; instead we use SystemParametersInfo to set the wallpaper.

  // The file name needs to be absolute!
  assert(Ioutils::TPath::IsPathRooted(Fn));

  std::unique_ptr<TRegistry> Reg(new TRegistry);
  Reg->RootKey = HKEY_CURRENT_USER;
  if (Reg->OpenKey(L"Control Panel\\Desktop", false))
    {
    Reg->WriteString(L"WallpaperStyle", L"1");
    Reg->WriteString(L"TileWallpaper", L"1");
    Reg->CloseKey();
    }
  SystemParametersInfoW(SPI_SETDESKWALLPAPER, 1, Fn.c_str(), SPIF_UPDATEINIFILE | SPIF_SENDCHANGE);
}

But when I test it in Windows 10, it doesn't work properly anymore: Windows 10 puts the wallpaper completely in the wrong place. Seeing as other people have asked questions about multi-monitor wallpapers in the past, I'm hoping there are people with experience of it on Windows 10.

auburg
  • 1,373
  • 2
  • 12
  • 22
Roel Schroeven
  • 1,778
  • 11
  • 12

1 Answers1

0

As far as I can see, Windows 10 places the top-left corner of the provided bitmap at the top-left corner of the desktop space (by which I mean the bounding rectangle of all monitors), instead of the top-left corner of the primary monitor. In code, that means: I leave out the ConvertWallpaperToDesktopCoords step, and then it works fine as far as I can see.

But I can't find any documentation on this, so I don't know if this is officially explanation of how Windows 10 does it. Use with care. Also I don't know when this different behavior started: in Windows 10, or maybe earlier in Windows 8.

Roel Schroeven
  • 1,778
  • 11
  • 12