Improving the GrabAPicture Application (Part 4)

Last week’s post, Improving the GrabAPicture Application (Part 3), began the process of modifying the forms to accept the special wallpaper settings supported by Windows 7 and newer versions of the Windows operating system. That post also stressed the need to make the forms flexible enough to support older versions of the operating system. The forms actually detect which operating system is in use and modify their appearance to match the capability of the operating system. This week you’ll see the code used to make the new settings work properly. Of course, these two posts together support the second objective outlined in the Improving the GrabAPicture Application (Part 1) post.

Modifying WinWallpaper.vb

The underlying code is less critical when it comes to detecting the operating system because the form already performs this task. The user can’t select an option that doesn’t work with the selected operating system if the forms don’t make that option available. Consequently, a lot of the changes in this example are straightforward. The first change is to extend the Styles enumeration initially discussed in the Exploring the GrabAPicture Application (Part 5) post as shown in the following code.

' A list of wallpaper display styles.
Public Enum Styles As Integer
   Tiled
   Centered
   Stretched
   Unknown
   Fit
   Fill
End Enum

Notice that I add the new options after Unknown to ensure that they don’t inadvertently affect any of the application code. Some developers might be tempted to add the options before Unknown, but this would be a mistake because you can’t be absolutely certain about potential side effects of doing so.

Now that you have the right enumerations available, it’s time to modify the code that relies on the enumerations. Before you can do anything though, you need to determine what values to provide for the  two new enumerations. Remember that in the Exploring the GrabAPicture Application (Part 7) post that the HKEY_CURRENT_USER\Control Panel\Desktop registry key WallpaperStyle value contains the information you need. When you set the desktop to use the Fit style, this value becomes 6 as shown here.

GrabAPictureUpdate0401

Setting the desktop wallpaper to Fill changes this value to 10. Consequently, the first change you need to make is to the Style() property (discussed in the Exploring the GrabAPicture Application (Part 7) post) as shown here.

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
 
      ' Windows 7 and above-specific values.
      If StyleKey.GetValue("WallpaperStyle") = 6 And _
         StyleKey.GetValue("TileWallpaper") = 0 Then
         Return Styles.Fit
      End If
      If StyleKey.GetValue("WallpaperStyle") = 10 And _
         StyleKey.GetValue("TileWallpaper") = 0 Then
         Return Styles.Fill
      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")
 
            ' Provide Windows 7 and above-specific values.
         Case Styles.Fit
            StyleKey.SetValue("WallpaperStyle", "6")
            StyleKey.SetValue("TileWallpaper", "0")
         Case Styles.Fill
            StyleKey.SetValue("WallpaperStyle", "10")
            StyleKey.SetValue("TileWallpaper", "0")
      End Select
   End Set
End Property

All of the required changes are marked with comments so you can find them easily. In all cases, it’s simply a matter of making the right registry changes. The behavior of the code remains unchanged.

During testing, I found that if the value of the WallpaperStyle registry setting is set to either 6 or 10 on a Windows XP system, Windows assumes you mean stretched (the option normally associated with a value of 2). Testing on a Vista system shows the same thing happens. I’ll be interested to hear from readers whether this is the case on their systems. If not, the example may very well need to include additional operating system-specific code for the Style() property.

Modifying the Command Line Interface

There is one place where things can get incredibly messy, and that’s with the command line interface. The Exploring the GrabAPicture Application (Part 9) post describes the command line interface in detail. In addition, you’ve already seen on update to this interface in the Improving the GrabAPicture Application (Part 2) post. In order to add the new Windows 7 functionality to the command line, you’ll need to perform another update of it that includes operating system version checking similar to the checking provided for the forms. For now, we’ll leave the command line interface alone and save this update for another time.

However, in order to keep the command line interface from working improperly, you do need to implement a few changes to the /Random command line switch code. This code ensures that the Windows 7 and above-specific settings are handled when working with random wallpaper selections as shown here:

If CmdArg.ToUpper() = "/RANDOM" Then
   ' Current application settings.
   Dim CurrentSettings As GrabAPictureSettings
 
   ' Number of settings to randomize.
   Dim Settings As Int32 = 0
 
   ' The selected image number.
   Dim Selection As Int32 = 0
 
   ' Contains the wallpaper location.
   Dim NewUri As Uri
 
   ' Wallpaper manipulation class.
   Dim Wallpaper As New WinWallpaper
 
   ' Initialize the stored settings.
   CurrentSettings = GrabAPictureSettings.LoadSettings()
 
   ' Verify that there are settings to use.
   If CurrentSettings Is Nothing Then
      MessageBox.Show( _
         "This application isn't configured to use random sources.")
      Return -1
   End If
 
   ' Obtain the number of settings to randomize.
   If CurrentSettings.LocalChecked And CurrentSettings.RemoteChecked Then
 
      ' Detect configuration errors.
      If CurrentSettings.LocalUris Is Nothing Or _
         CurrentSettings.RemoteUris Is Nothing Then
 
         MessageBox.Show("Application not set up to use " + _
                         "both local and remote locations.")
         Return -2
      End If
 
      ' Determine the number of settings.
      Settings = CurrentSettings.LocalUris.Length + _
         CurrentSettings.RemoteUris.Length - 1
 
      ' Randomize the settings.
      Randomize(DateTime.Now.Millisecond)
      Selection = Convert.ToInt32(Settings * Rnd())
 
      ' Obtain the style and location.
      If Selection < CurrentSettings.LocalUris.Length Then
 
         ' Choose the wallpaper style.
         Select Case CurrentSettings.LocalUris(Selection).Style
            Case WinWallpaper.Styles.Stretched
               Wallpaper.Style = WinWallpaper.Styles.Stretched
            Case WinWallpaper.Styles.Centered
               Wallpaper.Style = WinWallpaper.Styles.Centered
            Case WinWallpaper.Styles.Tiled
               Wallpaper.Style = WinWallpaper.Styles.Tiled
 
               ' Add the Windows 7 and above-specific options.
            Case WinWallpaper.Styles.Fit
               Wallpaper.Style = WinWallpaper.Styles.Fit
            Case WinWallpaper.Styles.Fill
               Wallpaper.Style = WinWallpaper.Styles.Fill
         End Select
 
         ' Create a wallpaper URI.
         NewUri = New Uri(CurrentSettings.LocalUris(Selection).Location)
      Else
         ' Subtract the local resources.
         Selection -= CurrentSettings.LocalUris.Length
 
         ' Choose the wallpaper style.
         Select Case CurrentSettings.RemoteUris(Selection).Style
            Case WinWallpaper.Styles.Stretched
               Wallpaper.Style = WinWallpaper.Styles.Stretched
            Case WinWallpaper.Styles.Centered
               Wallpaper.Style = WinWallpaper.Styles.Centered
            Case WinWallpaper.Styles.Tiled
               Wallpaper.Style = WinWallpaper.Styles.Tiled
 
               ' Add the Windows 7 and above-specific options.
            Case WinWallpaper.Styles.Fit
               Wallpaper.Style = WinWallpaper.Styles.Fit
            Case WinWallpaper.Styles.Fill
               Wallpaper.Style = WinWallpaper.Styles.Fill
         End Select
 
         ' Create a wallpaper URI.
         NewUri = New Uri(CurrentSettings.RemoteUris(Selection).Location)
      End If
 
      ' Set the wallpaper location.
      Wallpaper.WallpaperURI = NewUri
 
   ElseIf CurrentSettings.LocalChecked Then
 
      ' Detect configuration errors.
      If CurrentSettings.LocalUris Is Nothing Then
         MessageBox.Show("Application not set up " + _
                         "to use local locations.")
         Return -2
      End If
 
      ' Determine the number of settings.
      Settings = CurrentSettings.LocalUris.Length - 1
 
      ' Randomize the settings.
      Randomize(DateTime.Now.Millisecond)
      Selection = Convert.ToInt32(Settings * Rnd())
 
      ' Obtain the style and location.
      ' Choose the wallpaper style.
      Select Case CurrentSettings.LocalUris(Selection).Style
         Case WinWallpaper.Styles.Stretched
            Wallpaper.Style = WinWallpaper.Styles.Stretched
         Case WinWallpaper.Styles.Centered
            Wallpaper.Style = WinWallpaper.Styles.Centered
         Case WinWallpaper.Styles.Tiled
            Wallpaper.Style = WinWallpaper.Styles.Tiled
 
            ' Add the Windows 7 and above-specific options.
         Case WinWallpaper.Styles.Fit
            Wallpaper.Style = WinWallpaper.Styles.Fit
         Case WinWallpaper.Styles.Fill
            Wallpaper.Style = WinWallpaper.Styles.Fill
      End Select
 
      ' Create a wallpaper URI.
      NewUri = New Uri(CurrentSettings.LocalUris(Selection).Location)
 
      ' Set the wallpaper location.
      Wallpaper.WallpaperURI = NewUri
 
   ElseIf CurrentSettings.RemoteChecked Then
 
      ' Detect configuration errors.
      If CurrentSettings.RemoteUris Is Nothing Then
         MessageBox.Show("Application not set up " + _
                         "to use remote locations.")
         Return -2
      End If
 
      ' Determine the number of settings.
      Settings = CurrentSettings.RemoteUris.Length - 1
 
      ' Randomize the settings.
      Randomize(DateTime.Now.Millisecond)
      Selection = Convert.ToInt32(Settings * Rnd())
 
      ' Obtain the style and location.
      ' Choose the wallpaper style.
      Select Case CurrentSettings.RemoteUris(Selection).Style
         Case WinWallpaper.Styles.Stretched
            Wallpaper.Style = WinWallpaper.Styles.Stretched
         Case WinWallpaper.Styles.Centered
            Wallpaper.Style = WinWallpaper.Styles.Centered
         Case WinWallpaper.Styles.Tiled
            Wallpaper.Style = WinWallpaper.Styles.Tiled
 
            ' Add the Windows 7 and above-specific options.
         Case WinWallpaper.Styles.Fit
            Wallpaper.Style = WinWallpaper.Styles.Fit
         Case WinWallpaper.Styles.Fill
            Wallpaper.Style = WinWallpaper.Styles.Fill
      End Select
 
      ' Create a wallpaper URI.
      NewUri = New Uri(CurrentSettings.RemoteUris(Selection).Location)
 
      ' Set the wallpaper location.
      Wallpaper.WallpaperURI = NewUri
 
   Else
      MessageBox.Show("None of the random settings are selected.")
   End If
   Return 0
End If

All of the Windows 7 additions are marked with comments. The code simply ensures that the stored settings are handled appropriately. There isn’t any change in command line syntax or behavior with these changes.

Modifying frmMain

The majority of the changes for this update appear in frmMain. The only way to accomplish the task is to go through the code one step at a time. The first change appears in frmMain_Load(), which was originally discussed in Exploring the GrabAPicture Application (Part 10). The Select statement needs to be updated to ensure that the form loads with the controls configured correctly as shown here.

' Get the current wallpaper style.
Select Case Wallpaper.Style
   Case WinWallpaper.Styles.Stretched
      rbStretched.Checked = True
      rbCentered.Checked = False
      rbTiled.Checked = False
      rbFit.Checked = False
      rbFill.Checked = False
   Case WinWallpaper.Styles.Centered
      rbStretched.Checked = False
      rbCentered.Checked = True
      rbTiled.Checked = False
      rbFit.Checked = False
      rbFill.Checked = False
   Case WinWallpaper.Styles.Tiled
      rbStretched.Checked = False
      rbCentered.Checked = False
      rbTiled.Checked = True
      rbFit.Checked = False
      rbFill.Checked = False
   Case WinWallpaper.Styles.Fit
      rbStretched.Checked = False
      rbCentered.Checked = False
      rbTiled.Checked = False
      rbFit.Checked = True
      rbFill.Checked = False
   Case WinWallpaper.Styles.Fill
      rbStretched.Checked = False
      rbCentered.Checked = False
      rbTiled.Checked = False
      rbFit.Checked = False
      rbFill.Checked = True
End Select

You must also add event handlers for the Fit and Fill radio buttons on frmMain. These event handlers as similar to those discussed for the other radio buttons as shown here.

Private Sub rbFit_CheckedChanged(sender As System.Object, _
                                 e As System.EventArgs) _
                              Handles rbFit.CheckedChanged
   Dim Wallpaper As New WinWallpaper
 
   ' Set the new style.
   Wallpaper.Style = WinWallpaper.Styles.Fit
End Sub
 
Private Sub rbFill_CheckedChanged(sender As System.Object, _
                                  e As System.EventArgs) _
                               Handles rbFill.CheckedChanged
   Dim Wallpaper As New WinWallpaper
 
   ' Set the new style.
   Wallpaper.Style = WinWallpaper.Styles.Fill
