ExcelHome技术论坛

 找回密码
 免费注册

QQ登录

只需一步,快速开始

快捷登录

搜索
EH技术汇-专业的职场技能充电站 妙哉!函数段子手趣味讲函数 Excel服务器-会Excel,做管理系统 Excel Home精品图文教程库
HR薪酬管理数字化实战 Excel 2021函数公式学习大典 Excel数据透视表实战秘技 打造核心竞争力的职场宝典
300集Office 2010微视频教程 数据工作者的案头书 免费直播课集锦 ExcelHome出品 - VBA代码宝免费下载
用ChatGPT与VBA一键搞定Excel WPS表格从入门到精通 Excel VBA经典代码实践指南
查看: 46411|回复: 53

[其他资源] [转载]Win32 API讲座

[复制链接]

TA的精华主题

TA的得分主题

发表于 2005-2-11 00:28 | 显示全部楼层 |阅读模式
本帖已被收录到知识树中,索引项:Windows API应用
第一课∶认识API

一、什么是API

首先,有必要向大家讲一讲,什么是API。所谓API本来是为C和C++程序员写的。API 说来说去,就是一种函数,他们包含在一个附加名为DLL的动态连接库文件中。用标准的定义来讲,API就是Windows的32位应用程序编程接口,是一系列很复杂的函数,消息和结构,它使编程人员可以用不同类型的编程语言编制出的运行在Windows95和Windows NT 操作系统上的应用程序。可以说,如果你曾经学过VC,那么API对你来说不是什么问题。但是如果你没有学过VC,或者你对Windows95的结构体系不熟悉,那么可以说,学习API 将是一件很辛苦的事情。

如果你打开WINDOWS的SYSTEM文件夹,你可以发现其中有很多附加名为DLL的文件。一个DLL中包含的API函数并不只是一个,数十个,甚至是数百个。我们能都掌握它嘛?回答是否定的∶不可能掌握。但实际上,我们真的没必要都掌握,只要重点掌握Windos系统本身自带的API函数就可以了。但,在其中还应当抛开掉同VB本身自有的函数重复的函数。如,VB的etAttr命令可以获得文件属性,SetAttr可以设置文件属性。对API来讲也有对应的函数GetFileAttributes和SetFileAttributes,性能都差不多。如此地一算,剩下来的也就5、600 个。是的,也不少。但,我可以敢跟你说,只要你熟悉地掌握100个,那么你的编程水平比现在高出至少要两倍。尽管人们说VB和WINDOWS具有密切的关系,但我认为,API更接近WINDOWS。如果你学会了API,首要的收获便是对WINDOWS体系结构的认识。这个收获是来自不易的。

如果你不依靠API会怎么样?我可以跟你说,绝大多是高级编程书本(当然这不是书的名程叫高级而高级的,而是在一开始的《本书内容》中指明《本书的阅读对象是具有一定VB 基础的读者》的那些书),首先提的问题一般大都是从API开始。因此可以说,你不学API,你大概将停留在初级水平,无法往上攀登。唯一的途径也许就是向别人求救∶我快死了,快来救救我呀,这个怎么办,那个怎么办?烦不烦呢?当然,现在网上好人太多(包括我在内,嘻嘻),但,你应当明白,通过此途径,你的手中出不了好的作品。这是因为缺乏这些知识你的脑子里根本行不成一种总体的设计构思。

二、API文本游览器。

很多API函数都是很长很长的。想看什么样子吗?如下就是作为例子的API DdeClientTransaction函数∶ Declare Function DdeClientTransaction Lib "user32" (pData As Byte, ByVal cbData As Long, ByVal hConv As Long, ByVal hszItem As Long, ByVal wFmt As Long, ByVal wType As Long, ByVal dwTimeout As Long, pdwResult As Long) As Long 哇!这么长?如果你从来没有接触过API,我想你肯定被吓住了。你也许考虑,该不该继续学下去。不过不要担心,幸运的是Microsoft的设计家们为我们提供了有用的工具,这便是API 文本查看器。

通过API文本查看器,我们可以方便地查找程序所需要的函数声明、结构类型和常数,然后将它复制到剪贴板,最后再粘贴到VB程序的代码段中。在大多数情况下,只要我们确定了程序所需要的函数、结构和常数这三个方面后,就可以通过对API文本游览器的以上操作将他们加入到程序段中,从而程序中可以使用这些函数了。这些是学习API最基本的常识问题,它远远占不到API的庞大的体系内容。今后我们把精力浪费(这绝不是浪费)在哪里呢?那就是∶ 什么时候使用什么函数,什么时候使用什么结构类型,什么时候使用什么常数。

三、API函数声明。

让我们回想一下。在VB中,如何声明函数呢?我想,如果你正在看此文,那么你绝对能够回答得出这个问题。以下便是你应该很熟悉的函数声明∶ Function SetFocus (ByVal hwnd As Long) As Long 即,这行代码定义了名为SetFocus的函数,此函数具有一个Long型数据类型的参数,并按值传递(ByVal),函数执行后将返回一个Long型数据。 API函数的声明也很类似,如,API中的SetFocus 函数是这样写的∶

Declare Function SetFocus Lib "user32" Alias "SetFocus" (ByVal hwnd As Long) As Long 有点复杂了一些。是的,是复杂了点。但我可以告诉你,除了这些多出来的部分,其他部分还是和你以前学到的东西是一样的。函数在程序中的调用也是一样。如: Dim dl As Long dl&=SetFoucs(Form1.Hwnd) 但,一点是清楚的。它不象你自己写的程序那样能够看到里面的运行机理,也不像VB 自带的函数那样,能够从VB的联机帮助中查到其用法。唯一的方法就是去学、查VB以外的资料。

Declare 语句用于在模块级别中声明对动态链接库 (DLL) 中外部过程的引用。对此,你只要记住任何API函数声明都必须写这个语句就可以了。 Iib 指明包含所声明过程或函数的动态链接库或代码资源。也就是说,它说明的是,函数或过程从何而来的问题。 如在上例中,SetFocus Lib "user32"说明 函数 SetFocus 来自 user32.dll文件。主要的dll动态连接库文件有∶ user32.dll Windows管理。生成和管理应用程序的用户接口。

GDI32.dll 图形设备接口。产生Windows设备的图形输出 Kernel32.dll 系统服务。访问操作系统的计算机资源。 注意,当DLL文件不在Windows或System文件夹中的时候,必须在函数中说明其出处( 路径)。如,SetFocus Lib "c:\Mydll\user32" 函数声明中的Alias 是可选的。表示将被调用的过程在动态链接库 (DLL) 中还有另外的名称(别名)。如,Alias "SetFocus" ,说明SetFocus函数在User32.dll中的另外一个名称是, SetFocus。怎么两个名都一样呢?当然,也可以是不同的。在很多情况下,Alias说明的函数名,即别名最后一个字符经常是字符A,如SetWindowsText函数的另一个名称是 SetWindowsTextA,表示为Alias "SetWindowsTextA"。这个A只不过是设计家们的习惯的命名约定,表示函数属于ANSI版本。

那么,别名究竟有什么用途呢?从理论上讲,别名提供了用另一个名子调用API的函数方法。如果你指明了别名,那么 尽管我们按Declare语句后面的函数来调用该函数,但在函数的实际调用上是以别名作为首要选择的。如,以下两个函数(Function,ABCD)声明都是有效的,他们调用的是同一个 SetFocus函数∶ Declare Function SetFocus Lib "user32" "SetFocus" (ByVal hwnd As Long) As Long Declare ABCD SetFocus Lib "user32" Alias "SetFocus" (ByVal hwnd As Long) As Long

需要注意的是,选用Alias的时候,应注意别名的大小写;如果不选用Alias 时的时候,函数名必须注意大小写,而且不能改动。当然,在很多情况下,由于函数声明是直接从API 文本游览器中拷贝过来的,所以这种错误的发生机会是很少的,但您有必要知道这一点。 最后提醒你一句,API声明(包括结构、常数)必须放在窗体或模块的"通用(General Declarations)段。

四、数据类型与"类型安全"

API函数中使用的数据类型基本上和VB中的一样。但作为WIN32的API函数中,不存在Integer 数据类型。另外一点是在API函数中看不到Boolean数据类型。 Variant数据类型在API函数中是以Any的形式出现,如Data As Any。尽管其含义是允许任意参数类型作为一个该API函数的参数传递,但这样做存在一定的缺点。其原因是,这将会使得对目标参数的所有类型检查都会被关闭。这自然会给各种类型的参数调用带来了产生错误的机会。

为了强制执行严格的类型检查,并避免上面提到的问题,一个办法是在函数里使用上面提到到Alias技术。如对API函数 GetDIBits 可进行另外一种声明方法。如下∶ GetDIBits函数的原型∶ Public Declare Function GetDIBits Lib "gdi32" Alias "GetDIBits" (ByVal aHDC As Long, ByVal hBitmap As Long, ByVal nStartScan As Long, ByVal nNumScans As Long, lpBits As Any, lpBI As BITMAPINFO, ByVal wUsage As Long) As Long GetDIBits函数的改型∶

Public Declare Function GetDIBitsLong Lib "gdi32" Alias "GetDIBits" (ByVal aHDC As Long, ByVal hBitmap As Long, ByVal nStartScan As Long, ByVal nNumScans As Long, lpBits As Long, lpBI As BITMAPINFO, ByVal wUsage As Long) As Long 通过本课程前面所学到的知识,我们已经可以得知原型 GetDIBits函数也好,改型 GetDIBitsLong函数也好,实际将调用的都是Alias所指定的 GetDIBits原函数。但你应当看到,两者的区别在于,我们在改型的函数中强制指定lpBits参数为Long形。这样就会使得函数调用中发生的错误机率减少到了最小。这种方法叫做"安全类型"声明。

API函数中经常看到的数据类型有∶Long,String,Byte,Any....(也就这些吧。)

五、常数

对于API常数来讲,没有什么太特别的学问。请看VB中的以下代码∶ Msg = MsgBox("您好", vbOKCancel) 我们知道, vbOKCancel这个常数的值等于1。对上面的代码我们完全可以这样写,而不会影响代码的功能∶ Msg = MsgBox("您好", 1) 但你大概不太愿意选择后一种,因为这会使得看懂代码费劲起来。这种方法也被API采取了。只是API常数必须在事情之前做好初始化声明VB本身是看不懂的。其内容仍然来自与API 文本游览器。具体形式如下等等∶

Public Const ABM_ACTIVATE = &H6 Public Const RIGHT_CTRL_PRESSED = &H4 Public Const RPC_E_SERVER_DIED = &H80010007 Private Const RPC_S_CALL_FAILED_DNE = 1727& 在常数的初始化中,有些程序使用Global,如Global Const ABM_ACTIVATE = &H6,但我认为Public完全可以代替它。过去我也用过Global,但现在不大用了。一会儿用这个,一会儿用那个,各程序之间不能保持一致性了,起码看起来别扭。

六、结构

结构是C和C++语言中的说法。在VB中一般称为自定义数据类型。想必很多朋友都已经认识它。在API领域里,我更喜欢把它叫做结构,因为API各种结构类型根本不是我定义( 自定义)的。 在VB中,API结构同样由TYPE.......END TYPE语句来定义。如,在API中,点(Point)结构的定义方法如下: Public Type POINTAPI X As Long '点在X坐标(横坐标)上的坐标值

Y As Long '点在Y坐标(纵坐标)上的坐标值 End Type 又如,API中矩形(Rect)结构的定义如下∶ Public Type RECT Left As Long '矩形左上角的X坐标 Top As Long '矩形左上角的Y坐标 Right As Long '矩形右下角的X坐标 Bottom As Long '矩形右下角的Y坐标

End Type 这些内容同样可以从API文本游览器中拷贝过来。这些结构中的变量名可随意改动,而不会影响结构本身。也就是说,这些成员变量都是虚拟的。如,POINTAPI结构可改为如下∶ Public Type POINTAPI MyX As Long '点在X坐标(横坐标)上的坐标值 MyY As Long '点在Y坐标(纵坐标)上的坐标值 End Type 不过,一般来讲,是没有这种必要的。结构本身是一种数据类型,因此,使用时必须声明具体变量为该结构型,才能在程序中真正使用到该结构。结构的声明方法和其他数据的声明方法一样,如,以下语句把变MyPoint声明为POINTAPI结构类型∶

MyPoint As POINTAPI 引用结构中的成员变量也十分简单,在结构名后面加上一个".",然后紧接着写要引用的成员变量即可。这很象VB中的引用一个对象的某个属性。如,假如我们把上面已经声明的MyPoint结构中的X变量的值赋给变量Temp& 则代码如下∶ Temp&=MyPoint.X 但,特别注意的是,你千万不要认为上例中的MyPoint是一个值。它不是值,而是地址( 指针)。值和地址是完全不同的概念。结构要求按引用传递给WINDOWS函数,即所有API 函数中,结构都是按ByRef传递的(在Declare语句 中ByRef是默认型)。对于结构的传递,你不要试图采用ByVal,你将一无所获。由于结构名实际上就是指向这个结构的指针(这个结构的首地址),所以,你也就传送特定的结构名就可以了(参见小结,我用红色字体来突出了这种传递方式)。

由于结构传送的是指针,所以函数将直接对结构进行读写操作。这种特性很适合于把函数执行的结果装载在结构之中。

七、小结

以下的程序是为了总结本课中学到的内容而给出的。启动VB,新建一个项目,添加一个命令按钮,并把下面的代码拷贝到代码段中,运行它。

Private Declare Function GetCursorPos Lib "user32" (lpPoint As POINTAPI) As Long Private Type POINTAPI '定义点(Point)结构 X As Long '点在X坐标(横坐标)上的坐标值 Y As Long '点在Y坐标(纵坐标)上的坐标值 End Type Sub PrintCursorPos( ) Dim dl AS Long Dim MyPoint As POINTAPI dl&= GetCursorPos(MyPoint) '调用函数,获取屏幕鼠标坐标

Debug.Print "X=" & Str(MyPoint.X) & " and " & "Y=" & Str(MyPoint.Y) End Sub Private Sub Command1_Click() PrintCursorPos

End Sub

输出结果为(每次运行都可能得到不同的结果,这得由函数调用时鼠标指针在屏幕中所处的位置而决定)∶ X= 240 and Y= 151

程序中,GetCursorPos函数用来获取鼠标指针在屏幕上的位置。

以上例子中,你可以发现,以参数传递的MyPpint结构的内容在函数调用后发生了实质性变化。这是由于结构是按ByRef传递的原因。

评分

1

查看全部评分

TA的精华主题

TA的得分主题

 楼主| 发表于 2005-2-11 00:29 | 显示全部楼层
第二课∶句柄、矩形和画点函数

一、句柄

今天开始,我向大家讲有关API的是实质性内容。我们就从"句柄"开始。 只要你来到了API的世界,经常碰到的问题之一就是句柄。那么究竟什么是句柄呢? 如果你从来都没有听说过"句柄"这个词,可能首先觉得句柄当中有很多内容。其实不然,所谓句柄实际上是一个数据,是一个Long (整长型)的数据。在API中,它经常是以一个参数的形式传递给各种API函数。如: Public Declare Function GetWindow& Lib "user32" (ByVal hwnd As Long, ByVal wCmd As Long)

其中,hwnd就是句柄。在VB里,句柄是一种属性,您打开VB中的对象游览器看一看Form 窗体或者PictureBox控件等究竟有没有hwnd属性。是有的。VB中的解释是这样的∶ Microsoft Windows 运行环境,通过给应用程序中的每个窗体和控件分配一个句柄(或 hWnd)来标识它们。hWnd 属性用于Windows API调用。许多 Windows 运行环境函数需要活动窗口的 hWnd 作为参数。

如果想更透彻一点地认识句柄,我可以告诉大家,句柄是一种指向指针的指针。我们知道,所谓指针是一种内存地址。应用程序启动后,组成这个程序的各对象是住留在内的。如果简单地理解,似乎我们只要获知这个内存的首地址,那么就可以随时用这个地址访问对象。但是,如果您真的这样认为,那么您就大错特错了。我们知道,Windows是一个以虚拟内存为基础的操作系统。在这种系统环境下,Windows内存管理器经常在内存中来回移动对象,依此来满足各种应用程序的内存需要。对象被移动意味着它的地址变化了。如果地址总是如此变化,我们该到哪里去找该对象呢?

为了解决这个问题,Windows操作系统为各应用程序腾出一些内存储地址,用来专门登记各应用对象在内存中的地址变化,而这个地址(存储单元的位置)本身是不变的。Windows 内存管理器在移动对象在内存中的位置后,把对象新的地址告知这个句柄地址来保存。这样我们只需记住这个句柄地址就可以间接地知道对象具体在内存中的哪个位置。这个地址是在对象装载(Load)时由系统分配给的,当系统卸载时(Unload)又释放给系统。 句柄地址(稳定)→记载着对象在内存中的地址────→对象在内存中的地址(不稳定)→ 实际对象

但是,必须注意的是程序每次从新启动,系统不能保证分配给这个程序的句柄还是原来的那个句柄,而且绝大多数情况的确不一样的。假如我们把进入电影院看电影看成是一个应用程序的启动运行,那么系统给应用程序分配的句柄总是不一样,这和每次电影院售给我们的门票总是不同的一个座位是一样的道理。 在VB中获得一个对象的句柄十分简单,如要获取Form1窗体的句柄,就可以这样写∶ Form1.Hwnd

对象的句柄还可以通过API函数来获得,如∶

