ScreenSaver


Description:
This sample code consists of three files. A form for the screen saver itself, that just displays a big "X" in the example. A class and module that provide the necessary support for all of the screen saver specific functionality. This is a very complete framework, it supports the basic screen saver functionality, as well as support for the standard password dialog under Win9x, and it displays a dummy settings message box, which can easily be replaced with a more complex settings screen. It even supports the small preview window in the display properties dialog.

I've used this to create a new mail display, and have been quite pleased with the results.

4/20/2000: Added some logic to block SC_SCREENSAVE messages, to prevent problems on NT.

11/10/2003: Added support for multi-mon so the main window is the proper size to span all monitors.
 
Code:
' ---------------------------------------------------------------------
' Begin: From frmScreenSaver

'This is the screen saver itself.  Because all of the screen saver code
' is in mod/clsScreenSaver, this is just concerned with what to draw on
' the screen while it's displayed.  A couple of notes however.  First,
' this form should have the following properties set:
'
'  BorderStyle = 0 - None
'  BackColor = (Black)
'  KeyPreview = True
'
' Also, you're responsible for tracking the mouse cursor in any child
' controls.  Ideally, just put the following in any of the child's
' _MouseMove events:
'
'     PostMessage Me.hwnd, WM_MOUSEMOVE, 0, 0
'
'  From there the framework will close the screensaver if necessary
'
'  Finally, make sure and set the start up to Sub Main()

Option Explicit

Private Sub Form_Paint()
    'Just draw a stupid 'X' on the form to verify it's size and
    ' and position both full screen and in the display
    ' properties dialog.

    Line (0, 0)-(Width, Height), vbWhite
    Line (Width, 0)-(0, Height), vbWhite

End Sub

' End: From frmScreenSaver
' ---------------------------------------------------------------------

' ---------------------------------------------------------------------
' Begin: Module modScreenSaver

Option Explicit

'Mutex name to prevent multiple versions
Public Const MUTEX_NAME = "Scott's Screen Saver Mutex"

'ID of timer to dismiss password window after 30 seconds
Public Const TIMER_ID = 10

'Windows API
Type POINTAPI
    X As Long
    Y As Long
End Type

Type RECT
    Left As Long
    Top As Long
    Right As Long
    Bottom As Long
End Type

Type OSVERSIONINFO
    dwOSVersionInfoSize As Long
    dwMajorVersion As Long
    dwMinorVersion As Long
    dwBuildNumber As Long
    dwPlatformId As Long
    szCSDVersion As String * 128
End Type

Declare Function GetCursorPos Lib "user32" (lpPoint As POINTAPI) As Long
Declare Function SetWindowPos Lib "user32" (ByVal hwnd As Long, ByVal _
        hWndInsertAfter As Long, ByVal X As Long, ByVal Y As Long, _
        ByVal cx As Long, ByVal cy As Long, ByVal wFlags As Long) As _
        Long
Declare Function PostMessage Lib "user32" Alias "PostMessageA" (ByVal _
        hwnd As Long, ByVal wMsg As Long, ByVal wParam As Long, ByVal _
        lParam As Long) As Long
Declare Function ShowCursor Lib "user32" (ByVal bShow As Long) As Long
Declare Function GetTickCount Lib "kernel32" () As Long
Declare Function CallWindowProc Lib "user32" Alias "CallWindowProcA" _
        (ByVal lpPrevWndFunc As Long, ByVal hwnd As Long, ByVal Msg As _
        Long, ByVal wParam As Long, ByVal lParam As Long) As Long
Declare Function SetWindowLong Lib "user32" Alias "SetWindowLongA" _
        (ByVal hwnd As Long, ByVal nIndex As Long, ByVal dwNewLong As _
        Long) As Long
Declare Function GetWindowLong Lib "user32" Alias "GetWindowLongA" _
        (ByVal hwnd As Long, ByVal nIndex As Long) As Long
Declare Function SetParent Lib "user32" (ByVal hWndChild As Long, ByVal _
        hWndNewParent As Long) As Long
Declare Function GetWindowRect Lib "user32" (ByVal hwnd As Long, lpRect _
        As RECT) As Long
Declare Function CreateMutex Lib "kernel32" Alias "CreateMutexA" _
        (lpMutexAttributes As Any, ByVal bInitialOwner As Long, ByVal _
        lpName As String) As Long
Declare Function ReleaseMutex Lib "kernel32" (ByVal hMutex As Long) As _
        Long
Declare Function CloseHandle Lib "kernel32" (ByVal hObject As Long) As _
        Long
Declare Function PwdChangePassword Lib "mpr" Alias "PwdChangePasswordA" _
        (ByVal lpcRegkeyname As String, ByVal hwnd As Long, ByVal _
        uiReserved1 As Long, ByVal uiReserved2 As Long) As Long