End Sub

Modifying frmConfigure

The two Select buttons on frmConfigure (discussed in the Exploring the GrabAPicture Application (Part 11) post) require modification to work with the new options. When a user clicks Select, the code configures the Desktop with the newly selected wallpaper. The updates merely make it possible for the configured sources to contain the new fit and fill options. The code for the local resources version of the Select button appears here.

Private Sub btnLSelect_Click(ByVal sender As System.Object, _
                             ByVal e As System.EventArgs) _
                          Handles btnLSelect.Click
   ' Wallpaper manipulation class.
   Dim Wallpaper As New WinWallpaper
 
   ' Choose the wallpaper style.
   Select Case CurrentSettings.LocalUris(lstLocal.SelectedIndex).Style
      Case WinWallpaper.Styles.Stretched
         Wallpaper.Style = WinWallpaper.Styles.Stretched
      Case WinWallpaper.Styles.Centered
         Wallpaper.Style = WinWallpaper.Styles.Centered
      Case WinWallpaper.Styles.Tiled
         Wallpaper.Style = WinWallpaper.Styles.Tiled
 
         ' Add the Windows 7 and above-specific Styles.
      Case WinWallpaper.Styles.Fit
         Wallpaper.Style = WinWallpaper.Styles.Fit
      Case WinWallpaper.Styles.Fill
         Wallpaper.Style = WinWallpaper.Styles.Fill
   End Select
 
   ' Create a wallpaper URI.
   Dim NewUri As New Uri( _
      CurrentSettings.LocalUris(lstLocal.SelectedIndex).Location)
 
   ' Set the wallpaper location.
   Wallpaper.WallpaperURI = NewUri
End Sub

The remote version of the Select button code appears here.

Private Sub btnSelect_Click(ByVal sender As System.Object, _
                            ByVal e As System.EventArgs) _
                         Handles btnSelect.Click
   ' Wallpaper manipulation class.
   Dim Wallpaper As New WinWallpaper
 
   ' Choose the wallpaper style.
   Select Case CurrentSettings.RemoteUris(lstRemote.SelectedIndex).Style
      Case WinWallpaper.Styles.Stretched
         Wallpaper.Style = WinWallpaper.Styles.Stretched
      Case WinWallpaper.Styles.Centered
         Wallpaper.Style = WinWallpaper.Styles.Centered
      Case WinWallpaper.Styles.Tiled
         Wallpaper.Style = WinWallpaper.Styles.Tiled
 
         ' Add the Windows 7 and above-specific Styles.
      Case WinWallpaper.Styles.Fit
         Wallpaper.Style = WinWallpaper.Styles.Fit
      Case WinWallpaper.Styles.Fill
         Wallpaper.Style = WinWallpaper.Styles.Fill
   End Select
 
   ' Create a wallpaper URI.
   Dim NewUri As New Uri( _
      CurrentSettings.RemoteUris(lstRemote.SelectedIndex).Location)
 
   ' Set the wallpaper location.
   Wallpaper.WallpaperURI = NewUri
End Sub

Modifying frmAddEdit

The final set of changes for this update is to frmAddEdit, which is discussed in the Exploring the GrabAPicture Application (Part 12) post. It’s important to remember that unlike frmMain, the radio buttons on this form don’t have any event handlers. The settings are handled in a different way in the code. The first set of changes appear in frmAddEdit_Load() to ensure that the form is configured correctly when the user opens it as shown here.

' Set the form up for editing.
If _IsEdit Then
   ' Make sure you use the correct database.
   If _IsRemote Then
      txtName.Text = _RemoteSources(_RecordNumber).Name
      txtLocation.Text = _RemoteSources(_RecordNumber).Location
      Select Case _RemoteSources(_RecordNumber).Style
         Case Styles.Stretched
            rbStretched.Checked = True
            rbCentered.Checked = False
            rbTiled.Checked = False
            rbFit.Checked = False
            rbFill.Checked = False
         Case Styles.Centered
            rbStretched.Checked = False
            rbCentered.Checked = True
            rbTiled.Checked = False
            rbFit.Checked = False
            rbFill.Checked = False
         Case Styles.Tiled
            rbStretched.Checked = False
            rbCentered.Checked = False
            rbTiled.Checked = True
            rbFit.Checked = False
            rbFill.Checked = False
         Case Styles.Fit
            rbStretched.Checked = False
            rbCentered.Checked = False
            rbTiled.Checked = False
            rbFit.Checked = True
            rbFill.Checked = False
         Case Styles.Fill
            rbStretched.Checked = False
            rbCentered.Checked = False
            rbTiled.Checked = False
            rbFit.Checked = False
            rbFill.Checked = True
      End Select
   Else
      txtName.Text = _Resources(_RecordNumber).Name
      txtLocation.Text = _Resources(_RecordNumber).Location
      Select Case _Resources(_RecordNumber).Style
         Case Styles.Stretched
            rbStretched.Checked = True
            rbCentered.Checked = False
            rbTiled.Checked = False
            rbFit.Checked = False
            rbFill.Checked = False
         Case Styles.Centered
            rbStretched.Checked = False
            rbCentered.Checked = True
            rbTiled.Checked = False
            rbFit.Checked = False
            rbFill.Checked = False
         Case Styles.Tiled
            rbStretched.Checked = False
            rbCentered.Checked = False
            rbTiled.Checked = True
            rbFit.Checked = False
            rbFill.Checked = False
         Case Styles.Fit
            rbStretched.Checked = False
            rbCentered.Checked = False
            rbTiled.Checked = False
            rbFit.Checked = True
            rbFill.Checked = False
         Case Styles.Fill
            rbStretched.Checked = False
            rbCentered.Checked = False
            rbTiled.Checked = False
            rbFit.Checked = False
            rbFill.Checked = True
      End Select
   End If
End If

The second set of changes occurs in the btnAddEdit_Click() event handler, which determines what happens to the user selections when the user closes the form (no matter what the button caption might say at the time). Here are the changes for this event handler.

Private Sub btnAddEdit_Click(ByVal sender As System.Object, _
                             ByVal e As System.EventArgs) _
                          Handles btnAddEdit.Click
   If IsEdit Then
      If _IsRemote Then
         ' Change the array data.
         _RemoteSources(_RecordNumber).Name = txtName.Text
         _RemoteSources(_RecordNumber).Location = txtLocation.Text
         If rbStretched.Checked Then
            _RemoteSources(_RecordNumber).Style = Styles.Stretched
         End If
         If rbCentered.Checked Then
            _RemoteSources(_RecordNumber).Style = Styles.Centered
         End If
         If rbTiled.Checked Then
            _RemoteSources(_RecordNumber).Style = Styles.Tiled
         End If
 
         ' Add the Windows 7 and above-specific options.
         If rbFit.Checked Then
            _RemoteSources(_RecordNumber).Style = Styles.Fit
         End If
         If rbFill.Checked Then
            _RemoteSources(_RecordNumber).Style = Styles.Fill
         End If
      Else
         ' Change the array data.
         _Resources(_RecordNumber).Name = txtName.Text
         _Resources(_RecordNumber).Location = txtLocation.Text
         If rbStretched.Checked Then
            _Resources(_RecordNumber).Style = Styles.Stretched
         End If
         If rbCentered.Checked Then
            _Resources(_RecordNumber).Style = Styles.Centered
         End If
         If rbTiled.Checked Then
            _Resources(_RecordNumber).Style = Styles.Tiled
         End If
 
         ' Add the Windows 7 and above-specific options.
         If rbFit.Checked Then
            _Resources(_RecordNumber).Style = Styles.Fit
         End If
         If rbFill.Checked Then
            _Resources(_RecordNumber).Style = Styles.Fill
         End If
      End If
   Else
      If _IsRemote Then
         ' Add a new array element.
         If _RemoteSources Is Nothing Then
            ReDim _RemoteSources(0)
         Else
            ReDim Preserve _RemoteSources(_RemoteSources.Length)
         End If
 
         ' Instantiate a new array item.
         _RemoteSources(_RemoteSources.Length - 1) = New RemoteUri
 
         ' Add the data to the array.
         _RemoteSources(_RemoteSources.Length - 1).Name = txtName.Text
         _RemoteSources(_RemoteSources.Length - 1).Location = txtLocation.Text
         If rbStretched.Checked Then
            _RemoteSources(_RemoteSources.Length - 1).Style = Styles.Stretched
         End If
         If rbCentered.Checked Then
            _RemoteSources(_RemoteSources.Length - 1).Style = Styles.Centered
         End If
         If rbTiled.Checked Then
            _RemoteSources(_RemoteSources.Length - 1).Style = Styles.Tiled
         End If
 
         ' Add the Windows 7 and above-specific options.
         If rbFit.Checked Then
            _RemoteSources(_RemoteSources.Length - 1).Style = Styles.Fit
         End If
         If rbFill.Checked Then
            _RemoteSources(_RemoteSources.Length - 1).Style = Styles.Fill
         End If
      Else
         ' Add a new array element.
         If _Resources Is Nothing Then
            ReDim _Resources(0)
         Else
            ReDim Preserve _Resources(_Resources.Length)
         End If
 
         ' Instantiate a new array item.
         _Resources(_Resources.Length - 1) = New LocalUri
 
         ' Add the data to the array.
         _Resources(_Resources.Length - 1).Name = txtName.Text
         _Resources(_Resources.Length - 1).Location = txtLocation.Text
         If rbStretched.Checked Then
            _Resources(_Resources.Length - 1).Style = Styles.Stretched
         End If
         If rbCentered.Checked Then
            _Resources(_Resources.Length - 1).Style = Styles.Centered
         End If
         If rbTiled.Checked Then
            _Resources(_Resources.Length - 1).Style = Styles.Tiled
         End If
 
         ' Add the Windows 7 and above-specific options.
         If rbFit.Checked Then
            _Resources(_Resources.Length - 1).Style = Styles.Fit
         End If
         If rbFill.Checked Then
            _Resources(_Resources.Length - 1).Style = Styles.Fill
         End If
      End If
   End If
End Sub

As you can see, the new options work precisely the same as the existing ones. When the user adds or edits a local or remote wallpaper, the code makes the appropriate changes to the stored settings.

If you compile your application at this point, you’ll find that you can now work with the Fit and Fill wallpaper settings in Windows 7. These settings will also be available when you move your application to Windows 8. All of these code changes come down to one thing really. The new settings involve additions to the WallpaperStyle registry settings: 6 for Fit and 10 for Fill. Let me know if you have any questions about these changes at John@JohnMuellerBooks.com.

 

Improving the GrabAPicture Application (Part 3)

The second feature described in the in the Improving the GrabAPicture Application (Part 1) post is to make it possible to use the Windows 7 extended wallpaper settings. It might seem like a simple task at first, but remember that the GrabAPicture application must continue to work on older systems that don’t have the required settings. Consequently, this feature requires that you not only add the new functionality, but that you also make it possible to detect the operating system and act accordingly. When the user has Windows 7 installed, the GrabAPicture application needs to allow and react to two new settings (my book, Professional Windows 7 Development Guide, explains these and other Windows 7-specific features in greater detail):

 

  • Fit: The graphic is resized to fit within the confines of the display, without modifying its aspect ration (as stretch will do).
  • Fill: The graphic is resized to completely fill the confines of the display, even if this means chopping off part of the graphic, without modifying its aspect ratio.


Modifying frmMain

There are a number of techniques you can use to implement features that are operating system version specific. The best way is to customize the handling of the feature so that users who have older, less capable, versions of the operating system don’t even realize the feature exists, which is the approach taken with this example. The first step in accomplishing this goal is to modify the interface so that the extra options are added with Windows 7 and above, but not with older versions. The new frmMain interface looks like this:

GrabAPictureUpdate0301

In order to achieve this update, you perform these steps:

 

  1. Change the frmMain.Size property to 328, 317.
  2. Change the cbRemote.Location property to 8, 256.
  3. Change the cbLocal.Location property to 8, 224.
  4. Change the GroupBox1.Size property to 192, 148.
  5. Add a new RadioButton control with the following property values:
    • (Name) = rbFit
    • Location = 16, 96
    • Modifiers = Friend
    • Size = 104, 24
    • TabIndex = 9
    • Text = &Fit
  6. Add a new RadioButton control with the following property values:
    • (Name) = rbFill
    • Location = 16, 120
    • Modifiers = Friend
    • Size = 104, 24
    • TabIndex = 10
    • Text = Fi&ll
  7. Change the cbRemote.TabIndex property to 11.
  8. Change the cbLocal.TabIndex property to 12.


