星期二, 十月 02, 2007

MultiWall - Wallpaper Tool for Multiple Monitors



 
 

Sent to you by Hudong via Google Reader:

 
 

via MSDN Blogs by Coding4Fun on 10/2/07

Multiple monitors Are you more productive with more monitors?  Would you be even more productive with better wallpaper management?  I thought so!  In this article, see how to create a utility to manage your Windows desktop wallpaper with separate images per screen.  It's addicting!
Arian Kulp

Difficulty: Intermediate
Time Required: 1-3 hours
Cost: None
Hardware: At least one monitor (more is better!)
Download:

Introduction

Everyone enjoys customizing his or her computer to the extent possible, and this almost always includes the desktop wallpaper.  Windows XP manages wallpaper resizing pretty transparently, increasing or reducing the image display size, or tiling it as specified.  Unfortunately, this isn't quite as smooth if you have more than one monitor.  After some tinkering around with display settings and different bitmaps, I threw together a utility to resize images easily so you can customize what shows on each display.

Before reading further, you may want to download the source code in either Visual Basic or C# from the links above the "Introduction" heading.  The code is identical in either language, so choose the one with which you're more comfortable.  If you haven't yet, download the appropriate version of Visual Studio 2005 Express Edition and let's begin!  You might also want to read through earlier articles that I've written about desktop wallpaper (here and here).

Understanding Wallpaper

Most people are familiar with setting wallpaper through the system's Display Properties control panel applet.  You browse to an image, select how the image should be sized to fit the screen, and it does its magic.  If you have two monitors though, you'll have noticed that it just duplicates the image on each monitor when in Stretch or Center mode, and repeats it unattractively across monitor boundaries in Tile mode.  This isn't generally the intent when selecting an image!

image

Image 1: Windows XP Display Properties - Desktop tab

I just want to be able to select a different image for each screen.  In figuring that out though, I learned that the Tile mode can be pretty useful if you happen to have an image of the right size.  For example, if you have two displays that are each 1024x768, you can select an image that's 2048x768, select Tile mode, and it will spill across the screens perfectly.  This doesn't do much good if your screens are aligned in a non-standard way, at different resolutions, or the image isn't sized right.

With this new understanding in mind, I realized that I could write a utility, that, when given multiple images, could read the display settings and generate a single bitmap with all images sized and positioned properly.  Tiling this image results in the perfect fit on each monitor.

Sizing and Placing

Reading your display configuration is as easy as accessing the System.Windows.Forms.Screens collection.  Each element is of type Screen and exposes its identifier, resolution, and coordinates.  Identifier and resolution should be self-explanatory, but the coordinates may take a little explaining.  By default, Windows will place two monitors side-by-side, such that display 1 is to the far-left, and subsequent monitors line up to the right.

The problem comes with unequal resolutions, or monitors of physically different sizes (as mine are).  To account for these differences, you can just drag the monitor previews around in the Display Properties dialog until things line up properly.  The exact placement is then reflected by the X,Y coordinates for a display.  Note that there can be no gap in-between displays, though you could get a pretty crazy configuration by placing two displays almost diagonal to each other.  Of course, given your physical monitor layout, this might make sense.

image

Image 2: Windows XP Display Properties - Settings tab

The first thing the application does is figure out the overall display bounds, taking into account all monitors.  In the simple case of two 1024x768 monitors, lined up perfectly side-by-side, the overall bounds would be 2048x768.  If you offset them similar to shown above, maybe the bounds become 2048x800.  The key is to create a single rectangle that contains all monitors, and to keep track of the visible regions in that rectangle.

To accomplish this, the AddBounds shared/static method (in the BoundsUtilities class) takes an overall Rectangle and a Rectangle to add.  The returned Rectangle represents the best fit overall bounds.

Visual Basic

Public Shared Function AddBounds(ByVal sourceBounds As Rectangle, ByVal newBounds As Rectangle) As Rectangle     If newBounds.Right > sourceBounds.Right Then         sourceBounds.Width += (newBounds.Right - sourceBounds.Width)     End If      If newBounds.Bottom > sourceBounds.Bottom Then         sourceBounds.Height += (newBounds.Bottom - sourceBounds.Height)     End If      If newBounds.Left < sourceBounds.Left Then         sourceBounds.X = newBounds.X     End If      If newBounds.Top < sourceBounds.Top Then         sourceBounds.Y = newBounds.Y     End If      Return sourceBounds End Function

