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

整体屏幕保护程序:从始至终

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (36投票s)

2003年6月4日

公共领域

23分钟阅读

viewsIcon

170965

downloadIcon

2683

屏保编码/分发的最佳实践。

目录

(c) 2003 Lucian Wischik。任何人都可以随意使用此代码,但不得出售或声明所有权。

引言

现在每个人都知道如何编写一个可用的屏幕保护程序的具体技术细节。本文介绍了屏幕保护程序开发的下一个层次:屏保整个生命周期的最佳实践,从编写开始,一直到部署给最终用户。它还解释了这些考虑因素如何影响屏保代码本身。我称之为“整体”屏幕保护程序开发。

在这里,您将找到我七年来编写32位屏幕保护程序的经验总结:程序的最佳结构是什么,如何最好地管理安装/卸载,如何包含JPEG、精灵图、音频和3D等基本功能,以及如何避免回答用户数千封技术支持邮件!

本文贯穿始终的一个重要主线是:让用户和开发者都保持简单。对于用户来说,这意味着将屏保保留在一个单一的自包含`.scr`文件中。对于开发者来说,这意味着将安装/卸载支持直接整合到`.scr`文件中,以便更容易部署。此外,为了帮助开发者,本文展示了如何实现标准效果(精灵图、JPEG、音频)以及如何将压缩媒体作为资源存储在`.scr`文件中,并无需磁盘上的临时文件即可提取它们。代码是用纯Win32/C++编写的,使用了标准模板库。我发现这是最易于编写和维护的方式。

背景

当微软发布Win95时,它带来了一种新的屏幕保护程序方式:通过“显示属性”对话框,包含预览窗口和密码管理。然而,他们没有文档说明如何编程这些功能,也没有人知道如何编程。于是我逆向工程了微软的屏保,并撰写了全面的技术文章如何编写32位屏幕保护程序。如果您阅读过任何屏幕保护程序教程或下载过任何示例代码,很可能它们都参考了这篇原始文章。现在每个人都知道了技术细节,并有了一些编写和部署屏保的经验,是时候推出一篇涵盖新领域的新技术文章了。

代码结构

问题:如何设计最佳的屏幕保护程序框架?

答案:不要。在设计(并销售)了许多屏保和框架之后,我的最终结论是框架无济于事。相反,制作一个完整、自包含、极简的屏保,它不给程序员施加任何结构,也不需要“学习”。保持简单。因此,我在这里提供了五个非常基本的示例屏幕保护程序。每个都具有相同的布局:其源代码包含在一个具有以下结构的单个`.cpp`文件中。

  // Comments at the top about how it works, and how
  // to achieve the same in your own savers.

  A bunch of standard header declarations

  // -----------------------------------------------

  Then comes the code that's unique to each saver.
  This is the important thing to read in the examples.

  // -----------------------------------------------

  Then comes the generic saver code that's common
  to all of them.