However, you still need to maintain the ability to use the old interface. This means making it possible to hide the two new controls and changing settings for four existing controls back to the old state when working with an older operation system. These four settings are:

 


  • frmMain.Size = 328, 269

  • GroupBox1.Size = 192,100

  • cbLocal.Location = 8, 176

  • cbRemote.Location = 8, 208

With this in mind, you must modify the frmMain_Load() method by adding some code to the beginning of the method. This code checks the operating system version and then makes changes as shown here to return the application appearance to its original condition (you originally saw this code in the Exploring the GrabAPicture Application (Part 10) post).

' Detect the operating system version.
Dim OS As OperatingSystem = Environment.OSVersion
 
' Check the operating system version and modify the interface
' as needed.
If OS.Version.Major < 6 Or OS.Version.Minor < 1 Then
 
   ' Disable the two wallpaper and make them invisible.
   rbFill.Enabled = False
   rbFill.Visible = False
   rbFit.Enabled = False
   rbFit.Visible = False
 
   ' Change the properites back to their old configuration.
   Me.Size = New Size(328, 269)
   GroupBox1.Size = New Size(192, 100)
   cbLocal.Location = New Point(8, 176)
   cbRemote.Location = New Point(8, 208)
End If

When GrabAPicture runs, it displays the appearance that best matches the host operating system. It’s important to disable the rbFill and rbFit controls so that the user can’t access them accidentally. In addition, you make them invisible so that the user can’t see them either. Notice that the If statement relies on an Or condition because either value could trigger the code. For example, Vista is version 6.0, so it would trigger the code because the Version.Minor value is 0.

Modifying frmAddEdit

The changes you make to frmMain also apply to frmAddEdit. However, this form is less complex, so the changes are less extensive. Follow this procedure to make the changes to the form.

  1. Change the frmAddEdit.Size property to 300, 316.
  2. Change the grpStyleSelect.Size property to 192, 148.
  3. Add a new RadioButton control with the following property values:
    • (Name) = rbFit
    • Location = 16, 96
    • Modifiers = Friend
    • Size = 104, 24
    • TabIndex = 3
    • Text = &Fit
  4. Add a new RadioButton control with the following property values:
    • (Name) = rbFill
    • Location = 16, 120
    • Modifiers = Friend
    • Size = 104, 24
    • TabIndex = 10
    • Text = Fi&ll

At this point, frmAddEdit should look like this:

GrabAPictureUpdate0302

You’ll need to add code to the frmAddEdit_Load() method to change the user interface back to it simpler form when the user has an older version of Windows. As before, add the following code to the beginning of the method (you originally saw this code in the Exploring the GrabAPicture Application (Part 12) post).

' Detect the operating system version.
Dim OS As OperatingSystem = Environment.OSVersion
 
' Check the operating system version and modify the interface
' as needed.
If OS.Version.Major < 6 Or OS.Version.Minor < 1 Then
 
   ' Disable the two wallpaper and make them invisible.
   rbFill.Enabled = False
   rbFill.Visible = False
   rbFit.Enabled = False
   rbFit.Visible = False
 
   ' Change the properites back to their old configuration.
   Me.Size = New Size(300, 266)
   grpStyleSelect.Size = New Size(192, 100)
End If

As before, the code checks the OS version and makes a decision about whether it can use the new features. If not, the form is configured to allow for the older wallpaper setup.

This is a good stopping point for this week. Next week you’ll see how to finish this particular update. You’ll need to modify the underlying code to allow use of the new settings. However, there are a few surprises in this area—things you have to discover by looking at the registry. In the meantime, let me know if you have any questions about this part of the change at John@JohnMuellerBooks.com. You can see the next post in this series at Improving the GrabAPicture Application (Part 4).

 

Improving the GrabAPicture Application (Part 2)

Just before the year ended, I discussed some additions you might want to make to the GrabAPicture application in the Improving the GrabAPicture Application (Part 1) post. One of those additions is allowing the user to modify the application configuration from the command line, rather than use the GUI to do it. Configuration modification is a common application requirement and any application that administrators have to support should have a command line option to make these modifications. You don’t have to provide access to absolutely every configuration option, but you should provide access to the most common configuration options. The options you support in your application is determined by:

 

  • Administrator configuration needs: The administrator may be able to provide input on precisely which tasks the application will need to perform remotely. Remote tasks are usually good candidates for command line support.

  • Application complexity: When working with a simple application, you have the luxury of including all of the configuration options at the command line. As applications become more complex, you must pick and choose command line features. Using multiple levels of command line switches also becomes a requirement. As an example, look at the WMIC command line utility.
  • Application type: The tasks that the application performs partially dictate the command line interface. An application designed for management tasks (such as WMIC) will require a more robust command line than one used for localized data handling (such as Notepad).


For the GrabAPicture application, the main consideration is application complexity. There are only two configuration settings, so the command line will support both of them. Of course, you have multiple options for adding the command line switches. Because the interface is relatively simple, we’ll change the command line arguments so they appear like this:

GrabAPicture
GrabAPicture /?
GrabAPicture /Random
GrabAPicture /Picture:<Picture URI> [/Style:Stretched | Centered | Tiled]
GrabAPicture /Configure [/Local:[True | False]] [/Remote:[True | False]]

The new option, /Configure, displays the current configuration when used alone. Adding the /Local command line switch changes the Use Random Local Sources setting, while adding the /Remote command line switch changes the Use Random Remote Sources setting.

At this point, you can add the required functionality. Modifying the code is a three-step process:

 

  • Change the help information.
  • Add the configuration code.
  • Add the frmMain-specific code required to perform tasks.


With these steps in mind, lets begin with the help information. The following listing shows the updated help code in bold.

' User is requesting help.
If CmdArg = "/?" Then
   ' Create an output string.
   Dim Output As New StringBuilder
 
   ' Create the usage instructions.
   Output.Append("Usage:" + vbCrLf)
   Output.Append("GrabAPicture" + vbCrLf)
   Output.Append("GrabAPicture /?" + vbCrLf)
   Output.Append("GrabAPicture /Random" + vbCrLf)
   Output.Append("GrabAPicture /Picture:<Picture URI> " + _
                 "[/Style:Stretched | Centered | Tiled]" + _
                 vbCrLf)
   Output.Append("GrabAPicture /Configure " + _
                 "[/Local:[True | False]] " + _
                 "[/Remote:[True | False]]" + vbCrLf + vbCrLf)
   Output.Append("When used alone, displays the configuration " + _
                 "interface" + vbCrLf)
   Output.Append("/? - Displays this help message." + vbCrLf)
   Output.Append("/Random - Displays one of the images provided as " + _
                 "random input through the configuration program." + _
                 vbCrLf)
   Output.Append("/Picture - Defines a local or Web location for a " + _
                 "picture." + vbCrLf)
   Output.Append("/Style - Determines the display style of the " + _
                 "picture. Use only with the /Picture switch." + vbCrLf)
   Output.Append("/Configure - Provides a means of discovering the " + _
                 "configuration when used alone or modifying the " + _
                 "configuration when used with the /Local and/or " + _
                 "/Remote command line switches." + vbCrLf)
   Output.Append("/Local - Sets the application to use local random " + _
                 "sources when set to True." + vbCrLf)
   Output.Append("/Remote - Sets the application to use remote random " + _
                 "sources when set to True." + vbCrLf)
 
   ' Display the help.
   MessageBox.Show(Output.ToString(),
                   "GrabAPicture Usage",
                   MessageBoxButtons.OK,
                   MessageBoxIcon.Information)
   Return 0
End If

As you can see, the updated code simply adds more help information to the existing help. The new help screen looks like this:

GrabAPictureUpdate0201

Now it’s time to add the code required to make the new switches work. This code appears directly before the /Picture command line switch code (the line that begins If CmdArg.Length > 8 Then in the Main() function):

' Work with the application configuration.
If CmdArg.ToUpper() = "/CONFIGURATION" Then
 
   ' Determine whether the user wants to see just the configuration
   ' information.
   If CmdArgs.Length = 1 Then
 
      ' Obtain the current settings.
      Dim CurrentSettings As GrabAPictureSettings
      CurrentSettings = GrabAPictureSettings.LoadSettings()
 
      ' Check to make sure there are settings to use.
      If CurrentSettings Is Nothing Then
 
         ' Tell the user there are no settings to display.
         MessageBox.Show("No Settings to Show!",
                         "Configuration Error",
                         MessageBoxButtons.OK,
                         MessageBoxIcon.Error)
         Return -2
      End If
 
      ' Display the settings information.
      MessageBox.Show("Local: " + CurrentSettings.LocalChecked.ToString() + _
                      vbCrLf + "Remote: " + _
                      CurrentSettings.RemoteChecked.ToString(),
                      "GrabAPicture Configuration Settings",
                      MessageBoxButtons.OK,
                      MessageBoxIcon.Information)
      Return 0
 
      ' There must be at least one settings argument.
   Else
      ' Create a variable to hold the current argument.
      Dim SettingArg As String()
 
      ' Create a varible to hold the new value.
      Dim NewValue As Boolean
 
      ' Obtain the current settings.
      Dim CurrentSettings As GrabAPictureSettings
      CurrentSettings = GrabAPictureSettings.LoadSettings()
 
      ' If there aren't any settings.
      If CurrentSettings Is Nothing Then
 
         ' Define default settings.
         CurrentSettings = New GrabAPictureSettings
      End If
 
      ' Process each settings argument in turn.
      For ArgNum As Int32 = 1 To CmdArgs.Length - 1
 
         ' Obtain the switch and argument.
         SettingArg = CmdArgs(ArgNum).Split(":")
 
         ' Verify that the user has actually included a value.
         If Not SettingArg.Length = 2 Then
            MessageBox.Show("You must provide the /Local or /Remote " + _
                            "command line switch followed by a colon (:), " + _
                            "followed by either True or False, such " + _
                            "as /Local:True or /Remote:True",
                            "Argument Error",
                            MessageBoxButtons.OK,
                            MessageBoxIcon.Error)
            Return -3
         End If
 
         ' Check for a local setting.
         If SettingArg(0).ToUpper() = "/LOCAL" Then
 
            ' Check the setting.
            If Boolean.TryParse(SettingArg(1), NewValue) Then
 
               ' Change the setting.
               CurrentSettings.LocalChecked = NewValue
            End If
 
            ' Save the setting.
            GrabAPictureSettings.SaveSettings(CurrentSettings)
         End If
 
         ' Check for a remote setting.
         If SettingArg(0).ToUpper() = "/REMOTE" Then
 
            ' Check the setting.
            If Boolean.TryParse(SettingArg(1), NewValue) Then
 
               ' Change the setting.
               CurrentSettings.RemoteChecked = NewValue
            End If
 
            ' Save the setting.
            GrabAPictureSettings.SaveSettings(CurrentSettings)
         End If
      Next
   End If
 
   Return 0
End If

This code begins by checking for the /Configuration command line switch. If the user has included it, then the code checks the number of command line arguments. When there is just one argument, the user has used the /Configuration command line switch alone, so the application displays the current configuration information. In order to perform this task, the code must create a GrabAPictureSettings object, CurrentSettings, and load the settings into it. At this point, displaying the information is simply a matter of accessing the current information in CurrentSettings. Make sure you convert the Boolean values to strings.

Setting the configuration is only a little harder. The first thing to consider is that you don’t know whether the user has included just the /Local or /Remote command line switches, or both of them. In addition, you have no idea of which order the user has used for the switches. As a consequence, this application relies on a For loop that doesn’t assume anything. It simply grabs the first switch and then processes it.

The command line switch is supposed to be followed by a colon (:) and then a value. Unfortunately, the user could provide anything as input. The code begins by verifying that the user has provided a switch and value combination (or at least the right number of arguments). It then checks for either /Local or /Remote. The value could still be incorrect, so the application uses the Boolean.TryParse() method to convert the input to a Boolean value safely. If everything works out correctly, the application makes the setting change and saves the result to disk by calling GrabAPictureSettings.SaveSettings(CurrentSettings).

With these changes, the application can now set the configuration from the command line. Next week you’ll see another new addition. In the meantime, let me know if you have any questions about this update at John@JohnMuellerBooks.com.

Improving the GrabAPicture Application (Part 1)

If you haven’t read about the GrabAPicture application yet, you really need to do so before we embark on the update described in the upcoming series of posts. You can find a list of these posts using a simple search. The GrabAPicture application covers a lot of ground and shows how to perform a wide range of tasks using VB.NET. You’ll have to read the posts to see everything it does, but you’ll gain an understanding of working at the command line, working with Windows API using P/Invoke, and creating a solution that has both command line interface and GUI. You’ll even see how to create a shortcut to ask GrabAPicture to perform tasks automatically.