Visual C#

public static Rectangle AddBounds(Rectangle sourceBounds, Rectangle newBounds) {     if (newBounds.Right > sourceBounds.Right)         sourceBounds.Width += (newBounds.Right - sourceBounds.Width);      if (newBounds.Bottom > sourceBounds.Bottom)         sourceBounds.Height += (newBounds.Bottom - sourceBounds.Height);      if (newBounds.Left < sourceBounds.Left)     {         sourceBounds.X = newBounds.X;     }      if (newBounds.Top < sourceBounds.Top)     {         sourceBounds.Y = newBounds.Y;     }      return sourceBounds; }

In the MainForm class, when the application starts up or the display properties change, the UpdateMonitorBounds method cycles through the screens calling AddBounds.  It also accounts for the fact that the primary display always establishes 0,0, and screens above or to the left of it will have negative coordinates.  This method also creates Bitmap instances for both the preview and the actual desktop display.

Visual Basic

Private Sub UpdateMonitorBounds()     screens = Screen.AllScreens      overallBounds = New Rectangle()     refPoint = New Point()      For Each scr As Screen In screens         overallBounds = BoundsUtilities.AddBounds(overallBounds, scr.Bounds)     Next      ' Screens to the left or above the primary screen cause 0,0 to be other     ' than the top/left corner of the Bitmap     If overallBounds.X < 0 Then         refPoint.X = Math.Abs(overallBounds.X)     End If     If overallBounds.Y < 0 Then         refPoint.Y = Math.Abs(overallBounds.Y)     End If      ' Cancels out the negative values from offset screens     Dim correctedBounds As Rectangle = ZeroRectangle(overallBounds, refPoint)      previewBitmap = New Bitmap(CInt(correctedBounds.Width / 4), CInt(correctedBounds.Height / 4))     desktopBitmap = New Bitmap(correctedBounds.Width, correctedBounds.Height) End Sub

Visual C#

