Acknowledgment
I would like to start off this article by thanking all of those individuals who have sent me unbelievable number of emails appreciating my previous articles, specially the ISAPI Extension one! You cannot imagine how I have been inspired by those lovely emails. I do appreciate your time, and I hope that this article suits your needs too.
Introduction
Unlike the human life that could be hardly saved 'coz of being invaded by other different so-called humans, computer monitors could be easily saved against the phosphor burn using a screen saver! Today, I am going to talk about Windows Screen Savers, one of those interesting topics that most of the programmers out there would like to know about.
Fundamentals
A screen saver is just a plain Win32 application that will be launched automatically when the mouse and keyboard have been idle for a specified period of time. Whenever this timer expires, the user could see either a blank screen or a very complex animation. Thereafter, if the user presses a key on her keyboard or moves her mouse, the screen will be changed back to the normal one that disappeared when the screen saver started.
To create your own screen saver, you have got two choices. First, you can create a normal Win32 application and handle the necessary messages sent to your window. Choosing the first way, you have to develop a program that creates a full screen window, as well as a window procedure that processes window messages sent to your application. Within the window procedure, you could have something as follows:
switch(message)
{
case WM_SYSCOMMAND:
if(wParam == SC_SCREENSAVE || wParam == SC_CLOSE)
return FALSE;
break;
case WM_LBUTTONDOWN:
case WM_MBUTTONDOWN:
case WM_RBUTTONDOWN:
case WM_KEYDOWN:
case WM_KEYUP:
case WM_MOUSEMOVE:
break;
}
In other words, you have to hard code the pieces required for your screen saver and take care of the messages that is common along all the Windows screen savers.
The second choice, though, is trying to use a library file that is being shipped with your compiler and does all of the above-mentioned hard coding (and even more)! This way, you can focus on the actual problem - that is creating the visual effects. Obviously, we are going to use this library to avoid re-inventing wheels.
The screen saver library (scrnsave.lib) contains the WinMain
function. This function among the other things, registers a window class that looks something as follows:
WNDCLASS cls;
cls.hCursor = NULL;
cls.hIcon = LoadIcon(hInst, MAKEINTATOM(ID_APP));
cls.lpszMenuName = NULL;
cls.lpszClassName = "WindowsScreenSaverClass";
cls.hbrBackground = GetStockObject(BLACK_BRUSH);
cls.hInstance = hInst;
cls.style = CS_VREDRAW | CS_HREDRAW | CS_SAVEBITS | CS_DBLCLKS;
cls.lpfnWndProc = (WNDPROC) ScreenSaverProc;
cls.cbWndExtra = 0;
cls.cbClsExtra = 0;
In other words, it registers a black background window, having no mouse cursor and an icon identified by ID_APP
. Moreover, it introduces its window procedure as ScreenSaverProc
. This procedure is the heart of any screen saver, and almost all the coding is done here.
This procedure, like all the other window procedures, is declared as follows:
LONG ScreenSaverProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
where hWnd
is the handle of the desktop window, message
is the actual message sent to the screen saver window, and wParam
and lParam
are additional message-specific information. If you could remember from my previous "MFC vs. Win32" series, you could recall that whenever we do not need to process a message, we pass it to the DefWindowProc
function. However, when developing a screen saver, we need to use the DefScreenSaverProc
instead. It is the same as DefWindowProc
with the exception of taking care of the other messages needed to keep the screen saver running!
Anyway, the library header file is named scrnsave.h and is placed in the include directory of the compiler. It contains the necessary prototypes to support the screen saver library. There are some global variables defined in the library and the extern
version is included in the above-mentioned header file. Two of those important variables have been declared as follows:
extern HINSTANCE hMainInstance;
extern BOOL fChildPreview;
where hMainInstance
is the instance handle of the screen saver, and fChildPreview
states whether the screen saver is currently running in preview mode. Don't worry, we will examine each of these, shortly.
Goals
We are going to create a screen saver, named, Ball Fusion, that contains 7 balls moving after each other on a sine wave path:
x = sin(�) * cos(�) * cos(2�)
y = cos(�)
where �
is a positive number between 0 and 360. You can change the formula, if you want.
Other considerations
The Ball Fusion will not have any configuration dialog box, and this means if you try to choose the Settings button in the screen saver control panel, you will get nothing in response. This is just because of the lack of time. Actually, my boss started to shout at me when he saw the screen saver running on my system at office! So I stopped any improvements, and that's why it has got no Settings box!
Getting started
To start, launch MSVC++ 6.0. From the "File" menu, select the "New" item. With the "Projects" tab selected, highlight "Win32 Application", and in the "Project Name" edit box, type BallFusion, and press the Ok button. Now, choose "An empty project" radio button, and press "finish" and "ok" respectively. Now, it is time to add the BallFusion.cpp to our project to start coding. To do so, select the "New" item from the "File" menu. Having the "C++ Source File" item selected in the "Files" tab, enter BallFusion as the "File Name" and press OK. This way, we have got the base framework in place.
Open the BallFusion.cpp to insert the following code:
#include "windows.h"
#include "scrnsave.h"
LRESULT WINAPI ScreenSaverProc(HWND hWnd,
UINT message, WPARAM wParam, LPARAM lParam)
{
return 0;
}
The screen saver library also needs us to include two other functions in our screen saver, ScreenSaverConfigureDialog
and RegisterDialogClasses
. The former receives messages sent to a screen saver's configuration dialog box whilst the latter is used to register any window class required by the configuration box. Since our screen saver doesn't have any configuration dialog box, we could simply skip the implementation. However, we have still included them in our screen saver program:
BOOL WINAPI ScreenSaverConfigureDialog(HWND hDlg,
UINT message, WPARAM wParam, LPARAM lParam)
{
return FALSE;
}
BOOL WINAPI RegisterDialogClasses(HANDLE hInst)
{
return TRUE;
}
The next step is to add scrnsave.lib to the library modules we are going to use in our program. To do so, from the "Project" menu, select "Settings" and make the necessary changes shown below:
Description
Having completed the setting process of the compiler options, it is now time to focus on the actual program. I have defined some global variables within the program, as follows:
#define MAX_BALLS 7
#define pi 3.141592625
#define szAppName "BallFusion 1.0"
#define szAuthor "Written by Mehdi Mousavi, � 2001"
#define szPreview "Ball Fusion 1.0"
typedef struct _BALLS
{
UINT uBallID;
HICON hIcon;
int x;
int y;
int angle;
}BALLS;
BALLS gBalls[] = {IDI_REDBALL, NULL, 0, 0, 0,
IDI_GREENBALL, NULL, 0, 0, 25,
IDI_BLUEBALL, NULL, 0, 0, 50,
IDI_PURPLEBALL, NULL, 0, 0, 75,
IDI_LIGHTREDBALL, NULL, 0, 0, 100,
IDI_YELLOWBALL, NULL, 0, 0, 125,
IDI_BLACKBALL, NULL, 0, 0, 150};
where MAX_BALLS
is the number of the maximum balls, szAppName
is the application's name that is shown at the bottom of the leftmost of the screen as well as the author's name (szAuthor
), szPreview
is the string that is shown to the user when she is previewing the screen saver from its control panel, and the BALLS
structure is the structure to store a ball configuration. This configuration consists of the identifier of the ICON (uBallID
), a handle to the icon itself after being loaded (hIcon
), the x
and y
position of the ball, and the starting angle
in which the ball starts moving. Finally, there's gBalls
, an array of the above-mentioned structure.
When the screen saver procedure receives the WM_CREATE
message, it loads the ball icons according to the identifier of each ball already declared in the gBalls
variable and stores this loaded icons under the handle already placed in the BALL
structure (hIcon
). However, please pay attention that to load the icons, the main instance handle of the application (hMainInstance
) is given to the LoadImage
function:
for(i = 0; i < MAX_BALLS; i++)
gBalls[i].hIcon = (HICON)LoadImage(hMainInstance,
MAKEINTRESOURCE(gBalls[i].uBallID),
IMAGE_ICON,
48,
48,
LR_DEFAULTSIZE);
Then the starting position (x
and y
member of the BALL
structure) is calculated for each ball, according to the following formula:
x = sin(�) * cos(�) * cos(2�)
y = cos(�)
In other words:
xpos = GetSystemMetrics(SM_CXSCREEN) / 2;
ypos = GetSystemMetrics(SM_CYSCREEN) / 2;
for(i = 0; i < MAX_BALLS; i++)
{
double alpha = gBalls[i].angle * pi / 180;
gBalls[i].x = xpos +
int((xpos - 30) * sin(alpha) * cos(alpha) * cos(2 * alpha));
gBalls[i].y = ypos - 30 + int(265 * cos(alpha));
}
And a black brush is created to be used while putting the icons on screen and a timer is started:
hBrush = CreateSolidBrush(RGB(0, 0, 0));
uTimer = SetTimer(hWnd, 1, 1, NULL);
When the timer is started, the screen saver procedure receives the WM_TIMER
message. For this message, all we do is calculate the x
and y
position of each ball, while increasing the angle
variable (0 <= angle
<= 360).
Then, the rectangular area of which the ball should be invalidated is calculated and placed in the rc
variable.
for(i = 0; i < MAX_BALLS; i++)
{
double alpha = gBalls[i].angle * pi / 180;
gBalls[i].x = xpos +
int((xpos - 30) * sin(alpha) * cos(alpha) * cos(2 * alpha));
gBalls[i].y = ypos - 30 + int(265 * cos(alpha));
gBalls[i].angle = (gBalls[i].angle >= 360) ? 0 : gBalls[i].angle + 1;
rc.left = gBalls[i].x;
rc.right = gBalls[i].x + 48;
rc.top = gBalls[i].y;
rc.bottom = gBalls[i].y + 48;
InvalidateRect(hWnd, &rc, FALSE);
}
When the invalidate function is called, the program receives the WM_PAINT
message. All we need to do upon the reception of this message is to put each ball on its position indicated by x
and y
:
if(fChildPreview)
{
SetBkColor(hDC, RGB(0, 0, 0));
SetTextColor(hDC, RGB(255, 255, 0));
TextOut(hDC, 25, 45, szPreview, strlen(szPreview));
}
else
{
SetBkColor(hDC, RGB(0, 0, 0));
SetTextColor(hDC, RGB(120, 120, 120));
TextOut(hDC, 0, ypos * 2 - 40, szAppName, strlen(szAppName));
TextOut(hDC, 0, ypos * 2 - 25, szAuthor, strlen(szAuthor));
for(i = 0; i < MAX_BALLS; i++)
DrawIconEx(hDC,
gBalls[i].x,
gBalls[i].y,
gBalls[i].hIcon,
48,
48,
0,
(HBRUSH)hBrush,
DI_IMAGE);
}
And what about fChildPreview
? As we said earlier, this flag states whether the screen saver is currently running in preview mode. I.e., if the user tries to watch the screen saver within the screen saver's control panel, fChildPreview
is TRUE
. Otherwise, it is FALSE
.
Final note
The compiler generates a .EXE file for this project. However, you need to rename it to .SCR so that the screen saver control panel can load it within the Screen Saver combo-box (as shown above). Another important thing to remember, is to include an ICON identified by ID_APP
in your application. This will be the screen saver's icon, since the library introduces this ID as the application's ID when registering the window class.
cls.hIcon = LoadIcon(hInst, MAKEINTATOM(ID_APP));
That's all folks - Aloha!
Further improvements
I've got an idea that improves this saver, that is keeping the current desktop as the background of the saver and start the animating process on that. To understand what I am talking about, add the WM_ERASEBKGND
message to your window procedure and compile the program again. Of course, this improvement will be possible if, and only if, I can convince my boss!
Any comments, suggestions and/or questions are welcomed.