It isn’t often that I can even demonstrate a program this large in a book, so this series of entries has given me a chance to do something a little different. Being able to interact with some of you while I’ve been writing the posts has also been a new experience. So, this series of blog entries has been interesting for me too and I plan to do others in the future. The next application might rely on C#, but whatever language it uses, it’ll be interesting.

As capable as GrabAPicture is, it has some limitations that you might want to address. That’s the purpose of the series of posts that follow. Now, you can extend GrabAPicture in any number of desirable ways, but let’s start with these ideas:

 

  • Add configuration options to the command line.
  • Make it possible to configure wallpaper to use the new Windows 7 settings.
  • Add a browse button to the main form.
  • Provide a snapshot of the highlighted image.


These ideas would make a good starting point, but I’m open to any ideas you might have. If you’ve had a chance to play with the application a bit, you’ve probably noticed some deficiencies. I can’t guarantee I’ll correct them all, but I’ll give every deficiency you note some thought to determine whether a fix is doable. Let me know what you’d like to see as additions at John@JohnMuellerBooks.com. You can find the next post in this series at Improving the GrabAPicture Application (Part 2).

Exploring the GrabAPicture Application (Part 12)

We’re finally down to the last baseline post for the GrabAPicture application, frmAddEdit, which is the chameleon form. The Exploring the GrabAPicture Application (Part 11) post describes how the frmConfigure code modifies the appearance of the code for this form so it can perform several tasks. At the center of this capability are several versions of the constructor that allow for different configuration techniques as shown in the following code.

Public Sub New()
   MyBase.New()
 
   'This call is required by the Windows Form Designer.
   InitializeComponent()
 
   'Add any initialization after the InitializeComponent() call
 
End Sub
 
Public Sub New(ByVal Title As String)
   MyBase.New()
 
   'This call is required by the Windows Form Designer.
   InitializeComponent()
 
   'Change the title.
   Me.Text = Title
End Sub
 
Public Sub New(ByVal Title As String, _
               ByVal AddButtonTitle As String)
   MyBase.New()
 
   'This call is required by the Windows Form Designer.
   InitializeComponent()
 
   'Change the title.
   Me.Text = Title
 
   ' Change the add/edit button title.
   btnAddEdit.Text = AddButtonTitle
End Sub
 
Public Sub New(ByVal Title As String, _
               ByVal AddButtonTitle As String, _
               ByVal RemoteEdit As Boolean)
   MyBase.New()
 
   ' This call is required by the Windows Form Designer.
   InitializeComponent()
 
   ' Change the title.
   Me.Text = Title
 
   ' Change the add/edit button title.
   btnAddEdit.Text = AddButtonTitle
 
   ' Remove the directory button and resize the
   ' location field as needed.
   If RemoteEdit Then
      btnGetResource.Visible = False
      btnGetResource.Enabled = False
      txtLocation.Size = txtName.Size
      _IsRemote = True
   End If
End Sub

As you can see, each of the constructors provides one more level of configuration. You might wonder why the application doesn’t simply use the most configurable of the four constructors, but this approach has some disadvantages.

 

  • Using just a single, complex constructor makes the application inflexible.
  • The developer would also need to provide the default values for title, button, and remote editing capability, even if these values didn’t change from the default dialog box.
  • Adding this requirement would make the application more susceptible to errors.
  • Using four different constructors also forces the developer to consider which entries actually do need to change. Giving someone the opportunity to think application design through is never a bad idea.

Notice that the fourth constructor contains special code for removing the browse button from view when working with a remote resource. The browse button wouldn’t do the user much good in this case and clicking it can only cause problems.

Using the custom constructors is really important in this case and makes the application much stronger. You can divide the rest of the code for this form into three areas:

 


  • Property creation and maintenance

  • Form loading

  • User controls

Most of the code for this form is straightforward, albeit a bit convoluted at time due to the form’s multipurpose nature. The following sections describe each of the coding areas.

Property Creation and Maintenance

This form is used in a number of ways, so it’s important that the application be able to track the form’s current state. Otherwise, the application could make an assumption about the form content that isn’t correct. The best way to provide state information outside of the form is to use properties. Relying on properties makes it possible to monitor and control interaction with form state information in a controlled way. The application uses the properties shown here.

' Private variables used for editing purposes.
Private _Resources As LocalUri()
Private _RemoteSources As RemoteUri()
Private _IsRemote As Boolean = False
Private _IsEdit As Boolean = False
Private _RecordNumber As Int32 = 0
 
' Tracks the current list of local resources.
Public Property Resources() As LocalUri()
   Get
      Return _Resources
   End Get
   Set(ByVal Value As LocalUri())
      _Resources = Value
   End Set
End Property
 
' Tracks the current list of remote resources
Public Property RemoteSources() As RemoteUri()
   Get
      Return _RemoteSources
   End Get
   Set(ByVal Value As RemoteUri())
      _RemoteSources = Value
   End Set
End Property
 
' Shows whether edit is for a local or remote resource.
Public ReadOnly Property IsRemote() As Boolean
   Get
      Return _IsRemote
   End Get
End Property
 
' Determines whether this is an add or edit action.
Public Property IsEdit() As Boolean
   Get
      Return _IsEdit
   End Get
   Set(ByVal Value As Boolean)
      _IsEdit = Value
   End Set
End Property
 
' Defines the edited record.
Public Property RecordNumber() As Int32
   Get
      Return _RecordNumber
   End Get
   Set(ByVal Value As Int32)
      ' Verify the input is positive.
      If Value >= 0 Then
 
         ' Perform separate checks for local and remote.
         If _IsRemote Then
 
            ' Verify the input is less than the number of
            ' records when using a remote resource. This
            ' check will also throw a null reference
            ' exception if the record source isn't
            ' properly initialized.
            If Value >= _RemoteSources.Length Then
 
               Throw New ArgumentOutOfRangeException( _
                  "Record Number", _
                  "Record number is too high.")
            End If
         Else
 
            ' Verify the input is less than the number of
            ' records when using a local resource. This
            ' check will also throw a null reference
            ' exception if the record source isn't
            ' properly initialized.
            If Value >= _Resources.Length Then
 
               Throw New ArgumentOutOfRangeException( _
                  "Record Number", _
                  "Local record number is too high.")
            End If
         End If
 
         ' Set the record number.
         _RecordNumber = Value
      Else
         Throw New ArgumentOutOfRangeException( _
            "Record Number", _
            "Provide a positive record number.")
      End If
   End Set
End Property

The properties are pretty straightforward. They serve the following purposes:

 


  • Resources: Defines a local URI.

  • RemoteSources: Defines a remote URI.

  • IsRemote: A read-only property that the application can use to determine whether to access the Resources or RemoteSources property for the current URI.
  • IsEdit: Controls whether the dialog box provides an adding or editing appearance. The adding dialog box is blank, while the editing dialog box contains the current local or remote record information.
  • RecordNumber: Contains the record number of the current local or remote source.

Form Loading

Form loading is important because the code in this area controls the initial appearance of the dialog box. A form used for adding a new record, whether local or remote, is always blank, so it doesn’t require any custom configuration. However, if the form is used to edit a current record, the form loading code must detect whether the form is editing a local or remote resource. In addition to URI differences, local resources have an additional button available for browsing the local hard drive. The following code shows how form loading works.

Private Sub frmAddEdit_Load(ByVal sender As Object, _
                            ByVal e As System.EventArgs) _
                         Handles MyBase.Load
   ' Set the form up for editing.
   If _IsEdit Then
      ' Make sure you use the correct database.
      If _IsRemote Then
         txtName.Text = _RemoteSources(_RecordNumber).Name
         txtLocation.Text = _RemoteSources(_RecordNumber).Location
         Select Case _RemoteSources(_RecordNumber).Style
            Case Styles.Stretched
               rbStretched.Checked = True
               rbCentered.Checked = False
               rbTiled.Checked = False
            Case Styles.Centered
               rbStretched.Checked = False
               rbCentered.Checked = True
               rbTiled.Checked = False
            Case Styles.Tiled
               rbStretched.Checked = False
               rbCentered.Checked = False
               rbTiled.Checked = True
         End Select
      Else
         txtName.Text = _Resources(_RecordNumber).Name
         txtLocation.Text = _Resources(_RecordNumber).Location
         Select Case _Resources(_RecordNumber).Style
            Case Styles.Stretched
               rbStretched.Checked = True
               rbCentered.Checked = False
               rbTiled.Checked = False
            Case Styles.Centered
               rbStretched.Checked = False
               rbCentered.Checked = True
               rbTiled.Checked = False
            Case Styles.Tiled
               rbStretched.Checked = False
               rbCentered.Checked = False
               rbTiled.Checked = True
         End Select
      End If
   End If
End Sub

As you can see, the first thing the code detect is whether the form is using for adding. If it is, then the application doesn’t do any configuration.

When the dialog box is used for editing, the application checks the status of _IsRemote to determine how to configure the dialog box. The code obtains the correct record and then displays the information on screen.

User Controls

There are three user controls, but you only need to provide code for two of them (btnCancel provides the default action when clicked). The Add/Edit button (btnAddEdit) is always visible. The relies on it to make any changes permanent. The browse button (btnGetResource) is only available when working with a local resource. The user relies on this button to locate resources on the local hard drive. The following code shows how these buttons work.

Private Sub btnAddEdit_Click(ByVal sender As System.Object, _
                             ByVal e As System.EventArgs) _
                          Handles btnAddEdit.Click
   If IsEdit Then
      If _IsRemote Then
         ' Change the array data.
         _RemoteSources(_RecordNumber).Name = txtName.Text
         _RemoteSources(_RecordNumber).Location = txtLocation.Text
         If rbStretched.Checked Then
            _RemoteSources(_RecordNumber).Style = Styles.Stretched
         End If
         If rbCentered.Checked Then
            _RemoteSources(_RecordNumber).Style = Styles.Centered
         End If
         If rbTiled.Checked Then
            _RemoteSources(_RecordNumber).Style = Styles.Tiled
         End If
      Else
         ' Change the array data.
         _Resources(_RecordNumber).Name = txtName.Text
         _Resources(_RecordNumber).Location = txtLocation.Text
         If rbStretched.Checked Then
            _Resources(_RecordNumber).Style = Styles.Stretched
         End If
         If rbCentered.Checked Then
            _Resources(_RecordNumber).Style = Styles.Centered
         End If
         If rbTiled.Checked Then
            _Resources(_RecordNumber).Style = Styles.Tiled
         End If
      End If
   Else
      If _IsRemote Then
         ' Add a new array element.
         If _RemoteSources Is Nothing Then
            ReDim _RemoteSources(0)
         Else
            ReDim Preserve _RemoteSources(_RemoteSources.Length)
         End If
 
         ' Instantiate a new array item.
         _RemoteSources(_RemoteSources.Length - 1) = New RemoteUri
 
         ' Add the data to the array.
         _RemoteSources(_RemoteSources.Length - 1).Name = txtName.Text
         _RemoteSources(_RemoteSources.Length - 1).Location = txtLocation.Text
         If rbStretched.Checked Then
            _RemoteSources(_RemoteSources.Length - 1).Style = Styles.Stretched
         End If
         If rbCentered.Checked Then
            _RemoteSources(_RemoteSources.Length - 1).Style = Styles.Centered
         End If
         If rbTiled.Checked Then
            _RemoteSources(_RemoteSources.Length - 1).Style = Styles.Tiled
         End If
      Else
         ' Add a new array element.
         If _Resources Is Nothing Then
            ReDim _Resources(0)
         Else
            ReDim Preserve _Resources(_Resources.Length)
         End If
 
         ' Instantiate a new array item.
         _Resources(_Resources.Length - 1) = New LocalUri
 
         ' Add the data to the array.
         _Resources(_Resources.Length - 1).Name = txtName.Text
         _Resources(_Resources.Length - 1).Location = txtLocation.Text
         If rbStretched.Checked Then
            _Resources(_Resources.Length - 1).Style = Styles.Stretched
         End If
         If rbCentered.Checked Then
            _Resources(_Resources.Length - 1).Style = Styles.Centered
         End If
         If rbTiled.Checked Then
            _Resources(_Resources.Length - 1).Style = Styles.Tiled
         End If
      End If
   End If
End Sub
 
Private Sub btnGetResource_Click(ByVal sender As System.Object, _
                                 ByVal e As System.EventArgs) _
                              Handles btnGetResource.Click
   If OFD.ShowDialog() = DialogResult.OK Then
      txtLocation.Text = OFD.FileName
   End If
End Sub

Let’s discuss the simple button first. When the user clicks the browse button, the application displays an OpenFileDialog control, OFD. If the user selects a file and clicks OK, the code places the name of the file into txtLocation.Text. Theoretically, a little customization would make this dialog box friendlier, but it works well as presented.

