January 23rd, 2009

An Updated Screensaver Example (Matt Gertz)

I’ve just completed a task that I set out to do about five years ago, and I am pretty proud (and tired)!  I have just finished scanning in every single photo that I’ve acquired over my 40+ years of life, fixing up their dates to reflect the date when the picture was taken, not when it was scanned.  Furthermore, I have tagged every photo on the disk drive (scanned or originally digital) with metadata saying who was in the photo and where it was taken. All of my family videos have been scanned in as well, and all of that work (plus all of my music tracks and personal documents) is now backed up weekly onto a 1.5TB disk that I store in a fireproof safe.   I’ve now got over 16,000 photos and 20+ hours of video on the system, going all the way back to the year 1875 (a picture of my great-great-grandparents on their wedding day), all of which are (hopefully) now preserved against tragic circumstances.

Of course, having done all that work, I wanted to relax and view the photos!  I didn’t want to buy one of those picture frames that can do slide shows, since they are expensive and I’d have to find one that would support a big enough (and also expensive) memory card – it’s 22GB worth of pictures.

My first choice was to use the screensaver that comes with Windows.  I run both Windows Vista and Windows XP at home (and Windows7 at work!), and the photo screensaver that comes with them is pretty decent in both cases.  But, after a while, I got tired of my kids asking “Who is that?  When was that?  Where was that?” over and over again.  If only I had a setting on the screensaver that would show the “DateTaken” property, and the Tags associated with the photo…

Well, of course, this was a good opportunity for me to learn about screensavers, which I hadn’t really ever thought much about before.  And thus began the journey…

What is a screensaver?

A screensaver is just an application renamed to have a .SCR extension, and placed in the Windows system directory.  That’s it.

Now, of course, the application should be a little special.  It needs to take up the whole screen, it shouldn’t have borders, menus, captions, or any other decorations, and it needs to turn itself off when the mouse moves.  Furthermore, it needs to expose its options in a certain way, so that the user can set them in the screensaver control panel.

I found out that MSDN has an example of a screensaver built using VS2003-level functionality, but targeting .NET 2.0.  You can find it here, and I’ll be referring back to it throughout this blog, much of which won’t make sense unless you’ve read through that code (consider it as homework J).  That screensaver draws random shapes on the screen and gives you a few options to change the speed and quality of the shapes.  However, it doesn’t leverage any of the new coolness we’ve introduced in recent versions of Visual Basic, so I figured that it was time for a screensaver makeover.   I copied it down to my machine and opened it up.  It has three classes in it – an Options class, an Options form, and a Screensaver form. We’ll discuss these in order.

The options class

The options class defines settings that the user wants to persist between runs, and also provides a mechanism for strong them.  The original settings were:

    Private m_Speed As String = “Fast”

    Private m_Shape As String = “Ellipse”

    Private m_IsTransparent As Boolean = True

 

I changed these to:

#Region “Member variables”

    Private m_Speed As Integer = 6

    Private m_ShowTags As Boolean = True

    Private m_ShowDates As Boolean = True

    Private m_Directory As String = “”

    Private m_UseSubdirectories As Boolean = True

#End Region

Basically, I’m going to let the used specify the time (in seconds) that the picture should stay up, whether or not to show the tags and the date superimposed on the photo,  where to look for photos, and whether or not to search subdirectories of that directory.  I also went ahead & generated get/set properties for each of those, ripping out the old ones.

Now, the rest of this file spends a lot of its code in opening up an options file, reading the data, writing to it, etc.  Fortunately, we don’t need any of that code anymore.  To set up automatic setting storage, I simply right-clicked the project, chose “Properties,” navigated to the Settings tab, and entered in my five settings as given above.  My entire persistence code then became:

    Public Sub LoadOptions()

        Me.Speed = My.Settings.Speed

        Me.ShowDate = My.Settings.ShowDate

        Me.ShowTags = My.Settings.ShowTags

        Me.Directory = My.Settings.Directory

        Me.UseSubdirectories = My.Settings.UseSubdirectories

 

        If String.IsNullOrEmpty(Directory) Then ‘ Default to “My Pictures” if not initialized

            Me.Directory = My.Computer.FileSystem.SpecialDirectories.MyPictures

        End If

    End Sub

 

    Public Sub SaveOptions()

        My.Settings.Speed = Me.Speed

        My.Settings.ShowDate = Me.ShowDate

        My.Settings.ShowTags = Me.ShowTags

        If Not String.IsNullOrEmpty(Directory) Then ‘ Don’t bother saving

            My.Settings.Directory = Me.Directory

        End If

        My.Settings.UseSubdirectories = Me.UseSubdirectories

        My.Settings.Save() ‘ Necessary since we’ll be forcing application exits later

    End Sub

 

