制作 C# 屏幕保护程序






4.84/5 (36投票s)
支持多显示器、预览等等。
引言
首先,让我澄清一件事。屏保程序不过是一个标准 Windows 可执行文件,只是其扩展名是 .scr 而不是标准的 .exe。它与 EXE 具有相同的文件格式,当然,您可以制作 .NET 屏保程序。您可以将计算机上的任何旧应用程序,将其扩展名更改为 .scr,您就会得到一个完全有效的 Windows 屏保程序。然而,您可能不想这样做。那是因为屏保程序确实有一些特定的职责
当 Windows 运行屏保程序时,它会向其传递某些参数。这些参数是 "/s"(显示屏保程序)、"/p"(在屏保程序选择对话框中的小显示器上预览屏保程序)和 "/c"(显示屏保程序的选项,或配置它)。Windows 期望您的应用程序在屏保程序的 Main
方法中读取这些参数,并相应地执行操作。
屏保程序通常会占据整个屏幕。因此,您必须在屏保程序中内置某种退出信号。传统上,屏保程序会在按下键盘键或移动鼠标时退出。
屏保程序还会保持在所有其他窗口之上,并且会隐藏光标。
读取参数
在 C# 应用程序中,唯一可以读取参数的地方是自动生成的 Main()
方法。转到解决方案资源管理器。应该有一个名为 "Program.cs" 的代码文件。在代码编辑器中打开它。它可能看起来像这样
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
namespace YourApplicationNamespace
{
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new YourApplicationMainForm());
}
}
}
我们只关心其中的 Main
方法
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new YourApplicationMainForm());
}
默认情况下,Main()
方法不带任何参数。您必须对其进行编辑,使其接受一个参数 "string[] args
";这样,您就可以读取传递给应用程序的参数
static void Main(string[] args)
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new YourApplicationMainForm());
}
args
参数现在将包含传递给您的应用程序的所有参数。现在,您必须解释这些参数。只应向屏保程序传递一个参数,该参数告诉屏保程序 Windows 希望它做什么。我们可以使用简单的 if
语句轻松读取参数,如下所示
if (args[0].ToLower().Trim().Substring(0,2) == "/s") //show
{
//show the screen saver
}
else if (args[0].ToLower().Trim().Substring(0,2) == "/p") //preview
{
//preview the screen saver
}
else if (args[0].ToLower().Trim().Substring(0,2) == "/c") //configure
{
//configure the screen saver
}
然而,屏保程序有可能没有传入任何参数。因此,我们可能应该添加另一个 if
语句
if (args.Length > 0)
{
if (args[0].ToLower().Trim().Substring(0,2) == "/s") //show
{
//show the screen saver
}
else if (args[0].ToLower().Trim().Substring(0,2) == "/p") //preview
{
//preview the screen saver
}
else if (args[0].ToLower().Trim().Substring(0,2) == "/c") //configure
{
//configure the screen saver
}
}
else
{
//no arguments were passed (we should probably show the screen saver)
}
这样,如果您尝试引用传入的第一个参数而那里什么都没有,您就不会得到索引超出范围的异常。
在此阶段,我们必须修改 Main
方法,使其看起来像这样
static void Main(string[] args)
{
if (args.Length > 0)
{
if (args[0].ToLower().Trim().Substring(0,2) == "/s") //show
{
//show the screen saver
}
else if (args[0].ToLower().Trim().Substring(0,2) == "/p") //preview
{
//preview the screen saver
}
else if (args[0].ToLower().Trim().Substring(0,2) == "/c") //configure
{
//configure the screen saver
}
}
else
{
//no arguments were passed (we should probably show the screen saver)
}
}
设置屏保程序
此时,如果我们尝试运行我们的应用程序,它将像没有代码的控制台应用程序一样运行,因为我们从未在 Main
中的任何地方调用 Application.Run()
。在我们做了一些事情之后,这将得到解决。现在,我已经用 C# 制作了几个支持多显示器的屏保程序,我将向您解释我用于制作它们的结构。我并不是说我的方法是最好的,但它有效,而且很有条理。为了支持多显示器,我没有简单地制作一个主窗体并将其拉伸以填充所有屏幕,而是制作屏保程序,使其在每个屏幕上显示不同的主窗体(顺便说一句,每当我在代码中提到主窗体时,我都会称之为 "MainForm
")。为此,System.Windows.Forms.Screen
类非常有用。看看这段代码
foreach (Screen screen in Screen.AllScreens)
{
MainForm screensaver = new MainForm(screen.Bounds);
screensaver.Show();
}
您看到的 foreach
循环正在遍历当前计算机上的所有屏幕(显示器)。然后,我为该显示器创建一个新的主窗体,将该屏幕的边界传递给它,并显示它。但是,等等,这不起作用,因为我们的主窗体中没有接受参数的构造函数方法。所以,让我们创建一个。我将这个精确的构造函数添加到我的主窗体中
public MainForm(Rectangle Bounds)
{
InitializeComponent();
this.Bounds = Bounds;
Cursor.Hide();
}
如您所见,它接受一个 System.Drawing.Rectangle
参数(即边界),在构造函数中,它会将窗体的边界设置为构造函数中传入的边界。它还会隐藏光标,因为就像我说过的,屏保程序会隐藏光标。
因此,要运行屏保程序,我们可以创建一个如下所示的方法
void ShowScreenSaver()
{
foreach (Screen screen in Screen.AllScreens)
{
MainForm screensaver = new MainForm(screen.Bounds);
screensaver.Show();
}
}
通常,我会创建一个这样的方法来显示屏保程序,并将其放在我的主方法所在的类(Program
)中。所以,您的 Program
类现在看起来像
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
namespace YourApplicationNamespace
{
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main(string[] args)
{
if (args.Length > 0)
{
if (args[0].ToLower().Trim().Substring(0,2) == "/s") //show
{
//show the screen saver
}
else if (args[0].ToLower().Trim().Substring(0,2) == "/p") //preview
{
//preview the screen saver
}
else if (args[0].ToLower().Trim().Substring(0,2) == "/c") //configure
{
//configure the screen saver
}
}
else
{
//no arguments were passed (we should probably show the screen saver)
}
}
static void ShowScreenSaver()
{
foreach (Screen screen in Screen.AllScreens)
{
MainForm screensaver = new MainForm(screen.Bounds);
screensaver.Show();
}
}
}
}
(注意:当我们将 ShowScreenSaver()
方法放在 Program
类中时,它必须是 static
的,因为它是一个 static
类。)
现在,让我们修改 Main()
方法,以便当我们的应用程序被传递 /s 或未传递任何内容时,它将实际使用该方法。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
namespace YourApplicationNamespace
{
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main(string[] args)
{
if (args.Length > 0)
{
if (args[0].ToLower().Trim().Substring(0,2) == "/s") //show
{
//show the screen saver
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
ShowScreenSaver(); //this is the good stuff
Application.Run();
}
else if (args[0].ToLower().Trim().Substring(0,2) == "/p") //preview
{
//preview the screen saver
}
else if (args[0].ToLower().Trim().Substring(0,2) == "/c") //configure
{
//configure the screen saver
}
}
else
{
//no arguments were passed (we should probably show the screen saver)
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
ShowScreenSaver(); //this is the good stuff
Application.Run();
}
}
static void ShowScreenSaver()
{
foreach (Screen screen in Screen.AllScreens)
{
MainForm screensaver = new MainForm(screen.Bounds);
screensaver.Show();
}
}
}
}
你猜怎么着,我们现在有一个工作的屏保程序了。正在工作,但尚未完成。我们仍然需要让 main()
方法支持 /p 和 /c。我们现在将它设置为 /c(配置)参数,因为这非常简单。您是否希望用户能够选择屏保程序中的某些选项?如果是,只需创建一个用户可以配置选项的窗体,并让您的应用程序将这些选项存储在应用程序的设置、ini 文件、注册表或任何地方。现在,我们只需要修改主方法中处理 /c 的部分
else if (args[0].ToLower().Trim().Substring(0,2) == "/c") //configure
{
//configure the screen saver
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new ConfigureForm()); //this is the good stuff
}
(在此代码中,我将配置窗体称为 "ConfigureForm
",但这可以更改。)请注意,我正在使用 Application.Run()
调用来启动应用程序,我所做的只是将配置窗体作为应用程序的主窗体传递给它。
另一方面,如果您没有任何用户可以设置的选项,我们可以做一些非常简单的事情,如下所示
else if (args[0].ToLower().Trim().Substring(0,2) == "/c") //configure
{
//nothing to configure
MessageBox.Show("This screensaver has no options that you can set",
"My Cool Screen Saver",
MessageBoxButtons.OK,
MessageBoxIcon.Information);
}
这将显示一条消息,告诉用户该屏保程序没有可配置选项。然后,由于我们从未调用 Application.Run()
,因此一旦主方法运行完毕,应用程序将像控制台应用程序一样退出。
现在,我们可以处理 /p(预览)参数。预览是 Windows 希望您的屏保程序在屏保对话框中的小电脑显示器中预览自身时。我们有两种方法可以做到这一点。通常,我选择完全放弃预览。顺便说一句,如果您不想要预览,您就完成了。您不需要在处理预览的部分中编写任何代码,因此当 Windows 调用您的应用程序进行预览时,它将启动、运行,并且由于从未调用 Application.Run()
,它将在难以置信的短时间内关闭,就像它从未运行过一样。这很容易。然而,如果您希望您的屏保程序具有预览功能,我将教您如何实现。
设置预览
这有点棘手。它需要一些 API 来完成。Windows 不会自动执行一些神秘魔法,使您的应用程序在预览窗口中完美显示,您必须自己完成。但是,它确实为您提供了一些帮助。当 Windows 向您的应用程序传递 /p 命令时,它还会传递另一个参数:选择屏保对话框上预览窗口的句柄。有了它,我们可以使用一些 API 使其成为屏保主窗体的父级。它将这样工作
- 主函数将看到第一个参数是 /p。然后它知道第二个参数是预览窗口的句柄。
- 主函数将使用
Application.Run()
运行应用程序,将MainForm
作为主窗体。它还将预览窗口的句柄传递给主窗体。 - 主窗体将自行配置,以便它将在预览窗口内显示。
- 他们都过着幸福的生活。
让我们开始实现它。首先,让我刷新您(和我)对主例程中处理 /p 的部分是什么样子的记忆
else if (args[0].ToLower().Trim().Substring(0,2) == "/p") //preview
{
//preview the screen saver
}
目前,它的设置方式是没有预览的,因为除了那个孤独的小注释之外没有代码。我们必须将其修改成这样
else if (args[0].ToLower().Trim().Substring(0,2) == "/p") //preview
{
//show the screen saver preview
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
//args[1] is the handle to the preview window
Application.Run(new MainForm(new IntPtr(long.Parse(args[1]))));
}
看来那个评论不再那么孤独了。无论如何,这应该是您需要对 Main
函数进行的所有操作。但是,请看这一行
Application.Run(new MainForm(new IntPtr(long.Parse(args[1]))));
这将以 IntPtr
的形式将预览窗口的句柄传递给 MainForm
。但是,我们没有为该窗体创建接受 IntPtr
的构造函数。好吧,是时候创建一个了。你猜怎么着,你不必再做任何工作,因为你所要做的就是使用我写好的这段代码
//This constructor is the handle to the select screen saver dialog preview window
//It is used when in preview mode (/p)
public MainForm(IntPtr PreviewHandle)
{
InitializeComponent();
//set the preview window as the parent of this window
SetParent(this.Handle, PreviewHandle);
//make this a child window, so when the select screensaver
//dialog closes, this will also close
SetWindowLong(this.Handle, -16,
new IntPtr(GetWindowLong(this.Handle, -16) | 0x40000000));
//set our window's size to the size of our window's new parent
Rectangle ParentRect;
GetClientRect(PreviewHandle, out ParentRect);
this.Size = ParentRect.Size;
//set our location at (0, 0)
this.Location = new Point(0, 0);
IsPreviewMode = true;
}
如果您还没有弄清楚,我正在编写本教程时制作一个真正的屏保程序。无论如何,这正是您应该使用的。但是等等,如果您现在尝试使用它,它将抛出大量错误。那是因为我使用了四个 API,它们还没有在您的代码中。它们是
#region Preview API's
[DllImport("user32.dll")]
static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent);
[DllImport("user32.dll")]
static extern int SetWindowLong(IntPtr hWnd, int nIndex, IntPtr dwNewLong);
[DllImport("user32.dll", SetLastError = true)]
static extern int GetWindowLong(IntPtr hWnd, int nIndex);
[DllImport("user32.dll")]
static extern bool GetClientRect(IntPtr hWnd, out Rectangle lpRect);
#endregion
我相信我已经很好地解释了到底发生了什么,所以我就继续了。哦,关于我给你的构造函数中的那个变量,叫做 IsPreviewMode
。这是为了让应用程序知道它在运行时是否处于预览模式。如果您正在阅读本文时制作一个屏保程序,请创建一个名为 IsPreviewMode
的全局布尔值。您将需要这样的变量,因为在预览模式下,您希望禁用任何会使屏保程序退出的鼠标或键盘事件。例如,如果您有一个按下键的事件,其中包含退出应用程序的代码,您将需要在它周围放置一个 if
语句,以确保您的应用程序不在预览模式下。为什么?当您的屏保程序在“选择屏保程序”对话框中的预览窗口中预览时,任何时候您将鼠标移动到预览窗口上、按下键盘键或单击预览窗口,您肯定不希望预览消失,如果我们不采取这些预防措施,就会发生这种情况。您还可以使用 IsPreviewMode
变量,以便在预览模式下某些代码不会运行,而某些代码会在预览模式下运行,而在标准模式下不会运行。这可能非常有用。
整合所有内容
好的,此时您的 Program.cs 文件应该看起来像这样
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
namespace YourApplicationNamespace
{
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main(string[] args)
{
if (args.Length > 0)
{
if (args[0].ToLower().Trim().Substring(0, 2) == "/s") //show
{
//run the screen saver
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
ShowScreensaver();
Application.Run();
}
else if (args[0].ToLower().Trim().Substring(0, 2) == "/p") //preview
{
//show the screen saver preview
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
//args[1] is the handle to the preview window
Application.Run(new MainForm(new IntPtr(long.Parse(args[1]))));
}
else if (args[0].ToLower().Trim().Substring(0, 2) == "/c") //configure
{
//Show the configuration dialog, or inform the user
//there are no options with a message box here
}
else
// an argument was passed, but it wasn't /s, /p,
// or /c, so we don't care wtf it was
{
//show the screen saver anyway
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
ShowScreensaver();
Application.Run();
}
}
else //no arguments were passed
{
//run the screen saver
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
ShowScreensaver();
Application.Run();
}
}
//will show the screen saver
static void ShowScreensaver()
{
//loops through all the computer's screens (monitors)
foreach (Screen screen in Screen.AllScreens)
{
//creates a form just for that screen
//and passes it the bounds of that screen
MainForm screensaver = new MainForm(screen.Bounds);
screensaver.Show();
}
}
}
}
为了清楚起见,这些是您的 MainForm
中应该有的两个新构造函数(如果您选择不进行预览,则只会有上面那个)
//This constructor is passed the bounds this form is to show in
//It is used when in normal mode
public Form1(Rectangle Bounds)
{
InitializeComponent();
this.Bounds = Bounds;
//hide the cursor
Cursor.Hide();
}
//This constructor is the handle to the select
//screensaver dialog preview window
//It is used when in preview mode (/p)
public Form1(IntPtr PreviewHandle)
{
InitializeComponent();
//set the preview window as the parent of this window
SetParent(this.Handle, PreviewHandle);
//make this a child window, so when the select
//screensaver dialog closes, this will also close
SetWindowLong(this.Handle, -16,
new IntPtr(GetWindowLong(this.Handle, -16) | 0x40000000));
//set our window's size to the size of our window's new parent
Rectangle ParentRect;
GetClientRect(PreviewHandle, out ParentRect);
this.Size = ParentRect.Size;
//set our location at (0, 0)
this.Location = new Point(0, 0);
IsPreviewMode = true;
}
现在,凭借我们目前所拥有的,我们已经差不多可以开始了,但还有一件事我们需要做。我们绝对必须添加功能,以便在按下键盘键、单击屏保程序或鼠标在屏保程序上大幅移动时,屏保程序会退出。您真正需要做的只是类似这样的事情
#region User Input
private void Form1_KeyDown(object sender, KeyEventArgs e)
{
//** take this if statement out if your not doing a preview
if (!IsPreviewMode) //disable exit functions for preview
{
Application.Exit();
}
}
private void Form1_Click(object sender, EventArgs e)
{
//** take this if statement out if your not doing a preview
if (!IsPreviewMode) //disable exit functions for preview
{
Application.Exit();
}
}
//start off OriginalLoction with an X and Y of int.MaxValue, because
//it is impossible for the cursor to be at that position. That way, we
//know if this variable has been set yet.
Point OriginalLocation = new Point(int.MaxValue, int.MaxValue);
private void Form1_MouseMove(object sender, MouseEventArgs e)
{
//** take this if statement out if your not doing a preview
if (!IsPreviewMode) //disable exit functions for preview
{
//see if originallocation has been set
if (OriginalLocation.X == int.MaxValue &
OriginalLocation.Y == int.MaxValue)
{
OriginalLocation = e.Location;
}
//see if the mouse has moved more than 20 pixels
//in any direction. If it has, close the application.
if (Math.Abs(e.X - OriginalLocation.X) > 20 |
Math.Abs(e.Y - OriginalLocation.Y) > 20)
{
Application.Exit();
}
}
}
#endregion
现在,您显然不能只是复制/粘贴,因为您必须将这些函数设为事件处理程序。无论如何,这根本不会很难。请注意,我使用了 Application.Exit()
调用而不是 this.close()
,因为尽管我将其称为 MainForm
,但它实际上并不是一个“主窗体”,因为如果它被关闭,应用程序仍将继续运行。为了解决这个问题,我们只需使用 Application.Exit()
调用。现在,您只剩下最后一件事要做:将您的构建应用程序的扩展名从 .exe 更改为 .scr,如果您想将其用作计算机上的屏保程序,请将其复制到 system32 目录并将其设置为您的屏保程序。
我说过我正在编写本教程时制作一个真正的屏保程序,作为示例将其提供给您是公平的。从顶部的链接下载它。我制作的屏保程序显示了一个假的但高度真实的死亡蓝屏,就像 SysInternals 上的 BSOD 屏保程序一样。无论如何,这不是重点。重点是它展示了我所说的一切。它还有一个预览。