Exploring the GrabAPicture Application (Part 7)

Last week, in the Exploring the GrabAPicture Application (Part 6) post, we discussed the use of the SystemParametersInfo() function call. Of course, this call is part of the Windows API, so you have to use P/Invoke to access it. The previous post doesn’t really tell you how to use the SystemParametersInfo() call—that’s the topic of this post.

Wallpaper is defined by three elements in the GrabAPicture application: Name, Location, and Style. The Name property is easyit can be any string. So, you don’t have to work really hard with it. The Location property is the most complex because a location can’t be anywhere there is a graphic that the application can grabeither local or remote. The Style property is still complex because of the way Windows stores information, but it’s not absurdly difficult because Windows only supports so many styles. The following code looks at the Style property in the WinWallpaper class first.

Public Property Style() As Styles
   Get
      ' Contains the registry key that holds the
      ' desktop values.
      Dim StyleKey As RegistryKey
 
      ' Get the desktop key.
      StyleKey = _
         Registry.CurrentUser.OpenSubKey( _
            "Control Panel\Desktop", False)
 
      ' Obtain the current value.
      If StyleKey.GetValue("WallpaperStyle") = 2 And _
         StyleKey.GetValue("TileWallpaper") = 0 Then
         Return Styles.Stretched
      End If
      If StyleKey.GetValue("WallpaperStyle") = 1 And _
         StyleKey.GetValue("TileWallpaper") = 0 Then
         Return Styles.Centered
      End If
      If StyleKey.GetValue("WallpaperStyle") = 1 And _
         StyleKey.GetValue("TileWallpaper") = 1 Then
         Return Styles.Tiled
      End If
 
      ' This value should only show up if someone has
      ' tampered with the registry and set the values
      ' incorrectly.
      Return Styles.Unknown
   End Get
   Set(ByVal Value As Styles)
      ' Because Styles.Unknown signifies an error
      ' condition, the developer can't provide it as an
      ' input value.
      If Value = Styles.Unknown Then
         Throw New ArgumentException( _
            "Cannot use Styles.Unknown as an input argument")
      End If
 
      ' Contains the registry key that holds the
      ' desktop values.
      Dim StyleKey As RegistryKey
 
      ' Get the desktop key.
      StyleKey = _
         Registry.CurrentUser.OpenSubKey( _
            "Control Panel\Desktop", True)
 
      ' Select one of the other input values and set the
      ' registry keys as needed.
      Select Case Value
         Case Styles.Stretched
            StyleKey.SetValue("WallpaperStyle", "2")
            StyleKey.SetValue("TileWallpaper", "0")
         Case Styles.Centered
            StyleKey.SetValue("WallpaperStyle", "1")
            StyleKey.SetValue("TileWallpaper", "0")
         Case Styles.Tiled
            StyleKey.SetValue("WallpaperStyle", "1")
            StyleKey.SetValue("TileWallpaper", "1")
      End Select
   End Set
End Property

The Style property defines which style to use to display an image on the Desktop. The current version of the GetAPicture program is compatible with all versions of Windows, including Windows XP. A later post will show how to upgrade this application so it also allows use with Windows 7-specific features (Fill and Fit). As you can see, the get portion of this property begins by accessing the HKEY_CURRENT_USER\Control Panel\Desktop key of the registry as shown here.

GrabAPicture0701

Eventually, you’ll use three values from this key: TileWallpaper, Wallpaper, and WallpaperStyle. For now, all you need to know about is TileWallpaper (used to define the wallpaper as Tiled) and WallpaperStyle (used for all of the other settings). The code examines the registry settings using the GetValue() method and returns a Styles enumeration value that reflects the current settings. If the code can’t recognize a setting, then it returns Styles.Unknown.

When it comes time to set the wallpaper style, the application verifies that the style is known and then uses two calls to SetValue() to configure the TitleWallpaper and WallpaperStyle settings in the registry. As you might imagine, updating the rudimentary functionality of the code won’t be hard, but to ensure that the code works properly with all versions of Windows, you need to add version checks. This is the part that will interest you the most in the future post.