eliminating about 40 lines of code devoted to serializing out the class.

The options form

Of course, by that point, I was getting build errors all over the place, since I was now out of synch with the options form and the screensaver form.  Let’s take a look at the options form.  For clarity’s sake, I wanted to hide the form-initialization code that VS2003 used to shove into the class file.  I created a new file in the project, named it “frmOptions.Designer.vb”, and typed the following into it:

<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()> _

Partial Class frmOptions

    Inherits System.Windows.Forms.Form

 

End Class

 

I then copied all of the generated crud into that partial class and got it out of the way.

All I really needed to do now is to replace the existing controls with three checkboxes (tags,date, and subdirectories) and two edit boxes (speed and directory).  That’s easy enough to do.  However, not content with doing it the easy way, I decided to limit the “speed” text box so that it can only accept numbers.  There’s a VB snippet that does exactly that for combo boxes (“Windows Forms Applications”-> Forms->Restrict a Control’s Acceptable Keystrokes”), and I’ve modified it for my edit box:

    Class restrictedTextBoxClass

        Inherits TextBox

        Const WM_KEYDOWN As Integer = &H100

 

        Protected Overrides Function ProcessCmdKey _

            (ByRef msg As Message, _

            ByVal keyData As Keys) As Boolean

 

            If msg.Msg = WM_KEYDOWN Then

                Return Not ((keyData >= Keys.D0 And keyData <= Keys.D9) _

                    Or keyData = Keys.Back Or keyData = Keys.Left _

                    Or keyData = Keys.Right Or keyData = Keys.Up _

                    Or keyData = Keys.Down Or keyData = Keys.Delete)

            End If

            Return MyBase.ProcessCmdKey(msg, keyData)

        End Function

    End Class

 

Then, given that, I reopened the frmOptions.Designer.vb file that I just created (can’t find it?  Make sure that “Show All Files” is turned on in the Solution Explorer’s tool bar) and changed TextBox to restrictedTextBoxClass in two locations:

    Friend WithEvents txtSpeed As frmOptions.restrictedTextBoxClass

    Me.txtSpeed = New ScreenSaver.frmOptions.restrictedTextBoxClass

 

I had to explicitly *rebuild* at this point – otherwise, the designer wouldn’t have known how to render the control if I were I to switch to the designer.

The screensaver form

Here’s where the real work happened.  Using the error messages in the Error List, I ripped out the lines of code that referred to things that didn’t exist anymore (like graphics, eliipses, etc).  I also moved the initialization crud into a partial class file, like I did for the options dialog.

The form used doesn’t actually start out with any controls on it. I set its BackColor to black, and then I added two controls (and the order was important):

(1)    A PictureBox control which I sized to be the same size as the form.  I set its BackColor to be black, and set its SizeMode property to Zoom.  (I will not be doing any cutesy transition effects – I dislike those intensely, so I’ll leave that as an exercise for the reader.)

(2)    A label control with the BackColor set to black and the ForeColor set to White.  My default text for this control was “Loading Pictures…”

There’s nothing else I have to do to the form itself – it’s already set up to fill the monitor (WindowState=Maximized), decorations are turned off (ControlBox = False, MinimizeBox and MaximizeBox are False, ShowInTaskbar = False, SizeGripStyle=Hide), and TopMost is equal to True.  Note that you will want to temporarily change TopMost to be False when developing & debugging; otherwise, you won’t be able to see the debugger, unless you are running dual-monitor! 

 I knew that I was going to need to use some .NET 3.0 functionality in order to dig out the picture metadata, so I right-clicked the project and chose “Properties.”  On the “Compile” tab, I clicked the “Advanced Compiler Options” button and, on the bottom of the resulting window, I changed the Target Framework to be the 3.5 version (3.0 would have worked also, but I like being current).  I then used the “Add Reference” menu item off of the project to add some needed references to the project – PresentationCore (found by navigating  to  C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\PresentationCore.dll on the Browse tab) and WindowsBase (found on the .NET tab).

Now, it was all coding.  My member variable had been whittled down to this:

    Private m_Options As New Options()

    Private m_Random As New Random()

    Private m_IsActive As Boolean = False

    Private m_MouseLocation As Point

    Private m_files As ReadOnlyCollection(Of String)

    Private m_fileCount As Integer = 0

 

Those would allow me to track the user options, generate random numbers, determine if the mouse was active and where it was at, hold onto the list of picture files that I wanted to display, and keeping a count of the latter as well.

I created a helper function to position my label.  Basically, I always want it exactly at the bottom of the screen, centered horizontally:

    Private Sub SetLabelLocation()

        Me.lblDescription.Top = Me.Size.Height – Me.lblDescription.Height

        Me.lblDescription.Left = (Me.Size.Width – Me.lblDescription.Width) \ 2

        Me.lblDescription.Show()

    End Sub

 

