技术文章

CSocket 编程之阻塞和非阻塞模式
我通过几个采用 CSocket 类编写并基于 Client/Server (客户端 / 服务端)的网络聊天和传输文件的程序 ( 详见: 源代码参考 ) ,在调试这些程序的过程中,追踪深入至 CSocket 类核心源码 Sockcore.cpp , 对于CSocket 类的运行机制可谓是一览无遗,并且对于阻塞和非阻塞方式下的 socket 程序的编写也是稍有体会。

阅读本文请先注意:

  这里的阻塞和非阻塞的概念仅适用于 Server 端 socket 程序。socket 意为套接字,它与 Socket 不同,请注意首字母的大小写。
  客户端与服务端的通信简单来讲:服务端 socket 负责监听,应答,接收和发送消息,而客户端 socket 只是连接,应答,接收,发送消息。此外,如果你对于采用 CSocket 类编写 Client/Server 网络程序的原理不是很了解,请先查询一下( 详见:参考书籍和在线帮助 )。
在此之前,有必要先讲述一下: 网络传输服务提供者, ws2_32.dll , socket 事件 和 socket window 。

1、网络传输服务提供者(网络传输服务进程), Socket 事件, Socket Window

  网络传输服务提供者 ( transport service provider )是以 DLL 的形式存在的,在 windows 操作系统启动时由服务进程 svchost.exe 加载。当 socket 被创建时,调用 API 函数 Socket (在 ws2_32.dll 中), Socket 函数会传递三个参数 : 地址族,套接字类型 ( 注 2 ) 和协议,这三个参数决定了是由哪一个类型的 网络传输服务提供者 来启动网络传输服务功能。所有的网络通信正是由网络传输服务提供者完成 , 这里将 网络传输服务提供者 称为 网络传输服务进程 更有助于理解,因为前文已提到 网络传输服务提供者 是由 svchost.exe 服务进程所加载的。
  下图描述了网络应用程序、 CSocket ( WSock32.dll )、 Socket API(ws2_32.dll) 和 网络传输服务进程 之间的接口层次关系:



当 Client 端 socket 与 Server 端 socket 相互通信时,两端均会触发 socket 事件。这里仅简要说明两个 socket 事件:
  • FD_CONNECT: 连接事件 , 通常 Client 端 socket 调用 socket API 函数 Connect 时所触发,这个事件发生在 Client 端。
  • FD_ACCEPT :正在引入的连接事件,通常 Server 端 socket 正在接收来自 Client 端 socket 连接时触发,这个事件发生在 Server 端。

  网络传输服务进程 将 socket 事件 保存至 socket 的事件队列中。此外, 网络传输服务进程 还会向 socket window 发送消息 WM_SOCKET_NOTIFY , 通知有 socket 事件 产生,见下文对 socket window 的详细说明。
  调用 CSocket::Create 函数后,socket 被创建。 socket 创建过程中调用 CAsyncSocket::AttachHandle(SOCKET hSocket, CAsyncSocket* pSocket, BOOL bDead) 。该函数的作用是:

  • 将 socket 实例句柄和 socket 指针添加至 当前模块状态 ( 注 1 )的一个映射表变量 m_pmapSocketHandle 中。
  • 在 AttachHandle 过程中,会 new 一个 CSocketWnd 实例 ( 基于 CWnd 派生 ) ,这里将这个实例称之为 socket window ,进一步理解为它是存放所有 sockets 的消息池 ( window 消息),请仔细查看,这里 socket 后多加了一个 s ,表示创建的多个 socket 将共享一个 消息池 。
  • 当 Client 端 socket 与 Server 端相互通信时 , 此时 网络传输服务进程 向 socket window 发送消息 WM_SOCKET_NOTIFY ,需要说明的是 CSocketWnd 窗口句柄保存在 当前模块状态 的 m_hSocketWindow 变量中。

2、阻塞模式

  阻塞模式下 Server 端与 Client 端之间的通信处于同步状态下。在 Server 端直接实例化 CSocket 类,调用 Create 方法创建 socket ,然后调用方法 Listen 开始侦听,最后用一个 while 循环阻塞调用 Accept 函数用于等待来自 Client 端的连接,如果这个 socket 在主线程(主程序)中运行,这将导致主线程的阻塞。因此,需要创建一个新的线程以运行 socket 服务。
调试跟踪至 CSocket::Accept 函数源码:

while(!Accept(...))
{ 
     // The socket is marked as nonblocking and no connections are present to be accepted. 
	if (GetLastError() == WSAEWOULDBLOCK) 
 PumpMessage(FD_ACCEPT); else return FALSE; }
  它不断调用 CAsyncSocket::Accept ( CSocket 派生自 CAsyncSocket 类)判断 Server 端 socket 的事件队列中是否存在正在引入的连接事件 - FD_ACCEPT (见 1 ),换句话说,就是判断是否有来自 Client 端 socket 的连接请求。
  如果当前 Server 端 socket 的事件队列中存在正在引入的连接事件, Accept 返回一个非 0 值。否则, Accept 返回 0,此时调用 GetLastError 将返回错误代码 WSAEWOULDBLOCK ,表示队列中无任何连接请求。注意到在循环体内有一句代码:
PumpMessage(FD_ACCEPT); 

  PumpMessage 作为一个消息泵使得 socket window 中的消息能够维持在活动状态。实际跟踪进入 PumpMessage 中,发现这个消息泵与 Accept 函数的调用并不相关,它只是使很少的 socket window 消息(典型的是 WM_PAINT 窗口重绘消息)处于活动状态,而绝大部分的 socket window 消息被阻塞,被阻塞的消息中含有 WM_SOCKET_NOTIFY。
  很显然,如果没有来自 Client 端 socket 的连接请求, CSocket 就会不断调用 Accept 产生循环阻塞,直到有来自 Client 端 socket 的连接请求而解除阻塞。
  阻塞解除后,表示 Server 端 socket 和 Client 端 socket 已成功连接, Server 端与 Client 端彼此相互调用 Send 和 Receive 方法开始通信。

3、非阻塞模式

  在非阻塞模式下 利用 socket 事件 的消息机制, Server 端与 Client 端之间的通信处于异步状态下。
  通常需要从 CSocket 类派生一个新类,派生新类的目的是重载 socket 事件 的消息函数,然后在 socket 事件 的消息函数中添入合适的代码以完成 Client 端与 Server 端之间的通信,与阻塞模式相比,非阻塞模式无需创建一个新线程。
  这里将讨论当 Server 端 socket 事件 - FD_ACCEPT 被触发后,该事件的处理函数 OnAccept 是如何进一步被触发的。其它事件的处理函数如 OnConnect, OnReceive 等的触发方式与此类似。
在 1 中已提到 Client/Server 端通信时, Server 端 socket 正在接收来自 Client 端 socket 连接请求,这将会触发 FD_ACCEPT 事件,同时 Server 端的 网络传输服务进程 向 Server 端的 socket window (CSocketWnd )发送事件通知消息 WM_SOCKET_NOTIFY , 通知有 FD_ACCEPT 事件产生 , CsocketWnd 在收到事件通知消息后,调用消息处理函数 OnSocketNotify:

LRESULT CSocketWnd::OnSocketNotify(WPARAM wParam, LPARAM lParam) 
{ 
	CSocket::AuxQueueAdd(WM_SOCKET_NOTIFY, wParam, lParam); 
	CSocket::ProcessAuxQueue(); 
	return 0L ; 
}
  消息参数 wParam 是 socket 的句柄, lParam 是 socket 事件 。这里稍作解释一下,CSocketWnd 类是作为 CSocket 类的 友元类 ,这意味着它可以访问 CSocket 类中的保护和私有成员函数和变量, AuxQueueAdd 和 ProcessAuxQueue 是 CSocket 类的静态成员函数,如果你对友元不熟悉,请迅速找本有关 C++ 书看一下友元的使用方法吧!
ProcessAuxQueue 是实质处理 socket 事件的函数,在该函数中有这样一句代码:
CAsyncSocket* pSocket = CAsyncSocket::LookupHandle((SOCKET)wParam, TRUE); 

  其实也就是由 socket 句柄得到发送事件通知消息的 socket 指针 pSocket:从 m_pmapSocketHandle 中查找(见 1 )!
  最后, WSAGETSELECTEVENT(lParam) 会取出事件类型,在一个简单的 switch 语句中判断事件类型并调用事件处理函数。在这里,事件类型是 FD_ACCEPT ,当然就调用 pSocket->OnAccept !

结束语
  Server 端 socket 处于阻塞调用模式下,它必须在一个新创建的线程中工作,防止主线程被阻塞。
  当有多个 Client 端 socket 与 Server 端 socket 连接及通信时, Server 端采用阻塞模式就显得不适合了,应该采用非阻塞模式 , 利用 socket 事件 的消息机制来接受多个 Client 端 socket 的连接请求并进行通信。
  在非阻塞模式下,利用 CSocketWnd 作为所有 sockets 的消息池,是实现 socket 事件 的消息机制的关键技术。文中存在用词不妥和可能存在的技术问题,请大家原谅,也请批评指正,谢谢!

注:

  1. 当前模块状态——用于保存当前线程和模块状态的一个结构,可以通过 AfxGetThreadModule() 获得。AFX_MODULE_THREAD_STATE 在 CSocket 重新定义为 _AFX_SOCK_THREAD_STATE 。
  2. socket 类型——在 TCP/IP 协议中, Client/Server 网络程序采用 TCP 协议:即 socket 类型为 SOCK_STREAM ,它是可靠的连接方式。在这里不采用 UDP 协议:即 socket 类型为 SOCK_DGRAM ,它是不可靠的连接方式。

源代码参考:

  1. http://www.codeproject.com/internet/SocketFileTransfer.asp 采用 CSocket 类编写的基于 Client/Server 的网络文件传输程序,它是基于阻塞模式的 Client/Server 端网络程序典型示例。
  2. http://www.codeguru.com/Cpp/I-N/network/messaging/article.php/c5453 采用 CSocket 类编写的基于 Client/Server 的网络聊天程序,它是基于非阻塞模式的 Client/Server 端网络程序典型示例。
WinSock学习笔记
Socket( 套接字

◆先看定义:
typedef unsigned int u_int;
typedef u_int SOCKET;◆Socket相当于进行网络通信两端的插座,只要对方的Socket和自己的Socket有通信联接,双方就可以发送和接收数据了。其定义类似于文件句柄的定义。

◆Socket有五种不同的类型:

1、流式套接字(stream socket)
定义:

#define SOCK_STREAM 1 流式套接字提供了双向、有序的、无重复的以及无记录边界的数据流服务,适合处理大量数据。它是面向联结的,必须建立数据传输链路,同时还必须对传输的数据进行验证,确保数据的准确性。因此,系统开销较大。

2、 数据报套接字(datagram socket)

定义:

#define SOCK_DGRAM 2 数据报套接字也支持双向的数据流,但不保证传输数据的准确性,但保留了记录边界。由于数据报套接字是无联接的,例如广播时的联接,所以并不保证接收端是否正在侦听。数据报套接字传输效率比较高。

3、原始套接字(raw-protocol interface)

定义:

#define SOCK_RAW 3 原始套接字保存了数据包中的完整IP头,前面两种套接字只能收到用户数据。因此可以通过原始套接字对数据进行分析。
其它两种套接字不常用,这里就不介绍了。

◆Socket开发所必须需要的文件(以WinSock V2.0为例):

头文件:Winsock2.h

库文件:WS2_32.LIB

动态库:W32_32.DLL

一些重要的定义

1、数据类型的基本定义:这个大家一看就懂。

typedef unsigned char u_char;
typedef unsigned short u_short;
typedef unsigned int u_int;
typedef unsigned long u_long;2、 网络地址的数据结构,有一个老的和一个新的的,请大家留意,如果想知道为什么,
请发邮件给Bill Gate。其实就是计算机的IP地址,不过一般不用用点分开的IP地
址,当然也提供一些转换函数。

◆ 旧的网络地址结构的定义,为一个4字节的联合:

struct in_addr {
union {
struct { u_char s_b1,s_b2,s_b3,s_b4; } S_un_b;
struct { u_short s_w1,s_w2; } S_un_w;
u_long S_addr;
} S_un;
#define s_addr S_un.S_addr /* can be used for most tcp & ip code */
//下面几行省略,反正没什么用处。
};其实完全不用这么麻烦,请看下面:

◆ 新的网络地址结构的定义:
非常简单,就是一个无符号长整数 unsigned long。举个例子:IP地址为127.0.0.1的网络地址是什么呢?请看定义:

#define INADDR_LOOPBACK 0x7f0000013、 套接字地址结构

(1)、sockaddr结构:

struct sockaddr {
u_short sa_family; /* address family */
char sa_data[14]; /* up to 14 bytes of direct address */
};sa_family为网络地址类型,一般为AF_INET,表示该socket在Internet域中进行通信,该地址结构随选择的协议的不同而变化,因此一般情况下另一个与该地址结构大小相同的sockaddr_in结构更为常用,sockaddr_in结构用来标识TCP/IP协议下的地址。换句话说,这个结构是通用socket地址结构,而下面的sockaddr_in是专门针对Internet域的socket地址结构。

(2)、sockaddr_in结构

struct sockaddr_in {
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};sin _family为网络地址类型,必须设定为AF_INET。sin_port为服务端口,注意不要使用已固定的服务端口,如HTTP的端口80等。如果端口设置为0,则系统会自动分配一个唯一端口。sin_addr为一个unsigned long的IP地址。sin_zero为填充字段,纯粹用来保证结构的大小。

◆ 将常用的用点分开的IP地址转换为unsigned long类型的IP地址的函数:

unsigned long inet_addr(const char FAR * cp )用法:

unsigned long addr=inet_addr("192.1.8.84")◆ 如果将sin_addr设置为INADDR_ANY,则表示所有的IP地址,也即所有的计算机。

#define INADDR_ANY (u_long)0x000000004、 主机地址:

先看定义:

struct hostent {
char FAR * h_name; /* official name of host */
char FAR * FAR * h_aliases; /* alias list */
short h_addrtype; /* host address type */
short h_length; /* length of address */
char FAR * FAR * h_addr_list; /* list of addresses */
#define h_addr h_addr_list[0] /* address, for backward compat */
};
h_name为主机名字。
h_aliases为主机别名列表。
h_addrtype为地址类型。
h_length为地址类型。
h_addr_list为IP地址,如果该主机有多个网卡,就包括地址的列表。另外还有几个类似的结构,这里就不一一介绍了。

5、 常见TCP/IP协议的定义:

#define IPPROTO_IP 0
#define IPPROTO_ICMP 1
#define IPPROTO_IGMP 2
#define IPPROTO_TCP 6
#define IPPROTO_UDP 17
#define IPPROTO_RAW 255 具体是什么协议,大家一看就知道了。

套接字的属性

为了灵活使用套接字,我们可以对它的属性进行设定。

1、 属性内容:

//允许调试输出
#define SO_DEBUG 0x0001 /* turn on debugging info recording */
//是否监听模式
#define SO_ACCEPTCONN 0x0002 /* socket has had listen() */
//套接字与其他套接字的地址绑定
#define SO_REUSEADDR 0x0004 /* allow local address reuse */
//保持连接
#define SO_KEEPALIVE 0x0008 /* keep connections alive */
//不要路由出去
#define SO_DONTROUTE 0x0010 /* just use interface addresses */
//设置为广播
#define SO_BROADCAST 0x0020 /* permit sending of broadcast msgs */
//使用环回不通过硬件
#define SO_USELOOPBACK 0x0040 /* bypass hardware when possible */
//当前拖延值
#define SO_LINGER 0x0080 /* linger on close if data present */
//是否加入带外数据
#define SO_OOBINLINE 0x0100 /* leave received OOB data in line */
//禁用LINGER选项
#define SO_DONTLINGER (int)(~SO_LINGER)
//发送缓冲区长度
#define SO_SNDBUF 0x1001 /* send buffer size */
//接收缓冲区长度
#define SO_RCVBUF 0x1002 /* receive buffer size */
//发送超时时间
#define SO_SNDTIMEO 0x1005 /* send timeout */
//接收超时时间
#define SO_RCVTIMEO 0x1006 /* receive timeout */
//错误状态
#define SO_ERROR 0x1007 /* get error status and clear */
//套接字类型
#define SO_TYPE 0x1008 /* get socket type */2、 读取socket属性:

int getsockopt(SOCKET s, int level, int optname, char FAR * optval, int FAR * optlen)s为欲读取属性的套接字。level为套接字选项的级别,大多数是特定协议和套接字专有的。如IP协议应为 IPPROTO_IP。

optname为读取选项的名称
optval为存放选项值的缓冲区指针。
optlen为缓冲区的长度用法:

int ttl=0; //读取TTL值
int rc = getsockopt( s, IPPROTO_IP, IP_TTL, (char *)&ttl, sizeof(ttl));
//来自MS platform SDK 20033、 设置socket属性:

int setsockopt(SOCKET s,int level, int optname,const char FAR * optval, int optlen)s为欲设置属性的套接字
level为套接字选项的级别,用法同上。
optname为设置选项的名称
optval为存放选项值的缓冲区指针。
optlen为缓冲区的长度

用法:

int ttl=32; //设置TTL值
int rc = setsockopt( s, IPPROTO_IP, IP_TTL, (char *)&ttl, sizeof(ttl)); 套接字的使用步骤

1、启动Winsock:对Winsock DLL进行初始化,协商Winsock的版本支持并分配必要的
资源。(服务器端和客户端)

int WSAStartup( WORD wVersionRequested, LPWSADATA lpWSAData )

wVersionRequested为打算加载Winsock的版本,一般如下设置:
wVersionRequested=MAKEWORD(2,0)
或者直接赋值:wVersionRequested=2

LPWSADATA为初始化Socket后加载的版本的信息,定义如下:
typedef struct WSAData {
WORD wVersion;
WORD wHighVersion;
char szDescription[WSADESCRIPTION_LEN+1];
char szSystemStatus[WSASYS_STATUS_LEN+1];
unsigned short iMaxSockets;
unsigned short iMaxUdpDg;
char FAR * lpVendorInfo;
} WSADATA, FAR * LPWSADATA;如果加载成功后数据为:

wVersion=2表示加载版本为2.0。
wHighVersion=514表示当前系统支持socket最高版本为2.2。
szDescription="WinSock 2.0"
szSystemStatus="Running"表示正在运行。
iMaxSockets=0表示同时打开的socket最大数,为0表示没有限制。
iMaxUdpDg=0表示同时打开的数据报最大数,为0表示没有限制。
lpVendorInfo没有使用,为厂商指定信息预留。该函数使用方法:

WORD wVersion=MAKEWORD(2,0);
WSADATA wsData;
int nResult= WSAStartup(wVersion,&wsData);
if(nResult !=0)
{
//错误处理
}2、创建套接字:(服务器端和客户端)

SOCKET socket( int af, int type, int protocol );
af为网络地址类型,一般为AF_INET,表示在Internet域中使用。
type为套接字类型,前面已经介绍了。
protocol为指定网络协议,一般为IPPROTO_IP。用法:

SOCKET sock=socket(AF_INET,SOCK_STREAM,IPPROTO_IP);
if(sock==INVALID_SOCKET)
{
//错误处理
}3、套接字的绑定:将本地地址绑定到所创建的套接字上。(服务器端和客户端)

int bind( SOCKET s, const struct sockaddr FAR * name, int namelen )
s为已经创建的套接字
name为socket地址结构,为sockaddr结构,如前面讨论的,我们一般使用sockaddr_in
结构,在使用再强制转换为sockaddr结构。
namelen为地址结构的长度。
用法:

sockaddr_in addr;
addr. sin_family=AF_INET;
addr. sin_port= htons(0); //保证字节顺序
addr. sin_addr.s_addr= inet_addr("192.1.8.84")
int nResult=bind(s,(sockaddr*)&addr,sizeof(sockaddr));
if(nResult==SOCKET_ERROR)
{
//错误处理
}4、 套接字的监听:(服务器端)

int listen(SOCKET s, int backlog )s为一个已绑定但未联接的套接字
backlog为指定正在等待联接的最大队列长度,这个参数非常重要,因为服务器一般可
以提供多个连接
用法:

int nResult=listen(s,5) //最多5个连接
if(nResult==SOCKET_ERROR)
{
//错误处理
}5、套接字等待连接::(服务器端)

SOCKET accept( SOCKET s, struct sockaddr FAR * addr, int FAR * addrlen )s为处于监听模式的套接字
sockaddr为接收成功后返回客户端的网络地址。
addrlen为网络地址的长度。

用法:

sockaddr_in addr;
SOCKET s_d=accept(s,(sockaddr*)&addr,sizeof(sockaddr));
if(s==INVALID_SOCKET)
{
//错误处理
}6、套接字的连结:将两个套接字连结起来准备通信。(客户端)

int connect(SOCKET s, const struct sockaddr FAR * name, int namelen )s为欲连结的已创建的套接字
name为欲连结的socket地址。
namelen为socket地址的结构的长度。

用法:

sockaddr_in addr;
addr. sin_family=AF_INET;
addr. sin_port=htons(0); //保证字节顺序
addr. sin_addr.s_addr= htonl(INADDR_ANY) //保证字节顺序
int nResult=connect(s,(sockaddr*)&addr,sizeof(sockaddr));
if(nResult==SOCKET_ERROR)
{
//错误处理
}7、套接字发送数据:(服务器端和客户端)

int send(SOCKET s, const char FAR * buf, int len, int flags )s为服务器端监听的套接字
buf为欲发送数据缓冲区的指针。
len为发送数据缓冲区的长度。
flags为数据发送标记。
返回值为发送数据的字符数。

◆这里讲一下这个发送标记,下面8中讨论的接收标记也一样:

flag取值必须为0或者如下定义的组合:0表示没有特殊行为。

#define MSG_OOB 0x1 /* process out-of-band data */
#define MSG_PEEK 0x2 /* peek at incoming message */
#define MSG_DONTROUTE 0x4 /* send without using routing tables */
MSG_OOB表示数据应该带外发送,所谓带外数据就是TCP紧急数据。
MSG_PEEK表示使有用的数据复制到缓冲区内,但并不从系统缓冲区内删除。
MSG_DONTROUTE表示不要将包路由出去。

用法:

char buf[]="xiaojin";
int nResult=send(s,buf,strlen(buf));
if(nResult==SOCKET_ERROR)
{
//错误处理
}8、 套接字的数据接收:(客户端)

int recv( SOCKET s, char FAR * buf, int len, int flags )s为准备接收数据的套接字
buf为准备接收数据的缓冲区。
len为准备接收数据缓冲区的大小。
flags为数据接收标记。
返回值为接收的数据的字符数。

用法:

char mess[1000];
int nResult =recv(s,mess,1000,0);
if(nResult==SOCKET_ERROR)
{
//错误处理
}9、中断套接字连接:通知服务器端或客户端停止接收和发送数据。(服务器端和客户端)

int shutdown(SOCKET s, int how)s为欲中断连接套接字
How为描述禁止哪些操作,取值为:SD_RECEIVE、SD_SEND、SD_BOTH。

#define SD_RECEIVE 0x00
#define SD_SEND 0x01
#define SD_BOTH 0x02用法:

int nResult= shutdown(s,SD_BOTH);
if(nResult==SOCKET_ERROR)
{
//错误处理
}10、 关闭套接字:释放所占有的资源。(服务器端和客户端)

int closesocket( SOCKET s )s为欲关闭的套接字

用法:

int nResult=closesocket(s);
if(nResult==SOCKET_ERROR)
{
//错误处理
}


与socket有关的一些函数介绍

1、读取当前错误值:每次发生错误时,如果要对具体问题进行处理,那么就应该调用这个函数取得错误代码。
int WSAGetLastError(void );
#define h_errno WSAGetLastError()
错误值请自己阅读Winsock2.h。

2、将主机的unsigned long值转换为网络字节顺序(32位):为什么要这样做呢?因为不同的计算机使用不同的字节顺序存储数据。因此任何从Winsock函数对IP地址和端口号的引用和传给Winsock函数的IP地址和端口号均时按照网络顺序组织的。

u_long htonl(u_long hostlong);
举例:htonl(0)=0
htonl(80)= 1342177280
3、将unsigned long数从网络字节顺序转换位主机字节顺序,是上面函数的逆函数。
u_long ntohl(u_long netlong);
举例:ntohl(0)=0
ntohl(1342177280)= 80
4、将主机的unsigned short值转换为网络字节顺序(16位):原因同2:
u_short htons(u_short hostshort);
举例:htonl(0)=0
htonl(80)= 20480
5、将unsigned short数从网络字节顺序转换位主机字节顺序,是上面函数的逆函数。
u_short ntohs(u_short netshort);
举例:ntohs(0)=0
ntohsl(20480)= 80
6、将用点分割的IP地址转换位一个in_addr结构的地址,这个结构的定义见笔记(一),实际上就是一个unsigned long值。计算机内部处理IP地址可是不认识如192.1.8.84之类的数据。
unsigned long inet_addr( const char FAR * cp );
举例:inet_addr("192.1.8.84")=1409810880
inet_addr("127.0.0.1")= 16777343
如果发生错误,函数返回INADDR_NONE值。

7、将网络地址转换位用点分割的IP地址,是上面函数的逆函数。
char FAR * inet_ntoa( struct in_addr in );
举例:char * ipaddr=NULL;
char addr[20];
in_addr inaddr;
inaddr. s_addr=16777343;
ipaddr= inet_ntoa(inaddr);
strcpy(addr,ipaddr); 这样addr的值就变为127.0.0.1。
注意意不要修改返回值或者进行释放动作。如果函数失败就会返回NULL值。

8、获取套接字的本地地址结构:
int getsockname(SOCKET s, struct sockaddr FAR * name, int FAR * namelen );
s为套接字
name为函数调用后获得的地址值
namelen为缓冲区的大小。
9、获取与套接字相连的端地址结构:

int getpeername(SOCKET s, struct sockaddr FAR * name, int FAR * namelen );
s为套接字
name为函数调用后获得的端地址值
namelen为缓冲区的大小。
10、获取计算机名:

int gethostname( char FAR * name, int namelen );
name是存放计算机名的缓冲区
namelen是缓冲区的大小
用法:
char szName[255];
memset(szName,0,255);
if(gethostname(szName,255)==SOCKET_ERROR)
{
//错误处理
}
返回值为:szNmae="xiaojin"
11、根据计算机名获取主机地址:
struct hostent FAR * gethostbyname( const char FAR * name );

name为计算机名。
用法:
hostent * host;
char* ip;
host= gethostbyname("xiaojin");
if(host->h_addr_list[0])
{
struct in_addr addr;
memmove(&addr, host->h_addr_list[0],4);
//获得标准IP地址
ip=inet_ ntoa (addr);
}

返回值为:hostent->h_name="xiaojin"
hostent->h_addrtype=2 //AF_INET
hostent->length=4
ip="127.0.0.1"
Winsock 的I/O操作:

1、 两种I/O模式
阻塞模式:执行I/O操作完成前会一直进行等待,不会将控制权交给程序。套接字 默认为阻塞模式。可以通过多线程技术进行处理。
非阻塞模式:执行I/O操作时,Winsock函数会返回并交出控制权。这种模式使用 起来比较复杂,因为函数在没有运行完成就进行返回,会不断地返回 WSAEWOULDBLOCK错误。但功能强大。
为了解决这个问题,提出了进行I/O操作的一些I/O模型,下面介绍最常见的三种:

2、select模型:

  通过调用select函数可以确定一个或多个套接字的状态,判断套接字上是否有数据,或
者能否向一个套接字写入数据。
int select( int nfds, fd_set FAR * readfds, fd_set FAR * writefds,
fd_set FAR *exceptfds, const struct timeval FAR * timeout );
◆先来看看涉及到的结构的定义:
a、 d_set结构:

#define FD_SETSIZE 64?
typedef struct fd_set {
u_int fd_count; /* how many are SET? */
SOCKET fd_array[FD_SETSIZE]; /* an array of SOCKETs */
} fd_set; fd_count为已设定socket的数量
fd_array为socket列表,FD_SETSIZE为最大socket数量,建议不小于64。这是微软建
议的。

B、timeval结构:
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* and microseconds */
};
tv_sec为时间的秒值。
tv_usec为时间的毫秒值。
这个结构主要是设置select()函数的等待值,如果将该结构设置为(0,0),则select()函数
会立即返回。