The WallpaperURI property code is a lot more complex than you might imagine for a very good reason. If you simply try to set the wallpaper location using the registry values, you won’t see any change on screen. The system won’t know to perform the update. In order to see the update and ensure that the change is reflected in any running applications, you must use the SystemParametersInfo() call as shown in the following code.

Public Property WallpaperURI() As Uri
   Get
      ' Create a variable to hold the result.
      Dim Result As New StringBuilder(120)
 
      ' Get the wallpaper URI.
      If SystemParametersInfo( _
         Convert.ToUInt32(SPI_GETDESKWALLPAPER), _
         Convert.ToUInt32(Result.Capacity), _
         Result, _
         Convert.ToUInt32(0)) Then
 
         ' Return the result as a URI.
         Return New Uri(Result.ToString())
      Else
         ' Throw an exception if there is an error.
         Throw New Exception( _
            "Error Calling SystemParametersInfo: " + _
            Marshal.GetLastWin32Error().ToString())
      End If
   End Get
   Set(ByVal Value As Uri)
      ' The usable, local path, location of the
      ' wallpaper.
      Dim Location As String
 
      ' A connection to the wallpaper online.
      Dim GetData As Stream
 
      ' The online image.
      Dim Img As Image
 
      ' Wallpaper on a Web page requires somewhat
      ' different processing than local wallpaper.
      If Value.Scheme.ToUpper() = "HTTP" Then
 
         ' Create a new path to save the image
         ' locally so it can be placed on the desktop.
         Location = Path.GetTempPath() + "WallPaper.BMP"
 
         ' The Web connection could fail, so use a Try
         ' block to obtain the file.
         Try
 
            ' Obtain a connection to the wallpaper.
            GetData = _
               New WebClient().OpenRead(Value.ToString())
 
            ' Read the wallpaper data.
            Img = Image.FromStream(GetData)
 
            ' Save the image.
            Img.Save(Location, Imaging.ImageFormat.Bmp)
 
         Catch ex As WebException
            ' The Web connection has failed.
            ' Use the existing Wallpaper.BMP
            ' file if it exists.
            If File.Exists(Location) Then
 
               ' Read the existing file instead.
               Img = Image.FromFile(Location)
 
            Else
               ' Otherwise, use the local path instead.
               Location = Value.LocalPath
 
               ' Verify the wallpaper is a BMP.
               If Not Location.ToUpper().EndsWith("BMP") Then
 
                  ' Load the file from disk.
                  Img = Image.FromFile(Location)
 
                  ' Create a new path to save the image
                  ' locally so it can be placed on the desktop.
                  Location = Path.GetTempPath() + "WallPaper.BMP"
 
                  ' Save the image.
                  Img.Save(Location, Imaging.ImageFormat.Bmp)
               End If
            End If
         End Try
      Else
         ' Assume that the wallpaper is local.
         Location = Value.LocalPath
 
         ' Verify the wallpaper is a BMP.
         If Not Location.ToUpper().EndsWith("BMP") Then
 
            ' Load the file from disk.
            Img = Image.FromFile(Location)
 
            ' Create a new path to save the image
            ' locally so it can be placed on the desktop.
            Location = Path.GetTempPath() + "WallPaper.BMP"
 
            ' Save the image.
            Img.Save(Location, Imaging.ImageFormat.Bmp)
         End If
      End If
 
 
      If SystemParametersInfo( _
         Convert.ToUInt32(SPI_SETDESKWALLPAPER), _
         Convert.ToUInt32(Location.Length), _
         New StringBuilder(Location), _
         Convert.ToUInt32(SPIF_UPDATEINIFILE Or SPIF_SENDWININICHANGE)) Then
      Else
         ' Throw an exception if there is an error.
         Throw New Exception( _
            "Error Calling SystemParametersInfo: " + _
            Marshal.GetLastWin32Error().ToString())
      End If
   End Set
End Property