Now I could rewrite the form’s Load event handler (comments in all of this code omitted for brevity, though you’ll see them in the final product):

    Private Sub frmSceenSaver_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load

 

        m_Options.LoadOptions()

        SetLabelLocation()

        Me.tmrUpdateScreen.Interval = m_Options.Speed * 1000

        Me.tmrUpdateScreen.Enabled = True

 

       

If m_Options.UseSubdirectories = True Then

            m_files = My.Computer.FileSystem.GetFiles(m_Options.Directory, FileIO.SearchOption.SearchAllSubDirectories, “*.jpg”)

        Else

            m_files = My.Computer.FileSystem.GetFiles(m_Options.Directory, FileIO.SearchOption.SearchTopLevelOnly, “*.jpg”)

        End If

        If m_files IsNot Nothing Then

            m_fileCount = m_files.Count

        Else

            Me.lblDescription.Text = “No pictures found!”

            SetLabelLocation()

        End If

End Sub

This is pretty straightforward code:  I load the options from the user settings, move the label to the correct position, translate to desired seconds of duration to milliseconds, and enable the Timer (which was already attached to the form in the original code).  Then, I do a search for all JPG files in the desired directory (and in subdirectories, if the user chose that option) and collect them in the m_files object.  If I got any files back, then I keep track of the count to use later on when generating a random number.  Otherwise, I tell the user that I couldn’t find any pictures.  (The string should really go into the resources, but that makes the blog posts less readable.)

As with the original version that I downloaded, I wanted the application to end when the user moves or clicks the mouse.  The problem, however, is that the label and picture are hiding the form, so that the form never gets mouse events.  This was easily remedied by adding the appropriate events to the event handlers (I’ve underlined those below), and otherwise this code is unchanged:

    Private Sub frmScreenSaver_MouseMove(ByVal sender As Object, _

ByVal e As System.Windows.Forms.MouseEventArgs) Handles MyBase.MouseMove, _

PictureBox1.MouseMove, lblDescription.MouseMove

 

and

    Private Sub frmScreenSaver_MouseDown(ByVal sender As Object, _

ByVal e As System.Windows.Forms.MouseEventArgs) Handles MyBase.MouseDown, _

PictureBox1.MouseDown, lblDescription.MouseDown

 

Periodically, the timer will fire, and that calls the Tick handler, wherein all I did was change the name of the helper function from DrawShape to DrawPicture (if any files exist to be drawn):

    Private Sub tmrUpdateScreen_Tick(ByVal sender As System.Object, ByVal e As _

System.EventArgs) Handles tmrUpdateScreen.Tick

        If m_fileCount <> 0 Then

            DrawPicture()

        End If

    End Sub

 

As I mentioned above, DrawShape became DrawPicture, and I ripped out all of the code from it and entered the strange and inscrutable world of image metadata.   Metadata such as “DakeTaken” and “Keywords” aren’t available from the normal FileInfo; you have to dig for them, and it’s not an intuitive process.  I started out by getting a random number from 0 to m_fileCount – 1, and then determining if I need to grab metadata from the resulting file:

    Private Sub DrawPicture()

       Dim index As Integer = Me.m_Random.Next(0, m_fileCount – 1)

        If m_Options.ShowDate = True OrElse m_Options.ShowTags = True Then

 

“Index” is the index of the picture I’ll be showing.  Now, I needed to crack that file open:

Dim jpegStream As New System.IO.FileStream(m_files(index), _

  FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite)

 

Dim jpegDecoder As New JpegBitmapDecoder(jpegStream, _

  BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default)

 

Dim jpegFrame As BitmapFrame = jpegDecoder.Frames(0)

 

Dim jpegInplace As InPlaceBitmapMetadataWriter = _

  jpegFrame.CreateInPlaceBitmapMetadataWriter()

 

The first line should look pretty familiar – I’m just opening up a file like normal.  In the second line, I’m attaching the resulting stream to a JpegBitmapDecoder, which allows me to treat the file as if it is a JPEG (which it is), so that I don’t need to know or care about the JPEG format when parsing through it.  The third line gets me the first frame of that JPEG (and yes, 99.9999% of the time, there’s only one frame), and in the fourth line, I get a pointer to the metadata of that frame. 

(BTW:  Yes, the object is called a “metadata writer.”  There is no such thing as a “metadata reader” – you use this object for both.  Yes, that’s silly.  My screensaver just does reading, of course; if you’re interested in how to actually *write* metadata into the file once you’ve gotten this far, I’ve coded up an example in the completed code, attached at the bottom of this post.)