GetActiveWindow 返回位于最顶部的具有输入焦点的窗口句柄 GetFocus 获得当前线程里补获鼠标输入的窗口句柄 GetForegroundWindow 从位于前台的线程里返回活动窗口的句柄 GetCursor 取得当前指针的句柄 GetDesktopWindow 获取整个桌面的句柄 GetWindow 获得一个窗口的句柄,该窗口与某源窗口有特定的关系 《以上函数说明均可在WinAPI.hlp文件中找到。》

注意以下几点∶ 1,线程内与线程外。(VB不支持多线程)。其他应用程序对此程序来说都是线程外。 2,在windows95操作系统下,各个窗体(包括一些控件,如文本框,图片框等, MICROSORT对它们均统称为窗体)拥有各自的鼠标指针。这和win16下各应用程序使用同样一个指针是截然不同的。

3,每次从新启动,各窗体的句柄都有所变化。Text5 的装载和卸载过程中,句柄始终是在变化着的。这说明了上面提的影院售门票中存在的现象是真实的。 获得对象句柄的函数还有很多,以后碰到它们时再介绍给大家。

二、驾驶句柄

只要弄清了什么是句柄,尤其是窗口句柄,那么操纵一个对象就变得自如一多了。比如,可以通过GetWindowText API函数,我们可以轻松地获得某特定窗口的标题。 GetWindowText 在VB中的声明如下: Private Declare Function GetWindowText& Lib "user32" Alias "GetWindowTextA" (ByVal hwnd As Long, ByVal lpString As String, ByVal cch As Long) 于是,我们可以通过以下一段代码来获得Form1的窗口标题。(新建一个项目,添加一个命令按钮,把以下的代码拷贝过去,还有上面的函数声明。你就可以运行了)

Private Sub Command1_Click() Dim dl As Long Dim FormCaption As String FormCaption = Space(128) dl& = GetWindowText(Form1.hwnd, FormCaption, 128) Print FormCaption End Sub 注∶其中128是指字符串变量FormCaption的长度(又称缓冲区大小)

这有必要吗?为了获得Form1窗体的标题,何必写这么多代码呢?难道这就是API。是的,的确在VB中用 Print Form1.Caption 一行代码就可以抵挡住以上代码了。但是,假如我们启动我们设计的应用程序后,想要在用鼠标点一下别的应用程序的时候,让我们的应用程序显示出那个窗体的标题,那又该怎么办呢?比方说,我们另外启动的是Micorsoft Word, 用鼠标点击Word时让程序显示出"您选择了Microsoft Word"字样。显然只靠VB是办不到的,还得靠API这老手。

当然,您已经具备了这种能力,可以办到这件事情。让我们一起来。 关键的问题是如何获得Word程序的句柄。首先要认清的是,对VB的应用程序来说,Word 是属于进程外应用程序。正和您已经想到的那样,我们可以使用前一个示范程序用到过的 GetForegroundWindow API函数获取它的句柄。因为,当我们用鼠标点击Word时,Word 将成为前台活动窗体。接着呢?当然是 GetWindowText函数显示它的标题就可以了。我们可以采用Timer控件来完成这一切。剩下的事情就不用我多说了。本教程附带的 Program2.vbp程序是,为那些懒得由自己动手写这几行代码的人准备的,但愿您不是。如果你还没有写过API应用程序,可以说这是一个好的机会,还是动一动你的手吧,会有好处的。

程序正常运行了没有?哈,这下感觉到API的魅力了吧? 还想不想继续学下去呀!

三、矩形和点

关于矩形和点,我们在上一个课堂中简单提了一下。在这里,就做一下详细的介绍。 先从简单点(Point)结构的开始。点的结构如下∶ Type POINTAPI x as Long y as Long End Type 在英语里,点叫Point,那么为什么不叫POINT,而叫POINTAPI呢?原来,VB中有Point 方法,这只是为了不必要的冲突或重名。Point用于描述一个位置,当然是一个点的位置了。在屏幕坐标中,x指的是从屏幕左边界到指定点的距离,y指的是从屏幕顶边界到指定点的距离。初次之外,没有太多的学问了,还是那句老话──牢记按引用传递。

矩形的结构和点结构差不到哪儿去,只不过是用两个点来描述的。它的结构定义如下 Public Type RECT

Left As Long Top As Long Right As Long Bottom As Long End Type 以下图描述了RECT结构各字段的含义。 可以看出,一个矩形区域是通过矩形的左上角的一个点和右下角的一个点,共两个点来描述的。其中,left和top字段描述了巨型的第一个角的位置,right和bottom字段描述了矩形的第二个角即右下角的位置。 在VB中,描述一个窗体或控件所处的矩形位置时,经常用Left,top以及Width,height来描述的。其中,Width是一个巨型区域的宽度,Height是高度。在此,您应当看到,RECT结构中并不是这样。如,想要获得宽度,必须从right中减掉left。

API中有若干个函数用来处理矩形数据结构,如,下表所列∶

函 数 功 能 EquaRect 判断两个矩形是否相同,如果相同则返回True(非0) InflateRect 增加或减少矩形的尺寸 InterSectRect 获得两个矩形相重叠的部分,即一个新的矩形 IsRectEmpty 如矩形为空,则返回TRUE OffsetRect 按规定的偏移量移动一个矩形的位置 ptInRect 判断一个指定点是否位于给出的矩形的内部 SetRect 设置巨型的参数字段值 SetRectEmpty 将所有字段都设为0,从而将矩形置空 SubtractRect 将一个矩形从另一个巨型里减去。"即切掉" UnionRect 获得同时包含两个矩形区域的最小矩形。 CopyRect 将一个矩形的内容复制给另一个 这几个函数,都是很好理解和实际应用的。

[此贴子已经被作者于2005-2-11 0:40:52编辑过]

TA的精华主题

TA的得分主题

 楼主| 发表于 2005-2-11 00:29 | 显示全部楼层
[广告] Excel易用宝 - 提升Excel的操作效率 · Excel / WPS表格插件       ★免费下载 ★       ★ 使用帮助
第三课∶窗口函数

一、 关于窗口函数

在上一堂课里,我们已经提出了"句柄"的概念,并为此进行了较深度的讨论。现在来想,我要补充的是,句柄并非是仅仅是窗口才有的。似乎所有的WINDOWS对象都具有句柄。如,GDI对象中的画笔、刷子等,不久即将要学习的设备场景等也有自己的句柄,等等。但,和一些控件不同,这些对象并不属于窗口。 什么是窗口呢?有一句非常有趣的话∶如果它位于屏幕,那么它肯定是在一个窗口里;如果它不在于屏幕,它仍然可能在一个窗口里。窗体也是窗口;滚动条、列表框,文本框,甚至是桌面上的快捷图表也是窗口。更有趣的是,就连作为背景的桌面也是窗口。

很多控件基本上都提供了hWnd属性,但没有提供的也有。对于这些控件可以用SetFocus 方法,将输入焦点设向控件,然后用API函数GetFocus取得当前具有焦点的那个窗口的句柄。当然,这一过程应当写在GoFocus事件中。在我碰到过的问题中有一个有趣的事情是, VB提供的IE控件的hWnd属性不管用。这个问题我一般都采用上述方法来解决的。 很多窗口函数都能对系统的任何窗体进行操作。这意味了VB程序可以直接操纵正在运行中的其他窗体。大家知道,如果对VB设计出的程序未做特殊的处理,那么我们可以启动多个该应用程序实例。我们可以利用API窗口函数来判断一个窗体的先例是否在运行当中,从而可以做到如果有先例则停止启动。很多应用程序就是这个样子的。比如四通利方中文平台,在已经启动的情况下再此启动,程序会告诉用户"四通利方已经在运行",并停止启动。

窗口函数主要可分为四个类型(也许说为"这是为了这次讲课分类出来的"更适合一些)∶ 1、窗口分级函数; 2、窗口位置与大小函数; 3、窗口信息函数; 4、其他窗口函数。 以下我们就一一讲述。但由于窗口函数比较多,在这里就选择性的进行讨论。关于窗口函数有多少,具体的用法如何,您可以注意"小雁侠"的VB API站点的技术文档,或者本站 程序下载栏目中的WinAPI帮助文。由于帮助文其内容来自"小雁侠"的网站,因此其内容更新比较起来会较晚一些。

二、窗口分级函数

系统中运行的窗口是有级别的高低之分的。谁不知道这样?这当然是废话。很多文章都是采用类似的这种废话来做导语,在这里我只不过也是学学而罢。 一、父子关系。 每个窗口都可能有自己的父窗口和子窗口。但,系统中运行的窗口是有限的,说明总得有个窗口是没有其父,我们把它叫做顶级窗口。一般把一个应用程序的主窗口就是顶级窗口,VB独立窗体及MDI窗体都是顶级窗口。窗口间的父子关系一般遵循以下规则∶ 1、父窗口显示时,所有包容在其中的可见的子窗口会随着父窗口的显示而显示出来。

2、父窗口隐藏时,所有包容在其中的子窗口会随着父窗口的隐藏而隐藏。 3、父窗口被卸载时,哈,您已经知道我想说什么了,当然是∶跟着自动卸载。 4、父窗口移动时,跟着移动。 二、兄弟关系及Z序列 当然,一位父亲有好几个儿女,都是常见的事情。同样,一个父窗口可以拥有多个子窗体。比如,位于一个窗体中的各种控件之间以及MDI窗口的各子窗口之间的关系。父窗口与子窗口的显示、隐藏、卸载及移动,其先后顺序是显而易见的。那么各兄弟窗口之间的情况会是如何呢?

显然,两个互相重叠的两个子窗体不能都同时显示出它的全貌,自然有个显示的顺序规则。这个顺序规则叫做Z序列。有个解释为,如果把屏幕坐标看层X和Y轴组成的平面(事实上正是如此),那么作为三维坐标系统Z轴可看做是垂直于屏幕的坐标轴。这样,可以认为屏幕上的所有窗口是垂直于这个Z轴的。在Z轴上,谁在前,谁在后,就产生了一个Z序列。很生动!可用WINDOWS API函数和Visual Basic Z序列方法对Z序列进行控制。

有了以上简单的知识以后,我们就不难应用API窗口分级函数,主要有以下及个∶ 窗口分级函数

函数名 说 明 FindWindow 按类名或窗口名(Caption)查找一个窗口 FindWindowEx 类似于FindWindow提供了更多的功能 GetLastActivePopup 针对指定的窗口,取回上一个活动的弹出式窗口的句柄 GetParent 获得指定窗口父窗口的句柄 GetTopWindow 获得指定窗口的第一个子窗口的句柄 GetWindow 如给定一个窗口句柄,该函数能取回具有特定关系的另一个窗口 的句柄。如,第一个子窗口、父窗口或窗口列表内的上一个或下一个窗口。 SetParent 改变任何窗口的父窗口。 从我个人的经验来看,我最常用的是GetWindow和SetParent函数。

三、窗口位置与大小函数

Windows API函数基本上都是(尤其是USER32.DLL动态连接库内的函数)以屏幕像素为度量单位的。这一点很重要,必须牢记。为此,在使用API函数的时候,我们经常把窗体或图片框控件的ScaleMode属性设置为3,即vbPixels(像素)。 理解窗口位置及大小函数的关键在于分清屏幕坐标、窗口坐标及客户坐标这三个概念。以下图展示了这三个坐标系统之间的关系。

屏目、窗口与客户区坐标系统

只要对这些坐标有了明确的概念,对使用窗口位置及大小函数就不难了。关于窗口的位置,有些函数返回的是上一堂课学习到的RECT结构。有关窗口位置及大小函数如下表所列∶ 窗口位置及大小函数

函数名 说 明 BringWindowToTop 使指定的窗口进入可见窗口列表的顶部,如它被部分或全部隐藏,则令其全部可见。同时,该窗口成为当前活动窗口。只有从前台线程调用时,才生效。 ChildWindowFromPoint 在规定的坐标取得某子窗口的句柄(如果有的话),这儿的坐标是指相对于父窗口的客户区坐标。 ChildWindowFromPointEx 与 ChildWindowFromPoint相同,功能更强。 ClientToScreen 判断指定点在窗口客户区内的屏幕坐标。 GetClientRect 获得对窗口客户区进行表述的一个矩形(RECT)。这是以像素为单位判断客户区大小的一个简便的方法。 GetWindowPlacement 获得指定窗口的一个WINDOWPLACEMENT结构。该结构说明了窗口的状态。 GetWindowRect 用于获得一个矩形(RECT)结构,它描述了窗体在屏幕坐标系统中的位置。 MapWindowPoints 对某窗口客户区坐标内的一个或多个点进行转换,用另一窗口的客户区坐标表示。 MoveWindow 移动指定窗口的位置,并能改变它的大小。 OpenIcon 将一个最小化窗口恢复为原始状态。 ScreenToClient 针对屏幕内一个指定的点,用某个特定窗口内的客户区坐标表示它。 SetWindowsPos 更改窗口的位置和大小,并能修改它在内部窗口列表内的位置(这个列表起着控制窗口先是顺序)。 SetWindowPlacement 在一个WINDOWPLACEMENT结构的基础上,设置某窗口的特征。该结构描述了窗口的状态,以及它在最小化、最大化或正常显示时的位置。 WindowFromPoint 根据屏幕上一个指定的点,判断哪个窗口正位于它的下面。 以上函数的具体用法均可在WinAPI帮助文中找到。在这些函数当中,SetWindowsPos 函数的使用率比较高,现在很多人都是用这个函数来实现"窗口总在前面"的效果,即通过 HWND_TOPMOST常数把窗口置于列表顶部。如果想把From1置于列表顶部,方法如下∶ SetWindowPos Form1.hWnd, HWND_TOPMOST, Form1.Left / Screen.TwipsPerPixelX, Fo rm1.Top \ Screen.TwipsPerPixelY, Form1.Width \ Screen.TwipsPerPixelX, Form1.Height \ Screen.TwipsPerPixelY, 0

您可以把这行代码放在Paint事件中。 另外,GetWindowRect函数、MoveWindow函数以及下一课堂即将要学到的 GetCursorPos函数的相互配合能够实现一个拖动无标题栏的窗口。这是您必须掌握的技巧之一。感兴趣的朋友,可以到《前线》的《源码解析》栏目,下载第4号演示程序。以下是其主要的代码部分∶ Private MyRect As RECT Private MyPoint As POINTAPI Private Movex As Long, Movey As Long

Private Sub Image1_MouseDown(Button As Integer, Shift As Integer, X As Single, Y As Single)

Dim dl& dl& = GetWindowRect(Form1.hwnd, MyRect) dl& = GetCursorPos(MyPoint) If Button = 1 Then Movex = MyPoint.X - MyRect.Left Movey = MyPoint.Y - MyRect.Top End If End Sub

Private Sub Image1_MouseMove(Button As Integer, Shift As Integer, X As Single, Y As Single) Dim dl& dl& = GetCursorPos(MyPoint) If Button = 1 Then dl& = MoveWindow(Form1.hwnd, MyPoint.X - Movex, MyPoint.Y - Movey, _

MyRect.Right - MyRect.Left, MyRect.Bottom - MyRect.Top, -1) End If End Sub 在MouseDown事件中,程序首先用 GetWindowRect 函数确定窗口在屏幕中的位置。再次是通过GetCursorPos函数确鼠标在屏幕中的位置。从而可通过计算获得鼠标位置与窗口左上角之间的横向与纵向距离(Movex与Movey)。 在紧接着发生的MouseMove事件中程序不断地用GetCursorPos函数获得鼠标当前的位置,并按前面已经求得的Movex与Movey判断窗口所应处的位置,而这在MoveWindow函数调用中直接完成。MoveWindow函数将窗体移动到新的位置。

四、窗口信息函数

所谓窗口信息函数就是用来获取有关窗口当前状态信息的函数。这类函数主要有∶

函 数 说 明 GetClassInfo 取得指定窗口的类信息结构 GetClassInfoEx 效果类似于GetClassInfo,但增加了一些功能 GetClassLong,GetClassWord 用于获取窗口类信息 SetClassLong,SetClassWord 用于设置窗口类信息 GetClassName 获取窗口类名 GetDesktopWindow 获取整个桌面(屏幕)的窗口句柄 GetWindowLong,GetWindowWord 获取与窗口有关的信息 SetWindowLong,SetWindowWord 设置与窗口有关的信息 GetWindowText 获得窗口文本。它的效果大致等价于窗体或控件的Text属性 GetWindowTextLength 获得窗口文本的长度,用字符数表示。 IsChild 判断某窗口是否为另一窗口的子窗口或从属窗口。 IsIconic 判断某窗口是否处于最小化状态 IsWindow 判断指定的句柄是否为窗口句柄。 IsWindowEnabled 判断指定的窗口是否处于活动状态。 IsWindowVisible 判断某窗口是否可见。 IsZoomed 判断窗口是否处于最大化状态。 SetWindowText 设置窗口文本。大致等价于窗体或控件的Text属性。 大部分窗口信息函数是非常好理解的,按照有关手册中进行的函数说明,按指定数据类型进行调用即可。有必要说明的是,关于类和窗口的样式位。Windows是用一个长整形的数据的位设置方式来记录类和窗口的样式的。其中,窗口样式由一个32位样式以及另一个32 位扩展样式来构成。类样式操作由上述列表中的GetClassLong以及GetClassLong来进行,窗口样式操作由GetWindowLong 以及SetWindowLong来进行。 由于样式位的内容较多,我无法在此给出,您可以参考有关手册。这里有必要提醒大家的是,您想改变或获取当前窗口或类的样式,绝大多数情况可以考虑样式位操作。下面,就这个问题举一个简单了例子来说明。