◆再来看看select函数各参数的作用:
nfds:没有任何用处,主要用来进行系统兼容用,一般设置为0。

readfds:等待可读性检查的套接字组。

writefds;等待可写性检查的套接字组。

exceptfds:等待错误检查的套接字组。

timeout:超时时间。

函数失败的返回值:调用失败返回SOCKET_ERROR,超时返回0。
readfds、writefds、exceptfds三个变量至少有一个不为空,同时这个不为空的套接字
种至少有一个socket,道理很简单,否则要select干什么呢。 举例:测试一个套接字是否可读:
fd_set fdread;
//FD_ZERO定义
// #define FD_ZERO(set) (((fd_set FAR *)(set))->fd_count=0)
FD_ZERO(&fdread);
FD_SET(s,&fdread); //加入套接字,详细定义请看winsock2.h
if(select(0,%fdread,NULL,NULL,NULL)>0
{
//成功
if(FD_ISSET(s,&fread) //是否存在fread中,详细定义请看winsock2.h
{
//是可读的
}
}◆I/O操作函数:主要用于获取与套接字相关的操作参数。

int ioctlsocket(SOCKET s, long cmd, u_long FAR * argp ); s为I/O操作的套接字
cmd为对套接字的操作命令。
argp为命令所带参数的指针。

常见的命令:
//确定套接字自动读入的数据量
#define FIONREAD _IOR(''''f'''', 127, u_long) /* get # bytes to read */
//允许或禁止套接字的非阻塞模式,允许为非0,禁止为0
#define FIONBIO _IOW(''''f'''', 126, u_long) /* set/clear non-blocking i/o */
//确定是否所有带外数据都已被读入
#define SIOCATMARK _IOR(''''s'''', 7, u_long) /* at oob mark? */
3、WSAAsynSelect模型:
WSAAsynSelect模型也是一个常用的异步I/O模型。应用程序可以在一个套接字上接收以
WINDOWS消息为基础的网络事件通知。该模型的实现方法是通过调用WSAAsynSelect函
数 自动将套接字设置为非阻塞模式,并向WINDOWS注册一个或多个网络时间,并提供一
个通知时使用的窗口句柄。当注册的事件发生时,对应的窗口将收到一个基于消息的通知。

int WSAAsyncSelect( SOCKET s, HWND hWnd, u_int wMsg, long lEvent); s为需要事件通知的套接字
hWnd为接收消息的窗口句柄
wMsg为要接收的消息
lEvent为掩码,指定应用程序感兴趣的网络事件组合,主要如下:
#define FD_READ_BIT 0
#define FD_READ (1 << FD_READ_BIT)
#define FD_WRITE_BIT 1
#define FD_WRITE (1 << FD_WRITE_BIT)
#define FD_OOB_BIT 2
#define FD_OOB (1 << FD_OOB_BIT)
#define FD_ACCEPT_BIT 3
#define FD_ACCEPT (1 << FD_ACCEPT_BIT)
#define FD_CONNECT_BIT 4
#define FD_CONNECT (1 << FD_CONNECT_BIT)
#define FD_CLOSE_BIT 5
#define FD_CLOSE (1 << FD_CLOSE_BIT)
用法:要接收读写通知:
int nResult= WSAAsyncSelect(s,hWnd,wMsg,FD_READ|FD_WRITE);
if(nResult==SOCKET_ERROR)
{
//错误处理
}
取消通知:

int nResult= WSAAsyncSelect(s,hWnd,0,0);
当应用程序窗口hWnd收到消息时,wMsg.wParam参数标识了套接字,lParam的低字标明
了网络事件,高字则包含错误代码。

4、WSAEventSelect模型
WSAEventSelect模型类似WSAAsynSelect模型,但最主要的区别是网络事件发生时会被发
送到一个事件对象句柄,而不是发送到一个窗口。

使用步骤如下:
a、 创建事件对象来接收网络事件:

#define WSAEVENT HANDLE
#define LPWSAEVENT LPHANDLE
WSAEVENT WSACreateEvent( void );
该函数的返回值为一个事件对象句柄,它具有两种工作状态:已传信(signaled)和未传信
(nonsignaled)以及两种工作模式:人工重设(manual reset)和自动重设(auto reset)。默认未
未传信的工作状态和人工重设模式。

b、将事件对象与套接字关联,同时注册事件,使事件对象的工作状态从未传信转变未
已传信。

int WSAEventSelect( SOCKET s,WSAEVENT hEventObject,long lNetworkEvents ); s为套接字
hEventObject为刚才创建的事件对象句柄
lNetworkEvents为掩码,定义如上面所述

c、I/O处理后,设置事件对象为未传信
BOOL WSAResetEvent( WSAEVENT hEvent );Hevent为事件对象

成功返回TRUE,失败返回FALSE。

d、等待网络事件来触发事件句柄的工作状态:

DWORD WSAWaitForMultipleEvents( DWORD cEvents,
const WSAEVENT FAR * lphEvents, BOOL fWaitAll,
DWORD dwTimeout, BOOL fAlertable );lpEvent为事件句柄数组的指针
cEvent为为事件句柄的数目,其最大值为WSA_MAXIMUM_WAIT_EVENTS
fWaitAll指定等待类型:TRUE:当lphEvent数组重所有事件对象同时有信号时返回;
FALSE:任一事件有信号就返回。
dwTimeout为等待超时(毫秒)
fAlertable为指定函数返回时是否执行完成例程

对事件数组中的事件进行引用时,应该用WSAWaitForMultipleEvents的返回值,减去
预声明值WSA_WAIT_EVENT_0,得到具体的引用值。例如:

nIndex=WSAWaitForMultipleEvents(…);
MyEvent=EventArray[Index- WSA_WAIT_EVENT_0];e、判断网络事件类型:

int WSAEnumNetworkEvents( SOCKET s,
WSAEVENT hEventObject, LPWSANETWORKEVENTS lpNetworkEvents );s为套接字
hEventObject为需要重设的事件对象
lpNetworkEvents为记录网络事件和错误代码,其结构定义如下:

typedef struct _WSANETWORKEVENTS {
long lNetworkEvents;
int iErrorCode[FD_MAX_EVENTS];
} WSANETWORKEVENTS, FAR * LPWSANETWORKEVENTS;f、关闭事件对象句柄:

BOOL WSACloseEvent(WSAEVENT hEvent);调用成功返回TRUE,否则返回FALSE。
mfc 添加自定义消息
Windows 应用程序所要做的每项工作几乎都是基于消息处理的, Windows 系统消息分为常用 Windows 消息,控件通知消息和命令。然而,有时我们需要定义自己的消息来通知程序什么事情发生了,这就是用户自定义消息。 ClassWizard 并没有提供增加用户自定义消息的功能,所以要使用用户自定义消息,必须手工编写代码。然后 ClassWizard 才可以象处理其它消息一样处理你自定义的消息。具体做法如下详解: 

   第一步:定义消息。一个消息实际上是开发 Windows95 应用程序时, Microsoft 推荐用户自定义消息至少是 WM_USER+100 ,因为很多新控件也要使用 WM_USER 消息。 

   第二步:实现消息处理函数。该函数使用 WPRAM 和 LPARAM 参数并返回 LPESULT 。 

   LPESULT CMainFrame::OnMyMessage(WPARAM wParam, LPARAM lParam){// TODO: 处理用户自定义消息 AfxMessageBox(" 处理用户自定义消息 "); return 0;} 

   第三步:在类头文件的 AFX_MSG 块中说明消息处理函数: 
   class CMainFrame:public CMDIFrameWnd{

   ...

   // 一般消息映射函数 

   protected:

   // {{AFX_MSG(CMainFrame)

   afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);

   afx_msg void OnTimer(UINT nIDEvent);

   afx_msg LRESULT OnMyMessage(WPARAM wParam, LPARAM lParam);

   //}}AFX_MSG

   DECLARE_MESSAGE_MAP()}


   第四步:在用户类的消息块中,使用 ON_MESSAGE 宏指令将消息映射到消息处理函数中。 
   BEGIN_MESSAGE_MAP(CMainFrame, CMDIFrameWnd)

   //{{AFX_MSG_MAP(CMainFrame)

   ON_WM_CREATE()

   ON_WM_TIMER()

   ON_MESSAGE(WM_MY_MESSAGE, OnMyMessage)

   //}}AFX_MSG_MAPEND_MESSAGE_MAP()


   这样,一个用户自定义消息就可以使用了,如果用户需要一个整个系统唯一的消息,可以调用 SDK 函数 RegisterWindowMessage 并使用 ON_REGISTER_MESSAGE 宏指令取代 ON_MESSAGE 宏指令,其余步骤同上。 

   VC++ 为程序员提供了一套功能强大、方便快捷的编程工具,它可以帮你方便的生成窗口、菜单等用户界面,可惜就是做出来的东西都一样,没有一点个性。下面,就介绍一些方法,让我们可以按照自己的设计定制出更加符合自己程序风格的窗口。
 
 
  如果用户需要一个定义整个系统唯一的消息,可以调用SDK函数RegisterWindowMessage定义消息:

static UINT WM_MY_MESSAGE=RegisterWindowMessage("User");

  并使用ON_REGISTERED_MESSAGE宏指令取代ON_MESSAGE宏指令,其余步骤同上。

  当需要使用自定义消息时,可以在相应类中的函数中调用函数PostMessage或SendMessage发送消息PoseMessage(WM_MY_MESSAGE,O,O); 如果向其他进程发送消息可通过如下方法发送消息:

DWORD result;
SendMessageTimeout(wnd->m_hWnd, // 目标窗口
WM_MY_MESSAGE, // 消息
0, // WPARAM
0, // LPARAM
SMTO_ABORTIFHUNG |
SMTO_NORMAL,
TIMEOUT_INTERVAL,
&result);

  以避免其它进程如果被阻塞而造成系统死等状态。

  可是如果需要向其它类(如主框架、子窗口、视类、对话框、状态条、工具条或其他控件等)发送消息时,上述方法显得无能为力,而在编程过程中往往需要获取其它类中的某个识别信号,MFC框架给我们造成了种种限制,但是可以通过获取某个类的指针而向这个类发送消息,而自定义消息的各种动作则在这个类中定义,这样就可以自由自在的向其它类发送消息了。

  下面举的例子叙述了向视类和框架类发送消息的方法:

  在主框架类中向视类发送消息:

  视类中定义消息:

ON_REGISTERED_MESSAGE(WM_MY_MESSAGE,OnMyMessage) //定义消息映射
视类定义消息处理函数:

// 消息处理函数
LRESULT CMessageView::OnMyMessage(WPARAM wParam, LPARAM lParam)
{
// TODO: 处理用户自定义消息
...
return 0;
}

//发送消息的测试函数
void CMainFrame::OnTest()
{
CView * active = GetActiveView();//获取当前视类指针
if(active != NULL)
active->PostMessage(WM_MY_MESSAGE,0,0);
}

  在其它类中向视类发送消息:

//发送消息的测试函数
void CMainFrame::OnTest()
{
CMDIFrameWnd *pFrame;
CMDIChildWnd *pChild;
CView *pView;
//获取主窗口指针
pFrame =(CMDIFrameWnd*)AfxGetApp()->m_pMainWnd;
// 获取子窗口指针
pChild = (CMDIChildWnd *) pFrame->GetActiveFrame();
//获取视类指针
pView = pChild->GetActiveView();
if(pView != NULL)
pView->PostMessage(WM_MY_MESSAGE,0,0);//发送消息
}

 

 在视类中向主框架发送消息:

  首先在主框架中定义相关的消息,方法同上,然后在发送消息的函数中添加代码如下

//发送消息的测试函数
void CMessageView::OnTest()
{
CFrameWnd * active = GetActiveFrame();//获取当前主窗口框架指针
if(active != this)
active->PostMessage(WM_MY_MESSAGE,0,0);
return 0;
}

  在其它类中向不同的类发送消息可依次方法类推,这样我们的程序就可以的不受限制向其它类和进程发送消息,而避免了种种意想不到的风险。

  下面一个例子程序为多文档程序里在一对话框中向视类发送消息,详述了发送自定义消息的具体过程。

 实现步骤:

  第一步:在VC++中新建工程Message,所有ClassWizard步骤选项均为缺省,完成。

  第二步:在主菜单中添加测试菜单为调出对话框,在框架类中建立相应函数OnTest()

  第三步:在资源中建立对话框,通过ClassWizard添加新类TestDialog,添加测试按钮,

  在对话框类中建立相应函数OnDialogTest()

//通过对话框按钮发送消息的函数
void TestDialog::OnDialogTest()
{
CMDIFrameWnd *pFrame;
CMDIChildWnd *pChild;
CView *pView;
//获取主窗口指针
pFrame =(CMDIFrameWnd*)AfxGetApp()->m_pMainWnd;
// 获取子窗口指针
pChild = (CMDIChildWnd *) pFrame->GetActiveFrame();
//获取视类指针
pView = pChild->GetActiveView();
if(active != NULL)
active->PostMessage(WM_MY_MESSAGE,0,0);//发送消息
}

  在Message.h头文件中添加如下语句:

static UINT WM_MY_MESSAGE=RegisterWindowMessage("Message");

  第四步:在视类中添加自定义消息:

  在头文件MessageView.h中添加消息映射

protected:
//{{AFX_MSG(CMessageView)
//}}AFX_MSG
afx_msg LRESULT OnMyMessage(WPARAM wParam, LPARAM lParam); //此行为添加代码
DECLARE_MESSAGE_MAP()
在视类文件MessageView.cpp中的消息映射中添加自定义消息映射
BEGIN_MESSAGE_MAP(CMessageView, CView)
//{{AFX_MSG_MAP(CMessageView)
//}}AFX_MSG_MAP
// Standard printing commands
ON_REGISTERED_MESSAGE(WM_MY_MESSAGE,OnMyMessage) //此行添加代码定义唯一消息
END_MESSAGE_MAP()

  添加相应的0消息处理函数

LRESULT CMessageView::OnMyMessage(WPARAM wParam, LPARAM lParam)
{
CRect rect;
GetClientRect(&rect);
InvalidateRect(&rect);
test=!test;
return 0;
}

  在MessageView.h中添加布尔变量 public:BOOL test;

  在视类构造函数中初始化 test变量:test=FALSE;

  修改CMessageView::OnDraw()函数

void CMessageView::OnDraw(CDC* pDC)
{
CMessageDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// 以下程序显示消息响应效果
if(test)
pDC->TextOut(0,0,"消息响应!");
}

  第五步:显示测试对话框

  在MainFrame类中包含对话框头文件:

#include "TestDialog.h";
OnTest()函数中添加代码
void CMainFrame::OnTest()
{
TestDialog dialog;
dialog.DoModal();
}

  运行程序,在测试菜单打开对话框,点击测试按钮即可看到结果。

vc 实现毫秒定时器
定时器使用方法

  定时器在VC中的使用频繁,以下讨论定义器的使用方法。

  定时器的原型是:

  WINUSERAPI UINT WINAPI SetTimer ( HWND hWnd , UINT nIDEvent, UINT uElapse, TIMERPROC lpTimerFunc);

    hWnd      是欲设置定时器的窗体句柄。定时时间到时,系统会向该窗体发送WM_TIMER消息。

    nIDEvent      定时器标识符。在一个窗体内可以使用多个定时器,不同的定时器根据nIDEvent来区分。

    uElapse         定时时间,单位是毫秒。

    lpTimerFunc 定时器的回调函数。如果该值为NULL,定时时间到时,定时器发送的消息WM_TIMER由窗体映像该消息的函数处理;否则由回调函数处理,说白一点,这里的回调函数就是取代OnTimer的处理函数。

         通常,我们在使用定时器时,只用到三个参数,即
    UINT CWnd::SetTimer( 
            UINT nIDEvent
     UINT nElapse
      void (CALLBACK EXPORT* lpfnTimer)( HWND, UINT, UINT, DWORD) 
    );

    其实,这个函数只是MFCAPI的封装,其实现函数为:

    _AFXWIN_INLINE UINT CWnd::SetTimer(UINT nIDEvent, UINT nElapse,

        void (CALLBACK* lpfnTimer)(HWND, UINT, UINT, DWORD))

  {
             ASSERT(::IsWindow(m_hWnd));
         return ::SetTimer(m_hWnd, nIDEvent, nElapse,(TIMERPROC)lpfnTimer); 
    }

    由此可见,CWnd::SetTimer只是将API函数SetTimer的第一个参数设置成它自己的句柄而已。


   
有了上面的认识,对定时器的使用就清楚了,下面举例说明定时器的具体使用。

    1.打开VC,新建一基于对话框的工程,工程名为Test在对话框上添加一按钮,将其ID改为IDC_BUTTON_STARTCaption改为Start. 映像该按钮的BN_CLICKED消息,void CTestDlg::OnButtonStart();

    2.再在对话框上添加一按钮,IDID_BUTTON_STOP,Caption改为Stop,映像消息为void CTestDlg::OnButtonStop();

    3.添加一个Lable,ID改为IDC_STATIC_TIME,用于记数,表明定时器函数的执行。

  4.映像对话框的WM_TIMER消息,void CTestDlg::OnTimer(UINT nIDEvent);


   
以上的实现函数如下所示:

void CTestDlg::OnButtonStart()

{

    SetTimer(1,1000,NULL);//启动定时器1,定时时间是1

}

 

void CTestDlg::OnButtonStop()

{

    KillTimer(1);        //关闭定时器1

}

 

void CTestDlg::OnTimer(UINT nIDEvent)

{

    static int nTimer=0;

    CString strTmp="";

    strTmp.Format("Timer:    %d",nTimer++);

    CWnd *pWnd=GetDlgItem(IDC_STATIC_TIME);

    pWnd->SetWindowText(strTmp);  //Lable

2006/4/15
opengl----使用3dmax建模后怎样把模型导入
//  importmodel.h/
#include
#include
#include   // Header File For Windows
#include    // Header File For Standard Input/Output
#include    // Header File For The OpenGL32 Library
#include    // Header File For The GLu32 Library
#include   // Header File For The Glaux Library
#include
//  基本块(Primary Chunk),位于文件的开始
#define PRIMARY       0x4D4D
//  主块(Main Chunks)
#define OBJECTINFO    0x3D3D  // 网格对象的版本号
#define VERSION       0x0002  // .3ds文件的版本
#define EDITKEYFRAME  0xB000  // 所有关键帧信息的头部
//  对象的次级定义(包括对象的材质和对象)
#define MATERIAL   0xAFFF  // 保存纹理信息
#define OBJECT    0x4000  // 保存对象的面、顶点等信息
//  材质的次级定义
#define MATNAME       0xA000  // 保存材质名称
#define MATDIFFUSE    0xA020  // 对象/材质的颜色
#define MATMAP        0xA200  // 新材质的头部
#define MATMAPFILE    0xA300  // 保存纹理的文件名
#define OBJ_MESH   0x4100  // 新的网格对象
#define MAX_TEXTURES  100   // 最大的纹理数目
//  OBJ_MESH的次级定义
#define OBJ_VERTICES  0x4110  // 对象顶点
#define OBJ_FACES   0x4120  // 对象的面
#define OBJ_MATERIAL  0x4130  // 对象的材质
#define OBJ_UV    0x4140  // 对象的UV纹理坐标
#define MAP_W       32       // size of map along x-axis 32
#define MAP_SCALE   24.0f     // the scale of the terrain map
#define MAP   MAP_W*MAP_SCALE/2
#define KEY_DOWN(vk_code)((GetAsyncKeyState(vk_code) & 0x8000) ? 1 : 0)
#define RAND_COORD(x)   ((float)rand()/RAND_MAX * (x))
#define FRAND   (((float)rand()-(float)rand())/RAND_MAX)
using namespace std;
class CVector3  //定义3D点的类,用于保存模型中的顶点
{public: float x, y, z;
};
class CVector2  //定义2D点类,用于保存模型的UV纹理坐标
{public: float x, y;
};
struct tFace  //面的结构定义
{ int vertIndex[3];   // 顶点索引
 int coordIndex[3];   // 纹理坐标索引
};
struct tMatInfo//材质信息结构体
{ char  strName[255];   // 纹理名称
 char  strFile[255];   // 如果存在纹理映射,则表示纹理文件名称
 BYTE  color[3];    // 对象的RGB颜色
 int   texureId;    // 纹理ID
 float uTile;    // u 重复
 float vTile;    // v 重复
 float uOffset;       // u 纹理偏移
 float vOffset;    // v 纹理偏移
} ;
struct t3DObject //对象信息结构体
{ int  numOfVerts;   // 模型中顶点的数目
 int  numOfFaces;   // 模型中面的数目
 int  numTexVertex;   // 模型中纹理坐标的数目
 int  materialID;   // 纹理ID
 bool bHasTexture;   // 是否具有纹理映射
 char strName[255];   // 对象的名称
 CVector3  *pVerts;   // 对象的顶点
 CVector3  *pNormals;  // 对象的法向量
 CVector2  *pTexVerts;  // 纹理UV坐标
 tFace *pFaces;    // 对象的面信息
};
struct t3DModel //模型信息结构体
{ int numOfObjects;   // 模型中对象的数目
 int numOfMaterials;   // 模型中材质的数目
 vectorpMaterials; // 材质链表信息
 vector pObject; // 模型中对象链表信息
};
struct tChunk //保存块信息的结构
{ unsigned short int ID;  // 块的ID  
 unsigned int length;  // 块的长度
 unsigned int bytesRead;  // 需要读的块数据的字节数
};
class CLoad3DS// CLoad3DS类处理所有的装入代码
{
public:
 CLoad3DS();        // 初始化数据成员
 virtual ~CLoad3DS();
 void show3ds(int j0,float tx,float ty,float tz,float size);//显示3ds模型
 void Init(char *filename,int j);
private:
 bool Import3DS(t3DModel *pModel, char *strFileName);// 装入3ds文件到模型结构中
 void CreateTexture(UINT textureArray[],LPSTR strFileName,int textureID);//  从文件中创建纹理
 int  GetString(char *);        // 读一个字符串
 void ReadChunk(tChunk *);       // 读下一个块
 void ReadNextChunk(t3DModel *pModel, tChunk *);  // 读下一个块
 void ReadNextObjChunk(t3DModel *pModel,t3DObject *pObject,tChunk *);// 读下一个对象块
 void ReadNextMatChunk(t3DModel *pModel, tChunk *); // 读下一个材质块
 void ReadColor(tMatInfo *pMaterial, tChunk *pChunk);// 读对象颜色的RGB值
 void ReadVertices(t3DObject *pObject, tChunk *); // 读对象的顶点
 void ReadVertexIndices(t3DObject *pObject,tChunk *);// 读对象的面信息
 void ReadUVCoordinates(t3DObject *pObject,tChunk *);// 读对象的纹理坐标
 void ReadObjMat(t3DModel *pModel,t3DObject *pObject,tChunk *pPreChunk);// 读赋予对象的材质名称
 void ComputeNormals(t3DModel *pModel);    // 计算对象顶点的法向量
 void CleanUp();          // 关闭文件,释放内存空间
 FILE *m_FilePointer;        // 文件指针
 tChunk *m_CurrentChunk;
 tChunk *m_TempChunk;
};
 
//
///ImportModel.cpp
#include "StdAfx.h"
//#include "Set3ds.h"
#include "ImportModel.h"
#include   // Header File For Windows
#include    // Header File For Standard Input/Output
#include    // Header File For The OpenGL32 Library
#include    // Header File For The GLu32 Library
#include   // Header File For The Glaux Library
#include
     
UINT g_Texture[10][MAX_TEXTURES] = {0}; 
t3DModel g_3DModel[10]; 
        
int   g_ViewMode   = GL_TRIANGLES;
bool  g_bLighting     = true;  
CLoad3DS::CLoad3DS()//  构造函数的功能是初始化tChunk数据
{ m_CurrentChunk = new tChunk; // 初始化并为当前的块分配空间
 m_TempChunk = new tChunk;  // 初始化一个临时块并分配空间
}
CLoad3DS::~CLoad3DS()
{ CleanUp();// 释放内存空间
 for(int j = 0; j <10;j++)
 for(int i = 0; i < g_3DModel[j].numOfObjects; i++)
 { delete [] g_3DModel[j].pObject[i].pFaces;// 删除所有的变量
  delete [] g_3DModel[j].pObject[i].pNormals;
  delete [] g_3DModel[j].pObject[i].pVerts;
  delete [] g_3DModel[j].pObject[i].pTexVerts;
 }
}

void CLoad3DS::Init(char *filename,int j)//
{ Import3DS(&g_3DModel[j], filename);   // 将3ds文件装入到模型结构体中
 for(int i =0; i {if(strlen(g_3DModel[j].pMaterials[i].strFile)>0)// 判断是否是一个文件名
 CreateTexture(g_Texture[j], g_3DModel[j].pMaterials[i].strFile, i);//使用纹理文件名称来装入位图   
  g_3DModel[j].pMaterials[i].texureId = i;// 设置材质的纹理ID
 }
}
//  从文件中创建纹理
void CLoad3DS::CreateTexture(UINT textureArray[], LPSTR strFileName, int textureID)
{ AUX_RGBImageRec *pBitmap = NULL;
 if(!strFileName) return;       // 如果无此文件,则直接返回
 pBitmap = auxDIBImageLoad(strFileName);    // 装入位图,并保存数据
 if(pBitmap == NULL)  exit(0);     // 如果装入位图失败,则退出
 // 生成纹理
 glGenTextures(1, &textureArray[textureID]);
 // 设置像素对齐格式
 glPixelStorei (GL_UNPACK_ALIGNMENT, 1);
 glBindTexture(GL_TEXTURE_2D, textureArray[textureID]);
 gluBuild2DMipmaps(GL_TEXTURE_2D, 3, pBitmap->sizeX, pBitmap->sizeY, GL_RGB, GL_UNSIGNED_BYTE, pBitmap->data);
 glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR_MIPMAP_NEAREST);
 glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR_MIPMAP_LINEAR);
 if (pBitmap)          // 释放位图占用的资源
 { if (pBitmap->data) free(pBitmap->data);       
  free(pBitmap);     
 }
}
void CLoad3DS::show3ds(int j0,float tx,float ty,float tz,float size) //显示3ds模型
{
 glPushAttrib(GL_CURRENT_BIT);//保存现有颜色属实性
 glPushMatrix();
 glDisable(GL_TEXTURE_2D);
 ::glTranslatef( tx, ty, tz);
 ::glScaled(size,size,size);
 glRotatef(90, 0, 1.0f, 0);
 // 遍历模型中所有的对象
 for(int i = 0; i < g_3DModel[j0].numOfObjects; i++)
 {if(g_3DModel[j0].pObject.size() <= 0) break;// 如果对象的大小小于0,则退出
  t3DObject *pObject = &g_3DModel[j0].pObject[i];// 获得当前显示的对象
  if(pObject->bHasTexture)// 判断该对象是否有纹理映射
   { glEnable(GL_TEXTURE_2D);// 打开纹理映射
   glBindTexture(GL_TEXTURE_2D, g_Texture[j0][pObject->materialID]);
   }
  else glDisable(GL_TEXTURE_2D);// 关闭纹理映射
//这里原来有错,不行正确调用模型的贴图,g_Texture应该为2维数组 
  glColor3ub(255, 255, 255);
  glBegin(g_ViewMode);//开始以g_ViewMode模式绘制     
  for(int j = 0; j < pObject->numOfFaces; j++)  // 遍历所有的面
  {for(int tex = 0; tex < 3; tex++)     // 遍历三角形的所有点
  {int index = pObject->pFaces[j].vertIndex[tex]; // 获得面对每个点的索引
   glNormal3f(pObject->pNormals[index].x,pObject->pNormals[index].y, 
           pObject->pNormals[index].z);  // 给出法向量
   if(pObject->bHasTexture)      // 如果对象具有纹理
   { if(pObject->pTexVerts)      // 确定是否有UVW纹理坐标
    glTexCoord2f(pObject->pTexVerts[index].x,pObject->pTexVerts[index].y);
   }
   else
   { if(g_3DModel[j0].pMaterials.size() && pObject->materialID>= 0)
   { BYTE *pColor = g_3DModel[j0].pMaterials[pObject->materialID].color;
    glColor3ub(pColor[0],pColor[1],pColor[2]);
   }
   }
   glVertex3f(pObject->pVerts[index].x,pObject->pVerts[index].y,pObject->pVerts[index].z);
  }
  }
 glEnd();// 绘制结束
 }
 glEnable(GL_TEXTURE_2D);
 glPopMatrix();
 glPopAttrib();//恢复前一属性
}
//
//  打开一个3ds文件,读出其中的内容,并释放内存
bool CLoad3DS::Import3DS(t3DModel *pModel, char *strFileName)
{ char strMessage[255] = {0};
 // 打开一个3ds文件
 m_FilePointer = fopen(strFileName, "rb");
 // 确保所获得的文件指针合法
 if(!m_FilePointer)
 { sprintf(strMessage, "Unable to find the file: %s!", strFileName);
  MessageBox(NULL, strMessage, "Error", MB_OK);
  return false;
 }
 // 当文件打开之后,首先应该将文件最开始的数据块读出以判断是否是一个3ds文件
 // 如果是3ds文件的话,第一个块ID应该是PRIMARY
 // 将文件的第一块读出并判断是否是3ds文件
 ReadChunk(m_CurrentChunk);
 // 确保是3ds文件
 if (m_CurrentChunk->ID != PRIMARY)
 { sprintf(strMessage, "Unable to load PRIMARY chuck from file: %s!", strFileName);
  MessageBox(NULL, strMessage, "Error", MB_OK);
  return false;
 }
 // 现在开始读入数据,ReadNextChunk()是一个递归函数
 // 通过调用下面的递归函数,将对象读出
 ReadNextChunk(pModel, m_CurrentChunk);
 // 在读完整个3ds文件之后,计算顶点的法线
 ComputeNormals(pModel);
 // 释放内存空间
// CleanUp();
 return true;
}
//  下面的函数释放所有的内存空间,并关闭文件
void CLoad3DS::CleanUp()
{ // 遍历场景中所有的对象
 fclose(m_FilePointer);      // 关闭当前的文件指针
 delete m_CurrentChunk;      // 释放当前块
 delete m_TempChunk;       // 释放临时块
}
//  下面的函数读出3ds文件的主要部分
void CLoad3DS::ReadNextChunk(t3DModel *pModel, tChunk *pPreChunk)
{ t3DObject newObject = {0};     // 用来添加到对象链表
 tMatInfo newTexture = {0};    // 用来添加到材质链表
 unsigned int version = 0;     // 保存文件版本
 int buffer[50000] = {0};     // 用来跳过不需要的数据
 m_CurrentChunk = new tChunk;    // 为新的块分配空间  
 //  下面每读一个新块,都要判断一下块的ID,如果该块是需要的读入的,则继续进行
 //  如果是不需要读入的块,则略过
 // 继续读入子块,直到达到预定的长度
 while (pPreChunk->bytesRead < pPreChunk->length)
 { // 读入下一个块
  ReadChunk(m_CurrentChunk);
  // 判断块的ID号
  switch (m_CurrentChunk->ID)
  {
  case VERSION:       // 文件版本号
   // 在该块中有一个无符号短整型数保存了文件的版本
   // 读入文件的版本号,并将字节数添加到bytesRead变量中
   m_CurrentChunk->bytesRead += fread(&version, 1, m_CurrentChunk->length - m_CurrentChunk->bytesRead, m_FilePointer);
   // 如果文件版本号大于3,给出一个警告信息
   if (version > 0x03)
    MessageBox(NULL, "This 3DS file is over version 3 so it may load incorrectly", "Warning", MB_OK);
   break;
  case OBJECTINFO:      // 网格版本信息
   // 读入下一个块
   ReadChunk(m_TempChunk);
   // 获得网格的版本号
   m_TempChunk->bytesRead += fread(&version, 1, m_TempChunk->length - m_TempChunk->bytesRead, m_FilePointer);
   // 增加读入的字节数
   m_CurrentChunk->bytesRead += m_TempChunk->bytesRead;
   // 进入下一个块
   ReadNextChunk(pModel, m_CurrentChunk);
   break;
  case MATERIAL:       // 材质信息
   // 材质的数目递增
   pModel->numOfMaterials++;
   // 在纹理链表中添加一个空白纹理结构
   pModel->pMaterials.push_back(newTexture);
   // 进入材质装入函数
   ReadNextMatChunk(pModel, m_CurrentChunk);
   break;
  case OBJECT:       // 对象的名称
   // 该块是对象信息块的头部,保存了对象了名称
   // 对象数递增
   pModel->numOfObjects++;
   // 添加一个新的tObject节点到对象链表中
   pModel->pObject.push_back(newObject);
   // 初始化对象和它的所有数据成员
   memset(&(pModel->pObject[pModel->numOfObjects - 1]), 0, sizeof(t3DObject));
   // 获得并保存对象的名称,然后增加读入的字节数
   m_CurrentChunk->bytesRead += GetString(pModel->pObject[pModel->numOfObjects - 1].strName);
   // 进入其余的对象信息的读入
   ReadNextObjChunk(pModel, &(pModel->pObject[pModel->numOfObjects - 1]), m_CurrentChunk);
   break;
  case EDITKEYFRAME:
   // 跳过关键帧块的读入,增加需要读入的字节数
   m_CurrentChunk->bytesRead += fread(buffer, 1, m_CurrentChunk->length - m_CurrentChunk->bytesRead, m_FilePointer);
   break;
  default:
   //  跳过所有忽略的块的内容的读入,增加需要读入的字节数
   m_CurrentChunk->bytesRead += fread(buffer, 1, m_CurrentChunk->length - m_CurrentChunk->bytesRead, m_FilePointer);
   break;
  }
  // 增加从最后块读入的字节数
  pPreChunk->bytesRead += m_CurrentChunk->bytesRead;
 }
 // 释放当前块的内存空间
 delete m_CurrentChunk;
 m_CurrentChunk = pPreChunk;
}
//  下面的函数处理所有的文件中对象的信息
void CLoad3DS::ReadNextObjChunk(t3DModel *pModel, t3DObject *pObject, tChunk *pPreChunk)
{ int buffer[50000] = {0};     // 用于读入不需要的数据
 // 对新的块分配存储空间
 m_CurrentChunk = new tChunk;
 // 继续读入块的内容直至本子块结束
 while (pPreChunk->bytesRead < pPreChunk->length)
 { // 读入下一个块
  ReadChunk(m_CurrentChunk);
  // 区别读入是哪种块
  switch (m_CurrentChunk->ID)
  {
  case OBJ_MESH:     // 正读入的是一个新块
   // 使用递归函数调用,处理该新块
   ReadNextObjChunk(pModel, pObject, m_CurrentChunk);
   break;
  case OBJ_VERTICES:    // 读入是对象顶点
   ReadVertices(pObject, m_CurrentChunk);
   break;
  case OBJ_FACES:     // 读入的是对象的面
   ReadVertexIndices(pObject, m_CurrentChunk);
   break;
  case OBJ_MATERIAL:    // 读入的是对象的材质名称
   // 该块保存了对象材质的名称,可能是一个颜色,也可能是一个纹理映射。同时在该块中也保存了
   // 纹理对象所赋予的面
   // 下面读入对象的材质名称
   ReadObjMat(pModel, pObject, m_CurrentChunk);   
   break;
  case OBJ_UV:      // 读入对象的UV纹理坐标
   // 读入对象的UV纹理坐标
   ReadUVCoordinates(pObject, m_CurrentChunk);
   break;
  default: 
   // 略过不需要读入的块
   m_CurrentChunk->bytesRead += fread(buffer, 1, m_CurrentChunk->length - m_CurrentChunk->bytesRead, m_FilePointer);
   break;
  }
  // 添加从最后块中读入的字节数到前面的读入的字节中
  pPreChunk->bytesRead += m_CurrentChunk->bytesRead;
 }
 // 释放当前块的内存空间,并把当前块设置为前面块
 delete m_CurrentChunk;
 m_CurrentChunk = pPreChunk;
}
//  下面的函数处理所有的材质信息
void CLoad3DS::ReadNextMatChunk(t3DModel *pModel, tChunk *pPreChunk)
{ int buffer[50000] = {0};     // 用于读入不需要的数据
 // 给当前块分配存储空间
 m_CurrentChunk = new tChunk;
 // 继续读入这些块,知道该子块结束
 while (pPreChunk->bytesRead < pPreChunk->length)
 { // 读入下一块
  ReadChunk(m_CurrentChunk);
  // 判断读入的是什么块
  switch (m_CurrentChunk->ID)
  {
  case MATNAME:       // 材质的名称
   // 读入材质的名称
   m_CurrentChunk->bytesRead += fread(pModel->pMaterials[pModel->numOfMaterials - 1].strName, 1, m_CurrentChunk->length - m_CurrentChunk->bytesRead, m_FilePointer);
   break;
  case MATDIFFUSE:      // 对象的R G B颜色
   ReadColor(&(pModel->pMaterials[pModel->numOfMaterials - 1]), m_CurrentChunk);
   break;
  case MATMAP:       // 纹理信息的头部
   // 进入下一个材质块信息
   ReadNextMatChunk(pModel, m_CurrentChunk);
   break;
  case MATMAPFILE:      // 材质文件的名称
   // 读入材质的文件名称
   m_CurrentChunk->bytesRead += fread(pModel->pMaterials[pModel->numOfMaterials - 1].strFile, 1, m_CurrentChunk->length - m_CurrentChunk->bytesRead, m_FilePointer);
   break;
  default: 
   // 掠过不需要读入的块
   m_CurrentChunk->bytesRead += fread(buffer, 1, m_CurrentChunk->length - m_CurrentChunk->bytesRead, m_FilePointer);
   break;
  }
  // 添加从最后块中读入的字节数
  pPreChunk->bytesRead += m_CurrentChunk->bytesRead;
 }
 // 删除当前块,并将当前块设置为前面的块
 delete m_CurrentChunk;
 m_CurrentChunk = pPreChunk;
}
//  下面函数读入块的ID号和它的字节长度
void CLoad3DS::ReadChunk(tChunk *pChunk)
{ // 读入块的ID号,占用了2个字节。块的ID号象OBJECT或MATERIAL一样,说明了在块中所包含的内容
 pChunk->bytesRead = fread(&pChunk->ID, 1, 2, m_FilePointer);
 // 然后读入块占用的长度,包含了四个字节
 pChunk->bytesRead += fread(&pChunk->length, 1, 4, m_FilePointer);
}
//  下面的函数读入一个字符串
int CLoad3DS::GetString(char *pBuffer)
{ int index = 0;
 // 读入一个字节的数据
 fread(pBuffer, 1, 1, m_FilePointer);
 // 直到结束
 while (*(pBuffer + index++) != 0) {
  // 读入一个字符直到NULL
  fread(pBuffer + index, 1, 1, m_FilePointer);
 }
 // 返回字符串的长度
 return strlen(pBuffer) + 1;
}
//  下面的函数读入RGB颜色
void CLoad3DS::ReadColor(tMatInfo *pMaterial, tChunk *pChunk)
{ // 读入颜色块信息
 ReadChunk(m_TempChunk);
 // 读入RGB颜色
 m_TempChunk->bytesRead += fread(pMaterial->color, 1, m_TempChunk->length - m_TempChunk->bytesRead, m_FilePointer);
 // 增加读入的字节数
 pChunk->bytesRead += m_TempChunk->bytesRead;
}
//  下面的函数读入顶点索引
void CLoad3DS::ReadVertexIndices(t3DObject *pObject, tChunk *pPreChunk)
{ unsigned short index = 0;     // 用于读入当前面的索引
 // 读入该对象中面的数目
 pPreChunk->bytesRead += fread(&pObject->numOfFaces, 1, 2, m_FilePointer);
 // 分配所有面的存储空间,并初始化结构
 pObject->pFaces = new tFace [pObject->numOfFaces];
 memset(pObject->pFaces, 0, sizeof(tFace) * pObject->numOfFaces);
 // 遍历对象中所有的面
 for(int i = 0; i < pObject->numOfFaces; i++)
 { for(int j = 0; j < 4; j++)
  { // 读入当前面的第一个点
   pPreChunk->bytesRead += fread(&index, 1, sizeof(index), m_FilePointer);
   if(j < 3)
   { // 将索引保存在面的结构中
    pObject->pFaces[i].vertIndex[j] = index;
   }
  }
 }
}
//  下面的函数读入对象的UV坐标
void CLoad3DS::ReadUVCoordinates(t3DObject *pObject, tChunk *pPreChunk)
{ // 为了读入对象的UV坐标,首先需要读入UV坐标的数量,然后才读入具体的数据
 // 读入UV坐标的数量
 pPreChunk->bytesRead += fread(&pObject->numTexVertex, 1, 2, m_FilePointer);
 // 分配保存UV坐标的内存空间
 pObject->pTexVerts = new CVector2 [pObject->numTexVertex];
 // 读入纹理坐标
 pPreChunk->bytesRead += fread(pObject->pTexVerts, 1, pPreChunk->length - pPreChunk->bytesRead, m_FilePointer);
}
//  读入对象的顶点
void CLoad3DS::ReadVertices(t3DObject *pObject, tChunk *pPreChunk)
{ // 在读入实际的顶点之前,首先必须确定需要读入多少个顶点。
 // 读入顶点的数目
 pPreChunk->bytesRead += fread(&(pObject->numOfVerts), 1, 2, m_FilePointer);
 // 分配顶点的存储空间,然后初始化结构体
 pObject->pVerts = new CVector3 [pObject->numOfVerts];
 memset(pObject->pVerts, 0, sizeof(CVector3) * pObject->numOfVerts);
 // 读入顶点序列
 pPreChunk->bytesRead += fread(pObject->pVerts, 1, pPreChunk->length - pPreChunk->bytesRead, m_FilePointer);
 // 现在已经读入了所有的顶点。
 // 因为3D Studio Max的模型的Z轴是指向上的,因此需要将y轴和z轴翻转过来。
 // 具体的做法是将Y轴和Z轴交换,然后将Z轴反向。
 // 遍历所有的顶点
 for(int i = 0; i < pObject->numOfVerts; i++)
 { // 保存Y轴的值
  float fTempY = pObject->pVerts[i].y;
  // 设置Y轴的值等于Z轴的值
  pObject->pVerts[i].y = pObject->pVerts[i].z;
  // 设置Z轴的值等于-Y轴的值
  pObject->pVerts[i].z = -fTempY;
 }
}
//  下面的函数读入对象的材质名称
void CLoad3DS::ReadObjMat(t3DModel *pModel, t3DObject *pObject, tChunk *pPreChunk)
{ char strMaterial[255] = {0};   // 用来保存对象的材质名称
 int buffer[50000] = {0};    // 用来读入不需要的数据
 // 材质或者是颜色,或者是对象的纹理,也可能保存了象明亮度、发光度等信息。
 // 下面读入赋予当前对象的材质名称
 pPreChunk->bytesRead += GetString(strMaterial);
 // 遍历所有的纹理
 for(int i = 0; i < pModel->numOfMaterials; i++)
 { //如果读入的纹理与当前的纹理名称匹配
  if(strcmp(strMaterial, pModel->pMaterials[i].strName) == 0)
  { // 设置材质ID
   pObject->materialID = i;
   // 判断是否是纹理映射,如果strFile是一个长度大于1的字符串,则是纹理
   if(strlen(pModel->pMaterials[i].strFile) > 0) {
    // 设置对象的纹理映射标志
    pObject->bHasTexture = true;
   } 
   break;
  }
  else
  { // 如果该对象没有材质,则设置ID为-1
   pObject->materialID = -1;
  }
 }
 pPreChunk->bytesRead += fread(buffer, 1, pPreChunk->length - pPreChunk->bytesRead, m_FilePointer);
}   
//  下面的这些函数主要用来计算顶点的法向量,顶点的法向量主要用来计算光照
// 下面的宏定义计算一个矢量的长度
#define Mag(Normal) (sqrt(Normal.x*Normal.x + Normal.y*Normal.y + Normal.z*Normal.z))
// 下面的函数求两点决定的矢量
CVector3 Vector(CVector3 vPoint1, CVector3 vPoint2)
{ CVector3 vVector;       
 vVector.x = vPoint1.x - vPoint2.x;   
 vVector.y = vPoint1.y - vPoint2.y;   
 vVector.z = vPoint1.z - vPoint2.z;   
 return vVector;        
}
// 下面的函数两个矢量相加
CVector3 AddVector(CVector3 vVector1, CVector3 vVector2)
{ CVector3 vResult;       
 vResult.x = vVector2.x + vVector1.x;  
 vResult.y = vVector2.y + vVector1.y;  
 vResult.z = vVector2.z + vVector1.z;  
 return vResult;        
}
// 下面的函数处理矢量的缩放
CVector3 DivideVectorByScaler(CVector3 vVector1, float Scaler)
{ CVector3 vResult;       
 vResult.x = vVector1.x / Scaler;   
 vResult.y = vVector1.y / Scaler;   
 vResult.z = vVector1.z / Scaler;   
 return vResult;        
}
// 下面的函数返回两个矢量的叉积
CVector3 Cross(CVector3 vVector1, CVector3 vVector2)
{ CVector3 vCross;        
 vCross.x = ((vVector1.y * vVector2.z) - (vVector1.z * vVector2.y));
 vCross.y = ((vVector1.z * vVector2.x) - (vVector1.x * vVector2.z));
 vCross.z = ((vVector1.x * vVector2.y) - (vVector1.y * vVector2.x));
 return vCross;        
}
// 下面的函数规范化矢量
CVector3 Normalize(CVector3 vNormal)
{ double Magnitude;       
 Magnitude = Mag(vNormal);     // 获得矢量的长度
 vNormal.x /= (float)Magnitude;    
 vNormal.y /= (float)Magnitude;    
 vNormal.z /= (float)Magnitude;    
 return vNormal;        
}
//  下面的函数用于计算对象的法向量
void CLoad3DS::ComputeNormals(t3DModel *pModel)
{ CVector3 vVector1, vVector2, vNormal, vPoly[3];
 // 如果模型中没有对象,则返回
 if(pModel->numOfObjects <= 0)
  return;
 // 遍历模型中所有的对象
 for(int index = 0; index < pModel->numOfObjects; index++)
 { // 获得当前的对象
  t3DObject *pObject = &(pModel->pObject[index]);
  // 分配需要的存储空间
  CVector3 *pNormals  = new CVector3 [pObject->numOfFaces];
  CVector3 *pTempNormals = new CVector3 [pObject->numOfFaces];
  pObject->pNormals  = new CVector3 [pObject->numOfVerts];
  // 遍历对象的所有面
  for(int i=0; i < pObject->numOfFaces; i++)
  { vPoly[0] = pObject->pVerts[pObject->pFaces[i].vertIndex[0]];
   vPoly[1] = pObject->pVerts[pObject->pFaces[i].vertIndex[1]];
   vPoly[2] = pObject->pVerts[pObject->pFaces[i].vertIndex[2]];
   // 计算面的法向量
   vVector1 = Vector(vPoly[0], vPoly[2]);  // 获得多边形的矢量
   vVector2 = Vector(vPoly[2], vPoly[1]);  // 获得多边形的第二个矢量
   vNormal  = Cross(vVector1, vVector2);  // 获得两个矢量的叉积
   pTempNormals[i] = vNormal;     // 保存非规范化法向量
   vNormal  = Normalize(vNormal);    // 规范化获得的叉积
   pNormals[i] = vNormal;      // 将法向量添加到法向量列表中
  }
  //  下面求顶点法向量
  CVector3 vSum = {0.0, 0.0, 0.0};
  CVector3 vZero = vSum;
  int shared=0;
  // 遍历所有的顶点
  for (i = 0; i < pObject->numOfVerts; i++)   
  { for (int j = 0; j < pObject->numOfFaces; j++) // 遍历所有的三角形面
   {            // 判断该点是否与其它的面共享
    if (pObject->pFaces[j].vertIndex[0] == i ||
     pObject->pFaces[j].vertIndex[1] == i ||
     pObject->pFaces[j].vertIndex[2] == i)
    { vSum = AddVector(vSum, pTempNormals[j]);
     shared++;        
    }
   }     
   pObject->pNormals[i] = DivideVectorByScaler(vSum, float(-shared));
   // 规范化最后的顶点法向
   pObject->pNormals[i] = Normalize(pObject->pNormals[i]); 
   vSum = vZero;        
   shared = 0;          
  }
  // 释放存储空间,开始下一个对象
  delete [] pTempNormals;
  delete [] pNormals;
 }
}
//
使用模型:
class basicpic
{
public:
 basicpic();
 virtual ~basicpic();
 GLUquadricObj *obj;
 CLoad3DS* m_3ds;
 void Scene(int obj,float size);
};
 
basicpic::basicpic()
{
 m_3ds=new CLoad3DS();
 
 m_3ds->Init("炮管1组.3DS",0);
    m_3ds->Init("炮盖1.3DS",1);
 m_3ds->Init("炮盖2.3DS",2);
 m_3ds->Init("炮盖3.3DS",3);
 m_3ds->Init("炮盖4.3DS",4);
 m_3ds->Init("导弹1.3DS",5);
 m_3ds->Init("导弹2.3DS",6);
 m_3ds->Init("导弹3.3DS",7);
 m_3ds->Init("导弹4.3DS",8);
 glEnable(GL_TEXTURE_2D);
}
void basicpic::Scene(int obj,float size)
{
 m_3ds->show3ds(obj,0,0,0,size);
}
 
到入时:用scene(模型号,大小);

 
opengl--贴图
 
这段代码用来加载位图文件。如果文件不存在,返回 NULL 告知程序无法加载位图。在我开始解释这段代码之前,关于用作纹理的图像我想有几点十分重要,并且您必须明白。此图像的宽和高必须是2的n次方;宽度和高度最小必须是64象素;并且出于兼容性的原因,图像的宽度和高度不应超过256象素。如果您的原始素材的宽度和高度不是64,128,256象素的话,使用图像处理软件重新改变图像的大小。可以肯定有办法能绕过这些限制,但现在我们只需要用标准的纹理尺寸。

首先,我们创建一个文件句柄。句柄是个用来鉴别资源的数值,它使程序能够访问此资源。我们开始先将句柄设为 NULL
AUX_RGBImageRec *LoadBMP(char *Filename) // 载入位图图象
{
FILE *File=NULL;
// 文件句柄
接下来检查文件名是否已提供。因为 LoadBMP() 可以无参数调用,所以我们不得不检查一下。您可不想什么都没载入吧.....:)
if (!Filename) // 确保文件名已提供。
{
return NULL;
// 如果没提供,返回 NULL
}
接着检查文件是否存在。下面这一行尝试打开文件。
File=fopen(Filename,"r"); //尝试打开文件
如果我们能打开文件的话,很显然文件是存在的。使用 fclose(File) 关闭文件。 auxDIBImageLoad(Filename) 读取图象数据并将其返回。
if (File) // 文件存在么?
{
fclose(File);
// 关闭句柄
return auxDIBImageLoad(Filename);
//载入位图并返回指针
}
如果我们不能打开文件,我们将返回NULL。这意味着文件无法载入。程序在后面将检查文件是否已载入。如果没有,我们将退出程序并弹出错误消息。
return NULL; // 如果载入失败,返回 NULL
}
下一部分代码载入位图(调用上面的代码)并转换成纹理。
int LoadGLTextures() // 载入位图(调用上面的代码)并转换成纹理
{
然后设置一个叫做 Status 的变量。我们使用它来跟踪是否能够载入位图以及能否创建纹理。 Status 缺省设为 FALSE (表示没有载入或创建任何东东)。
int Status=FALSE; // Status 状态指示器
现在我们创建存储位图的图像记录。次记录包含位图的宽度、高度和数据。
AUX_RGBImageRec *TextureImage[1]; // 创建纹理的存储空间
清除图像记录,确保其内容为空。
memset(TextureImage,0,sizeof(void *)*1); // 将指针设为 NULL
现在载入位图,并将其转换为纹理。 TextureImage[0]=LoadBMP("Data/NeHe.bmp") 调用 LoadBMP() 的代码。载入 Data 目录下的 NeHe.bmp 位图文件。如果一切正常,图像数据将存放在 TextureImage[0] 中, Status 被设为 TRUE ,然后我们开始创建纹理。
// 载入位图,检查有无错误,如果位图没找到则退出。
if (TextureImage[0]=LoadBMP("Data/NeHe.bmp"))
{
Status=TRUE;
// 将 Status 设为 TRUE
现在使用中 TextureImage[0] 的数据创建纹理。第一行 glGenTextures(1, &texture[0]) 告诉OpenGL我们想生成一个纹理名字(如果您想载入多个纹理,加大数字)。值得注意的是,开始我们使用 GLuint texture[1] 来创建一个纹理的存储空间,您也许会认为第一个纹理就是存放在 &texture[1] 中的,但这是错的。正确的地址应该是 &texture[0] 。同样如果使用 GLuint texture[2] 的话,第二个纹理存放在 texture[1] 中。『译者注:学C的,在这里应该没有障碍,数组就是从零开始的嘛。』

第二行 glBindTexture(GL_TEXTURE_2D, texture[0]) 告诉OpenGL将纹理名字 texture[0] 绑定到纹理目标上。2D纹理只有高度(在 Y 轴上)和宽度(在 X 轴上)。主函数将纹理名字指派给纹理数据。本例中我们告知OpenGL, &texture[0] 处的内存已经可用。我们创建的纹理将存储在 &texture[0] 的 指向的内存区域。
glGenTextures(1, &texture[0]); // 创建纹理

// 使用来自位图数据生成 的典型纹理

glBindTexture(GL_TEXTURE_2D, texture[0]);
下来我们创建真正的纹理。下面一行告诉OpenGL此纹理是一个2D纹理 ( GL_TEXTURE_2D )。数字代表图像的详细程度,通常就由它为零去了。数字是数据的成分数。因为图像是由红色数据,绿色数据,蓝色数据三种组分组成。 TextureImage[0]->sizeX 是纹理的宽度。如果您知道宽度,您可以在这里填入,但计算机可以很容易的为您指出此值。 TextureImage[0]->sizey 是纹理的高度。数字是边框的值,一般就是零。 GL_RGB 告诉OpenGL图像数据由红、绿、蓝三色数据组成。
GL_UNSIGNED_BYTE 意味着组成图像的数据是无符号字节类型的。最后... TextureImage[0]->data 告诉OpenGL纹理数据的来源。此例中指向存放在 TextureImage[0] 记录中的数据。
// 生成纹理
glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[0]->sizeX, TextureImage[0]->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[0]->data);
下面的两行告诉OpenGL在显示图像时,当它比放大得原始的纹理大 ( GL_TEXTURE_MAG_FILTER )或缩小得比原始得纹理小( GL_TEXTURE_MIN_FILTER )时OpenGL采用的滤波方式。通常这两种情况下我都采用 GL_LINEAR 。这使得纹理从很远处到离屏幕很近时都平滑显示。使用 GL_LINEAR 需要CPU和显卡做更多的运算。如果您的机器很慢,您也许应该采用 GL_NEAREST 。过滤的纹理在放大的时候,看起来斑驳的很『译者注:马赛克啦』。您也可以结合这两种滤波方式。在近处时使用 GL_LINEAR ,远处时 GL_NEAREST
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR); // 线形滤波
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
// 线形滤波
}
现在我们释放前面用来存放位图数据的内存。我们先查看位图数据是否存放在处。如果是的话,再查看数据是否已经存储。如果已经存储的话,删了它。接着再释放 TextureImage[0] 图像结构以保证所有的内存都能释放。
if (TextureImage[0]) // 纹理是否存在
{
if (TextureImage[0]->data)
// 纹理图像是否存在
{
free(TextureImage[0]->data);
// 释放纹理图像占用的内存
}

free(TextureImage[0]);
// 释放图像结构
}
最后返回状态变量。如果一切OK,变量 Status 的值为 TRUE 。否则为 FALSE