The btnAddEdit_Click() event handler is a little more complex. It actually performs one of four actions depending on the dialog box options.

 

  • Edit/Remote: The code changes the record information found in _RemoteSources for the current record pointed at by _RecordNumber.
  • Edit/Local: The code changes the record information found in _Resources for the current record pointed at by _RecordNumber.
  • Add/Remote: The code either creates a new _RemoteSources array or adds an element to the existing _RemoteSources array. In both cases, the code adds the new record to the end of the array. Note that because the array is zero-based, you must subtract 1 from the array length in order to find the last record of the new array.
  • Add/Local: The
    code either creates a new _Resources array or adds an element to
    the existing _Resources array. In both cases, the code adds the new
    record to the end of the array.

Well, now you have the full story on the base application—the application I started with. The next post will discuss some of the additions we’ll pursue in the new year. For now, please keep those comments coming. Let me know if you have any questions at all about this form at John@JohnMuellerBooks.com.

 

Exploring the GrabAPicture Application (Part 11)

The previous post in this series, Exploring the GrabAPicture Application (Part 10), discussed frmMain, which allows the user to perform application configuration and manually configure the Desktop wallpaper. The focus of this application, however, is automated configuration, which is the purview of frmConfigure—the topic of discussion today. The user relies on frmConfigure to add, edit, delete, and select either local or remote graphic sources for the Desktop wallpaper as shown here.

GrabAPicture1101

There are really two sets of four buttons here: the local set and the remote set. Each set performs the same sets of tasks with a different part of the same XML database. The information about this database appears in previous posts, but the main post of concern is Exploring the GrabAPicture Application (Part 8). When adding or editing a database element, the user sees a form similar to the one shown here:

GrabAPicture1102

Even though frmConfigure looks a little complex, there are really only three groups of tasks to perform:

 

  • Load the current database
  • Manage local database resources
  • Manage remote database resources

Load the Current Database

The first task is to load the current database. In this case, the application creates and configures a global variable that provides access to the preconfigured wallpaper choices as shown here.

' Current application settings.
Dim CurrentSettings As GrabAPictureSettings
 
Private Sub frmConfigure_Load(ByVal sender As Object, _
                              ByVal e As System.EventArgs) _
                              Handles MyBase.Load
   ' Initialize the stored settings.
   CurrentSettings = GrabAPictureSettings.LoadSettings()
 
   ' Configure the controls.
   If Not CurrentSettings Is Nothing Then
      If Not CurrentSettings.LocalUris Is Nothing Then
         For Each Item As LocalUri In CurrentSettings.LocalUris
            lstLocal.Items.Add(Item.Name)
         Next
      End If
      If Not CurrentSettings.RemoteUris Is Nothing Then
         For Each Item As RemoteUri In CurrentSettings.RemoteUris
            lstRemote.Items.Add(Item.Name)
         Next
      End If
   Else
      ' Define default settings.
      CurrentSettings = New GrabAPictureSettings
   End If
End Sub

The global variable, CurrentSettings, is of type GrabAPictureSettings. The application begins trying to fill CurrentSettings by calling GrabAPictureSettings.LoadSettings(). When this is the first application use and the user hasn’t added any wallpaper yet, the result of this call is that CurrentSettings equals Nothing, so the application creates a new database by creating a new GrabAPictureSettings database.

When there are settings to use, the application must determine which settings are available. The database may only contain local or remote settings, but not both. The next step determines whether CurrentSettings.LocalUris contains Nothing. If there are local settings, the application places the name of each of these entries in the lstLocal list box. The application performs a similar task for the remote settings. As a result, the user sees the names of all of the available local and remote wallpaper sources when the application dialog first appears on screen.

Manage Local Database Resources

After the form appears on screen, the user sees the local resources at the top of the form in the Local Sources list. The user can add, edit, delete, or select local resources using the associated controls as shown here:

Private Sub btnLAdd_Click(ByVal sender As System.Object, _
                          ByVal e As System.EventArgs) _
                       Handles btnLAdd.Click
   ' Create a form to add the record.
   Dim AddItem As New frmAddEdit
 
   ' Set the resources when available.
   If Not CurrentSettings.LocalUris Is Nothing Then
      AddItem.Resources = CurrentSettings.LocalUris
   End If
 
   ' If the user makes changes, add the information to
   ' the settings database and display it on screen.
   If AddItem.ShowDialog(Me) = DialogResult.OK Then
      CurrentSettings.LocalUris = AddItem.Resources
      GrabAPictureSettings.SaveSettings(CurrentSettings)
      lstLocal.Items.Add(AddItem.Resources(AddItem.Resources.Length - 1).Name)
   End If
End Sub
 
Private Sub btnLEdit_Click(ByVal sender As System.Object, _
                           ByVal e As System.EventArgs) _
                        Handles btnLEdit.Click
   ' Make sure the user has selected an entry.
   If lstLocal.SelectedIndex = -1 Then
      MessageBox.Show("Select an entry to edit.")
      Return
   End If
 
   ' Create a new form for editing the data.
   Dim EditItem As New frmAddEdit("Edit Local Resource", "&Edit")
   EditItem.IsEdit = True
 
   ' Make sure there is data to edit.
   If CurrentSettings.LocalUris Is Nothing Then
      MessageBox.Show("No Records to Edit")
      Return
   Else
      ' Provide the records and choose the one to edit.
      EditItem.Resources = CurrentSettings.LocalUris
      EditItem.RecordNumber = lstLocal.SelectedIndex
   End If
 
   ' If the user changes the data, update the settings
   ' database and show changes on screen.
   If EditItem.ShowDialog(Me) = DialogResult.OK Then
      CurrentSettings.LocalUris = EditItem.Resources
      GrabAPictureSettings.SaveSettings(CurrentSettings)
      lstLocal.Items(lstLocal.SelectedIndex) = _
         EditItem.Resources(lstLocal.SelectedIndex).Name
   End If
End Sub
 
Private Sub btnLDelete_Click(ByVal sender As System.Object, _
                             ByVal e As System.EventArgs) _
                             Handles btnLDelete.Click
   ' Make sure the user has selected an entry.
   If lstLocal.SelectedIndex = -1 Then
      MessageBox.Show("Select an entry to remove.")
      Return
   End If
 
   ' Create a temporary record array.
   Dim Temp(CurrentSettings.LocalUris.Length - 2) As LocalUri
 
   ' Remove the requested record.
   Dim Count As Int32 = 0
   For Each Item As LocalUri In CurrentSettings.LocalUris
 
      ' Add the record if it doesn't match the target.
      If Not Item.Name = lstLocal.Items(lstLocal.SelectedIndex) Then
         Temp(Count) = Item
         Count += 1
      End If
   Next
 
   ' Update the settings.
   CurrentSettings.LocalUris = Temp
   GrabAPictureSettings.SaveSettings(CurrentSettings)
 
   ' Update the display.
   lstLocal.Items.RemoveAt(lstLocal.SelectedIndex)
End Sub
 
Private Sub btnLSelect_Click(ByVal sender As System.Object, _
                             ByVal e As System.EventArgs) _
                          Handles btnLSelect.Click
   ' Wallpaper manipulation class.
   Dim Wallpaper As New WinWallpaper
 
   ' Choose the wallpaper style.
   Select Case CurrentSettings.LocalUris(lstLocal.SelectedIndex).Style
      Case WinWallpaper.Styles.Stretched
         Wallpaper.Style = WinWallpaper.Styles.Stretched
      Case WinWallpaper.Styles.Centered
         Wallpaper.Style = WinWallpaper.Styles.Centered
      Case WinWallpaper.Styles.Tiled
         Wallpaper.Style = WinWallpaper.Styles.Tiled
   End Select
 
   ' Create a wallpaper URI.
   Dim NewUri As New Uri( _
      CurrentSettings.LocalUris(lstLocal.SelectedIndex).Location)
 
   ' Set the wallpaper location.
   Wallpaper.WallpaperURI = NewUri
End Sub

Let’s look at each of these event handlers in turn. The btnLAdd_Click() event handler is the simplest of the group. It begins by creating a new frmAddEdit using the default constructor. When there are local resources to provide in CurrentSettings.LocalUris, the program supplies them to the AddItem.Resources property. At this point, the application displays the dialog box. If the user clicks Add, rather than Cancel, in the Add Local Resource dialog box (which produces a return value of DialogResult.OK), the application obtains the updated list of resources from AddItem.Resources and places them in CurrentSettings.LocalUris. The application then saves the database by calling GrabAPictureSettings.SaveSettings() with CurrentSettings. Finally, the application displays the newly added item in the Local Sources list.

The btnLEdit_Click() event handler begins by checking whether the user has actually selected an entry to edit by checking lstLocal.SelectedIndex (a value of -1 indicates no selection). If not, the application displays an error message and exits. It then creates a new frmAddEdit() using a special constructor that allows modification of the title bar text and the btnAddEdit.Text property, so that the resulting dialog box is completely customized. You’ll find that there are actually four constructors for frmAddEdit:

 

  • No modification
  • Modify the title
  • Modify both title and btnAddEdit.Text property
  • Modify both title and btnAddEdit.Text property in a remote source setting

Providing this sort of customization reduces the work you need to do and yet provides a better interface for the user. The next step is to set the EditItem.IsEdit property to True, which tells frmAddEdit to fill the fields with data to edit, rather than present a blank form. Again, this is the type of customization that will actually save you time later. (A second property, EditItem.IsRemote, determines whether the data should come from the local or remote wallpaper database—the default is to use the local database.)

The application has to make two decisions when editing a record. First, it has to check whether there is a record to edit. If there aren’t any records, the application displays a dialog box stating as much and exits. Second, it must determine which record to edit. The code passes the value in lstLocal.SelectedIndex onto EditItem.RecordNumber. As the application is currently configured, the contents of lstLocal must precisely match the contents of CurrentRecords, which means that lstLocal remains unsorted. A future update will make it possible to sort the list, if desired, but for now, the application is designed for simplicity of understanding, rather than aesthetic appeal. The remainder of the btnLEdit_Click() code works just like the btnLAdd_Click() event handler, with the result that any changes are immediately saved to the database.

Deleting a record means checking for the required record in the database by name and then removing that particular record. The btnLDelete_Click() event handler begins by checking whether the user has actually selected a record (lstLocal.SelectedIndex must equal a value other than -1). It then creates a temporary array, Temp, of type LocalUri, that will hold one record less than the current number of records. The next bit of code may look confusing at first, but all it really does is move all of the records in CurrentSettings.LocalUris that don’t have the name of the selected record as defined by lstLocal.Items(lstLocal.SelectedIndex) to Temp. The result is Temp has one less record than CurrentSettings.LocalUris when the process is finished. The code then copies the new list of records from Temp to CurrentSettings.LocalUris and saves the result to disk. The final step is to remove the name from the Local Sources list.

Selecting a specific local source is relatively straightforward. The btnLSelect_Click() event handler begins by creating Wallpaper, which is type WinWallpaper. It’s important to remember that modifying the Wallpaper object automatically changes the Desktop wallpaper. You saw how this works in the previous post and it works the same here. The application relies on the index provided by the user’s selection, lstLocal.SelectedIndex, to select the correct wallpaper in CurrentSettings.LocalUris. The application then sets the wallpaper style and provides it with a location in the form of a Uri object, NewUri. The result is that the user sees the selected wallpaper on screen.

Manage Remote Database Resources

The code for managing the remote database resources is almost precisely the same as the code for managing local database resources. In fact, the modifications are mainly cosmetic, such as changes to the frmAddEdit title and button text. However, there are a few distinct differences to note as shown here.

Private Sub btnRAdd_Click(ByVal sender As System.Object, _
                          ByVal e As System.EventArgs) _
                       Handles btnRAdd.Click
   ' Create a form to add the record.
   Dim AddItem As New frmAddEdit("Add Remote Resource", "&Add", True)
 
   ' Set the resources when available.
   If Not CurrentSettings.RemoteUris Is Nothing Then
      AddItem.RemoteSources = CurrentSettings.RemoteUris
   End If
 
   ' If the user makes changes, add the information to
   ' the settings database and display it on screen.
   If AddItem.ShowDialog(Me) = DialogResult.OK Then
      CurrentSettings.RemoteUris = AddItem.RemoteSources
      GrabAPictureSettings.SaveSettings(CurrentSettings)
      lstRemote.Items.Add( _
         AddItem.RemoteSources(AddItem.RemoteSources.Length - 1).Name)
   End If
End Sub
 