下面是用BS_LEFTTEXT样式位将VB复选框或选项按钮的文本在左边和右边之间相互移动来、移动去的程序(是附带的Program1.vbp的部分内容)。程序的原理很简单。首先用 GetWindowLong函数获得当前样式位的信息,然后通过位操作来准备新的样式位信息,最后用SetWindowLong实际地去更改。如下∶ Dim f&, dl& f& = GetWindowLong(Option1.hWnd, GWL_STYLE) '获得当前样式位的信息 If Index = 0 Then f& = f& Or BS_LEFTTEXT

Else f& = f& And Not BS_LEFTTEXT End If dl& = SetWindowLong(Option1.hWnd, GWL_STYLE, f&) '设置新的样式位 Option1.Refresh (对Or和And位操作不熟悉的朋友,请参考有关技术资料) 在这里,对样式位不进行更详细讨论,主要有这样一个原因。用SetWindowLong函数改变一个样式位之后,不会导致窗口发生相应的变化(至少不会立即变化)。有些样式位可能在运行时候才会成功变化,而大多数都只在窗口创建时才生效。因为,用API方式创建一个窗体已经超出了本教程的范围,就算我在这里对样式位谈得再多,您可能也没有多大用处。同时,微软公司没有告诉我们哪些样式位在运行期间安全地改变,因此对具体的情况,只好靠自己进行具体试验。而从我个人的实际编程经验来看,没有特别的要求,我们不大会涉及到这些样式位操作,很多都可在VB中很方便地实现。

本教程还附带了一个Program2.vbp的演示程序。是我本人随便编写的,没什么特别希罕之处。想看就看看好了。 最后,想简单提一提的是,使用SetWindowLong函数的时候,改变GWL_WNDPROC 数据是非常危险的(系统或VB经常挂死),即更改窗口函数的位置。一般,这种更改在需要进行子类处理的地方应用到。每次试运行程序,都应当习惯性地进行存盘。

五、其他窗口函数

API中还有以下本教程未列出的窗口函数,以供大家参考。

函数名 说 明 AnyPopup 判断是否存在可见的弹出式窗口 CascadeWindows 令窗口在一个父窗口内层叠显示 CloseWindow 对指定的窗口进行最小化处理(如果它是个钉级窗口)对弹出式及子窗口无效 DestroyWindow 清除指定的窗口以及下属所有子窗口与包容窗口 DrawAnimatedRects 获得窗口打开或关闭的动画效果 EnableWindow 激活或屏蔽(禁用)指定窗口 FlashWindow 令指定窗口的标题闪烁显示 GetUpdateRect 判断需要更新的那个窗口的位置 GetWindowContextHelpId 取得与窗口关联在一起的帮助场景 InvalidateRect 指定窗口内需要更新的全部或部分客户区 IsWindowUnicode 判断一个窗口是否期望文本消息采用Unicode格式 LockWindowUpdate 允许或禁止描绘指定的窗口 RedrawWindow 一个功能强大的函数,用于控制全部或部分窗口重画 ScrollWindow,ScrollWindowEx 滚动显示窗口的全部或部分客户区 SetWindowContextHelpId 设置与窗口关联在一起的帮助场景 ShowOwnedPopups 隐藏或显示从属于指定窗口的所有保容弹出窗口 ShowWindow 用于设置窗口的状态,其中包括窗口的隐藏、显示、最小化、最大化以及激活等 ShowWindowAsync 类似于ShowWindow,增加了对其他进程内的窗口进行操作的能力 TileWindows 令窗口在一个父窗口内平铺显示 UpdateWindow 立即更新窗口内需要更新的任何部分 ValidateRect 指出全部或部分矩形已经更新,毋需再更新 其中,FlashWindow函数非常有趣,不妨大家试一试。

TA的精华主题

TA的得分主题

 楼主| 发表于 2005-2-11 00:29 | 显示全部楼层
第四课:鼠标、插入符及系统函数

一、指针剪切

什么是指针呢?我想大家都知道,没必要我多讲。只是概念上应当清楚,指针是指针,鼠标是鼠标,鼠标控制着指针。在win16中,指针只有一个,运行在系统中的应用程序共享这个指针。但在win32中,各个窗体都具有着自己的指针。这倒不是说屏幕上能同时出现好几个指针,而是说每个窗体都具有它自己的样式和一些特征的指针。指针移动到某窗体的时候,指针就自动变成那个窗体的指针样式。 对指针需要认识的另一点是,指针的位置都是以像素屏幕坐标指定的。这一点很重要。 接下来,我们进入本小节的主题。

只要你细心一些就能够发现,鼠标指针一般是不能超出屏幕(显示器屏幕)范围的。但这倒也不是绝对的,一会儿你就会明白。指针一般控制在屏幕以内,这是事实。从这个事实中我们可以知道,指针是限定在某个区域之内活动着的。把指针的活动限定一个区域的过程叫做指针剪切。 有关指针剪切的函数有两个。一个是GetClipCursor函数,它可以获得当前鼠标指针的剪切区域。此函数只有一个RECT结构的参数,函数调用结束后,这个RECT结构的数据,中便装载当前鼠标的矩形剪切区域大小。如果在一般情况下调用此函数,你大概获得的正好是屏幕大小。

另一个函数是,SetClipCursor函数,作用是设置指定大小的指针剪切区域。欲设置的指针大小是装在一个RECT结构的数据中传递的。 这两个函数一般搭配使用。在设置新的指针剪切区域前用GetClipCursor函数获取当前指针剪切区域,以便保存其值,而后调用SetClipCursor函数设置新的剪切区域。当这种新的剪切区域不再需要时就向SetClipCursor函数传递先前保存下来的区域值,就可以恢复到原来的指针剪切区域了。 本教程为此提供了Program1.vbp演示程序。本人在设计这个程序时,忘了恢复原来的指针区域,结果指针无法脱离新设定的框架(自然也就无法按动敗翑按钮了),不得不用 CTRL+ALT+DELETE强行关闭VB设计器。望也成为您的一个经验。

二、指针位置函数

指针位置函数,简单得和指针剪切函数差不多。WINDOWS为此也提供了两个函数,一个是GetCursorPos,另一个是SetCursorPos。GetCursorPos函数只有一个参数,用来装载一个POINTAPI结构的数据,该数据说明的是当前指针的X和Y的坐标。但SetCursorPos 函数有所不同,它不依靠POINTAPI结构的数据,需要直接向它传递指针的X和Y两个参数。 本教程提供的program2.vbp正是对这两个函数的使用演示,演示一种指针的自动移动。

在这两个函数中GetCursorPos函数的使用率比较高。作为一个比较精彩的例子,本人把自己的第4号演示程序简化为本教程的program6.vbp。此程序演示如何拖动无标题栏的窗体。程序还使用了我们在前几个课堂当中学习到的一些窗口函数,可作为一个好的复习材料,欢迎阅读。 在program2.vbp中,对程序中采用的一点数学知识,怕有些朋友难以看出来(注意∶不是斈岩岳斫鈹),在此简单说明。首先程序通过一些方法计算出了鼠标指针的开始位置( OldPoint)和终点位置(NewPoint)。指针需要在连接这两个点的直线上移动。我们知道,如果设(X,Y)是指这个直线上的一个点,那么这个直线的方程可以是这样写的∶Y=aX+b

这里a和b是常数。 如果我们知道了a和b,那么只要X按一定的单位量增加,那么Y的值也就可以得知了。问题是如何确定这里的a和b呢? 解决方法是∶ 设,鼠标起点坐标为∶X1,Y1 鼠标终点坐标为∶X2,Y2 则∶ Y1=aX1+b ① Y2=aX2+b ② 从①可得∶b=Y1-aX1 ③

把③代入②可得,Y2=aX2+Y1-aX1 ④ 从④可得∶a=(Y2-Y1)÷(X2-X1) 获得a以后剩下的事情就好办了,可以用①或②获得b。 (这实际上是初中一年级的内容!不过一年级的学生可能不一定知道它代表的是直线。解析几何好象在高中二年级开课。你也可以用解析几何的方法来理解。)

三、其他指针函数

有关的指针函数还有以下几个

函 数 说 明 CreateCursor 在两幅位图的基础上创建一个指针 DestroyCursor 清除一个指针,并释放它占用的系统资源 GetCursor 取得当前指针的句柄 GetDoubleClickTime 了解两次连续鼠标单击之间被看作双击事件的时间长度 LoadCursor 从一个文件载入指针资源 mouse_event 生成一个鼠标模拟事件 SetCursor 选择要使用的指针 SetDoubleClickTime 设置两次连续单击之间被看作一次双击的时间长度 SetSystemCursor 改变任何系统标准指针 ShowCursor 控制指针的可视性 SwapMouseButton 交换左右鼠标按钮的作用

这里写出的很多函数并没有太大的用处。当然,您正好找这些函数,那这对你来说可能是一件非常斨卮髷的发现。你已经看到,对于有些函数功能,VB自身已经拥有的。在这几个函数中本人最喜欢mouse_even函数,有必要向大家介绍。 如果你还没有看Program2.vbp,那么最好先启动并运行它一下。程序的目的是用鼠标指针的移动来自动演示擷ing对你说“Xing对你说”这个按钮的用法。可喜,鼠标指针移动到这个按钮后,按钮并没有被按下。当然,这个程序根本没有设计成按那个按钮,而是直接调用Command2_Click过程。这当然是假的了,糊弄人的了。

有了mouse_even函数以后,我们可以更换一下设计思路。也就是说,当鼠标指针移动到“Xing对你说”这个按钮以后,不要直接调用Command2_Click过程,而是模拟产生对按钮的点击操作。Program3.vbp正是把这个设计思路搬到了实际程序。程序中还使用了一个还没有学到的API函数Sleep。此函数用来使线程等待一定时间,时间以毫秒表示。如果想等待20毫秒,就可以写为: Sleep 20 。由于经常需要使用,先在这里简单讲一讲为好。 请打开并运行Program3.vbp,现在,示范过程是否像个那么一回事?

四、关于插入符

作为一个资源,插入符通常用于表示文本编辑器中的一个位置,用来输入文字。外观上一般是闪烁的线段或者光标块。但,事实上插入符可以是任何样式。其样式可以用位图来处理。但由于使用位图是以后的学习内容,本教程不予演示程序。等您学会了位图,这些都是很轻松的事情。您现在需要认识的是什么是插入符,它到底是如何创建,如何固定位置,如何显示等内容,以便在自己的程序中应用。

函 数 说 明 CreateCaret 创建和选择一个插入符 DestroyCaret 清除插入符 GetCaretBlinkTime 获得插入符的闪烁频率 GetCaretPos 了解插入符在逻辑坐标中的位置 HideCaret 隐藏插入符 SetCaretBlinkTime 设置插入符的闪烁速率 SetCaretPos 设置插入符在逻辑坐标中的位置 ShowCaret 让插入符可见

按道理来讲,插入符函数应当在控件的GotFocus事件中进行创建和初始化处理。也就是说,在控件得到焦点的时候。不幸的是,VB中只有在控件丢失焦点,并由当前应用程序中另一个控件继承焦点的事后,才会触发GotFocus事件。你可以启动Program4.vbp作试验。具体方法是,应用程序启动以后点一下VB设计器,使VB设计器成为活动窗体。这时,应用程序窗口将退到VB后面看不到。确认VB设计器中看到了闪烁的插入符光标后,从任务栏中点一下应用程序任务条,使应用程序窗体成为活动窗口。这时应用程序窗体将从VB 设计器后面跳到前台并显示。这时你可以观察到,上面的Picture1控件的光标消失了。用鼠标点击控件也无任何反应。除非你先点一下下方的Text控件,然后再点Picture控件,光标是不会出现的。当然完全可以用一个计时器来探测控件得到焦点的情况,但这种作法显然麻烦。对于这个问题我一般是在控件的Click事件中写一行代码来激活GotFocus事件内部的代码(参看程序,并将Private Sub Picture1_Click()事件中的代码Picture1_GotFocus无效)。

书本上说,插入符应当在LostFocus事件中清楚掉。但本人认为没有必要,这反而会产生一些符作用。比如在本教程提供的Program4中,如果使LostFocus事件内的代码dl& = DestroyCaret有效,结果当我们点击Text控件的时候会看不到插入符的。所以我认为,在想用插入符的时候,您尽管创建就可以了。不想看到它,就采用HideCaret函数隐藏之。 附带的演示程序Program4.vbp总结了一些常用的插入符函数。

五、键盘控制函数

要使用键盘控制函数,首先必须认识什么是扫描码,什么是虚拟键码。所谓扫描码是一种计算机键盘的硬件决定的代码,可以说不同类型的键盘有不同标准的扫描码。如果我们直接用扫描码来设计程序,那大概是一件非常枯燥的事情,况且很难保证程序的兼容性。与扫描码不同,虚拟键码对每台计算机来讲都是一样的。这里,所谓键码不是别的,只是用哪个数据来表示哪个按键的问题。 从工作原理来讲,计算机键盘向计算机发送的是扫描码,然后来自键盘的扫描码将被转换为虚拟键码,究竟谁去做这种转换,我们就不用关心了。最后,WINDOWS将虚拟键码再转换为ASCII码或字符。正如所看到过的那样,VISUAL BASIC的KeyPressed等事件中递送过来的键码就是ASCII码了。

另外,Unicode字符集和ANSI字符集的概念也需要掌握一些。时间关系,以下直接把一本叫《VB核心技术》的书中一段精彩的论述摘录给大家,作者是Bruce Mckinney。现在就要学习的朋友请点击这里。 现在,把有关键盘处理函数列到如下表,大家先大概看一下究竟都有哪些功能的函数∶

键盘处理API函数

函 数 说 明 ActivatekeyBoardLayout 在由系统定义的键盘布局中选择一种 EnumSystemCodePages 获得系统支持或安装的所有代码页的一个列表 GetACP 获得目前使用的ANSI代码页的表示符 GetAsyncKeyState 获取函数调用时指定虚拟键的状态 GetCPinfo 获取与一个代码页有关的信息 GetKeyboardLayout 判断一个指定线程使用的键盘布局 GetkeyboardLayoutList 取得目前可用键盘布局的一个列表 GetKeyboardLayoutName 取得键盘布局的名称 GetKeyboardState 取得256个虚拟键目前的状态 GetKeyboardType 判断目前使用的键盘类型 GetKeyNameText 判断一个特定虚拟键的名称 GetKeyState 取得指定虚拟键的状态 GetOEMCP 取得目前正在使用的OEM代码页的标识符 keybd_event 为系统生成一个新的键盘布局 LoadKeyboardLayout 载入一个新的键盘布局 MapVirtualKey 在字符与虚拟键、扫描码以及ASCII码之间进行转换 MapVirtualKeyEx 同上 OemKeyScan 将ASCII代码转换成OEM扫描码 SetKeyboardState 将256个虚拟键的状态设为WINDOWS所能理解的状态 ToAsciiToAsciiEx 将一个扫描码和Shift状态转换成对应的ASCII码字符 ToUnicode 将一个扫描码和Shift状态转换成对应的Unicode字符 UnloadKeyboardLayout 卸载以前有LoadKeyboardLayout函数载入的键盘布局 VkkeyScan,VkKeyScanEx 将ASCII代码转换成虚拟键码和Shift键状态。

已经看到有很多键盘控制函数。但这些函数的参数基本上都局限在虚拟码和扫描码,也就是说你能够为这些函数提供虚拟码和扫描码,这些函数基本上都能够掌握和使用的。虚拟键码供有256个,很难在这里给出,但我觉得也没必要给出,简单说明一下就可以了。虚拟键码是以“VK_”开头的,比如Ctrl键为VK_CONTROL,Alt键为VK_MENU。只要你打开API文本游览器,选择常数(Consts)后键入“VK_”那么从列表框中可以看到一大堆以“VK_”开头的虚拟键码。另一种技巧是VB本身也有自己的简码表,VK_CONTROL在VB中大概是vbKeyControl了,你把前面的“vbKey”字样去掉,然后换上“VK_”,估计也能查到相关的虚拟键名( 好象其值也一样,比如vbKeyContol的值为17(十进制),而VK_CONTRIL的值为&H11(16进制)。我不敢保证,也没有太多的时间一一对照,有兴趣的朋友可以直接用vbKeyXXXXX来试一试。)。至于扫描码,就依靠MapVirtualKey函数从一个虚拟键码转换一下就可以了。

为了帮助大家理解好这些函数的实际应用,本教程附带了两个例程,一个是Program4.vbp,另一个是Program8.vbp。 前一个程序是从我的一个演示程序中转化过来的,主要演示按键检测与设定快捷键。这个程序可能有另外的一个用处。举例来说,我一开始不明白键盘上的“Print Screen SysRq”键的虚拟键名是什么。我当然有虚拟键码表,但正如一般人只知道“Ctrl”键名而不知道“CONTROL”这个键名一样(两者实际上是一样),我在列表上无法找到“Print Screen SysRq”这个键的虚拟键名了。后来我启动我这个程序,按了一下“Print Screen SysRq”一看,程序表明它的虚拟键值等于是40(十进制),接着拿Windows提供的计算器一换算,其16进制数据为&H2C。哈,这下就好办了,一看列表,合&H2C对应的虚拟键名为“VK_SNAPSHOT”怎么样,很有意思吧?

第二个程序是我曾经回答一位网由时做的小程序。不幸的是这位网友提的问题在我上一次的硬盘故障中丢失了。大概内容是这样的,很像考试卷里的提问∶“窗体里只有一个文本框,在文本框中输入一些文字后点击一下已经启动的Word,Windows的笔记本等文本编辑器程序窗口,这时,文本框里的内容直接粘贴到这些编辑页中,而不按任何其他的键(如Ctrl+V)。”我的这个程序是以Word为例编写的,要成功地运行它事先您必须启动Word编辑器,并打开一个新的编辑页。