Declare Function VerifyScreenSavePwd Lib "password.cpl" (ByVal hwnd As _
        Long) As Long
Declare Function GetVersionEx Lib "kernel32" Alias "GetVersionExA" _
        (lpVersionInformation As OSVERSIONINFO) As Long
Declare Function SystemParametersInfo Lib "user32" Alias _
        "SystemParametersInfoA" (ByVal uAction As Long, ByVal uParam _
        As Long, lpvParam As Long, ByVal fuWinIni As Long) As Long
Declare Function SetTimer Lib "user32" (ByVal hwnd As Long, ByVal _
        nIDEvent As Long, ByVal uElapse As Long, ByVal lpTimerFunc As _
        Long) As Long
Declare Function KillTimer Lib "user32" (ByVal hwnd As Long, ByVal _
        nIDEvent As Long) As Long
Declare Sub keybd_event Lib "user32" (ByVal bVk As Byte, ByVal bScan As _
        Byte, ByVal dwFlags As Long, ByVal dwExtraInfo As Long)
Declare Function GetSystemMetrics Lib "user32" (ByVal nIndex As Long) _
        As Long

Public Const KEYEVENTF_KEYUP = &H2
Public Const VK_ESCAPE = &H1B
Public Const WM_TIMER = &H113
Public Const SPI_SCREENSAVERRUNNING = 97
Public Const VER_PLATFORM_WIN32_NT = 2
Public Const WS_CHILD = &H40000000
Public Const GWL_WNDPROC = (-4)
Public Const WM_MOUSEMOVE = &H200
Public Const WM_CLOSE = &H10
Public Const HWND_TOPMOST = -1
Public Const SWP_SHOWWINDOW = &H40
Public Const GWL_STYLE = (-16)
Public Const ERROR_ALREADY_EXISTS = 183&
Public Const WM_ACTIVATE = &H6
Public Const WM_ACTIVATEAPP = &H1C
Public Const WM_SYSCOMMAND = &H112
Public Const SC_SCREENSAVE = &HF140&
Public Const SM_XVIRTUALSCREEN = 76
Public Const SM_YVIRTUALSCREEN = 77
Public Const SM_CXVIRTUALSCREEN = 78
Public Const SM_CYVIRTUALSCREEN = 79

Private m_ScreenSaver As clsScreenSaver 'Screen saver class, mostly
 ' used to catch the form's events

Private m_hWnd As Long 'hWnd of main form
Private m_lpPrevProc As Long 'Previous window procedure
Private m_hMutex As Long 'Handle to mutex to prevent multiple instances

Sub Main()
    
    DoEvents 'Gets rid of stray windows events
    
    Dim rt As RECT
    Dim hwnd As Long
    
    Select Case LCase(Mid(Command$, 1, 2))
        Case "/a" 'Password
            
            'Trigger the change password dialog
            PwdChangePassword "SCRSAVE", Val(Mid(Command$, 3)), 0&, 0&
            
        Case "", "/s" 'Show screen saver
            
            'Create a mutex to prevent two instances of the screen
            ' saver starting.  This is used vs App.PrevInstance
            ' because I want to allow a preview window and the
            ' screen saver to co-exist
            m_hMutex = CreateMutex(ByVal 0, True, MUTEX_NAME & _
                                   "[Saver]")
                                   
            'Did the mutex already exist?
            If Err.LastDllError = ERROR_ALREADY_EXISTS Then
                ReleaseMutex m_hMutex
                CloseHandle m_hMutex
                Exit Sub
            End If
            If m_hMutex = 0 Then
                Exit Sub
            End If
            
            'Create the screensaver class and point it to our form
            Set m_ScreenSaver = New clsScreenSaver
            Set m_ScreenSaver.Form = frmScreenSaver
            
            Load frmScreenSaver
            
            'Display the form full-screen and topmost
            Dim nWidth As Long
            Dim nHeight As Long
            Dim nLeft As Long
            Dim nTop As Long
            
            ' Attempt to find Multi-mon information
            nWidth = GetSystemMetrics(SM_CXVIRTUALSCREEN)
            nHeight = GetSystemMetrics(SM_CYVIRTUALSCREEN)
            nLeft = GetSystemMetrics(SM_XVIRTUALSCREEN)
            nTop = GetSystemMetrics(SM_YVIRTUALSCREEN)
            
            If nWidth <> 0 And nHeight <> 0 Then
                SetWindowPos frmScreenSaver.hwnd, HWND_TOPMOST, nLeft, _
                             nTop, nWidth, nHeight, SWP_SHOWWINDOW
            Else
                'It looks like we couldn't find multi-mon info,
                ' just fall back to VB's intrinsic handlers
                SetWindowPos frmScreenSaver.hwnd, HWND_TOPMOST, 0, 0, _
                             Screen.Width / Screen.TwipsPerPixelX, _
                             Screen.Height / Screen.TwipsPerPixelY, _
                             SWP_SHOWWINDOW
            End If
        Case "/p" 'Tiny preview window the display properties
            
            'Create the screensaver class and let it know
            ' that it's in a preview window
            Set m_ScreenSaver = New clsScreenSaver
            Set m_ScreenSaver.Form = frmScreenSaver
            m_ScreenSaver.IsPreview = True
            
            'Make the main form a child of the display properties
            SetWindowLong frmScreenSaver.hwnd, GWL_STYLE, _
                          GetWindowLong(frmScreenSaver.hwnd, GWL_STYLE) _
                          Or WS_CHILD
            SetParent frmScreenSaver.hwnd, Val(Mid(Command$, 3))
            
            'Get the size of the little display window
            GetWindowRect Val(Mid(Command$, 3)), rt
            
            Load frmScreenSaver
            'Show the window, not topmost, and the size specified
            SetWindowPos frmScreenSaver.hwnd, 0, 0, 0, rt.Right - _
                         rt.Left, rt.Bottom - rt.Top, SWP_SHOWWINDOW
            
        Case "/c" 'Settings
            
            'Use a mutex to prevent multiple display settings
            ' dialogs
            m_hMutex = CreateMutex(ByVal 0, True, MUTEX_NAME & _
                       "[Setting]")
            If Err.LastDllError = ERROR_ALREADY_EXISTS Then
                ReleaseMutex m_hMutex
                CloseHandle m_hMutex
                Exit Sub
            End If
            If m_hMutex = 0 Then
                Exit Sub
            End If
            
            ' I was lazy, you'll want to replace this with your
            '  own form
            MsgBox "No settings", vbInformation, "Sorry"
            
            ReleaseMutex m_hMutex
            CloseHandle m_hMutex
    End Select
    
