创建屏幕保护程序






4.69/5 (23投票s)
讨论 Windows 屏幕保护程序,并演示如何实现您自己的屏幕保护程序。
致谢
我想首先感谢所有给我写了很多邮件来赞赏我以前文章的人,尤其是 ISAPI 扩展的那篇!您无法想象那些可爱的邮件给了我多大的灵感。我非常感谢您抽出宝贵的时间,希望这篇文章也能满足您的需求。
引言
与容易被其他所谓的“人类”侵害而难以拯救的生命不同,计算机显示器可以通过屏幕保护程序轻松地免受磷屏余辉的影响!今天,我将要谈论 **Windows 屏幕保护程序**,这是大多数程序员都想了解的有趣话题之一。
基本原理
屏幕保护程序只是一个普通的 Win32 应用程序,当鼠标和键盘闲置指定时间后会自动启动。每当这个计时器到期时,用户会看到一个空白屏幕或非常复杂的动画。之后,如果用户按下键盘上的某个键或移动鼠标,屏幕将恢复到屏幕保护程序启动时消失的正常状态。
要创建自己的屏幕保护程序,您有两个选择。第一种是创建一个普通的 Win32 应用程序并处理发送到您窗口的必要消息。选择第一种方式,您需要开发一个创建全屏窗口的程序,以及一个处理发送到您应用程序的窗口消息的窗口过程。在窗口过程中,您可以这样做:
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: //Quit the application here break; }
换句话说,您需要硬编码屏幕保护程序所需的组件,并处理所有 Windows 屏幕保护程序通用的消息。
第二种选择是使用编译器附带的库文件,该文件可以完成上述所有硬编码(甚至更多)的工作!这样,您就可以专注于实际问题——创建视觉效果。显然,我们将使用这个库来避免重复造轮子。
屏幕保护程序库(scrnsave.lib)包含 `WinMain` 函数。此函数除其他外,还会注册一个窗口类,该类看起来大致如下:
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;
换句话说,它注册了一个带有黑色背景、没有鼠标光标且图标由 `ID_APP` 标识的窗口。此外,它将窗口过程指定为 `ScreenSaverProc`。此过程是任何屏幕保护程序的关键,几乎所有的编码都在这里完成。
此过程与其他所有窗口过程一样,声明如下:
LONG ScreenSaverProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
其中 `hWnd` 是桌面窗口的句柄,`message` 是发送到屏幕保护程序窗口的实际消息,而 `wParam` 和 `lParam` 是附加的消息特定信息。如果您还记得我之前写的“MFC 与 Win32”系列文章,您会记得当我们不需要处理消息时,我们会将其传递给 `DefWindowProc` 函数。但是,在开发屏幕保护程序时,我们需要使用 `DefScreenSaverProc` 代替。它与 `DefWindowProc` 相同,只是它还负责处理使屏幕保护程序保持运行所需的其他消息!
总之,库的头文件名为 `scrnsave.h`,位于编译器的 include 目录中。它包含了支持屏幕保护程序库的必要原型。库中定义了一些全局变量,上述头文件中包含它们的 `extern` 版本。其中两个重要变量声明如下:
extern HINSTANCE hMainInstance; extern BOOL fChildPreview;
其中 `hMainInstance` 是屏幕保护程序的实例句柄,`fChildPreview` 表示屏幕保护程序当前是否处于预览模式。别担心,我们稍后会逐一研究它们。
目标
我们将创建一个名为 **Ball Fusion** 的屏幕保护程序,其中包含 7 个球沿着正弦波路径相互跟随移动。
x = sin(ß) * cos(ß) * cos(2ß)
y = cos(ß)
其中 `ß` 是一个介于 0 和 360 之间的正数。如果您愿意,可以更改公式。
其他注意事项
Ball Fusion 将没有任何配置对话框,这意味着如果您尝试在屏幕保护程序控制面板中选择 **设置** 按钮,您将不会收到任何响应。这仅仅是因为时间不足。实际上,当我的老板看到我的系统在办公室里运行屏幕保护程序时,他开始对我大喊大叫!所以我停止了任何改进,这就是为什么它没有设置框!
入门
首先,启动 MSVC++ 6.0。从“文件”菜单中,选择“新建”项。在“项目”选项卡下,选择“Win32 Application”,然后在“项目名称”编辑框中输入 BallFusion,然后按 _确定_ 按钮。现在,选择“空项目”单选按钮,然后分别按“完成”和“确定”。现在,是时候将 _BallFusion.cpp_ 添加到我们的项目中开始编码了。为此,请从“文件”菜单中选择“新建”项。在“文件”选项卡下选择“C++ 源文件”项,将 BallFusion 输入为“文件名”,然后按 _确定_。这样,我们就有了基础框架。
打开 _BallFusion.cpp_ 以插入以下代码:
#include "windows.h" #include "scrnsave.h" LRESULT WINAPI ScreenSaverProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { return 0; }
屏幕保护程序库还要求我们在屏幕保护程序中包含另外两个函数:`ScreenSaverConfigureDialog` 和 `RegisterDialogClasses`。前者接收发送到屏幕保护程序配置对话框的消息,而后者用于注册配置框所需的任何窗口类。由于我们的屏幕保护程序没有配置对话框,我们可以跳过实现。但是,我们仍然在屏幕保护程序程序中包含了它们。
BOOL WINAPI ScreenSaverConfigureDialog(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) { //We simply return FALSE to indicate that //we do not use the configuration dialog box return FALSE; } BOOL WINAPI RegisterDialogClasses(HANDLE hInst) { //Since we do not register any special window class //for the configuration dialog box, we must return TRUE return TRUE; }
下一步是将 `scrnsave.lib` 添加到我们程序中要使用的库模块中。为此,从“项目”菜单中,选择“设置”,然后进行如下所示的必要更改:
描述
完成编译器选项的设置过程后,现在是时候专注于实际程序了。我在程序中定义了一些全局变量,如下所示:
#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};
其中 `MAX_BALLS` 是最大球数,`szAppName` 是应用程序的名称,显示在屏幕左下角,以及作者的姓名(`szAuthor`),`szPreview` 是用户在从控制面板预览屏幕保护程序时显示的字符串,而 `BALLS` 结构是用于存储球配置的结构。此配置包括图标的标识符(`uBallID`)、加载后的图标句柄(`hIcon`)、球的 `x` 和 `y` 位置,以及球开始移动的起始 `angle`。最后,`gBalls` 是上述结构的数组。
当屏幕保护程序过程收到 `WM_CREATE` 消息时,它会根据 `gBalls` 变量中已声明的每个球的标识符加载球图标,并将这些加载的图标存储在 `BALL` 结构中已放置的句柄(`hIcon`)下。但是,请注意,要加载图标,应用程序的主实例句柄(`hMainInstance`)会传递给 `LoadImage` 函数。
for(i = 0; i < MAX_BALLS; i++) gBalls[i].hIcon = (HICON)LoadImage(hMainInstance, MAKEINTRESOURCE(gBalls[i].uBallID), IMAGE_ICON, 48, 48, LR_DEFAULTSIZE);
然后,根据以下公式计算每个球的起始位置(`BALL` 结构中的 `x` 和 `y` 成员):
x = sin(ß) * cos(ß) * cos(2ß)
y = cos(ß)
换句话说
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)); }
创建一个黑画刷用于在屏幕上绘制图标,并启动一个计时器。
hBrush = CreateSolidBrush(RGB(0, 0, 0)); uTimer = SetTimer(hWnd, 1, 1, NULL);
启动计时器后,屏幕保护程序过程会收到 `WM_TIMER` 消息。对于此消息,我们所做的就是计算每个球的 `x` 和 `y` 位置,同时增加 `angle` 变量(0 <= `angle` <= 360)。
然后,计算应使球无效的矩形区域,并将其存储在 `rc` 变量中。
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); }
调用 invalidate 函数后,程序会收到 `WM_PAINT` 消息。收到此消息后,我们需要做的就是将每个球放置在其 `x` 和 `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); }
那么 `fChildPreview` 呢?正如我们之前所说,这个标志表示屏幕保护程序当前是否处于预览模式。也就是说,如果用户尝试在屏幕保护程序的控制面板中查看屏幕保护程序,`fChildPreview` 为 `TRUE`。否则,它为 `FALSE`。
最后说明
编译器会为该项目生成一个 `.EXE` 文件。但是,您需要将其重命名为 `.SCR`,以便屏幕保护程序控制面板可以在屏幕保护程序组合框中加载它(如上所示)。另一个需要记住的重要事项是,要在您的应用程序中包含一个由 `ID_APP` 标识的图标。这将是屏幕保护程序的图标,因为库在注册窗口类时将此 ID 作为应用程序 ID 引入。
cls.hIcon = LoadIcon(hInst, MAKEINTATOM(ID_APP));
各位,这就是全部内容 - 再见!
进一步改进
我有一个可以改进这个保护程序的想法,那就是将当前桌面作为保护程序的背景,并在其上开始动画过程。要理解我在说什么,请在您的窗口过程中添加 `WM_ERASEBKGND` 消息,然后重新编译程序。当然,只有在我能说服我的老板后,这种改进才可能实现!
欢迎任何评论、建议和/或问题。