六、系统函数

对与Windows的系统函数,我觉得没必要进行特别的说明,因为这些函数根本就没有特别之处。只是,这些函数主要是用来获取和设置系统有关信息的。比如设置桌面壁纸,默认的窗体呀,命令按钮呀之类的颜色呀什么的,还是您自己看更好一些。如下列表∶

函 数 说 明 Beep 使PC喇叭发出声音 ExitWindowsEx 以各种方式退出和重新启动WINDOWS ExpandEnvironmentStrings 构件环境字串 FreeEnvironmentStrings 释放由GetEnvironmentStrings分配的一个环境字串块 GetCommandLine 取得用于启动应用程序的命令行 GetCOmputerName 取得当前电脑的名字 GetEnvironmentString 取得一个环境块,它提供的对环境变量的访问途径 GetEnvironmentVariable 获得环境块中一个单独的环境变量 GerLastError 取得上一个API函数调用的扩展错误信息。Visual Basic中请用Err对象的LastDLLError属性获得这个值 GetSysColor 取得Windows当前的各种颜色设置 GetSystemInfo 取得与硬件平台有关的信息 GetSystemMetrics 判断一系列系统样式信息;比如菜单栏或窗体标题的高度,垂直滚动条的高度,窗口最小尺寸等等。 GetSystemPowerStatus 取得与电源以及系统状态的有关信息 GetUserName 取得当前的用户名字 GetVersion,GetVersionEx 判断正在运行的Windows和DOS的版本 MessageBeep 生成一个标准系统声音 SetComputerName 设置电脑名 SetEnvironmentVariable 设置一个环境块中的环境变量 SetSysColors 设置Windows对象的当前颜色 SystemParametersInfo 一个功能强大的函数,用来获取与设置众多的系统参数比如保护程序,桌面墙纸,键盘延迟和重复率等等。

当然,从这些函数中忽乱选择一些函数,也做了一个演示程序(Program7.vbp)。分析代码时请掌握好SYSTEM_INFO和OSSYSTEMINFO结构的用法。

TA的精华主题

TA的得分主题

 楼主| 发表于 2005-2-11 00:30 | 显示全部楼层
第五课∶设备场景

一、什么是设备场景

关于设备场景,叫法颇多,有些书上说为设备环境、显示场景,更常见的叫做设备描述表或设备描述体。当然你爱怎么叫随你的便,我还是喜欢说为设备场景。 那么究竟什么是设备场景呢? 设备场景是一种Windows对象,而Windows则是一种图形环境,其图形系统令人难以自信地灵活和强大。而实质上,Widnows下的所有绘图都是通过设备场景进行的,而不是直接对窗口和设备本身进行。为了说明设备场景,很多书都拿一些现实生活中的现象来进行对照说明。其中,最常见的是把它比喻为一位画家在作画。我想大家都看过画家是如何画画的,最起码是在电影里或者是在道旁的广告牌上作画的画家。我们可以想象一下∶有个风景秀丽的白云山(是我瞎起的名)上,有位画家一只手拿着调色板,另一只手则拿着画笔,面对一个画板正在写风景画。有些书认为画家的调色板相当于设备场景,有些书则认为画板相当于设备场景,说法不一。鉴于这种情况,我认为还是直接去说明设备场景比较好。

作为Windows的对象,设备场景实际上是一种Windows内部的数据结构。就象POINTAPI数据结构具有x和y两个属性一样,设备场景同样具有着它自身的属性,只是属性比较多而已,如下表∶

设备场景属性

属 性 默 认 值 背景色(Background color) 白色(White) 背景模式(Background mode) 不透明(Opaque) 位图(Bitmap) 无(None) 刷子(Brush) 白色刷子(White brush) 刷子起点(Brush origin) 0,0 剪切区(Clipping region) 整个窗口或设备表面(Entire window or device surface) 调色板(Color palette) 默认调色板(Default palette) 画笔位置(Pen position) 0,0 绘图模式(Drawing mode) R2_COPYPEN 字体(Font) 系统字体 字间距(Intercharater spacing) 0 影射模式(Mapping mode) MM_TEXT 画笔(Pen) 黑色(Black) 多边形填充模式(Mapping Mode) ALTERNATE 伸缩模式(Stretching mode) BLACKONWHITE 文本色(Text color) 黑色(Black) 视口起点(Viewport origin) 0,0 视口范围(Viewport extents) 1,1 窗口起点(Window origin) 0,0 窗口范围(Window extents) 1,1

请你多看看这张表,对设备场景都有哪些属性,脑子里应当有个印象。事实上,设备场景的很多属性对应于VB中的Form、PictureBox、Text等窗体或控件的属性。比如,字体、背景色、绘图模式等等。可想而知,很多学VB的朋友尽管并不知道什么叫设备场景,但实质上都不知不觉地使用了设备场景。可以说,设备场景是Windows编程中最重要的概念之一。 对于设备场景,有些朋友可能一时不大好理解,这很自然,不用担心谁都是一样。不知对你能否作为一个帮助,我是把设备场景想象成一种配套的(包括画板、调色板、画笔、刷子等)的绘画工具。其中画板是最重要的,其他的东西都是为这个画板服务的。如过你创建了一个设备场景,就等于是你从百货商店买来了这一套绘画工具,从而具备了绘画的条件。但,你的房间总不是那么宽敞的。为了继续绘出别的画、继续购买新的绘画工具,无用的工具应当及时清理掉。因为设备场景本身是占用内存的。不要担心这会降低运行速度,对计算机来说创建一个设备场景,再删掉一个设备场景,那都是瞬息之间的事情,根本谈不上什么浪费时间,绝对不像跑一趟百货商店那么麻烦、费时。对于绘图,你应当认识的一点是,绘图并不是简单地指绘画,输出文本也是一种绘图过程。尽管如此,API函数中图形函数与文本函数大体都是各自各的。绘画和写文本都是在同样的设备场景中进行,这一点很重要。

我想,你大概还是没有理解好,不过没有关系,继续往下看好了。本节中请记住一点∶ Widnows下的所有绘图都是通过设备场景进行的。

二、如何从VB里使用设备场景

如何从VB里使用设备场景呢?VB的设计者们已经为我们想到了这一点,就象为了直接操纵窗体而提供了窗体句柄hWnd一样,为设备场景提供了hDC的句柄属性。很多API函数都是以hDC作为它的一个参数。如果控件没有提供hDC属性,你也可以用GetDC或 GetDCEx函数去获得,不用时就用ReleaseDC函数把它释放掉即可。不过这得需要控件具有hWnd属性。如果连hWnd属性也没有,那就没办法了,大概那根本不是绘图的地方。 按自己的需要也可以创建一个或多个设备场景,需要多少就创建多少。这可能是 CreateCompatibleDC和CreateDC函数的最拿手的好戏。设备场景可以同某一窗口关联,也可以以孤立的方式存在。所谓关联就是说,你在这个设备场景中绘图,内容将立即输出到关联的窗口,所谓孤立就是指尽管你在这个设备场景中绘出了图形,但它只存在于这个设备场景,而不显示在哪里。我们可以通过多个设备场景来对一个最终图形进行光栅运算,从而进行加工,最后把图象传送给已与窗体关联的设备场景,让它显示出来。被创建的设备场景,如果不再使用,应当删去。(这段描述,请参考附带的演示程序WindowDC.vbp。)

以下表格总结了用于获取和释放设备场景的API函数。

函 数 说 明 CreateCompatibleDC 创建一个与源DC兼容的内存设备场景。内存设备场景可看作一种对内存中设备的模拟。通过在设备中选进一幅位图,可创建与设备兼容的内存影响。 CreateDC 为指定的设备创建一个设备场景,它通常用于为打印机创建一个DC。 CreateIC 为指定的设备创建一个信息场景(IC)。IC类似于设备场景,只是所需系统开销更少,它可用来获取关于设备的信息,但不能作绘图操作。 DeleteDC 删除一个已经建立的设备场景。可用它释放CreateDC,CreateIC和CreateCompatibleDC 函数建立的设备场景。 GetDC,GetDCEx 为指定窗口获取一个设备场景。若窗口类使用专用DC,用该函数取回设备场景,否则它从Windows缓存中获取一个DC,用GetDCEx才能在窗口使用专用DC时获得一个缓存场景。 GetWindowDC 该函数与GetDC类似,只是取回的设备场景是针对窗口整个,而不是客户区。 ReleaseDC 释放用GetDC,GetDCEx和GetWindowDC获得的设备场景。若DC是一个专用设备场景,该函数无效。 WindowFromDC 判断与指定设备场景关联在一起的窗口句柄。 以下的演示程序是一个非常敾奶茢的程序。这个程序的目的是这样的∶我们要在Text控件中画一个圆。当然我估计没有人会选择Text控件来绘图。我总是喜欢用API做出一些歪门邪道的事情。以下是程序的源代码,你也可以直接启动教程附送的Program1.vbp演示程序。使用方法是,当我们点击Text控件时那里将出现了一个空心圆。

Option Explicit

Private Declare Function GetDC& Lib "user32" (ByVal hwnd As Long) Private Declare Function Ellipse& Lib "gdi32" (ByVal hdc As Long, ByVal X1 As Long, ByVal Y1 As Long, ByVal X2 As Long, ByVal Y2 As Long) Private Declare Function ReleaseDC& Lib "user32" (ByVal hwnd As Long, ByVal hdc As Long)

Private Sub Form_Load() Text1.Text = "" End Sub

Private Sub Text1_Click() Dim TextDC As Long TextDC = GetDC(Text1.hwnd) '获取设备场景句柄

Ellipse TextDC, 0, 0, 100, 100 '画空心圆 ReleaseDC Text1.hwnd, TextDC '释放设备场景 End Sub 程序的工作原理是这样的。首先在Form_Load()事件中,我们用Text1.Text = ""代码来清除Text控件内的所有文本。接着,当我们点击Text控件时,程序在那里画一个圆,用的是API函数Ellipse(这个函数是下一课堂的内容)。Ellipse函数的功能很简单,在左上角为(x1,x2),右下角为(x2,y2)的矩形中画一个内切的椭圆。而Ellipse函数需要一个DC(设备场景句柄),但Text控件却没有提供hDC属性,没有办法只好用GetDC函数来获取∶TextDC = GetDC(Text1.hwnd)。然后做绘图操作,最后用ReleaseDC函数来把设备场景释放掉(关联设备场景,不能删除)。

如果我们把上述代码中的 TextDC = GetDC(Text1.hwnd) Ellipse TextDC, 0, 0, 100, 100 改成 TextDC = GetDC(0)

Ellipse TextDC, 0, 0, 500, 500 会怎么样呢?不妨你试试看。学会了什么?还是直接告诉你吧∶整个屏幕的设备场景句柄总是等于0。

有意识吧?你也可以让Text控件输出一张图片(照片)。方法很简单。你可以使用Picture控件,在那里先装入一个图片,然后用API函数BitBlt把图片传送到Text控件就可以了,由你自己研究好了。

哈,现在感觉到设备场景真的存在了吧?呵呵,接着来吧!

三、深入设备场景( 逻辑坐标,逻辑窗口,设备坐标,视口 )

设备坐标(Device Coordinate)又称物理坐标(Physical Coordinate),是指输出设备(显示器,打印机等)上的坐标。通常将屏幕上的设备坐标称为屏幕坐标。屏幕坐标以对象距离屏幕左上角的水平距离和垂直距离来指定对象的位置,是以像素为单位来表示的。X轴方向向右,Y轴方向向下,坐标原点位于窗口左上角。 逻辑坐标(Logical Coordinate)是系统用作记录的坐标。在缺省的坐标模式(MM_TEXT坐标模式)下,逻辑坐标的方向和单位与设备坐标的方向和单位相同。但这还得有个前提条件∶1,窗口为非滚动窗口;2,窗口为滚动窗口,但垂直滚动条位于边框的最上端,水平滚动条位于边框的最左端。

为了在不同领域使用逻辑坐标,Windows提供了以下8种坐标模式(也称影射模式)。

坐标模式 说 明 MM_TEXT 设备场景默认坐标模式,也是VB窗体或图片控件的坐标模式,等同于ScaleMode=vbPixels。以像素为逻辑单位,X轴向右为正,Y轴向下为正。也是VC的缺省模式。 MM_HIENGLISH 以0.001英寸为逻辑单位,X轴向右为正,Y轴向上为正。 MM_LOENGLISH 以0.001英寸为逻辑单位,X轴向右为正,Y轴向下为正。 MM_HIMETRIC 以0.01毫米为逻辑单位,X轴向右为正,Y轴向下为正。 MM_LOMETRIC 以0.1毫米为逻辑单位,X轴向右为正,Y轴向下为正。 MM_TWIPS 以一旂緮(1/1440英寸)为逻辑单位,X轴向右为正,Y轴向下为正。 MM_ANISOTROPIC 可以自行设定逻辑单位的长度, X轴向右为正,Y轴向上为正。X轴和Y轴的长度单位可以不相同。 MM_ ISOTROPIC 可以自行设定逻辑单位的长度, X轴向右为正,Y轴向上为正。X轴和Y轴的长度单位必须相同。

尽管在影射过程中,逻辑坐标到设备坐标的坐标转换是自动进行的,但你也可以用有关的API函数进行逻辑坐标与物理坐标间的坐标转换。有以下两个函数∶

函 数 说 明 DPtoLP 将设备坐标系统中的点转换到相应设备场景的逻辑坐标系统 LPtoDP 将设备场景逻辑坐标系统中的点转换到物理或设备坐标系统

逻辑坐标就是设备场景内的坐标。这个坐标系统内有一个逻辑窗口,我们就是在这个逻辑窗口上进行绘图操作的。逻辑窗口本身是有大小的。这个大小正是由设备场景的窗口起点(Window origin)和窗口范围(Window extents)两个属性来决定的。两个属性分别表示逻辑窗口在逻辑坐标上的左上角的点和由下角的点。由于逻辑坐标的坐标模式不一定是缺省的MM_TEXT,所以这两个属性中的x,y值有可能是负数(当然,这不是常见的情况)。 在大多数情况下,我们需要画在设备窗口中的图象实际地输出到设备窗口中。对显示器来讲,设备窗口就是屏幕了。那么这里自然存在这样一个问题∶即,逻辑窗口中的图象应该出现在屏幕的哪一个位置,也是说设备窗口的哪一个位置。Windows早已经为我们想到了这一点∶设备上对应于逻辑窗口的区域叫做视口(Viewport)。既然是和逻辑窗口对应的,那么,视口自然也有大小。这个大小,正是由设备场景的视口起点(Viewport origin)和窗口范围(Window extents)两个属性来决定。

为了加深理解以上的内容,我做了一个影射过程图示(适合MM_TEXT坐标模式),建议看好了从新阅读以上内容。

逻辑窗口和视口的大小并不相同的,两者的高宽比率也不一定相同。可以看出,逻辑窗口到视口的影射当中必然存在坐标转换。但不用操心,我们根本用不着管那些事情, Windows会自动的为我们做好这一切,你需要把握的只是逻辑窗口的大小和视口的大小。在MM_TEXT模式下,只要两者的大小一定,不管其大小是多大,逻辑窗口的图象正好伸缩成视口大小并在视口中显示。嗯,你大概能想起VB中的Scale方法吧,该方法用来更改Form、PictureBox或Printer的坐标系统。想必,你已经猜到了,它更改的实际就是设备场景的逻辑窗口的大小。

设备场景影射函数

函 数 说 明 GetDCOrgEx 用屏幕坐标定义窗口客户区起点位置。 GetGraphicsMode 定义是否允许增强影射模式。 GetMapMode 定义设备场景当前影射模式 GetViewportExtEx 获取视口范围 GetViewportOrgEx 获取视口起点 GetWindowExtEx 获取逻辑窗口范围 GetWindowOrgEx 获取逻辑窗口起点 OffsetViewportOrgEx 视口起点偏移 OffsetWindowOrgEx 窗口起点偏移 ScaleViewportExtEx 视口范围缩放 ScaleWindowExtEx 逻辑窗口范围缩放

四、区域和剪裁区域

请注意,注意、注意、注意!现在,估计有些朋友认为,视口就是窗口在屏幕中的实际位置和大小。如果真的这样认为的话,那你就大错特错了。因为在很多情况下你会发现,实际图象小于窗口或大于窗口。还存在另一个问题。即,屏幕中有很多窗口,按每个窗口一个视口算的话视口也就和窗口一样多了。这些视口会重叠的。那么在重叠的部分中应该显示哪一个又不应该显示哪一个呢?为了解决这类问题,Windows提供了一个剪裁功能。 所谓剪裁功能是指若画面超出指定区域(剪裁区)范围将被自动剪切掉而不显示出来的功能。这个功能由Windows自动完成,你所要做的是要么保持默认的剪裁区域,要么重新设定它。至于区域,你就把它理解为多边型也无妨。标准的概念是,区域是指描述设备场景中某一块平面范围的GDI对象。每个区域都有一个对象句柄,并且,同任何GDI对象一样,在不用时可调用API函数DeleteObject(下一课堂中讲)删除。API相关的区域函数,由下表所列。

区域有句柄