End Sub

Public Sub Hook(hwnd As Long)
    'Procedure to subclass the main form
    
    m_hWnd = hwnd
    m_lpPrevProc = SetWindowLong(m_hWnd, GWL_WNDPROC, AddressOf WndProc)
    
End Sub

Public Sub Unhook()
    'Cease subclassing of the main form
    
    SetWindowLong m_hWnd, GWL_WNDPROC, m_lpPrevProc
    
    ReleaseMutex m_hMutex
    CloseHandle m_hMutex
    
End Sub

Public Function WndProc(ByVal hwnd As Long, ByVal uMsg As Long, ByVal _
       wParam As Long, ByVal lParam As Long) As Long
    ' This is the subclassed window procedure, basically, look for
    '  loss of focus, and the timer to dismiss the password dialog

    Dim bBlockMsg As Boolean 'Block this message from being sent

    Select Case uMsg
        Case WM_SYSCOMMAND
            If wParam = SC_SCREENSAVE Then
                'Stop WinNT from trying to re-enable the screen saver
                ' and disabling it if the password is on
                bBlockMsg = True
            End If
        Case WM_ACTIVATE, WM_ACTIVATEAPP
            If wParam = 0 Then
                PostMessage m_hWnd, WM_MOUSEMOVE, 0, 0
            End If
        Case WM_TIMER
            If m_ScreenSaver.IsPasswordVisible Then
                If GetTickCount - m_ScreenSaver.PasswordShown > 30000 _
                      Then
                    keybd_event VK_ESCAPE, 0, 0, 0
                    keybd_event VK_ESCAPE, 0, KEYEVENTF_KEYUP, 0
                End If
            End If
    End Select
    
    'Pass the message to VB's window procedure if it's not blocked
    If Not bBlockMsg Then
        WndProc = CallWindowProc(m_lpPrevProc, hwnd, uMsg, _
                  wParam, lParam)
    End If
    
End Function

' End: Module modScreenSaver
' ---------------------------------------------------------------------

' ---------------------------------------------------------------------
' Begin: Class Module clsScreenSaver

Option Explicit

Private WithEvents m_Form As Form 'The form that this class
' is watching

Private m_bPreview As Boolean 'Are we in the display properies?

Private m_ptLast As POINTAPI 'The last seen mouse position
Private m_bLast As Boolean 'Is the last seen mouse position valid
Private m_nTickLoad As Long 'Time that the form loaded
Private m_bPasswordVisible As Boolean 'Pwd dialog displayed?
Private m_nPasswordShown As Long 'Time pwd dialog shown

Public Property Get PasswordShown() As Long
    PasswordShown = m_nPasswordShown
End Property

Public Property Get IsPasswordVisible() As Boolean
    IsPasswordVisible = m_bPasswordVisible