return Status; // 返回 Status
}

 

-----------------------------------------------------------

int InitGL(GLvoid) // 此处开始对OpenGL进行所有设置
{
if (!LoadGLTextures())
// 调用纹理载入子例程( 新增 )
{
return FALSE;
// 如果未能载入,返回FALSE( 新增 )
}

glEnable(GL_TEXTURE_2D);
// 启用纹理映射( 新增 )

.............

---------------------------------------------------------------------------------------------

下一行代码选择我们使用的纹理。如果您在您的场景中使用多个纹理,您应该使用来 glBindTexture(GL_TEXTURE_2D, texture[ 所使用纹理对应的数字 ]) 选择要绑定的纹理。当您想改变纹理时,应该绑定新的纹理。有一点值得指出的是,您不能 glBegin() glEnd() 之间绑定纹理,必须在 glBegin() 之前或 glEnd() 之后绑定。注意我们在后面是如何使用 glBindTexture 来指定和绑定纹理的。
glBindTexture(GL_TEXTURE_2D, texture[0]); // 选择纹理
为了将纹理正确的映射到四边形上,您必须将纹理的右上角映射到四边形的右上角,纹理的左上角映射到四边形的左上角,纹理的右下角映射到四边形的右下角,纹理的左下角映射到四边形的左下角。如果映射错误的话,图像显示时可能上下颠倒,侧向一边或者什么都不是。

glTexCoord2f 的第一个参数是X坐标。 0.0f 是纹理的左侧。 0.5f 是纹理的中点, 1.0f 是纹理的右侧。 glTexCoord2f 的第二个参数是Y坐标。 0.0f 是纹理的底部。 0.5f 是纹理的中点, 1.0f 是纹理的顶部。

所以纹理的左上坐标是 X:0.0f,Y:1.0f ,四边形的左上顶点是 X: -1.0f,Y:1.0f 。其余三点依此类推。

试着玩玩 glTexCoord2f 的X,Y坐标参数。把 1.0f 改为 0.5f 将只显示纹理的左半部分,把 0.0f 改为 0.5f 将只显示纹理的右半部分。
glBegin(GL_QUADS);
// 前面
glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f); // 纹理和四边形的左下
glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f);
// 纹理和四边形的右下
glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 1.0f);
// 纹理和四边形的右上
glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 1.0f);
// 纹理和四边形的左上
// 后面
glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, -1.0f);
// 纹理和四边形的右下
glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f);
// 纹理和四边形的右上
glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, 1.0f, -1.0f);
// 纹理和四边形的左上
glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, -1.0f);
// 纹理和四边形的左下
// 顶面
glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f);
// 纹理和四边形的左上
glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, 1.0f, 1.0f);
// 纹理和四边形的左下
glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, 1.0f, 1.0f);
// 纹理和四边形的右下
glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, -1.0f); 
// 纹理和四边形的右上
// 底面
glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, -1.0f, -1.0f);
// 纹理和四边形的右上
glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, -1.0f, -1.0f);
// 纹理和四边形的左上
glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f);
// 纹理和四边形的左下
glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f);
// 纹理和四边形的右下
// 右面
glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, -1.0f, -1.0f);
// 纹理和四边形的右下
glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, -1.0f);
// 纹理和四边形的右上
glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 1.0f);
// 纹理和四边形的左上
glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f);
// 纹理和四边形的左下
// 左面
glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, -1.0f, -1.0f);
// 纹理和四边形的左下
glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f);
// 纹理和四边形的右下
glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 1.0f);
// 纹理和四边形的右上
glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f);
// 纹理和四边形的左上
glEnd();