The next bits retrieve metadata from the writer and combine them into a display string:

            Dim sDescription As String = “”

            If m_Options.ShowDate = True Then

                If Not String.IsNullOrEmpty(dt) Then

                    Dim ddt As Date = CDate(“#” & dt & “#”)

                    sDescription = ddt.Date & “: “

                End If

            End If

 

“DateTaken” actually returns the time as well, and I just wanted the date, so I converted the DateTaken string to a true date and then retrieved the Date portion from it.

            If m_Options.ShowTags = True Then

                If jpegInplace.Keywords IsNot Nothing AndAlso jpegInplace.Keywords.Count > 0 Then

                    For Each keyword As String In jpegInplace.Keywords

                        If Not keyword.StartsWith(” “) Then

                            sDescription += keyword & “, “

 

                        End If

                    Next

                End If

            End If

 

Tags are referred to as “Keywords” in metadata, and they are stored in a read-only collection which I have to iterate through.  I ignore tags that start with a space character – this is because I personally mask out incorrect tags with spaces  rather than removing them.  This is because it’s important to keep the overall size of the metadata constant when modifying the file in-place, and I (for whatever reason) frequently write in the wrong tag.  (The proper way to remove bad tags is to create a copy of the file which lacks them, but that’s heavy-weight.)

After that, I do a bit of cosmetic work on the string:

            If sDescription.EndsWith(“, “) OrElse sDescription.EndsWith(“: “) Then

                sDescription = Microsoft.VisualBasic.Left(sDescription, sDescription.Length – 2)

            End If

 

As you can see, I’ve been concatentating the strings with “: “ or “; “, and since I’m too lazy to use fancy logic in the loop to look ahead to make sure I don’t add an extra one, I just trim any that I don’t need when all done.

Then I set the text of the label to be the description I’ve just created, center the label, and close the file:

            Me.lblDescription.Text = sDescription

            SetLabelLocation()

            jpegStream.Close()

It was important for me to remember to close the file, since I’m about to tell the picture control to use that same file, and they can’t use it at the same time.  But first, before loading up the picture, my code hides the label if the user didn’t want to show any text at all:

        Else

            Me.lblDescription.Hide()

        End If

 

Now all that remains is to show the picture:

        Me.PictureBox1.Load(m_files(index))

    End Sub

 

And that’s it; the old shape screensaver has been successfully transformed into a new picture screensaver that also shows the viewer the date the picture was taken, who is in the picture, and where they were.  The last thing to do, after building and debugging the app, is to copy it up to the c:\windows\system32 direcotry and change its extension to be SCR instead of EXE. 

Calling the screensaver from Windows

Note that I didn’t need to change the “Main” function, but I’ll briefly describe it here anyway.  First, we note that this is a single-threaded application which can handle arguments passed to it:

    <STAThread()> Shared Sub Main(ByVal args As String())

        If args.Length > 0 Then

 

Windows will pass one of three flags as arguments.  The first possibility is “p”, which means “show a preview of the screensaver.”  As with the original version of this code, I didn’t do any special here, but just exited, since previews are beyond the scope of this blog and  frankly, if you need a preview of a slideshow, then you’re not understanding the screensaver altogether:

            If args(0).ToLower = “/p” Then

                Application.Exit()

            End If

 

The second option is “c”, which means that Windows wants to display the option settings.  Once the user dismisses the options, I close down the application:

            If args(0).ToLower.Trim().Substring(0, 2) = “/c” Then

                Dim userOptionsForm As New frmOptions()

                userOptionsForm.ShowDialog()

                Application.Exit()

            End If

 

The third option (and also the default option, if no arguments are supplied) is “s”, which means that I should show the screensaver itself, and then close it down once it’s exited:

            If args(0).ToLower = “/s” Then

                Dim screenSaverForm As New ScreenSaver.frmScreenSaver()

                screenSaverForm.ShowDialog()

                Application.Exit()

            End If

        Else

            Dim screenSaverForm As New ScreenSaver.frmScreenSaver()

            screenSaverForm.ShowDialog()

            Application.Exit()

        End If

    End Sub

And there you have it.  The completed code (along with another method which describes setting metadata) is attached to this blog post, and will also be posted on my Temple of VB site.

‘Til next time,

  –Matt–*

[Edit:  I re-posted the ZIP file and modified the blog so that the SizeMode for the Picture was correctly set to Zoom instead of Center Image, and I now force a save of the options when changes are applied so that Application.Exit doesn’t skip that step.]

 

VB Screensaver.zip

1 comment

  • Bryan Bentz

    Matt, I enjoyed this. I started doing something along these lines, but wanted many additions: a few keystrokes to save a filename to a list for further editing, delete a file from the images directories (by moving it to another dir, outside the path), also to be able to show .HEIC files, .PDF's, .MOV's, etc. (one digital Olympus camera I had recorded sound for a second or two as the image was taken, wanted...

    Read more