End Property

Public Property Set Form(newForm As Form)
    Set m_Form = newForm
End Property

Public Property Let IsPreview(bPreview As Boolean)
    m_bPreview = bPreview
End Property

Public Property Get IsPreview() As Boolean
    IsPreview = m_bPreview
End Property

Private Sub Class_Initialize()
    
    'Just make note of the display time, that way we can ignore
    ' mouse and keyboard events for 1/2 a second, so the stray
    ' events don't trigger the unload
    
    m_nTickLoad = GetTickCount
    
End Sub

Private Sub m_Form_KeyUp(KeyCode As Integer, Shift As Integer)
    
    'Key press, call the generic unload procedure
    TriggerUnload
    
End Sub

Private Sub m_Form_Load()
    
    If Not m_bPreview Then
        Hook m_Form.hwnd ' Subclass this form to watch for
                        ' the timer and focus events
        
        ShowCursor False 'Hide the mouse cursor
        Dim nTemp As Long
        ' Let windows know we're running, primarily to
        '  prevent the system keys
        SystemParametersInfo SPI_SCREENSAVERRUNNING, 1, nTemp, 0
    End If
    
End Sub

Private Sub m_Form_MouseMove(Button As Integer, Shift As Integer, X As _
        Single, Y As Single)
    
    If Not m_bLast Then
        'If the last mouse cursor position hasn't been used yet,
        ' save the current position
        GetCursorPos m_ptLast
        m_bLast = True
        Exit Sub
    End If
    
    'Compare the last with the current, if they differ, trigger
    ' the generic unload procedure
    Dim pt As POINTAPI
    GetCursorPos pt
    If pt.X <> m_ptLast.X Or pt.Y <> m_ptLast.Y Then
        TriggerUnload
        m_ptLast.X = pt.X
        m_ptLast.Y = pt.Y
    End If
    
End Sub

Private Sub m_Form_MouseUp(Button As Integer, Shift As Integer, X As _
        Single, Y As Single)
    
    'Mouse button pressed, trigger the generic unload procedure
    TriggerUnload
    
End Sub

Private Sub m_Form_QueryUnload(Cancel As Integer, UnloadMode As Integer)
    
    'Make sure we don't re-enter ourselves
    Static bInSub As Boolean
    
    If bInSub Then
        Exit Sub
    End If
    bInSub = True
    
    'This just displays the password dialog if the screen saver
    ' isn't being run in the display properties dialog.
    If Not m_bPreview Then
        If Not VerifyPassword Then
            Cancel = True
        Else
            Exit Sub
        End If
    End If
    
    ' Make note of the time incase the password was wrong
    m_nTickLoad = GetTickCount
    bInSub = False
    
End Sub

Private Sub m_Form_Unload(Cancel As Integer)
    'Undo what we did in Form_Load
    
    If Not m_bPreview Then
        Unhook
        
        ShowCursor True
        Dim nTemp As Long
        SystemParametersInfo SPI_SCREENSAVERRUNNING, False, nTemp, 0
    End If
    
End Sub

Private Sub TriggerUnload()
    
    ' Check to see if we should unload because of user actions,
    '  if so, then check to see if it's been atleast 1/2 a
    '  second since we started.
    
    If m_bPreview Then
        Exit Sub
    End If
    
    If GetTickCount - m_nTickLoad > 500 Then
        'Just use postmessage to avoid the problems with
        ' forcing and unload this very instant
        PostMessage m_Form.hwnd, WM_CLOSE, 0, 0
    End If
    
End Sub

Public Function VerifyPassword() As Boolean
    'Display and verify the users password
    
    'Check to see if this is NT.  If it is, let the os handle
    ' the password
    Dim osv As OSVERSIONINFO
    osv.dwOSVersionInfoSize = Len(osv)
    GetVersionEx osv
    
    If osv.dwPlatformId = VER_PLATFORM_WIN32_NT Then
        VerifyPassword = True
        Exit Function
    End If
    
    ShowCursor True 'The cursor should be visible for this
    m_bPasswordVisible = True
    m_nPasswordShown = GetTickCount
    SetTimer m_Form.hwnd, TIMER_ID, 1000, 0 'Used to dismiss
      'the password dialog after 30 seconds
    
    'Call the DLL function and return it's result
    VerifyPassword = (VerifyScreenSavePwd(m_Form.hwnd) <> 0)
    
    'Kill the timer, and hide the cursor (if the password is
    ' right, the cursor will be redisplayed when the form is
    ' killed
    KillTimer m_Form.hwnd, TIMER_ID
    m_bPasswordVisible = False
    ShowCursor False
    
End Function

' End: Class Module clsScreenSaver
' ---------------------------------------------------------------------
 
Sample Usage:
 
n/a