本文末尾给出了这五个屏保的顶部注释。这些注释解释了如何将它们的功能整合到您自己的代码中:MinScr(非常基本);Images(透明精灵、JPEG,存储在资源中的`.zip`文件中);PlayOgg(直接从内存播放OGG文件——OGG类似于MP3但更好);AudioIn(采样);ThreeDee(OpenGL)。但首先,这里有一些基本的代码课程。

  • 使用全局变量来存储屏保的设置。我知道这与人们的正常直觉相悖,但在这种情况下,屏保的设置本质上是全局的:它们在启动时从注册表加载,通过配置对话框修改,并在点击“确定”按钮时保存到注册表。当您的配置对话框中有预览时,它们在预览和对话框之间共享。当屏保在多个显示器上运行时,设置在每个显示器之间共享。如果您试图一直传递一个“设置对象”,那会很混乱。只需将您的设置设为全局即可。
  • 规划变量的用途。有些东西(例如从注册表加载的配置属性)全局应用于所有屏保窗口和配置对话框。其他东西(例如后备缓冲区)每个屏保窗口需要一个。还有一些东西(例如音频输入)应该在整个系统上发生一次。
  • 尽可能延迟分配资源(即在首次使用时分配)。这意味着:将全局句柄或指针初始化为0,然后在OnPaint处理程序或您首次使用它的任何地方,检查它是否仍为0,如果是则创建它。此检查每个循环都会执行,但成本不高。

    此代码片段来自MinScr示例,展示了您如何使用注册表中的设置。配置对话框包含屏保的预览。当配置对话框中发生更改时,它应立即反映在预览中。这是通过将设置存储在全局变量中,并响应WM_COMMAND更新它,以及响应PSN_APPLY保存它来完成的。

    // These are the saver's global settings. We use a global
    // refcount so that we don't load the settings more than
    // once. This isn't strictly necessary in such a small
    // example, but it's elegant.
    int refcount=0;      
    bool BigBlobs=false; // our only configuration setting
    
    void CommonInit()
    { refcount++; if (refcount!=1) return;
      //
      BigBlobs=RegLoad(_T("bigblobs"),true);
    }
    
    void CommonExit()
    { refcount--; if (refcount!=0) return;
      //
      // ... There's nothing to do here. But if CommonInit had
      // allocated some big storage (eg. reading a configuration
      // file, or precomputing an array whose size you don't
      // know at compiletime) then here's where to deallocate it
    }

    TSaverWindow 类是我在所有屏保中使用的。文件底部的通用屏保代码创建一个窗口和这个类的一个实例:一个实例用于预览窗口(无论是在显示属性中还是在屏保自己的配置对话框中);在多显示器系统中以全屏运行时,每个显示器一个实例。在构造函数中,id 对于预览是 -1,对于主显示器是 0,对于辅助显示器是其他索引。

    struct TSaverWindow
    { HWND hwnd; int id;  
    
      TSaverWindow(HWND _hwnd, int _id) : hwnd(_hwnd),id(_id)
      { CommonInit(); SetTimer(hwnd,1,100,NULL);
        ...
      }
    
      ~TSaverWindow()
      { CommonExit();
      }
    }

    所有优秀的屏保的配置对话框都是属性表。第一页是通用页,处理热点、鼠标灵敏度和密码延迟。它是所有这些示例屏保共有的通用代码的一部分。第二页包含此屏保特定的选项。它还有一个带有预览的监视器。使用ScrMonitor类将监视器放置在对话框编辑器中。它将自动创建一个TSaverWindow实例来容纳它。

    DLG_OPTIONS DIALOG  0, 0, 237, 220
    STYLE DS_SETFONT | DS_MODALFRAME | WS_POPUP | WS_VISIBLE
          | WS_CAPTION | WS_SYSMENU
    CAPTION "Options"
    FONT 8, "MS Sans Serif"
    BEGIN
      CONTROL  "",101,"ScrMonitor",0,52,12,123,105
      CONTROL  "Big Blobs",102,"Button",
               BS_AUTOCHECKBOX | WS_TABSTOP, 13,120,65,15
    END
    BOOL CALLBACK OptionsDlgProc(HWND hwnd,UINT msg,
                                 WPARAM wParam,LPARAM lParam)
    { switch (msg)
      { case WM_INITDIALOG:
        { CommonInit();
          CheckDlgButton(hwnd,102,BigBlobs?BST_CHECKED
                                          :BST_UNCHECKED);
        } return TRUE;
    
        case WM_COMMAND:
        { int id=LOWORD(wParam);
          bool x = (IsDlgButtonChecked(hwnd,102)==BST_CHECKED);
          if (id==102 ) BigBlobs=x;
        } return TRUE;
    
        case WM_NOTIFY:
        { LPNMHDR nmh=(LPNMHDR)lParam; UINT code=nmh->code;
          switch (code)
          { case (PSN_APPLY):
            { RegSave(_T("bigblobs"),BigBlobs);
              SetWindowLong(hwnd,DWL_MSGRESULT,PSNRET_NOERROR);
            } return TRUE;
          }
        } return FALSE;
    
        case WM_DESTROY:
        { CommonExit();
        } return TRUE;
      }
      return FALSE;
    }

    注意:我为整数、布尔值和字符串提供了RegSave(..)RegLoad(..)函数。这是因为它们非常常见,并且每个屏保都需要它们。它们将数据存储在HKEY_CURRENT_USER\ Software\ Scrplus\ <savername>键中。_T("text")宏在头文件`<tchar.h>`中定义,并提供与Unicode和ASCII的互换性:如果您使用Unicode编译,则将其视为Unicode字符串;使用ASCII编译,编译器将其视为ASCII。所有示例都可以在任一设置下编译。

    以下代码来自Images示例。它使用双缓冲来减少闪烁。每个屏保都需要自己的后备缓冲区。因此,缓冲区是TSaverWindow的一部分。在下面的代码中还要注意我们如何延迟分配它。这是一种更好的编码范式,因为代码更具弹性。(即,每次使用分配时,您也会明确验证其有效性)。

    struct TSaverWindow
    { HWND hwnd; int id;  
      HBITMAP hbmr;  // the back buffer
      //
      TSaverWindow(HWND h,int i) : hwnd(h), id(i), hbm(0)
      { 
      }
    
      ~TSaverWindow()
      { if (hbm!=0) DeleteObject(hbm); hbm=0;
      }
    
      void OnPaint(HDC hdc,const RECT &rc)
      { int w=rc.right, h=rc.bottom;
        if (hbm==0) hbm=CreateCompatibleDC(hdc,w,h);
         
        HDC bufdc=CreateCompatibleDC(hdc);
        SelectObject(bufdc,hbm);
        // ... compose our image into bufdc
        // ... once it's done, blt it to the screeen:
        BitBlt(hdc,0,0,w,h,bufdc,0,0,SRCCOPY);
        DeleteDC(bufdc);
      }
    };
  • 多显示器 - 它们的时代已经到来!从一开始就考虑多显示器进行编程。在后期改造支持通常太困难。提前规划:哪些变量是全局的(在所有屏幕之间共享,也可能在配置对话框之间共享),哪些是特定于某个屏幕的。例如,音乐不应由每个显示器的屏保窗口播放。

    以下代码来自AudioIn示例。其中,一行代码在每个显示器上从左到右构建了一个“声纹”。此绘图共享给TSaverWindow的每个实例,因此我们将其设为全局。(这里的代码仅关注如何为多显示器构建代码;要了解如何使用音频采样和傅里叶变换的示例代码,请点击示例链接)。

    void DrawInit();  
    void Draw();      
    // These are global functions, since they're shared by all
    // TSaverWindow instances
    
    
    struct TSaverWindow
    { HWND hwnd; int id; 
    
      TSaverWindow(HWND _hwnd,int _id) : hwnd(_hwnd),id(_id)
      { CommonInit();
        // id==-1 for a preview window,
        // or id==0 for the primary monitor
        if (id<=0) {DrawInit(); SetTimer(hwnd,1,0);}
      }
    
      ~TSaverWindow()
      { if (id<=0) KillTimer(hwnd,1);
      }
    
      void OnPaint(HDC hdc,const RECT &rc)
      { FillRect(hdc,&rc,(HBRUSH)GetStockObject(BLACK_BRUSH));
      }
    
      void OnTimer()
      { Draw();
        // note: only id<=0 allocated a timer
      }
    };
    int linex; RECT rcl;
    // We'll draw a single vertical line, xcoord 'linex',
    // stepping over each monitor. Rcl is its enclosing box,
    // probably larger than just the primary monitor.
    
    void DrawInit()
    { rcl.top = (monitors[0].top+monitors[0].bottom-300)/2;
      
      // Which will be the leftmost monitor? The rightmost?
      // Start from the primary monitor and trace left and
      // right to find them.
      rcl.bottom = rcl.top+300;
      rcl.left=monitors[0].left; rcl.right=monitors[0].right;
      for (vector<RECT>::const_iterator i=monitors.begin()+1;
           i!=monitors.end(); i++)
      { if (i->bottom>rcl.top && i->top<rcl.bottom)
        { rcl.left = min(i->left, rcl.left);
          rcl.right = max(i->right, rcl.right);
        }
      }
      linex=rcl.left;
    }
    
    void Draw()
    { linex++; if (linex>=rcl.right) linex=rcl.left;
    
      unsigned int m=0;    // Find out which monitor we're on
      for (vector<RECT>::const_iterator i=monitors.begin();
           i!=monitors.end(); i++, m++)
      { if (linex>=i->left && linex<i->right &&
            i->bottom>rcl.top && i-*>top<rcl.bottom) break;
      }
    
      HWND hwnd=SaverWindow[m]->hwnd;
      HDC hdc=GetDC(hwnd); 
      int relx=linex-monitors[m].left;
      int rely=rcl.bottom-monitors[m].top;
      MoveToEx(hdc,relx,rely,NULL); LineTo(hdc,relx,rely-300);
      ReleaseDC(hwnd,hdc);
    }

    注意:如果您只有一个显示器,您仍然可以通过使用参数运行屏保来使用“假”显示器测试多显示器支持。

    mysaver.scr /sm         // test multi-monitor
    mysaver.scr /p scrprev  // test preview

部署屏保

问题:如何部署屏保?

答案

  1. 将所有美术作品和额外文件作为资源嵌入屏保本身。
  2. 直接在屏保中构建安装/卸载支持。