函 数 说 明 CombineRgn 以选定的方法合并两个区域 CreateEllipticRgnCreateEllipticRgnIndirect 创建圆形和椭圆形区域 CreateRectRgnCreateRectRgnIndirect 创建矩形区域 CreateRoundRectRgn 创建圆角矩形区域 EquaRgn 确定两个区域是否相等 ExtCreateRegion 允许把某一转换应用于区域 GetRgnBox 获取一个矩形,该矩形为选定区域的边界 GetRegionData 取回描述区域的数据 OffsetRgn 平移选定区域 PtInRegion 确定某点是否在选定区域内 RectInRegion 确定矩形的一部分是否在选定区域内 SetRectRgn 修改选定区域以描述一个矩形区

这样剪裁区域实际上也是属于一个区域,只是它多负有着剪裁的功能。你可以获取一个窗口的剪裁区域句柄,然后用区域函数加工它,再付给该窗口,或者干脆为窗体从新创建一个剪裁区域,从而改变一个窗口的剪裁效果。窗体的整个窗口(包括标题栏和状态栏等)、还有窗口的客户区都有着自己的剪裁区域。剪裁区域一般是矩形,但也可以是任何形状,甚至是由互相分离的多个区域组成。所谓窗口的剪裁区实际上就是设备场景的剪切区(Clipping region)属性。一个窗口默认的剪裁区就是改窗口本身。

窗口也有剪裁区、窗口的客户区也有剪裁区,这无疑是说明窗口也有设备场景,客户区也有设备场景。由于习惯问题,经常说的斈骋淮翱诘纳璞赋【皵指的是该窗口的客户区的设备场景。你可以设置整个窗口的剪裁区为任何形状以实现任何形状的窗体。这就得靠宝贝函数SetWindowsRng啦!至于整个窗口的设备场景句柄,可以用API函数GetWindowDC来获取(WindowsDC.vbp演示程序中有此函数的用法)。

区域剪裁函数

函 数 说 明 ExcludeClipRect 在剪裁区中挖去一个特定巨型区 ExSelectClipRegion 用当前剪裁区组合一个区域 GetCliBox 获取一个矩形,该矩形为剪裁区的边界 GetClipRgn 获取某个窗口的剪裁区 IntersectClipRect 将当前区域和选定矩形的交集设为剪裁区 OffsetClipRgn 按定义量平移剪裁区 PtVisible 确定选定点(当前剪裁区中的)是否可见 RectVisible 确定当前矩形某部分(当前剪裁区中的)是否可见 SelectClipPath 为设备场景选择一个路径作为剪裁区(由于路径的概念问题,此函数在下一课堂中讨论) SetWindowRgn 为窗口设定剪裁区;这使您能够创建出任意形状的窗口。

本教程附带的演示程序Clip.vbp是为了展示Windows的剪切功能而设计的。 另外,坐标转换中还有一个叫"世界转换"的内容,但它只设计到WindowsNT用户,感兴趣朋友请自己查看有关资料。

TA的精华主题

TA的得分主题

 楼主| 发表于 2005-2-11 00:30 | 显示全部楼层

五、设备场景在VB

有时候,我们可能对设备场景属性进行大量的修改。使用完一个设备场景后,作为一个好的编程习惯,应当把设备场景状态恢复到我们控制这个设备场景以前的状态,以便保证其他代码正确进行绘图工作。这倒并不是说一定,只是一个好的习惯问题而已。 要把VB设备场景恢复到VB控制之前的初始状态,可以使用API函数SaveDC和RestoreDC函数,前者用来把设备场景保存到设备场景堆栈,后者则从堆栈中恢复设备场景属性。还是保持统一风格,画一个表吧∶

设备场景堆栈函数

函 数 说 明 SaveDC 将某设备场景状态和属性保存到设备场景堆栈。 RestoreDC 从Windows设备场景堆栈恢复某一设备状态和属性。

接下来是,让我们研究一下设备场景的自动重画属性方面的问题。我们知道,VB 中Form和PictureBox控件具有AutoRedraw属性,用来决定是否用持久图形或通过Paint事件重绘对象。当AutoRedraw属性设为Flase时,用GetDC函数获取的设备场景句柄和控件提供的hDC句柄正好相同,说明都是指同一个设备场景。但是,当将其设为True时,用GetDC 获取的设备场景句柄不同于hDC属性所返回的场景句柄。有一点是清楚的,在这种场合的确存在两个设备场景。但是,这两个设备场景不可能同时与某一窗体关联的,不然的话,窗体应该显示哪一个设备场景中的图象呢?我们可以编写一个小小的程序来考察这个问题,(program2.vbp),程序代码如下∶

Option Explicit

Private Declare Function GetDC& Lib "user32" (ByVal hwnd As Long) Private Declare Function WindowFromDC& Lib "user32" (ByVal hdc As Long)

Private Sub Command1_Click() Dim MyDC As Long

Form1.AutoRedraw = True MyDC = GetDC(Form1.hwnd) Text1.Text = MyDC Text2.Text = Form1.hdc Text3.Text = WindowFromDC(MyDC) Text4.Text = WindowFromDC(Form1.hdc) Text5.Text = Form1.hwnd End Sub

程序运行结果如下(在你的计算机上运行不一定是这些数据)∶ 1、用GetDC来获取的设备场景句柄∶10010,其关联窗体句柄为∶3024 2、hDC书信返回的设备场景句柄∶10234,其关联窗体句柄为∶0 3、Form窗体的句柄为3024

1、 程序说明,用hDC来获取关联窗口时,函数执行出错,说明hDC并不与窗体关联。所以,当我们用以下代码来向窗体画一个圆时,我们将什么也看不到。(新建一个项目,把下面的代码粘贴上去,就可以运行) Private Declare Function Ellipse& Lib "gdi32" (ByVal hdc As Long, ByVal X1 As Long, ByVal Y1 As Long, ByVal X2 As Long, ByVal Y2 As Long)

Private Sub Form_Click() Form1.AutoRedraw = True Ellipse Form1.hdc, 0, 0, 100, 100 End Sub 尽管什么也看不到,但如果你用其他窗体掩盖程序窗体后,让程序窗体再次显示时,你会发现图形出现了。为什么呢?原来是Refresh方法所做所谓(请参看后面的表)。

以下的代码能够直接的把绘图内容显示出来。(新建一个项目,把下面的代码粘贴上去,就可以运行) Private Declare Function GetDC& Lib "user32" (ByVal hwnd As Long) Private Declare Function Ellipse& Lib "gdi32" (ByVal hdc As Long, ByVal X1 As Long, ByVal Y1 As Long, ByVal X2 As Long, ByVal Y2 As Long)

Private Sub Form_Click() Dim MyDC As Long Form1.AutoRedraw = True MyDC = GetDC(Form1.hwnd) Ellipse MyDC, 0, 0, 100, 100 End Sub 尽管图形出现了,但这次的情况正好相反。也就是说,如果你用其他窗体掩盖程序窗体后,让程序窗体再次显示时,你会发现图形消失了。(但如果遮住图形的一半,图形仍然会存在)。是因为hDC中没有图象,Refresh方法用hDC来覆盖GetDC了。

所以,如果你使用的是API绘图函数,那么在AutoRedraw = True时候,设计前对预期的结果应当有超前认识。在这种情况下windows是先在hDC中绘图,然后用GetDC来创建临时与窗体关联的设备场景,然后把hDC的内容拷贝到这个临时缓冲设备场景中来最终显示图形。我觉得这个过程已经封装在VB的Line等绘图函数里面了(注意,这只是我的猜测,不一定准确)。如下∶ Option Explicit

Private Declare Function GetDC& Lib "user32" (ByVal hwnd As Long)

Private Declare Function Ellipse& Lib "gdi32" (ByVal hdc As Long, ByVal X1 As Long, ByVal Y1 As Long, ByVal X2 As Long, ByVal Y2 As Long) Private Declare Function BitBlt& Lib "gdi32" (ByVal hDestDC As Long, ByVal x As Long, ByVal y As Long, ByVal nWidth As Long, ByVal nHeight As Long, ByVal hSrcDC As Long, ByVal xSrc As Long, ByVal ySrc As Long, ByVal dwRop As Long) Private Declare Function ReleaseDC& Lib "user32" (ByVal hwnd As Long, ByVal hdc As Long)

Private Sub Form_Click()

Dim MyDC As Long Form1.AutoRedraw = True Ellipse hdc, 20, 20, 80, 80 '实际使用中可以用Form1.Refresh代替以下三行,以简化代码。 MyDC = GetDC(Form1.hwnd) BitBlt MyDC, 0, 0, Form1.Width, Form1.Height, Form1.hdc, 0, 0, vbSrcCopy ReleaseDC Form1.hwnd, MyDC End Sub 运行此程序后,可以发现,不管任何时候,图象总是显示在窗体之中。 以下表格说明了绘制和刷新窗口有关的VB属性。

VB属性

AutoRedraw=FALSE

AutoRedraw=TRUE

hDC属性

窗口设备场景句柄,即与窗口关联。

指向内存设备场景的句柄(常说为内存设备场景),与窗口兼容,这幅位图叫作固定图象位图,针对窗口则叫屏幕。此设备场景不与窗口关联。

Picture属性

窗口背景图象位图的句柄

背景图象位图的句柄,改变它将引起固定图象位图立即刷新以反映它。

Image属性

窗口固定图象位图句柄,绘制时该图象不能改变。

窗口固定图象位图句柄,由hDC属性指给设备场景的句柄。

Cls方法

清除窗口使其只能现实背景位图

清楚固定位图到背景色,并将复制到背景位图(若有)拷贝到固定固定位图。

Refresh方法

将背景图片(若有)复制给窗口,然后然后产生Paint事件。所有的绘图都是通过hDC设备场景直接对窗口进行。

将固定图象复制到窗口,所有的绘图都是通过hDC设备场景直接的对固定图象位图进行。

在这一节中要学会的关键的一个应用问题是,会使用内存设备场景。这里AutoRedraw = True时的以hDC为句柄的设备场景就是内存设备场景,它不与窗体关联。因此可以在此设备场景中进行多方面的图形加工处理,最后传送到用GetDC获取的设备场景,这样图象的加工过程就可以隐藏掉了。作为例子,你可以下载《VB前线》《源码解析》的Play024.zip文件(滤波器演示程序)。

六、设备场景信息函数

在本课堂一开始,讨论什么是设备场景的时候,我们已经给出了设备场景的各个属性。本节主要讨论有关获取这些属性信息的函数及其应用实例。 设备场景的信息是用Windows通用设备接口(GDI)提供的GetDeviceCaps函数获取设备场景信息。此函数的功能是,根据指定设备场景代表的设备的功能返回信息。此函数有两个属性,其一是设备场景句柄,其二是返回信息的类型参数。对于使用该函数来说,主要的就是认识这些常数的含义。由于,本人精力有限,在此不能提供中文文档了,请参考MSDN等其他有关资料。

总算把设备场景讲完了,下一堂课我们将讨论如何在这个设备场景中绘图。我想,那是很轻松的事情。当然,只要你理解好了设备场景。

TA的精华主题

TA的得分主题

 楼主| 发表于 2005-2-11 00:31 | 显示全部楼层
[广告] VBA代码宝 - VBA编程加强工具 · VBA代码随查随用  · 内置多项VBA编程加强工具       ★ 免费下载 ★      ★使用手册
第六课∶绘图函数

一、位操作

前几天,在很远很远又是那么远的地方,有位网友来信问一些有关位操作的内容。我一开始不大注意这个环节,认为估计大家都能知道。可现在来仔细一想,也并非如此。《Win32 API开发人员指南》一书中也讲了一些位操作的内容,但它位于一开始的象是概论的部分。那么,我想,对位操作不太熟悉的朋友,可以通过以下我对这位网友的回答,学习或加深一些认识。 这位网友的提问非常认真,叫我不得不也跟着认真回答。以下是来信中的一段内容∶ "Not" "And" "Or" 的用法?(因为资料里只是讲了什么"True"呀"False"呀,请麻烦您了)经过实践(正所谓实践出真知嘛),得到以下结论:

12 And 15 =12 '简化了的---返回小 12 Or 15 =15 '返回大

12 And 16 =0 '返回零?? 12 Or 16 = 28 '返回 相加??

Not 12 = -13 '(取整数部分+1)的相反数 Not 100 = -101 '(取整数部分+1)的相反数

以下是我的回复内容∶(对原文做了很多改动和加工。) 是位操作运算符。 例如 3 and 1 = 1 来说,为什么结果会等于1呢? 原因是这样的。3等于是11(2进制),1等于是01(2进制) (这里只考虑了两位,实际一般是8位、16位和32位等)

And就是斖?睌的意识。二进制数据中,1代表真(True),0代表假(False)。其运算规则就是∶在两个数相对应的位中,如果两个位同时是1则得1,否则就得0。 具体来讲,11 和 01,首先底位都同时是1,因此得到1,而高位来说前一个数(11)是1,而另一个(01)则是0,这样就不符合斖?睌的要求了,结果得到0。这样11 and 01 就等于是01了 。二进制的01就是十进制的1。因此 可以判断 3 And 1=1 啦。不知,学会了没有? 不访拿你的例子来说,比如 ∶12 And 15=12。结果为什么会等于12呢?

让我们分析一下。首先将各数据转换为2进制数,如下∶ 12=1100 15=1111

这样从左开始按斖?笔?则得1,否则得0 數脑?蚓涂梢缘玫?100。表示如下∶

1110

1111 and

1100

我们知道二进制的1100是等于十进制的12,因此可以得出12 And 15 =12的结论

接下来看一看or运算吧。意识当然是《或者》啦。两个数相对应的位中,只要有一个是1就能得到1,所谓∶这个等于1 或者那个等于1,反正有一个是1就得1,否则得0。 再看你的一个例子∶

12 Or 15 =15 按照前述类似的步骤可得∶

12=1100 15=1111

然后按照刚才讲的规则,可做如下的计算∶

1100

1111 or

1111

我们知道二进制的1111是等于十进制的15,因此可以得出12 or 15 =15的结论

or 操作一般用于位设定。比如说 (现在我是举例,并不一定这样) ,某一个数值代表一个窗体的样式,我们一般称之为样式位数据。第一位=1 则表示窗体有标题栏,第2位等于1则表明窗体有滚动条。其他各位也表示其他的信息,但现在我们只讨论这两个位。现在,我们要使窗体不但具有标题栏,而且还想让它具有滚动条, 一般采取的办法是,先用某特定函数获得这个窗体的样式位数据(在WIN32中,实际的窗体样式位数据经常是Long类型的数据),比如我们得到了一个样式位数据MYWINDOWSTYLE=12,当然等于是1100(二进制)。看它的第一位和第二位都是零,说明当前窗体确实没有标题栏和滚动条。要让窗体具有标题栏和滚动条,我们需要做到使其第一位和第二位都变成1。

为此需要两个常数。当然这个常数自然是由API文本游览器提供的(这里是作为例,文本游览器中根本没有与此一致的内容) Const WS_CAPTION=1 Const WS_VSCROLL=2

(注∶1=0001,2=0010)

现在我们可以通过or操作进行具体的位设定了。如下∶

MYWINDOWSTYLE = MYWINDOWSTYLE or WS_CAPTION or WS_CAPTION 其结果将等于MYWINDOWSTYLE =15,即1111。(1100 or 0010 or 0001 =1111) 最后,我们把这个数据,通过某一函数再传送到实际的窗体处理函数当中,窗体的样式就变化了,变成一个具有标题栏和滚动条的窗体。 这样,or 就变得有点象"同时"的意识了,如∶具有标题栏的 同时 又具有滚动条。但实际运算中仍然是表现为 或者 。千万不要混淆啦。

Not 运算符就有点怪。如果凭空猜测的话很可能猜测成1则得0,0则得1。那么究竟是不是这样呢?其实是这样。但以下的事实会使一些朋友惊奇∶ 即Not 12= -13 因为,按上述的规则得出结论的话Not 12,即Not 1100似乎应当等于是0011, 即3,可结果竟是 -13。那么该如何解释这一事实呢? 其实,通常我们在数字的前面冠以?敾驍-敺?爬幢硎臼?恼?海??诩扑慊?惺怯脭0 敽蛿1?0表示正,1表示负)来表示的。如果按8位来考虑问题的话,12应当是00001100。那么通过Not运算以后,它应当是11110011了。

那么11110011又应该如何解释呢?请看,左边第一个1算是说明符号敚瓟吧,不过剩余的7位,即1110011也并不等于13呀?应当是等于115。 -115 ? 这是怎么一回事? 原来呢,这里牵涉到补码计算问题。补码的计算规则是这样的。 如果第一位(高段)为0,表示正数,用其他各位来表示数值部分;但如果第一位为1,表示负数,数值部分可用以下算法来获得∶ 设数据X的数据位总数为n,各位依次表示为 x1,x2...xn,那么 X补=2^(n-1)-0x2...xn (注∶这是整数补码定义,小数补码定义有所不同,读者可以自己翻阅有关材料)

也就是说11110011=-(2^7-01110011)=-(10000000-01110011)=-(00001101)=-13 这么麻烦?不用担心,有个很好的演算规则,这就是∶在原有的数据上加1后把符号取反。所以这也就很好理解了。 VB中TURE=-1的。Not(-1) = 0 , Not 0 = -1 ,您已经能够理解这一点。也因此Not TURE=FLASE,Not FLASE=TURE。但在IF判断语句中,您应当小心使用NOT运算符。 If 语句会把 所有不等于0的数据看成是TURE(条件成立)。有必要忠告您,如果您不能断定某一个数据n (不是逻辑型数据)肯定是-1或0两个数之间的一个数据(但我仍然不推荐这种余地),千万别用Not运算符号(这是我推荐的)。比如n=1的时候,If 1 then也好, If Not -1 then也好,都是一个样,都是满足If语句的条件成立。很多API函数是返回1的。而且API函数中不大能看到逻辑型数据,反正到目前为止我还没有看到。所以If Not n Then这样一个语句应当写成If n<>0 then 或者If n=0 then的形式来表示。尤其是在API,请记住在API千万要这样。

