用C#创建一个屏幕保护程序
原文地址: http://www.harding.edu/fmccown/screensaver/screensaver.html
简介
一个Windows屏幕保护程序是根据接收到的命令行参数来显示一个配置窗体,或者一个预览窗体的简单的Windows应用程序。Windows屏幕保护程序以.scr为后缀名并且一般存储在Windows\system32目录下,如果你的是64位的版本,则存储在Windows\SysWOW64目录下。
这个教程会像你演示怎样用Visual Studio .NET中的C#语言创建一个屏幕保护程序。我们的屏幕保护程序会将同一行文本随机的在屏幕上的不同位置显示。用户能够更改我们保存在注册表中用来显示文本的内容。这个教程假设你对Visual Studio 和C#有些熟悉。
如果你赶时间并且只想下载已经完成的项目,点击这里,解压这个文件夹,在Visual Studio中载入解决方案(我的项目使用 VS 2010创建的)。
这是我在几年前写的这个修正后的屏幕保护程序教程。非常感谢对修正我的教程很有帮助的Jacob Jordan'sMaking a C# screensaver教程,同时我也在Lucian Wischik's 的文章中发现了一些有用的技巧,How to write a 32bit screen saver.
准备开始
尽管我们可以用.NET编程语言中的任意一种来开发一个屏幕保护程序,但在这我们将使用C#编程语言。
首先启动Visual Studio(我将会用VS 2010,但是Visual C#2010学习版也可以)并且创建一个名字为ScreenSaver的Widnows窗体应用程序。
接下来,双击解决方案资源管理器中的Program.cs 文件。这个文件包含主函数,是.NET执行的入口点:
static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Form1()); }
Application.Run() 调用上面的实例Form1窗体并不断循环执行,处理窗体信息。当窗体被关闭时,方法返回值并且程序终止。
我们很快会对这个循环进行修改,但是,在修改之前,我们首先重命名 Form1 为更有描述性的文字。用鼠标选中文字“Form1”并按F2。输入ScreenSaverForm作为这个窗体的新名称并确定。
命令行参数
注意简介中的信息,一个屏幕保护程序接收命令行参数来选择哪个"模式"应该被执行。这里有三个可以使大写或小写的命令行参数:
1. /p - 显示屏幕保护程序在屏幕保护程序的选择对话框中
2. /c - 显示屏幕保护程序的配置对话框
3. /s - 全屏显示屏幕保护程序
如果没有传递任何参数,屏幕保护程序默认提供/C
如果传递的是参数/p,一个十六进制的数作为父窗体的句柄也被传递。例如,我们的屏幕保护程序可能传递/p 1234567。一个窗体句柄也可能通过/c传递。或者,这个窗体的句柄也可能在第一个参数和句柄中使用逗号来传递。一些例子:/p:1234567 和 /c:7654321。因此我们需要处理这些情况。
让我们来重写Main方法以便于它接收命令行参数并分别处理这三个参数。用下面的代码覆盖Main方法的全部内容。
static void Main(string[] args) { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); if (args.Length > 0) { string firstArgument = args[0].ToLower().Trim(); string secondArgument = null; // Handle cases where arguments are separated by colon. // Examples: /c:1234567 or /P:1234567 if (firstArgument.Length > 2) { secondArgument = firstArgument.Substring(3).Trim(); firstArgument = firstArgument.Substring(0, 2); } else if (args.Length > 1) secondArgument = args[1]; if (firstArgument == "/c") // Configuration mode { // TODO } else if (firstArgument == "/p") // Preview mode { // TODO } else if (firstArgument == "/s") // Full-screen mode { ShowScreenSaver(); Application.Run(); } else // Undefined argument { MessageBox.Show("Sorry, but the command line argument \"" + firstArgument + "\" is not valid.", "ScreenSaver", MessageBoxButtons.OK, MessageBoxIcon.Exclamation); } } else // No arguments - treat like /c { // TODO } }
上面的代码需要三个参数,但只实现了/s;在后面我们会接着完成/c和/p。如果提供了一个无效的参数,我们将弹出一个模式对话框来提示用于错误的信息。
为了让上面的代码能够运行,我们需要写一个ShowScreenSaver()方法。把这个方法放在Main()之后。
static void ShowScreenSaver() { foreach (Screen screen in Screen.AllScreens) { ScreenSaverForm screensaver = new ScreenSaverForm(screen.Bounds); screensaver.Show(); } }
这个方法将在每个连接到我们电脑的显示器上启动一个ScreenSaverForm的实例。它也需要我们为ScreenSaverForm实现一个能够接受矩形参数的构造函数来显示窗体的边界。我们将在下一章节添加这个构造函数。
屏幕保护程序窗体
现在让我们来修改主屏幕保护窗体,以便于随机在屏幕的任意位置上显示一行文本。
由于我们在屏幕保护程序的代码中更改过Form1的名字,所以我们也应该更改Form1.cs的名字为ScreenSaverForm.cs。用鼠标左键点击解决方案资源管理器中的Form1.cs,右击选择重命名并输入ScreenSaverForm.cs。
现在双击打开ScreenSaverForm.cs,并且更改窗体的backColor属性为Black。更改FormBorderStyle属性为None,使得标题栏、按钮和窗体框架消失;这会使我们用黑色的背景填充整个显示器。最后,更改StartPosition为Manual,否则屏保不会在双显示器上正确地显示。
现在从工具栏拖一个Label到窗体的任意位置。更改label的名字为textLabel,Text属性为“Demo”或者是你喜欢的任何文字,修改label的Font属性以便显示稍大一点的字体,修改ForeColor为别的颜色以便于能够在黑色的背景上可见。
下面的例子是我的窗体。窗体的大小并不重要因为我们稍后会以编程的方式重新设置它的大小。
现在按F7显示窗体后台的代码。添加下面设置窗体边界的构造函数:
public ScreenSaverForm(Rectangle Bounds) { InitializeComponent(); this.Bounds = Bounds; }
当我们加载窗体时,我们想要隐藏光标并且将窗体放置到其它窗体的最前端。为了实现这些,我们首先添加一个载入窗体的事件处理器。事件处理器是响应并执行特殊的事件的方法。
为了添加事件处理器,首先在设计模式中查看窗体(点击标签标示ScreenSaverForm.cs【设计】),找到Visual Studio中窗体的右下角的窗体的属性。从窗体属性的下拉菜单中选中ScreenSaverForm,并且点击列出所有事件的闪电装图标。下面这张图是我的窗体属性。
向下滑动到Load event并且双击文字"Load"。这样就创建了一个载入事件处理器,返回查看窗体的代码。在这个方法中输入下面两行代码:
private void ScreenSaverForm_Load(object sender, EventArgs e) { Cursor.Hide(); TopMost = true; }
现在不要运行你的程序!如果你运行了,你只能用任务管理器来关闭屏幕保护程序。因为我们只是让窗体显示在Program.cs,但是关闭窗体并不能关闭应用程序。我们需要先添加一些逻辑,当一个按键或鼠标移动、点击的时候终止程序。
添加MouseMover、MouseClick和KeyPress的事件处理器。像你添加载入窗体事件处理器一样通过鼠标下滑窗体属性的屏幕保护程序的事件列表并在每个事件上双击。在事件处理器创建好之后,键入下面的代码。
如果你只是复制和粘贴下面所有的代码到你的程序,而不是正确的创建事件处理器,则这个事件处理器不会被调用。Visual Studio需要写特殊的代码来关联这些窗体上的事件处理器。如果你很好奇这些代码是什么样子,看一下ScreenSaverForm.Designer.cs文件,找到用 += operator 的代码的那一行。不要更改这个文件的文本内容,除非你知道它是如果运作的,因为这些代码是Visual Studio自动生成的。
private Point mouseLocation; private void ScreenSaverForm_MouseMove(object sender, MouseEventArgs e) { if (!mouseLocation.IsEmpty) { // Terminate if mouse is moved a significant distance if (Math.Abs(mouseLocation.X - e.X) > 5 || Math.Abs(mouseLocation.Y - e.Y) > 5) Application.Exit(); } // Update current mouse location mouseLocation = e.Location; } private void ScreenSaverForm_MouseClick(object sender, MouseEventArgs e) { Application.Exit(); } private void ScreenSaverForm_KeyPress(object sender, KeyPressEventArgs e) { Application.Exit(); }
注意,事件处理器需要额外的逻辑,因为当窗体第一次显示的时候会接受到 MouseMove 事件,因此我们需要做一点额外的工作:初始化鼠标指针的看它从上次接收事件后是否移动了一段明显的距离(如果你喜欢,你可以设置比5大或者小的值)。
如果你现在运行你的程序(Ctrl+F5),你会看到它没任何反应...它立即停止了。因为我们没有传送它任何命令行参数,而且我们还没有完成屏幕保护程序的部分配置。
为了能够在我们运行程序时看到屏保是怎么运行于全屏模式的,我们给它传递一个参数/s。从主菜单中选择 项目->属性...。选择调试并在命令行参数下输入/s。保存项目并按Ctrl+F5。这个屏幕会编程黑色,并且文字会显示在屏幕上的固定位置。点击或移动鼠标或按下键盘上的任意键会结束程序。
现在我们来添加一些逻辑让文字在屏幕上移动。
首先添加一个 Timer 控件到窗体,更改它的名字为moveTimer。如下所示,添加一个随机数产生器作为一个类级别的变量,并添加一些会每三秒唤醒一次moverTimer_Tick()的代码到ScreenSaverForm_Load()的结尾处。moverTimer_Tick()会将文本显示在屏幕中的随机的位置,确保能够被看到。你可以改变3000的大小来改变文本改变显示位置的速度。
private Random rand = new Random(); private void ScreenSaverForm_Load(object sender, EventArgs e) { Cursor.Hide(); TopMost = true; moveTimer.Interval = 3000; moveTimer.Tick += new EventHandler(moveTimer_Tick); moveTimer.Start(); } private void moveTimer_Tick(object sender, System.EventArgs e) { // Move text to new location textLabel.Left = rand.Next(Math.Max(1, Bounds.Width - textLabel.Width)); textLabel.Top = rand.Next(Math.Max(1, Bounds.Height - textLabel.Height)); }
重新运行程序,这次文本将会每隔3秒会更换一次位置。按下或移动鼠标结束程序。
预览模式
当传递参数/p时,我们的屏幕保护程序需要运行在预览模式下,或者是一个小的长方形在屏保配置窗体对话框中。下面的屏幕截图是运行在我的windows7中的预览模式中的屏保。
如果你不想实现一个预览模式,可以不必实现。当接收/p时仅仅是让程序停止就行了。我们将为我们的程序实现一个预览模式,因为它所需要的是一些Windows API 函数和改变label中文本字体的大小。
在Program.cs中修改我们用来接受/p的if语句:
else if (firstArgument == "/p") // Preview mode { if (secondArgument == null) { MessageBox.Show("Sorry, but the expected window handle was not provided.", "ScreenSaver", MessageBoxButtons.OK, MessageBoxIcon.Exclamation); return; } IntPtr previewWndHandle = new IntPtr(long.Parse(secondArgument)); Application.Run(new ScreenSaverForm(previewWndHandle)); }
如果配置对话框处理为提供,上面的代码会显示一个错误的对话框并且终止。除非应用程序为屏保使用一个特殊的构造函数,接下来我们会写。
返回ScreenSaverForm.cs,下面的构造函数会将窗体的句柄当作一个参数:
public ScreenSaverForm(IntPtr PreviewWndHandle) { InitializeComponent(); // Set the preview window as the parent of this window SetParent(this.Handle, PreviewWndHandle); // Make this a child window so it will close when the parent dialog closes // GWL_STYLE = -16, WS_CHILD = 0x40000000 SetWindowLong(this.Handle, -16, new IntPtr(GetWindowLong(this.Handle, -16) | 0x40000000)); // Place our window inside the parent Rectangle ParentRect; GetClientRect(PreviewWndHandle, out ParentRect); Size = ParentRect.Size; Location = new Point(0, 0); // Make text smaller textLabel.Font = new System.Drawing.Font("Arial", 6); previewMode = true; }
上面的代码使用了几个 Windows API 函数:SetParent(), SetWindowLong(), GetWindowLong(), 和GetClientRect()。这些函数现在还不能访问你的程序,所以编译器会标记每个函数都不存在。为了解决这个问题,把下面的代码添加到ScreenSaverForm类中:
[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);
上面的代码将告诉编译器去寻找在user32.dll中被签名的API函数。函数 DllImport()是在System.Runtime.InteropServices中定义的。
返回到ScreenSaverForm的构造函数的代码中,由于我们还没有定义变量previewMode,因此它也被编译器标记。再别的变量旁声明此类级变量,并初始化为false:
private bool previewMode = false;
我们将会在方法ScreenSaverForm_MouseMove(), ScreenSaverForm_KeyPress(), and ScreenSaverForm_MouseClick()中使用变量previewMode,用来终止移动或点击鼠标或按下键盘上的按键所对程序产生的影响。
如下所示,通过将if语句中检查变量previewMode的方法封装起来的方式来修改三个方法中的每一个方法。
private void ScreenSaverForm_KeyPress(object sender, KeyPressEventArgs e) { if (!previewMode) Application.Exit(); }
安装你的屏幕保护程序
为了测试你键入的预览代码,你的屏幕保护程序需要正确的安装,以便屏幕保护程序的配置对话框能够启动它。遵循这些步骤:
1、将你的可执行文件的扩展名从.exe更改为.scr。现在你的屏幕保护程序被命名为ScreenSaver.scr。
2、右击ScreenSaver.scr并从弹出的菜单中选择安装。这将启动屏幕保护程序配置对话框中选定的屏幕保护程序。
你现在能在小型的配置对话框中看到你正在运行的屏幕保护程序。如果你想让它称为你的新的屏幕保护程序,点击确认,否则点击取消。
如果你拥有管理员权限,另一种安装屏保的方式是将它放在 C:\windows\system32下,如果你的是64位的系统,则放在C:\windows\SysWOW64下。当屏保程序第一次启动时,它会搜索后缀为.scr的文件并且将它们列在对话框中的下拉菜单里面。
配置模式
你不需要为屏幕保护程序提供一个配置;当接收到/c或没有任何参数的时候,仅仅显示一个对话框(用MessageBox.Show()也可以实现相同的效果),这会用一个适当的信息来提示用户无法配置屏保。
让我们来提供当修改在屏保上显示的文本内容的能力。我们将创建一个新的窗体,它需要一个textbox允许用户设置label中显示的内容。除此之外,如果用户能够改变字体的属性和颜色将会更好,但是这里我把它留给你们来完成。
首先像你的项目中新添加一个名为SettingForm的窗体,然后添加一个TextBox控件(命名为textbox),确认(okButton)和取消(cancelButton)按钮。你可能想要添加一些Labels用来表明是谁开发了这个屏幕保护程序和textbox的作用。下面是我的配置窗体的截图。
将屏保的配置信息存储在注册表中是一个很不错的地方。让我们来写一个方法用来将键入在textbox中的值写入注册表中,和另一个从注册表中读取值的方法。打开SettingsForm.cs像SettingsForm类中添加下面的方法:
private void SaveSettings() { // Create or get existing Registry subkey RegistryKey key = Registry.CurrentUser.CreateSubKey("SOFTWARE\\Demo_ScreenSaver"); key.SetValue("text", textBox.Text); } private void LoadSettings() { // Get the value stored in the Registry RegistryKey key = Registry.CurrentUser.OpenSubKey("SOFTWARE\\Demo_ScreenSaver"); if (key == null) textBox.Text = "C# Screen Saver"; else textBox.Text = (string)key.GetValue("text"); }
SaveSettings()方法在注册表中HKEY_CURRENT_USER下的Software/Demo_ScreenSaver中创建了一个叫做text的键,由于我们在HKEY_CURRENT_USER中写入的键会因为用户的不同而不同,所以每个用户可以自定义屏保的文本。LoadSettings()方法读取键的值,但是如果键不存在(只有我们第一配置之后才会存在),它将使用默认的文本。RegistryKey定义在Microsoft.Win32命名空间下。
在运行中输入regedit就可以查看注册表。当用这个程序的时候应格外小心...改变注册表将使依赖它的应用程序出现无法预测的情况。
不管注册表中的文本是什么,我们将SettingForm显示的文本初始化,让我们在form的构造函数中调用LoadSetting():
public SettingsForm() { InitializeComponent(); LoadSettings(); }
现在添加确认和取消按钮的Click事件处理句柄。当确认按钮被点击的时候调用SaverSetting(),点击取消按钮的时候关闭窗体。
private void okButton_Click(object sender, EventArgs e) { SaveSettings(); Close(); } private void cancelButton_Click(object sender, EventArgs e) { Close(); }
现在,当接收到命令行参数/c或者没有参数的时候,我们需要启动配置窗体(SettingForm)。返回Program.cs修改处理/c和没有参数的情况的逻辑:
if (firstArgument == "/c") // Configuration mode { Application.Run(new SettingsForm()); } ... skip ... else // No arguments - treat like /c { Application.Run(new SettingsForm()); }
最后,当屏幕保护窗体载入的时候我们需要载入注册表的值,所以我们添加下面的代码到ScreenSaverForm_Load()中:
private void ScreenSaverForm_Load(object sender, EventArgs e) { // Use the string from the Registry if it exists RegistryKey key = Registry.CurrentUser.OpenSubKey("SOFTWARE\\Demo_ScreenSaver"); if (key == null) textLabel.Text = "C# Screen Saver"; else textLabel.Text = (string)key.GetValue("text"); ...skip... }
你现在可以通过将项目配置中的命令行参数移除并按Ctrl+F5来测试配置对话框。或者你可以更改可执行程序的扩展名为.scr,右击文件在弹出菜单选项中选择配置。你也可以安装屏幕保护程序(右击扩展名为.scr的文件选择安装)点击配置对话框中的配置按钮。
结论
此处省略若干个单词......
附加:
//写入颜色值到注册表 key.SetValue("color", colorDialog1.Color.ToArgb()); //从注册表中读取颜色值 textLabel.ForeColor = Color.FromArgb((int)key.GetValue("color"));