通过嵌入艺术品,用户更容易控制自己的硬盘。通过内置安装/卸载支持,屏保开发变得更容易:只有一个项目在一个开发环境中构建,完全自包含,并且这里的安装程序比您使用InstallShield或MSI获得的更流畅、更快。这很重要,因为用户通常会下载大量屏保进行试用,因此需要同样快速地卸载它们。

  • 屏保使用单一自包含文件。不要使用任何附加文件:相反,将所有美术作品和音乐作为资源嵌入屏保中。这意味着您必须压缩它们。我提供了示例屏保,展示了如何解压缩和使用OGG音乐文件、ZIP文件和JPEG图片等资源。我重写了标准的公共OGG和ZIP源代码,使其更易于使用,并且(在后一种情况下)避免了对临时文件的需求:相反,压缩直接从资源到内存缓冲区进行。

    以下代码来自Images示例屏保。这个屏保有一个RT_RCDATA资源,它是一个zip文件,zip文件中包含一个位图和一个JPEG。Zip支持来自名为`unzip.cpp`/`unzip.h`的模块。这基本上是Info-zip的源代码,但我将其整理成一个单独的`.cpp`文件,并为其提供了更像Windows的API。(我的`unzip.cpp`也曾用于另一篇CodeProject文章)。

    // Lock the ZIP resource in memory.
    // (error-checks have been omitted here for clarity)
    HRSRC hrsrc=FindResource(hInstance,_T("ZIPFILE"),RT_RCDATA);
    DWORD size=SizeofResource(hInstance,hrsrc);
    HGLOBAL hglob=LoadResource(hInstance,hrsrc);
    void *buf=LockResource(hglob); 
    
    HZIP hzip=OpenZip(buf,size,ZIP_MEMORY); 
    
    ZIPENTRY ze;
    ZRESULT zr=FindZipItem(hzip,"background.jpg",true,NULL,&ze);
    if (zr==ZR_OK)
    { HGLOBAL hglob = GlobalAlloc(GMEM_MOVEABLE,ze.unc_size);
      void *buf=GlobalLock(hglob);
      UnzipItem(hzip,index,buf,ze.unc_size,ZIP_MEMORY);
      GlobalUnlock(hglob);
      hbmBackground = LoadJpeg(hglob);
      GlobalFree(hglob);
    }

    这里重点介绍如何从zip资源中解压缩媒体。`LoadJpeg()`函数和制作透明蒙版函数在其他地方讨论,详见Images示例

    zr=FindZipItem(hzip,"sprite.bmp",true,NULL,&ze);
    if (zr==ZR_OK)
    { vector<byte> vbuf(ze.unc_size); byte *buf = &vbuf[0];
      // the magic of STL will deallocate vbuf automatically
      // at end of scope
      UnzipItem(hzip,index,buf,ze.unc_size,ZIP_MEMORY);
      BITMAPFILEHEADER *bfh=(BITMAPFILEHEADER*)buf;
      BITMAPINFOHEADER *bih=(BITMAPINFOHEADER*)
                            (buf+sizeof(BITMAPFILEHEADER));
      int ncols=bih->biClrUsed;
      if (ncols==0) ncols = 1<<bih->biBitCount;
      char *srcbits=(char*)(buf+bfh->bfOffBits), *dstbits;
      hbmSprite=CreateDIBSection(NULL,(BITMAPINFO*)bih,
                      DIB_RGB_COLORS,(void**)&dstbits,NULL,0);
      unsigned int numbytes = bih->biSizeImage;
      if (numbytes==0) numbytes=((bih->biWidth*
               bih->biBitCount/8+3)&0xFFFFFFFC)*bih->biHeight;
      CopyMemory(dstbits,srcbits,numbytes);
      //
      BITMAP bmp; GetObject(hbmSprite,sizeof(BITMAP),&bmp);
      int w=bmp.bmWidth, h=bmp.bmHeight; 
    }
    
    CloseZip(hzip);
  • 将安装程序/卸载程序/屏保合并到一个可执行文件中。也就是说,创建一个名为`saver_setup.EXE`的单个可执行文件。当用户运行它时,它会通过将自身复制到`Windows`目录并重命名为`saver.SCR`来安装自己(包括卸载支持)。在那里,它表现得像一个普通的屏保。当它带有`/u`参数运行时,它表现得像一个卸载程序。

    这个简单的方案非常神奇。它简化了您的构建过程,因为您只需要构建一个文件(而不是在一个开发环境中构建,然后在另一个开发环境中制作安装程序)。它也受益于我长期以来在用户遇到的安装程序相关问题上的经验!最棒的是,您可以提供两种下载方式:一种是用户只需双击即可安装的自安装可执行文件,另一种是高级用户可以下载和测试的.scr文件。实际上,它们是同一个文件——在Web服务器上,您甚至可以将它们存储为指向同一个二进制文件的硬链接。代价是您必须将所有媒体作为资源内部压缩,而不是依赖外部压缩安装程序。但我们已经看到了如何使用压缩资源,所以这不是问题。

    安装程序在所有示例屏保通用的通用代码中实现。当文件具有`.EXE`后缀且未提供任何参数时,它就会启动。它会在其资源中查找`STRINGTABLE`资源 #1,并将其用作文件名。(注意:所有屏保的技术要求是它们将自己的名称存储为string#1,并且它们使用相同的文件名,并且它们放置在`Windows`目录中)。string#2被安装程序用作“添加/删除程序”中的帮助链接。并且请记住更改`VERSIONINFO`,以及清单文件!

    // This is the saver's resource (.rc) file. I personally
    // like to edit .rc files with notepad rather than a visual
    // resource editor. But it's up to you how you edit it.
    
    1        ICON            "playogg.ico"
    1        RT_MANIFEST     "manifest.txt"
    
    ZIPFILE  RCDATA          "test.zip"
    
    STRINGTABLE 
    BEGIN
        1                    "Play Ogg"
        2                    "http://www.wischik.com/lu/scr"
    END

小心带宽。2002年8月,我在网站上放了一个屏保,然后去度假了。当我九月份回来时,它已经被下载了几十万次,我仅那一个月就收到了400美元的超额带宽账单。要小心!

调试与部署

下载中包含我的`scrprev`工具。这有助于调试`Preview`功能。您首先要做的事情应该是将其复制到您的`Windows`目录中。

使用`.SCR`扩展名构建您的屏保,并使用这些参数进行测试

     <no arguments>  to test the configuration dialog
     /p scrprev      to test the preview mode
     /s              to run full-screen
     /sm             to test multi-monitor support

通常,当屏保运行时,它们会运行在最上层,并且当有鼠标移动时会自动退出。这在调试其全屏功能时有点令人讨厌,因为您无法使用调试器单步调试!为了帮助调试,请查看文件开头的两行

const tstring DebugFile = _T("OutputDebugString");
const bool    SCRDEBUG  = (DebugFile!=_T(""));

第一个表示有趣事件将被记录。您可以将其保留为`OutputDebugString`,或者给它一个文件名,或者将其置空以关闭它。使用`Debug(_T("a message"))`来记录您自己的消息。`SCRDEBUG`标志会抑制上述令人讨厌的行为,以帮助您进行调试。在部署之前,请记住关闭这两个功能。

  • 警惕逐渐的内存泄漏。屏幕保护程序可能会连续运行数小时甚至数天。如果存在任何内存或资源泄漏,最终会导致问题。务必小心,并使用`任务管理器`监视程序大小和GDI堆。您需要明确了解每次分配和释放大块内存的情况。特别注意,不要编写“通用清理”析构函数,在终止时才清理(如果需要)。因为如果没有它们,任何未能及时释放内存的错误都将被您的内存泄漏检测器(假设您有)标记出来,从而使泄漏在调试阶段更早地显现。