现在回过头来想一想And和Or运算。请问,在那里不存在补码运算吗?也是存在的。只是我们还没有讨论有负数的情况。您可以自己研究看看,很容易就能明白到的。 好了继续来,让我们继续讨论我们的Not。当然以下的算式对您来说已经不是什么加一个 ? 号的事情了。

Not 12= -1×(12+1)= -13

Not 13= -1×(-13+1)= 12 现在,请您不要想别的事情,不要老想那位白天在大街上看到的长得不怎么样的小姑娘,来记住这样一个要点∶Not运算符同And运算符相结合常用于 清除位 位操作运算。 比如∶ x%=x% and (not &h0001%) 作用是设置数据x%(x% 指的是Integer数据类型的数据x)的第1位为0。

再给出一个例子。以下运算用于清除位9 x%=x% and (not &h0200%) 为什么呢。因为,&h0200%=0000000100000000 ,呵呵,不用我再多一嘴吧? 那么为什么会有这种结果呢。对此我不想在这里深入了。因为,在上面,我都已经讲了,只要你按照这些规则认真思考一下,或者算一算就可以弄清楚其中的奥秘了。 位操作中还有其他一些运算符,比如xor等,感兴趣的朋友,就自己那个什么好了。另外,今后您可能遇到敼庹ぴ怂銛这样的名词,不是别的,就是位运算。 好了。接下来,一边回顾上述内容,一边来看看实际的应用程序吧。以下给出的程序代码是教程三中的一个附带应用程序的代码内容。这个程序的功能是,使单选框控件的那个小圆空来回设置到靠左和靠右。大概,您已经想起来了吧。

Option Explicit

Private Declare Function GetWindowLong& Lib "user32" Alias "GetWindowLongA" (ByVal hWnd As Long, ByVal nIndex As Long) Private Declare Function SetWindowLong& Lib "user32" Alias "SetWindowLongA" (ByVal hWnd As Long, ByVal nIndex As Long, ByVal dwNewLong As Long)

Private Const BS_LEFTTEXT& = &H20&

Private Sub Command1_Click(Index As Integer) Dim f&, dl& f& = GetWindowLong(Option1.hWnd, GWL_STYLE) Debug.Print f& '这里设一个

If Index = 0 Then f& = f& Or BS_LEFTTEXT Else f& = f& And Not BS_LEFTTEXT End If Debug.Print f& '这里,再设一个 dl& = SetWindowLong(Option1.hWnd, GWL_STYLE, f&) Option1.Refresh End Sub

其实很简单,f& = GetWindowLong(Option1.hWnd, GWL_STYLE) 用来获取样式位数据 f& 。 然后,结合上面讲的内容,首先是∶ f& = f& Or BS_LEFTTEXT 的作用是设置数据f&的第6位为1。 这是因为&h20&=0000000000000000000000000100000 (注∶后面的& 说名此数据是长整型数据,共32位)

其次就是上面所说的《Not运算符同And运算符相结合 常用于 清除位》这句话的具体应用了,即把f& 的第6位恢复为0。 f& = f& And Not BS_LEFTTEXT 可以看到Not运算符的优先级比And大(先计算Not)。最后,把改动的样式位数据回放到控制中∶ dl& = SetWindowLong(Option1.hWnd, GWL_STYLE, f&)

为了便于观察,在上述的代码中,我特别加了两行 Debug.Print f& 调式代码 。我想,您应当知道他们会起什么作用 程序运行结果如下(两个Command1各被点击了一下,Index =0和Index=1)∶

1409359876 1409359908 1409359908 1409359876

可以看出,第一次点击过程中(Index=0),数据从1409359876 变成 1409359908 ,然后在第二次点击过程中(Index=1),从 1409359908恢复到了 1409359876 现在可以比较一下这两个数据(可以用WINDOWS提供的科学型计算器来转换看看∶10进制到2进制),如下∶

1409359876 = 1010100000000010010000000000100 1409359908 = 1010100000000010010000000100100 是不是所谓水落石出 ?

二、创建GDI绘图对象

今天我们要讨论的是Win32 API中最有有趣的部分───用绘图函数完成图形输出。可以说,所有前面讲的内容都是本课程的前期准备。当时,我们在一些试例程序中偶尔用了一些绘图函数,可能当时您有些不太好理解。没有关系,只要您已经来到了这里,并且对前期的各内容有一点点的蒙胧记忆,那已经是足够了。因为,前期的各内容,必须与本课堂中内容相结合才能形成一个完整的理解。看完了本期教程以后,再回头看过去的几个教程,对您来说问题会变得更加清晰,透明。

我们已经讲过,Windows中的绘图是在设备场景中进行的。设备场景有两种,一是关联设备场景,之所以说它是关联设备场景,只是因为设备场景是同某一窗口关联在一起的。关联的意识就是,只要你在这个设备场景中绘图,那么绘图内容自动反映(显现)在窗体中,使得您能够看到。那么另一种设备场景是不关联的设备场景啦。当然,正如你已经猜到的那样,这种设备场景,就算你在其中绘了图,它也不会显示在窗口中的。 那么,这两种设备场景在其内部结构上有什么区别呢?其实没有区别,大概同已婚和未婚的关系一样,一个有老婆,一个还没有老婆,只不过就是这样。

既然,与窗口不关联的设备场景中的绘图内容是看不到的,那么它又有什么用途呢?嗨,别小看它,很多图象是需要在这种设备场景中加工的,目的是为了让您看不到图形的加工过程。等到图形加工完了以后,可以一次性地把完成的图象传送到其他已经与窗口关联的设备场景,让它显示出来。当然效果是更好的。 那么,在真正的绘图以前,我们必须学会如何进行选笔,配色。是不是?绘图总得去选择一个笔或者画刷吧,而且得考虑用什么颜色来绘图。 画笔和画刷是最常用的GDI绘图对象。其中,画笔是定义如何画线的GDI对象。WIN32的标准画笔具有三个属性,分别是颜色、宽度和线型。画笔颜色用来定义线条的RGB颜色,实际使用的颜色与设备有关。而GDI能自动选择与设备最接近的颜色。宽度属性的单位是逻辑单位。标准画笔可画出的线型有∶实线、不可见线和几种虚线、点线。注意,只有实线与不可见线的宽度能大于1。WIN32还提供了扩展画笔,准备以后接触的时候再讲。

刷子的用途是填充区域。它定义一块小区域(一般是8×8像素),然后和WINDOWS95的桌面背景图案平埔操作一样,把这个小块中定义的图案复制到整个填充区域中。 刷子主要有三种类型。其一是,实体刷。块图为单一色的固定颜色,可用RGB来确定颜色。其二是,图样刷。这时块图就是一个用户指定的小位图,当然不能大于8×8像素点。最后一种是阴影刷。说是阴影刷,实际上是由一些各种类型的交叉的网格线来构成。这些究竟采用哪种网格线,就得由一些BS_为前缀的参数来指定。 那么如何去获得这些画笔或花刷呢?可以采用以下列出的API函数。(有关其函数说明均可在APIBROW中找到)

函 数 说 明 CreateBrushIndirect 在一个LOGBRUSH数据结构的基础上创建一个刷子 CreateDIBPatternBrush 用一幅与设备无关的位图创建一个刷子,以便指定刷子样式(图案) CreateDIBPatternBrushPt 用一幅与设备无关的位图创建一个刷子,以便指定刷子样式(图案) CreateHatchBrush 创建带有阴影图案的一个刷子(阴影图案见注解) CreatePatternBrush 用指定了刷子图案的一幅位图创建一个刷子 CreatePen 用指定的样式、宽度和颜色创建一个画笔 CreatePenIndirect 根据指定的LOGPEN结构创建一个画笔 CreateSolidBrush 用纯色创建一个刷子 ExtCreatePen 创建一个扩展画笔(装饰或几何) GetStockObject 取得一个固有对象(Stock)。这是可由任何应用程序使用的windows标准对象之一

例1∶创建一个红色实线画笔,画笔宽度为3个像素点 Dim NewPen As Long Private Const PS_SOLID = 0 NewPen&=CreatePen (PS_SOLID,3,RGB(255,0,0)) 注∶其中PS_SOLID常数代表实线 例2∶创建阴影刷子 LOGBRUSH结构的定义如下∶ Private Type LOGBRUSH lbStyle As Long lbColor As Long lbHatch As Long End Type 以下代码餍了创建一个 刷子样式为 阴影(BS_HATCHED) ,阴影类型为十字交叉(HS_CROSS)的红色画笔。 Dim BrushInfo As LOGBRUSH BrushInfo.lbStyle = BS_HATCHED

BrushInfo.lbColor = RGB(255,0,0) BrushInfo.lbHatch = HS_CROSS NewBrush = CreateBrushIndirect(BrushInfo)

例3∶用纯色创建刷子(这个例子中是红色) NewBrush =CreateSolidBrush(vbRed)

同样,用其他几个函数,按照其用法可以创建相应的GDI绘图对象。现在,您大概了解有以上这些函数和,理解给出的几个例子就可以了。稍后,我们结合实际例子,更深入地探讨这些函数的用法。

TA的精华主题

TA的得分主题

 楼主| 发表于 2005-2-11 00:31 | 显示全部楼层
[广告] Excel易用宝 - 提升Excel的操作效率 · Excel / WPS表格插件       ★免费下载 ★       ★ 使用帮助

三、拿起和放下画笔(GDI对象)

现在我感觉好象向一群绘画系的学生讲课,尽管自己不怎么会绘画。首先是练基本功,怎样拿起画笔和放下画笔,这可能是绘画专业学生首先要学习的吧?当然,更广义地讲应当是怎样选择和删除GDI绘图对象。 在通过前述方法来创建一个GDI对象句柄(上例中的NewBrush,NewPen等)以后,为了使用它们,我们必须用SelectObjecth API函数把它们选入相应的设备场景。一个设备场景在某一时刻、在每一种类型中只能拥有一个对象,如一个画笔和一个刷子一个位图等。 SelectObjecth函数的用法非常简单,需要记住的是,此被调用后,如果成功将返回旧的对象句柄。你需要把它保存起来。当然,这一过程只需要把返回值附值于某一Long型变量就可以了。如∶

OldPen&=SelectObject(Picture1.hDC , NewPen&)

接下来该做什么呢?对对,这位同学说的对∶绘图。该怎么绘图呢?不,不,不要着急,这个问题,我们留在下一节中讨论,现在你只需记住,这里可以画些圆呀、矩形呀、添充多边型呀的操作。那么,绘图操作结束以后该怎么办呢?答案是∶应当把旧的绘图对象回设到设备场景中去。如下∶

SelectObject Picture1.hDC,OldPen&

这样,设备场景将恢复到我们为其选入绘图对象以前的状态。因为,我们不能断定其他绘图函数会使用什么样的绘图对象。因此把原来的绘图对象放回去乃是一个上策。但,如果你接着要为设备场景选择另一个对象,这个步骤可以留在后面进行。那么在这次的选入过程中就没有必要保存旧的对象句柄了,这是因为SelectObject函数返回的旧对象的句柄就是刚才我们为其选择的句柄。 绘图也完了,设备场景也恢复了原始状况,那么就操作告一段落了吗?不,还有一点。您最好把您自己创建的GDI对象删除掉,释放掉刚才使用过的资源。操作如下∶

DeleteObject NewPen 又如∶ DeleteObject NewBrush 其实,您不删除这些对象资源,应用程序退出时会自动释放的。这是因为在Win32中,资源为每个应用程序私有的。由于这种原因,应用程序之间也不能共享一个GDI对象。但是,删除你所创建的GDI对象仍是一个好的编程习惯。既然不用,留着它做什么呢,何必占用资源空间呢? 外,您千万千万切记,切记,千万∶不要删除已经选入设备场景的系统GDI对象。 还有一点,GetStockObject函数返回的对象是系统对象,请不要用DeleteObject函数删除它,否则会出现非常非常可怕的事情───你的硬盘将被永远用不了啦。@@~ 呵呵,吓唬你一把,其实没那么严重。不过,我想您大概不是明知整了坏,偏向坏里整的人吧? 如果是的话,随便整好了。

OK,以下展示了使用GDI对象的API函数。

函 数 说 明 DeleteObject 用这个函数删除GDI对象,比如画笔、刷子、字体、位图、区域以及调色板等等。对象使用的所有系统资源都会被释放 EnumObjects 枚举可随同指定设备场景使用的画笔和刷子 GetCurrentObject 用于获得指定类型的当前选定对象 GetObjectAPI 取得对指定对象进行说明的一个结构。windows手册建议用GetObject 这个名字来引用该函数。GetObjectAPI在vb中用于避免与GetObject关键字混淆 GetObjectType 判断由指定句柄引用的GDI对象的类型 SelectObject 每个设备场景都可能有选入其中的图形对象。其中包括位图、刷子、字体、画笔以及区域等等。一次选入设备场景的只能有一个对象。选定的对象会在设备场景的绘图操作中使用。例如,当前选定的画笔决定了在设备场景中描绘的线段颜色及样式 除了DeleteObject和SelectObject以外的其他函数用于从系统或指定设备场景中获取有关GDI对象的信息,一般不十分常用。 这样,假如我们要用画笔和刷子来做一些绘图朝着的话,编写代码的大概步骤是这样的。 Dim NewPen As Long Dim NewBrush As Long Dim OldPen As Long Dim OldBrush As Long

NewPen& = CreatePen(PS_SOLID, 3, RGB(255, 0, 0)) * 创建画笔 NewBrush& = CreateSolidBrush(vbRed) * 创建画刷 OldPen& = SelectObject(Picture1.hDC,NewPen&) '添加绘图操作代码

OldBrush& = SelectObject(Picture1.hDC,NewBrush) '添加填充操作代码 SelectObject Picture1.hDC,OldPen& SelectObject Picture1.hDC,OldBrush& DeleteObject NewPen& DeleteObject NewBrush& 注意一点,要用API绘图函数,并非一定要创建画笔和刷子。完全可以使用现有的GDI对象,直接调用函数来绘图。记住,设备场景中总有个默认的画笔和刷子,问题是它符不符合您的要求了。但我觉得,在绘图之前选择画笔是个好习惯。有些VB功能也可以结合使用,比如你想用红色画笔,你可以设置Forcolor属性为红色、想加宽画笔的宽度,可以设置DrawWidth属性等。

四、绘图属性与绘图函数

到目前为止,我们已经学会了绘图所需要的一切准备工作。上一节中最后给出的代码就说明这一点。代码中,现在只缺少具体的绘图代码。本节就讨论关于如何绘图的问题。 在接触绘图函数之前,首先需要了解绘图属性。设备场景定义了一系列绘图属性。这些绘图属性定义了刷子和画笔与窗口或设备表面当前内容相互作用的方法。比如,当前画笔的位置、当前背景颜色、圆弧和矩形的绘制方向、光栅操作模式等等。虽然后面给出了很多属性控制函数,但用VB自身的函数和方法属性,更容易实现。比如,设置背景模式,只要设置控件的BackColor属性就可以很轻松、带愉快地完成。但是,如果是要在一个不与窗口关联的自建设备场景中绘图的话,想必依靠这些函数是不可逃避的。

线光栅操作∶我们已经知道,光栅操作是一种位操作。通常你想用画笔进行绘图时,都假定画笔色彩只是简单的绘制到显示器或设备上。实际上,WINDOWS支持16种不同的线绘图模式,它们定义了一条线如何与显示器上已有的信息组合。这些模式就叫做线光栅操作(有时叫ROP2模式)。并且它们被作为绘图模式引入到了VisualBasic。ROP2光栅操作相当于设置VB的DrawMode属性。 背景模式∶阴影刷子、虚线画笔和文本都有一个背景。对于阴影刷,它是指阴影线之间的区域,对于虚线画笔,则指点和虚线之间的区域。而对于文本,它是指每个字符单元的背景。背景模式决定了WINDOWS如何处理这些背景区。它可以是不透明的,也可以是透明的。若是不透明的,则背景区设置为背景色;否则如果是透明的,则背景区域保持原状。

当前位置∶在VB中,要画一条直线其实非常简单,采用Line方法就可以,而且能够在一个语句中表达完成。如Line (5,5)-(10,10) 但在API中并不这样简单了(但也不是太麻烦)。要画直线,需要首先设定直线的起点。一般用MoveToEx函数来完成。然后在下一行代码中绘制直线,如LineTo 10,10。MoveToEx函数是经常使用的函数之一,用来确定绘图前的起始位置。。

绘图属性控制函数

函 数 说 明 GetArcDirection 画圆弧的时候,判断当前采用的绘图方向 GetBkColor 取得指定设备场景当前的背景颜色 GetBkMode 针对指定的设备场景,取得当前的背景填充模式 GetCurrentPositionEx 在指定的设备场景中取得当前的画笔位置 GetMiterLimit 取得设备场景的斜率限制(Miter)设置——斜率限制是指斜角长度与线宽间的比率 GetNearestColor 根据设备的显示能力,取得与指定颜色最接近的一种纯色 GetPolyFillMode 针对指定的设备场景,获得多边形填充模式。 GetROP2 针对指定的设备场景,取得当前的绘图模式。这样可定义绘图操作如何与正在显示的图象合并起来 MoveToEx 为指定的设备场景指定一个新的当前画笔位置。 SetArcDirection 设置圆弧的描绘方向 SetBkColor 为指定的设备场景设置背景颜色。背景颜色用于填充阴影刷子、虚线画笔以及字符(如背景模式为OPAQUE)中的空隙。也在位图颜色转换期间使用。 SetBkMode 指定阴影刷子、虚线画笔以及字符中的空隙的填充方式 SetMiterLimit 设置设备场景当前的斜率限制 SetPolyFillMode 设置多边形的填充模式。 SetROP2 设置指定设备场景的绘图模式。与vb的DrawMode属性完全一致。