opengl---设置光源
接着设置用来创建光源的数组。我们将使用两种不同的光。第一种称为环境光。环境光来自于四面八方。所有场景中的对象都处于环境光的照射中。第二种类型的光源叫做漫射光。漫射光由特定的光源产生,并在您的场景中的对象表面上产生反射。处于漫射光直接照射下的任何对象表面都变得很亮,而几乎未被照射到的区域就显得要暗一些。这样在我们所创建的木板箱的棱边上就会产生的很不错的阴影效果。
创建光源的过程和颜色的创建完全一致。前三个参数分别是RGB三色分量,最后一个是alpha通道参数。
因此,下面的代码我们得到的是半亮(0.5f)的白色环境光。如果没有环境光,未被漫射光照到的地方会变得十分黑暗。
GLfloat LightAmbient[]= { 0.5f, 0.5f, 0.5f, 1.0f }; //环境光参数 ( 新增 )
下一行代码我们生成最亮的漫射光。所有的参数值都取成最大值1.0f。它将照在我们木板箱的前面,看起来挺好。
GLfloat LightDiffuse[]= { 1.0f, 1.0f, 1.0f, 1.0f }; // 漫射光参数 ( 新增 )
最后我们保存光源的位置。前三个参数和glTranslate中的一样。依次分别是XYZ轴上的位移。由于我们想要光线直接照射在木箱的正面,所以XY轴上的位移都是0.0f。第三个值是Z轴上的位移。为了保证光线总在木箱的前面,所以我们将光源的位置朝着观察者(就是您哪。)挪出屏幕。我们通常将屏幕也就是显示器的屏幕玻璃所处的位置称作Z轴的0.0f点。所以Z轴上的位移最后定为2.0f。假如您能够看见光源的话,它就浮在您显示器的前方。当然,如果木箱不在显示器的屏幕玻璃后面的话,您也无法看见箱子。『译者注:我很欣赏NeHe的耐心。说真的有时我都打烦了,这么简单的事他这么废话干嘛?但如果什么都清楚,您还会翻着这样的页面看个没完么?』
最后一个参数取为1.0f。这将告诉OpenGL这里指定的坐标就是光源的位置,以后的教程中我会多加解释。