Private Sub btnEdit_Click(ByVal sender As System.Object, _
                          ByVal e As System.EventArgs) _
                       Handles btnEdit.Click
   ' Make sure the user has selected an entry.
   If lstRemote.SelectedIndex = -1 Then
      MessageBox.Show("Select an entry to edit.")
      Return
   End If
 
   ' Create a new form for editing the data.
   Dim EditItem As New frmAddEdit("Edit Remote Resource", "&Edit", True)
   EditItem.IsEdit = True
 
   ' Make sure there is data to edit.
   If CurrentSettings.RemoteUris Is Nothing Then
      MessageBox.Show("No Records to Edit")
      Return
   Else
      ' Provide the records and choose the one to edit.
      EditItem.RemoteSources = CurrentSettings.RemoteUris
      EditItem.RecordNumber = lstRemote.SelectedIndex
   End If
 
   ' If the user changes the data, update the settings
   ' database and show changes on screen.
   If EditItem.ShowDialog(Me) = DialogResult.OK Then
      CurrentSettings.RemoteUris = EditItem.RemoteSources
      GrabAPictureSettings.SaveSettings(CurrentSettings)
      lstRemote.Items(lstRemote.SelectedIndex) = _
         EditItem.RemoteSources(lstRemote.SelectedIndex).Name
   End If
End Sub
 
Private Sub btnRDelete_Click(ByVal sender As System.Object, _
                             ByVal e As System.EventArgs) _
                          Handles btnRDelete.Click
   ' Make sure the user has selected an entry.
   If lstRemote.SelectedIndex = -1 Then
      MessageBox.Show("Select an entry to remove.")
      Return
   End If
 
   ' Create a temporary record array.
   Dim Temp(CurrentSettings.RemoteUris.Length - 2) As RemoteUri
 
   ' Remove the requested record.
   Dim Count As Int32 = 0
   For Each Item As RemoteUri In CurrentSettings.RemoteUris
 
      ' Add the record if it doesn't match the target.
      If Not Item.Name = lstRemote.Items(lstRemote.SelectedIndex) Then
         Temp(Count) = Item
         Count += 1
      End If
   Next
 
   ' Update the settings.
   CurrentSettings.RemoteUris = Temp
   GrabAPictureSettings.SaveSettings(CurrentSettings)
 
   ' Update the display.
   lstRemote.Items.RemoveAt(lstRemote.SelectedIndex)
End Sub
 
Private Sub btnSelect_Click(ByVal sender As System.Object, _
                            ByVal e As System.EventArgs) _
                         Handles btnSelect.Click
   ' Wallpaper manipulation class.
   Dim Wallpaper As New WinWallpaper
 
   ' Choose the wallpaper style.
   Select Case CurrentSettings.RemoteUris(lstRemote.SelectedIndex).Style
      Case WinWallpaper.Styles.Stretched
         Wallpaper.Style = WinWallpaper.Styles.Stretched
      Case WinWallpaper.Styles.Centered
         Wallpaper.Style = WinWallpaper.Styles.Centered
      Case WinWallpaper.Styles.Tiled
         Wallpaper.Style = WinWallpaper.Styles.Tiled
   End Select
 
   ' Create a wallpaper URI.
   Dim NewUri As New Uri( _
      CurrentSettings.RemoteUris(lstRemote.SelectedIndex).Location)
 
   ' Set the wallpaper location.
   Wallpaper.WallpaperURI = NewUri
End Sub

Of course, everything is done using CurrentSettings.RemoteUris in this case because you’re working with the remote settings. In addition, every one of the dialog boxes uses the most complex frmAddEdit constructor, which configures the application for a remote source. I’ll leave it to you to explore this code based on the conversation of the local source workings.

Well, that’s it for frmConfigure. The next post will complete the basic application. We’ll look at how frmAddEdit works. In the meantime, feel free to contact me about any questions at John@JohnMuellerBooks.com. I’ll also start entertaining some additions to this basic application. I have a few in mind, but I’d love to hear your thoughts on the subject. You can find the last post in this series at: Exploring the GrabAPicture Application (Part 12).

Exploring the GrabAPicture Application (Part 10)

In the previous post, Exploring the GrabAPicture Application (Part 9), I discussed the command line interface for the GrabAPicture application. This week we begin looking at the GUI controls for frmMain, which is the first form the user sees. Here’s how this form appears when you’re using it.

GrabAPicture1001

It’s important to analyze the functionality of this interface. You can divide the tasks that it performs into the following areas:

 

  • Dialog Box Management (dialog box configuration and Close button)
  • Direct Wallpaper Access (Wallpaper Location text box, radio buttons in the Style Selection group, and Set Value button)
  • Wallpaper Database Access (Random Sources button)
  • Command Line Interface Configuration (Use Random Local Sources and Use Random Remote Sources check boxes)

It’s helpful to perform such an analysis of any interface you design. Doing so makes it possible to look for ways to make the application more efficient (by reducing the number of controls when possible) and easier to use (by placing some controls on another form). Theoretically, you could make this application more efficient by making the Command Line Interface Configuration part of the command line interface. Likewise, you could make it easier to use by placing the Direct Wallpaper Access controls on a separate form. However, the form is relatively simple as is and most people won’t have trouble using it. This post discusses all four areas in the order shown.

Dialog Box Management

Part of the dialog box management task is to configure the form when you first start the application. This application, like most applications out there, uses the Load event to perform the task. In this case, it means using the frmMain_Load() handler shown here.

' Current application settings.
Dim CurrentSettings As GrabAPictureSettings
 
Private Sub frmMain_Load(ByVal sender As Object, _
                         ByVal e As System.EventArgs) _
                         Handles MyBase.Load
 
   ' Create an instance of the WinWallpaper class to gain
   ' access to the current Windows settings.
   Dim Wallpaper As New WinWallpaper
 
   ' Get the current wallpaper URI and display it.
   txtWallpaperURI.Text = _
      Wallpaper.WallpaperURI.ToString()
 
   ' Get the current wallpaper style.
   Select Case Wallpaper.Style
      Case WinWallpaper.Styles.Stretched
         rbStretched.Checked = True
         rbCentered.Checked = False
         rbTiled.Checked = False
      Case WinWallpaper.Styles.Centered
         rbStretched.Checked = False
         rbCentered.Checked = True
         rbTiled.Checked = False
      Case WinWallpaper.Styles.Tiled
         rbStretched.Checked = False
         rbCentered.Checked = False
         rbTiled.Checked = True
   End Select
 
   ' Initialize the stored settings.
   CurrentSettings = GrabAPictureSettings.LoadSettings()
 
   ' Configure the controls.
   If Not CurrentSettings Is Nothing Then
      cbLocal.Checked = CurrentSettings.LocalChecked
      cbRemote.Checked = CurrentSettings.RemoteChecked
   Else
      ' Define default settings.
      CurrentSettings = New GrabAPictureSettings
   End If
End Sub

The code begins by creating a global GrabAPictureSettings object, CurrentSettings, that contains the content of the user’s wallpaper database. Normally, I prefer not to use global variables because they’re prone to all sorts of problems, especially when the code your working with could be multithreaded (reentrancy problems are extremely tough to debug). However, using the global variable in this case shouldn’t be a problem.

The frmMain_Load() method begins by creating creating a WinWallpaper object, Wallpaper. The reason you must create this variable every time you need it is because external applications can indeed change the wallpaper. If you didn’t create these variable every time, the user could end up using old information. Admittedly, this wouldn’t be much of a problem when it comes to wallpaper, but it’s good practice to follow in every application where an external application could affect your application’s data. The code then uses Wallpaper to set the Wallpaper Location and Style Selection settings of the application.

At this point, the code instantiates CurrentSettings. You know from the Exploring the GrabAPicture Application (Part 8) post that the LoadSettings() method returns Nothing when the user hasn’t saved any settings before, which is always the case when the user starts the application for the first time. This code checks whether CurrentSettings is Nothing after the LoadSettings() call. If so, the application creates an entirely new GrabAPictureSettings object. Otherwise, it uses the existing settings to configure the Use Random Local Sources and Use Random Remote Sources check boxes.

The configuration process is complete. The only remaining task is to provide a means of ending the application. Clicking Close, which is btnCancel, performs this task. Here is the code used to accomplish this task.

Private Sub btnCancel_Click(ByVal sender As System.Object, _
                            ByVal e As System.EventArgs) _
                         Handles btnCancel.Click
   ' Exit with the Cancel code.
   Environment.Exit(DialogResult.Cancel)
End Sub

Notice that this application doesn’t simply close the form. Instead, it calls Environment.Exit() with an ending value of DialogResult.Cancel. The purpose for this approach is that GrabAPicture is designed for use in a batch file. Any batch file you create can rely on the ErrorLevel value to detect how the application exited (see my Understanding the Connection Between Application Output and ErrorLevel post for details), so you must provide a value to detect. It’s good practice not to simply close the form anyway—you should always provide some sort of exit value.

Direct Wallpaper Access

This application provides the means for the user to simply type the full path to a piece of wallpaper, set the style, and then click Set Value to use it. In short, the user could decide not to use any of the random option at all. There is a command line interface feature to allow the user to work this way as well. Here is the code used to provide direct wallpaper access.

Private Sub rbStretched_CheckedChanged(ByVal sender As System.Object, _
                                       ByVal e As System.EventArgs) _
                                    Handles rbStretched.CheckedChanged
   Dim Wallpaper As New WinWallpaper
 
   ' Set the new style.
   Wallpaper.Style = WinWallpaper.Styles.Stretched
End Sub
 
Private Sub rbCentered_CheckedChanged(ByVal sender As System.Object, _
                                      ByVal e As System.EventArgs) _
                                   Handles rbCentered.CheckedChanged
   Dim Wallpaper As New WinWallpaper
 
   ' Set the new style.
   Wallpaper.Style = WinWallpaper.Styles.Centered
End Sub
 
Private Sub rbTiled_CheckedChanged(ByVal sender As System.Object, _
                                   ByVal e As System.EventArgs) _
                                Handles rbTiled.CheckedChanged
   Dim Wallpaper As New WinWallpaper
 
   ' Set the new style.
   Wallpaper.Style = WinWallpaper.Styles.Tiled
End Sub
 
Private Sub btnOK_Click(ByVal sender As System.Object, _
                        ByVal e As System.EventArgs) _
                        Handles btnOK.Click
   Dim Wallpaper As New WinWallpaper
 
   ' Set the wallpaper URI.
   Try
      Wallpaper.WallpaperURI = New Uri(txtWallpaperURI.Text)
   Catch UFE As UriFormatException
      MessageBox.Show("Provide a valid URI!" & vbCrLf & _
                      UFE.Message)
   Catch ex As Exception
      MessageBox.Show(ex.Message)
   End Try
 
   ' Exit with the OK code.
   'Environment.Exit(DialogResult.OK)
End Sub

Each of the check box event handlers performs essentially the same task. They set the Wallpaper.Style property to the appropriate Wallpaper.Styles value. A possible enhancement to these event handlers would be to force them to make the change automatically. The user still has to click Set Value to make the change. After testing this example out with a few willing volunteers, I found it was less confusing if the application waited for the user to click Set Value, rather than have the change occur automatically. The strategy you pursue will likely depend on your users and the complexity of the application you create.

Clicking Set Value calls the btnOK_Click() event handler. In this case, the code performs a conversion of the text the user types into Wallpaper Location to a Uri object. This is the first potential source of an exception. If the text doesn’t form a useful URI, then the application generates a UriFormatException. The assignment could possibly generate other exceptions, so the code adds a general exception handler as well. The act of making the assignment changes the Desktop wallpaper.

This example used to close the dialog box automatically after the user clicked Set Value. A few testers complained about this practice—preferring to experiment with the wallpaper. Consequently, the current version of the example has this piece of code commented out. If you choose to enable the old method of doing things, you’ll want to note that the application will exit with a value of DialogResult.OK in this case, which you can easily trap using the ErrorLevel value in a batch file.

Wallpaper Database Access

Wallpaper database configuration is performed using a combination of frmConfigure (to display the database content) and frmAddEdit (to make changes). When the user clicks Random Sources, the application calls btnConfigure_Click(). This event handler displays frmConfigure so that the user can see the list of configured wallpapers and make changes to the list. The following code shows how to perform this task.

Private Sub btnConfigure_Click(ByVal sender As System.Object, _
                               ByVal e As System.EventArgs) _
                               Handles btnConfigure.Click
   ' Create a new configuration dialog box.
   Dim Config As New frmConfigure
 
   ' Display the dialog box on screen.
   Config.ShowDialog(Me)
 
   ' Update the current configuration settings.
   CurrentSettings = GrabAPictureSettings.LoadSettings()
 
   ' Get the current wallpaper URI and display it.
   Dim Wallpaper As New WinWallpaper
   txtWallpaperURI.Text = _
      Wallpaper.WallpaperURI.ToString()
 
   ' Define default settings if necessary.
   If CurrentSettings Is Nothing Then
      CurrentSettings = New GrabAPictureSettings
   End If
