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

一个完整的C#屏幕保护程序,可以在多显示器系统上进行双缓冲!

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (35投票s)

2005年5月31日

34分钟阅读

viewsIcon

281388

downloadIcon

1828

示例屏幕保护程序源代码。还提供迷你预览!

引言

我想编写一个屏幕保护程序。是的,我打赌每个人都会经历这个阶段,但这次我获得了所需的信息,因此本文的目的是向您传达那些多年来缺失、隐藏或仅供少数特权人士使用信息。

首先,我要感谢 Rakesh Rajan 的精彩文章,他的文章启动了我的思路,我得以填补空白,并完成了一个具有我真正想要的所有功能的屏幕保护程序。您可以查看他的精彩文章 - 如何在 C# 中开发屏幕保护程序

嗯,那个 URL 好长,希望它能显示完整!再次感谢!更多致谢信息可在源代码中找到。

双缓冲

这是一种实现无闪烁、更快速绘制的技术。它涉及在屏幕外位图上进行所有绘制。由于此位图未显示,绘制例程运行速度会快得多,因为绘制不会触发重绘事件。所有绘制完成后,将进行处理以显示。有一些极其快速的双缓冲,窗体和其他控件内置了这些,但它也有其问题,这意味着您可能会被 InvalidOperationException 错误折磨。这是一个特别棘手的错误,因为在我的程序中,它会直接穿过我为这个错误设置的 catch 语句,就好像它不匹配一样,更糟糕的是,它非常不稳定,可能立即出现,也可能运行数小时后才露出丑陋的面目。要摆脱这个有害的错误,请在完成一个绘制周期后,在您的 Graphics 对象上使用 Flush 方法。例如... pev.Graphics.Flush(System.Drawing.Drawing2D.FlushIntention.Flush); 这将消除错误。该错误的原因是,除非您使用上述命令,否则有时缓冲区未正确绘制到屏幕,您的代码可能会在缓冲区被写入时,或者通常在缓冲区被释放时进行另一个重绘周期,从而导致冲突,您的代码试图使用缓冲区,而此时缓冲区不可用!GDI+ 不会对此进行检查。如果您使用上述 Flush,缓冲区将被写入屏幕并刷新,并在您需要时准备好使用。

更新:2005 年 6 月 18 日 - 我已解决了上述问题。这是一个非常有趣的发现,为什么会发生。有关更多信息,请参阅文章末尾。

2005 年 6 月 10 日 - 添加了很棒的 **新** 功能。您必须尝试在七个屏幕群上运行此程序!

我已成功地让此屏幕保护程序同时在同一系统上的多个显示器上以双显示模式运行!我将上传新代码,然后我会处理代码片段,因为这是一项艰巨的任务。据我所知,这是唯一能够做到这一点的屏幕保护程序。我尝试了许多其他程序,但没有一个成功。我在源代码中的 EntryPoint.cs 中解释了我所做的工作。我遇到了一些稳定性问题。希望我于 2005 年 6 月 13 日所做的更改可以解决其中一些问题。

2005 年 6 月 16 日 - 我实现了事件和委托驱动的引擎。我将一些变得相当混乱的代码分离到单独的类中,解决了稳定性问题并修改了代码片段。

请参阅文章末尾!

在互联网上搜索了数周,试图找到编写自己的屏幕保护程序所需的信息,却发现了非常不完整的示例,这些示例忽略了关键信息,或者示例巨大但缺乏有意义的注释,以至于在我找到答案时,我已经忘记了问题。几乎所有的示例都无法正常工作,或者存在阻止我测试它们的错误。我决定编写一个“完整”的屏幕保护程序并将其发布到网上,以便其他人不必经历我所遭受的寻找所需信息的痛苦。我之所以在“完整”一词上加上引号,是因为我没有实现的唯一功能是当程序接收到作为参数的任何参数时显示密码对话框。我不想让 Windows 中的某个错误或未来的更改导致我的屏幕保护程序锁定用户系统。注意:在 NT 2000 上,如果计算机启用了在 X 分钟不活动后锁定计算机的功能,并且在屏幕保护程序运行时计算机被锁定,那么当您触碰机器时,屏幕保护程序会关闭,但 Windows 提示输入密码的欢迎屏幕不会显示。修复很简单。按下 Ctrl-Alt-Delete 或 Ctrl-C,就会出现欢迎屏幕,您就可以正常操作了。

本文仍将为您提供编写自己的屏幕保护程序所需的信息。

我需要解决的问题是....

  1. 如何将配置对话框作为 Windows 显示属性框的子窗口附加。
  2. 如何在上述表单上绘制迷你预览框。
  3. 在哪里放置绘制例程以及如何执行它们。
  4. 如何将我的配置保存到注册表,以及最近,如何将配置信息保存到 XML 文件而不是注册表。
  5. 如何让屏幕保护程序实例绘制到系统上所有活动的屏幕!

使用代码

供您自己使用,只需将 clsInsects 替换为您自己的绘制例程类,然后修改主 ScreenSaverForm.cs 中的 load 事件和 paint 事件,以及 EntryPoint.csScreenSaver.csMiniPreview.csXmlScreenSaverConfig.csConfigForm.cs 中的代码,按您的喜好进行修改并尽情玩乐!

该项目以前被分成几个 DLL 和一个 EXE。现在没有 DLL 了,您只需编译,将 EXE 重命名为 .scr 并复制到 system32 目录,然后将其选为您的屏幕保护程序即可完成,或者您可以配置一些不错的选项。

StructsAndFunctions.cs 中的代码可以用于您自己的程序,它不依赖于任何其他模块,但是 Insects 类和其他模块都依赖于 StructsAndFunctions.cs

ScreenSaverFormload 事件初始化窗体的大小和我的 Insects 类实例(负责所有绘制)。然后在 EntryPoint 类的 Main 方法中的一个 while 循环通过适当的方法调用每个窗体,以触发屏幕绘制并显示结果。

**这是我让程序与多个显示器协同工作的方式**。代码在 Entrypoint.cs 中的 "/s" case 语句下。

我创建了一个 ScreenSaverForm 数组,它负责全屏绘制,称为 sf。然后,我通过一个 while 语句遍历数组,并调用数组中每个 ScreenSaverForm 的 show 方法,然后调用 Application.DoEvents(),当 manualEvents 发出信号表明某个委托已被调用以显示就绪的绘制时。我发现我必须通过对窗体使用 Refresh() 方法来强制窗体绘制图形,然后调用 Application.DoEvents() 来使绘制出现在多个显示器上。

  1. 如果系统只有一个显示器,我发现使用 sf[i].Invalidate(sf[i].Bounds) 方法(其中 i = 显示器编号,主显示器)效果很好,并且当此方法未能使第二个显示器显示任何内容时速度更快。第一个显示器会占用所有 CPU 时间,而第二个显示器则完全被忽略。
  2. 我发现,如果我在有多个活动屏幕时使用 ScreenSaverFormRefresh() 方法,我就能够触发两个显示器的绘制,并在调用 Application.DoEvents() 时看到结果。我不再使用 Invalidate,因为它导致了与时间同步的问题,因为我了解到事件位于单独的线程上,而 Refresh() 是获取一个变量的状态的**真相**的唯一方法,我用它来查询窗体是否已准备好进行下一次 PaintMe() 调用。
  3. 我的屏幕保护程序窗体中的绘制代码是每个显示器一个窗体,但我遇到了程序在窗体完成绘制并准备再次绘制之前触发其绘制的问题。