同VisualBasic相比较,API提供了功能更强大的绘图函数。大部分绘图函数的用法都非常简单明了,只要按其说明使用就可以,觉得没有必要我多加说明。

WindoesAPI绘图函数

函 数 说 明 AngleArc 用一个连接弧画一条线,参考注解 Arc 画一个圆弧 ArcTo 画一个圆弧,并更新当前位置 CancelDC 取消另一个线程里的长时间绘图操作 Chord 画一条弦线(椭圆的平分线) Ellipse 描绘一个椭圆,由指定的矩形围绕。椭圆用当前选择的画笔描绘,并用当前选择的刷子填充 FillRect 用指定的刷子填充一个矩形 FloodFill 用当前选定的刷子在指定的设备场景中填充一个区域。区域是由颜色crColor定义的 FrameRect 用指定的刷子围绕一个矩形画一个边框(组成一个帧),边框的宽度是一个逻辑单位 GetPixel 在指定的设备场景中取得一个指定像素的当前RGB值 InvertRect 通过反转每个像素的值,从而反转一个设备场景中指定的矩形 LineDDA 枚举指定线段中的所有点 Pie 画一个扇形 PolyBezier 绘一条或多条贝塞尔(Bezier)曲线。 PolyBezierTo 绘一条或多条贝塞尔(Bezier)曲线,并将当前画笔位置设为前一条曲线的终点 PolyDraw 描绘一条复杂的曲线,由线段及贝塞尔曲线组成 Polygon 描绘一个多边形,由两点或三点的任意系列构成。windows会将最后一个点与第一个点连接起来,从而封闭多边形。多边形的边框用当前选定的画笔描绘,多边形用当前选定的刷子填充 Polyline 用当前画笔描绘一系列线段。使用PolylineTo函数时,当前位置会设为最后一条线段的终点。它不会由Polyline函数改动 PolylineTo 同上,并设置当前画笔位置用当前选定画笔描绘两个或多个多边形。根据由SetPolyFillMode函数指定的多边形填充模式,用当前选定的刷子填充它们。每个多边形都必须是封闭的 PolyPolygon 用当前选定画笔描绘两个或多个多边形。根据由SetPolyFillMode函数指定的多边形填充模式,用当前选定的刷子填充它们。每个多边形都必须是封闭的 PolyPolyline 用当前选定画笔描绘两个或多个多边形 Rectangle 用当前选定的画笔描绘矩形,并用当前选定的刷子进行填充 RoundRect 用当前选定的画笔画一个圆角矩形,并用当前选定的刷子在其中填充。X3和Y3定义了用于生成圆角的椭圆 SetPixel 在指定的设备场景中设置一个像素的RGB值,并返回该点的颜色 SetPixelV 在指定的设备场景中设置一个像素的RGB值

我把上表中大部分的函数的用法例举到了本教程附带的program1.vbp中。另外, Bezier曲线的用法比较有趣。如果你用过3D Studio三维动画制作软件就知道,其中的很多绘图工作,尤其是二维平面绘图,就是采用Bezier曲线技术。本教程附带的program3.vbp 程序简单展示了这种技术的应用。《前线》网站源码解析中的第24号(滤波器演示程序)、第26号(如何用指定颜色填充不规则封闭线框区域)等程序,也是这里部分函数的好例程,可以下载看看。

Windows还提供了一些更特殊的绘图函数,你可以在Windows的内部用它们来绘制控件外框、标题栏、3D控件和桌面等系统对象。

Win32 API其他绘图函数

函 数 说 明 DrawEdge 用指定的样式(包括3D效果)描绘一个矩形的边框 DrawEscape 换码(Escape)函数将数据直接发至显示设备驱动程序(在vb里使用:能够使用。但由于Escape对设备有较强的依赖性,所以除非万不得以,尽量不要用它) DrawFocusRect 画一个焦点矩形。这个矩形是在标志焦点的样式中通过异或运算完成的(焦点通常用一个点线表示)。如用同样的参数再次调用这个函数,就表示删除焦点矩形 DrawFrameControl 这个函数用于描绘一个标准控件。例如,可描绘一个按钮或滚动条的帧 DrawState 这个函数可为一幅图象或绘图操作应用各式各样的效果 GdiFlush 在绘图操作前注意队列。 执行任何未决的绘图操作。注释 GdiGetBatchLimit 判断有多少个GDI绘图命令位于队列中 GdiSetBatchLimit 指定有多少个GDI绘图命令能够进入队列 PaintDesktop 在指定的设备场景中描绘桌面墙纸图案

这里有几个函数很有趣,比如DrawEdge、DrawFrameControl。使用他们可以非常轻松地绘出按钮控件、编辑框控件等的外观。我已经把常用的函数的用法包含到了附带程序program2.vbp。

五、路 径

应当说,路径是较为高级的话题,尽管它不是难于理解的。我学到的有关路径的知识,来自于Dan的《Visual Basic 5.0 WIN32开发人员指南》一书中的不到两页的内容中,在其他的书中尚未看到。 糟糕的是路径没有句柄,所以说它不是GDI对象的成员。不过,千万要记住一点,任何一个设备场景只有一个路径。从这一点来看,就算为路径设置了句柄也是多余的。从我的感觉来看,路径像是在一个设备场景中绘出的任意形状的多边形区域(尽管它不是区域)。

我在路径中体验出的一个好处就是,创建一个路径后,可以把它转换为区域。这一点可以用PathToRegion函数来完成。一旦这一步成功了就好办了,得到区域句柄以后就可以和其他区域对象一样处理了。总而言之,通过路径我们可以很轻松地创建复杂的图形区域。 创建一个路径非常简单。具体形式如下∶

dl& = BeginPath&(Out.hdc) (绘图) dl& = EndPath&(Out.hdc) 在(绘图)的位置上编写代码来绘出什么图形,就能形成什么样的路径了。不过,并非任何绘图函数都可以产生路径的。可以用来产生路径的函数如下所列。

函数 Windows NT Windows 95 AngleArc Yes No Arc Yes No ArcTo Yes No Chord Yes No Ellipse Yes No ExtTextOut Yes Yes LineTo Yes Yes MoveToEx Yes Yes Pie Yes No PolyBezier Yes Yes PolyBezierTo Yes Yes PolyDraw Yes No

Polygon Yes Yes Polyline Yes Yes PolylineTo Yes Yes PolyPolygon Yes Yes PolyPolyline Yes Yes Rectangle Yes No RoundRect Yes No TextOut Yes Yes

看完这个表,我想Windows95的用户就可能有点心痛∶这么多函数用不了!嗨,我也没办法,只好责怪微软了。以下是有关路径的API函数∶

API 路径函数

函 数 说 明 AbortPath 抛弃选入指定设备场景中的所有路径。也取消目前正在进行的任何路径的创建工作 BeginPath 启动一个路径分支。在这个命令后执行的GDI绘图命令会自动成为路径的一部分。对线段的连接会结合到一起。设备场景中任何现成的路径都会被清除。参考下表,其中列出的函数都可记录到路径中 CloseFigure 描绘到一个路径时,关闭当前打开的图形(将当前路径段转为闭图) EndPath 停止定义一个路径。如执行成功,BeginPath函数调用和这个函数之间发生的所有绘图操作都会正式成为指定设备场景的路径 FillPath 关闭路径中任何打开的图形,并用当前刷子填充 FlattenPath 将一个路径中的所有曲线都转换成线段 GetPath 取得对当前路径进行定义的一系列数据 PathToRegion 将当前选定的路径转换到一个区域里 SelectClipPath 将设备场景当前的路径合并到剪切区域里 StrokeAndFillPath 针对指定的设备场景,关闭路径上打开的所有区域。用当前画笔描绘路径的一个轮廓,并用当前刷子填充路径 StrokePath 用当前画笔描绘一个路径的轮廓。打开的图形不会被这个函数关闭 好了,其实也没啥,很简单的,请阅读program4.vbp演示程序吧,你会触目惊心!哇!这就是路径?!

课后练习

A、制作画笔和画刷观察器。

提示∶

①创建一个图片框控件,用当前画笔来画一个矩形(使用Rectangle,巨型的内部将自动被当前刷子填充)。我们要准备依此来观察当前画笔和当前刷子。

