VB 与Windows API 讲座(二:图像处理)
VB 与Windows API 讲座(二) --------------------------------------------------------------------------------
萤幕抓取与萤幕保护程式
「VB 没有提供这样的功能, 必须呼叫 Windows API」, 当笔者这样回答读者之後,有些读者会要求笔者介绍 Windows API 方面的书籍, 尤其是中文书, 但实际上,目前市面上只看到一本原文书(ZD Press 出版的 Visual Basic 5.0 Programming Guide to Win32 API), 中文书则未见过, 为什麽没有 Windows API 方面的中文书呢?道理很简单, Windows API 对 VB 的学习者而言, 犹如山峰之颠, 能够一路爬上来的人原本就十分有限,而越往山顶, 人烟越稀少, 因此这样的书写出来, 销售成绩必然惨不忍睹, 也许有些读者会觉得本土的作者实在很混,只愿意写简单的书, 但撰写深入的书籍, 费时费力费神而销售成绩又不好, 最终结果是这种不混的作者混不下去,当过读者而现在又是作者的笔者, 对於此一现象有时也颇感无奈。
不管怎样, 笔者现在已经开始介绍 Windows API 了, 也许真的不想混了,但混与不混不是笔者开启本系列讲座最担心的事情, 笔者所担心的是这样的主题对读者来说会不会太枯燥?会不会太艰涩?会不会像笔者当初研读 Windows API 时, 越研读心中的疑问越多, 就好像进入无底黑洞一样?
为了避免以上的结果, 在内容的介绍上, 笔者会倾向於即学即用的 API, 短期内尽量避免深入 Windows 无底黑洞地带, 而会先到那些有花有草地方。
本期笔者想引领您去的地方是:萤幕抓取程式与萤幕保护程式。
--------------------------------------------------------------------------------
萤幕抓取程式简介
--------------------------------------------------------------------------------
如果不写程式, 抓取萤幕图像的方法是:按下 Print Screen 键, 然後开启 Windows 的小画家, 接着选取小画家功能表的「编辑/贴上」, 即可将萤幕图像复制到小画家的绘图区域中。
如果写程式呢?稍後笔者就会介绍, 介绍以前, 建议您先进入笔者的网站下载笔者所撰写的萤幕抓取程式(CopyScr.vbp),一来可了解程式的基本运作模式, 二来不必自己输入程式, 此一程式执行後,选取功能表的「我抓我抓我抓抓」, 即可将萤幕的图像复制进来。
图-1「萤幕抓取程式」
--------------------------------------------------------------------------------
萤幕保护程式简介
--------------------------------------------------------------------------------
保护萤幕最简单的方法就是不用时关掉它, 但有这种好习惯的人实在不多,所以有人写程式侦测滑鼠与键盘的输入, 如果有一段时间没有滑鼠或键盘的输入,便将电脑视为没有人在使用, 而把萤幕显示成黑色, 用以保护萤幕, 後来又有人觉得黑色太难看了,就加了一点动画, 慢慢地动画越来越多也越有趣, 结果反倒成为萤幕保护程式不可或缺的功能,而不再以保护萤幕为主了。
如何撰写萤幕保护程式当然也是本期的重点, 同样的, 介绍以前, 请进入笔者的网站下载笔者所撰写的萤幕保护程式(saver.vbp),此一程式预设的等待时间是 0.5 分钟, 如果您等待 30 秒不操作键盘与滑鼠,程式便会进入萤幕保护状态, 您也可以设定等待的时间, 如图-2, 或者直接按下「立刻测试」钮以检视萤幕保护程式的动画。(特别注意:请务必以程式右上角的关闭钮结束程式,不要以 VB 的结束钮来结束程式, 否则…, 玩看看, 就知道会有什麽後果)
图-2 「萤幕保护程式」的设定画面
了解萤幕抓取程式及萤幕保护程式的基本运作模式, 接下来让笔者来说明相关的工作概念。
--------------------------------------------------------------------------------
Windows 的绘图观念
--------------------------------------------------------------------------------
VB 用来绘图的方法有 Line(画线及矩形)、Circle(画圆)、PSet(画点)、PaintPicture(描绘图片)…等,这些方法有一共通特点 — 只能描绘在表单(Form)视窗的「显示区域」(Windows 称之为 client rectangle), 想跨越显示区域而把文字或图形描绘在视窗标题区、功能表区、萤幕任意区域…,想都别想, 而以萤幕保护程式为例, 则必须在萤幕任意位置绘图。想在萤幕任意位置绘图,我们必须先了解 Windows 的绘图观念
绘图之前, 先取 hDC
--------------------------------------------------------------------------------
所有 Windows 的绘图, 都必须先取得 hDC, 然後以 hDC 呼叫绘图的 API。hDC 是 handle of DC(Device Context) 的缩写, 有关 handle 的观念笔者上一期已经有所说明,本期不再重述, 至於 DC 呢?Windows 规定任何程式绘图之前都必须先配置一块绘图专用的系统资源(通常是记忆体),这绘图专用的系统资源就是 DC 啦。DC 与其他系统资源一样, 会有一个唯一的 handle 值, 此一 handle 值便是 hDC。
VB 程式呼叫 Line、Circle、Pset、PaintPicture…等方法时, 表面上好像与 DC 无关, 但实际上它们都会预先配置好 DC, 然後透过 DC 来绘图, 您可以查阅表单及 PictureBox 物件的说明文件, 结果可以发现它们都含有 hDC 属性, 此一属性即为表单及 PictureBox 物件用来呼叫 Windows 绘图函数的 DC handle。
BitBlt:图像描绘函数
--------------------------------------------------------------------------------
二话不说, 让我们直接观察一个利用 DC 来绘图的 API 函数 — BitBlt, BitBlt 的作用与 PaintPicture 有点类似, 假设表单上含有一 PictureBox, 而此一 PictureBox 的 Picture 属性亦设定有图片, 若使用 VB 的绘图功能, 则将 PictureBox 的图片绘制在 Form1 的方法是:
Form1.PaintPicture Picture1.Picture, 0, 0
如果使用 BitBlt 函数则是:
ret = BitBlt(Form1.hDC, 0, 0, Picture1.Width, Picture1.Height, Picture1.hDC, 0, 0, SRCCOPY)
作用是把 Picture1 的图像复制到 Form1, 使用此一函数时, 请特别注意参数四(如以上叙述之 Picture1.Width)及参数五(如以上叙述之 Picture1.Height), 这两个参数分别表示长与宽,但单位是「像素」(pixel), 所以执行以上叙述之前, 必须先把表单的 ScaleMode 属性都设定成 "3 - 像素" 才可以, 此一 BitBlt 函数之使用实例请参阅笔者所完成的 BitBlt.vbp(包含在下载之档案中)。
萤幕 hDC 的取得与释回
--------------------------------------------------------------------------------
表单及 PictureBox 物件的 DC 是 VB 预先帮我们准备好的, 在萤幕抓取及保护程式中,我们则必须呼叫 Windows API 取得萤幕的 DC, 如此方可读取萤幕的图像, 以及复制图像到萤幕上。取得萤幕 DC 的函数是 GetDC, 呼叫之叙述如下:
Dim hDC As Long
hDC = GetDC(0)
传回值 hDC 即等於萤幕的 hDC。在此笔者顺便要说明 Windows 另一个重要的观念:系统资源不用时应释回,所以在我们取得萤幕 DC, 并且完成绘图之後, 别忘了将 DC 释回, 此时呼叫的 API 是:
' hDC 是先前 GetDC(0) 的传回值
ret = ReleaseDC(0, hDC)
将萤幕图像复制到 VB 物件中
--------------------------------------------------------------------------------
介绍 BitBlt 及 GetDC 两个 API 函数之後, 萤幕抓取程式几乎是呼之欲出了,例如使用以下的叙述可以将萤幕图像复制到表单物件上:
Dim hDC As Long
hDC = GetDC(0)
ret = BitBlt (Form1.hDC, 0, 0, 萤幕宽, 萤幕高, hDC, 0, 0, SRCCOPY)
ret = ReleaseDC(0, hDC)
不过请注意「萤幕宽」及「萤幕高」两个参数, 这两个参数的长度单位是「像素」,以 640×480 的萤幕为例, 应该指定成 640 及 480, 但问题是别人使用的萤幕可能不是 640×480, 所以我们应该利用 Screen 物件的 Width 及 Height 属性读取萤幕的宽与高(注:此时读取之宽与高是以 Twips 为单位), 然後再利用以下公式求取萤幕以 pixel 为单位的宽与高:
pixel单位之萤幕宽 = Screen.Width \ Screen.TwipsPerPixelX
pixel单位之萤幕高 = Screen.Height \ Screen.TwipsPerPixelY
将萤幕图像复制到表单上面的完整程式, 请参阅笔者所完成的 ScrForm.vbp(包含在下载之档案中)。
--------------------------------------------------------------------------------
萤幕抓取程式之制作
--------------------------------------------------------------------------------
接着请以笔者所完成的 CopyScr.vbp 来例, 让我们来了解萤幕抓取程式还有哪些地方该注意。
AutoRedraw(自动重绘)属性的使用
--------------------------------------------------------------------------------
首先笔者想请您做个实验:执行 ScrForm.vbp 程式, 并且复制萤幕图像到表单上,接着以其他视窗盖住此一表单, 然後再移开, 结果表单上曾经被盖住的区域其图像都会消失。
解决以上问题常用的方法是将表单的 AutoRedraw(自动重绘)属性设定为 True,当我们将表单的 AutoRedraw 属性设定为 True 之後, 将来表单若有区域被覆盖住而又回到萤幕前端,则表单都会自动重绘被覆盖的区域, 使得表单能够展现原有的图像。
利用 AutoRedraw 属性来解决以上的问题看起来十分简单, 但它有个严重的缺点:当我们将表单的 AutoRedraw 属性设定为 True 之後, 表单将会记录每一个绘图的动作, 而当绘图动作越来越多时,所使用的记忆体将会逐渐增加, 自动重绘的时间也会越来越长。为了改善以上的缺点,笔者的作法如下:
Form名.AutoRedraw = True ' 设定自动重绘
...
... 连续性的绘图动作
...
Set Form名.Picture = Form名.Image ' 将表单的图像设定成常驻性的图片
Form名.AutoRedraw = False ' 取消自动重绘
由於 Picture 属性中的图片是常驻的, 因此当我们将表单的 Image 属性(等於表单上的图像)设定给它,便可以将 AutoRedraw 属性设定为 False, 以避免不必要的重绘动作。
以上程式使用了 AutoRedraw、Image、及 Picture 属性, 这些属性除了适用於表单物件之外,也适用於 PictureBox 物件, 而实际上, 笔者所撰写的 CopyScr.vbp 并不是把萤幕复制到表单上,而是复制到 PictureBox 物件之中。
浏览图片的操作介面
--------------------------------------------------------------------------------
除了将萤幕图像复制到 PictureBox 之外, 另一个问题则是萤幕总是比表单的显示区域来得大,以致被复制到 PictureBox 的图像会超出表单的显示区域, 为了让使用者可以看到萤幕的全貌,我们必须在表单上布置卷动轴, 并提供浏览的操作介面。
有关大图片浏览程式, 常见的作法是把显示图片的 PictureBox 布置在另一个 PictureBox 里面, 然後再利用卷动轴调整内部 PictureBox 相对於外部 PictureBox 的位置, 而达到浏览图片的目的, 如图-3。
图-3 图片浏览程式所需之控制元件及其布置
至於相关程式则请参考 CopyScr.frm 的 SetPicture 副程式、VScroll1_Change、及 HScroll1_Change 事件程序。
图片的存档
--------------------------------------------------------------------------------
最後就是图片的存档了, VB 提供的存档叙述是 SavePicture, 呼叫格式如下:
SavePicture 图片, 档案名称
其中「图片」参数可以是物件的 Picture 属性或利用 LoadPicture 所载入的 Picture 物件, 以本程式为例, 则指定成 PictureBox 的 Picture 属性。
--------------------------------------------------------------------------------
萤幕保护程式的制作
--------------------------------------------------------------------------------
笔者所撰写的萤幕保护程式, 是将萤幕切割成 M×N 个方块, 然後在萤幕保护程式启动时,随机变换任意两个方块, 此时所需执行的动作是:
(1) 将 (M1, N1) 座标的方块复制到一暂存区。
(2) 将 (M2, N2) 座标的方块复制到 (M1, N1) 座标。
(3) 将暂存区的方块复制到 (M2, N2) 座标。
假设每一方块的边长等於 80, 则所需执行的 BitBlt 叙述如下:
' hDCMem 为暂存区之 hDC
' hDCScreen 为萤幕之 hDC
ret = BitBlt(hDCMem, 0, 0, 80, 80, hDCScreen, M1 * 80, N1 * 80, SRCCOPY)
ret = BitBlt(hDCScreen, M1 * 80, N1 * 80, 80, 80, hDCScreen, M2 * 80, M2 * 80, SRCCOPY)
ret = BitBlt(hDCScreen, M2 * 80, M2 * 80, 80, 80, hDCMem, 0, 0, SRCCOPY)
暂存区 DC 的取得与释回
--------------------------------------------------------------------------------
在以上程式中, hDCScreen 是利用 GetDC(0) 取得, 而 hDCMem(暂存区的 DC)则必须利用 CreateCompatibleDC 函数来取得, 如下:
Dim hDCMem As Long
hDCMem = CreateCompatibleDC(hDCScreen)
此一函数的作用是建立相容性 DC, 由於传入的是 hDCScreen(萤幕的 hDC),所以建立出来的 hDCMem 将与萤幕相容(主要是色盘之颜色数目相同)。
暂存区 DC 不再使用时应将其释回, 此时呼叫之 API 是 DeleteDC, 如下:(释回萤幕 DC 的函数是 ReleaseDC, 两者不可以混合使用)
ret = DeleteDC(hDCMem)
CreateCompatibleBitmap:为 DC 建立点阵图
--------------------------------------------------------------------------------
取得 hDCMem 之後, 利用 BitBlt 应该就可以将 hDCScreen 的图像复制到暂存区 DC 中, 但实际上不然, BitBlt 只对含有「点阵图」(Bitmap)的 DC 有作用,而经由 CreateCompatibleDC 所建立的暂存区 DC 一开始是不含点阵图的, 所以呼叫 BitBlt 函数并没有作用, 为了让 BitBlt 能够复制图像到暂存区 DC, 以及从暂存区 DC 读取图像, 我们必须为暂存区 DC 建立点阵图, 此时呼叫之 API 如下:
Dim hBitmap As Long
hBitmap = CreateCompatibleBitmap(hDCScreen, 宽, 高) ' 建立点阵图
ret = SelectObject(hDCMem, hBitmap) ' 将点阵图设定给 hDCMem
请特别注意传入 CreateCompatibleBitmap 的 hDC 是萤幕的 hDC, 不是暂存区的 hDC, 其作用是建立与萤幕相容的点阵图(颜色数目与萤幕相同的点阵图), 而接下来必须呼叫 SelectObject 让此一点阵图的 handle(hBitmap) 附属於 hDCMem, 也就是让暂存区 DC 含有点阵图, 如此一来 hDCMem 方可用於 BitBlt 函数。
点阵图的释回
--------------------------------------------------------------------------------
不使用点阵图时, 可呼叫 DeleteObject(hBitmap) 将其释回, 但请注意附属於 DC 的点阵图是不可以释回的, 除非 DC 也已经被释回系统, 例如:
' hBitmap 附属於 hDCMem
ret = DeleteDC(hDCMem) ' 先释回 DC
ret = DeleteObject(hBitmap) ' 再释回点阵图
萤幕保护程式之动画
--------------------------------------------------------------------------------
使用以上所介绍的 API 函数, 总算可以达到随机变换萤幕任意两个方块的目的了,请看笔者所完成的副程式:(此一副程式放置 saver.vbp 专案的 saver.bas 档案中,也包含在下载的档案中)
Sub Saver()
Dim M1 As Long, M2 As Long, N1 As Long, N2 As Long, ret As Long
Dim hDCMem As Long, hDCScreen As Long, hBitmap As Long
Dim sx As Integer, sy As Integer
sx = Screen.Width \ Screen.TwipsPerPixelX ' 以 pixel 为单位之萤幕宽
sy = Screen.Height \ Screen.TwipsPerPixelY ' 以 pixel 为单位之萤幕高
hDCScreen = GetDC(0) ' 取得萤幕 DC
hDCMem = CreateCompatibleDC(hDCScreen) ' 建立暂存区 DC
hBitmap = CreateCompatibleBitmap(hDCScreen, BSize, BSize) ' 建立点阵图
ret = SelectObject(hDCMem, hBitmap) ' 将点阵图设定给暂存区 DC
M1 = CInt(Rnd * sx \ BSize)
N1 = CInt(Rnd * sy \ BSize) ' (M1, N1) 为方块一
M2 = CInt(Rnd * sx \ BSize)
N2 = CInt(Rnd * sy \ BSize) ' (M2, N2) 为方块二
' 方块一与方块二互换
ret = BitBlt(hDCMem, 0, 0, BSize, BSize, hDCScreen, M1 * BSize, N1 * BSize, SRCCOPY)
ret = BitBlt(hDCScreen, M1 * BSize, N1 * BSize, BSize, BSize, hDCScreen, M2 * BSize, N2 * BSize, SRCCOPY)
ret = BitBlt(hDCScreen, M2 * BSize, N2 * BSize, BSize, BSize, hDCMem, 0, 0, SRCCOPY)
ret = ReleaseDC(0, hDCScreen) ' 释回萤幕 DC
ret = DeleteDC(hDCMem) ' 释回暂存区 DC
ret = DeleteObject(hBitmap) ' 释回点阵图, 一定要放在 DeleteDC 之後
End Sub
有了 Saver 副程式, 再配合连续的变换动作, 这就是笔者所制作的萤幕保护程式动画。
键盘及滑鼠的侦测
--------------------------------------------------------------------------------
当萤幕保护程式进入动画时, 程式必须侦测使用者是否有操作键盘或滑鼠,如果有, 则终止动画, 并且将萤幕还原, 而当程式离开动画或尚未进入动画时,必须侦测使用者没有操作键盘或滑鼠的时间是否超过等待时间, 如果是, 则进入动画。
但是在 Windows 的多工作业环境底下, 所有键盘及滑鼠的输入是由 Windows 统筹管里的, 当使用者操作键盘或滑鼠时, 键盘与滑鼠的接收者是 Windows,而 Windows 会根据当时的情况, 决定键盘及滑鼠的输入该传给哪一个程式或视窗,因此每一个程式或视窗只会收到属於自己的键盘及滑鼠输入。
以上的运作模式就好像电话一样, 正常情况之下, 每个人都只会收到属於自己的电话,除非, 嘿嘿, 窃听。在 Windows 的工作模式底下, 想要窃听键盘及滑鼠的输入是可能的,一旦窃听成功, 属於其他视窗的键盘及滑鼠输入都会先传到我们的程式中。
有关窃听方面的程式设计, 笔者想留待下一回再介绍, 因为必须说明的观念实在不少,本期请直接使用笔者所完成的键盘及滑鼠侦测程式 — Saver.vbp 中的 hook.bas 模组, 此一模组虽然没有提供窃听的功能, 但可以侦测到是否有键盘及滑鼠被输入(不管输入的对象是哪一个程式或视窗都可以侦测到)。使用 hook.bas 的方法如下:
1. 侦测键盘及滑鼠的输入之前, 呼叫 StartHook 副程式。
2. 呼叫 StartHook 之後, 只要发生键盘或滑鼠的输入, hook 模组便会将键盘或滑鼠被输入的时间记录在 tmKbMouse 全域变数中, 其他程式可以利用目前时间(呼叫 Now 函数)减去此一变数, 得知使用者隔多久没有操作键盘及滑鼠了, 如果超过等待时间,便可以进入萤幕保护程式的动画。
3. 进入萤幕保护程式的动画之前, 记得把 hook.bas 的 KbMouseDetect 全域变数设定为 False, 接着进入萤幕保护状态之後, 只要使用者再度操作键盘或滑鼠,此一变数便会被 hook.bas 模组设定 True, 而其他程式可以再判断此一变数,以停止动画并还原萤幕。
4. 萤幕保护程式结束以前一定要呼叫 FreeHook 副程式, 不然会造成 Windows 运作不正常, 举例来说, 笔者所撰写的 saver.frm 中便是在 Form_Load 事件程序中呼叫 StartHook, 在 Form_Unload 事件程序中呼叫 FreeHook。
萤幕的还原
--------------------------------------------------------------------------------
当萤幕保护程式停止动画时, 我们必须把萤幕还原, 还原的方法有很多种,例如启动动画之前先储存萤幕的图像, 结束时再回存图像, 不过这个方法比较浪费记忆体,笔者使用的方法是把表单隐藏起来, 然後再显示出来, 接着放到最大, 然後恢复成原大小,作用是利用表单覆盖整个萤幕, 则最後当表单又恢复成原大小时, 萤幕图像即会自动还原,程式如下:
Sub RestoreScreen()
Me.Visible = False
Me.Visible = True
Me.WindowState = vbMaximized
Me.WindowState = vbNormal
End Sub
时间的控制
--------------------------------------------------------------------------------
除了 saver 及 hook 两个模组之外, 最重要的事情就是时间的控制了, 在笔者的 saver 表单中, 布置了两个 Timer 控制元件(Timer1 及 Timer2), 其中 Timer2 用来侦测使用者隔多久没有操作键盘及滑鼠, 若超过等待时间, 则驱动 Timer1 进入动画描绘状态, 其程式如下:
Private Sub Timer2_Timer()
If DateDiff("s", tmKbMouse, Now) > WaitTime * 60 Then
Timer1.Interval = 10 ' 驱动动画
End If
End Sub
至於 Timer1 则是用来显示动画, 并判断使用者是否操作了键盘及滑鼠以决定是否中断动画,程式如下:
Private Sub Timer1_Timer()
If Not StartSaver Then ' 进入动画的初始动作
StartSaver = True
KbMouseDetect = False
Exit Sub
End If
If KbMouseDetect Then ' 用者操作了键盘或滑鼠
Timer1.Interval = 0
StartSaver = False
RestoreScreen ' 还原萤幕
Else
Saver ' 显示动画
End If
End Sub
--------------------------------------------------------------------------------
制作您自己的萤幕保护程式
--------------------------------------------------------------------------------
以上介绍的是笔者所制作的萤幕保护程式, 虽然动画的部分稍嫌简单, 但 Windows 技术的难处均已克服, 如果您想制作自己的萤幕保护程式, 需要修改的是动画的部分,也就是 saver.bas 中的 saver 副程式, 此外, 萤幕保护程式平常是隐藏起来的,所以 saver 表单载入後要呼叫 Me.Hide 自我隐藏, 最後则是把编译成执行档的萤幕保护程式复制到 Windows 的「启动」资料夹中, 让 Windows 启动时, 把萤幕保护程式载入系统中。
[[it] 本帖最后由 三断笛 于 2008-11-22 04:53 编辑 [/it]]