End Sub

The first part of this code looks like any dialog box display code you’ve used in the past. The example creates a frmConfigure object, Config. It then calls the Config.ShowDialog() method to display the dialog box with the current dialog box as the parent. A lot of developers create subordinate dialog boxes without the proper parent, which makes the application behave incorrectly. Always assign a parent to a subordinate dialog box when the subordinate should be dismissed before going back to the parent.

On return, the user has supposedly made changes to the wallpaper database. With this in mind, the application updates CurrentSettings to match the new wallpaper list. In addition, the wallpaper itself may have changed, so the application updates the wallpaper settings as well. The last bit of code may seem confusing at first, but think about it for a minute. The user might have deleted the last wallpaper entry from the list. If this is the case, the application needs to set CurrentSettings to a new instance of GrabAPictureSettings.

Command Line Interface Configuration

The final task for frmMain is at hand. The application needs some method of configuring the command line interface to use the correct random settings. The Use Random Local Settings and Use Random Remote Settings check boxes perform the task. Here is the code for the associated event handlers.

Private Sub cbLocal_CheckStateChanged(ByVal sender As Object, _
                                      ByVal e As System.EventArgs) _
                                   Handles cbLocal.CheckStateChanged
   ' Save the current checked state.
   CurrentSettings.LocalChecked = cbLocal.Checked
   GrabAPictureSettings.SaveSettings(CurrentSettings)
End Sub
 
Private Sub cbRemote_CheckStateChanged(ByVal sender As Object, _
                                       ByVal e As System.EventArgs) _
                                    Handles cbRemote.CheckStateChanged
   ' Save the current checked state.
   CurrentSettings.RemoteChecked = cbRemote.Checked
   GrabAPictureSettings.SaveSettings(CurrentSettings)
End Sub

As you can see, the application sets the correct CurrentSettings property, and then calls GrabAPictureSettings.SaveSettings() with the CurrentSettings object as an argument to save the settings to disk. The result is a change to the user’s disk-based XML settings. A side effect of this process is that the user’s wallpaper list is also saved to disk. The SaveSettings() method saves all of the settings at one time.

Well, that’s it for frmMain. The next post will look at frmConfigure. Until that time, please let me know if you have any questions at John@JohnMuellerBooks.com. You can find the next post in this series at Exploring the GrabAPicture Application (Part 11).

Exploring the GrabAPicture Application (Part 9)

In the previous post, Exploring the GrabAPicture Application (Part 8), you saw how all of the GrabAPicture application settings are put together and stored on disk from a low level perspective. This post diverges from the topic of data for a while to consider the user interface that relies on the data. In this case, you discover how the command line interface for this application works. Of course, the first requirement is deciding why the application needs a command line interface. In order to support automatic desktop wallpaper changes, the application does indeed use a command line interface. You can use the command line interface to create a shortcut like the one shown here and add it to the Shortcut folder on your system.

GrabAPicture0901

Look at the Target field. The application is configured to use the /Random command line switch to display any of the configured wallpapers at random. You can configure GrabAPicture in other ways, such as selecting a specific wallpaper. As with most applications that sport a command line interface, GrabAPicture provides a full help screen. The command line interface for this application is as follows:

GrabAPicture /?

GrabAPicture /Random

GrapAPicture /Picture:<Picture URI> [/Style:Stretched | Tiled | Centered]

where:

 

  • /?: Displays the help screen.
  • /Random: Selects any of the configured wallpapers at random.
  • /Picture: Displays a specific graphic found at the specified URI.
  • /Style: Configures Windows to display the wallpaper in Stretched, Tiled, or Centered mode.

Many applications have a considerably more complex command line interface, but you’ll find that this one is more than complex enough. The command line interface resides in frmMain.vb in the Main() function. When you see the code for this part of the example your first thought will be that the code is overly complicated and you’ll never understand it, but this isn’t the case. The Main() function does have a number of decisions to make, but they really come down to a number of selections.

 

  1. If there aren’t any command line arguments, the start the GUI version of the application.
  2. If the user has entered the /? command line switch as the first argument (regardless of anything that follows), display the help screen.
  3. If the user has entered the /Picture command line argument, then process the specific picture.
    1. Obtain the specific URI as the second part of the first command line argument.
    2. Check for a /Style command line switch as a second command line argument.
    3. Obtain the specific style as the second part of the second command line argument.
    4. Display the graphic on screen.
  4. If the user has entered the /Random command line switch, determine which picture to use from the preconfigured selections.
    1. Check for a configuration file. If none exists, display an error message and exit.
    2. If the user has checked both local and remote options, make sure the configuration settings are usable, then obtain a wallpaper selection from the preconfigured choices.
    3. If the user has checked just the local option, make sure there are local wallpaper selections to use, and then obtain the wallpaper selection from the local choices.
    4. If the user has checked just the remote option, make sure there are remote wallpaper selections to use, and then obtain the wallpaper selection from the remote choices.
    5. When the user hasn’t selected either local or remote random selections, display an error message and exit.

That’s really all there is to the code—a number of choices. With that in mind, here’s the massive looking, but not all that difficult code for this part of the example (normally, I don’t present code segments this long, but this particular example requires that I show the entire segment so you can see how it works).

Shared Function Main(ByVal CmdArgs() As String) As Integer
   ' Process the command line arguments.
   If CmdArgs.Length > 0 Then
      ' Get the first argument.
      Dim CmdArg As String = CmdArgs(0)
 
      ' User is requesting help.
      If CmdArg = "/?" Then
         ' Create an output string.
         Dim Output As New StringBuilder
 
         ' Create the usage instructions.
         Output.Append("Usage:" + vbCrLf)
         Output.Append("GrabAPicture" + vbCrLf)
         Output.Append("GrabAPicture /?" + vbCrLf)
         Output.Append("GrabAPicture /Random" + vbCrLf)
         Output.Append("GrabAPicture /Picture:<Picture URI> " + _
                       "[/Style:Stretched | Centered | Tiled]" + _
                       vbCrLf + vbCrLf)
         Output.Append("When used alone, displays the configuration " + _
                       "interface" + vbCrLf)
         Output.Append("/? - Displays this help message." + vbCrLf)
         Output.Append("/Random - Displays one of the images provided as " + _
                       "random input through the configuration program." + _
                       vbCrLf)
         Output.Append("/Picture - Defines a local or Web location for a " + _
                       "picture." + vbCrLf)
         Output.Append("/Style - Determines the display style of the " + _
                       "picture. Use only with the /Picture switch." + vbCrLf)
 
         ' Display the help.
         MessageBox.Show(Output.ToString(),
                         "GrabAPicture Usage",
                         MessageBoxButtons.OK,
                         MessageBoxIcon.Information)
         Return 0
      End If
 
      ' User wants to display a particular picture.
      If CmdArg.Length > 8 Then
         If CmdArg.Substring(0, 8).ToUpper() = "/PICTURE" Then
 
            ' Wallpaper manipulation class.
            Dim Wallpaper As New WinWallpaper
 
            ' Split the string.
            Dim NewUri As Uri
            NewUri = New Uri(CmdArg.Substring(9))
 
            ' Determine whether there is a Style argument to process.
            If CmdArgs.Length > 1 Then
 
               ' Get the style argument.
               Dim StyleArg As String = CmdArgs(1)
 
               ' Change the style.
               If StyleArg.Substring(0, 6).ToUpper() = "/STYLE" Then
                  Select Case StyleArg.Substring(7).ToUpper()
                     Case "STRETCHED"
                        Wallpaper.Style = WinWallpaper.Styles.Stretched
                     Case "CENTERED"
                        Wallpaper.Style = WinWallpaper.Styles.Centered
                     Case "TILED"
                        Wallpaper.Style = WinWallpaper.Styles.Tiled
                  End Select
               End If
            End If
 
            ' Change the wallpaper.
            Wallpaper.WallpaperURI = NewUri
         End If
         Return 0
      End If
 
      If CmdArg.ToUpper() = "/RANDOM" Then
         ' Current application settings.
         Dim CurrentSettings As GrabAPictureSettings
 
         ' Number of settings to randomize.
         Dim Settings As Int32 = 0
 
         ' The selected image number.
         Dim Selection As Int32 = 0
 
         ' Contains the wallpaper location.
         Dim NewUri As Uri
 
         ' Wallpaper manipulation class.
         Dim Wallpaper As New WinWallpaper
 
         ' Initialize the stored settings.
         CurrentSettings = GrabAPictureSettings.LoadSettings()
 
         ' Verify that there are settings to use.
         If CurrentSettings Is Nothing Then
            MessageBox.Show( _
               "This application isn't configured to use random sources.")
            Return -1
         End If
 
         ' Obtain the number of settings to randomize.
         If CurrentSettings.LocalChecked And CurrentSettings.RemoteChecked Then
 
            ' Detect configuration errors.
            If CurrentSettings.LocalUris Is Nothing Or _
               CurrentSettings.RemoteUris Is Nothing Then
 
               MessageBox.Show("Application not set up to use " + _
                               "both local and remote locations.")
               Return -2
            End If
 
            ' Determine the number of settings.
            Settings = CurrentSettings.LocalUris.Length + _
               CurrentSettings.RemoteUris.Length - 1
 
            ' Randomize the settings.
            Randomize(DateTime.Now.Millisecond)
            Selection = Convert.ToInt32(Settings * Rnd())
 
            ' Obtain the style and location.
            If Selection < CurrentSettings.LocalUris.Length Then
 
               ' Choose the wallpaper style.
               Select Case CurrentSettings.LocalUris(Selection).Style
                  Case WinWallpaper.Styles.Stretched
                     Wallpaper.Style = WinWallpaper.Styles.Stretched
                  Case WinWallpaper.Styles.Centered
                     Wallpaper.Style = WinWallpaper.Styles.Centered
                  Case WinWallpaper.Styles.Tiled
                     Wallpaper.Style = WinWallpaper.Styles.Tiled
               End Select
 
               ' Create a wallpaper URI.
               NewUri = New Uri(CurrentSettings.LocalUris(Selection).Location)
            Else
               ' Subtract the local resources.
               Selection -= CurrentSettings.LocalUris.Length
 
               ' Choose the wallpaper style.
               Select Case CurrentSettings.RemoteUris(Selection).Style
                  Case WinWallpaper.Styles.Stretched
                     Wallpaper.Style = WinWallpaper.Styles.Stretched
                  Case WinWallpaper.Styles.Centered
                     Wallpaper.Style = WinWallpaper.Styles.Centered
                  Case WinWallpaper.Styles.Tiled
                     Wallpaper.Style = WinWallpaper.Styles.Tiled
               End Select
 
               ' Create a wallpaper URI.
               NewUri = New Uri(CurrentSettings.RemoteUris(Selection).Location)
            End If
 
            ' Set the wallpaper location.
            Wallpaper.WallpaperURI = NewUri
 
         ElseIf CurrentSettings.LocalChecked Then
 
            ' Detect configuration errors.
            If CurrentSettings.LocalUris Is Nothing Then
               MessageBox.Show("Application not set up " + _
                               "to use local locations.")
               Return -2
            End If
 
            ' Determine the number of settings.
            Settings = CurrentSettings.LocalUris.Length - 1
 
            ' Randomize the settings.
            Randomize(DateTime.Now.Millisecond)
            Selection = Convert.ToInt32(Settings * Rnd())
 
            ' Obtain the style and location.
            ' Choose the wallpaper style.
            Select Case CurrentSettings.LocalUris(Selection).Style
               Case WinWallpaper.Styles.Stretched
                  Wallpaper.Style = WinWallpaper.Styles.Stretched
               Case WinWallpaper.Styles.Centered
                  Wallpaper.Style = WinWallpaper.Styles.Centered
               Case WinWallpaper.Styles.Tiled
                  Wallpaper.Style = WinWallpaper.Styles.Tiled
            End Select
 
            ' Create a wallpaper URI.
            NewUri = New Uri(CurrentSettings.LocalUris(Selection).Location)
 
            ' Set the wallpaper location.
            Wallpaper.WallpaperURI = NewUri
 
         ElseIf CurrentSettings.RemoteChecked Then
 
            ' Detect configuration errors.
            If CurrentSettings.RemoteUris Is Nothing Then
               MessageBox.Show("Application not set up " + _
                               "to use remote locations.")
               Return -2
            End If
 
            ' Determine the number of settings.
            Settings = CurrentSettings.RemoteUris.Length - 1
 
            ' Randomize the settings.
            Randomize(DateTime.Now.Millisecond)
            Selection = Convert.ToInt32(Settings * Rnd())
 
            ' Obtain the style and location.
            ' Choose the wallpaper style.
            Select Case CurrentSettings.RemoteUris(Selection).Style
               Case WinWallpaper.Styles.Stretched
                  Wallpaper.Style = WinWallpaper.Styles.Stretched
               Case WinWallpaper.Styles.Centered
                  Wallpaper.Style = WinWallpaper.Styles.Centered
               Case WinWallpaper.Styles.Tiled
                  Wallpaper.Style = WinWallpaper.Styles.Tiled
            End Select
 
            ' Create a wallpaper URI.
            NewUri = New Uri(CurrentSettings.RemoteUris(Selection).Location)
 
            ' Set the wallpaper location.
            Wallpaper.WallpaperURI = NewUri
 
         Else
            MessageBox.Show("None of the random settings are selected.")
         End If
         Return 0
      End If
 
      ' Indicate an error condition for the command
      ' line switches.
      MessageBox.Show( _
         "Use the /? switch to see application usage.", _
         "GrabAPicture", _
         MessageBoxButtons.OK, _
         MessageBoxIcon.Error)
      Return -1
   Else
      ' Run the application for configuration purposes.
      Application.Run(New frmMain)
   End If