调试完成后,将屏保重命名为类似`MySaver-setup.exe`的名称。然后,当用户双击它时,它将运行其内置安装程序。

使用示例

要创建新的屏保,我从一个示例中复制目录,重命名所有文件,在记事本中打开它们并使用“查找/替换”更改名称。不要忘记`.rc`文件中的`STRINGTABLE`资源,以及`VERSIONINFO`或清单文件。

下面描述了示例屏保。特别是,对于每个屏保,我解释了它是如何实现其特定效果的,以及如何将相同的功能添加到您自己的代码中。

  • MinScr - 不做任何特殊操作,只是使用双缓冲在屏幕上绘制斑点。
  • Images - JPEG、位图、精灵图透明度,使用嵌入为资源的压缩数据文件。
  • PlayOgg - 从嵌入为资源的 OGG 文件在后台播放音频。(OGG 类似于 MP3 但更好)。
  • AudioIn - 监听当前播放的音频并进行响应,使用快速傅里叶变换。
  • ThreeDee - 使用 OpenGL 进行 3D 图形,改变屏幕分辨率。

每个示例都附带了Visual C++6、Visual Studio .NET和Borland C++ Builder 5的项目文件。我确信代码在其他编译器下也能正常工作。如果您愿意,可以删除您不使用的编译器的项目文件。扩展名为

   .sln .vcproj    -  Visual Studio .NET workspace/project
   .dsw .dsp       -  Visual C++6 workspace/project
   .bpg .bpf .bpr  -  Borland C++Builder5 group/project

如果您想从头开始创建一个新项目,这里有一些技巧。所有这些都是基本的Visual C++知识,但许多初级程序员似乎不知道。

  1. 从示例中复制相关的`.cpp`代码和`.rc`文件;
  2. 在“项目”>“设置”>“链接器”下,添加`comctl32.lib`,如果使用音频,还需添加`winmm.lib`;
  3. 我的所有示例都不使用预编译头,因此请在“编译器”>“预编译头”下将其关闭;
  4. 您应该始终前往“编译器”>“常规”,并将“警告级别”设置为最高——除了VC++ 6,它无法正确处理STL;
  5. 同样,转到“编译器”>“预处理器”,并添加`STRICT`定义;
  6. 对于您的发布版本,在“项目”>“设置”的常规选项下,打开“全局优化”。在“编译器”>“代码生成”下也打开它们。

如果您不理解这些步骤的任何目的,请阅读在线帮助!

MinScr 示例

此屏保在启动时拍摄桌面快照,将快照保留为后备缓冲区,并在其上绘制斑点。有一个配置选项可以更改斑点的大小。

待办事项中标记的是要更改的基本内容。它们是:加载/保存全局设置,为屏保创建动画,制作其“配置”对话框,并在字符串表资源中设置其名称。但是源代码非常简短清晰,因此程序员应该有信心理解和修改所有这些内容。

以下代码展示了此屏保如何在启动时获取桌面快照。这是一个好主意,但它在NT/Win2K/XP下实际上不起作用,因为它们在单独的桌面上运行屏保,因此看不到任何东西。哦,好吧。

struct TSaverWindow
{ HWND hwnd; int id;
  HANDLE hbm;        // we store the snapshot here

  TSaverWindow(HWND _hwnd,int _id) : hwnd(_hwnd),id(_id)
  { // If we're a preview window, we snapshot the primary
    // desktop. If we're a full-screen saver, we snapshot
    // whatever monitor we were running on.
    RECT rc; GetClientRect(hwnd,&rc); 
    int w=rc.right, h=rc.bottom;
    //
    if (id>=0) GetWindowRect(hwnd,&rc);
    else
    { rc.right=GetSystemMetrics(SM_CXSCREEN);
      rc.bottom=GetSystemMetrics(SM_CYSCREEN);}
    }
    //
    HDC sdc=GetDC(0), bufdc=CreateCompatibleDC(sdc);
    hbm=CreateCompatibleBitmap(sdc,bw,bh);
    SelectObject(bufdc,hbm);
    SetStretchBltMode(bufdc,COLORONCOLOR);
    StretchBlt(bufdc,0,0,w,h, sdc,rc.left,rc.top,
               rc.right-rc.left,rc.bottom-rc.top,SRCCOPY);
    DeleteDC(bufdc);
    ReleaseDC(0,sdc);
  }

  ~TSaverWindow()
  { if (hbm!=0) DeleteObject(hbm); hbm=0;
  }
};

图像示例

此屏保显示一个精灵图(`.BMP`)在一个背景图(`.JPG`)上弹跳。两张图片已压缩到一个ZIP文件中,此ZIP作为`RT_RCDATA`资源存储在`.scr`文件中。在运行时,ZIP/BMP/JPG都直接从资源提取到内存中:不使用任何临时文件。

保持文件大小很重要。这就是我们使用zip的原因。(实际上,将JPG压缩到zip中只节省了大约10K……真正需要zip的是未压缩的文件,例如BMP)。

至于精灵图,这个屏保展示了几种技术

  • 如何从内存加载JPEG。如果要在自己的代码中添加此功能,则必须#include <ole2.h><olectl.h>并复制LoadJpeg()函数。
  • 如何从内存加载BMP。代码在`EnsureBitmaps()`中。
  • 如何制作透明精灵。精灵实际上存储为两个位图,AND掩码`hbmClip`和OR掩码`hbmSprite`。加载/生成这两个的代码在`EnsureBitmaps()`中。绘制它们的代码在`OnPaint()`中。
  • 如何使用双缓冲来避免闪烁。位图`hbmBuffer`存储我们的后备缓冲区。它在`TSaverWindow()`中创建,并在`OnPaint()`中使用。
  • 如何读取zip文件。相关代码在`EnsureBitmaps()`中。此外,它还在单独的模块`UNZIP.CPP`/`UNZIP.H`中,该模块主要由此处的代码组成。感谢info-zip!
// To load a jpeg:

HGLOBAL hglob = GlobalAlloc(GMEM_MOVEABLE,size);
void *buf=GlobalLock(hglob);
// now copy the raw jpeg data into this buffer
// eg. by reading from disk, or from a locked resource
GlobalUnlock(hglob);
HBITMAP hbm = LoadJpeg(hglob);
GlobalFree(hglob);
...
DeleteObject(hbm);