GLfloat LightPosition[]= { 0.0f, 0.0f, 2.0f, 1.0f }; // 光源位置

现在开始设置光源。下面下面一行设置环境光的发光量,光源light1开始发光。这一课的开始处我们我们将环境光的发光量存放在LightAmbient数组中。现在我们就使用此数组(半亮度环境光)。
glLightfv(GL_LIGHT1, GL_AMBIENT, LightAmbient); // 设置环境光
接下来我们设置漫射光的发光量。它存放在LightDiffuse数组中(全亮度白光)。
glLightfv(GL_LIGHT1, GL_DIFFUSE, LightDiffuse); // 设置漫射光
然后设置光源的位置。位置存放在 LightPosition 数组中(正好位于木箱前面的中心,X-0.0f,Y-0.0f,Z方向移向观察者2个单位<位于屏幕外面>)。
glLightfv(GL_LIGHT1, GL_POSITION,LightPosition); // 光源位置
最后,我们启用一号光源。我们还没有启用GL_LIGHTING,所以您看不见任何光线。记住:只对光源进行设置、定位、甚至启用,光源都不会工作。除非我们启用GL_LIGHTING。

glEnable(GL_LIGHT1); // 启用一号光源

 

until:glEnable(GL_LIGHTING); //启用光源

不想用的时候:glDisable(GL_LIGHTING); //禁用光源


 

 
OpenGL--着色透明
glColor3f(r,g,b)。括号中的三个参数依次是红、绿、蓝三色分量。取值范围可以从0,0f到1.0f。
OpenGL中的绝大多数特效都与某些类型的 (色彩 )混合有关。混色的定义为,将某个象素的颜色和已绘制在屏幕上与其对应的象素颜色相互结合。至于如何结合这两个颜色则依赖于颜色的 alpha通道的分量值,以及 /或者所使用的混色函数。 Alpha通常是位于颜色值末尾的第 4个颜色组成分量。前面这些课我们都是用 GL_RGB来指定颜色的三个分量。相应的 GL_RGBA可以指定 alpha分量的值。更进一步,我们可以使用 glColor4f()来代替 glColor3f()
 
透明:
先声明:OpenGL::init():
glColor4f(1.0f,1.0f,1.0f,0.5f);   // 全亮度,50% Alpha 混合( 新增) 
glBlendFunc(GL_SRC_ALPHA,GL_ONE);  // 基于源象素alpha通道值的半透明混合函数( 新增)
 
使用的时候:
if(flag_bp == true)
 {
  glEnable(GL_BLEND);   // Turn Blending On
     glDisable(GL_DEPTH_TEST); // Turn Depth Testing Off
 }

 Obj->Scene(0,1.0);//3dmax载入模型
 glPopMatrix();
 glDisable(GL_BLEND);  // Turn Blending Off
 glEnable(GL_DEPTH_TEST); // Turn Depth Testing On
 
ps: flag_bp is true,Turn Blending On.or off.
 
opengl---平移旋转
int DrawGLScene(GLvoid) // 此过程中包括所有的绘制代码
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 清除屏幕及深度缓存

glLoadIdentity();
// 重置视口
当您调用glLoadIdentity()之后,您实际上讲当前点移到了屏幕中心,X坐标轴从左至右,Y坐标轴从下至上,Z坐标轴从里至外。OpenGL屏幕中心的坐标值是X和Y轴上的0.0f点。中心左面的坐标值是负值,右面是正值。移向屏幕顶端是正值,移向屏幕底端是负值。移入屏幕深处是负值,移出屏幕则是正值。

glTranslatef(x, y, z)沿着 X, Y 和 Z 轴移动。根据前面的次序,下面的代码沿着X轴左移1.5个单位,Y轴不动(0.0f),最后移入屏幕6.0f个单位。注意在glTranslatef(x, y, z)中当您移动的时候,您并不是相对屏幕中心移动,而是相对与当前所在的屏幕位置
glTranslatef(-1.5f,0.0f,-6.0f); // 左移 1.5 单位,并移入屏幕 6.0
现在我们已经移到了屏幕的左半部分,并且将视图推入屏幕背后足够的距离以便我们可以看见全部的场景-创建三角形。glBegin(GL_TRIANGLES)的意思是开始绘制三角形,glEnd() 告诉OpenGL三角形已经创建好了。通常您会需要画3个顶点,可以使用GL_TRIANGLES。在绝大多数的显卡上,绘制三角形是相当快速的。如果要画四个顶点,使用GL_QUADS的话会更方便。但据我所知,绝大多数的显卡都使用三角形来为对象着色。最后,如果您想要画更多的顶点时,可以使用GL_POLYGON。

本节的简单示例中,我们只画一个三角形。如果要画第二个三角形的话,可以在这三点之后,再加三行代码(3点)。所有六点代码都应包含在glBegin(GL_TRIANGLES) 和 glEnd()之间。在他们之间再不会有多余的点出现,也就是说,(GL_TRIANGLES) 和 glEnd()之间的点都是以三点为一个集合的。这同样适用于四边形。如果您知道实在绘制四边形的话,您必须在第一个四点之后,再加上四点为一个集合的点组。另一方面,多边形可以由任意个顶点,(GL_POLYGON)不在乎glBegin(GL_TRIANGLES) 和 glEnd()之间有多少行代码。

glBegin之后的第一行设置了多边形的第一个顶点,glVertex 的第一个参数是X坐标,然后依次是Y坐标和Z坐标。第一个点是上顶点,然后是左下顶点和右下顶点。glEnd()告诉OpenGL没有其他点了。这样将显示一个填充的三角形。


{译者:这里要注意的是存在两种不同的坐标变换方式,glTranslatef(x, y, z)中的x, y, z是相对与您当前所在点的位移,但glVertex(x,y,z)是相对于glTranslatef(x, y, z)移动后的新原点的位移。因而这里可以认为glTranslate移动的是坐标原点,glVertex中的点是相对最新的坐标原点的坐标值。}
glRotatef(Angle,Xvector,Yvector,Zvector)负责让对象绕某个轴旋转。这个命令有很多用处。 Angle 通常是个变量代表对象转过的角度。 Xvector , Yvector Zvector 三个参数则共同决定旋转轴的方向。比如(1,0,0)所描述的矢量经过X坐标轴的1个单位处并且方向向右。(-1,0,0)所描述的矢量经过X坐标轴的1个单位处,但方向向左。
D. Michael Traub:提供了对 Xvector , Yvector Zvector 的上述解释。
为了更好的理解X, Y 和 Z的旋转,我举些例子...

X轴-您正在使用一台台锯。锯片中心的轴从左至右摆放(就像OpenGL中的X轴)。尖利的锯齿绕着X轴狂转,看起来要么向上转,要么向下转。取决于锯片开始转时的方向。这与我们在OpenGL中绕着X轴旋转什么的情形是一样的。(译者注:这会儿您要把脸蛋凑向显示器的话,保准被锯开了花 ^-^。)

Y轴-假设您正处于一个巨大的龙卷风中心,龙卷风的中心从地面指向天空(就像OpenGL中的Y轴)。垃圾和碎片围着Y轴从左向右或是从右向左狂转不止。这与我们在OpenGL中绕着Y轴旋转什么的情形是一样的。

Z轴-您从正前方看着一台风扇。风扇的中心正好朝着您(就像OpenGL中的Z轴)。风扇的叶片绕着Z轴顺时针或逆时针狂转。这与我们在OpenGL中绕着Z轴旋转什么的情形是一样的。

OpenGL--我的编码框架

// stdafx.h : include file for standard system include files,
//  or project specific include files that are used frequently, but
//      are changed infrequently
//

#if !defined(AFX_STDAFX_H__A9DB83DB_A9FD_11D0_BFD1_444553540000__INCLUDED_)
#define AFX_STDAFX_H__A9DB83DB_A9FD_11D0_BFD1_444553540000__INCLUDED_

#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000

#define WIN32_LEAN_AND_MEAN  // Exclude rarely-used stuff from Windows headers

#include  // Windows的头文件

#include
#include
#include
#include
#include
#include   // OpenGL32库的头文件
#include   // GLu32库的头文件
#include  // GLaux库的头文件

#pragma comment( lib, "winmm.lib")
#pragma comment( lib, "opengl32.lib") // OpenGL32连接库
#pragma comment( lib, "glu32.lib")  // GLu32连接库
#pragma comment( lib, "glaux.lib")  // GLaux连接库


// TODO: reference additional headers your program requires here

//{{AFX_INSERT_LOCATION}}
// Microsoft Visual C++ will insert additional declarations immediately before the previous line.

#endif // !defined(AFX_STDAFX_H__A9DB83DB_A9FD_11D0_BFD1_444553540000__INCLUDED_)

----------------------------------------------------------------------------------

// stdafx.cpp : source file that includes just the standard includes
// OpenGL的基本图形.pch will be the pre-compiled header
// stdafx.obj will contain the pre-compiled type information

#include "stdafx.h"

// TODO: reference any additional headers you need in STDAFX.H
// and not in this file
----------------------------------------------------------------------------------------
#include "stdafx.h"
#include "OpenGL.h"
//
OpenGL* m_OpenGL;
HDC  hDC;  // GDI设备句柄,将窗口连接到 GDI( 图形设备接口)
HGLRC hRC=NULL; // 渲染描述句柄,将OpenGL调用连接到设备描述表
HWND hWnd=NULL; // 保存 Windows 分配给程序的窗口句柄
int  Width = 800;// 窗口宽
int  Height= 600;// 窗口高
int  bits  = 16; // 颜色深度

void GameLoop()
{   MSG msg;
    BOOL fMessage;
    PeekMessage(&msg, NULL, 0U, 0U, PM_NOREMOVE);
    while(msg.message != WM_QUIT) // 消息循环
    {   fMessage = PeekMessage(&msg, NULL, 0U, 0U, PM_REMOVE);
        if(fMessage)    //有消息
   { TranslateMessage(&msg);
              DispatchMessage(&msg);
   }
        else  m_OpenGL->Render(); //无消息
    }
}
LRESULT WINAPI MsgProc(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam )// 消息处理
{ switch(message)
 { case WM_CREATE:      // 建立窗口
   hDC = GetDC(hWnd);    // 获取当前窗口的设备句柄
   m_OpenGL->SetupPixelFormat(hDC);// 调用显示模式安装功能
   return 0;  break;
  case WM_CLOSE:      // 关闭窗口
   m_OpenGL->CleanUp();   // 结束处理
   PostQuitMessage(0);
   return 0;  break;
  case WM_SIZE:      // 窗口尺寸变化
   Height = HIWORD(lParam);  // 窗口的高
   Width  = LOWORD(lParam);  // 窗口的宽
   if (Height==0) Height=1;  // 防止被0 除
   m_OpenGL->init(Width,Height);
   return 0;  break;
  case WM_DESTROY:     // 退出消息
            PostQuitMessage(0);
            return 0;  break;
        case WM_KEYUP:      // 按ESC退出,全屏模式必需要加入的退出方式。
            switch (wParam)
            { case VK_ESCAPE:
     m_OpenGL->CleanUp(); // 结束处理
        PostQuitMessage(0);
        return 0;break;
            }
  default:   break;
 }
 return (DefWindowProc(hWnd, message, wParam, lParam));
}
INT WINAPI WinMain(HINSTANCE hInst,HINSTANCE,LPSTR,INT )// WinMain程序入口
{   // 注册窗口类
 bool fullScreen =TRUE;
 DWORD dwExStyle;  // Window 扩展风格
 DWORD dwStyle;  // Window 窗口风格
 RECT windowRect;  // 窗口尺寸
 int  nX=0,nY=0;
/* 
 if (MessageBox(NULL,"使用全屏模式吗?", "将进入OpenGL,选择显示模式",
             MB_YESNO|MB_ICONQUESTION|MB_SYSTEMMODAL)==IDNO)
  {fullScreen =false;}   // 选择窗口模式
 if (fullScreen)      // 选择全屏模式
 { DEVMODE dmScr;     // 设备模式
  memset(&dmScr,0,sizeof(dmScr)); // 确保内存分配
  dmScr.dmSize=sizeof(dmScr);  // Devmode 结构的大小
  dmScr.dmPelsWidth = Width;  // 屏幕宽
  dmScr.dmPelsHeight= Height;  // 屏幕高
  dmScr.dmBitsPerPel= 16;   // 色彩深度
  dmScr.dmDisplayFrequency=75; // 刷屏速度
  dmScr.dmFields=DM_BITSPERPEL|DM_PELSWIDTH|DM_PELSHEIGHT|DM_DISPLAYFREQUENCY;
  if (ChangeDisplaySettings(&dmScr, CDS_FULLSCREEN) != DISP_CHANGE_SUCCESSFUL)
   {fullScreen=FALSE;}
  dwExStyle=WS_EX_APPWINDOW;  // Window 扩展风格
  dwStyle=WS_POPUP;    // Window 窗口风格
  ShowCursor(FALSE);    // 隐藏鼠标
 }
 else*/
 { dwExStyle=WS_EX_APPWINDOW|WS_EX_WINDOWEDGE; // 使窗口具有3D外观
  dwStyle=WS_OVERLAPPEDWINDOW;    // 使用标准窗口
  //WS_OVERLAPPEDWINDOW是有标题栏,窗口菜单,最大、小化按钮和可调整尺寸的窗口
  int wid=GetSystemMetrics(SM_CXSCREEN);  // 获取当前屏幕宽
  int hei=GetSystemMetrics(SM_CYSCREEN);  // 获取当前屏幕高
  nX=(wid-Width)/2;nY=(hei-Height)/2;   // 计算窗口居中用
 }
//-------------------------------------------------------------------
 AdjustWindowRectEx(&windowRect,dwStyle,FALSE,dwExStyle);
         //根据窗口风格来调整窗口尺寸达到要求的大小
 char cc[]="tml";
    WNDCLASSEX wc = { sizeof(WNDCLASSEX), CS_CLASSDC, MsgProc, 0L, 0L,
                      GetModuleHandle(NULL), NULL, NULL, NULL, NULL,
                      cc, NULL };
    RegisterClassEx( &wc );
 m_OpenGL=new OpenGL();//
 hWnd = CreateWindowEx(NULL,cc,"学OpenGL编3D游戏 [ 2.OpenGL的基本图形 ])",
        dwStyle|WS_CLIPCHILDREN|WS_CLIPSIBLINGS,
        nX, nY,Width, Height,
        NULL,NULL,hInst,NULL); // 创建窗口
 ShowWindow( hWnd, SW_SHOWDEFAULT );    // 显示窗口
 UpdateWindow( hWnd );       // 刷新窗口
 GameLoop();          // 进入消息循环
    return 0;
}

--------------------------------------------------------------------------------------------------------

// OpenGL.h: interface for the OpenGL class.
//
//

#if !defined(AFX_OPENGL_H__17B7289C_7956_41C5_89B9_621E3C435389__INCLUDED_)
#define AFX_OPENGL_H__17B7289C_7956_41C5_89B9_621E3C435389__INCLUDED_

#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000
#include "bsipic.h"
class OpenGL 
{ public: OpenGL();
 virtual ~OpenGL();
 public:
 bsipic  m_bsipic; // 定义bsipic类变量
 HDC  hDC;  // GDI设备描述表
 HGLRC hRC;  // 永久着色描述表
 BOOL SetupPixelFormat(HDC hDC);
 void init(int Width, int Height);
 void Render();
 void CleanUp();
 void play();
};

#endif // !defined(AFX_OPENGL_H__17B7289C_7956_41C5_89B9_621E3C435389__INCLUDED_)

------------------------------------------------------------------------------------------------------------