更新:我最终编写了一些委托并将它们传递给窗体,并使用这些委托通过 Application.DoEvents() 通知调用类 ScreenSaver 窗体已准备好进行绘制,然后通过窗体的 Refresh() 方法调用新的 paint 事件。

最初,我编写的代码是为了使用多线程,但我发现工作线程速度非常慢,不如让 ScreenSaverForm 的所有实例都在主线程下运行。这也有助于避免线程锁定问题。通过 **一个** 线程充当多个线程,就不会有线程锁定问题。现在,我尽可能在 paint 事件中进行绘制。

您会立即注意到的一件事是,源代码充满了注释,以至于代码本身几乎被掩埋了,原因有两个...

  1. 这比搜索大量的难以理解的代码要好,结果发现我试图模仿的功能根本不在示例中,或者示例如此糟糕以至于我无法使其工作,或者发现代码太难理解以至于我无法实现或找到解决方案。
  2. 代码包含一种特殊的注释,以 /// 开头,其中包含标签,IDE 使用这些标签生成一个本地网页,用户可以通过单击链接来查找代码中几乎所有对象的资讯,并从中获得对代码的良好理解。在提取此示例创建的文件夹中,您会找到一个名为 CodeCommentReport 的文件夹,其中包含 Solution_SwarmScreenSaver.HTM。这是您应该用浏览器打开的文件,以查看此很棒的功能。另请参阅 GettingStarted.txt 以获取有关安装屏幕保护程序的资讯。

大多数注释是出于第 2 个原因,但我希望确保代码能被理解。我尽量少做读者什么都知道的假设。