②设置5个滚动条,用来分别调整画笔的颜色,画笔的宽度,画笔的样式(实线、虚线实体画刷颜色,阴影画刷样式。画笔的创建可以用函数CreatePen,实体画刷的创建可以用函数CreateSolidBrush,阴影画刷的创建可以用函数CreateHatchBrush。(图样刷子的创建留在下一课再讨论,暂时可以不练)

通过以上思路,当某个滚动条移动的时候,图片框中的图象相应地进行变化,如宽度加厚, 颜色变暗等。

B、创建一个类模块来模仿Check控件的最基本功能。

提示∶

①需要添加的属性有Wight,Height,Value属性

②需要添加的事件有Click事件。可以用Timer控件来作事件源。

③控件外观可以用DrawFrameControl函数来绘制。

说真的,还真的想不起来好例子。如果学完了位图就好了。请等待,下期就是位图。

TA的精华主题

TA的得分主题

 楼主| 发表于 2005-2-11 00:34 | 显示全部楼层
[广告] Excel易用宝 - 提升Excel的操作效率 · Excel / WPS表格插件       ★免费下载 ★       ★ 使用帮助
第七课∶位图

一、概述

在Windows中每屏是一个图形图像,灵巧的Windows制作系统,面对庞大的图形编程任务,建立了为绘画多彩的边界、按钮、图标、字体的函数库。当然啦,通过Windows API,这些函数都是可调用的。所谓Windows显示屏幕以及数量众多的打印机其实都是属于“光栅设备”。在光栅设备中,一幅图象由多条扫描线以及能访问的单独像素构成。Windows也支持非光栅设备,比如绘图仪等,本人对此一无经验,无从谈起,想来也差不到哪儿去。以下只以显示器为重点展开讨论。

计算机视频系统的核心是内存。该内存包含代表着显示图案的数据,而这些图案显示在监视器(显示器)上。每次鼠标移动时,内存中的少量数据发生变化。然后你会看到鼠标指针在屏幕上移动。每次以及每一个图形操作都会影响视频内存,因为GDI执行计算并以相应方式更改视频内存。计算机中还有一个存放图象数据的内存,叫做位图内存。位图内存与视频内存的重要的一个区别是∶位图内存看不到,而视频内存则能够看到。也就是说放在位图内存的图象数据并不反映在屏幕上,而视频内存中存放的图象信息则反映在监视器上。如果把驻留在位图内存的数据移动到视频内存中,那么图象将显示在监视器上。以一个桌面图标为例,图标从磁盘加载到内存中(位图内存)中,然后内存被移到视频内存中的适当地址上,这样当视频内存通过视频硬件被显示到屏幕上时,图标成为可见的。

从图象的种类来讲,Windows中存在两种位图,一种叫与设备有关位图(或叫设备相关位图),另一种叫与设备无关位(或叫设备无关位图,或DIB)。除非特别声明,W indows中的位图都是与设备有关的位图。有些朋友一说位图,就可能想到BMP图象,但它是属于与设备无关的位图。在编程的领域我们常常说位图,一般并不是指BMP图象,而是指对他们来说也是想象中存在的那个叫与设备有关的位图。另一个区别与设备有关位图和与设备无关位图的重要依据是,判断该位图是否具有句柄。具有句柄的位图便是与设备有关的位图,因为它是GDI对象之一。常见的BMP图象则属于与设备无关的位图。我们一般不叫它位图,而是叫做“BMP图象”或者叫做“DIB”,它是一种数据的组织方式,并非GDI绘图对象。 可以把与设备无关位图理解为对与设备有关位图数据的一种标准格式的数据保存方式。比如我们平常看到的附加名为BMP的文件。这种位图文件会在文件头上放置文件的组织信息,形式上讲和一些数据库文件的文件头起着同样的作用━━描述文件的结构。文件头后面紧跟着的是图象的颜色数据。由于这种机制的存在,使得与设备无关的位图可以在各种设备之间进行读写。

与设备无关的位图只有一种格式,而与设备有关的位图则可能...大概有多少种设备就有多少种格式吧?鬼才知道!幸运的是您根本没有必要了解它的格式,只需简单理解为保存图象数据的内存块就可以了。就好像您跟本不用了解我长得什么模样,也照样能和我打交道一样。

PC的视频系统显示一个像素的矩阵(整齐排列的小光点)。在一个基于字符的环境中,视频系统包含一套"硬件编程"的形状,代表标准字符的位置及各种画图字符(直线、角线、实线等等)和几个符号(笑脸、钻石、铲等)。通过发送给系统一个ASCII码或ANSI字符码来显示一个字符。而在Windows这样一个图形显示系统中,计算机及其软件定义出现在屏幕上的图形形状。这些形状是用位图来代表的。用这种方法的最主要的优点是图象和文本能以不同的大小、字体、方式来显示。那么,准确地说,什么是位图呢?它们是数据元素的集合,这些数据元素决定在每个屏幕位置上显示什么样的颜色。在单色的(黑和白)图形图像中,每位代表一个屏幕像素,0对应黑(没有颜色),1对应白(显示色)。不久您就会看到,位图能描述彩色图像。每个像素的位数决定能在单个位图中出现的不同色彩的数目。除了每个像素一位的单色视频,还有其他三种∶每个像素4位产生16种颜色,8位产生256种颜色,24位产生16'777'216种颜色(补充:我已经亲眼看到了32位显卡)。如下表所示∶

Windows支持的彩色

每个像素的位数 颜色总数 典型设备 1 2 单色图形 4 16 标准备VGA 8 256 256色VGA 16 32'768或65'536 32K或64K色SVGA 24 16'777'216 24位真彩色设 计算机显示系统的性能决定Windows对彩色图象的处理。现在常看到的显示器是SVGA(AGP也出来了,我的就是,但资料少无从谈起)。理论上讲,SVGA系统能显示惊人的含有16'777'216种颜色数组。把能够显示16'000'000种颜色范围的系统通常称为真彩色(true color)。那么"真彩色"的真实意思是什么呢?它源于对是否一个彩色显示器能显示无限大范围的颜色的想象,图象将看起来完全逼“真”。真彩色的显示用24位来决定每个像素的颜色。24位被分成三组,每组8位,来表示红、绿、蓝三种颜色。(参考∶自然界的颜色是通过红、绿、蓝(RGB)三种颜色来组合而成的。叫做三元色。)

二、与设备无关位图(DIB)

所谓DIB是指与设备无关的位图。DIB并不是Windows的对象,它没有自己的句柄,实际上是一种数据文件。和很多数据文件相同,DIB位图文件的开头部分有它内在的数据结构描述部分。不少书做了一些示意图来说明这个问题,但我觉得还是亲眼目睹DIB文件的内部更好一些,百闻不如一睹嘛。 现在我们准备要看的BMP文件是一个256色位图,文件名叫Hua.bmp ,这个文件曾在《VB前线》的演示程序第25号中使用过。

首先让我们查看一下文件在磁盘中的大体状况∶ D:\vbplay\vbplay25\>dir

Volume in drive D is PRO Volume Serial Number is 268F-AB4B Directory of D:\vbplay\vbplay25

. <DIR> 03-27-99 20:47 . .. <DIR> 03-27-99 20:47 .. PLAY25 VBW 111 09-20-99 23:43 play25.vbw MSSCCPRJ SCC 193 04-07-99 23:52 MSSCCPRJ.SCC BITMAP CLS 12,711 06-16-99 21:27 Bitmap.cls MYMEMORY CLS 3,419 04-08-99 2:07 MyMemory.cls PLAY25 VBP 636 06-16-99 21:42 play25.vbp HUA BMP 96,446 04-04-99 12:06 hua.bmp PL25_1 FRM 3,865 06-16-99 21:25 pl25_1.frm 7 file(s) 117,381 bytes 2 dir(s) 569,049,088 bytes free

看到了吧?它的大小是96?46个字节。接下来,我们可以用DEBUG的d命令以16进制形式来显示文件的文件头信息。操作如下∶

D:\vbplay\vbplay25\>debug hua.bmp -d 100 1000 0EAE:0100 42 4D BE 78 01 00 00 00-00 00 36 04 00 00 28 00 BM.x......6...(. 0EAE:0110 00 00 69 01 00 00 06 01-00 00 01 00 08 00 00 00 ..i............. 0EAE:0120 00 00 88 74 01 00 C4 0E-00 00 C4 0E 00 00 00 00 ...t............ 0EAE:0130 00 00 00 00 00 00 00 00-00 00 80 80 80 00 00 00 ................ 0EAE:0140 80 00 00 80 80 00 00 80-00 00 80 80 00 00 80 00 ................

0EAE:0150 00 00 80 00 80 00 40 80-80 00 40 40 00 00 FF 80 ......@...@@.... 0EAE:0160 00 00 80 40 00 00 FF 00-40 00 00 40 80 00 FF FF ...@....@..@.... 省略了57行∶50 - 16 -1 = 39(16进制) = 57(10进制) 0EAE:0500 CC 00 AD E8 FD 00 78 AE-D3 00 1F 38 65 00 66 89 ......x....8e.f. 0EAE:0510 F0 00 63 9B C3 00 79 AD-DA 00 08 13 73 00 0A 1C ..c...y.....s... 0EAE:0520 A5 00 BB ED FD 00 83 C1-E5 00 2A 50 74 00 7C B3 ..........*Pt.|.

0EAE:0530 ED 00 56 82 BD 00 B3 66-B3 B3 6C B3 BD 8B C9 C9 ..V....f..l..... 0EAE:0540 BD 50 21 60 6C 6C 44 44-B3 44 6C 86 44 66 44 D9 .P!`llDD.Dl.DfD. 0EAE:0550 44 B3 6C C9 87 7B B9 7B-44 BD 87 60 87 C9 6C AB D.l..{.{D..`..l. 0EAE:0560 60 D6 6C AB AB 2A 88 55-88 7B 7B 7B 7B 3D 3D 3D `.l..*.U.{{{{=== 0EAE:0570 C5 3D C5 24 7B 7B 7B 7B-B4 B9 7B B9 B5 C6 3D 7B .=.${{{{..{...={ (以下全部省略) 0EAE:1000 B3 .

-q 注∶若使用D:\vbplay\vbplay25\>debug hua.bmp >> Temp.txt则可以把结果保存到Temp.txt文件,并可加以分析研究。但这样给出命令后什么都看不见。这时,你应当键入d 100 1000这个命令后回车,然后再按q就可以返回到DOS命令行提示符下。这段内容是这样获取的。详细内容,可参考有关DOS命令参考手册。用pctools5.0也可以。

通过上述方式获取的是该位图文件第一个字节开始的若干个字节的16进制形式的文本内容。以下,我们具体分析一下。在分析以前为了在整体上把握DIB文件的结构,有必要先给出其整体情况,如下表格所示∶

DIB文件的组成

三大部分 字节 说明 BITMAPFILEHEADER 14 用红色表示的部分 BITMAPINFO BITMAPINFOHEADER 40 用深绿色表示的部分 RGBQUAD(结构数组) 最小0,最大4×256。(在16位以上的位图中,根本不存在此部分) 用深红色表示的部分 数据部分 大于0(具体大小信息记录在BITMAPINFOHEADER结构中) 用下划线表示的部分 可以看出,DIB的文件头主要由两个部分组成,即BITMAPFILEHEADER和BITMAPINFO, 而BITMAPINFO又分为BITMAPINFOHEADER和RGBQUAD两个小部分。让我们一个一个分析。首先是开头以红色表示的14个字节,如下

42 4D BE 78 01 00 00 00-00 00 36 04 00 00 这是BITMAPFILEHEADER结构部分。BITMAPFILEHEADER结构如表所示∶

BITMAPFILEHEADER结构

结构内各字段 数据类型 说明 bfType Integer 指定文件类型,必须 BM bfSize Long 指定位图文件大小,以字节为单位 bfReserved1 Integer 保留未用,必须设为0 bfReserved2 Integer 同上 bfOffBits Long 从此结构到位图数据位的字节偏移量 此结构,主要记录了DIB文件大小以及结构有关的信息。在很多情况下,可以计算来获取这些这些信息,所以很多人并不访问这个结构内的数据。不少人说 BITMAPFILEHEADER结构记录着无大用处的信息。其中bfOffBits说明的是从此结构到位图数据位的字节偏移量。通过对此字段的访问,我们可以知道这个位图文件的颜色数据是从哪里开始的,或者可以知道DIB文件头的结构有多长。

m_BMFileHeader.bfType = 42 4D

这是一个Integer整型数据,对DIB文件来说此数据必须是42 4D。那么为什么定为42 4D呢,其实是在ASCII码表中42即十进制的66表示大写B,4D即十进制的77表示M,也就是说字符擝M數囊馐丁?梢远哉飧稣?褪?莸姆梦世磁卸衔募?欠袷荄IB(或BMP)文件。

m_BMFileHeader.bfSize = BE 78 01 00 = &H178BE = 96446个字节

说明文件的总长度。注意,右侧的数据是高字节,即右侧字节的地址比左侧字节的地址值大,所以BE 78 01 00 = &H178BE。在上面,用dir命令观察文件时,我们已经看到文件的长度正是96446个字节。如∶ HUA BMP 96,446 04-04-99 12:06 hua.bmp

m_BMFileHeader.bfReserved1 = 00 00 (系统保留,没有设置数据) m_BMFileHeader.bfReserved2 = 00 00 (系统保留,没有设置数据)

m_BMFileHeader.bfOffBits = 36 04 00 00 = &H00000436 = 1078 =14 + 40 + 256 * 4

(从文件开始处起,带下划线的数据位的以字节为单位的长度,总共1078个字节。对真彩色图象来说,这个数据经常是36 00 00 00,即54。说明文件头只有BITMAPFILEHEADER(14个字节) + BITMAPINFOHEADER(40个字节)的部分,即没有RGBQUAD部分。为什么没有RGBQUAD部分也可以?稍后你就明白怎么一回事。继续往下看,继续)

TA的精华主题

TA的得分主题

 楼主| 发表于 2005-2-11 00:34 | 显示全部楼层

与此结构紧连在一起的下一个叫BITMAPINFO的结构,其组成如下表所示∶

BITMAPINFO结构(表二)

结构内各字段 数据类型 说明 bmiHeader BITMAPINFOHEADER 一个 BITMAPINFOHEADER bmiColors RGBQUAD 一个 RGBQUAD结构组成的数组 从此表中可以看出,结构内部还存在两个结构,即BITMAPINFOHEADER和RGBQUAD。因此,从某种意义上讲,位图文件的文件头结构可以说为三个结构来构成的: BITMAPFILEHEADER 、BITMAPINFOHEADER和RGBQUAD。但,必须认清的是BITMAPINFO结构并不是离开BITMAPINFOHEADER和RGBQUAD这两个结构而独立存在的。BITMAPINFOHEADER结构的长度是固定着的,为40个字节。BITMAPINFOHEADER结构的有关说明,可参考如下表∶

BITMAPINFOHEADER结构(表三)

结构内各字段 数据类型 说明 biSize Long 结构长度(40) biWidth Long 指定位图的宽度,以像素为单位 biHeight Long 指定位图的高度,以像素为单位 biPlanes Integer 指定目标设备的级数(必须为 1 ) biBitCount Integer 每一个像素的位(1,4,8,16,24,32) biCompression Long 指定压缩类型(BI_RGB 为不压缩) biSizeImage Long 指定图象的大小,以字节为单位 biXPelsPerMeter Long 指定设备水平分辨率,以每米的像素为单位 biYPelsPerMeter Long 垂直分辨率,其他同上 biClrUsed Long 在颜色表中实际使用的色彩索引的个数,用O表示全要使用 biClrImportant Long 指定认为重要的颜色索引个数,用 0 表示所有颜色均重要 BITMAPINFOHEADER结构主要记载了数据区的大小及颜色信息。必须认清的是, biSizeImage字段说明的是图象的大小,也就是数据部分的大小,而不是文件的大小。如果我们只把位图的数据部分读入到内存(而把结果部分读入到数组),就需要按此大小来申请内存大小。为了便于对照,我把上面给出的示范数据在这里重贴一下了∶ 0EAE:0100 28 00 BM.x......6...(. 0EAE:0110 00 00 69 01 00 00 06 01-00 00 01 00 08 00 00 00 ..i.............

0EAE:0120 00 00 88 74 01 00 C4 0E-00 00 C4 0E 00 00 00 00 ...t............ 0EAE:0130 00 00 00 00 00 00 ................

分析说明如下∶

biSize = 28 00 00 00 = &H28 = 40 此结构的长度,总共40个字节。总是这个数据。

biWidth = 69 01 00 00 = &H169 = 361 图象的宽度(像素质为单位)。把此图片放到PictureBox控件,并运行如下命令∶ Text1.Text = Picture1.ScaleX(Picture1.Picture.Width, vbHimetric, vbPixels) 结果为∶360.9827。可以看出VB的计算存在一点误差。但如果你用整形变量来读取这个数据,那么就一样了。

biHeigh = 06 01 00 00 = &H106 = 262 图象的高度,请参照bitWidth .

biPlanes = 01 00 = &H1 = 1 必须为1。

biBitCount = 08 00 = &H8 = 8 每个像素的位。当前我例举的这个叫hua.bmp的位图文件是以8个位来表示一个像素的,即一个字节。一个256(2的8次方)色位图能显示的颜色总数限定在256种颜色,但这并不意味着任何256色位图都只能显示相同的256种颜色。一个256色位图所能够显示的256种颜色被规定在该位图的RGBQUAD结构中。256色位图所能显示的颜色范围,能够达到设备所允许的范围(16位增强模式中可能的颜色数为2的16次方,24位真彩模式中可能的颜色数为2 的24次方),但必须是其中的256个颜色。RGBQUAD结构所描述的颜色值用来产生调色板。在这种情况下数据区内存放的是对这256种颜色的索引值,而不是实际颜色。当用1位来表示一个像素的时候,RGBQUAD的数组个数为2,存放着黑色和白色两个颜色,而数据区存放着1和0两个索引值;当用4位来表示一个像素的时候,则RGBQUAD的数组个数为16,存放着16种颜色,数据区存放着0至15的索引号;当8位的时候存放256种颜色,数据区存放着0至255的索引号;而当像素位超过或等于16位的时候,RGBQUAD结构部分不存在,数据区存放着以16位或24位表示的实际颜色。在这种时候就不存在调色板了。其中的原因一想就知道,难道我们要设上万种的调色板项?

biCopression = 00 00 00 00 = &H0 = 0 指定压缩类型。不是说BI_RGB 为不压缩吗?让我查查,BI_RGB究竟多少? 来喽! Const BI_RGB& = 0& ,呵呵,原来这种文件叫做没有压缩。 还有∶Const BI_RLE4& = 2& 和 Const BI_RLE8& = 1& 估计没有什么用处,想压缩就采用JPG吧,何必用BMP ?

biSizeImage = 88 74 01 00 = &H17488 = 95368 图象数据的长度(大小)。从数据关系上来讲,应该是∶图象数据的长度+文件头长度=文件长度。让我们验证一下,是否是这样∶

biSizeImage + m_BMFileHeader.bfOffBits = 95368 + 1078 = 96446 = m_BMFileHeader.bfSize @@~果然如此!

biXPelsPerMeter = C4 0E 00 00 = &HEC4 = 3780

biYPelsPerMeter = C4 0E 00 00 = &HEC4 = 3780

水平分辨率和垂直分辨率。

ibClrUsed = 00 00 00 00 = &H0 = 0

颜色表(调色板)中使用的索引个数。既然是0,说明全要使用了。全就是256个,再多了不可能。你千万不要用此数据来判断RGBQUAD数组的个数。

biClrImportant = 00 00 00 00 = &H0 = 0

指定认为重要的颜色索引个数,用 0 表示所有颜色均重要。请大家多多去选用0吧,这样才能有利于团结。

与BITMAPINFOHEADER 结构不同RGBQUAD是一个数组,其长度(RGBQUAD结构长度×数组元素个数)可以是多种情况,但不能超过256个。RGBQUAD结构如表所示∶

RGBQUAD结构(表四)

结构内各字段 数据类型 说明 rgbBlue Byte 指定彩色中蓝色成分的多少 rgbGreen Byte 指定彩色中绿色成分的多少 rgbRed Byte 指定彩色中红色成分的多少 rgbReserved Byte 保留,可设为0 0EAE:0130 00 00-00 00 80 80 80 00 00 00 ................ 0EAE:0140 80 00 00 80 80 00 00 80-00 00 80 80 00 00 80 00 ................ 0EAE:0150 00 00 80 00 80 00 40 80-80 00 40 40 00 00 FF 80 ......@...@@.... 0EAE:0160 00 00 80 40 00 00 FF 00-40 00 00 40 80 00 FF FF ...@....@..@.... 以下省略(还很长)

您大概已经注意到了,我为什么用红色标示了一些 00 呢?原因是因为它们都是白痴,只占地方,不起作用,是微软公司保留的RGBQUAD结构中的rgbReserved字段。每个 00 和 00 和之间有三个字节,当然,他们分别表示RGB颜色的了。这样颜色表中索引号为0的颜色应当是等于RGB(&H0,&H0,&H0),索引号为1的颜色应当是等于RGB(&H80,&H80, &H80),索引号为2的颜色那就是等于RGB(&H0,&H0,&H80),依次类推,至到索引号为255的那个颜色。

OKay!现在已经把文件头讲得够细的了,不能再细,再细了就没戏了。接着看一下数据部分。

0EAE:0530 ED 00 56 82 BD 00 B3 66-B3 B3 6C B3 BD 8B C9 C9 ..V....f..l..... 0EAE:0540 BD 50 21 60 6C 6C 44 44-B3 44 6C 86 44 66 44 D9 .P!`llDD.Dl.DfD.

以下省略

我们可以看到第一个像素点的数据是B3,即179,说明此颜色相当与颜色表中的索引号为237的那个颜色。那么那个颜色究竟什么颜色呢?糟糕,给我省略了,没有列出来。来找一个没有省略的吧,看看有没有。好象没有。算了,就到这里吧。其实找也是那么一回事。不过我还是告诉你吧,因为你自己找的话可能找不到。原因是∶行是以逆序存放的。 也就是说,位图文件的像素数据部分中的第一行实际上是图象中的最后一行。Dan说∶除非将BITMAPINFOHEADER 结构的 biHeight 段设为负值,否则起点就位于左下角。但大多情况下biHeight并非负值,所以起点一般也就在左下角了。但这并不是说扫描行的起点等于图象的高度,起点还是等于0,终点才等于高度。也就是说扫描行的编号是从最后一行开始顺序排列为 0,1,2,3 的。

现在,我们已经完成了对一个位图文件的全面分析。如果还有需要讲的话,那么有两点。第一点是关于真彩色位图。已经在上面提到过,真彩色位图是没有颜色表的。这种图的数据部分存放的是实际的RGB颜色。那么如何知道没有颜色表呢?其实很简单,好象上面也已经讲到了,m_BMFileHeader.bfOffBits 将等于 &H36,即54。还有biBitCount 会是大于等于16位。

第二点,是一个很讨厌的规定。我们把位图中的一行数据信息叫做扫描线。Windows 规定,每一个扫描线必须结束与一个32位的边界。也就是说,一个扫描线的位长度(按位计算)必须整除于32,或字节长度必须整除于4 。也就是说如果只有8位,那么拿空白的24 位来补充,如果只有48位,那么也就拿空白的16位来补充。我们可以证实一下∶

在刚才的图片中,我们已经知道图象的宽度为361个像素点,高度为262个像素点。那么数据区的总的像素点个数为361×262=94582。本例中一个字节代表一个像素,但字节个数并不等于94582。正如已经看到的那样,是为biSizeImage = 95368。还缺少786个字节。这说明一些位给补上了。总共是262行,因此说明每行补充了786÷262=3个字节,即24位。为什么每行补充了24位呢?现在看来只有一种可能。也就是,32位等于是4个字节,因此一行361个字节除以4以后,必须剩下1,Windows才补充了不足的3 个字节。呵呵,361 MOD 4 = 1 @@~

计算DIB各行中的字节数(扫描线长度)的VB代码总结如下∶

imageWidth -->位图的宽度(像素为单位) bmBits -->每像素的位个数

Function GetLineWidth(bmBits As Integer, imageWidth As Long) As Long

GetLineWidth = imageWidth

Select Case bmBits

Case 1

GetLineWidth = (GetLineWidth + 7) / 8

Case 4

GetLineWidth = (GetLineWidth + 1) / 2

Case 8

Case 16

GetLineWidth = GetLineWidth * 2

Case 24

GetLineWidth = GetLineWidth * 3

Case 32

GetLineWidth = GetLineWidth * 4

Case Else

GetLineWidth = 0 'error

Exit Function

End Select

If (GetLineWidth And 3&) > 0 Then _

GetLineWidth = (GetLineWidth And &HFFFFFFFC) + 4

End Function

以下给出了与设备无关位图有关的函数

函 数 说 明 CreateDIBitmap 在一幅DIB的基础上创建与设备有关位图 CreateDIBSection 创建一个DIBSection对象(一种新的位图对象,和普通设备相关位图相似,但也能在内存中指定一个缓冲区,用以保存DIB格式的位图数据。具体内容已超出本讲座的范围,读者自己参考有关资料) CreateDIBColorTalbe 为DIBSection对象取得颜色表信息。 GetDIBits 在与设备无关位图里载如来自一幅与设备有关位图的数据。很不错的函数。 SetDIBColorTalbe 为DIBSection设置颜色表信息。 SetDIBits 在与设备相关位图里用来自一个DIB的数据设置图象。 SetDIBitsToDevice 将来自DIB的数据直接设到一个设备。可用它将数据从DIB直接传给屏幕或打印机。 StretchDIBits 将来自DIB的数据设到设备场景,同时根据需要伸缩图象。有些设备支持用这个函数将数据直接设到输出设备。可用GetDeviceCaps函数判断具体是否支持。常用函数之一。 好了,大家稍微休息以下,下一节我们学习与设备有关的位图。

您需要登录后才可以回帖 登录 | 免费注册

本版积分规则

关闭

最新热点上一条 /1 下一条

手机版|关于我们|联系我们|ExcelHome

GMT+8, 2024-4-23 18:28 , Processed in 0.058126 second(s), 10 queries , Gzip On, MemCache On.

Powered by Discuz! X3.4

© 1999-2023 Wooffice Inc.

沪公网安备 31011702000001号 沪ICP备11019229号-2

本论坛言论纯属发表者个人意见,任何违反国家相关法律的言论,本站将协助国家相关部门追究发言者责任!     本站特聘法律顾问:李志群律师

快速回复 返回顶部 返回列表