End Function

The Main() function begins by checking CmdArgs.Length, which contains the number of command line arguments passed from the command line by the user. When this value is 0, the example bypasses all of the rest of that command line processing code and calls the standard code, Application.Run(New frmMain).

Of course, you’re interested in finding out what happens when the user does provide a command line argument. The first thing you’ll normally check is the /? command line switch. In this case, all it does is create a StringBuilder object, Output, and display it to the user. Unlike most command line utilities, which display help screen at the command prompt, GrabAPicture does use a dialog box like the one shown here to display help.

GrabAPicture0902

Because Main() is a function, you need to return some sort of value that indicates success or failure. If you always return 0, then the utility isn’t very useful in a batch file. The example returns a value of 0 to indicate success. However, it uses other values to indicate errors:

 

  • -1: The application hasn’t been configured to use random sources.
  • -2: The application hasn’t been configured to use both local and/or remote random sources.

You can actually test for these errors in a batch file and then do something about it. Yes, batch files do offer limited capabilities for error trapping and even fewer options for recovery, but these outputs do provide the opportunity recover in at least some cases.

The next test is against a word, Picture, which can be in any case. Whether you use uppercase or lowercase is up to youI happen to prefer uppercase, so the next test looks for /PICTURE. The command line switch is only part of the argument though, so you must extract the switch from the URI of the image the user wants to use by calling CmdArg.Substring(0, 8).

On to the picture processing. The code begins by creating a new WinWallpaper object, Wallpaper. It then creates a Uri object, NewUri, from the second half of the first command line argument using CmdArg.Substring(9). This method skips the colon that separates /Picture from the URI of the picture.

Now, the application has to decide how to display the image. If the user doesn’t supply the /Style command line switch, the application uses whatever setting is in place. Otherwise, the application uses a Select Case block to choose the appropriate style.

Only after the application has done all of this other processing does it set the Wallpaper.WallpaperURI property value. Changing this value changes the wallpaper (see Exploring the GrabAPicture Application (Part 7) for details).

So, what happens with the /Random command line switch? The processing is more straightforward than you might think. There are four possibilities:

 

  • Both the Use Random Local Sources and Use Random Remote Sources check boxes are checked.
  • Only the Use Random Local Sources check box is checked.
  • Only the Use Random Remote Sources check box is checked.
  • Neither check box is checked.

In the last case, the program exits with an error message and an error code. All three other cases follow the same pattern.

 

  1. The application checks for the required resources.
  2. When the settings exist, the application obtains the number of available settings.
  3. The application then selects a random item from the list of available settings.
  4. The code sets the Wallpaper.Style property value.
  5. The code creates a Uri from the settings.
  6. The code sets the Wallpaper.WallpaperURI property value, which changes the wallpaper on the Desktop.

There is one final consideration for this example and you’re bound to miss it in all of that code. The user could enter a command line switch that’s pure gibberish. Users do that. In this case, you need to tell the user how to get help. This little piece of code keeps things from blowing up or frustrating the user.

' Indicate an error condition for the command
' line switches.
MessageBox.Show( _
   "Use the /? switch to see application usage.", _
   "GrabAPicture", _
   MessageBoxButtons.OK, _
   MessageBoxIcon.Error)
Return -1

This is a short course on the command line interface for the GrabAPicture application. If you have any questions about it, please contact me at John@JohnMuellerBooks.com. Next week we’ll look at the code behind the GrabAPicture frmMain controls. You can find the next post in this series at Exploring the GrabAPicture Application (Part 10).

Exploring the GrabAPicture Application (Part 8)

Last week, in the Exploring the GrabAPicture Application (Part 7) post, you discovered the Style and WallpaperURI properties of the WinWallpaper class. These complex properties are used to define two of three wallpaper entry values for each wallpaper used to decorate the Desktop. It may have surprised you to see how complex a task these settings are, but they require interaction with the Win32 API, among other APIs, in order to function properly. So far, you haven’t saved a single setting to disk though. This post takes things another step. We’ll discuss how to start working with settings within the application, which includes creating local and remote wallpaper lists.

Before you can do anything, you need to create another class named GrabAPictureSettings (contained in GrabAPictureSettings.vb). The Exploring the GrabAPicture Application (Part 5) post contains a set of steps you can use to create a new class. This new class contains everything needed to manage settings. It doesn’t actually perform any settings tasks, but it provides the resources to accomplish them.

The first step is to provide all of the Imports statements used for this class. In this case, the requirements are somewhat obvious. You’ll need resources to manage the XML files, serialize the data, and also perform I/O. Here are the three Imports you’ll need.

Imports System.Xml
Imports System.Xml.Serialization
Imports System.IO

The class also needs a number of private variables to manage the settings. As with all other private settings in this example, these settings use a leading underscore to show they’re private:

' Private local variables represent settings.
Private _LocalChecked As Boolean = False
Private _RemoteChecked As Boolean = False
Private _LocalUris() As LocalUri
Private _RemoteUris() As RemoteUri

The first two settings _LocalChecked and _RemoteChecked correspond to the Use Random Local Sources and Use Random Remote Sources checkboxes on the main GrabAPicture application dialog (see the Exploring the GrabAPicture Application (Part 1) and Exploring the GrabAPicture Application (Part 2) posts for details). These private variables make user changes to those checkboxes permanent. In addition, the state of these variables determines which sources the application uses to select wallpaper.

The second two settings, _LocalUris and _RemoteUris, are arrays of wallpaper entries. These arrays will ultimately provide the list of wallpaper entries that the application randomly chooses from to display on the Desktop. These arrays also provide the content for the Local Sources and Remote Sources lists in the Configure a Random Source dialog box (see the see the Exploring the GrabAPicture Application (Part 1) and Exploring the GrabAPicture Application (Part 3) posts for details).

These variables are private, so the GrabAPictureSettings class requires some way to make them public. It does so using these four properties.

' Works with the local random setting on frmMain.
Public Property LocalChecked() As Boolean
   Get
      Return _LocalChecked
   End Get
   Set(ByVal Value As Boolean)
      _LocalChecked = Value
   End Set
End Property
 
' Works with the remote random setting on frmMain
Public Property RemoteChecked() As Boolean
   Get
      Return _RemoteChecked
   End Get
   Set(ByVal Value As Boolean)
      _RemoteChecked = Value
   End Set
End Property
 
' Works with the collection of local resources.
Public Property LocalUris() As LocalUri()
   Get
      Return _LocalUris
   End Get
   Set(ByVal Value As LocalUri())
      _LocalUris = Value
   End Set
End Property
 
' Works with the collection of remote resources.
Public Property RemoteUris() As RemoteUri()
   Get
      Return _RemoteUris
   End Get
   Set(ByVal Value As RemoteUri())
      _RemoteUris = Value
   End Set
End Property

As you can see, there is nothing special about the properties. The reason these properties are relatively simple is that most of the work is done in the classes discussed in earlier posts. The WinWallpaper class, especially, does a lot of heavy lifting for this application.

It’s time to save the settings to disk. The result will be an XML file that contains two settings that determine the checked state of the Use Random Local Sources and Use Random Remote Sources checkboxes, along with two separate lists of wallpaper entries for local and remote sources. Here is the code used to save wallpaper settings to disk.

' Saves the settings to disk.
Public Shared Sub SaveSettings(ByVal Current As GrabAPictureSettings)
   ' Obtain a user-specific storage location for this application.
   Dim UserPath As String
   UserPath = _
      Environment.GetFolderPath( _
         Environment.SpecialFolder.ApplicationData) + _
         "\GrabAPicture"
 
   ' Create the path if necessary.
   If Not Directory.Exists(UserPath) Then
      Directory.CreateDirectory(UserPath)
   End If
 
   ' Create an XML serializer.
   Dim DataWrite As New XmlSerializer(GetType(GrabAPictureSettings))
 
   ' Create a stream writer to output the data.
   Dim Output As New StreamWriter(UserPath + "\AppData.Config")
 
   ' Save the settings.
   DataWrite.Serialize(Output, Current)
   Output.Close()
End Sub

The one question you might have at this point is how the application gets by with so little code. The answer is in the details, such as marking the LocalUri and RemoteUri classes serializable so that the .NET Framework does most of the work for you. In this case, the code begins by creating a user-specific path that includes the user’s application data folder and a subfolder named GrabAPicture. On my Windows 7 machine, this folder equates to C:\Users\John\AppData\Roaming\GrabAPicture. Of course, it’ll be different on your system (at least the name will be different unless we use the same log on name).

 

If you find that your knowledge of serialization is lacking, check out Rod Stephen’s site for additional information. Make sure you check out the Object Serialization tutorial as a starting point. Be sure to check out “Serialize and deserialize objects in Visual Basic .NET” and “Control serialization and deserialization with attributes in Visual Basic .NET” as well. In fact, Visual Basic developer’s should definitely check out Rod’s entire list of Software Engineering topics.

The example then checks for the existence of the target folder. If the folder doesn’t exist (it won’t the first time you use the application), the application creates it.

At this point, the application can create an XmlSerializer object of type GrabAPictureSettings named DataWrite. It’s important to remember that GrabAPictureSettings relies on all of the classes we’ve discussed to this point, including those Win32 API calls. Splitting the task up into a number of classes makes things look simple, when they’re really a little on the complex side.

In order to write data to disk, the code must also create a StreamWriter object named Output that points to the AppData.Config file in the UserPath folder discussed earlier. Finally, the code can save the Current settings (passed to SaveSettings() by the caller) to the Output using the DataWrite.Serialize() method. Here’s an example of what the output might look like on your system:

GrabAPicture0801

The output is actually quite readable and there isn’t anything unexpected in it. The XML file contains every wallpaper entry, both local and remote, that the user wants to use for the Desktop on a random basis.

Of course, you’re now asking yourself where this mysterious Content variable comes from. One way that the application generates it is to load settings from disk using the LoadSettings() method shown here (a future post will discuss techniques for working with content in situations where the data doesn’t already exist).

' Loads the settings from disk.
Public Shared Function LoadSettings() As GrabAPictureSettings
   Dim UserPath As String
   UserPath = _
      Environment.GetFolderPath( _
         Environment.SpecialFolder.ApplicationData) + _
         "\GrabAPicture"
 
   ' Determine whether the file exists.
   If Not File.Exists(UserPath + "\AppData.Config") Then
      ' It doesn't, so there aren't any settings.
      Return Nothing
   End If
 
   ' Create an XML serializer.
   Dim DataRead As New XmlSerializer(GetType(GrabAPictureSettings))
 
   ' Create a stream reader to load the data.
   Dim Input As New StreamReader(UserPath + "\AppData.Config")
 
   ' Load the settings.
   Dim Current As GrabAPictureSettings
   Current = CType(DataRead.Deserialize(Input), GrabAPictureSettings)
   Input.Close()
 
   ' Return the result.
   Return Current
End Function

The process is similar to saving the settings. The code begins by creating the appropriate UserPath variable, an XmlSerializer object named DataRead, and a StreamReader named Input. The main difference this time is that the application uses the DataRead.Deserialize() method to turn the XML in the AppData.Config file into settings that the application can use. Of course, this means converting the type of the output to type GrabAPictureSettings. The method returns Current as output to the caller.

Next week we’ll move back to frmMain. Now that you have a good idea of how the settings work, it’s time to see how the application puts them into practice. The GrabAPicture application has both a GUI and a command line interface. This next post will describe the command line interface so that you can begin to understand how this application makes Desktop wallpaper changes through a simple addition to the user’s Startup folder. In the meantime, please let me know if you have any questions at John@JohnMuellerBooks.com. You can find the next post in this series at Exploring the GrabAPicture Application (Part 9).

 

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).