必须解决的问题...

  1. 代码注释 HTML 未正确工作。要找到修复方法,请访问 链接,或按照以下步骤进行操作...

    在 VS 2003 和 Windows XP SP2 中构建注释网页。如果您使用 Visual Studio 2003 中的“构建注释网页”创建的文档,在升级到 Windows XP SP2 后,您在查看文档时会遇到问题。目前,您可以下载测试版的发布候选版本 此处:服务包包含许多修复程序,以提高 Windows 和 Internet Explorer 的整体安全性。不幸的是,一项修复程序阻止了“构建注释网页”功能生成的标签。

    变通方法

    最简单的变通方法是使用 Visual Studio 进行查找和替换,将文档中的标签更改为 Internet Explorer 接受的标签。

    1. 转到 编辑/查找和替换/在文件中替换。
    2. 在“查找内容:”中输入:<!-- saved from url=(0007)http:// -->
    3. 在“替换为”中输入:<!-- saved from url=(0014)about:internet -->
    4. 确保未选中“正则表达式”。
    5. 在“查找范围”中输入存储网页的目录。
    6. 单击“全部替换”以执行搜索。

    背景

    此处的问题标签称为“Web 标记”。IE 会将“Web 标记”添加到从 Internet 保存的网页。这样 IE 就可以检测到内容最初来自 Internet 区域,并且应被视为该区域的内容。在这种情况下,该页面不是从 Internet 保存的,而是 Visual Studio 使用 http:// 将其强制到 Internet 区域。在 Windows XP SP2 中,IE 团队进行了工作以收紧允许的项,并且 http:// 被意外排除。IE 团队向我保证,VS 2003 生成的原始 Web 标记不构成安全风险。

    如果您有任何疑问,请告诉我。

    -Sean Laberee

    发布于 2004 年 7 月 16 日星期五下午 5:07,作者 VSEditor。

    重要的命名空间、类和文件名....

    CommonCode 命名空间包含解决方案中各种项目使用的结构定义。 StructsAndFunctions.cs 包含这些结构和一个名为 CommonFunctions 的类,该类包含用于生成随机数和调整颜色的例程,因此群集不会太暗而看不见。这些被 clsInsects 中的绘制例程使用。根据该网站,颜色调整例程并不完全准确,但它**帮助很大**。感谢 George Shepherd 的 网站

    XmlSwarmScreenSaverConfig.cs 包含一个自恢复的 XML 配置读写器,用于保存您的屏幕保护程序设置。该保护程序尝试读取名为 SaverName.scr.config.xml 的配置。您必须将生成的 .exe 扩展名重命名为 .scr,并将编译此源代码产生的两个 DLL 复制到 System32 目录。

    如果读取器未能找到文件,或者发现文件结构因我添加了某个功能而有所不同,它会创建一个包含默认值的新文件。这使得配置保存极其透明且易于使用。您可以随意使用此模块在您的应用程序中保存配置信息。

    SwarmScreenSaver 命名空间包含 EntryPoint 类,程序从这里开始执行,以及 CONFIG 和 ScreenSaverForm 的定义,ScreenSaverForm 是用于在全屏模式下显示绘制的窗体。还有一个 MiniPreview 类,负责绘制到迷你预览窗口。ScreenSaver 类用于在全屏模式下运行屏幕保护程序,并在完成后处理应用程序的关闭。

    Insects 命名空间包含我的 clsInsects 代码,该类负责绘制接收到的图形对象。

  2. 如何将我的应用程序的窗口作为 Windows 屏幕保护程序对话框的子窗口,使用 Windows 指示用户想要配置屏幕保护程序时传入的数字。

    当用户单击设置按钮时,Windows 会以如下命令行启动程序... /c:########,其中 # 是 10 位数字中的一位数字,是上方显示对话框的句柄。我已对此进行了修改,以便用户可以键入 swarm /c 来打开 CONFIG 框。

    之所以需要这样做,是因为如果用户关闭了对话框,我的程序也应该关闭。在 C# 中建立这种父子关系是不可能的,因为较新版本的 Windows 实现了安全机制,不允许这种关系,除非两个窗口都在同一个应用程序中!解决 CONFIG 窗体的问题是放置一个计时器控件在窗体上,并设置它每十分之一秒检查一次显示属性窗口的可见性,如果它消失则关闭。

    下面使用的 API 调用位于 StructsAndFunctions 中的一个类中,该类被命名为 CommonFunctions。首先,我声明了我需要的 API 调用,然后声明它们的包装器,因为它们不能直接调用,只能通过这些包装器调用。

    小心!!代码来了!

    声明一个 API 调用以确定窗口是否可见。hWnd 是正在检查可见性的窗口的句柄。如果窗口可见,则返回 true,否则返回 false

    [DllImport("user32.DLL",EntryPoint="IsWindowVisible")]
      private static extern bool IsWindowVisible(IntPtr hWnd);

    声明 API 调用以获取客户端窗口的大小和位置。

    hWnd 是正在处理的窗口的句柄。包含大小和位置的结构,该结构将被填充为传递的 RECT rect 实例中的客户端区域大小和位置(以像素为单位)。成功则返回 true,否则返回 false

    [DllImport("user32.dll")]
      private static extern bool GetClientRect(IntPtr hWnd, ref RECT rect);

    调用 IsWindowVisible API 调用的包装器。hWnd = 桌面属性窗口的句柄。如果可见,则返回 true,否则返回 false

    public bool IsWindowVisibleApi(IntPtr hWnd)
    {
        return IsWindowVisible(hWnd);
    }

    调用 user32.dll 中的 GetClientRect API 函数以获取窗口客户端区域大小的包装器。hWnd 是桌面属性对话框的句柄。rect 是一个 RECT struct 实例,将被填充为客户端区域的位置和大小。成功则返回 true,失败则返回 false

    public bool GetClientRectApi(IntPtr hWnd, ref RECT rect)
    {
       return GetClientRect(hWnd, ref rect);
    }

    这是我的 CONFIG 窗体的一个示例片段。

    namespace SwarmScreenSaver
    { 
     public class ConfigForm : System.Windows.Forms.Form 
     {   //... Some code inserted by the form creater.  
      // Put the declaration of cParentWindowHandle 
      // in your class declaration 
      // and above the class constructor.
      // the c prefix is to remind me that this 
      // is a handle to the config window.
      private static IntPtr cParentWindowHandle = 
                                      new IntPtr(0);
      // The class instance cf contains 
      // API call wrappers other
      // functions we will use.
      CommonCode.CommonFunctions cf = new CommonFunctions ();

    这是构造函数。将下面的代码插入您的配置窗体构造函数中 InitializeComponants 调用之后。我对下面的代码进行了修改,以便在 CONFIG 被运行的 swarm 使用 /c 作为命令行启动时也能正常工作。

    在配置模式下,句柄从第三个字符开始 - 例如:/c:345678。

    public ConfigForm(int IntArg)
    {
      InitializeComponent();
      // The line above is inserted by the form generator VS.

    ***** 您需要插入的代码开头。

    如果没有句柄,则跳过。用户通过命令行启动了 CONFIG。我使用 EntryPoint 类中的解析例程来获取句柄作为数字,然后根据需要将其传递给程序的其余部分。

    if (IntArg != 0) 
    {
      cParentWindowHandle = (IntPtr) IntArg; 
      // start the timer to checking if the parent 
      // window is closed if needed. 
      CheckToCloseTimer.Enabled = true;
    } // Constructor end. --------------------------

    以下是处理关闭 CONFIG 窗体的代码....

    // Here is my event handler for the timer ...
    private void CheckToCloseTimer_Tick(object sender, 
                                      System.EventArgs e)
    {
    if (cf.IsWindowVisible(cParentWindowHandle) == false) 
     {
        // Turn off the timer.
        CheckToCloseTimer.Enabled = false; 
        Close(); // Close the form.
       } // End if.
      } // end timer eventhandler.  
     } // End class definition.
    } // end namespace SwarmScreenSaver.

    以上就是控制 CONFIG 窗体在显示属性框关闭时关闭所需的所有内容。请仔细查看代码中检索、验证和保存配置数据的各个方法。

    注意:我想实际跟踪 Windows 显示属性对话框的位置,但当我移动它时,上面的代码会认为它不可见并关闭我的 CONFIG 窗口。

  3. 当屏幕保护程序收到 /p:####### 时,表示我的屏幕保护程序应该绘制到显示属性对话框中的小型迷你预览窗口。我们需要获取我们在其中绘制迷你预览的小黑盒的大小。为此,我们需要一个 RECT 结构来传递。

    这是我在 CommonCode 命名空间中对 Rectangle 的定义。**注意**!!这实际上不在这个文件中,而是在别处,这里是为了清晰起见。这个 RECT 定义在 StructsAndFunctions 类中。rect 通过 GetClientRectApi 传递给 API 调用 GetClientRect,以获取迷你预览窗口的大小(以像素为单位)。。。

    public struct RECT 
    { 
    public int left;  
    public int top;  
    public int right;  
    public int bottom;
    public RECT(int l, int t, int r, int b)  
      {   
       left = l;   
       top = t;   
       right = r;   
       bottom = b;  
      }  // End struct constructor.
    } // End struct.

    这是 EntryPoint 类的代码开头。

    using CommonCode.XmlConfig; 
    namespace SwarmScreenSaver {

    [STAThread] 在 Main 前面已不再需要,因为新的 1.1 框架。

    public class Entrypoint
    {
        // This parses the command line into a prefix 
        // and ArgHandle int. This = 0 if no handle.
        private static void ParseArgsToPrefixAndArgInt(string[] args, 
                             out string argPrefix, out int argHandle)
        {
            string curArg; // The current argument we are processing.
            // we need to remove this trash.
            char[] SpacesOrColons = {' ', ':'}; 
            switch(args.Length)
            {
                
                case 0:   // Nothing on command line, so 
                          // just start the screensaver.
                    argPrefix = "/s"; // Start in full sized screensaver 
                                      // mode by default.
                    argHandle = 0;    // No preview window to watch, 
                                      // therefore no handle.
                    break;
                case 1:   // One argument. Usually means a config command.
                    curArg = args[0];
                    argPrefix = curArg.Substring(0,2); // Get the prefix.
                    // Drop the slash /? part.
                    curArg = curArg.Replace(argPrefix,""); 
                    curArg = curArg.Trim(SpacesOrColons); // Remove trash.
                    // if nothing more return 0. else handle.
                    argHandle = curArg == "" ? 0 : int.Parse(curArg); 
                    break;
                case 2:   // If two arguments, it is probably a mini-preview.
                    argPrefix = args[0].Substring(0,2); // Get the prefix.
                    // Return the handle as an int.
                    argHandle = int.Parse(args[1].ToString()); 
                    break;
                default:   // If we don't put this here, compiler has a 
                           // fit about unassigned variables.
                    argHandle = 0;
                    argPrefix = ""; 
                    break;
            } // End of switch 
        } // End of method.

    这是程序的起点。它尝试读取,如果失败则创建配置信息,从 swarm.scr.config.xml 读取,并包含一个 case 语句,处理各种命令行状态和请求。

    static void Main(string[] args) {
    
        // Variables to hold configuration and 
        // parsed command line results.
        int baseVelocity;
        int beeCount; // Number of bees per swarm.
        // Number of seconds a swarm remains a 
        // particular color.
        int colorCycleSeconds; 
        // if true, overrides above and swarm is 
        // multi-colored bees.
        bool glitterOn; 
        // Number of swarms on each screen.
        int swarmsPerScreen; 
        // /s for full screen mode, /p for preview 
        // mode, /c for config mode.
        string argPrefix; 
        int argHandle; // Will be 0 if no number found.
        XmlConfigSaver xm = 
              new CommonCode.XmlConfig.XmlConfigSaver();
        // read xml config ini file, if missing, create 
        // and return default values.
        xm.readConfigXml(out baseVelocity, out beeCount, 
                   out colorCycleSeconds, out glitterOn, 
                   out swarmsPerScreen);
        
        // We are done with the xml config class for now.
        xm = null; 
        if (args.Length > 2) 
        {
            MessageBox.Show("Too many arguments " + 
                                 "on the command line.");
            return;
        }
        // This is a routine to get the ArgPrefix and 
        // an int for the window
        // handle or zero if there is no handle.
        ParseArgsToPrefixAndArgInt(args, out argPrefix, 
                                          out argHandle);
        switch (argPrefix)
        { // A full screen preview is desired. 
            case "/p": 
            // No handle. Full preview desired.
            if (argHandle == 0) goto case "/s";  
            else
            {
                // Create a mini preview.
                MiniPreview mpTemp = new MiniPreview(); 
                mpTemp.DoMiniPreview(argHandle, baseVelocity, 
                         beeCount, colorCycleSeconds,glitterOn);
                // This does not execute until preview is closed.
                mpTemp = null; 
            }
            // break out of the main loop and exit 
            // program. End of MiniPreview section.
            break;
    1. 显示属性对话框在调用 IsWindowVisible API 返回 true 之前的几秒钟内似乎是不可见的。我必须编写一个循环,等待长达 30 秒才能显示窗口。我可能只需要 4 秒就能过得去,但这没关系,我不必等太久。这是解决该问题的代码。与 CONFIG 窗体一样,将此代码放在包含您的 static void Main(String[] args) 方法调用的 .cs 文件中。在我的例子中,我的类是 EntryPoint.cs 中的 EntryPoint。以下是代码,处理此解决方案的代码用星号括起来。请查看下一段代码中的**斜体**部分。

      这是 MiniPreview 类中的代码...

      using System;
      using CommonCode; // Useful commonly used 
                        // functions such as API calls.
      using System.Windows.Forms; // So we can show 
                                  // a messagebox.
      using System.Drawing;
      using Insects; // My drawing class.
      using System.Threading; // To slow the preview 
                              // mode to normal speed.
      
      namespace SwarmScreenSaver
      {
           public class MiniPreview
           {
               public void DoMiniPreview(int argHandle, 
                   int baseVelocity, int beeCount,
                   int colorCycleSeconds, bool glitterOn)
               {
                  // Pointer to windows Display Properties window.
                       IntPtr ParentWindowHandle = new IntPtr(0);
                  
                  // Get the pointer to Windows Display 
                  // Properties dialog box.
                    ParentWindowHandle = (IntPtr) argHandle;
                    RECT rect = new RECT();
                  
                  /* The Using construct makes sure all 
                  resources are disposed if errors. */
                  // This is the graphics obect for the 
                  // mini-preview window from the OS.
                  
                     using(Graphics PreviewGraphic = 
                          Graphics.FromHwnd(ParentWindowHandle))
                     {
                          CommonFunctions cf = new CommonFunctions();
                          // Get the dimensions and location 
                          // of the preview window.
                          cf.GetClientRectApi(ParentWindowHandle, 
                                                        ref rect);
                          // We need to wait for the preview 
                          // window to return visible.
                          // The parent window is never ready when 
                          // preview is chosen
                          // before we try to get the visible state for 
                          // the first time or so.
                          // So we must wait. I limit my waiting 
                          // to 30 seconds.
                         
                          
                          DateTime dt30Seconds = 
                                  DateTime.Now.AddSeconds(30);
                          while (cf.IsWindowVisibleApi(
                                          ParentWindowHandle) == false)
                          {
                               // If times out, exit program.
                               if (DateTime.Now > dt30Seconds) return; 
                               // Respond to windows events.
                               Application.DoEvents(); 
                          }
                      
                          // Create a bitmap for double buffering.
                          Bitmap OffScreenBitmap = 
                              new Bitmap(rect.right - rect.left,
                              rect.bottom - rect.top, PreviewGraphic);
                      
                          // Create a Graphics object 
                          // for OffScreenBitmap.
                          Graphics OffScreenBitmapGraphic = 
                                  Graphics.FromImage(OffScreenBitmap);
                      
                          /* Insects is a class that contains 
                          an instance of a Wasp,
                          a Swarm and routines for drawing 
                          to the output graphic. */
                          clsInsects insects = 
                                 new clsInsects(baseVelocity, 
                                 beeCount, colorCycleSeconds,glitterOn);
                          // Do some intialization, that was not 
                          // allowed in the constructor.
                          // The 3 means the wasp can't get closer 
                          // to the edge than 3 pixels.
                          insects.initSwarm(3, rect.right - rect.left, 
                                                 rect.bottom - rect.top);
                      
                          // Now that the window is visible ...
                          while (cf.IsWindowVisibleApi(
                                               ParentWindowHandle) == true)
                          {
                              // Since the drawing routine 
                              // expects a black background.
                              OffScreenBitmapGraphic.Clear(Color.Black);
                              // Execute an iteration of drawing the 
                              // wasp and a swarm of bees.
                              insects.DrawWaspThenSwarm(
                                                OffScreenBitmapGraphic);
                              // Slow down the mini-preview.
                              Thread.Sleep(50); 
                              try
                              {
                                // Draw the image created.
                                PreviewGraphic.DrawImage(OffScreenBitmap,
                                               0,0,OffScreenBitmap.Width,
                                                 OffScreenBitmap.Height);
                              }
                              // the most likely reason we get an 
                              // exception here is because
                              catch 
                              {   
                                  // the user hits cancel button while 
                                  // drawing to mini-preview.
                                    break; // Either way we must get out 
                                           // of the program.
                              }
                          
                              // We don't need to clear the 
                              // bitmap, because 
                              // windows does this for us here.
                              Application.DoEvents(); // Wait for events 
                                                      // to be processed.
                          } // End of while.
                          // CLOSING DOWN preview.
                          insects.StopNow = true;
                          cf = null;
                          OffScreenBitmap.Dispose();
                           OffScreenBitmapGraphic.Dispose();
                           // We are done, trash this.
                           PreviewGraphic.Dispose(); 
                     } // End of using statement.
                } // End of while preview window visible.
           } // End of definition of MiniPreview.
      } // End of namespace SwarmSaver for MiniPreview class.

      这是在 EntryPoint 中处理全屏屏幕保护程序模式的代码...

      case "/s": // Start screensaver.
              ScreenSaver screenSaver = new ScreenSaver();
              screenSaver.RunMeTillShutdown( baseVelocity, 
                              beeCount, colorCycleSeconds, 
                              glitterOn, swarmsPerScreen);
              screenSaver = null; // This doesn't execute 
                                  // until shutdown is done. 
              break; // Exit program we are done.
              default: // If something strange is 
                       // passed in, just ignore and exit.
              break;
          } // Switch end.
      } // End of try statement.

      现在,我只使用下面的 catch 来显示任何未处理的异常。

      catch(System.Exception e)
      {
           MessageBox.Show("ScreenSaver:" + e.Message + 
                           " Src:" + e.Source.ToString());
      }

      现在这里是 ScreenSaver 类中的代码,它处理全屏模式。请特别注意粗体部分。我将在文章末尾的更新部分讨论这些内容。

      using System;
      using System.Windows.Forms;
      using System.Diagnostics; // For logging to error log.
      
      namespace SwarmScreenSaver
      {
           // Declare the DonePaitingDelegate 
           // signature. Used by the screensaver
           // forms to notify this class that 
           // painting is done.
          
           public delegate void DonePaintingDelegate(int screenNumber);
          
           // Declares the signature of the Shutdown Delegate.
           // Used to create actual delegate instances later.
          
           // Called when saver shutting down.
           public delegate void ShutDownDelegate(); 
          
           public class ScreenSaver
           { 
               // Actual delegate instances.
               
               // Assigned to a method later. 
               public DonePaintingDelegate DonePaintingDel; 
               public ShutDownDelegate ShutDownDel;
              
               int screenCount; // We exit when this becomes = 0. 
                                // We assign it later.
              
               // When true, we shut down all 
               // screens and set screenCount = 0;
               
               // Set to true by a
               // delegate call from a form. 
               bool shuttingDown = false; 
              
               // This will contain one ScreenSaverForm 
               // per active screen.
               ScreenSaverForm [] sf;
              // An array of event objects, these are 
              // special objects that provide
              // cross thread signaling. The while loop 
              // below goes to sleep until
              // the one of the events in this array is 
              // set. Instead of gobbling up
              // cpu cycles like a hungry animal by 
              // loop while waiting for an event
              // to occur, these nice manualEvents provide 
              // a way to tell the loop thread
              // to stop looping and wait 
              // till there is an event to respond to!
              // The last element in this array is for 
              // signaling that a shutdown event
              // has occurred, and the main thread 
              // wakes up and the program exits. 
              // The other elements are objects for 
              // signaling that a form is finished
              // drawing and is ready to display.
              ManualResetEvent[] manualEvents;
              
              // This constructor creates the 
              // manualEvents array instances, sets the 
              // length to hold the paint events for 
              // the screens plus a shutdown event.
              // Finally, set all events has being uncalled.
              public ScreenSaver()
               {
                   manualEvents = new 
                        ManualResetEvent[Screen.AllScreens.Length+1];
                   for (int i = 0; i <= Screen.AllScreens.Length; i++)
                   {
                       manualEvents[i] = new ManualResetEvent(false);
                   }
               }
              
               // This is called by ScreenSaverForm 
               // via the delegate when done painting.
               
               // Keep delegates simple.
               public void DonePainting(int screenNumber) 
               {
                   lock(ScreenDonePainting)
                   {
                       ScreenDonePainting[screenNumber] = true;
                       // Signal main thread to wake up 
                       // and process this event.
                       manualEvents[screenNumber].Set(); 
                   }
               }
              
               // This is called by ScreenSaverForm 
               // when it's shutting down.
               public void ShutDown()
               {
                  // Lock needs a reference object. ;-/
                  lock(manualEvents) 
                  {
                    shuttingDown = true;
                    // Last element is for ShutDown 
                    // event so signal the shut down.
                 manualEvents[Screen.AllScreens.Length].Set();
                  }
               }
               
               public void RunMeTillShutdown(int baseVelocity, 
                          int beeCount, int colorCycleSeconds, 
                          bool glitterOn, int swarmsPerScreen)
               {
                  DonePaintingDel = 
                        new DonePaintingDelegate(DonePainting);
                  ShutDownDel = new ShutDownDelegate(ShutDown);
                  // Get count of
                  // screens on system.
                  screenCount = Screen.AllScreens.Length;  
                                                          
                  
                  // One form for each screen found just 
                  // to get the array to create instances. 
                  // We need to create instances to fill out 
                  // the array before we assign the.
                  // ScreenSaverForm instances we will use 
                  // otherwise when I try to assign using
                  // the ScreenSaverForm constructors, I get 
                  // assignment to null object errors.
                  
                  sf = new ScreenSaverForm[screenCount]; 
                  
                  int i = 0; // Loop through the array assigning 
                             // to the array the instances.
                  for (i = 0; i < screenCount;i++)
                  {   
                      // Replace the forms in the array 
                      // with the Real ScreenSaverForms.
                      // Send the screenNo and config values 
                      // to the ScreenSaverForm constructor.
                      sf[i] = new ScreenSaverForm(i,DonePaintingDel, 
                                          ShutDownDel, baseVelocity, 
                                          beeCount, colorCycleSeconds, 
                                          glitterOn, swarmsPerScreen); 
                      sf[i].Show(); // This just shows the form but it 
                                    // doesn't start the drawing.
                      // This evokes the paint event which
                      // starts the form working.
                      sf[i].Refresh();  
                                       
                  } 
                  // When all forms shut down,
                  //we will exit program.
                  while (screenCount > 0)  
                                             
                  { 
                      // Don't waste CPU cycles, looping, 
                      // instead go to sleep
                      // until any event in the array is 
                      // set, then process it.
                      
                      // Thread sleeps while waiting.
                      WaitHandle.WaitAny(manualEvents); 
                      // If a screen is shut down, shut down the rest.
                      if (shuttingDown == true) 
                      {
                          // Note that if we are shutting down, we don't 
                          // need to reset manualEvents.
                          for (i = 0; i < Screen.AllScreens.Length; i++)
                          {
                              // Don't try to shut down a form that 
                              // is already down.
                              if ((sf[i].Visible == true)) 
                              {
                                  // Calls on the form to close itself.
                                  sf[i].CloseMe(); 
                                  // Now this while loop will exit.
                                  screenCount = 0; 
                                  Application.DoEvents();
                              }
                          }
                          // Loop on up and then loop will exit.
                          continue; 
                      } // End of if shutting down.
                      else // Not shutting down so display and 
                           // paint any forms needing painting.
                      {
                      try 
                        { 
                          // For each screen check to see if it is 
                          // done painting and if so, display by
                          // calling Application.DoEvents() then 
                          // invoke another paint event.
                          for (i = 0; i < Screen.AllScreens.Length; i++)
                          {
                             // Does screen need 
                             // showing and a repaint?
                             if (ScreenDonePainting[i]) 
                             {
                                // Set the event as having been 
                                // processed so we will wait again.
                                                         
                                
                                //instead of wasting
                                //CPU cycles looping.
                                manualEvents[i].Reset();
                                Application.DoEvents();
                                sf[i].Refresh(); // Invoke a paint event.
                             }// End if.
                          } // End for.
                        } // End try
                      catch( InvalidOperationException e)
                        {
                           EventLog.WriteEntry("SwarmScreenSaver" + 
                                               e.Source, e.Message, 
                                               EventLogEntryType.Error);
                        } // End catch.
                      } // End of else not shutting down.
                  } // End of While.
              } // End of RunTillShutdown.
          } // End of class ScreenSaver definition.
      } // End of namespace.
  4. 选择的随机颜色通常太暗,无法在黑色背景上看到。

    颜色亮度调整的代码位于 StructsAndFunctions.cs 文件中的 CommonFunctions 类中。它很庞大,这篇文章已经非常庞大和笨拙了,我来回跳转,当我记得或**如果**我记得我忘记了什么重要的事情!

  5. 卡顿和闪烁。我通过双缓冲并将其绘制调用移至 paint 事件来解决此问题。

    请记住,我在 ScreenSaverForm_Paint 事件中进行了绘制例程,但在 ScreenSaver.RunTillShutdown() 方法的 while 循环中从窗体外部调用它,调用 ScreenSaverForm.Refresh() 然后调用 Application.DoEvents() 以在屏幕上显示结果,当 manualEvents 中的元素之一被设置,表明窗体已完成绘制。

    原始代码使用了许多 short 来表示屏幕上的点,这很有意义,所以我想通过使用 short 来节省内存,但是所有可用的随机数生成例程都返回 int,我必须进行的所有显式转换都减慢了程序的速度,所以我删除了 short 并将所有内容都改成了 int

    这是 ScreenSaverForm.cs 中相关的代码片段....

    这部分代码放在类的声明中,构造函数之前……包含鼠标位置。如果鼠标移动,则关闭所有 ScreenSaverForm 实例并结束应用程序。

    private Point MouseXY; 
    private int ScreenNumber; 
    // Will hold the index to the current 
    // screen in AllScreens array.
    
    private int SwarmCount; // Number of swarms 
                            // desired per screen.
    clsInsects[] insects; // One insects instance per 
                          // on-screen swarm on this form.
    // Declare the delegate variables to hold 
    // the methods to call via delegates.
    DonePaintingDelegate DonePaintingDel;
    ShutDownDelegate ShuttingDownDel; 
    // stopNow starts out as false, when it 
    // becomes true due to mouse or key 
    // actions, the form closes. 
    private bool stopNow = false;

    paint 事件调用绘制例程,只要 stopNowfalse,然后调用 Invalidate,这反过来又会再次激发 paint 事件,直到需要退出。不需要计时器。

    此窗体的构造函数,初始化任何组件

    public ScreenSaverForm(int scrn, 
                 int baseVelocity, int beeCount,
                 int colorCycleSeconds, 
                 bool glitterOn, int swarmCount)
    {
        InitializeComponent();
        SwarmCount = swarmCount;
        DonePaintingDel = donePaintingDel;
        ShuttingDownDel = shutDownDel;
        // Size an array of insect 
        // classes to draw a swarm. 
        insects = new clsInsects [SwarmCount]; 
        // Create an insects instance for 
        // each swarm user wants on the screen. 
        // Initialize the array of insects class 
        for (int i = 0; i < SwarmCount; i++) 
            insects[i] = new clsInsects(baseVelocity, beeCount, 
               // Save the Screen Number this
               // form addresses.
               colorCycleSeconds, glitterOn);  
                                               
            ScreenNumber = scrn;

    下面的代码告诉窗体它将在 paint 事件中进行所有绘制,并使用它自己的内置双缓冲例程。UserPaint 表示窗体代码将负责重绘,而不是由操作系统决定何时进行重绘,而 **Opaque** 表示我们将控制何时重绘背景,由于背景始终设置为黑色并进行双缓冲,因此此操作永远不需要执行。这是一段非常有趣的 C# 代码。通过指定包括 DoubleBuffer 在内的所有变量,我们告诉窗体使用其自己的特殊 Win32 底层位图例程,这些例程速度非常快。但这有一个副作用。此窗体使用的特殊缓冲区的所有位都设置为 0,这是一个黑色背景,并且不包含任何先前调用绘制例程的内容。这会导致先前调用绘制例程中的任何绘制都会被清除。因此,您会注意到许多用于擦除先前位置的代码已被注释掉,因为没有什么可擦除的。您所有的绘制例程只需要进行绘制,而无需担心尝试通过绘制一条黑线来擦除线条等内容,所有这些都已为您处理好。缺点是我有一个选项可以使群集对象留下轨迹。这很有趣,但使用窗体内置的卓越双缓冲获得的性能提升太好了,不容错过,尽管有这个副作用。我可能仍会想办法解决这个问题。我以后会研究一下。

    在下面的代码中,所有 enums 都代表 32 位 int 中的 ONE。这些 enums 与其他值进行 OR 运算,最后与值 true 进行 OR 运算,其净效应是将这些值表示的所有位设置为 1。

      SetStyle // Had to remove form's double-buffering. It's too buggy!
      (
       ControlStyles.AllPaintingInWmPaint |   
       ControlStyles.UserPaint |
       ControlStyles.DoubleBuffer |
       ControlStyles.Opaque, true); } } // End of constructor.

    这是 load 事件处理程序,它初始化我的缓冲区和 Insects 爬行类,然后调用 invalidate,这会启动 paint 事件,该事件进行绘制,然后调用自身。它调用自身,我不知道是什么。呵呵,对不起,我喜欢搞笑。

    private void ScreenSaverForm_Load(object sender, System.EventArgs e)
    {
      // Set the form to the size of this screen.
      Bounds = Screen.AllScreens[ScreenNumber].Bounds;
      Cursor.Hide();
      TopMost = true;
      for (int i = 0; i < SwarmCount; i++) // Initialize all desired swarms.
        insects[i].initSwarm( 50, Bounds.Width, Bounds.Height);
    }

    注意我们对 GraphicBitmap 对象的处置如下...

    // We must dispose our bitmaps and graphics objects on exiting.
    protected override void Dispose( bool disposing )
    {
       if( disposing ) // No componants to dispose of.
       {
          if (components != null) 
          {
            components.Dispose();
          }
       }
       // ScreenGraphic.Dispose(); // not needed.
       // ScreenBitmap.Dispose(); // not needed.
       base.Dispose( disposing );
    }

    ScreenSaver.RunMeTillShutdown 方法使用一个 while 循环来调用 Application.DoEvents(),这会在 ScreenSaverForm 完成绘制后显示屏幕,然后它调用 ScreenSaverForm.Refresh() 来激发 paint 事件。只有刷新才能在系统上的所有屏幕上工作。

    当用户移动、单击鼠标或按下按钮时,会调用相应的事件。

    ShuttingDownDel() 是设置 ScreenSaver.ShuttingDown = true 的委托。然后 ScreenSaver.RunMeTillShutDown 调用以下方法来关闭所有窗体。

    这是 ScreenSaverFormCloseMe 方法...

    public void CloseMe()
    {
     stopNow = true;
     for (int i = 0; i < SwarmCount; i++)
         insects[i].stopNow = true; // Tell drawing processes to stop.
     Close();
    }

    有标准的鼠标和键盘例程,如果用户移动或单击鼠标或按下键,它们会调用 CloseMe()

    最后,我们有 paint 事件本身....

    protected override void ScreenSaverForm_OnPaint(PaintEventArgs e)
    {
      // Are we staying open?
      if (stopNow == false) // Don't paint if we need to exit.
      {
        try
        { // Draw each swarm in turn while stopNow == false.
          for (int i = 0;( i < SwarmCount) && (stopNow == false); i++)
          {
            insects[i].DrawWaspThenSwarm(e.Graphics);
            SuspendLayout();
            e.Graphics.Flush(
                System.Drawing.Drawing2D.FlushIntention.Flush);
            ResumeLayout(); 
          } 
        } 
        catch (InvalidOperationException ev) 
        // This wont catch the error caused by 
        // the form's native double-buffering. 
        // I don't know why. 
        // The drawing is done very fast because 
        // it's off screen and doesn't require 
        // rendering every change. This is why 
        // double-buffering is flicker free. 
        { 
          EventLog.WriteEntry("SwarmScreenSaver" + 
                            ev.Source,ev.Message, 
                            EventLogEntryType.Error); 
          // Ignore this error if it occurs but log it. 
        } // End catch. 
      } // End of If stopNow == false. 
      DonePaintingDel(ScreenNumber); 
      // Call delegate to inform. 
      // ScreenSaver that this form is 
      // ready to show and refresh(). 
    } // End of Paint event handler.

好了,差不多了。这里有一些我没有放在代码片段中的 try catch 语句。

Insects 类中有一些 Lock 语句。一个锁定了传递的 e.Graphics 对象以及绘制函数,直到实例完成绘制。

还有另一个模块,即 clsInsects.cs,其中有**更多**代码,但如果我要评论它,这篇文章的篇幅将是现在的一倍!

关注点

别忘了用浏览器查看 HTML。这可以在 CodeCommentReport\Solution_SwarmScreenSaver.HTM 中找到。

历史

发现在主线程上通过 invalidate 运行 ScreenSaverForm 实例,并将绘制代码放在 paint 事件中,然后该事件曾调用 Invalidate() 来绘制下一帧,这比在工作线程或计时器事件上要好。更好的方法是下面的消息传递和 WaitHandle.WaitForAny() 方法。

  • 我发现,如果我不使用抗锯齿,我就不必每次完成绘制后都清除源位图,这在我进行双缓冲**之前**!为了让我的代码擦除一条线,我实际上会在旧线上绘制一条黑线。看起来,GDI+ 的 DrawImage 很聪明,它不会将与背景颜色相同的线条视为要复制的内容,而只会绘制可见的部分并跳过其他所有内容。如果您选择不同的颜色进行擦除,绘制速度就会变慢,这一点就变得很明显了。
  • 我发现窗体的双缓冲功能**非常快**!程序现在的绘制速度比以往任何时候都快!这种自动双缓冲不会将屏幕内容保留到 paint 事件的调用之间,因此您不必在旧内容上绘制以模拟擦除,如果您正在使用这种类型的双缓冲。
  • 我可以将 DLL 的导入封装到包装器中!这确实使事情变得容易,所有特殊代码都放在一个地方。所谓的特殊代码是指您发现自己需要**大量**执行但不是关键任务代码部分的代码。

我将 API 调用移至 CommonFunctions 类,并为这些函数编写了包装器,因为我无法通过类的实例直接调用它们。包装器的名称与 API 调用相同,但末尾附加了 API,所以...

像这样的代码...

if (IsWindowVisible(ParentWindow) == true) becomes ....
if (cf.IsWindowVisibleApi(ParentWindow) == true ...

在检查进程时遇到异常,从 rgesswien 处获得了下面粗体引用的回复。请自行承担使用风险,但是,我将删除检查多个实例的代码,因为据我所知,我根本不需要它。我见过一些带此代码的屏幕保护程序,也见过一些不带此代码的,但在我的测试中,我发现 Windows 自动终止了我的程序,而无需我编写该代码。

**供您参考**:如果您在 IsInstanceAlreadyRunning() 函数的 GetCurrentProcess 上遇到 InvalidOperationException,则修复方法如下。(至少对我来说是这样。)使用 Regedit 打开 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\PerfProc\Performance 将“Disable Performance Counters”的值从 1 更改为 0。(此错误在网络上随处可见,但我只能找到一个新闻组帖子解释如何解决该问题。)

我不再使用 IsInstanceAlreadyRunning(),因为我的操作系统在 CONFIG、迷你预览和全屏预览之间切换时会终止程序。我还收到了一条关于 CONFIG 屏幕无法由输入 /c 的用户使用的通知,而是假定它是由显示属性窗口启动的。我将修复此问题。

**重大变更**!根据 GDI 专家的建议,我已将除预览外的绘制例程移至 ScreensaverFormpaint 事件中。

我了解到在事件外部绘制屏幕是一个非常糟糕的主意,因此不再有 SaverLoop。绘制内容在 load 事件中初始化,但所有绘制都发生在 paint 周期内。

2005 年 6 月 2 日 问题...

我现在已将 CONFIG 信息保存到 XML 文件。

  1. 我终于能够让我的系统的两个显示器在双视图模式下正常工作。我发现我以前遇到过与其他尝试多显示器方法的每个人相同的问题,即一个显示器获得了屏幕保护程序,而第二个显示器(我启动保护程序的显示器)则**没有**为屏幕保护程序进行处理。有关多显示器屏幕保护程序的进一步评论见下文。
  2. 我查看了 CodeProject 上的所有屏幕保护程序,根据回复,它们都存在相同的问题。考虑到克隆和跨屏显示可以工作,但双视图模式存在问题,在这种模式下只有一个显示器可用,或者将应用程序分配到特定的显示器运行。我已经如上所述解决了这个问题。

最新更改(截至 2005 年 6 月 4 日)

配置现在保存为 XML 文件,并且**简单得多**。如果您的 CONFIG 文件不存在,它将被创建,并且名称将是执行文件名加上 .config.xml。例如。 Swarm.scr 创建 Swarm.scr.config.xml。对代码的更改很小,所以我不会更改上面的代码片段,因为概念是相同的,除了小改动和重新制作代码片段是一场噩梦,所以我只是上传了一个新的源文件 zip。如果您愿意,只需查看包含的代码和代码注释网页即可了解更改。注意额外的文件 XmlSwarmScreenSaverConfig.cs

我还添加了一个调用到 entrypoint.cscase 语句中的解析例程,以更清晰地解析命令行,以便您更好地了解发生了什么。

06/07/2005

我只是清理了文章中的一些糟糕的语法和对不再存在的 SaverLoop 的引用。试图消除拼写和语法错误就像试图将一张铺满沙子的地毯上的所有沙子吸走一样,是不是很有趣,而这张地毯已经被冲到了海滩上?

2005 年 6 月 10 日 - 新增内容...

  1. 我向负责绘制的 insects 类添加了一些代码,以便在它正在绘制的窗体正在关闭时立即停止。我希望这能解决我之前遇到的无法捕获的 InvalidOperationException
  2. 增加了处理**多个群集**和同一屏幕上的黄蜂组的能力!拥有几个舞动的群集看起来确实很棒。我发现群集会从大致相同的区域开始,然后发散,一些群集独立运行,然后奇怪地,所有群集都同步移动,就像跳舞一样!这真是太棒了!我通过将单个 insects 类实例更改为数组来实现此目的。我添加了一个配置选项,您可以在 1 到 100 个群集之间进行选择,所以如果您想疯狂一下,尽管去做吧,给计算机一个**真正的**压力测试!
  3. 向 XML CONFIG 保存程序读取方法添加了代码,以便如果它发现某个字段丢失(在本例中是新的 SwarmsPerScreen 字段)或以其他方式损坏,它会通过重新创建具有默认值的配置文​​件来自动恢复,因此无需担心忘记删除旧的 CONFIG 文件。
  4. 我发现 insects 类使用了 CommonFunctions 类的**静态**实例,该类用于派生随机数、颜色调整和其他函数,这可能解释了为什么它在多个显示器上运行失败。稍后,我可能会取消注释多显示器代码并尝试一下,但现在还不行。显然,如果一个对象同时处理一个实例,这可能会有点麻烦。

请享用!我真的很喜欢这些跳舞的群集!

6/11/2005

Hopefully fixed issue with object in use exception. It seems the issue was caused by my process trying to access the Graphics object in the paint event before the screen was done painting. I could have put in some sort of waiting between calls, but I wanted to have the thing run as fast as possible, so I simply put a lock(e.Graphics) statement on the Graphics object passed in the paint event argument e. This seems to have cleared it up, but as with all intermittent bugs it has a life of its own, thus proving the truth of my suspicion that C# is an upgrade of Murphy's C++!

我已成功地让此屏幕保护程序同时在同一系统上的多个显示器上以双显示模式运行!

6/15/2005

为持续解决 InvalidOperationException 事件“对象正在被其他地方使用”问题,我采用了图形路径而不是一次绘制一条线。我构建图形,然后绘制图形路径。我使用 Graphics 对象的 CloseFigure() 方法,该方法启动一个新的子图形,使每个蜜蜂被视为一个单独的子图形,否则蜜蜂之间会有一条线连接。效果就像一张纸的模型。看起来很有趣。

我使用委托来告知 ScreenSaver 类何时某个窗体正在关闭,以及另一个委托来告知何时绘制完成,并设置一个布尔数组,以便它知道哪些窗体已准备好进行绘制。布尔数组不是必需的,因为我发现如果一个窗体需要刷新,到它刷新的时候,另一个窗体也需要。

我觉得很有趣的是 Application.DoEvents() 方法和程序事件是联系在一起的,换句话说,没有办法控制什么被停止。如果我不定期调用 DoEvents,窗体事件就会停止。我不能随意调用 Application.Events(),而没有窗体在准备好之前进行重绘。paint 事件绘制图形,但直到调用 Application.DoEvents() 才会显示在显示器上,这当然会影响所有屏幕!

我已将迷你预览和全屏预览/屏幕保护程序代码分离到 MiniPreview.csScreenSaver.cs 中。

更新:2005 年 6 月 18 日晚

我找到了消除仅仅运行循环等待事件而浪费 CPU 周期的 CPU 周期的方法,而不是优雅地等待事件。我唯一知道的关于 Windows 的是它是消息驱动的,当我开始实现事件驱动的 ScreenSaver 类时,我寻找一种等待事件而不是浪费 CPU 周期的 *** 的方法。

我找到了一种名为 WaitHandle.WaitForAny(Array[]) 的方法。此方法接受一个 ManualResetEventsAutoResetEvents 数组,并可通过 manualEvent.Set(); 停止当前线程等待,直到数组中的任何事件实例被设置。通过使用这个单一语句,然后在调用 ScreenSaverForm.Refresh() 之前使用 manualEvent.Reset(),我能够使线程睡眠并等待事件,而不是像一群饥饿的食人鱼一样吞噬 CPU 周期并使计算机过载。

通过使用上面提到的 WaitForAny 方法,我能够消除一个不必要的系统负担源。我所做的代码更改在上面的代码片段中以粗体显示。

更新:2005 年 6 月 19 日晚上 8 点 PST

由于 InvalidOperationEvent 问题的顽固性,我添加了一个 CONFIG 选项来切换基于样式的双缓冲,以支持我的手动双缓冲代码,该代码从未产生此错误,但速度慢得多。注意:出于某种原因,当我在开发环境中执行程序时,如果找到一个新选项但该选项不在 CONFIG 文件中,IDE 就会挂起。如果我在开发环境外运行应用程序或删除旧的 CONFIG 文件,它就不会挂起。

我已上传新的源代码。我保留了上面的代码片段,因为很少有真正重要的更改。

我想我已经找到了 InvalidOperationException 事件的原因...

手动双缓冲永远不会崩溃。在这种缓冲类型中,Graphics 对象和显示器之间没有双缓冲,因此无需猜测缓冲区何时被写入屏幕,因为我可以通过代码完全控制这一点。

控件内置的双缓冲是自动的,因为控件有一个后备缓冲区,该缓冲区在运行时决定的时间写入,除非您调用前面提到的 flush 方法。在这种情况下,控件必须猜测何时将后备缓冲区复制到显示器,以及何时写入缓冲区,并且它倾向于选择不佳。在通过 setstyle 进行双缓冲时,控件决定在我的程序尝试写入此缓冲区时复制后备缓冲区到屏幕并释放它!

这可以解释延迟时间,例如屏幕上有七个群集,但只有一个群集每 4 秒出现一次,直到它们全部出现在屏幕上。

更新:2005 年 6 月 20 日

看起来,上述理论可能是正确的。我收到了 Bob Powell 的回复,他怀疑自动双缓冲存在某种错误,但尚未证实,除了那个可怕的错误已经解决。它已经运行了大约 15 小时而没有崩溃,因此在我进行了下面的更改后,问题就解决了...

更改...

我删除了很多关于我提到的错误的抱怨和评论。我允许我的判断受到沮丧情绪的影响并对此发泄。虽然我相信这对我来说很有趣,但对您来说可能很无聊。我对此表示歉意。

重要更改:2005 年 6 月 24 日

我找到了 InvalidOperationException 错误的原因。我是在缓冲区**外面**绘制,迫使其释放和调整大小!是的,是我的错!我以为在 paint 事件参数中获得的 Graphics 对象会自动将其剪裁区域设置为窗体的大小。我错了。默认情况下,它似乎是无限的。当使用双缓冲进行绘制时,并且此绘制发生在边界之外,自动双缓冲中的后备缓冲区会自行销毁并调整大小。这导致了异常。我尝试通过窗体的边界来设置它,但在计算机以多屏幕模式运行时,次要显示器的 Paint 事件参数中的边界和剪裁边界会带有 X 和 Y 值的偏移。这是我对此采取的措施...

  • 虽然使用 Graphics.SetClip(e.ClipRectangle) 似乎有效,但我不信任它。相反,我将代码放在原来在 swarm 类中用于跳过绘制任何图形的代码,除非所有图形都完全位于宽度和高度指定的屏幕尺寸内,并且没有图形部分位于负像素位置。我可能需要在此之外进行一些调整,因为存在像素偏移,但至少我走在正确的轨道上。
  • 我删除了 flush 语句,因为我现在不需要它了。
  • **重要** - 我发现当一个窗体被锁定而我尝试在代码中调整它大小时,这会导致无尽的 paint 命令尝试调整窗体大小但失败。我的代码会忽略这些命令,但它们会一次又一次地发送,直到我解锁窗体。
  • 添加了几个测试和一个 paintStatus 变量来跟踪所有 paint 事件完成的状态。我的代码现在驻留在 paint 事件中,而不是 OnPaint 中。
  • 设置 paintEventArgs 的 pev.Graphics.SetClip(Bounds) 阻止了次要显示器的绘制。这在这里有效,但我不信任它... pev.Graphics.SetClip(pev.ClipRectangle);

我以后可能会修改代码片段,但如果您将此用于您自己的保护程序,您会想要重新下载源代码。

好了,就到这里了...

尽情享用!

© . All rights reserved.