Christian和James的Code Project屏幕保护程序






4.41/5 (14投票s)
2002年4月18日
9分钟阅读

423993

946
我们尝试用 C# 编写一个主题为 Code Project 的屏幕保护程序。
摘要
本文将记录我们 Code Project 屏幕保护程序开发的各个方面。我开始开发是在比赛宣布的时候,这与我寻找一个能帮助我更多地学习 C# 的项目计划非常吻合。不过,途中我遇到了 James,他参与了进来。本文将涵盖我编写的部分,当 James 编写文章介绍他的贡献时,我也会从这里链接到他的文章。事情并不是那么简单,James 在很多方面都给了我很大的帮助。
用 C# 编写屏幕保护程序
我本来以为主要的挑战就是创建一个屏幕保护程序。我很快在网上找到一个(无法编译的)示例,它只是在 C# 中创建一个置顶窗口,并响应命令行参数来运行屏幕保护程序。我对它做了一些修改,很快就开始了。你需要响应的开关如下:
Switch | 目的 |
/p | 传递预览的 HWND |
/c | 显示选项对话框 |
/s | 启动屏幕保护程序 |
/a | 密码对话框 |
此外,James 还添加了代码,使得将该屏幕保护程序作为 EXE(当然,它就是 EXE)运行,可以直接启动。我们还没有来得及支持密码,虽然我觉得并不难。我感谢 Neil Van Note,他编写的代码最终让 `/p` 选项变得有意义,没有他,我们就无法提供预览。
多显示器支持
在确认用 C# 制作屏幕保护程序很容易之后,下一步就是确保我们支持多显示器。我自己就有两个显示器,所以很快就意识到,如果我的屏幕保护程序只在一个显示器上运行,那它就不完整了。
Windows Forms 很快就帮了大忙,有一个名为 `System.Windows.Forms.Screen` 的属性。这个小巧玲珑的东西给了我 `PrimaryScreen`,以及一个名为 `AllScreens` 的屏幕数组。从中,我能够构建一个 `Rectangle` 来呈现所有屏幕,以及我自己的数组。为了让弹跳的 Bob 像素完美(即,无论尺寸如何,都能从每个显示器的边缘反弹),我需要跟踪它们在哪个屏幕上,并判断它们何时即将越过边缘。棘手的地方在于屏幕是相对于主屏幕存储的,换句话说,如果我的显示器设置是我的(较小的)次显示器在主显示器的左边,它的矩形会显示为 -1024, 0 到 0, 768,但绘制在 0,0 处会绘制在最左边的屏幕上,而不是报告尺寸为 0,0 的屏幕上。因此,我需要创建一个偏移量来进行检查。
Screen [] scr = Screen.AllScreens;
rcScreens = new Rectangle[scr.Length];
rcScreen = Screen.PrimaryScreen.Bounds;
nTextPos = rcScreen.Bottom;
for (int i = 0; i < Screen.AllScreens.Length; ++ i)
{
rcScreens[i] = scr[i].Bounds;
if (rcScreens[i] != rcScreen)
{
if (rcScreens[i].Right == rcScreen.Left)
{
rcScreen.X = rcScreens[i].Left;
rcScreen.Width += rcScreens[i].Width;
rcScreen.Height = Math.Max(rcScreens[i].Bottom, rcScreen.Bottom);
}
else if (rcScreens[i].Left == rcScreen.Right)
{
rcScreen.Width += rcScreens[i].Width;
rcScreen.Height = Math.Max(rcScreens[i].Bottom, rcScreen.Bottom);
nTextPos = Math.Min(rcScreens[i].Bottom, rcScreen.Bottom);;
}
else if (rcScreens[i].Top == rcScreen.Bottom)
{
rcScreen.Y = rcScreens[i].Top;
rcScreen.Height += rcScreens[i].Height;
rcScreen.Width = Math.Max(rcScreens[i].Right, rcScreen.Right);
}
else if (rcScreens[i].Bottom == rcScreen.Top)
{
rcScreen.Height += rcScreens[i].Height;
rcScreen.Width = Math.Max(rcScreens[i].Right, rcScreen.Right);
}
}
}
Actor.rcScreen = rcScreen;
Actor.rcScreens = rcScreens;
Bounds = rcScreen;
添加一些动态效果
这个项目说明了如果不进行适当的设计,事情可能会变得有点糟糕。它很快就超出了我最初的计划,有些地方可以看出来。良好的设计应该始终有效,如果开始添加代码来处理特殊情况,那就意味着需要更多的设计。
话虽如此,我们最终实现的系统效果相当不错。一个名为 `Actor` 的基类,作为 `BitmapActor` 和 `TextActor` 两个类的共同父类。我还为两者手工编写了数组类,它们只是包装了 `ArrayList` 类,以避免所有那些难看的类型转换,并提供了一些在容器级别上效果更好的方法。
BitmapActor
两个类的通用方法是 `Create`、`Move` 和 `Draw`。`Bitmap` 类中的 `Create` 方法使用作为名称传递的 `string` 从资源加载图像,并将其透明化,颜色键为洋红色。`Draw` 和 `Move` 相当直观。值得一提的两个方面是旋转的 Bob 和带颜色键的 CPian。由于类结构在项目后期才确定,Bitmap Actors 实际上并没有使用它,它们是在主类中绘制的。这不好,但会修复,虽然它不会影响屏幕保护程序的行为。
CPian 的行为很简单。在屏幕保护程序运行时,会维护一个从 0 到 255 再回退的计数器,并将其与 `ImageAttributes` 对象一起使用。代码如下:
if (BitmapActor.nCPian >=0 && nMode!= 3)
{
BitmapActor actor = (BitmapActor)arCPians[BitmapActor.nCPian];
ImageAttributes ia = new ImageAttributes(); ia.SetColorKey(Color.Black,
Color.FromArgb(BitmapActor.nCPianAlpha, BitmapActor.nCPianAlpha,
BitmapActor.nCPianAlpha));
grScreen.DrawImage(actor.actor, new Rectangle(actor.pt.X,
actor.pt.Y, actor.actor.Width, actor.actor.Height), 0, 0,
actor.actor.Width, actor.actor.Height, GraphicsUnit.Pixel, ia);
ia.Dispose();
}
基本上,这意味着我们使一系列颜色透明,底部为黑色,范围逐渐扩大直到覆盖整个范围。这很容易实现,我希望您也认为它看起来非常酷。
旋转的 Bob 也非常简单——它们只是使用一个 `DrawImage` 方法,该方法指定了绘制的矩形,并逐渐缩小它,然后随着它们再次展开而翻转 Bob。技巧是在 `Move` 方法中使用相同的代码,这样 Bob 就只会当它们碰到边缘时才会弹跳,而不仅仅是当它们的矩形达到最大时。
for (int i = 0; i < arActors.Count; ++i)
{
BitmapActor a = (BitmapActor)arActors[i];
if (bRotate)
{
Rectangle rcDraw = new Rectangle(a.pt, a.actor.Size);
if (a.bRotateHorz)
{
rcDraw.Inflate(0, -a.actor.Height * a.nRotate/200);
if (a.bFlip)
grScreen.DrawImage(a.actor, rcDraw, 0,
a.actor.Height, a.actor.Width, -a.actor.Height,
GraphicsUnit.Pixel);
else
grScreen.DrawImage(a.actor, rcDraw, 0, 0, a.actor.Width,
a.actor.Height, GraphicsUnit.Pixel);
}
else
{
rcDraw.Inflate(-a.actor.Width * a.nRotate/200, 0);
if (a.bFlip)
grScreen.DrawImage(a.actor, rcDraw, a.actor.Width, 0,
-a.actor.Width, a.actor.Height, GraphicsUnit.Pixel);
else
grScreen.DrawImage(a.actor, rcDraw, 0, 0,
` a.actor.Width, a.actor.Height, GraphicsUnit.Pixel);
}
}
else
grScreen.DrawImageUnscaled(a.actor, a.pt);
}
TextActor
`TextActor` 最初只是一个 `string` 数组,在主类中绘制。然而,Chris 在屏幕保护程序页面上发帖,建议了四种不同的文本效果:
- 文本在随机位置淡入淡出
- 文本在一个围绕屏幕弹跳的块中
- “某种旋转效果”
- 一个填字游戏效果
我曾希望能完成所有这些,但最终,只有前三种及时完成。正是在这个时候,我决定将 actor 移到类中,所以 `TextActor` 类比 `Bitmap` 类更具独立性。
我猜最有趣的文本模式是旋转模式。第一次调用 `Create` 方法时,它会填充一个包含形成主屏幕上旋转模式的点数组,并与字体大小一起配对在一个类中。然后,当字符串绘制时,当它到达屏幕中间时,它就开始递增一个索引来告诉它要绘制多少字符串,并创建一个包含一个 `string`(只有一个字符,但将其转换为 `string` 可以很好地转换为 `DrawString`)的类的实例,以及一个到数组的索引。当我们停止将文本移入数组时,我们就开始移入一个新的 `string`。项不断递增,这使它们在螺旋中移动,直到它们的索引大于数组,然后被删除。
文本的另一件很酷的事情是,我们的某些模式实现了 HTML 解析,这是 James 的功劳,他的文章可以在这里找到。这里。基本上,我解析他返回的信息并构建一个数组,所以文本会根据标签的位置一点一点地绘制。我没有实现上标和下标,尽管我只需要将字体大小减半并偏移我的下标绘制位置。总的来说,我认为 `TextActor` 最令人满意的地方在于添加新模式到框架中的便捷性,而且我认为基于提供的不同选项添加其他文本模式几乎是轻而易举的。
数组
`Bitmap` 和 `Text` actor 类都有一个手工编写的数组类。如果说 C# 有一件事让我讨厌(我保证不止一件),那就是在愚蠢的情况下需要进行多少类型转换。不得不转换容器的返回值是很糟糕的,所以我会在一个包装数组的类中进行转换。`TextActor` 特别有额外的功能,因为有些模式绘制的文本会填满屏幕,而有些模式一次只绘制一个项。一个名为 `DrawOnce` 的方法可以处理这个问题——它设置了所有必需的东西,包括将除一项之外的所有项标记为不绘制。
链接
屏幕保护程序还提供了一个 Bob 光标,它只是一个我绘制的位图。当它位于滚动或淡入淡出文本项上方时,该项将用 Bob 的位图画笔(也是 Bob)代替纯色绘制,这表示它是热链接。此时单击该项将关闭屏幕保护程序并启动默认浏览器。
绘制速度
屏幕保护程序包含为加快绘制速度而放入的代码的残余部分,这就是为什么 `Move` 方法需要一个 tick 数的原因,想法是使用一个计时器来调用 `Move` 方法,并使用 tick 来基于时间增加一个数字,这样它总是以相同的速度运行,但在较慢的机器上会卡顿。遗憾的是,我们找不到任何方法可以显著加快屏幕保护程序的速度,而缓慢比我启用它时出现的卡顿要好。C# 根本无法胜任当前的任务,尤其是在两个显示器(1280x1024 和 1024x768)上进行如此多的动画,而我的开发机器就是这样。
GDI+
屏幕保护程序使用了许多不同的 GDI+ 功能,包括五种可用画笔中的四种(HatchBrush
未被使用),缩放和翻转绘制,透明蒙版,精确文本测量等等。我鼓励任何想看大量 GDI+ 示例的人购买 Petzold 的书,但如果买不起,你可能会在我们的代码中找到一些有帮助的东西。
最后一分钟更新!!!
我想添加一个最后一分钟的功能,所以设置了一个新选项——清屏。基本上,如果您取消选中此选项,屏幕保护程序将不会清屏,而是会截取桌面快照并将其用作背景,从而产生屏幕永不清除的错觉。这会大大减慢速度,绘制图像比用一种颜色填充位图慢得多。但是,如果您有一台足够快的机器,或者以较低的分辨率运行,它看起来就很酷。
实现屏幕捕获有点麻烦——我基本上不得不导入一半的 GDI 来完成它。
[System.Runtime.InteropServices.DllImport("user32.dll")]
public static extern IntPtr GetDC(IntPtr hWnd);
[System.Runtime.InteropServices.DllImport("gdi32.dll")]
public static extern IntPtr CreateCompatibleBitmap(IntPtr HDC, int X, int Y);
[System.Runtime.InteropServices.DllImport("gdi32.dll")]
public static extern IntPtr CreateCompatibleDC(IntPtr HDC);
[System.Runtime.InteropServices.DllImport("gdi32.dll")]
public static extern bool BitBlt(IntPtr HDC, int Top, int Left,
int Width, int Height, IntPtr SourceHDC,
int X, int Y, int ROP);
[System.Runtime.InteropServices.DllImport("gdi32.dll")]
public static extern IntPtr SelectObject(IntPtr hMemDC, IntPtr hObject);
[System.Runtime.InteropServices.DllImport("gdi32.dll")]
public static extern bool DeleteDC(IntPtr hMemDC);
[System.Runtime.InteropServices.DllImport("user32.dll")]
public static extern int GetSystemMetrics(int nMetrics);
完成之后,我基本上是通过老式方式捕获屏幕。我不需要 `GetSystemMetrics` 来完成这个,因为我已经使用了 `AllScreens` 数组来计算我的总绘制区域的尺寸。
IntPtr ScreenDC = GetDC(IntPtr.Zero);
IntPtr NewDC = CreateCompatibleDC(ScreenDC);
IntPtr BMScreen = CreateCompatibleBitmap(ScreenDC, rcScreen.Width,
rcScreen.Height);
IntPtr OldBitmap = SelectObject(NewDC, BMScreen);
BitBlt(NewDC, 0, 0, rcScreen.Width, rcScreen.Height, ScreenDC,
rcScreen.Left, rcScreen.Top, 0xCC0020); // SRCCOPY
SelectObject(NewDC, OldBitmap);
DeleteDC(NewDC);
bmScreen = Bitmap.FromHbitmap(BMScreen);
其余部分
其余的基本上是 James 的手笔,所以我将把细节留给他,并在他完成后提供链接。我希望至少在不久的将来能添加一个很酷的新模式,但是周一代码冻结,以便我们有时间编写文章和检查 bug,但我就是没时间了。与 James 一起工作非常有趣,并且有这个项目来推动我的 C# 学习过程——我希望您喜欢这个屏幕保护程序,就像我们喜欢制作它一样。
许可证
本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。
作者可能使用的许可证列表可以在此处找到。