// LoadJpeg: from an HGLOBAL containing jpeg data, we
// load it. (error-checking has been omitted, for clarity)
HBITMAP LoadJpeg(HGLOBAL hglob)
{ IStream *stream=0;
  CreateStreamOnHGlobal(hglob,FALSE,&stream);
  IPicture *pic;
  OleLoadPicture(stream,0,FALSE,IID_IPicture,(void**)&pic);
  stream->Release();
  HBITMAP hbm0=0; pic->get_Handle((OLE_HANDLE*)&hbm0);
  //
  // Now we make a copy of it into our own hbm
  DIBSECTION dibs; GetObject(hbm0,sizeof(dibs),&dibs);
  if (dibs.dsBm.bmBitsPixel!=24) {pic->Release(); return 0;}
  int w=dibs.dsBm.bmWidth, h=dibs.dsBm.bmHeight;
  dibs.dsBmih.biClrUsed=0;
  dibs.dsBmih.biClrImportant=0;
  void *bits;
  HDC sdc=GetDC(0);
  HBITMAP hbm1=CreateDIBSection(sdc,
        (BITMAPINFO*)&dibs.dsBmih,DIB_RGB_COLORS,&bits,0,0);
  //
  HDC hdc0=CreateCompatibleDC(sdc);
  HDC hdc1=CreateCompatibleDC(sdc);
  HGDIOBJ hold0=SelectObject(hdc0,hbm0);
  HGDIOBJ hold1=SelectObject(hdc1,hbm1);
  BitBlt(hdc1,0,0,w,h,hdc0,0,0,SRCCOPY);
  SelectObject(hdc0,hold0); SelectObject(hdc1,hold1);
  DeleteDC(hdc0); DeleteDC(hdc1);
  ReleaseDC(0,sdc);
  pic->Release();
  return hbm1;
}

以下代码用于实现透明精灵。使用`TransparentBlt`可能更容易,但它在多个桌面上存在问题,并且在Win95下不起作用。

// To create a bitmap from memory (eg. from a resource)
// Let 'buf' be the start address of the raw BMP file's data

BITMAPFILEHEADER *bfh = (BITMAPFILEHEADER*)buf;
BITMAPINFOHEADER *bih = (BITMAPINFOHEADER*)(buf
                          + sizeof(BITMAPFILEHEADER));
int ncols=bih->biClrUsed;
if (ncols==0) ncols = 1<<bih->biBitCount;
char *srcbits=(char*)(buf+bfh->bfOffBits), *dstbits;
HBITMAP hbm=CreateDIBSection(NULL,(BITMAPINFO*)bih,
                  DIB_RGB_COLORS,(void**)&dstbits,NULL,0);
unsigned int numbytes = bih->biSizeImage;
if (numbytes==0) numbytes=((bih->biWidth*bih->biBitCount/8
                            +3)&0xFFFFFFFC)*bih->biHeight;
CopyMemory(dstbits,srcbits,numbytes);
BITMAP bmp; GetObject(hbmSprite,sizeof(BITMAP),&bmp);
int w=bmp.bmWidth, h=bmp.bmHeight; 
...
DeleteObject(hbm);


// To create the AND/OR masks to treat 'hbm/w/h' as a sprite
// with transparency. (We take the top-left-pixel-color as
// transparent)

vHDC screendc=GetDC(0);
HDC bitdc=CreateCompatibleDC(screendc);
HGDIOBJ holdb = SelectObject(bitdc,hbm);
SetBkColor(bitdc,RGB(0,0,0));
//
struct MONOBITMAPINFO {BITMAPINFOHEADER bmiHeader;
                       RGBQUAD bmiColors[2];};
MONOBITMAPINFO bmi; ZeroMemory(&bmi,sizeof(bmi));
bmi.bmiHeader.biSize=sizeof(BITMAPINFOHEADER);
bmi.bmiHeader.biWidth=w;
bmi.bmiHeader.biHeight=h;
bmi.bmiHeader.biPlanes=1;
bmi.bmiHeader.biBitCount=1;
bmi.bmiHeader.biCompression=BI_RGB;
bmi.bmiHeader.biSizeImage=((w+7)&0xFFFFFFF8)*w/8;
bmi.bmiHeader.biXPelsPerMeter=1000000;
bmi.bmiHeader.biYPelsPerMeter=1000000;
bmi.bmiHeader.biClrUsed=0;
bmi.bmiHeader.biClrImportant=0;
bmi.bmiColors[0].rgbRed=0;
bmi.bmiColors[0].rgbGreen=0;
bmi.bmiColors[0].rgbBlue=0;
bmi.bmiColors[0].rgbReserved=0;
bmi.bmiColors[1].rgbRed=255;
bmi.bmiColors[1].rgbGreen=255;
bmi.bmiColors[1].rgbBlue=255;
bmi.bmiColors[1].rgbReserved=0;
hbmAnd=CreateDIBSection(screendc,(BITMAPINFO*)&bmi,
                   DIB_RGB_COLORS,(void**)&dstbits,NULL,0);
//
// Now create a mask, by blting the image onto the
// monochrome DDB, and then onto DIB. (If you blt straight
// from image to DIB, then it chooses the closest mask
// colour, not absolute mask colour).
HDC monodc=CreateCompatibleDC(screendc);
HDC maskdc=CreateCompatibleDC(screendc);
HBITMAP hmonobm = CreateBitmap(w,h,1,1,NULL);
HGDIOBJ holdm = SelectObject(monodc,hmonobm);
HGDIOBJ holdmask = SelectObject(maskdc,hbmClip);
COLORREF transp = GetPixel(bitdc,0,0);
SetBkColor(bitdc,transp);
// use top-left pixel as transparent colour
BitBlt(monodc,0,0,w,h,bitdc,0,0,SRCCOPY);
BitBlt(maskdc,0,0,w,h,monodc,0,0,SRCCOPY);
// the mask has 255 for the masked areas, and 0 for the
// real image areas.
//
// Well, that has created the AND mask. Now we have to zero-
// out the original bitmap's masked area to make it an OR.
BitBlt(bitdc,0,0,w,h,monodc,0,0,0x00220326);
// 0x00220326 is the ternary raster operation 'DSna', which
// is reverse-polish for "bitdc AND (NOT monodc)"
SelectObject(maskdc,holdmask); DeleteDC(maskdc);
SelectObject(monodc,holdm);
DeleteDC(monodc); DeleteObject(hmonobm);
// 
SelectObject(bitdc,holdb); DeleteDC(bitdc);
ReleaseDC(0,screendc);


// To paint transparently to 'hdc' using these AND/OR masks:

HDC memdc=CreateCompatibleDC(hdc);
SelectObject(memdc,hbmAnd);
BitBlt(bufdc,x,y,sw,sh,memdc,0,0,SRCAND);
SelectObject(memdc,hbm);
BitBlt(bufdc,x,y,sw,sh,memdc,0,0,SRCPAINT);
DeleteDC(memdc);

PlayOgg 示例

这个屏保播放嵌入为资源的OGG文件中的音乐。我曾经认为屏保中的音乐很傻(因为屏保在您不在电脑旁时运行!)但我错了:对于我的一些音乐屏保,许多高兴的父母给我写信说他们的孩子被音乐和图像的结合迷住了。OGG是一种类似于MP3的文件格式,只是更好。

主要的用户界面问题是,许多用户已在系统托盘(或在屏保偏好设置)中将声音静音,但没有意识到这一点,他们会给我发电子邮件抱怨声音不工作。我的解决方案是

  1. 在安装程序中,如果声音已关闭(无论是在系统托盘中还是仅针对屏保),我们会询问是否应将其打开。
  2. 在配置对话框中,如果对话框中声音被打开,那么我们会在系统托盘和屏保中都打开声音。