// OpenGL.cpp: implementation of the OpenGL class.
//
#include "stdafx.h"
#include "OpenGL.h"
//
#include "stdafx.h"
#include "OpenGL.h"
//
extern HWND hWnd;
float r;
//
OpenGL::OpenGL()
{
}
OpenGL::~OpenGL()
{ CleanUp();
}
BOOL OpenGL::SetupPixelFormat(HDC hDC0)//检测安装OpenGL
{ int nPixelFormat;       // 象素点格式
 hDC=hDC0;
 PIXELFORMATDESCRIPTOR pfd = {
     sizeof(PIXELFORMATDESCRIPTOR),    // pfd结构的大小
     1,                                // 版本号
     PFD_DRAW_TO_WINDOW |              // 支持在窗口中绘图
     PFD_SUPPORT_OPENGL |              // 支持 OpenGL
     PFD_DOUBLEBUFFER,                 // 双缓存模式
     PFD_TYPE_RGBA,                    // RGBA 颜色模式
     16,                               // 24 位颜色深度
     0, 0, 0, 0, 0, 0,                 // 忽略颜色位
     0,                                // 没有非透明度缓存
     0,                                // 忽略移位位
     0,                                // 无累加缓存
     0, 0, 0, 0,                       // 忽略累加位
     16,                               // 32 位深度缓存    
     0,                                // 无模板缓存
     0,                                // 无辅助缓存
     PFD_MAIN_PLANE,                   // 主层
     0,                                // 保留
     0, 0, 0                           // 忽略层,可见性和损毁掩模
 };
 if (!(nPixelFormat = ChoosePixelFormat(hDC, &pfd)))
  { MessageBox(NULL,"没找到合适的显示模式","Error",MB_OK|MB_ICONEXCLAMATION);
       return FALSE;
  }
 SetPixelFormat(hDC,nPixelFormat,&pfd);//设置当前设备的像素点格式
 hRC = wglCreateContext(hDC);          //获取渲染描述句柄
 wglMakeCurrent(hDC, hRC);             //激活渲染描述句柄

 return TRUE;
}
void OpenGL::init(int Width, int Height)
{ glViewport(0,0,Width,Height);   // 设置OpenGL视口大小。 
 glMatrixMode(GL_PROJECTION);   // 设置当前矩阵为投影矩阵。
 glLoadIdentity();      // 重置当前指定的矩阵为单位矩阵
 gluPerspective       // 设置透视图
  ( 54.0f,       // 透视角设置为 45 度
    (GLfloat)Width/(GLfloat)Height, // 窗口的宽与高比
    0.1f,        // 视野透视深度:近点1.0f
    3000.0f       // 视野透视深度:始点0.1f远点1000.0f
  );
 // 这和照象机很类似,第一个参数设置镜头广角度,第二个参数是长宽比,后面是远近剪切。
 glMatrixMode(GL_MODELVIEW);    // 设置当前矩阵为模型视图矩阵
 glLoadIdentity();      // 重置当前指定的矩阵为单位矩阵
//====================================================
}
void OpenGL::Render()//OpenGL图形处理
{ glClearColor(0.0f, 0.0f, 0.6f, 1.0f);    // 设置刷新背景色
 glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);// 刷新背景
 glLoadIdentity();         // 重置当前的模型观察矩阵
 play();
 glFlush();           // 更新窗口
 SwapBuffers(hDC);         // 切换缓冲区
 r+=1;if(r>360) r=0;
}
void OpenGL::CleanUp()
{  wglMakeCurrent(hDC, NULL);                       //清除OpenGL
  wglDeleteContext(hRC);                           //清除OpenGL
}
void OpenGL::play()
{ glPushMatrix();
 glPointSize(4); 
 glTranslatef (-5, 4,-13);glRotatef(r,1.0,1.0,1.0);
 glColor3f(1.0f, 0.0f, 0.0f);m_bsipic.Point(); 
 glPopMatrix();
 glPushMatrix();
 glTranslatef ( 0, 4,-13);glRotatef(r,1.0,1.0,1.0);
 glColor3f(0.0f, 1.0f, 0.0f);m_bsipic.Line(); 
 glPopMatrix();
 glPushMatrix();
 glTranslatef ( 5, 4,-13);glRotatef(r,1.0,1.0,1.0);
 glColor3f(0.0f, 0.0f, 1.0f);m_bsipic.Triangle();
 glPopMatrix();
 glPushMatrix();
 glTranslatef (-5, 0,-13);glRotatef(r,1.0,1.0,1.0);
 glColor3f(1.0f, 1.0f, 0.0f);m_bsipic.Square();
 glPopMatrix();
 glPushMatrix();
 glTranslatef ( 0, 0,-13);glRotatef(r,1.0,1.0,1.0);
 glColor3f(0.0f, 1.0f, 1.0f);m_bsipic.Esquare(); 
 glPopMatrix();
 glPushMatrix();
 glTranslatef ( 5, 0,-13);glRotatef(r,1.0,1.0,1.0);
 glColor3f(1.0f, 0.0f, 1.0f);m_bsipic.Park();
 glPopMatrix();
 glPushMatrix();
 glTranslatef (-5,-4,-13);glRotatef(r,1.0,1.0,1.0);
 glColor3f(1.0f, 1.0f, 1.0f);m_bsipic.Pillar(); 
 glPopMatrix();
 glPushMatrix();
 glTranslatef ( 0, -4,-13);glRotatef(r,1.0,1.0,1.0);
 glColor3f(0.7f, 0.7f, 0.7f);auxSolidCone(1,1);
 glPopMatrix();
 glPushMatrix();
 glTranslatef ( 5,-4,-13);glRotatef(r,1.0,1.0,1.0);
 glColor3f(0.4f, 0.4f, 0.4f);auxWireTeapot(1);
 glPopMatrix();
}

----------------------------------------------------------------------------------------------------

// bsipic.h: interface for the bsipic class.
//
//

#if !defined(AFX_BSIPIC_H__726BAD2E_C0FE_4F4C_8282_BD53CCED1A9D__INCLUDED_)
#define AFX_BSIPIC_H__726BAD2E_C0FE_4F4C_8282_BD53CCED1A9D__INCLUDED_

#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000

class bsipic 
{
public:
 bsipic();
 virtual ~bsipic();
 void Point();
 void Line();
 void Triangle(); 
 void Square();
 void Esquare();
 void Park(); 
 void Pillar();
};

#endif // !defined(AFX_BSIPIC_H__726BAD2E_C0FE_4F4C_8282_BD53CCED1A9D__INCLUDED_)

------------------------------------------------------------------------------------------------------------

// bsipic.cpp: implementation of the bsipic class.

//

#include "stdafx.h"
#include "bsipic.h"

//
bsipic::bsipic()
{

}

bsipic::~bsipic()
{

}
///
void bsipic::Point()//画点
{ glBegin(GL_POINTS);//
   glVertex3f( 0.0f, 1.0f,-1.0f);//a点
   glVertex3f(-1.0f,-1.0f, 0.0f);//b点
   glVertex3f( 1.0f,-1.0f, 0.0f);//c点
  glEnd();
}
void bsipic::Line()//画线
{ glBegin(GL_LINE_LOOP); //
   glVertex3f( 0.0f, 1.0f,-1.0f);//a点
   glVertex3f(-1.0f,-1.0f, 0.0f);//b点
   glVertex3f( 1.0f,-1.0f, 0.0f);//c点
  glEnd();
}
void bsipic::Triangle()//画面
{ glBegin(GL_POLYGON);//
 glVertex3f( 0.0f, 1.0f,-1.0f);//a点
 glVertex3f(-1.0f,-1.0f, 0.0f);//b点
 glVertex3f( 1.0f,-1.0f, 0.0f);//c点
  glEnd();
}
void bsipic::Square()//画正方面
{ glBegin(GL_POLYGON);//
 glVertex3f(0.0f,0.0f ,0.0f);//a点
 glVertex3f(1.0f,0.0f, 0.0f);//b点
 glVertex3f(1.0f,0.0f,-1.0f);//c点
 glVertex3f(0.0f,0.0f,-1.0f);//d点
  glEnd();
}
void bsipic::Esquare()//画正方体
{ glBegin(GL_QUAD_STRIP);//
    glVertex3f(0.0f,0.0f ,0.0f);//a0点
    glVertex3f(0.0f,1.0f ,0.0f);//a1点
    glVertex3f(1.0f,0.0f, 0.0f);//b0点
    glVertex3f(1.0f,1.0f, 0.0f);//b1点
    glVertex3f(1.0f,0.0f,-1.0f);//c0点
    glVertex3f(1.0f,1.0f,-1.0f);//c1点
    glVertex3f(0.0f,0.0f,-1.0f);//d0点
    glVertex3f(0.0f,1.0f,-1.0f);//d1点
    glVertex3f(0.0f,0.0f ,0.0f);//a0点
    glVertex3f(0.0f,1.0f ,0.0f);//a1点
  glEnd();

  glBegin(GL_POLYGON);//
 glVertex3f(0.0f,0.0f ,0.0f);//a0点
 glVertex3f(1.0f,0.0f, 0.0f);//b0点
 glVertex3f(1.0f,0.0f,-1.0f);//c0点
 glVertex3f(0.0f,0.0f,-1.0f);//d0点
 glVertex3f(0.0f,1.0f ,0.0f);//a1点
 glVertex3f(1.0f,1.0f, 0.0f);//b1点
 glVertex3f(1.0f,1.0f,-1.0f);//c1点
 glVertex3f(0.0f,1.0f,-1.0f);//d1点
  glEnd();
}
void bsipic::Park ()//画园
{ glBegin(GL_TRIANGLE_FAN);//
   glVertex3f(0,0,0.0f );  
   for(int i=0;i<=390;i+=30)
   {float p=(float)(i*3.14/180);
    glVertex3f((float)sin(p),(float)cos(p),0.0f );
   }
  glEnd();
}
void bsipic::Pillar () //园柱
{glBegin(GL_QUAD_STRIP);//
   for(int i=0;i<=390;i+=30)
   { float p=(float)(i*3.14/180);
 glVertex3f((float)sin(p)/2,(float)cos(p)/2,1.0f );
 glVertex3f((float)sin(p)/2,(float)cos(p)/2,0.0f );
   }
 glEnd();
}

写一些OPENGL-新手上路
我们要画出一个图形首先要做的:(openGL编码的框架)
代码的前4行包括了我们使用的每个库文件的头文件。如下所示:
#include
#include
#include
#include
// Windows的头文件
// OpenGL32库的头文件
// GLu32库的头文件
// GLaux库的头文件
接下来您需要设置您计划在您的程序中使用的所有变量。本节中的例程将创建一个空的OpenGL窗口,因此我们暂时还无需设置大堆的变量。余下需要设置的变量不多,但十分重要。您将会在您以后所写的每一个OpenGL程序中用到它们。
第一行设置的变量是Rendering Context(着色描述表)。每一个OpenGL都被连接到一个着色描述表上。着色描述表将所有的OpenGL调用命令连接到Device Context(设备描述表)上。我将OpenGL的着色描述表定义为 hRC 。要让您的程序能够绘制窗口的话,还需要创建一个设备描述表,也就是第二行的内容。Windows的设备描述表被定义为 hDC 。DC将窗口连接到GDI(Graphics Device Interface图形设备接口)。而RC将OpenGL连接到DC。第三行的变量 hWnd 将保存由Windows给我们的窗口指派的句柄。最后,第四行为我们的程序创建了一个Instance(实例)。
HGLRC hRC=NULL;
HDC hDC=NULL;
HWND hWnd=NULL;
HINSTANCE hInstance;
// 永久着色描述表
// 私有GDI设备描述表
// 保存我们的窗口句柄
// 保存程序的实例
下面的第一行设置一个用来监控键盘动作的数组。有许多方法可以监控键盘的动作,但这里的方法很可靠,并且可以处理多个键同时按下的情况。

active 变量用来告知程序窗口是否处于最小化的状态。如果窗口已经最小化的话,我们可以做从暂停代码执行到退出程序的任何事情。我喜欢暂停程序。这样可以使得程序不用在后台保持运行。

fullscreen 变量的作用相当明显。如果我们的程序在全屏状态下运行, fullscreen 的值为TRUE,否则为FALSE。这个全局变量的设置十分重要,它让每个过程都知道程序是否运行在全屏状态下。
bool keys[256];
bool active=TRUE;
bool fullscreen=TRUE;
// 用于键盘例程的数组
// 窗口的活动标志,缺省为TRUE
// 全屏标志缺省设定成全屏模式
现在我们需要先定义WndProc()。必须这么做的原因是CreateGLWindow()有对WndProc()的引用,但WndProc()在CreateGLWindow()之后才出现。在C语言中,如果我们想要访问一个当前程序段之后的过程和程序段的话,必须在程序开始处先申明所要访问的程序段。所以下面的一行代码先行定义了WndProc(),使得CreateGLWindow()能够引用WndProc()。
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); // WndProc的定义
 
下面的代码的作用是重新设置OpenGL场景的大小,而不管窗口的大小是否已经改变(假定您没有使用全屏模式)。甚至您无法改变窗口的大小时(例如您在全屏模式下),它至少仍将运行一次--在程序开始时设置我们的透视图。OpenGL场景的尺寸将被设置成它显示时所在窗口的大小。
GLvoid ReSizeGLScene(GLsizei width, GLsizei height)
{
if (height==0)
{
height=1;
}
glViewport(0, 0, width, height);
// 重置并初始化GL窗口大小

// 防止被零除

// 将Height设为1

// 重置当前的视口(Viewport)
下面几行为透视图设置屏幕。意味着越远的东西看起来越小。这么做创建了一个现实外观的场景。此处透视按照基于窗口宽度和高度的45度视角来计算。0.1f,100.0f是我们在场景中所能绘制深度的起点和终点。

glMatrixMode(GL_PROJECTION)指明接下来的两行代码将影响projection matrix(投影矩阵)。投影矩阵负责为我们的场景增加透视。 glLoadIdentity()近似于重置。它将所选的矩阵状态恢复成其原始状态。调用 glLoadIdentity()之后我们为场景设置透视图。
glMatrixMode(GL_MODELVIEW)指明任何新的变换将会影响 modelview matrix(模型观察矩阵)。模型观察矩阵中存放了我们的物体讯息。最后我们重置模型观察矩阵。如果您还不能理解这些术语的含义,请别着急。在以后的教程里,我会向大家解释。只要知道如果您想获得一个精彩的透视场景的话,必须这么做。
glMatrixMode(GL_PROJECTION);
glLoadIdentity();

// 计算窗口的外观比例
gluPerspective(45.0f,(GLfloat)width/(GLfloat)height,0.1f,100.0f);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
}
// 选择投影矩阵
// 重置投影矩阵


// 选择模型观察矩阵
// 重置模型观察矩阵

 
接下的代码段中,我们将对OpenGL进行所有的设置。我们将设置清除屏幕所用的颜色,打开深度缓存,启用smooth shading(阴影平滑),等等。这个例程直到OpenGL窗口创建之后才会被调用。此过程将有返回值。但我们此处的初始化没那么复杂,现在还用不着担心这个返回值。
int InitGL(GLvoid)
{
// 此处开始对OpenGL进行所有设置
 
下一行启用smooth shading(阴影平滑)。阴影平滑通过多边形精细的混合色彩,并对外部光进行平滑。我将在另一个教程中更详细的解释阴影平滑。
glShadeModel(GL_SMOOTH); // 启用阴影平滑
下一行设置清除屏幕时所用的颜色。如果您对色彩的工作原理不清楚的话,我快速解释一下。色彩值的范围从0.0f到1.0f。0.0f代表最黑的情况,1.0f就是最亮的情况。glClearColor 后的第一个参数是Red Intensity(红色分量),第二个是绿色,第三个是蓝色。最大值也是1.0f,代表特定颜色分量的最亮情况。最后一个参数是Alpha值。当它用来清除屏幕的时候,我们不用关心第四个数字。现在让它为0.0f。我会用另一个教程来解释这个参数。

通过混合三种原色(红、绿、蓝),您可以得到不同的色彩。希望您在学校里学过这些。因此,当您使用glClearColor(0.0f,0.0f,1.0f,0.0f),您将用亮蓝色来清除屏幕。如果您用 glClearColor(0.5f,0.0f,0.0f,0.0f)的话,您将使用中红色来清除屏幕。不是最亮(1.0f),也不是最暗 (0.0f)。要得到白色背景,您应该将所有的颜色设成最亮(1.0f)。要黑色背景的话,您该将所有的颜色设为最暗(0.0f)。
glClearColor(0.0f, 0.0f, 0.0f, 0.0f); // 黑色背景
接下来的三行必须做的是关于depth buffer(深度缓存)的。将深度缓存设想为屏幕后面的层。深度缓存不断的对物体进入屏幕内部有多深进行跟踪。我们本节的程序其实没有真正使用深度缓存,但几乎所有在屏幕上显示3D场景OpenGL程序都使用深度缓存。它的排序决定那个物体先画。这样您就不会将一个圆形后面的正方形画到圆形上来。深度缓存是OpenGL十分重要的部分。
glClearDepth(1.0f);
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);
// 设置深度缓存
// 启用深度测试
// 所作深度测试的类型
接着告诉OpenGL我们希望进行最好的透视修正。这会十分轻微的影响性能。但使得透视图看起来好一点。
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); // 真正精细的透视修正
最后,我们返回TRUE。如果我们希望检查初始化是否OK,我们可以查看返回的 TRUE或FALSE的值。如果有错误发生的话,您可以加上您自己的代码返回FALSE。目前,我们不管它。
return TRUE;
}
// 初始化 OK
 
下一段包括了所有的绘图代码。任何您所想在屏幕上显示的东东都将在此段代码中出现。以后的每个教程中我都会在例程的此处增加新的代码。如果您对OpenGL已经有所了解的话,您可以在 glLoadIdentity()调用之后,返回TRUE值之前,试着添加一些OpenGL代码来创建基本的形。如果您是OpenGL新手,等着我的下个教程。目前我们所作的全部就是将屏幕清除成我们前面所决定的颜色,清除深度缓存并且重置场景。我们仍没有绘制任何东东。

返回TRUE值告知我们的程序没有出现问题。如果您希望程序因为某些原因而中止运行,在返回TRUE值之前增加返回FALSE的代码告知我们的程序绘图代码出错。程序即将退出。
int DrawGLScene(GLvoid)
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 
glLoadIdentity(); 
return TRUE; 
}
// 从这里开始进行所有的绘制

// 清除屏幕和深度缓存
// 重置当前的模型观察矩阵
// 一切 OK
 
