65.9K
CodeProject 正在变化。 阅读更多。
Home

创建屏幕保护程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.69/5 (23投票s)

2001 年 11 月 9 日

CPOL

8分钟阅读

viewsIcon

280258

downloadIcon

6570

讨论 Windows 屏幕保护程序,并演示如何实现您自己的屏幕保护程序。

Sample screensaver image

致谢

我想首先感谢所有给我写了很多邮件来赞赏我以前文章的人,尤其是 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` 添加到我们程序中要使用的库模块中。为此,从“项目”菜单中,选择“设置”,然后进行如下所示的必要更改:

Project Settings

描述

完成编译器选项的设置过程后,现在是时候专注于实际程序了。我在程序中定义了一些全局变量,如下所示:

#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`。

Display Properties

最后说明

编译器会为该项目生成一个 `.EXE` 文件。但是,您需要将其重命名为 `.SCR`,以便屏幕保护程序控制面板可以在屏幕保护程序组合框中加载它(如上所示)。另一个需要记住的重要事项是,要在您的应用程序中包含一个由 `ID_APP` 标识的图标。这将是屏幕保护程序的图标,因为库在注册窗口类时将此 ID 作为应用程序 ID 引入。

cls.hIcon = LoadIcon(hInst, MAKEINTATOM(ID_APP));

各位,这就是全部内容 - 再见!

进一步改进

我有一个可以改进这个保护程序的想法,那就是将当前桌面作为保护程序的背景,并在其上开始动画过程。要理解我在说什么,请在您的窗口过程中添加 `WM_ERASEBKGND` 消息,然后重新编译程序。当然,只有在我能说服我的老板后,这种改进才可能实现!

欢迎任何评论、建议和/或问题。

© . All rights reserved.