至于播放 OGG 文件的技术方面,我们使用一个在后台运行的单线程。应用程序可以向它发送“播放”、“停止”或“退出”消息。OGG 代码来自这里。谢谢!

要将此 OGG 播放功能添加到您自己的屏保中,请执行以下操作:

  1. 在“项目设置”>“链接器”>“输入”下,链接`winmm.lib`。
  2. 复制所有 OGG 线程的代码,以及 `AudioStart`...`AudioQuit` 函数;
  3. 如果你想实现静音功能,这包含在`MuteControl()`函数中,以及对`DoInstall()`和`GeneralDlgProc()`例程的修改中。
  4. 将`ogg.cpp`和`ogg.h`添加到您的项目中。

OGG 播放。这由一个在后台运行的“ogg-player-thread”处理。在本文中,我描述了它的API;要查看其实现,请参阅`audioin.cpp`中的代码。

const UINT OGGM_INIT  = WM_USER+1;
const UINT OGGM_PLAY  = WM_USER+2;   
const UINT OGGM_STOP  = WM_USER+3;
const UINT OGGM_QUIT  = WM_USER+4;
//
const UINT OGGN_DONE  = WM_APP;

启动 ogg-player-thread 如下。它以高优先级运行,因此声音不会中断。`while`-循环的目的是确保线程的消息队列已经创建。

hthread=CreateThread(NULL,0,OggPlayerThread,0,0,&idthread);
SetThreadPriority(hthread,THREAD_PRIORITY_ABOVE_NORMAL);
while (true)
{ if (PostThreadMessage(idthread,OGGM_INIT,0,0)) break;
  Sleep(0);
}

以下代码将一个项目推送到线程的播放列表。线程将负责释放缓冲区。要播放`RT_RCDATA`资源,请使用“res://#num”或“res://name”。当歌曲自然结束且`hwnd!=0`时,它将向`hwnd`发送`OGGN_DONE`。

TCHAR *buf=new TCHAR[MAX_PATH]; _tcscpy(buf,_T("test1.ogg"));
PostThreadMessage(idthread,OGGM_PLAY,(WPARAM)hwnd,
                  (LPARAM)buf);

要停止音乐,请执行以下操作

PostThreadMessage(idthread,OGGM_STOP,0,0);
// Use wParam=0 to stop the whole playlist,
// or wParam=1 to stop just the current item

PostThreadMessage(idthread,OGGM_QUIT,0,0);
WaitForSingleObject(hthread,INFINITE);       
CloseHandle(hthread); hthread=0; idthread=0; 
// Here we tell the player-thread to terminate, then wait
// until it's done, and then close it.

静音控制。函数`bool MuteControl(bool *get,bool *set)`与系统托盘中的“静音”按钮相关。它将当前设置检索到“get”变量中,如果“set”非`NULL`,则也会设置它。成功或失败返回true/false。要查看其实现,请查看`audioin.cpp`;本文重点介绍如何(以及何时)调用它。

配置对话框最好做成属性表,第一页包含通用设置(包括静音)。我们修改此第一页的`dialogproc`。如果用户已经修改了“静音”按钮,并在将其关闭的情况下点击了“确定”,那么我们确保所有静音都被关闭。

BOOL CALLBACK GeneralDlgProc(HWND hwnd,UINT msg,
                             WPARAM wParam,LPARAM lParam)
{ switch (msg)
  { case (WM_INITDIALOG):
    { // use DWL_USER to track whether the user has fiddled
      SetWindowLong(hwnd,DWL_USER,0);
      // 'MuteSound' is a global variable that's read from
      // the registry at startup. 'AudioPlay' launches the
      // ogg-player-thread, and start it playing something.
      if (MuteSound) CheckDlgButton(hwnd,113,BST_CHECKED);
      else AudioPlay(hwnd);
    } return TRUE;

    case WM_DESTROY:
    { AudioQuit();      // terminates the ogg-player-thread
    } return TRUE;

    case OGGN_DONE:     // OGGN_DONE is sent to use by the
    { AudioPlay(hwnd);  // player-thread when it's finished
    } return TRUE;      // an item. So we play it again Sam!

    case WM_COMMAND:
    { int id=LOWORD(wParam), code=HIWORD(wParam);
      if (id==113 && code==BN_CLICKED)
      { // If the user changes the button, we stop
        // or start the audio
        bool x=(IsDlgButtonChecked(hwnd,113)==BST_CHECKED);
        AudioStop(); if (!x) AudioPlay(hwnd);
        // and record the fact that the user has fiddled.
        SetWindowLong(hwnd,DWL_USER,1);
      }
    } return TRUE;

    case WM_NOTIFY:
    { LPNMHDR nmh=(LPNMHDR)lParam; UINT code=nmh->code;
      switch (code)
      { case (PSN_APPLY):
        { AudioQuit();
          bool oldMuteSound=MuteSound;
          DWORD x = IsDlgButtonChecked(hwnd,113);
          MuteSound = (x==BST_CHECKED);
          WriteGeneralRegistry();
          //
          // if the user fiddled with the sound button and
          // left sound on, we also turn off muting in the
          /// system tray
          bool fiddled = (GetWindowLong(hwnd,DWL_USER)>0);
          if (!MuteSound && (oldMuteSound || fiddled))
          { bool sysmute, ok=MuteControl(&sysmute,NULL);
            if (ok&&sysmute)
            { bool nf=false; MuteControl(NULL,&nf);
            }
          }
          SetWindowLong(hwnd,DWL_MSGRESULT,PSNRET_NOERROR);
        } return TRUE;
      }
    } return FALSE;

  }
  return FALSE;
}

对`DoInstall`函数进行了以下更改,以提示用户有关声音的信息。

...
ReadGeneralRegistry(); 
// reads in the global variable 'MuteSound' from the Saver
// Preferences. This is normally done ahead of full-screen/
// preview/config activation, but here we need it also
// ahead of install.

// Mow we read in the volume control in the system tray
bool sysmute, ok=MuteControl(&sysmute,NULL); 
const TCHAR *c=0;
if (MuteSound) c = _T("Note: the sound is currently ")
   _T("turned off for screen savers.\r\n")
   _T("Do you want it turned back on?");
if (ok&&sysmute) c = _T("Note: the sound has been ")
   _T("turned off.\r\nDo you want it turned back on?");
if (c!=0)
{ int res = MessageBox(NULL,c,_T("Saver Sound"),MB_YESNO);
  if (res==IDYES)
  { LONG lres; HKEY skey; DWORD disp,val;
    lres=RegCreateKeyEx(HKEY_CURRENT_USER,
                  (REGSTR_PATH_SETUP _T("\\Screen Savers")),
                  0,NULL,REG_OPTION_NON_VOLATILE,
                  KEY_ALL_ACCESS,NULL,&skey,&disp);
    if (lres==ERROR_SUCCESS)
    { val=0;
      RegSetValueEx(skey,_T("Mute Sound"),0,REG_DWORD,
                    (CONST BYTE*)&val,sizeof(val));
      RegCloseKey(skey);
    }
    if (ok&&sysmute)
    { bool n=false; MuteControl(NULL,&n);
    }
  }
}