Working with a Windows API function requires that you do some special coding. Otherwise, the call will never work. It’s important to remember that the Windows API works mainly with numbers and not with other data types. If you want to work with a string, then the string parameter takes the form of a pointer to memory in the caller’s address space in most cases. Of course, Visual Basic doesn’t provide a means to do this directly. Creating a StringBuilder object tells Visual Basic to marshal the string as a pointer to the Windows API. In this case, the code creates a StringBuilder capable of holding 120 characters, which is normally enough to hold the location of the current wallpaper on disk.

 

PInvoke comes in handy for all sorts of purposes in Visual Basic. This application shows just one example. You can find another great example of working with PInvoke on Rod Stephen’s site with the “Remove the X Close button from a form’s system menu in Visual Basic .NET” example. I haven’t seen this particular use of PInvoke explained anywhere else, so you’ll definitely want to take a look. Of course, you can get all the information you need for working with PInvoke in my book, “.NET Framework Solutions: In Search of the Lost Win32 API.”

The call to SystemParametersInfo() comes next. Normally, you’d make a call that could fail in a try…catch block. However, a Windows API call won’t work that way. In this case, the call returns True when the call succeeds and False when it doesn’t (individual Windows API calls behave in different ways, so make sure you understand how errors are presented by individual call). When the call returns True, the property returns a new Uri containing the result provided by SystemParametersInfo(). Otherwise, the code throws an Exception. Unfortunately, you don’t know what happened. The only way to get this information is to call Marshal.GetLastWin32Error() and that information is only available when you set the value of the SetLastError property of the <DllImport> attribute (explained in the previous post) to True.

 

Some readers will take me to task for not providing a more specific exception, especially since I pound on this particular failing in many applications in my books. Because you don’t know the nature of the exception, you can’t provide anything better than an general Exception in most cases. The only way around this is to create code that laboriously tracks every sort of potential error returned by the Windows API and creates an appropriate exception for it. This may be a good topic for another post in the future, but definitely not the point of this post.

Setting the wallpaper location is complicated by the fact that the wallpaper can be either local or remote. The code pursues the remote route first. The example assumes that the remote wallpaper will be on an HTTP site. You’d need to modify the code to accommodate other schemes such as HTTPS. This might make a good future post, but let’s get into a common HTTP remote location first.

The first step is to create a copy of the wallpaper on the local drive in the user’s temporary folder. A call to Path.GetTempPath() obtains this information. The remote wallpaper is always stored in a file named Wallpaper.BMP (I chose the BMP format for compatibility purposes).

The code performs a number of tasks in a try…catch block. First, it attempts to read the file containing the image from the remote location into GetData (of type Stream). If this is successful (the image exists and you can read it), the code converts the stream into an Image object, Img. The image can be of any type that Image supports:

 

  • BMP
  • GIF
  • JPEG
  • PNG
  • TIFF

The final step is to save the image, using Img.Save() to Wallpaper.BMP in the BMP format. If there is an exception in any of these steps, the application automatically recovers in one of two ways. First, it looks for an example Wallpaper.BMP and uses it when the file exists. Second, if this first technique fails, then the application looks at the current value for the local wallpaper selection and uses it instead. In this case, the application uses the Image.FromFile() method to obtain a copy of the image and saves it to Wallpaper.BMP.

When the wallpaper is local, the code first checks to see if the wallpaper is already in BMP format. If it is, the application simply places the information in Location. Otherwise, the application reads the file using Image.FromFile() and saves the image in BMP format to Wallpaper.BMP.

No matter how the information gets there, eventually Location contains a path and filename for the file you want to display on the Desktop. At this point, the code calls SystemParametersInfo() to set the wallpaper, which indirectly sets the Wallpaper value in the registry. The call also notifies Windows that it needs to update the Desktop and any applications that are currently running that they need to repaint their displays. The user sees the new wallpaper displayed on screen.

At this point, you have a good idea of how the WinWallpaper class works. In the next post, you learn about the GrabAPictureSettings class, which is used to interact with the settings you’ve learned about in the past several posts. Please let me know if you have any questions at John@JohnMuellerBooks.com. You can see the next post in this series at Exploring the GrabAPicture Application (Part 8).