Constructing Your Screen SaverFull-Screen Window |
This page shows the C++ code you need for the screen saver full-screen window.
I've had enough! I want to go home
I want to see the previous page
Having stripped your application down to the bare minimum, it is now time to build it up again. The next task is to insert a class for your main screen saver window.
Use Class Wizard to add a new class, based on ‘generic CWnd’.
Then add methods to create the window and process the various windows messages it will receive.
Use Class Wizard to add a "Create" function that will build the window. Change the function so that it does not use any parameters (you will need to alter the ".cpp" and the ".h" files). The code I use is :
BOOL FullScreenWnd::Create() |
This creates a borderless, captionless window that fills the entire screen.
Your screen saver is likely to use a timer to carry out animations, so this is a good place to start your timer going. I use a class variable ‘m_iTimer’ to retain the timer id, and to indicate when the timer is active. This variable should be set to zero in the class’s constructor function (the code for this appears later).
The "Create" function is also a good place to record the screen saver starting time. You may want to do this if your screen saver needs to know how long it has been running. I use the start time when password protection is on, because I only want to ask for the password if the screen saver has been running for more than 10 seconds or so. I use a class variable ‘m_startTime’ (type CTime) to record the starting time.
The "Create" function that is generated assumes that the window class has already been registered, and allows you to specify the window name and window styles. The code I use sets all this up within the "Create" function itself, which is why it needs changing to remove the parameters. Setting everything up within the "Create" function means that the attributes required for a screen saver window are retained within the window’s class.
Note that I don’t set the "WS_EX_TOPMOST" style if I am building the Debug version. I found that I had trouble debugging the application when it was used, because the screen saver will then sit on top of all other windows, and effectively hide them. This includes the Visual C++ windows.
This "Create" function is not associated with any windows messages. It will get explicitly called from within the ‘InitInstance’ function (you will be adding this code later on).
Use Class Wizard to add a function for the ‘WM_CREATE’ message. It will create this function as ‘OnCreate’.
The ‘OnCreate’ function gets called during processing of the ‘CreateEx’ API. It is called after the window is built, but before it becomes visible. Therefore, it is a good place to do things like loading and initialising the images you are going to use, and hiding the cursor. Because the window has been built, you can access the window’s attributes and its device context.
My code looks like this :
int FullScreenWnd::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CWnd::OnCreate(lpCreateStruct) == -1) return -1; // Hide the cursor. while ( ::ShowCursor(FALSE) >= 0 ) ; // Load any images // create any palettes or graphics buffers return 0; } |
The ‘ShowCursor’ API has an internal display counter that is decremented every time you call it using FALSE. It returns the updated display counter value. The cursor is only hidden if the display counter value is less than zero, which is why the ‘while’ loop is used.
Use Class Wizard to add a function for the ‘WM_ERASEBKGND’ message. It will call this function ‘OnEraseBkGnd’.
The ‘OnEraseBkGnd’ function gets called just before your window is displayed, with a clipping region covering the whole window. It should have code to fill in the window’s background.
‘OnEraseBkGnd’ also gets called whenever another window overlaps the screen saver window and is moved or closed. This will only occur when the ‘password request’ dialog box is being displayed. In this case, the clipping region will just cover the area that needs to be repainted.
A ‘WM_PAINT’ message is processed immediately after the ‘OnEraseBkGnd’ function returns, so that the clipping region gets fully repainted. This means that ‘OnPaint’ will get called after ‘OnEraseBkGnd’ finishes.
My code looks like this :
BOOL FullScreenWnd::OnEraseBkgnd(CDC* pDC) { RECT winSize ; pDC->GetClipBox( &winSize) ; pDC->FillSolidRect ( &winSize , RGB(0,0,0) ) ; return TRUE ; } |
It just paints the window black.
Because ‘OnPaint’ gets called straight after ‘OnEraseBkGnd’ finishes, you could omit painting in ‘OnEraseBkGnd’ and rely on ‘OnPaint’ instead (and, in fact, omit ‘OnEraseBkGnd’ altogether).
Use Class Wizard to add a function for the WM_PAINT message. This function will be called ‘OnPaint’.
‘OnPaint’ is called when your screen saver starts, and whenever another window that overlaps the screen saver window is moved or closed. It should include code to repaint the specified area.
‘OnPaint’ does not need to include code to erase the background if ‘OnEraseBkGnd’ is used to do that. You can check whether the background needs erasing by looking at the ‘m_ps.fErase’ of the CPaintDC object that ‘OnPaint’ uses.
‘OnPaint’ is really only needed with Windows 95 when the ‘password request’ dialog box is displayed, if you use ‘OnEraseBkGnd’ to initially set the window to the background colour.
My code looks like this :
void FullScreenWnd::OnPaint() { CPaintDC dc(this); // device context for painting CRect paintRect ( &dc.m_ps.rcPaint ) ; // retrieve clipping region // insert code here to repaint the clipping region. // You can use dc as the device context } |
If you use a timer to control your screen saver, then use Class Wizard to add a function for the WM_TIMER messages. This function will be called ‘OnTimer’.
You should insert code into ‘OnTimer’ to update the window, for example to move sprites, change colours, or whatever else that you want the screen saver to do. You can access the window’s device context to paint directly to the window, rather than via the ‘OnPaint’ function.
My code looks like this :
void PropWnd::OnTimer(UINT nIDEvent) { CDC * pDC = GetDC() ; // update and redraw sprites, etc. ReleaseDC (pDC) ; CWnd::OnTimer(nIDEvent); } |
Your screen saver needs to detect when a key is pressed, when a mouse button is clicked, or when the mouse is moved. Any of these should cause the screen saver to terminate, or the ‘request password’ dialog box to be displayed (if password protection is on).
Because each type of input is handled in a very similar manner, we can use the shortcut of processing the appropriate window messages directly, rather than creating separate functions for each type of input message.
Use Class Wizard to add the ‘WindowProc’ function. This function will receive all window messages. We can process the ones we are interested in, and just pass the rest on to be processed as normal.
Your screen saver should have code to handle the following window messages :
· WM_KEYDOWN
· WM_SYSKEYDOWN
· WM_MOUSEMOVE
· WM_LBUTTONDOWN
· WM_RBUTTONDOWN
· WM_MBUTTONDOWN
The code should look like this :
#define THRESHOLD 3 LRESULT FullScreenWnd::WindowProc(UINT message, WPARAM wParam, LPARAM lParam) { static bool bGotPos ; static POINT ptLast ; POINT ptCursor, ptCheck; switch (message) { case WM_MOUSEMOVE: if(! bGotPos) { GetCursorPos(&ptLast); bGotPos = true; } else { GetCursorPos(&ptCheck); ptCursor.x = ptCheck.x - ptLast.x ; if (ptCursor.x < 0) ptCursor.x *= -1; ptCursor.y = ptCheck.y - ptLast.y ; if (ptCursor.y < 0) ptCursor.y *= -1; if ( (ptCursor.x + ptCursor.y) <= THRESHOLD ) break ; } case WM_LBUTTONDOWN: case WM_MBUTTONDOWN: case WM_RBUTTONDOWN: case WM_KEYDOWN: case WM_SYSKEYDOWN: if ( ! m_bIsWin95 || AskPassword() ) PostMessage(WM_CLOSE, 0, 0l); else bGotPos = false ; break; } return CWnd::WindowProc(message, wParam, lParam); } |
If a key or mouse button is pressed, the screen saver will try to terminate. If the mouse is moved, it will only try to terminate if it has moved more than the THRESHOLD distance from its starting location.
Before terminating, the screen saver will check to see if a password is required. If so, it will only terminate if a password has been correctly entered.
You need to trap WM_SYSKEYDOWN as well as WM_KEYDOWN to detect when the 'Alt' key is pressed. Without this, users can enter sequences such as Alt-Tab without your screen saver terminating.
The ‘AskPassword’ function will display the standard ‘password request’ screen if password protection is on, and return ‘true’ if a correct password is entered. If passwords are not being used, ‘AskPassword’ just returns ‘true’. The code for this function is shown next.
‘m_bIsWin95’ is a class variable (type bool) that is set up in the constructor function. It is set to ‘true’ if the operating system is Windows 95, to ‘false’ if it is Windows NT. The code for this is shown later on, in the ‘constructor’ code.
The screen saver is terminated by sending a WM_CLOSE message to itself. This will cause the ‘DestroyWindow’ function to be called (described later).
If screen saver password protection is on, the ‘AskPassword’ function displays the standard ‘password request’ dialog box, and returns ‘true’ if a correct password is entered. It returns ‘false’ if the dialog box is closed without a password being entered.
If password protection is not wanted, ‘AskPassword’ just returns ‘true’.
It uses the undocumented ‘VerifyScreenSavePwd’ function in the file "PASSWORD.CPL".
The code is :
bool FullScreenWnd::AskPassword () { if ( ! m_bIsWin95 ) return true ; // don't ask for password if within 10 seconds of starting CTime currTime ; CTimeSpan elapsedTime ; currTime = CTime::GetCurrentTime() ; elapsedTime = currTime - m_startTime ; if ( elapsedTime.GetTotalSeconds() < 20 ) return true ; m_bAskingPassword = true ; bool bResult = true ; HKEY hKey ; DWORD dwUsePW ; DWORD dwValType ; DWORD dwValSize = sizeof(DWORD) ; if ( ::RegOpenKeyEx(HKEY_CURRENT_USER , "Control Panel\\Desktop" , 0 , KEY_READ , &hKey ) ) { ::RegQueryValueEx ( hKey , "ScreenSaveUsePassword" , NULL , &dwValType , (LPBYTE)&dwUsePW , &dwValSize ) ; ::RegCloseKey ( hKey ) ; } if ( dwUsePW != 0 ) { HINSTANCE hPasswordCpl = ::LoadLibrary("PASSWORD.CPL") ; if ( hPasswordCpl != NULL ) { typedef BOOL (WINAPI *VERIFYSCREENSAVEPWD)(HWND) ; VERIFYSCREENSAVEPWD VerifyScreenSavePwd = (VERIFYSCREENSAVEPWD)GetProcAddress(hPasswordCpl, "VerifyScreenSavePwd" ) ; int iPrev ; SystemParametersInfo ( SPI_SCREENSAVERRUNNING , 1 , &iPrev , 0 ) ; ::ShowCursor(TRUE); result = ( VerifyScreenSavePwd(this->m_hWnd) == TRUE ) ; ::ShowCursor(FALSE) ; SystemParametersInfo ( SPI_SCREENSAVERRUNNING , 0 , &iPrev , 0 ) ; } ::FreeLibrary(hPasswordCpl); } m_bAskingPassword = false ; return result ; } |
First of all, note that I work out the time that has elapsed since the screen saver was started. I only ask for a password if the screen saver has been running for more than 20 seconds. This gives users time to stop the screen saver if it kicks in while they are doing something else.
Also note that I set a class variable, ‘m_bAskingPassword’ to ‘true’ while the dialog box is being displayed. This is important because I use this variable to stop the WM_ACTIVATEAPP messages trying to terminate the screen saver when the input focus is lost. Normally, if input focus is lost, we want to terminate the screen saver. But we do not want to do this when the ‘password request’ dialog box is displayed.
The ‘AskPassword’ function checks if password protection is wanted by examining the registry key ‘HKEY_CURRENT_USER\Control Panel\Desktop’ and looking at the value of ‘ScreenSaveUsePassword’. If not zero, then password protection is wanted.
The ‘VerifyScreenSavePwd’ function in the ‘PASSWORD.CPL’ file is linked to dynamically. We do this dynamically, because this file is always present with Windows 95, but not Windows NT.
The ‘ShowCursor’ API is used to display the cursor while the dialog box is displayed. Remember, the screen saver hid the cursor when it started up.
The ‘SystemParametersInfo’ API is used to disable Alt-Tab and Ctrl-Alt-Del while the dialog box is being displayed. We do not want users to be able to switch applications or terminate applications instead of entering a password.
Use Class Wizard to add a function for the WM_ACTIVATEAPP message. This function will be called ‘OnActivateApp’.
The WM_ACTIVATEAPP message is sent when the screen saver window is taking over the input focus from another application or losing the input focus to another application. Your screen saver should terminate if it is losing the input focus (unless the password dialog box is being displayed).
The code looks like this :
void FullScreenWnd::OnActivateApp(BOOL bActive, HTASK hTask) { CWnd::OnActivateApp(bActive, hTask); if ( ! bActive && ! m_bAskingPassword ) PostMessage(WM_CLOSE, 0 , 01 ) ; } |
I have seen other screen savers that do the same thing for WM_ACTIVATE and WM_NCACTIVATE messages, but as far as I can work out, these are superfluous. However, there may be cases when the screen saver is running, where Windows wants a particular program to take control. It will send the screen saver a WM_ACTIVATEAPP message with 'bActive' set to 'false', in the hope it will terminate. If the screen saver does not terminate, and is using a window with WM_EXTOPMOST set, the other program will not become visible.
The correct action therefore is to terminate the screen saver (except when the message was sent because the 'change password' dialog box was being displayed.
Use Class Wizard to add a function for the WM_SYSCOMMAND message. This function will be call ‘OnSysCommand’.
We are only interested in one of the system commands : SC_SCREENSAVE.
A SC_SCREENSAVE command is sent to the current foreground window when it is time to start a screen saver. This gives the foreground application the opportunity to prevent the screen saver starting. In our case, we need to trap this command to prevent running multiple instances of the screen saver. If we notice this command, we throw it away, because we know the screen saver is already going.
My code looks like this :
void FullScreenWnd::OnSysCommand(UINT nID, LPARAM lParam) { if ( nID != SC_SCREENSAVE && nID != SC_CLOSE ) CWnd::OnSysCommand(nID, lParam); } |
When a WM_CLOSE message is received, the following sequence of events takes
place :
· ‘PostNcDestroy’ is called at the end of WM_NCDESTROY processing
The ‘DestroyWindow’ function is the place to clean up the class variables, stop any timers and re-enable the cursor.
The WM_DESTROY causes a WM_QUIT message to be sent, to make the message processing loop terminate. Windows does this automatically, so there is no need for your program to process WM_DESTROY.
The WM_NCDESTROY message is the place where internal memory associated with the window is freed. Programs should ignore this message, and instead put cleanup code in the ‘PostNcDestroy’ function.
The ‘PostNCDestroy’ function should delete the screen saver window object. The MFC framework we are using does not do this automatically, so we have to do it ourselves to prevent memory leaks.
Use Class Wizard to add the ‘DestroyWindow’ and ‘PostNcDestroy’ functions.
The code to insert looks like this :
BOOL FullScreenWnd::DestroyWindow() { if ( m_iTimer != 0 ) KillTimer(1) ; ::ShowCursor(TRUE) ; return CWnd::DestroyWindow(); } void FullScreenWnd::PostNcDestroy() { delete this; } |
Insert code in the class constructor to initialise the class variables. It is also a good place to check whether we are using Windows 95 or Windows NT, and to initialise the randomiser if you are using random numbers.
My constructor code looks like this :
FullScreenWnd::FullScreenWnd () { m_iTimer = 0 ; m_bAskingPassword = false ; srand(::GetTickCount()) ; // initialize OSVERSIONINFO osVersion ; osVersion.dwOSVersionInfoSize = sizeof(OSVERSIONINFO) ; if ( ::GetVersionEx ( &osVersion ) ) { m_bIsWin95 = ( osVersion.dwPlatformId == VER_PLATFORM_WIN32_WINDOWS ) ; } // initialise any other class variables here } |
Nothing specific is needed in the class destructor. I use it to clean up any memory areas that I have allocated, such as for the images I use while running the screen saver.
You can read configuration values out of the registry. If you are using the 'profile' API's, you will need to access the application object. The MFC App Wizard generates the application object as a global variable with name 'theApp'. You can get access to this by specifying it as an 'extern' variable at the start of your screen saver window code :
extern CSaverApp theApp ; |
Replace 'CSaverApp' with the name of your application class, as generated by App Wizard.
I read the configuration values in the 'OnCreate()' method. For example :
frameRate = theApp.GetProfileInt("Config","Frame Rate", 10) ; maxPlasma = theApp.GetProfileInt("Config" , "Max Plasma Size" , 200 ) ; minPlasma = theApp.GetProfileInt("Config" , "Min Plasma Size" , 100 ); |
This reads three configuration values from the registry. The names should match those that you use to store the values when processing the Configuration Dialog box. Refer to the 'InitInstance()' description later on for more details.
If the screen is running in 256-colour mode, you will need to deal with palettes (unless you are only using Window’s basic set of 20 colours). This is not the place for a full discussion of palette management, but I will show the code I insert to cope when the system palette changes.
This is probably not an issue when the screen saver is running in full-screen mode, because it will always be the foreground window, so will not have other windows changing the system palette. The only exception is when the ‘password change’ dialog box is displayed, but that will not cause the system palette to change.
Anyway, I use Class Wizard to generate functions to handle the WM_PALETTECHANGED and WM_QUERYNEWPALETTE messages.
The code is :
void FullScreenWnd::OnPaletteChanged(CWnd* pFocusWnd) { // System palette has changed CWnd::OnPaletteChanged(pFocusWnd); // only a concern if it was not us that changed it if ( pFocusWnd != this ) { if ( m_iScreenBpp == 8 ) { CDC * pDC = GetDC() ; pDC->SelectPalette(m_MasterPalette,FALSE); int i = pDC->RealizePalette() ; // returns number of colours // that were remapped if ( i > 0 ) { // redraw the screen, so that colours are mapped properly InvalidateRect ( NULL , TRUE ) ; // cause OnPaint to be called } ReleaseDC(pDC); } } } BOOL FullScreenWnd::OnQueryNewPalette() { if ( m_iSscreenBpp == 8) { CDC * pDC = GetDC() ; pDC->SelectPalette(m_MasterPalette,FALSE); int i = pDC->RealizePalette() ; // returns number of colours // that were remapped if ( i > 0 ) { // redraw the screen, so that colours are mapped properly InvalidateRect ( NULL , TRUE ) ; // cause OnPaint to be called } ReleaseDC(pDC); return i ; } else return CWnd::OnQueryNewPalette(); } |
This code assumes that there are class variables containing the current screen colour mode (m_iScreenBpp) and the palette to use (m_MasterPalette).
The next page shows you the code you need for the preview window.