AudioIn 示例

这个屏保采样当前播放的音频,进行FFT,并将其显示在屏幕上。重要的最终用户考虑因素是用户可能必须在“声音属性”中启用音频输入。大多数用户不知道如何做到这一点。因此,配置对话框必须告诉他们如何操作,并向他们展示结果。这个配置对话框显示了音频的波形,有一个名为“尝试监听”的按钮,并有详细的说明。这是`.rc`文件中的配置对话框

// Note: .rc files don't allow strings to be split over
// more than one line. But I've split them here to fit
// on the page. If you copy this out, you'll have to
// unsplit them. Easier instead to copy it from audioin.cpp
DLG_OPTIONS DIALOG 0, 0, 237, 220
STYLE DS_SETFONT | DS_MODALFRAME | WS_POPUP | WS_VISIBLE
      | WS_CAPTION | WS_SYSMENU
CAPTION "Options"
FONT 8, "MS Sans Serif"
BEGIN
  CONTROL "(waveform)",101,"Waveform",0x0,11,10,214,62
  CTEXT   "The saver will dance to the music, if it can "
          "hear any playing.\r\nCheck above that a signal "
          "is present.", -1,11,74,214,20
  LTEXT   "Audio troubleshooting:",-1,11,100,106,11
  LTEXT   "1.",-1,11,113,8,9
  LTEXT   "Right-click on the volume control on the taskbar"
          ", Open Volume Control > Options > Properties > "
          "Recording, OK. Then select whichever sound "
          "source you want to use. ""Mixer"" is a "
          "good bet.", -1,19,113,206,26
  LTEXT   "2.",-1,11,141,8,9
  LTEXT   "Some older soud cards don't support listening at"
          " the same time as playing ('full duplex'). In "
          "this case, audio will only work from microphone "
          "or CD, not from MP3s.", -1,19,141,206,26
  LTEXT   "3.",-1,11,168,8,9
  LTEXT   "Only one program at a time is allow to listen to"
          " music; this might stop the saver from listening"
          " as well. Winamp with a CD usually counts as "
          "'listening'. Windows Media Player with MP3s "
          "does not.", -1,19,168,206,25
  CONTROL "Try to listen to the music",102,"Button",
           BS_AUTOCHECKBOX | WS_TABSTOP,11,198,105,15
END

要将音频采样添加到您自己的项目

  1. 在项目设置中链接`winmm.lib`。
  2. #include <complex>,<list>
  3. 从`audioin.cpp`中复制代码主体。
  4. 如果你想在配置对话框中显示波形查看器,你必须在你的WinMain中调用`RegisterClass("Waveform")`,就像`audioin.cpp`中做的那样。

注意:FFT在调试模式下运行较慢。但是当您在发布模式下重新编译它并打开优化时,它就足够快了。无论如何,如果队列中有消息,则不会调用`fft()`,以确保它不会干扰用户响应。注意:FFT需要`buf_nsamples`>=1024。

在 Visual C++ 6 下,这段代码在默认的“发布”优化设置下生成了一个内部编译器错误。因此,我选择了“自定义优化”并全部打开了它们。有 Bug 的 VC++。在 VS.NET 和 BCB5 下一切正常。

在本文中,我只描述了使用音频输入代码的API。要了解其实现方式,请阅读`audioin.cpp`。(我对此实现非常自豪!请务必查看!)

typedef short sample_t;
const int waveid=WAVE_MAPPER, nchannels=2, frequency=22050;
const int nbufs=8, buf_nsamples=1024;  
const int buf_nlog=10, buf_nsqrt=32;   
// nsamples must be a power of 2, at least 1024
// log and sqrt: ie. 2^nlog==nsqrt^2==nsamples

这些配置常量控制音频的特性。`typedef sample_t`可以是`unsigned char`(用于8位音频)或(带符号的)`short`(用于16位音频)。Windows不支持其他格式。请注意,我通过编译时`typedef`而不是运行时变量来做出选择。这使得编译器可以生成最优代码。常量`waveid`表示要使用的音源,`nchannels`和`frequency`表示其格式。

音频系统内部将使用`nbufs`个缓冲区:当您处理一个缓冲区时,Windows可以填充其他缓冲区以接收音频。8是一个不错的数字。每个缓冲区将包含`buf_nsamples`个样本。因此,每个缓冲区持续`nsamples`/`frequency`秒(在本例中约为50ms),您的应用程序将通过消息频繁地收到通知。

对于傅里叶变换,`buf_nlog`和`buf_nsqrt`必须根据`nsamples`进行设置。同样,这两个是编译时常量,用于生成最佳代码。傅里叶变换代码要求`nsamples`至少为1024。如果您注释掉FFT代码,可以使用较小的缓冲区。这可以让您获得更频繁的通知。

void AudioStart(HWND hwnd);
void AudioStop(HWND hwnd);

sample_t *waveformData=wavbuf; 
unsigned char spectrumData[buf_nsamples][nchannels]; 
// Access the first with waveformData[i*nchannels+chan]
// The second  is the result of an FFT.
// Max frequency in the array is frequency/2

当窗口想要监听音频时,调用`AudioStart(hwnd)`。每次听到新的音频采样时,窗口都会收到`WM_APP`消息。当窗口不再感兴趣时,调用`AudioStop(hwnd)`。许多窗口可以调用`AudioStart()`;它们都会在每次采样时收到通知。当第一个窗口调用`AudioStart`时,音频采样系统被打开,当最后一个窗口调用`AudioStop`时,它被关闭。

当一个窗口收到`WM_APP`消息时,原始的波形音频数据存储在`waveformData[i*nchannels+chan]`中,其中`i`的范围在0到`buf_nsamples`之间。傅里叶数据存储在`spectrumData[i][chan]`中,其中`i`的范围相同,但此处对应于从`0Hz ... frequency/2 Hz`的频率范围。

ThreeDee 示例

此屏保使用图形硬件加速(通过OpenGL)绘制一个旋转立方体。如果您想在自己的代码中使用相同的技术,请执行以下操作:

  1. #include <GL gl.h><GL glu.h>
  2. 在“项目设置”>“链接器”>“输入”下,添加`opengl32.lib`和`glu32.lib`。
  3. 在`WinMain`中执行`RegisterClass`的部分,OpenGL要求屏保窗口具有`CS_OWNDC`样式。
  4. 复制中间的代码块,包括函数`ChoosePixelFormatEx()`、`EnsureGL()`、`EnsureMode()`等。

存在一些问题。在某些系统(尤其是Win95/98)上,加载3D图形驱动程序可能需要很长时间。这对于“显示属性”>“屏幕保护程序”对话框中的预览来说可能无法接受。您可以考虑在预览中不使用任何3D图形——而是只显示一张静止图像。

在多显示器上实现3D加速的可能性微乎其微。因此,在全屏模式下运行时,此屏保仅在主显示器上执行其3D操作,而其他显示器则留空。如果初始化3D图形时发生错误,它会将错误消息在屏幕上弹跳。