下一段代码只在程序退出之前调用。KillGLWindow() 的作用是依次释放着色描述表,设备描述表和窗口句柄。我已经加入了许多错误检查。如果程序无法销毁窗口的任意部分,都会弹出带相应错误消息的讯息窗口,告诉您什么出错了。使您在您的代码中查错变得更容易些。
GLvoid KillGLWindow(GLvoid) 
{
// 正常销毁窗口
 
我们在KillGLWindow()中所作的第一件事是检查我们是否处于全屏模式。如果是,我们要切换回桌面。我们本应在禁用全屏模式前先销毁窗口,但在某些显卡上这么做可能会使得桌面崩溃。所以我们还是先禁用全屏模式。这将防止桌面出现崩溃,并在Nvidia和3dfx显卡上都工作的很好!
if (fullscreen) 
{
// 我们处于全屏模式吗?
 
我们使用ChangeDisplaySettings(NULL,0)回到原始桌面。将NULL作为第一个参数,0作为第二个参数传递强制Windows使用当前存放在注册表中的值(缺省的分辨率、色彩深度、刷新频率,等等)来有效的恢复我们的原始桌面。切换回桌面后,我们还要使得鼠标指针重新可见。
ChangeDisplaySettings(NULL,0); 
ShowCursor(TRUE); 
}
// 是的话,切换回桌面
// 显示鼠标指针

 
接下来的代码查看我们是否拥有着色描述表(hRC)。如果没有,程序将跳转至后面的代码查看是否拥有设备描述表。
if (hRC) 
{
// 我们拥有着色描述表吗?
 
如果存在着色描述表的话,下面的代码将查看我们能否释放它(将 hRC从hDC分开)。这里请注意我使用的的查错方法。基本上我只是让程序尝试释放着色描述表(通过调用wglMakeCurrent(NULL,NULL),然后我再查看释放是否成功。巧妙的将数行代码结合到了一行。
if (!wglMakeCurrent(NULL,NULL)) 
{
// 我们能否释放DC和RC描述表?
 
如果不能释放DC和RC描述表的话,MessageBox()将弹出错误消息,告知我们DC和RC无法被释放。NULL意味着消息窗口没有父窗口。其右的文字将在消息窗口上出现。"SHUTDOWN ERROR"出现在窗口的标题栏上。MB_OK的意思消息窗口上带有一个写着OK字样的按钮。
MB_ICONINFORMATION将在消息窗口中显示一个带圈的小写的i(看上去更正式一些)。 
MessageBox(NULL,"Release Of DC And RC Failed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
}
下一步我们试着删除着色描述表。如果不成功的话弹出错误消息。
if (!wglDeleteContext(hRC)) 
{
// 我们能否删除RC?
 
如果无法删除着色描述表的话,将弹出错误消息告知我们RC未能成功删除。然后hRC被设为NULL。
MessageBox(NULL,"Release Rendering Context Failed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
}
hRC=NULL;                                                                                                   
  // 将RC设为 NULL
}
现在我们查看是否存在设备描述表,如果有尝试释放它。如果不能释放设备描述表将弹出错误消息,然后hDC设为NULL。
if (hDC && !ReleaseDC(hWnd,hDC))                                                         // 我们能否释放 DC?
{
MessageBox(NULL,"Release Device Context Failed.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
hDC=NULL;                                                                                                    
// 将 DC 设为 NULL
}
现在我们来查看是否存在窗口句柄,我们调用 DestroyWindow( hWnd )来尝试销毁窗口。如果不能的话弹出错误窗口,然后hWnd被设为NULL。
if (hWnd && !DestroyWindow(hWnd))                                                        // 能否销毁窗口?
{
MessageBox(NULL,"Could Not Release hWnd.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
hWnd=NULL;                                                                                                 
// 将 hWnd 设为 NULL
}
最后要做的事是注销我们的窗口类。这允许我们正常销毁窗口,接着在打开其他窗口时,不会收到诸如"Windows Class already registered"(窗口类已注册)的错误消息。
if (!UnregisterClass("OpenGL",hInstance))                                                 // 能否注销类?
{
MessageBox(NULL,"Could Not Unregister Class.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
hInstance=NULL;                                                                                         
// 将 hInstance 设为 NULL
}
}
接下来的代码段创建我们的OpenGL窗口。我花了很多时间来做决定是否创建固定的全屏模式这样不需要许多额外的代码,还是创建一个容易定制的友好的窗口但需要更多的代码。当然最后我选择了后者。我经常在EMail中收到诸如此类的问题:怎样创建窗口而不使用全屏幕?怎样改变窗口的标题栏?怎样改变窗口的分辨率或pixel format(象素格式)?以下的代码完成了所有这一切!尽管最好要学学材质,这会让您写自己的OpenGL程序变得容易的多!

正如您所见,此过程返回布尔变量(TRUE 或 FALSE)。他还带有5个参数:窗口的标题栏,窗口的宽度,窗口的高度,色彩位数(16/24/32),和全屏标志(TRUE --全屏模式, FALSE--窗口模式 )。返回的布尔值告诉我们窗口是否成功创建。
BOOL CreateGLWindow(char* title, int width, int height, int bits, bool fullscreenflag)
{
当我们要求Windows为我们寻找相匹配的象素格式时,Windows寻找结束后将模式值保存在变量PixelFormat中。
GLuint PixelFormat; // 保存查找匹配的结果
wc用来保存我们的窗口类的结构。窗口类结构中保存着我们的窗口信息。通过改变类的不同字段我们可以改变窗口的外观和行为。每个窗口都属于一个窗口类。当您创建窗口时,您必须为窗口注册类。
WNDCLASS wc; // 窗口类结构
dwExStyle和dwStyle存放扩展和通常的窗口风格信息。我使用变量来存放风格的目的是为了能够根据我需要创建的窗口类型(是全屏幕下的弹出窗口还是窗口模式下的带边框的普通窗口);来改变窗口的风格。
DWORD dwExStyle;
DWORD dwStyle;
// 扩展窗口风格
// 窗口风格
下面的5行代码取得矩形的左上角和右下角的坐标值。我们将使用这些值来调整我们的窗口使得其上的绘图区的大小恰好是我们所需的分辨率的值。通常如果我们创建一个640x480的窗口,窗口的边框会占掉一些分辨率的值。
RECT WindowRect;
WindowRect.left=(long)0;
WindowRect.right=(long)width;
WindowRect.top=(long)0;
WindowRect.bottom=(long)height;
// 取得矩形的左上角和右下角的坐标值
// 将Left   设为 0
// 将Right  设为要求的宽度
// 将Top    设为 0
// 将Bottom 设为要求的高度
下一行代码我们让全局变量fullscreen等于fullscreenflag。如果我们希望在全屏幕下运行而将fullscreenflag设为TRUE,但没有让变量fullscreen等于fullscreenflag的话,fullscreen变量将保持为FALSE。当我们在全屏幕模式下销毁窗口的时候,变量fullscreen的值却不是正确的TRUE值,计算机将误以为已经处于桌面模式而无法切换回桌面。上帝啊,但愿这一切都有意义。就是一句话,fullscreen的值必须永远fullscreenflag的值,否则就会有问题。{CKER也觉得此处太废话,懂的人都要不懂啦.....:(  }
fullscreen=fullscreenflag; // 设置全局全屏标志
下一部分的代码中,我们取得窗口的实例,然后定义窗口类。

CS_HREDRAW 和 CS_VREDRAW 的意思是无论何时,只要窗口发生变化时就强制重画。CS_OWNDC为窗口创建一个私有的DC。这意味着DC不能在程序间共享。WndProc是我们程序的消息处理过程。由于没有使用额外的窗口数据,后两个字段设为零。然后设置实例。接着我们将hIcon设为NULL,因为我们不想给窗口来个图标。鼠标指针设为标准的箭头。背景色无所谓(我们在GL中设置)。我们也不想要窗口菜单,所以将其设为NULL。类的名字可以您想要的任何名字。出于简单,我将使用"OpenGL"。
hInstance = GetModuleHandle(NULL);
wc.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
wc.lpfnWndProc = (WNDPROC) WndProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(NULL, IDI_WINLOGO);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = NULL;
wc.lpszMenuName = NULL;
wc.lpszClassName = "OpenGL";
// 取得我们窗口的实例
// 移动时重画,并为窗口取得DC
// WndProc处理消息
// 无额外窗口数据
// 无额外窗口数据
// 设置实例
// 装入缺省图标
// 装入鼠标指针
// GL不需要背景
// 不需要菜单
// 设定类名字
现在注册类名字。如果有错误发生,弹出错误消息窗口。按下上面的OK按钮后,程序退出。
if (!RegisterClass(&wc))                                    // 尝试注册窗口类
{
MessageBox(NULL,"Failed To Register The Window Class.","ERROR",MB_OK|MB_ICONEXCLAMATION);
return FALSE;                                               file://退出并返回FALSE
}
查看程序应该在全屏模式还是窗口模式下运行。如果应该是全屏模式的话,我们将尝试设置全屏模式。
if (fullscreen)
{
// 要尝试全屏模式吗?
 
下一部分的代码看来很多人都会有问题要问关于.......切换到全屏模式。在切换到全屏模式时,有几件十分重要的事您必须牢记。必须确保您在全屏模式下所用的宽度和高度等同于窗口模式下的宽度和高度。最最重要的是要在创建窗口之前设置全屏模式。这里的代码中,您无需再担心宽度和高度,它们已被设置成与显示模式所对应的大小。
DEVMODE dmScreenSettings; 
memset(&dmScreenSettings,0,sizeof(dmScreenSettings)); 
dmScreenSettings.dmSize=sizeof(dmScreenSettings); 
dmScreenSettings.dmPelsWidth = width; 
dmScreenSettings.dmPelsHeight = height; 
dmScreenSettings.dmBitsPerPel = bits; 
dmScreenSettings.dmFields=DM_BITSPERPEL|DM_PELSWIDTH|DM_PELSHEIGHT;
// 设备模式
// 确保内存分配
// Devmode 结构的大小
// 所选屏幕宽度
// 所选屏幕高度
// 每象素所选的色彩深度
 
上面的代码中,我们分配了用于存储视频设置的空间。设定了屏幕的宽,高,色彩深度。下面的代码我们尝试设置全屏模式。我们在dmScreenSettings中保存了所有的宽,高,色彩深度讯息。下一行使用ChangeDisplaySettings来尝试切换成与dmScreenSettings所匹配模式。我使用参数CDS_FULLSCREEN来切换显示模式,因为这样做不仅移去了屏幕底部的状态条,而且它在来回切换时,没有移动或改变您在桌面上的窗口。
// 尝试设置显示模式并返回结果。注: CDS_FULLSCREEN 移去了状态条。
if (ChangeDisplaySettings(&dmScreenSettings,CDS_FULLSCREEN)!=DISP_CHANGE_SUCCESSFUL)
{
如果模式未能设置成功,我们将进入以下的代码。如果不能匹配全屏模式,弹出消息窗口,提供两个选项:在窗口模式下运行或退出。
// 若模式失败,提供两个选项:退出或在窗口内运行。
if (MessageBox(NULL,"The Requested Fullscreen Mode Is Not Supported By/nYour Video Card. Use Windowed Mode Instead?","NeHe GL",MB_YESNO|MB_ICONEXCLAMATION)==IDYES)
{
如果用户选择窗口模式,变量fullscreen 的值变为FALSE,程序继续运行。
fullscreen=FALSE;
}
else
{
// 选择窗口模式(Fullscreen=FALSE)


 
如果用户选择退出,弹出消息窗口告知用户程序将结束。并返回FALSE告诉程序窗口未能成功创建。程序退出。
// Pop Up A Message Box Letting User Know The Program Is Closing.
MessageBox(NULL,"Program Will Now Close.","ERROR",MB_OK|MB_ICONSTOP);
return FALSE;                                                               
//退出并返回 FALSE
}
}
}
由于全屏模式可能失败,用户可能决定在窗口下运行,我们需要在设置屏幕/窗口之前,再次检查fullscreen的值是TRUE或FALSE。
if (fullscreen)
{
// 仍处于全屏模式吗?
 
如果我们仍处于全屏模式,设置扩展窗体风格为WS_EX_APPWINDOW,这将强制我们的窗体可见时处于最前面。再将窗体的风格设为WS_POPUP。这个类型的窗体没有边框,使我们的全屏模式得以完美显示。
最后我们禁用鼠标指针。当您的程序不是交互式的时候,在全屏模式下禁用鼠标指针通常是个好主意。
dwExStyle=WS_EX_APPWINDOW;
dwStyle=WS_POPUP;
ShowCursor(FALSE);
}
else
{
// 扩展窗体风格
// 窗体风格
// 隐藏鼠标指针


 
如果我们使用窗口而不是全屏模式,我们在扩展窗体风格中增加了 WS_EX_WINDOWEDGE,增强窗体的3D感观。窗体风格改用 WS_OVERLAPPEDWINDOW,创建一个带标题栏、可变大小的边框、菜单和最大化/最小化按钮的窗体。
dwExStyle=WS_EX_APPWINDOW | WS_EX_WINDOWEDGE;
dwStyle=WS_OVERLAPPEDWINDOW;
}
// 扩展窗体风格
// 窗体风格
 
下一行代码根据创建的窗体类型调整窗口。调整的目的是使得窗口大小正好等于我们要求的分辨率。通常边框会占用窗口的一部分。使用AdjustWindowRectEx 后,我们的OpenGL场景就不会被边框盖住。实际上窗口变得更大以便绘制边框。全屏模式下,此命令无效。
AdjustWindowRectEx(&WindowRect, dwStyle, FALSE, dwExStyle); // 调整窗口达到真正要求的大小
下一段代码开始创建窗口并检查窗口是否成功创建。我们将传递CreateWindowEx()所需的所有参数。如扩展风格、类名字(与您在注册窗口类时所用的名字相同)、窗口标题、窗体风格、窗体的左上角坐标(0,0 是个安全的选择)、窗体的宽和高。我们没有父窗口,也不想要菜单,这些参数被设为NULL。还传递了窗口的实例,最后一个参数被设为NULL。

注意我们在窗体风格中包括了 WS_CLIPSIBLINGS 和 WS_CLIPCHILDREN。要让OpenGL正常运行,这两个属性是必须的。他们阻止别的窗体在我们的窗体内/上绘图。
if (!(hWnd=CreateWindowEx( dwExStyle,
"OpenGL",
title,
WS_CLIPSIBLINGS |
WS_CLIPCHILDREN |
dwStyle,
0, 0,
WindowRect.right-WindowRect.left,
WindowRect.bottom-WindowRect.top,
NULL,
NULL,
hInstance,
NULL)))
// 扩展窗体风格
// 类名字
// 窗口标题
// 必须的窗体风格属性
// 必须的窗体风格属性
// 选择的窗体属性
// 窗口位置
// 计算调整好的窗口宽度
// 计算调整好的窗口高度
// 无父窗口
// 无菜单
// 实例
// 不向WM_CREATE传递任何东东
下来我们检查看窗口是否正常创建。如果成功, hWnd保存窗口的句柄。如果失败,弹出消息窗口,并退出程序。
{
KillGLWindow();                                                             
// 重置显示区
MessageBox(NULL,"Window Creation Error.","ERROR",MB_OK|MB_ICONEXCLAMATION);
return FALSE;                                                               
// 返回 FALSE
}
下面的代码描述象素格式。我们选择了通过RGBA(红、绿、蓝、alpha通道)支持OpenGL和双缓存的格式。我们试图找到匹配我们选定的色彩深度(16位、24位、32位)的象素格式。最后设置16位Z-缓存。其余的参数要么未使用要么不重要(stencil buffer模板缓存和accumulation buffer聚集缓存除外)。
static PIXELFORMATDESCRIPTOR pfd=
{
sizeof(PIXELFORMATDESCRIPTOR),
1,
PFD_DRAW_TO_WINDOW |
PFD_SUPPORT_OPENGL |
PFD_DOUBLEBUFFER,
PFD_TYPE_RGBA,
bits,
0, 0, 0, 0, 0, 0,
0,
0,
0,
0, 0, 0, 0,
16,
0,
0,
PFD_MAIN_PLANE,
0,
0, 0, 0
};
//pfd 告诉窗口我们所希望的东东

file://上诉格式描述符的大小
// 版本号
// 格式必须支持窗口
// 格式必须支持OpenGL
// 必须支持双缓冲
// 申请 RGBA 格式
// 选定色彩深度
// 忽略的色彩位
// 无Alpha缓存
// 忽略Shift Bit
// 无聚集缓存
// 忽略聚集位
// 16位 Z-缓存 (深度缓存)
// 无模板缓存
// 无辅助缓存
// 主绘图层
// 保留
// 忽略层遮罩
 
如果前面创建窗口时没有错误发生,我们接着尝试取得OpenGL设备描述表。若无法取得DC,弹出错误消息程序退出(返回FALSE)。
if (!(hDC=GetDC(hWnd)))                                                      //取得设备描述表了么?
{
KillGLWindow();                                                             
// 重置显示区
MessageBox(NULL,"Can't Create A GL Device Context.","ERROR",MB_OK|MB_ICONEXCLAMATION);
return FALSE;                                                               
// 返回 FALSE
}
设法为OpenGL窗口取得设备描述表后,我们尝试找到对应与此前我们选定的象素格式的象素格式。如果Windows不能找到的话,弹出错误消息,并退出程序(返回FALSE)。
if (!(PixelFormat=ChoosePixelFormat(hDC,&pfd)))                             // Windows 找到相应的象素格式了吗?
{
KillGLWindow();                                                            
// 重置显示区
MessageBox(NULL,"Can't Find A Suitable PixelFormat.","ERROR",MB_OK|MB_ICONEXCLAMATION);
return FALSE;                                                              
// 返回 FALSE
}
Windows 找到相应的象素格式后,尝试设置象素格式。如果无法设置,弹出错误消息,并退出程序(返回FALSE)。
if(!SetPixelFormat(hDC,PixelFormat,&pfd))                                   // 能够设置象素格式么?
{
KillGLWindow();                                                    
        // 重置显示区
MessageBox(NULL,"Can't Set The PixelFormat.","ERROR",MB_OK|MB_ICONEXCLAMATION);
return FALSE;                                                 
            
// 返回 FALSE
}
正常设置象素格式后,尝试取得着色描述表。如果不能取得着色描述表的话,弹出错误消息,并退出程序(返回FALSE)。
if (!(hRC=wglCreateContext(hDC)))                                           // 能否取得着色描述表?
{
KillGLWindow();
                                                             // 重置显示区
MessageBox(NULL,"Can't Create A GL Rendering Context.","ERROR",MB_OK|MB_ICONEXCLAMATION);
return FALSE;
                                                               // 返回 FALSE
}
如果到现在仍未出现错误的话,我们已经设法取得了设备描述表和着色描述表。接着要做的是激活着色描述表。如果无法激活,弹出错误消息,并退出程序(返回FALSE)。
if(!wglMakeCurrent(hDC,hRC))                                               // 尝试激活着色描述表
{
KillGLWindow();                                                    
        // 重置显示区
MessageBox(NULL,"Can't Activate The GL Rendering Context.","ERROR",MB_OK|MB_ICONEXCLAMATION);
return FALSE;                                                 
             // 返回 FALSE
}
一切顺利的话,OpenGL窗口已经创建完成,接着可以显示它啦。将它设为前端窗口(给它更高的优先级),并将焦点移至此窗口。然后调用ReSizeGLScene 将屏幕的宽度和高度设置给透视OpenGL屏幕。
ShowWindow(hWnd,SW_SHOW);
SetForegroundWindow(hWnd);
SetFocus(hWnd);
ReSizeGLScene(width, height);
// 显示窗口
// 略略提高优先级
// 设置键盘的焦点至此窗口
// 设置透视 GL 屏幕
跳转至 InitGL(),这里可以设置光照、纹理、等等任何需要设置的东东。您可以在 InitGL()内部自行定义错误检查,并返回 TRUE (一切正常)或FALSE (有什么不对)。例如,如果您在InitGL()内装载纹理并出现错误,您可能希望程序停止。如果您返回 FALSE的话,下面的代码会弹出错误消息,并退出程序。
if (!InitGL())                                                              // 初始化新建的GL窗口
{
KillGLWindow();                                                            
// 重置显示区
MessageBox(NULL,"Initialization Failed.","ERROR",MB_OK|MB_ICONEXCLAMATION);
return FALSE;                                                              
// 返回 FALSE
}
到这里可以安全的推定创建窗口已经成功了。我们向WinMain()返回TRUE,告知WinMain()没有错误,以防止程序退出。
return TRUE; // 成功
}
下面的代码处理所有的窗口消息。当我们注册好窗口类之后,程序跳转到这部分代码处理窗口消息。
LRESULT CALLBACK WndProc( HWND hWnd, 
UINT uMsg, 
WPARAM wParam, 
LPARAM lParam) 
{
// 窗口的句柄
// 窗口的消息
// 附加的消息内容
// 附加的消息内容
 
下来的代码比对uMsg的值,然后转入case处理,uMsg 中保存了我们要处理的消息名字。
switch (uMsg) 
{
// 检查Windows消息
 
如果uMsg等于WM_ACTIVE,查看窗口是否仍然处于激活状态。如果窗口已被最小化,将变量active设为FALSE。如果窗口已被激活,变量active的值为TRUE。
case WM_ACTIVATE: 
{
if (!HIWORD(wParam)) 
{
active=TRUE; 
}
else
{
active=FALSE; 
}
return 0; 
}
// 监视窗口激活消息

// 检查最小化状态

// 程序处于激活状态



// 程序不再激活

// 返回消息循环
 
如果消息是WM_SYSCOMMAND(系统命令),再次比对wParam。如果wParam 是 SC_SCREENSAVE 或 SC_MONITORPOWER的话,不是有屏幕保护要运行,就是显示器想进入节电模式。返回0可以阻止这两件事发生。
case WM_SYSCOMMAND:
{
switch (wParam) 
{
case SC_SCREENSAVE: 
case SC_MONITORPOWER: 
return 0; 
}
break; 
}
// 中断系统命令Intercept System Commands

// 检查系统调用Check System Calls

// 屏保要运行?
// 显示器要进入节电模式?
// 阻止发生

// 退出
 
如果 uMsg是WM_CLOSE,窗口将被关闭。我们发出退出消息,主循环将被中断。变量done被设为TRUE,WinMain()的主循环中止,程序关闭。
case WM_CLOSE:
{
PostQuitMessage(0); 
return 0; 
}
//收到Close消息?

// 发出退出消息

  
如果键盘有键按下,通过读取wParam的信息可以找出键值。我将键盘数组keys[ ]相应的数组组成员的值设为TRUE。这样以后就可以查找key[ ]来得知什么键被按下。允许同时按下多个键。
case WM_KEYDOWN: 
{
keys[wParam] = TRUE; 
return 0; 
}
// 有键按下么?

// 如果是,设为TRUE
// 返回

  
同样,如果键盘有键释放,通过读取wParam的信息可以找出键值。然后将键盘数组keys[ ]相应的数组组成员的值设为FALSE。这样查找key[ ]来得知什么键被按下,什么键被释放了。键盘上的每个键都可以用0-255之间的一个数来代表。举例来说,当我们按下40所代表的键时,keys[40]的值将被设为TRUE。放开的话,它就被设为FALSE。这也是key数组的原理。
case WM_KEYUP: 
{
keys[wParam] = FALSE; 
return 0; 
}
// 有键放开么?

// 如果是,设为FALSE
// 返回

 
当调整窗口时,uMsg 最后等于消息WM_SIZE。读取lParam的LOWORD 和HIWORD可以得到窗口新的宽度和高度。将他们传递给ReSizeGLScene(),OpenGL场景将调整为新的宽度和高度。
case WM_SIZE: 
{
ReSizeGLScene(LOWORD(lParam),HIWORD(lParam));  
return 0; 
}
}
// 调整OpenGL窗口大小

// LoWord=Width,HiWord=Height
// 返回


 
其余无关的消息被传递给DefWindowProc,让Windows自行处理。
//向 DefWindowProc传递所有未处理的消息。
return DefWindowProc(hWnd,uMsg,wParam,lParam);
}
下面是我们的Windows程序的入口。将会调用窗口创建例程,处理窗口消息,并监视人机交互。
int WINAPI WinMain( HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine, 
int nCmdShow) 
{
//实例
// 前一个实例
// 命令行参数
// 窗口显示状态
  
我们设置两个变量。msg 用来检查是否有消息等待处理。done的初始值设为FALSE。这意味着我们的程序仍未完成运行。只要程序done保持FALSE,程序继续运行。一旦done的值改变为TRUE,程序退出。
MSG msg; 
BOOL done=FALSE; 
// Windowsx消息结构
// 用来退出循环的Bool 变量
这段代码完全可选。程序弹出一个消息窗口,询问用户是否希望在全屏模式下运行。如果用户单击NO按钮,fullscreen变量从缺省的TRUE改变为FALSE,程序也改在窗口模式下运行。
// 提示用户选择运行模式
if (MessageBox(NULL,"Would You Like To Run In Fullscreen Mode?", "Start FullScreen?",MB_YESNO|MB_ICONQUESTION)==IDNO)
{
fullscreen=FALSE;
file://窗口模式

}
接着创建OpenGL窗口。CreateGLWindow函数的参数依次为标题、宽度、高度、色彩深度,以及全屏标志。就这么简单!我很欣赏这段代码的简洁。如果未能创建成功,函数返回FALSE。程序立即退出。
// 创建OpenGL窗口
if (!CreateGLWindow("NeHe's OpenGL Framework",640,480,16,fullscreen))
{
return 0;
// 失败退出

}
 
下面是循环的开始。只要done保持FALSE,循环一直进行。
while(!done) // 保持循环直到 done=TRUE
{
 
我们要做的第一件事是检查是否有消息在等待。使用PeekMessage()可以在不锁住我们的程序的前提下对消息进行检查。许多程序使用GetMessage(),也可以很好的工作。但使用GetMessage(),程序在收到paint消息或其他别的什么窗口消息之前不会做任何事。
if (PeekMessage(&msg,NULL,0,0,PM_REMOVE))
{
 //有消息在等待吗?
  
下面的代码查看是否出现退出消息。如果当前的消息是由PostQuitMessage(0)引起的WM_QUIT,done变量被设为TRUE,程序将退出。
if (msg.message==WM_QUIT)
{
done=TRUE;
}
else
{
//收到退出消息?

// 是,则done=TRUE

// 不是,处理窗口消息
 
如果不是退出消息,我们翻译消息,然后发送消息,使得WndProc() 或 Windows能够处理他们。
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
else
{
// 翻译消息
// 发送消息


// 如果没有消息
 
如果没有消息,绘制我们的OpenGL场景。代码的第一行查看窗口是否激活。如果按下ESC键,done变量被设为TRUE,程序将会退出。
// 绘制场景。监视ESC键和来自DrawGLScene()的退出消息
if (active)
{
if (keys[VK_ESCAPE])
{
done=TRUE;
}
else
{

// 程序激活的么?

// ESC 按下了么?

// ESC 发出退出信号

// 不是退出的时候,刷新屏幕
 
如果程序是激活的且ESC没有按下,我们绘制场景并交换缓存(使用双缓存可以实现无闪烁的动画)。我们实际上在另一个看不见的"屏幕"上绘图。当我们交换缓存后,我们当前的屏幕被隐藏,现在看到的是刚才看不到的屏幕。这也是我们看不到场景绘制过程的原因。场景只是即时显示。
DrawGLScene();
SwapBuffers(hDC);
}
}
// 绘制场景
// 交换缓存 (双缓存)

下面的一点代码是最近新加的(05-01-00)。允许用户按下F1键在全屏模式和窗口模式间切换。
if (keys[VK_F1])
{
keys[VK_F1]=FALSE;
KillGLWindow();
fullscreen=!fullscreen;
// 重建 OpenGL 窗口
if (!CreateGLWindow("NeHe's OpenGL Framework",640,480,16,fullscreen))
{
return 0;
}
}
}
}
//  F1键按下了么?

// 若是,使对应的Key数组中的值为 FALSE
// 销毁当前的窗口
// 切换 全屏 / 窗口 模式



// 如果窗口未能创建,程序退出



 
If the done variable is no longer FALSE, the program quits. We kill the OpenGL window properly so that everything is freed up, and we exit the program.
如果done变量不再是FALSE,程序退出。正常销毁OpenGL窗口,将所有的内存释放,退出程序。
// 关闭程序
KillGLWindow();
return (msg.wParam);
}

// 销毁窗口
// 退出程序

 


 

2006/4/2
韬光养晦
成大事者必备的9种手段
  
    每个人做人办事的手段都是不一样的,可以讲,一个人就有一种手段,一个人就有一种靠自己手段获得成功的途径。无数事实表明,有些人就是太过于自信,想念自己确认的手段能够解决任何问题,但不知道这种往往是起不到任何作用。因此,他们总觉得离成功的目标不是越来越近,而实际上越来越远。
  
    人生的计划和行动,是需要靠章法来完成的,而不是靠一些怪招去谋划的。这就好比在拳击台比赛一样:两个拳手相互较量,激战正酣,进退躲闪、扑让攻守,都有相当灵活的步伐和拳路,他们的一招一式都是为成功而做准备的,这一招一式就叫手段。可惜的是,有很多人并不能看到这一招一式的寓意。
  
    手段是成功的保证,没有手段的行动和计划一定是事倍功半的,孙悟空与牛魔王一比高低,靠的是什么?靠的是他七十二变的手段:“飞人”乔丹叱咤NBA赛场靠什么?靠的是他灵活自如、左右盘带,飞身灌蓝的手段。一名话,没有手段,你永远吃不到成功的甜果。
  
    手段从何而来?对于那些成大事者来说,他们善于总结自己、反思自己、比较自己,从而避实就虚,找到自己人生的强项——自己究竟能干什么和不能干什么,并付出实际的行动。这个过程就是确立自己成大事手段的过程。不明白这一点,一个人永远就会在错误的方向走下去。
  
    成大事的九种手段:
  
    1、敢于决断——克服犹豫不定的习性
  
    很多人之所以一事无成,最大的毛病就是缺乏敢于决断的手段,总是左顾右盼、思前想后,从而错失成功的最佳时机。成大事者在看到事情的成功可能性到来时,敢于做出重大决断,因此取得先机。
  
    2、挑战弱点——彻底改变自己的缺陷
  
    人人都有弱点,不能成大事者总是固守自己的弱点,一生都不会发生重大转变;能成大事者总是善于从自己的弱点上开刀,去把自己变成一个能力超强的人。一个连自己的缺陷都不能纠正的人,只能是失败者!
  
    3、突破困境——从失败中撮成功的资本
  
    人生总要面临各种困境的挑战,甚至可以说困境就是“鬼门关”。一般人会在困境面前浑身发抖,而成大事者则能把困境变为成功的有力跳板。
  
    4、抓住机遇——善于选择、善于创造
  
    机遇就是人生最大的财富。有些人浪费机遇轻而易举,所以一个个有巨大潜力的机遇都悄然溜跑,成大事都是绝对不允许溜走,并且能纵身扑向机遇。
  
    5、发挥强项——做自己最擅长的事情
  
    一个能力极弱的人肯定难以打开人生局面,他必定是人生舞台上重量级选手的牺牲品;成大事者关于在自己要做的事情上,充分施展才智,一步一步地拓宽成功之路。
  
    6、调整心态——切忌让情绪伤害自己
  
    心态消极的人,无论如何都挑不起生活和重担,因为他们无法直面一个个人生挫折,成大事者则关于高速心态,即使在毫无希望时,也能看到一线成功的亮光。
  
    7、立即行动——只说不做,徒劳无益
  
    一次行动胜过百遍心想。有些人是“语言的巨人,行动的矮子”,所以看不到更为实际现实的事情在他身上发生;成大事者是每天都靠行动来落实自己的人生计划的。
  
    8、善于交往——巧妙利用人力资源
  
    一个人不懂得交往,必然会推动人际关系的力量。成大事者的特点之一是:善于靠借力、借热去营造成功的局势,从而能把一件件难以办成的事办成,实现自己人生的规划。
  
    9、重新规划——站到更高的起点上
  
    人生是一个过程,成功也是一个过程。你如果满足于小成功,就会推动大成功。成大事者懂得从小到大的艰辛过程,所以在实现了一个个小成功之后,能继续拆开下一个人生的“密封袋”。
  
    可以讲任何一种手段,都可以导致一种结果,但这个结果是不是最佳的结果,恐怕就很难说了。成大事者总是关于选择最佳的手段,达到最完善的结果,这就是非一般人所能做到的。因此在成功之路上,你要想成大事,首先要解决的问题就是:你的手段对你推动成功的计划是否立竿见影!
2006/4/1
空指针问题
编码的时候遇到的一个问题,我觉得我确实应该好好学学模式。
一个包里面的一个类向调用另一个包里面的类的成员函数~ 比如~
//"A.class"
package one;
import two.B;
public class A
{
  B b;//这里不能为B b=new B();因为构造函数不能声明为PUBLIC
  b.test();
}
//"B.class"
package two;
public class B
{
B(){}//构造函数
public void test()//要调用的函数
{}
}
这样调用产生下面的空指针错误
Exception in thread "main" java.lang.NullPointerException
请问在不把两个包合并的情况下如何解决~
谢谢~
 
网上大大的解决方法:
package two;
public class B
{
private B(){}//构造函数
public static B getInstance(){
    return new B();
}
public void test()//要调用的函数
{}
}
在A类中用下面的方法调用以创建一个B的实例:
B b=B.getInstance();

使用JDBC访问DB2的问题

昨天下午遇到一个问题:使用DB2自带的驱动db2java.zip文件中的type2类型的驱动访问DB2,总是报错:

java.sql.SQLException: java.lang.UnsatisfiedLinkError: no db2jdbc in java.library.path

或者:
java.lang.ClassNotFoundException: COM.ibm.db2.jdbc.app.DB2Driver

要么就报:没有合适的驱动。

我开始查classpath,把多余的驱动都删除了,还是报错,后来我又把db2java.zip文件改名为db2java.jar,也还是不行。

折腾了一个小时,我意识到访问DB2和访问Oracle不太一样,于是google,结果找到这篇文章:

http://www-128.ibm.com/developerworks/cn/db2/library/techarticles/0402chenjunwei/0402chenjunwei.html

原来DB2的驱动还分几个版本,不同的DB2使用的版本的效果也不太一样。我开始怀疑使是我的驱动的问题,于是拿MyEclipse来试,结果MyEclipse也连接不上,但用DB2的客户端可以连接到远程服务器,后来在同事的帮助下,MyEclipse连上了,解决办法就是把我改成jar的后缀再改回来:o(。

但使用程序还是访问不了DB2,经过MyEclipse的测试,可以肯定那个驱动是没有问题的,所以,还是使用方法或者配置不对,我又搜了很多文章,结果发现这个问题很多人问,国外的帖子也很多,但没有详细回答的,又郁闷:o(,后来从几篇文章中零碎的找到一些提示,是db2jdbc.dll文件找不到,于是我试着把这个文件从DB2的bin目录下复制到System32目录下

,还是不行,我又把它复制到Java_Home/bin下面,重启机器,OK!一定要记住:是bin下面!!!

db2java.zip文件要改名为db2java.jar,并且放到Common/lib下。

解决方法很简单,问题是很多人知道了,这样可以节省大家的时间,但没人写下来。

另外还有一篇参考文章:

http://www-912.ibm.com/s_dir/slkbase.NSF/0/3f2a44217ec5c05786256c3e007194b3?OpenDocument

但对这个问题并没有提出什么有意义的意见。

2006/3/28
DB2中几种遇到的SQL1032N出错的解决
在使用DB2以来,碰到了几次出现提示SQL1032N错误,每次出错时出错信息大概如下:
11/21/2004 22:15:33 0 0 SQL1042C 发生意外的系统错误。
 
SQL1032N 未发出启动数据库管理器的命令。 SQLSTATE=57019。

每次出现问题后,都到网上找了很多资料,也问了许多人,费了些力才搞定的。几次出错的原因和解决方法都不尽相同,解决后我也只做了个简单的记录。一直想把它们写下来,方便方便后来也遇到同样问题,跟我一样到处查找的人,中间也写了一些废话,比如我如何查找错误,甚至于作了哪些无用功。
第一种SQL1032N出错,是某天DB2的实例突然无法启动了,用db2start就提示大概如下的出错信息:

12/30/2004 11:28:39 0 0 SQL1042C 发生意外的系统错误。
SQL1032N 未发出启动数据库管理器的命令。 SQLSTATE=57019。

初次遇到这种问题,还以为会不会是数据库没起来,情急之下什么命令比如激活数据库只类的,都拿来试了试,实例都起不来,当时运行这些命令,肯定都是不行的了。
后来突然发现,在开启机器的时候,提示有个服务出错了没启动,由此推想应该就是在Window服务里设置为自动启动的DB2实例服务没有正常启动,我在服务里面手动启动它,提示这样的错误:

WINDOWS不能在本地计算机启动DB2-DB2-0.有关更多信息,查阅系统事件日志.并参考特定服务代码-8000.

查看事件管理器,有这样的记录:

DB2-DB2-0服务因4294959296服务性错误而停止.来源SERVICE CONTROL 事件ID:7024

做了这么多,全都是无用功,只限于知道了服务没起来,等于没找。
之后通过各方询问,终于找到了原因:License到期了。
在db2cmd界面下运行db2licm -l,可以很明显的看到许可证已经过期了。
知道原因所在了,剩下的,就是自己想办法去解决这个问题了。

小结:直到现在,在有些论坛中,还很经常看到有人发这种帖子来问,至少我在两个月内就碰到了三次这种帖子。所以,如果不是可以确定已经有永久授权的情况下,发生这种情况,用db2licm -l查一下,也不算坏事。

还有一种情况,跟前面的差不多
也是在启动实例的时候出现如上的SQL1032N错误。在windows NT服务中无法启动DB2-DB0服务,同时提示:
出错1069,登陆失败错误。
这个错误比较简单,是用来启动服务的用户名或密码错误。只需要在服务的属性中,选择登陆选项卡,选择用户,并填好密码,重新启动服务就可以了。

小结:这种情况,一般发生在切换用户登陆NT系统或者更改了DB2用户的密码的情况下。

第三种情况是这样的:
最开始,是突然DB2的客户端连接不上server了,提示如下:

 C:/Documents and Settings/Administrator>db2 connect to fjdldw user install using install3211
 SQL30081N  检测到通信错误。正在使用的通信协议:"TCP/IP"。正在使用的通信API:
 "SOCKETS"。检测到错误的位置:"10.142.12.1"。检测到错误的通信函数:"connect"。协
 议特定的错误代码:"10061"、"*"、"*"。  SQLSTATE=08001

我本来还以为真是什么TCP/IP协议的问题,去查找了很多与SQL30081N错误相关的信息,都无法解决问题。后来到了在服务器上检查,发现DB2实例未起来。
用db2start命令,仍是提示:

 D:/Program QLLIB/BIN>db2start
 12/30/2004 11:28:39 0 0 SQL1042C 发生意外的系统错误。
 SQL1032N 未发出启动数据库管理器的命令。 SQLSTATE=57019

 用db2 get dbm cfg查看配置文件,因未作过其他操作,所以没有什么异常
查看相应实例下的db2diag.log文件,摘取真正有用的部分出错日志:

 Failed to create the memory segment used for communication with fenced routines. If re-starting db2, ensure no db2fmp processes were on the instance prior to start. Otherwise, you can ajust this value through DB2_FMP_COMM_HEAPSZ db2set value, or by decreasing your ASLHEAPSZ setting.
 
依据ensure no db2fmp processes were on the instance prior to start,将任务管理器里的db2fmp进程全部杀掉,然后重新启动实例。db2start,OK!

小结:后来查了一查,db2fmp进程用于执行受保护的存储过程,或者自定义函数。这次出错的原因,一直没有弄清楚。但是,通过这次解决,可以说明一点,出了错误,查查db2diag.log文件,总是不会错的。^_^
2006/3/5
JDBC连接数据库经验技巧集萃

JDBC是如何连接数据库的?

jdbc.sql.Driver d =
            (Driver)Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");         jdbc.sql.Connection con =
            DriverManager.getConnection("jdbc:odbc:pubs","sa",""); 

以上的语句您已经很熟了吧 但
 到底是怎么连上数据库的?:-)

java.sql包中的 java.sql.Driver, jdbc.sql.Connection等提供给程序开发人员统一的开发接口,数据库提供商提供相应的实现,对程序开发人员来讲只要知道这些接口都有哪些方法就可以了,但我们可以深入一些 看看到底这里面都做了那些事, 同时也可以学习其中的编程模式(如Interface模式等)

1 Class.forName(String classname) 的源码为:
public final
class Class implements java.io.Serializable {
...
public static Class forName(String className)
    throws ClassNotFoundException {
 return forName0(className, true, ClassLoader.getCallerClassLoader());
}

...
}
关于forName0 请自己查看jdk source.

目的是把指定的Class装载到JVM中来。(注意class的装载、初始化过程)
在装载过程中将执行被装载类的static块(如下)

2 sun的JdbcOdbcDriver 源码
public class JdbcOdbcDriver extends JdbcOdbcObject
    implements JdbcOdbcDriverInterface
{
  ...
  /**
   * connect to DB
   */
  public synchronized Connection connect(String s, Properties properties)
        throws SQLException
    {
        if(JdbcOdbcObject.isTracing())
            JdbcOdbcObject.trace("*Driver.connect (" + s + ")");
        if(!acceptsURL(s))
            return null;
        if(hDbc != 0)
        {
            disconnect(hDbc);
            closeConnection(hDbc);
            hDbc = 0;
        }
        if(!initialize())
        {
            return null;
        }
        else
        {
            JdbcOdbcConnection jdbcodbcconnection = new JdbcOdbcConnection(OdbcApi, hEnv, this);
            jdbcodbcconnection.initialize(getSubName(s), properties, DriverManager.getLoginTimeout());
            jdbcodbcconnection.setURL(s);
            return jdbcodbcconnection;
        }
   }
 
  static
    {
        if(JdbcOdbcObject.isTracing())
            JdbcOdbcObject.trace("JdbcOdbcDriver class loaded");
        JdbcOdbcDriver jdbcodbcdriver = new JdbcOdbcDriver();
        try
        {
            DriverManager.registerDriver(jdbcodbcdriver);
        }
        catch(SQLException sqlexception)
        {
            if(JdbcOdbcObject.isTracing())
                JdbcOdbcObject.trace("Unable to register driver");
        }
    }
}

public interface JdbcOdbcDriverInterface
    extends Driver
{
  ...
}

3 连接过程
jdbc.sql.Connection con =
            DriverManager.getConnection("jdbc:odbc:pubs","sa","");

public class DriverManager {
public static synchronized Connection getConnection(String url,
              String user, String password) throws SQLException {
        java.util.Properties info = new java.util.Properties();

        // Gets the classloader of the code that called this method, may
 // be null.
 ClassLoader callerCL = DriverManager.getCallerClassLoader();

 if (user != null) {
     info.put("user", user);
 }
 if (password != null) {
     info.put("password", password);
 }

        return (getConnection(url, info, callerCL));
}
private static synchronized Connection getConnection(
        String url,
        java.util.Properties info,
        ClassLoader callerCL) throws SQLException
{
...
Connection result = di.driver.connect(url, info);
...
}             
}
4  结构图:
请点击查看 结构图   http://jdeveloper.myrice.com/doc/jdbc/images/howjdbc.jpg

JDBC连接DB2数据库详解  

关于DB2数据库的JDBC连接文章有很多,比较出名的有诸如“JDBC数据库连接大全”和“JSP的DB2连接数据库”,虽然都是很详细的资料,也都说解决了前人没有解决的问题,但还是有许多纰漏。我就这两天的经验给大家写一篇关于JDBC连接数据库的文章,以解决一部分人的疑问。
     第一:JDBC是JDK的一部分(至少在
Java Tiger Development Kits中是这样),使用JDBC直接在程序文件中写import java.sql.*;即可使用了。
     第二:连接字符串的格式。本地连接的连接字符串格式为jdbc:product_name:database_name,远程连接的格式为:product_name://host_name/port_number:database_name。即如果我的
数据库名字为rdb,则本地连接字符串为jdbc:db2:rdb(当然rdb一定是处于DB2的默认实例之中的),而远程连接字符串为jdbc:db2://192.168.1.10/50000:rdb(这里192.168.1.10为数据库所在服务器IP地址,而50000为DB2连接服务的端口号)。
     第三: 安装DB2
数据库提供的为JDBC准备的类库(在.NET中叫Provider,在Java中怎么叫还没研究过)。查找IBM DB2 UDB的安装目录或者Java  Tiger的JDK目录你会找到db2java.zip,把它先做一个副本以后就它最有用了。现在我们开始讨论数据库连接的程序代码。

     应用
程序连接DB2数据库
先将db2
java.zip解压缩,把COM目录转移到代码的当前目录,然后我们开始注册这个Provider的实例,代码为:
     Class.forName(“COM.ibm.db2.jdbc.app.DB2Driver”).newInstance();
     Class.forName(“COM.ibm.db2.jdbc.net.DB2Driver”).newInstance():
这两句任选其一,作用稍有不同,前者是具有DB2客户端的Provider实例,后者是无DB2客户端的Provider实例。
此后再写Connection con=DriverManager.getConnection();即可得到
数据库连接的实例。

   
JSP中连接DB2数据库
   这里以Tomcat作为Servlet容器,如果想在Tomcat中使用DB2 
   Provider必须把db2
java.zip更名成db2java.jar然后复制到tomcat主目录下c

Class.forName("com.ibm.db2.jdbc.app.DB2Driver").newInstance();
String url="jdbc:db2://localhost:5000/sample"; //sample为你的数据库名
String user="admin";
String password="";
Connection conn= DriverManager.getConnection(url,user,password);
  

你可能感兴趣的:(socket,null,db2,float,windows,struct)