private void UpdateMonitorBounds() {     screens = Screen.AllScreens;      overallBounds = new Rectangle();     refPoint = new Point();      foreach (Screen scr in screens)     {         overallBounds = BoundsUtilities.AddBounds(overallBounds, scr.Bounds);     }      // Screens to the left or above the primary screen cause 0,0 to be other     // than the top/left corner of the Bitmap     if (overallBounds.X < 0) refPoint.X = Math.Abs(overallBounds.X);     if (overallBounds.Y < 0) refPoint.Y = Math.Abs(overallBounds.Y);      // Cancels out the negative values from offset screens     Rectangle correctedBounds = ZeroRectangle(overallBounds, refPoint);      previewBitmap = new Bitmap(correctedBounds.Width / 4, correctedBounds.Height / 4);     desktopBitmap = new Bitmap(correctedBounds.Width, correctedBounds.Height); }

The display preview serves little purpose in its current form, but it could be useful with many monitors.  It's a size-reduced version to scale quicker.  It also shows the display index (1, 2, etc) similar to in Display Properties.  The AddImageToPreview method is a bit long to show here, but it accounts for negative screen values, resizes accordingly, and renders each image onto its corresponding display.  The last step draws the number on in two steps using the GraphicsPath object to convert the supplied text to its vector path information.  It's drawn in black, a bit smaller in white to create the outlined format.  If you only draw with one color, there will always be bitmaps that render it invisible.  A white digit with a black outline takes care of this problem.

Visual Basic

Private Sub RenderCaption(ByVal g As Graphics, ByVal bounds As Rectangle, ByVal caption As String)     Dim captionFont As New Font(FontFamily.GenericSansSerif, bounds.Height / 4)     Dim layoutRect As New Rectangle(bounds.X, bounds.Y, bounds.Width, bounds.Height)     Dim path As New GraphicsPath()     path.AddString(caption, captionFont.FontFamily, CInt(captionFont.Style), _         CSng(captionFont.Height), layoutRect, StringFormat.GenericDefault)      Dim p As New Pen(Brushes.Black, 5)     g.DrawPath(p, path)     g.FillPath(Brushes.White, path) End Sub

Visual C#

private void RenderCaption(Graphics g, Rectangle bounds, string caption) {     Font captionFont = new Font(FontFamily.GenericSansSerif, bounds.Height/4);     Rectangle layoutRect = new Rectangle(bounds.X, bounds.Y, bounds.Width, bounds.Height);     GraphicsPath path = new GraphicsPath();     path.AddString(caption, captionFont.FontFamily,         (int)captionFont.Style, (float)captionFont.Height,         layoutRect, StringFormat.GenericDefault);          Pen p = new Pen(Brushes.Black, 5);     g.DrawPath(p, path);     g.FillPath(Brushes.White, path); }

The preview appears thus:

image

Image 3: The MultiWall application preview window (photos by the author)

Rendering the actual desktop image is almost the same, though at full-size and without the digit overlay.  One complication is that monitors can be in the "negative" zone (to the left or above the primary display).  The composite bitmap must have its 0,0 corresponding to the 0,0 of the primary display.  To account for the negative images, you must actually render the images to the far right, or far bottom.  The tiling effect performs a "wrap-around" to make it fit.  The worst part, is that, depending on where it is, you must draw the image multiple times to get all directions to tile properly!

Dragging, dropping, and layout

Figuring out a good way to actually set the monitor's wallpapers posed a design challenge.  I originally had Browse buttons in the preview window, but it was unwieldy.  I really wanted to be able to drag-and-drop onto the preview regions, but figuring out the drop coordinates relative to the original images, accounting for negative coordinates and a user-resizeable window wasn't realistic for the purposes of this article, but it's definitely possible!  Perhaps the easiest way would be convert the single PictureBox to dynamically placed PictureBox controls for each display.  But I digress...

My solution was to create a drop region in the top-middle of each display.  If you drag an image file to this region it will update the wallpaper accordingly.  Images are always cached using a WeakReference to avoid reloading if you switch back and forth.  Currently used images are also stored in a collection to provide a hard reference to prevent garbage collection on those.  If an image is reclaimed from cache it just gets reloaded.

image

Image 4: The bitmap drop region

These windows are set partially transparent and will disappear when the preview window is minimized to tray, unless the Show Drop Regions option is checked in the tray menu.  This setting is remembered when the application is closed.

Each time an image is dropped onto a region, the code builds both the preview bitmap and the desktop bitmap.  The application remembers the filenames so upon next startup it can show the individual bitmaps.  It could load the desktop's composite image, but it would need to follow extra steps to chop the composite image back into its constituent parts to redraw when one monitor is updated.

For more information on the registry update and system call required to actually update the wallpaper at the system level, see my earlier articles (referenced in the introduction).  The generated composite Bitmap file is saved to the My Pictures folder with the name "MultiWallImage.bmp".  The file could be hidden as an extra step as well.  Notice that it's quite large (12MB on my system!).  Unfortunately, Windows XP requires a BMP file.  Though you can supply a JPEG or other format using the Display Properties dialog, it converts it to BMP before it shows it.

Next Steps

No program is ever complete!  I wanted to add a way to take a single large image and have it chopped up in the best way to span multiple monitors, but it seemed more complex than expected when I dug into it.  I also wanted to add drag-and-drop to the preview window.  The preview window doesn't really serve much purpose, but it was a good exercise.  The program could be made more efficient with a background thread, and it should only redraw images if display settings change.  Currently if you drag an image to display #3, all display's images are redrawn/rescaled.  Finally, an image randomizer would be easy to add.  Use Windows (Desktop) Search or a given folder and cycle wallpapers upon startup/interval/random.

Conclusion

This article covered some graphics drawing techniques, drag-and-drop, system events, and wallpaper settings.  It's not the first wallpaper application on Coding 4 Fun, but it's probably the best (of mine anyway!).  For any comments, questions, or suggestions, contact me through my blog.  Happy papering!


Avatar80 Arian Kulp is an independent software developer and writer working in the Midwest.  He has been coding since the fifth grade on various platforms, and also enjoys photography, nature, and spending time with his family.  Arian can be reached through his web site at http://www.ariankulp.com.


 
 

Things you can do from here:

 
 

没有评论:

发表评论