如果我们的3D窗口移动,某些硬件显卡需要被通知。但考虑一下当我们在“显示设置”对话框中有一个预览窗口的情况。当用户移动对话框时,我们永远不会收到任何通知。我们的解决方案是在每个计时器滴答时调用`GetWindowRect()`,并为显卡重置视口。这在`EnsureProj()`函数中。

我们确实想要硬件加速。正常的API函数`ChoosePixelFormat()`对硬件加速的专注程度不如我们所愿。因此,我们使用自己的`ChoosePixelFormatEx()`函数,它更专注于此。

这个屏保有一个选项可以改变全屏模式的屏幕分辨率。这里有几个问题。我们不会立即改变屏幕模式,而是等到我们的窗口绘制自身(从而隐藏正常桌面)。这样做是为了当我们改变模式时,用户不会看到现有窗口的改变。我们执行此操作的地方是在`OnTimer`中,响应第一个计时器滴答。这是“惰性求值”的一个例子,即初始化只在实际需要时才进行,而不是提前进行。

要更改屏幕分辨率,请调用`ChangeDisplaySettings()`函数。但在某些系统上,如果您只指定宽度/高度/BPP,而不指定刷新频率,则它会给您一个难看的低频率。为避免此问题,我们枚举所有显示模式,并选择具有最佳刷新频率的模式。这在`EnsureMode()`函数中。

当模式更改发生时,系统会发送一些虚假的鼠标移动消息。通常,鼠标移动会导致屏保终止。我们使用一种简陋的方式来避免这种情况:我们设置了全局标志`IsDialogActive`,该标志通常仅用于屏幕上出现的密码验证对话框。当该标志设置时,我们的屏保窗口`WndProc`会忽略鼠标移动消息。我鼓励所有使用屏幕模式更改的人在`SCRDEBUG`设置为false且鼠标灵敏度设置为高的情况下进行测试。

在Win95/98/ME上,屏保本身会调用密码验证对话框。但如果我们在对话框显示的同时渲染3D图形,并且正在调用`SwapBuffers()`,那么密码验证对话框就会被破坏。为避免这种情况,我们在对话框活动时暂停动画。

在本文中,我们重点介绍 API 及其调用方式。要查看各种函数的实现,请阅读`opengl.cpp`。

int ChoosePixelFormatEx(HDC hdc,int *p_bpp,int *p_depth,
                        int *p_dbl,int *p_acc);
// int bpp=-1, depth=16, dbl=0, acc=1;
// ChoosePixelFormat(hdc,&bpp,&depth,&dbl,&acc);
// Initial values of the variables are -1=don't care,
// 16=desired bpp, 1=turn-on, 0=turn-off
// It chooses the best pixel format, and updates the
// parameters with what was chosen.

void EnsureGL(HDC hdc);
// Question: when is a safe time to create a GL context?
// Answer: Not too early, otherwise it fails. In response
// to the first WM_TIMER is a good place. (You can call this
// function multiple times, eg. every timer tick; it only
// does stuff the first time you call it.)

void ProjGL(const RECT rc);
// This 'prepares the projection', by telling the OpenGL
// system our window's size and screen position and camera
// parameters. This must be called every time the window
// moves on-screen. But we don't. Therefore, call it every
// timer tick just to be on the safe side. (If the window
// hasn't actually moved, then the function does nothing).

void ExitGL();
// Call this when you've finished.
// In ~TSaverWindow (ie. WM_DESTROY) is a good place.

void EnsureMode(HWND hwnd);
// Changes to a 640x480x16bpp resolution. The window 'hwnd'
// should be a topmost window which has already drawn
// itself, so as to blank out the desktop. (You can call
// this function multiple times, eg. every timer tick.
// If the mode has already changed, it does nothing).

void ExitMode();
// Restores screen to normal when you're done.
// In ~TSaverWindow (ie. WM_DESTROY) is a good place.

以下代码显示了何时以及如何从屏保代码片段中调用上述函数。

struct TSaverWindow
{ HWND hwnd; int id;
  HDC hdc;
  // OpenGL requires the CS_OWNDC style. This is it.

  TSaverWindow(HWND h,int i) : hwnd(h),id(i),hdc(0)
  { SetTimer(hwnd,1,50,NULL);
  }

  ~TSaverWindow()
  { KillTimer(hwnd,1); timer=false;
    if (hdc!=0) ReleaseDC(hwnd,hdc); hdc=0;
    if (id==0) {ExitGL(); ExitMode();}
  }

  void OnPaint(HDC hdc,const RECT &rc)
  { // If we're using OpenGL in this window, and it is
    // setup ok, then OnTimer will do a complete redraw.
    // otherwise we do it ourselves.
    if (id==0 && glok &&!IsDialogActive) {OnTimer();return;}
    //
    FillRect(hdc,&rc,(HBRUSH)GetStockObject(BLACK_BRUSH));
    if (id==0 && !IsDialogActive)
    { // If there was an opengl error, we display it.
      int y=(GetTickCount()/20)%rc.bottom, x=y;
      RECT trc; trc.left=x-400; trc.top=y-20;
      trc.right=x+400; trc.bottom=y+20;
      SelectObject(hdc,GetStockObject(DEFAULT_GUI_FONT));
      SetBkColor(hdc,RGB(0,0,0));
      SetTextColor(hdc,RGB(255,128,128));
      DrawText(hdc,glerr.c_str(),-1,&trc,
               DT_CENTER|DT_VCENTER|DT_SINGLELINE);
    }
  }

  void OnTimer()
  { // On secondary monitors, just do normal GDI painting:
    if (id>0) {InvalidateRect(hwnd,NULL,FALSE); return;}
    // If we're the primary monitor, change screen. We do
    // this screen-change lazily, here instead of in the
    // constructor, so that our window has had a change to
    // paint itself (so hiding the desktop) before we do
    // the mode-change.
    if (id==0 && ChangeScreenMode) EnsureMode(hwnd);
    // Our window has CS_OWNDC. 'hdc' is it.
    if (hdc==0) hdc=GetDC(hwnd);
    // EnsureGL can be called multiple times:
    EnsureGL(hdc);
    // We have to reproject ourselves every time our window
    // moves (eg. in the display properties dialog). This
    // can't be done through a WM_MOVE message since, as a
    // child in the display properties, we won't even get
    // a WM_MOVE when the dialog is dragged around.
    RECT rc; GetWindowRect(hwnd,&rc); ProjGL(rc);
    // If opengl didn't work, resort to normal GDI painting
    if (!glok) {InvalidateRect(hwnd,NULL,FALSE); return;}
    // If the password-verify dialog is up (Win95/98/ME
    // only) then we shouldn't be calling SwapBuffers
    // on top of it:
    if (IsDialogActive) return;

    RotX += 5; RotY += 5; RotZ += 5;

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 
    glMatrixMode(GL_MODELVIEW); glPushMatrix();
    // ... all the gl drawing functions go here
    glPopMatrix();
    SwapBuffers(hdc);
  }
};
© . All rights reserved.