第十五章 联网
15.1联网背景
套接字是一个网络连接的端点。域名是为了方便记忆。
略,还有很多很基本的概念。
15.2处理地址和主机名
socket模块提供了几个函数处理主机名和地址。socket模块包装了C套接字库,并且与C版本一样,也支持所有种类的选项。模块内定义了直接映射到C等价的变量。
gethostname()将返回当前计算机的主机名。gethostbyname(name)将试图把指定主机名分解成IP地址。失败将产生异常。我测试的机器没有连接,抛出错误为:
socket.gaierror: (11001, 'getaddrinfo failed')
扩展格式gethostbyname_ex(name)将返回一个三元组,包含特定地址的主要主机名,相同IP地址的可选择机器名列表,以及一个主机界面上的其他IP地址列表:
>>> socket.gethostbyname('www.yahoo.com')
'64.58.76.178'
>>> socket.gethostbyname_ex('www.yahoo.com')
('www.yahoo.akadns.net',['www.yahoo.com'],
['64.58.76.178','64.58.76.176','216.32.74.52',
'216.32.74.50','64.58.76.179','216.32.74.53',
'64.58.76.177','216.32.74.51','216.32.74.55'])
gethostbyaddr(address)完成同样的任务,只不过参数是IP
getservbyname(service,protocol)获取一个服务名,如telnet或ftp,和一个协议,如tcp或udp。并返回服务端口号。
>>> socket.getservbyname('http','tcp')
80
通常,非Python程序以32位的打包格式存储和使用IP地址:
inet_aton(ip_addr)
inet_ntoa(packed)
这两个函数在32位打包格式和IP地址字符串之间进行转换。如:
>>> socket.inet_aton('177.20.1.201')
'/261/024/001/311' # 一个4字节的字符串
>>> socket.inet_ntoa('/x7F/x00/x00/x01')
'127.0.0.1'
socket定义了几个保留IP地址:
INADDR_ANY和INADDR_BROADCAST是保留的IP地址,用于任意IP和广播。INADDR_LOOPBACK引用127.0.0.1这个地址的环回设备。几个保留IP地址都是保留的32位格式。
getfqdn([name])函数返回主机名的合格域名,省略name则返回本机的。是Python2.0中的新特性:
>>> socket.getfqdn('')
'dialup84.lasal.net'
15.3与低层套接字通信
15.3.1创建和撤消套接字
socket模块的socket(family,type[,protocol])创建一个新的套接字对象。family常取AF_INET,偶尔也用其他值如AF_IPX,取决于平台。type最常用的是SOCK_STREAM(就是TCP)或SOCK_DGRAM(就是UDP)。
family和type的组合就可以指定一个协议了,但是仍然可以指定protocol参数,如IPPROTO_TCP或IPPROTO_RAW。可用函数getprotobyname(proto)获取协议的代码:
>>> getprotobyname('tcp')
6
>>> IPPROTO_TCP
6
fromfd(fd,family,type[,proto])用于从打开的文件描述符(文件的fileno()方法获得)创建套接字对象。套接字对象的方法fileno()将返回这个套接字的文件描述符(一个整数)。
用完套接字之后应该用close()方法关闭。显式的关闭较好,尽管可以自动关闭,使用弱引用?。也可用shutdown(how)方法关闭一端或两端,参数为0时停止接受,1停止发送,2双方停止。
15.3.2连接套接字
一个TCP连接,监听端先要bind(address)一个地址和端口的元组,之后调用listen([backlog])侦听传入连接,之后用accept()接受新的传入连接。
>>> s=socket(AF_INET,SOCK_STREAM)
>>> s.bind(('127.0.0.1',4444))
>>> s.listen(1)
>>> q,v=s.accept() #返回套接字q和地址v
上面的代码在未接到连接时会挂起。只要另外启动一个解释器连接即可,连接函数connect(address)。
发送数据q.send('hello'),返回已发送的字节数
接受数据s.recv(1024),最多一次接受1024字节的数据,返回数据
"地址"是由IP地址和端口构成的二元组。
扩展函数connect_ex(address)如果遇到调用的错误则返回错误代码,成功返回0,而不会产生异常,更接近于C socket。
调用listen时需要的参数是暂时无法响应时保存连接的队列长度,一般设为5。socket.SOMAXCONN变量指明了最大允许的数量。
accept()方法返回一个地址,包含IP和端口。
UDP套接字不是面向连接的,但仍可用connect()。
15.3.3发送和接收数据
send(string[,flags])发送指定数据到远程套接字。sendto(string [,flags],address)把指定字符串发送到特殊地址。通常send用于面向连接的,sendto用于无连接的。但是如果UDP套接字调用了connect(),就可以用send而不必是sendto了。
都返回实际发送的字节数。如下函数用于确保发送整个消息:
def safeSend(sock,msg):
sent=0
while msg:
i=sock.send(msg)
if i==-1: # Error
return -1
sent+=i
msg=msg[i:]
time.sleep(25) #等待队列为空
return sent
这样当一次发送不完时也可以持续的发送数据,并最终返回发送成功的字节数。
recv(bufsize[,flags])方法接收数据,如果有很多数据则只返回前bufsize字节的数据。recvfrom(bufsize[,flags])完成同样的任务,只是带有返回信息(data,(ipAddress,port))的AF_INET套接字,可以看到信息的发源地。
send、sendto、recv、recvfrom都接受一个flags参数,默认值为0。可以把一些socket.MSG_*变量按位OR操作之后创建flags值。可用值的平台相关的。
MSG_OOB处理超区段数据
MSG_DONTROUTE不使用路由表,直接发送到界面
MSG_PEEK在不从队列删除的情况下,返回等待数据
例如使用MSG_PEEK就可以偷偷的查看消息而不必从数据队列中删除。
makefile([mode[,bufsize]])方法返回一个类似文件的对象,该对象包装了这个套接字。这样可以使用文件的操作方法操作套接字。可选的mode和bufsize参数获取的值与内置的open函数相同。
经测试在Windows2000SP4+Python2.4.2下的UDP编程中,最大允许发送的UDP数据报为65527字节,而这个数据报是收不到的。最大能够接收的数据报大小为65507字节。在65507到65527字节之间的数据报发送会丢失,发送端不会出错,接收端却不会收到。对于超过65527字节的数据报会发生socket.error错误,winsock错误号为10040,信息"Message too long" 。这个问题同样存在于Linux中。经测试linux-2.6.9内核中这个数据长度区间是62501到65507字节。
在Windows2000SP4+Python2.4.2下尝试在recv中使用MSG_OOB标志,但是返回10045号winsock错误,提示"Operation not supported"。另外,如果还没有bind()就使用recv()接收数据会发生10022号winsock错误,提示"Invalid argument"。
15.3.4使用套接字选项
套接字getpeername()和getsockname()方法将返回一个2元组,包含IP地址和端口。getpeername()返回远程套接字信息,getsockname()返回本地套接字信息。
默认情况下的套接字是阻塞的,例如发送缓冲区已满的情况下,send函数也会阻塞,直到可以把更多的数据写入到缓冲区。通过采用0值调用setblocking(flag)方法,就可以改变这种行为。
在非阻塞时,如果发现recv()方法没有返回数据则会发生错误socket.error。Winsock错误号为10035。在套接字运行中也可以随时改变阻塞状态。在非阻塞状态中,一般放入一个带有延时的循环中。在延时发生的时候,其他线程可以改变循环的进入条件,控制程序合法退出。
在非阻塞时,也可以在监听套接字上用select或poll来检测是否有新的连接到达。
setsockopt(level,name,value)和getsockopt(level,name[,buflen ])方法用于设置和返回套接字选项。level参数指定引用选项的层,如套接字层、TCP层、IP层。level的值以SOL_开头,如SOL_SOCKET、SOL_TCP等。选项的名字指定了正在交换的选项,socket模块定义了读者的平台上可以使用的选项。
C版本的setsockopt要求用缓冲区传递value参数,但在Python中,这个选项输入数字,可直接用数字或用字符串(代表缓冲区)。
getsockopt函数不指定buflen时会返回数字值,而提供了buflen时返回一个字符串(代表缓冲区),并且最大长度为buflen个字节。
各个平台的选项不同,各个选项的值类型也不尽相同。如设置发送缓冲区为65KB:
>>> s.setsockopt(SOL_SOCKET,SO_SNDBUF,65535)
SOL_SOCKET的选项(选项名、值、说明):
SO_TYPE(只读)套接字类型(如SOCK_STREAM)
SO_ERROR(只读)套接字的最后一个错误
SO_LINGERBoolean如果数据存在则拖延close
SO_RCVBUFNumber接收缓冲区大小
SO_SNDBUFNumber发送缓冲区大小
SO_RCVTIMEOTime struct接收超时
SO_SNDTIMEOTime struct发送超时
SO_REUSEADDRBoolean启用本地地址/端口的多个用户
SOL_TCP选项:
TCP_NODELAYBoolean立即发送数据,不等待最小发送量
SOL_IP选项:
IP_TTL0-255包能够传送的最大转发器数
IP_MULTICAST_TTL0-255包能够广播的最大转发器数
IP_MULTICAST_IF inct_aton(ip)选择界面,以便通过它传输
IP_MULTICAST_LOOPBoolean启用发送器,以接收它发送的多
点传送包的副本
IP_ADD_MENBERSHIPip_mreq联接一个多点传送组
IP_DROP_MENBERSHIPip_mreq去掉多点传送组
15.3.5转换数字
平台字节序和网络字节序多半不同,所以通过网络传输之前需要先转换。nthol(x)和ntohs(x)函数从网络数字转换到本地字节序。htonl(x)和htons(x)函数从本地字节序转换到网络字节序。
15.4示例:多点传送的聊天应用程序
一个聊天程序,允许在共享的白板上绘画。
使用IP多播实现,代码太长见page224-228。
在互联网上运行会有问题,因为有些路由器不允许IP多播的转发。而且设置TTL最好足够大。SO_REUSEADDR选项允许一个地址(IP,port)允许被绑定到多个socket。IP_ADD_MEMBERSHIP连接一个IP多播组,退出多播组用IP_DROP_MENBERSHIP。
15.5使用SocketServers
15.5.1SocketServer族
TCPServer和UDPServer都是SocketServer的子类。
SocketServer模块同时提供UnixStreamServer(TCPServer的子类)和UnixDatagramServer(UDPServer的子类)。他们的父类相同,只是监听套接字是AF_UNIX族,而不是AF_INET。
默认的套接字服务器一次处理一个连接,但可以使用ThreadingMix和ForkingMixIn类创建SocketServer的线程版本或分支版本。还有一些已经实现好的类:ForkingUDPServer、ForkingTCPServer、 ThreadingUDPServer、ThreadingTCPServer、ThreadingUnixStreamServer、ThreadingUnixDatagramServer。线程版本只能在支持线程的操作系统上运行。分支版本也只可以在支持os.fork的平台上运行。
关于混合类见第7章;分支的解释见11章;线程见26章。
SocketServer以常规方式处理传入的连接,可以自己定义请求处理程序类,只要继承BaseRequestHandler类即可。然后将此类传递给构造函数即可。
import SocketServer
...#创建请求处理程序类
addr=('IP',port)
server=SocketServer.ThreadingTCPServer(addr,请求处理程序类)
server.serve_forever()
这样每次有新的连接时就创建一个"请求处理类"的实例对象,并调用他的handle()方法。代替用server_forever,也可用handle_request() ,可用于等待、接收单个连接。server_forever()仅限在无限循环中调用handle_request()。
如果需要创建自己的套接字服务器类,需要实现如下方法:
在__init__()函数中调用server_bind()方法,把self.socket绑定到正确的地址self.server_address。然后调用server_activate()激活服务器,默认情况下还会调用套接字的listen()方法。
在调用handle_request()或serve_forever()之前,套接字服务器不会进行任何操作。handle_request()调用get_request(),并等待接受一个新的套接字连接,然后调用verify_request(request,client_address) ,以便查看服务器是否应该处理该连接(可以把这种方法用于访问控制-默认情况下,verify_request()总是返回真)。如果处理请求成功,则handle_request()会调用process_request(request,client_address),如果process_request()产生了异常,则调用handle_error(request, client_address)。默认时,process_request()简单的调用finish_request(request,client_address)。
分支和线程混合类将会启动一个分支或线程来调用finish_request。finish_request()是一个新的请求处理程序,接下来会调用他的handle()方法。
当SocketServer创建一个新的请求处理程序时,会把处理程序的__init__函数传递给self变量。
SocketServer的fileno()方法返回监听套接字的文件描述符。address_family成员变量指定了监听套接字的套接字族,如AF_INET。 server_address保留了监听套接字绑定到的地址。socket变量保留了监听套接字本身。
15.5.2请求处理程序
包含setup()、handle()和finish()方法,而且默认情况下他们不作任何事情,可以通过重载来定制自己的行为。BaseRequestHandler的__init__函数调用setup()完成初始化工作,调用handle()处理请求,调用finish()进行清除工作。如果handle()和setup产生了异常则不会进入finish。
每个请求创建了请求处理程序的一个新实例。
request成员变量包含流(TCP)服务器中新接受的套接字。对数据报(UDP)服务器,它是一个元组,包含传入的消息及监听套接字。client_address保存发送者地址,server具有对SocketServer的引用。
下面是一个Echo协议的例子EchoRequestHandler:
import SocketServer
class EchoRequestHandler(/
SocketServer.BaseRequestHandler):
def handle(self):
print 'Got new Connection!'
while True:
msg=self.request.recv(1024)
if not msg:
break
print 'Recived:',msg
self.request.send(msg)
print 'Done with connection'
if __name__=='__main__':
server=SocketServer.ThreadingTCPServer(/
('127.0.0.1',12321),EchoRequestHandler)
server.handle_request() #开始等待一个连接
下面的程序构造一个客户端进行连接:
>>> from socket import *
>>> s=socket(AF_INET,SOCK_STREAM)
>>> s.connect(('127.0.0.1',12321))
>>> s.send('Hello!')
6
>>> print s.recv(1024)
Hello!
>>> s.close()
SocketServer模块也定义了BaseRequestHandler的两个子类:StreamRequestHandler和DatagramRequestHandler。他们重载了setup和finish两个方法,并创建了两个文件对象rfile和wfile,可以使用他们进行对客户端的读写数据。
15.6处理Web浏览器请求
有了SocketServer以后,还可以扩展用于各种WEB请求:BaseHTTPServer、SimpleHTTPServer、CGIHTTPServer。
可以把他们作为建立程序的起点。至少应该获得一些内容:
import SocketServer,CGIHTTPServer
SocketServer.ThreadingTCPServer(('127.0.0.1',80),/
CGIHTTPServer.CGIHTTPRequestHandler).serve_forever()
这时通过浏览器浏览http://127.0.0.1/file,这里的file是程序当前目录下的一个文件。
15.6.1BaseHTTPRequestHandler
WEB服务器请求处理程序的公共基类是BaseHTTPServer模块里的BaseHTTPRequestHandler类,是StreamRequestHandler的子类。它接受一个HTTP连接,调用适当的方法处理请求。
BaseHTTPRequestHandler的子类不应该重载__init__方法和handle方法,但对每一个HTTP命令,应该实现一个形如do_<command>的方法,如do_PUT()方法。这样相关请求会自动转到对应方法。
原始请求存放在raw_request实例变量中,并有属性,如command存储HTTP命令(如GET、POST等),path(例如/index.html), request_version(如HTTP/1.0),headers是mimetools.Message的一个实例,包含解析版本的请求标题。
mimetools.Message类的信息参阅17章,另外可以更改BaseHTTPRequestHandler.MessageClass类变量的值来指定另外一个类读取和解析标题。
使用rfile和wfile可以读写数据。如果请求包含了超过请求标题的其他数据,则rfile会依据处理程序调用适当的do_<command>的时间定位数据的开始位置。
有两个版本信息server_version和protocol_version,分别是服务器HTTP协议版本和协议版本,默认为HTTP/1.0。
在do_<command>方法中,发送输出应该用send_response(code [,message])方法,code是一个HTTP代码(如200),message是文本消息,而调用此方法时BaseHTTPRequestHandler会添加Date和服务器标题。
如果调用发生错误,可以调用send_error(code[,message])。
调用send_response之后,可以在需要时用send_header(key,value) ,以便写出MIME标题,完成处理之后调用end_headers():
def do_GET(self):
self.send_response(200)
self.send_header('Content-type','text/html')
self.send_header('Content-length',len(data))
self.end_headers()
#发送剩余数据
还可以使用日志工具,调用log_request([code[,size]]),可以记录代码和size。log_message(format,arg0,arg1,...)多用途记录,有格式的输出。
self.log_message('%s :%d','Time taken',425)
这个讲采用NCSA httpd记录格式,每个请求自动记录在stdout中。
15.6.2SimpleHTTPRequestHandler
由于BastHTTPRequestHandler并不处理HTTP命令,而SimpleHTTPServer模块中的SimpleHTTPRequestHandler类可以处理客户机的文件请求(当前目录或子目录中),来对HEAD和GET命令的支持。如果请求的文件是一个目录则会返回目录列表的WEB页面。
如下例子在8000端口开启WEB服务器,会启动线程:
>>> import Webbrowser,threading,SimpleHTTPServer
>>> def go():
... t=SimpleHTTPServer.test
... threading.Thread(target=t).start()
... Webbrowser.open('http://127.0.0.1:8000')
>>> go()
Serving HTTP on port 8000
endor --[28/Dec/2000 18:00:48] "GET /3dsmax3/ HTTP/1.1" 200
endor --[28/Dec/2000 18:00:50] "GET /3dsmax3/Maxsdk/
HTTP/1.1" 200 -
endor --[28/Dec/2000 18:00:53]"GET /3dsmax3/Maxsdk/Include/
HTTP/1.1" 200 -
SimpleHTTPServer模块中的test()函数在8000端口上启动了一个简单的服务器。
这个类除了从BaseHTTPRequestHandler继承之外还包含extensions_map词典,把文件扩展名映射为MIME数据类型,这样可以使客户的WEB浏览器正确处理接受的文件。可以自己扩展这个列表。
15.6.3CGIHTTPRequestHandler
CGIHTTPRequestHandler在CGIHTTPServer模块中,提供了CGI的支持。
CGI一直有安全性的问题,需要自己学习一下。
对每个到达的GET或POST命令,先检查是否是CGI程序,如果是则以外部程序的方式启动,如果不是则发送文件。只有CGI程序支持POST方法。
CGIHTTPRequestHandler会在cgi_directories成员列表中检查文件是否是CGI程序的路径,一般指/cgi-bin和htbin两个目录。如果文件存在于响应目录或子目录中,而且该文件是Python模块或可执行文件则开始执行,并发送输出到客户端。
15.6.4示例:形成处理程序CGI脚本
本节示例CGIHTTPRequestHandler的使用。
如下是HTML表单,保存在一个特定的路径,如c:/temp:
<html><body>
<form method=GET
action="http://127.0.0.1:8000/cgi-bin/handleForm.py">
Your Name:<input name="User">
<input type="Submit" value="Go!">
</form>
</body></html>
在如上目录下建立目录cgi-bin,放入CGI脚本handleForm.py:
import os
print "Content-type: text/html/r/n<html><body>"
name=os.environ.get('QUERY_STRING','')
print 'Hello, %s!<p>' % name[len('User='):]
print '</body></html>'
在刚才的c:/temp目录下启动解释器:
>>> import CGIHTTPServer
>>> CGIHTTPServer.test()
打开浏览器http://127.0.0.1:8000/form.html,查看CGI功能。
CGIHTTPRequestHandler能把用户信息保存在环境变量中。
写入stdout的信息,采用print或sys.stdout.write,会返回到客户端,其信息可以是文本或二进制数据。
CGIHTTPRequestHandler输出一些响应标题,但在需要时可以添加。
在标题的后面,一定要在数据之前加入一个空行
在UNIX上,外部程序会与nobody用户ID一起运行。
15.7在不使用线程的情况下处理多个请求
线程之间的切换要耗费大量的时间。可以使用select或asyncore模块一次处理多个请求,避免无意义的线程切换。
select模块中的select(inList,outList,errList[,timeout])函数获取三个列表,这些对象正在等待I/O,或者等待错误。select将返回3个列表,即原始对象的子集,他们包含那些无需阻塞就可以完成I/O的对象。还可以指定timeout超时值,浮点数,等待的秒数。指的是select的超时,即当timeout到达时如果没有有效结果就返回空列表。当timeout为0时则进行快速检查而不进行阻塞。
三个列表分别指出了需要等待的结果,都可以为空,对象也可以是整型文件描述符或类似文件的对象(带有fileno()方法)。详细参见第10章。
通过使用select,可以启动几个读写操作来代替阻塞操作。是编写高性能服务器的好方法。
在Windows系统上select()只对套接字起作用,UNIX系统上可对任何文件描述符起作用。
在UNIX下还可用select.poll()返回一个轮询对象。之后用register(fd[,eventmask])注册特殊的文件描述符(或带有fileno()方法的对象)。可选的eventmask可由如下几个值OR连接:
select.POLLIN用于输入
select.POLLPRI紧急输入
select.POLLOUT用于输出
select.POLLERR错误
可以注册所需的文件描述符,并且通过轮询对象的unregister(fd)方法从对象中删除。调用轮询对象的poll([timeout])方法,可以查看那些准备完成I/O而无需阻塞的文件描述符。poll会返回(fd,event)格式的元组列表,是状态已经更改的文件描述符。事件可是几个eventmask的OR连接,也可是POLLHUP(挂起)或POLLNVAL(无效的文件描述符)。
15.7.1asyncore
提供了方便的将select和poll用于套接字的方法。
asyncore定义了dispatcher类,围绕常规套接字的包装程序。包含connect(addr)、send(data)、recv(bufsize)、listen([backlog])、 bind(addr)、accept()、close()方法。
创建套接字create_socket(family,type)。
当不同的事件发生时,asyncore会调用程序对象的方法。例如可以在无需阻塞的情况下写入到套接字,调用handle_write()方法。当数据可用于读时,调用handle_read()。连接成功时调用handle_connect()。关闭时调用handle_close()。而且在socket.accept()不会阻塞,将实现handle_accept()。
asyncore的dispatcher对象的readable()和writable()方法查看是否出于可以进行读写的状态,默认总是返回True,可以自己重载,避免没有数据可读时浪费时间检查数据。
将事件放入调度程序对象中,asyncore.poll([timeout])。在UNIX系统上可用asyncore.poll2([timeout])使用轮询,来代替select()或asyncore.loop([timeout])。这些函数用select模块检查I/O状态的更改,并生成事件。poll只检查一次(默认超时为0秒),loop会一直检查,直到没有更多的、为readable或writable返回真值的调度程序对象为止,或者到达了timeout (默认值为30秒)。
下面的示例从WEB站点检索index.html页面,并写入到磁盘。
import asyncore,socket
class AsyncGet(asyncore.dispatcher):
def __init__(self,host):
asyncore.dispatcher.__init__(self)
self.host=host
self.create_socket(socket.AF_INET, /
socket.SOCK_STREAM)
self.connect((host,80))
self.request='GET /index.html HTTP/1.0/r/n/r/n'
self.outf=None
print 'Requesting index.html from',host
def handle_connect(self):
print 'Connect',self.host
def handle_read(self):
if not self.outf:
print 'Creating',self.host
self.outf=open(self.host,'wt')
data=self,recv(8192)
if data:
self.outf.write(data)
def writeable(self):
return len(self.request)>0
def handle_write(self):
#并非所有数据都被发送,所以跟踪检查一下
num_sent=self.send(self.request)
self.request-self.request[num_sent:]#一种return
def handle_close(self):
asyncore.dispatcher.close(self)
print 'Socket closed for',self.host
if self.outf:
self.outf.close()
#获取一些数据
AsyncGet('www.yahoo.com')
AsyncGet('www.cnn.com')
AsyncGet('www.python.org')
asyncore.loop() #等待完成
通过使用这些方法,I/O-bound程序把时间花费在最大性能界线(I/O)上,而不是把时间浪费在无用的线程切换上。
15.8小结
略...
完成...
---------------------------------------------------------
第十九章 使用Tkinter
19.1Tkinter简介
Tkinter用于在Python程序中建立GUI,在部分系统如UNIX、Windows和Macintosh上是Python的标准库,并且一般是捆绑的。
Tkinter易于使用,建立在高级脚本编程语言Tcl的基础之上。
下例是一个很短的例子,演示了建立GUI:
import Tkinter
#创建一个根窗口
root=Tkinter.Tk()
#将一个label放入窗体
LabelText="Ekky-ekky-ekky-ekky-z'Bang,zoom-Boing,/
z'nourrrwringmm"
LabelWidget=Tkinter.Label(root,text=LabelText)
#打包以适应显示
LabelWidget.pack()
#开启主循环,并显示,主循环不会立即返回,窗口关闭时返回
root.mainloop()
在Windows系统上使用pythonw.exe而不是python.exe来运行会显得更加专业。脚本的扩展名使用.pyw而不是.py会发送到pythonw。pythonw不会创建控制台窗口。使用.py结尾时窗口关闭之前,通过命令行调用的模块不会返回。
19.2创建GUI
19.2.1用部件建立界面
GUI包含不同的部件,对应Java中的组件,Microsoft的控件。大多数部件显示在父部件上,或者所有者上。构造部件的第一个参数的就是他的父部件。
Toplevel部件是特殊的部件,他没有上层。大多数程序需要一个Toplevel,即调用Tkinter.Tk()时创建的根部件。
例如帧是一个部件,存在的目的就是包含其他部件。
root=Tkinter.Tk() #创建一个top-level窗体
frame=Tkinter.Frame(root)
#其他部件安装在Frame之上
label=Tkinter.Label(frame,text="***")
label.pack()
button=Tkinter.Button(frame,text="button")
button.pack()
frame.pack()
frame.mainloop() #或root.mainloop()
19.2.2部件选项
部件具有选项(属性)。如大多数部件有background选项,控制背景色。按钮有command选项指定单击按钮需要调用的函数(没有参数)。
可用多种不同的方式访问选项:
NewLabel=Tkinter.Label(ParentFrame,background='gray50')
NewLabel['background']='#FFFFFF'
NewLabel.config(background='blue')
CurrentColor=NewLabel['background']
CurrentColor=NewLabel.cget('background')
由于有些选项是Python保留字,使用时要在选项名之后加下划线,例如from:
VolumeWidget=Tkinter.Scale(ParentFrame,from_=0,to=200)
VolumeWidget['from']=20 #这里不是用from_
19.3部件布局
geometry manager(布局管理器)负责在平面上安排布局。最简单的布局是packer。packer可以安排在父部件的左、右、顶、底。调用pack方法就是调用packer。
grid布局管理器将父部件划分为网格,并将每个子部件安排在一个方格上。(by gashero)通过在部件上的grid(row=x,column=y)激活网格布局管理器。方格编号从0开始。
place准确安排部件位置。
不同的布局管理器很难协调工作。必须在每个部件上调用布局管理器方法,否则不会显示。
19.3.1packer选项
下面就是可以传递给pack方法的选项。默认的包装是在父部件中从上到下的布局(side=TOP),每个部件都居中(anchor=CENTER)。并不会扩展以填充空间(expand=NO),边缘没有额外的填充(padx=pady=0)。
side选项指定部件放在父部件的某个指定边,可用LEFT、RIGHT、 TOP、BOTTOM。默认是TOP。如果两个部件都布置在父部件的同一个边缘,则第一个安排的部件和边缘最靠近。
在同一个父部件中混合使用LEFT/RIGHT和TOP/BOTTOM通常产生很烂的外观。封装很多部件时一般用中间帧或使用网格布局。
expand传递YES使得部件扩展以填充所有的空间。fill传递X,Y,或者BOTH决定扩展哪一个方向。适用于用户控制窗口大小。如下代码将创建画布,并拉伸,状态栏水平拉伸。
DrawingArea=Tkinter.Canvas(root)
DrawingArea.pack(expand=Tkinter.YES,fill=Tkinter.BOTH)
StatusBar=Tkinter.Label(root,text="Ready.")
StatusBar.pack(side=Tkinter.BOTTOM,expand=/
Tkinter.YES,fill=Tkinter.X)
如果部件具有比他需要的更多的屏幕空间,则锚选项决定了部件位于所分配空间的位置,不影响fill=BOTH的部件。有效值是罗盘方向:N、 NW、W、SW、S、SE、E、NE、CENTER。
padx、pady给予部件一些边缘,防止一些误操作,并易于理解。指的是部件距离父部件边缘的象素数。是至少要保留的边缘宽度,而不是指定精确的。
19.3.2网格选项
传递给grid方法的选项。必须为每个部件指定行列值,否则会有不可预料的后果。
row,column指定部件所处方格,0始。总是可以添加新的行列。如下是建立电话号码播盘:
for Digit in range(9):
Tkinter.Button(root,text=Digit+1).grid(row=Digit/3,/
column=Digit%3)
sticky指定部件应该靠近方格的哪一个边,和packer的anchor类似。有效值是罗盘方向和CENTER。可以组合值,来在单元格内拉伸部件。如下就是一个在单元格内拉伸:
BigButton=Tkinter.Button(root,text='X')
BigButton.grid(row=0,column=0,sticky=Tkinter.W+Tkinter.E+/
Tkinter.N+Tkinter.S)
columnspan,rowspan用于创建跨越多行或多列的大部件。
19.4示例:Breakfast按钮
通过几个按钮来生成列表:
import Tkinter
#在Tkinter中常见的方式是生成Tkinter.Frame的子类,并在子类中
#生成GUI
class FoodWindow(Tkinter.Frame):
def __init__(self):
#调用父类的构造方法
Tkinter.Frame.__init__(self)
self.FoodItems=[]
self.CreateChildWidgets()
def CreateChildWidgets(self):
ButtonFrame=Tkinter.Frame(self)
ButtonFrame.pack(side=Tkinter.TOP,fill=Tkinter.X)
SpamButton=Tkinter.Button(ButtonFrame)
SpamButton['text']='Spam'
SpamButton['command']=self.BuildButtonAction('spam')
SpamButton.pack(side=Tkinter.LEFT)
EggsAction=self.BuildButtonAction('Eggs')
EggsButton=Tkinter.Button(ButtonFrame,test='Eggs',/
command=EggsAction)
EggsButton.pack(side=Tkinter.LEFT)
Tkinter.Button(ButtonFrame,text='Beans',/
command=self.BuildButtonAction('Beans'))./
pack(side=Tkinter.LEFT)
SausageButton=Tkinter.Button(ButtonFrame)
SausageAction=self.BuildButtonAction('Sausage')
SausageButton.config(text='Sausage',command=/
SausageAction)
SausageButton.pack(side=Tkinter.LEFT)
#这里保存一个label的引用,以便以后更改显示文字
self.FoodLabel=Tkinter.Label(self,wraplength=190,/
relief=Tkinter.SUNKEN,borderwidth=2,text='')
self.FoodLabel.pack(side=Tkinter.BOTTOM,pady=0,/
fill=Tkinter.X)
self.pack()
def ChooseFood(self,FoodItem):
#添加一个项到食物列表,并显示
self.FoodItems.append(FoodItem)
LabelText=''
TotalItems=len(self.FoodItems)
for Index in range(TotalItems):
if Index>0:
LabelText+=', '
if TotalItems>1 and Index==TotalItems-1:
LabelText+='and '
LabelText+=self.FoodItems[Index]
self.FoodLabel['text']=LabelText
def BuildButtonAction(self,Label):
#使用lambda可以方便的定义按钮常见行为
Action=lambda Food=self,Text=Label: /
Food.ChooseFood(Text)
return Action
if __name__=='__name__':
MainWindow=FoodWindow()
MainWindow.mainloop()
19.5使用常见选项
19.5.1颜色选项
background,foreground前景色和背景色,同bg和fg
activebackground,activeforeground对菜单和按钮的激活状态颜色
disabledforeground禁用按钮或菜单时的前景色
selectforeground,selectforegroundCanvas,Entry,Text或者
Listbox部件的选定元素
highlightcolor,highlightbackground围绕菜单的矩形颜色
19.5.2大小选项
width:部件宽度,以部件字体的平均大小测量,默认的0值代表仅仅容纳当前文本
height:部件高度,以平均字符大小测量
padx,pady:部件外的距离,象素,如部件显示图像则忽略
19.5.3外观选项
text:在部件中显示的文本
image:按钮或选项卡上显示的图像,如有图像则忽略text,之后再传递字符串则删除图像
relief:为部件指定3D边界。有效值为FLAT、GROOVE、RAISED、 RIDGED、SOLID、SUNKEN
borderwidth:部件的3D部件宽度,象素
font:文本字体
19.5.4行为选项
command:单击部件时调用的函数,没有参数,适于按钮、标尺、滚动条
state:部件状态为NORMAL、ACTIVE或DISABLED。DISABLED部件忽略用户输入,并通常以灰色显示。ACTIVE为激活状态
underline:可以使用键盘快捷方式。是部件文本内字母的索引,这个字母成为部件的热键。
takefocus:如为真,则是TAB列表的一部分,可用TAB切换到
19.6搜集用户输入
Entry部件获取输入的一行文本,Checkbox获取布尔值。这些部件将值存放在Tkinter变量中,包括StringVar、IntVar、DoubleVar、 BooleanVar。每个Tkinter变量提供了set和get方法来存取。
>>> Text=Tkinter.StringVar()
>>> Text.get()
''
>>> Text.set('Howdy!')
>>> Text.get()
'Howdy!'
通过设置某个部件选项,就可以将部件与变量相联系。复选按钮一般选BooleanVar,用variable选项来添加:
SmokingFlag=BooleanVar()
B1=Checkbutton(ParentFrame,text='Smoking',/
variable=SmokingFlag)
SmokingFlag.set(1)
Entry和OptionMenu部件一般使用StringVar,用textvariable选项:
PetBunnyName=StringVar()
NameEntry=Entry(ParentFrame,text='Bubbles',/
textvariable=PetBunnyName)
ChocolateName=StringVar()
FoodChoice=OptionMenu(ParentFrame,ChocolateName,/
'Crunchy Frog','Sprint Surprise','Anthrax Ripple')
几个Radiobutton可以共享一个变量,加到variable选项上,value选项存储那个按钮的值。
一些部件如Listbox和Text使用定制方法(不是Tkinter变量)来访问内容。
19.7示例:打印奇特的文本
下例打印不同颜色和大小的文本。还使用了Tkinter变量来搜集用户输入。
需要引入import tkFont模块来导入字体类。
构造方法初始化各个Tkinter变量,设置初始值。
有一个CreateWidgets()方法用于创建部件并显示。
PaintText()方法更改字体。
def PaintText(self):
if self.TextItem!=None:
self.TextCanvas.delete(self.TextItem)
if self.BoldFlag.get():
FontWeight=tkFont.BOLD
else:
FontWeight=tkFont.NORMAL
#创建并配置字体对象,获取字体列表
TextFont=tkFont.Font(self,'Courier')
TextFont.configure(size=self.FontSize.get(),/
underline=self.UnderlineFlag.get(),/
weight=FontWeight)
self.TextItem=self.TextCanvas.create_text(5,5,/
anchor=Tkinter.NW,text=self.Text.get(),/
fill=self.ColorName.get(),font=TextFont)
19.8使用文本部件
文本部件(Tkinter.Text)是多行文本编辑部件,甚至可以嵌入窗口和图形。其内容可以按照行列来索引,行是1始,列是0始。所以文本部件的开头索引是1.0。也可以用特殊索引END、INSERT(插入光标位置)、CURRENT(鼠标位置)。
可在文本部件内用get(start[,end])检索文本,将从start返回文本,直到但是不包括end。如果忽略end则返回start处的一个字符。
TextWidget.get('1.0',Tkinter.END)
TextWidget.get('3.0','4.0')#返回第3行
TextWidget.get('1.5')
delete(start[,end])按照与get相同的范围删除文本。insert(pos, str)在索引pos之前插入字符串str:
TextWidget.insert('1.0','bob')
TextWidget.insert(Tkinter.END,'bob')
TextWidget.insert(Tkinter.CURRENT,'bob')
TextWidget.delete('1.0',Tkinter.END)#删除所有文本
19.9建立菜单
使用菜单部件Tkinter.Menu。之后添加入口来刷新菜单。添加指定的标签用add_command(label=?,command=?)。add_separator()添加菜单分组行。
add_cascade(label=?,menu=?)添加复选按钮到菜单上,可以为新的Checkbutton部件传递其他的选项(Variable)到add_checkbutton。
创建Menu的一个实例表示菜单栏本身,然后为每个真正的菜单创建一个Menu实例。菜单不可以被封装。使用Toplevel的menu选项将菜单添加到窗口。
root=Tkinter.Tk()
MenuBar=Tkinter.Menu(root)
root['menu']=MenuBar
FileMenu=Tkinter.Menu(MenuBar) #创建子菜单
FileMenu.add_command(label="Load",command=LoadFile)
FileMenu.add_command(label="Save",command=SaveFile)
MenuBar.add_cascade(label="File",menu=FileMenu)
创建弹出式菜单用tk_popup(x,y[,default])作为弹出菜单,并指定了位置,并可以指定默认值。
import Tkinter
def MenuCommand():
print 'Howdy!'
def ShowMenu():
PopupMenu.tk_popup(*root.winfo_pointerxy())
root=Tkinter.Tk()
PopupMenu=Tkinter.Menu(root)
PopupMenu.add_command(label="X",command=MenuCommand)
PopupMenu.add_command(label="Y",command=MenuCommand)
Tkinter.Button(root,text='Popup',command=ShowMenu).pack()
root.mainloop()
19.10使用Tkinter对话框
模块tkMessageBox提供了几个函数用于显示对话框。每个都有title和message参数来控制窗口标题和显示信息。
showinfo显示通知性信息
showwarning显示警告
showerror显示错误信息
askyesno显示YES和NO按钮,如果选择Yes则返回真
askokcancel显示OK和Cancel按钮,选择OK返回真
askretrycancel显示Retry和Cancel按钮,选择Retry返回真
askquestion和askyesno一样,只是用字符串返回YES或NO
一个在退出之前确认的对话框:
def Quit(self):
if self.FileModified:
if not tkMessageBox.askyesno("Confirm",/
'Really quit?'):
return #不退出
sys.exit()
19.10.1文件对话框
模块tkFileDialog提供了一些函数来打开文件选择对话框。函数askopenfilename让用户选择一个现存文件。asksaveasfilename让用户选择现存文件或新的文件名。两个函数都返回文件的完整路径,用户取消则返回空字符串。
可选的filetypes参数限制文件类型,filetypes是一个元组的列表格式如(描述,扩展名):
MusicFileName=tkFileDialog.askopenfilename(
filetypes=[('music files','mp3')])
19.11实例:文本编辑器
一个简单的文本编辑器,包含文件的打开、保存和编辑。说明了文本部件、菜单和一些标准对话框的使用:
import Tkinter
import tkFileDialog
import tkMessageBox
import os
import sys
#文件类型选择列表
TEXT_FILE_TYPES=[("Text files","txt"),("All files","*")]
class TextEditor:
def __init__(self):
self.FileName=None
self.CreateWidgets()
def CreateWidgets(self):
self.root=Tkinter.Tk()
self.root.title("New file")
MainFrame=Tkinter.Frame(self.root)
#创建文件菜单
MenuFrame=Tkinter.Frame(self.root)
MenuFrame.pack(side=Tkinter.TOP,fill=Tkinter.X)
FileMenuButton=Tkinter.Menubutton(MenuFrame,/
text='File',underline=0)
FileMenuButton.pack(side=Tkinter.LEFT,
anchor=Tkinter.W)
FileMenu=Tkinter.Menu(FileMenuButton,tearoff=0)
FileMenu.add_command(label="New",underline=0,/
command=self.ClearText)
FileMenu.add_command(label="Open",underline=0,/
command=self.Open)
FileMenu.add_command(label="Save",underline=0,/
command=self.Save)
FileMenu.add_command(label="Save as...",/
underline=0,command=self.SaveAs)
FileMenu.add_separator()
self.FixedWidthFlag=Tkinter.BooleanVar()
FileMenu.add_checkbutton(label='Fixed-width',
variable=self.FixedWidthFlag,command=self.SetFont
FileMenu.add_separator()
FileMenu.add_command(label='Exit',underline=1,/
command=sys.exit)
FileMenu.Button['menu']=FileMenu
#创建帮助菜单
HelpMenuButton=Tkinter.MenuButton(MenuFrame,/
text='help',underline=0)
HelpMenu=Tkinter.Menu(HelpMenuButton,tearoff=0)
HelpMenu.add_command(label='About',underline=0,/
command=self.About)
HelpMenuButton['menu']=HelpMenu
HelpMenuButton.pack(side=Tkinter.LEFT,
anchor=Tkinter.W)
#创建主菜单条
self.TextBox=Tkinter.Text(MainFrame)
self.TextBox.pack(fill=Tkinter.BOTH,/
expand=Tkinter.YES)
#pack the top-level widget:
MainFrame.pack(fill=Tkinter.BOTH,expand=Tkinter.YES)
def SetFont(self):
if self.FixedWidthFlag.get():
self.TextBox['font']='Courier'
else:
self.TextBox['font']='Helvetica'
def About(self):
tkMessageBox.showinfo('About textpad ...','hi')
def ClearText(self):
self.TextBox.delete('1.0',Tkinter.END)
def Open(self):
FileName=tkFileDialog.askopenfilename(
filetypes=TEXT_FILE_TYPES)
if FileName==None or FileName=='':
return
try:
File=open(FileName,'r')
NewText=File.read()
File.close()
self.FileName=FileName
self.root.title(FileName)
except IOError:
tkMessageBox.showerror("Read error...",
"Count not read '%s'"%FileName)
return
self.ClearText()
self.TextBox.insert(Tkinter.END,NewText)
def Save(self):
if self.FileName==None or self.FileName=='':
self.SaveAs()
else:
self.SaveToFile(self.FileName)
def SaveAs(self):
FileName=tkFileDialog.asksaveasfilename(
filetypes=TEXT_FILE_TYPES)
if FileName==None or FileName=='':
return
self.SaveToFile(FileName)
def SaveToFile(self,FileName):
try:
File=open(FileName,'w')
NewText=self.TextBox.get('1.0',Tkinter.END)
File.write(NewText)
File.close()
self.FileName=FileName
self.root.title(FileName)
except IOError:
tkMessageBox.showerror('Save error...',
"Could not save to '%s'"%FileName)
return
def Run(self):
self.root.mainloop()
if __name__=='__main__':
TextEditor().Run()
19.12处理颜色和字体
19.12.1颜色
由三个数字来定义,格式如#RGB、#RRGGBB、#RRRGGGBBB。一些著名的颜色:#FFFFFF是白色、#000000是黑色、#FF00FF是紫色。字符串越长,指定的颜色就越精确。
Tkinter也提供一些预定义颜色。如红绿等,也包含一些特别的颜色。
19.12.2字体
字体描述符是(family,size[,styles])的元组。如下例的一个字体:
root=Tkinter.Tk()
Tkinter.Button(root,text="Fancy",
font=('Helvetica',24,'italic')).pack()
如果字体名没有包含空格,则直接用'family size styles'形式的字符串也可以描述字体。
19.13绘制图形
PhotoImage类可以自己添加图像,支持GIF,PPM,PGM格式。构造函数可以命名图像,也可以从指定文件中读取图像
MisterT=PhotoImage('Mr. T',file='mrt.gif')
#另一种获取图像的方法:
ImageFile=open('mrt.gif')
ImageData=ImageFile.read()
ImageFile.close()
MisterT=PhotoImage(data=ImageData)
一旦有了PhotoImage对象,就可以使用image选项将其添加到其他部件上。
MisterT=Tkinter.PhotoImage(file='mrt.gif')
Tkinter.Button(root,image=MisterT).pack()
可用width和height方法查询图像的大小。只能在实例化toplevel实例之后(by gashero)才可以构造PhotoImage对象。
19.13.1画布部件
画布部件是一个窗口,可以编程绘制多种图形。以下是各个函数:
create_line(x1,y1,x2,y2,...,xn,yn)
按照顺序绘制连接点,如果设置了smooth选项则可以绘制平滑曲线。
create_polygon(x1,y"1",x2,y2,...,xn,yn)
和如上类似,使用由fill提供的颜色,默认透明。填充线条包围区域。outline选项提供线条颜色,smooth选项平滑曲线。
create_image(x,y,image=?[,anchor=?])(x,y)
在画布上绘制指定图像,image选项可以是PhotoImage实例或以前创建的PhotoImage名称。anchor的默认值为CENTER,指定图像的哪一个部分在(x,y)。
create_oval(x1,y1,x2,y2)
在点(x1,y1)和(x2,y2)所定义的矩形内绘制椭圆。在outline选项中传递轮廓颜色。在fill内指定内部填充颜色。width控制轮廓宽度/象素。
create_rectangee(x1,y"1",x2,y2)
绘制矩形。fill、outline、width同上。
create_text(x,y,text=?[,font=?])
绘制文本,可指定字体。
19.13.2处理画布项
在画布上绘制的各项内容都拥有自己的部件,可以移动,可以绑定事件。用create_开头的方法返回画布项的ID。通过画布方法,可以指定哪个ID来处理画布项。例如delete(ID)可以删除指定的项。move(ID, DeltaX,DeltaY)可以将画布项移动。
19.14使用计时器
Tkinter内部提供计时器。Toplevel部件上调用after(wait, function)可以在wait毫秒之后执行指定的函数。为使得定时的操作重复,可在function的末尾再次调用after。
下例就是每10秒调用一次的函数:
import Tkinter
def MinuteElapsed():
print 'Ding!'
root.after(1000*60,MinuteElapsed)
root=Tkinter.Tk()
root.after(10000,MinuteElapsed)
root.mainloop()
19.15实例:反弹图片
一个移动的图片,在各个边上反弹。
import Tkinter
class Bouncer:
def __init__(self,Master):
self.Master=Master
self.X=0
self.Y=0
self.DeltaX=5
self.DeltaY=5
self.Figure=Tkinter.Canvas(self,Master)
self.GrailWidth=GrailPicture.width()
self.GrailHeight=GrailPicture.height()
self.GrailID=self.Figure.create_image(/
0,0,anchor=Tkinter.NW,image=GrailPicture)
self.Figure.pack(fill=Tkinter.BOTH,/
expand=Tkinter.YES)
#每0.1秒移动一次
root.after(100,self.MoveImage)
def MoveImage(self):
self.X+=self.DeltaX
self.Y+=self.DeltaY
self.Figure.coords(self.GrailID,self.X,self.Y)
#在边框反弹
if self.X<0:
self.DeltaX=abs(self.DeltaX)
if self.Y<0:
self.DeltaY=abs(self.DeltaY)
if self.X+self.GrailWidth>self.Figure.winfo_width():
self.DeltaX=-abs(self.DeltaX)
if self.Y+self.GrailHeight>/
self.Figure.winfo_height():
self.DeltaY=-abs(self.DeltaY)
#在0.1秒后重复
if __name__=='__main__':
root=Tkinter.Tk()
GrailPicture=Tkinter.PhotoImage(file='HolyGrail.gif')
Bouncer(root)
root.mainloop()
19.16小结
略...
完成...
---------------------------------------------------------------
第二十五章 图像处理
25.1图像基础
图像由一组像素构成,一般通过图像的宽和高限定尺寸。保存图像的格式很多。GIF具有256色,并可以使用16777217种颜色。还有些格式定义调色板。
大多需要压缩,分为无损和有损的。
有些格式支持透明效果。基于索引的格式把某种颜色定义为透明,其他格式还包括表明各个像素透明程度的alpha通道。
25.2识别图像文件类型
imghdr模块合理的推测文件中的图像格式:
>>> import imghdr
>>> imghdr.what('c://temp//sleep.jpg')
'jpeg'
此模块只是检查文件头的几个字节,不保证文件的完整性。只要传递文件头的几个字节就可以识别。
>>> hdr=open('snake.bmp','rb').read(50)
>>> imghdr.what(' '.h=hdr)
'bmp'
下标是可识别的返回值对应文件类型:
返回值 |
图像类型 |
---|---|
gif |
CompuServe可交换图形 |
jpeg |
符合JPEG的JFIF |
bmp |
Windows或OS/2位图 |
png |
可移植网络图形 |
rgh |
SGI图像库(RGB) |
tiff |
标记图像文件格式 |
pbm |
可移植位图 |
ppm |
可移植像素图 |
pgm |
可移植灰度图 |
rasl |
Sun Raster图像 |
xbm |
X11位图 |
通过添加imghdr的tests函数表,可以让他检查更多的文件类型。此模块仅用于识别,并不更改文件。下例识别Python字节码。
>>> def test_pyc(h,f):
... import imp
... if h.startswith(imp.get_magic()):
... return 'pyc'
>>> imghdr.tests.append(test_pyc)
>>> imghdr.what('leptolex.pyc')
'pyc'
两个参数,前一个带有文件头的几个字节。如果用户调用带有文件名的what,则f是文件打开对象。
25.3在颜色系统间转换
25.3.1颜色系统
colorsys模块支持最流行的4中颜色系统之间的转换。各个系统中都用0.0-1.0之间的三元数代表各种颜色。
RGB:最基本的一种,就是光学三元色的红绿蓝。人眼就是识别这三种颜色的。
YIQ:美国国家电视系统协会(NTSC)所用的颜色系统。Y表示颜色的亮度,占据了绝大多数带宽,I包含了橙青颜色,还支持现实色调颜色,Q带有红绿颜色信息并占有最小的带宽。
HLS:色度、亮度、饱和度。先选定一种纯色(如纯绿色),之后添加黑白来产生色调,色彩,阴影。L表示亮度0.0黑色,1.0白色。S表示色度饱和及,1.0表示饱和(纯色),0.0表示完全不饱和,可以获得灰色阴影。
HSV:除了纯色度的V组件(与HLS中的L对应)值为0.5之外,HSV颜色系统非常接近HLS系统。HSV为色度、饱和度、纯度。
25.3.2从一种系统转换到另一种系统
colorsys模块中包含着可将RGB转换与其他系统的转换函数:
>>> import colorsys
>>> colorsys.hls_to_rgb(0.167,0.5,1.0) #黄色
(0.998,1.0,0.0)
其他的颜色系统之间的转换需要RGB作为中介。人眼最多只能识别出83000种(by gashero)颜色。
下例为转换HLS到RGB的程序choosecolor.py:
from Tkinter import *
import colorsys
def update(*args):
'改变调色板的颜色'
r,g,b=colorsys.hls_to_rgb(h.get()/255.0,
l.get()/255.0,s.get()/255.0)
r,g,b=r*255,g*255,b*255
rgb.configure(text='RGB:(%d,%d,%d)'%(r,g,b))
c.configure(bg='#%02X%02X%02X'%(r,g,b))
rook=Tk()
hue=Label(root,text='Eue')
hue.grid(row=0,column=0)
light=Label(root,text='Lightness')
light.grid(row=0,column=1)
sat=Label(root,text='Saturation')
sat.grid(row=0,column=2)
rgb=Label(root,text='RGB:(0,0,0)')
rgb.grid(row=0,column=3)
h=Scale(root,from_=255,to=0,command=update)
h.grid(row=1,column=0)
l=Scale(root,from_=255,to=0,command=update)
l.grid(row=1,column=1)
s=Scale(root,from_=255,to=0,command=update)
s.grid(row=1,column=2)
c=Canvas(root,width=100,height=100,bg='Black')
c.grid(row=1,column=3)
root.mainloop()
25.4处理原始图像数据
imageop模块用于处理以字节串传递的原始图像数据。数据可能是8位一个字符,代表象素,或者是32位代表一个象素。如果是SGI计算机还可以使用imgfile模块。如果有一个SGI RGB文件,可以使用rgbimg模块载入此文件。
imageop可以裁剪和缩放图像,但还是有大量函数用于转换不同灰度的图像。
25.5使用Python图像库
可在www.pythonware.com中查阅Python图像库(PIL)。免费库。
如果需要了解更多图像处理工具,访问Vautls of Parnassus:
http://www.vex.net/parnassus
25.5.1检索图像信息
PIL中的主要模块Image,通过此模块可以完成多种操作。
>>> import Image
>>> i=Image.open('shadow.bmp')
>>> i.mode
'RGB'
>>> i.size
(320,240)
>>> i.format
'BMP'
>>> i.getbands()
('R','G','B')
>>> i.show() #显示图像
图像模式制定了颜色深度和存储位置,如下:
模式 |
说明 |
---|---|
1 |
1位像素,黑白 |
L |
8位像素,黑白 |
P |
8位像素,256色调色板 |
RGB |
每像素3字节,真彩色 |
RGBA |
每像素4字节,具有alpha带的真彩色 |
l |
32位整型像素 |
F |
32位浮点型像素 |
图像具有几个波段的数据,如RGB图像具有红绿蓝三个波段。
show()方法是一个调试函数,将用默认浏览器打开图像,打开的图像是将当前图像保存到临时文件夹之后的,不一定是原图像。
有些IDE中使用show()会有问题。
PIL的优秀功能在于:真正需要读入和解码文件数据时才发挥作用。适于处理巨大的图像。
25.5.2复制与转换图像
copy()方法将返回与原图像相同的新对象,可用于修改备份。
convert(mode)返回指定模式下的新图像。
save(filename)将当前图像保存到文件中。PIL会自动按照后缀名保存为对应格式:
>>> Image.open('test.jpg').save('test.gif')#文件格式转换
PIL是在需要进行各种操作的时候才去读取图像文件。可用draft(mode,(w,h))方法在图像装入图像之前进行一些转换。例如一个5000×5000的图像,但是仅需要在较小的256色草稿上处理:
>>> img=Image.open('huge.jpg').draft('p',(250,250))
25.5.3使用带有Tkinter的PIL
ImageTk 模块支持两个类BitmapImage和PhotoImage。可创建Tkinter兼容的位图和图像。还可以用他们来装入较多的文件格式。
Tkinter的内容参见19和20章。
PIL还具有如下两种函数:一种函数用于创建与Windows兼容的位图并置于Windows设备相关上下文中,另一种函数可将图像写入PostScript文件或打印机中。
25.5.4裁剪和调整图像尺寸
crop((left,top,right,bottom))返回矩形部分图像
resize((w,h)[,filter])返回调整尺寸之后的图像。filter参数用于控制相对于原始图像应采用的采样类型,可为BILINEAR、BICUBIC、 NEAREST(默认)中的任一。
另一有用办法是thumbnail((w,h)),这种方法可用于适当的调整对象,同时保持原始图像的高宽比例。
25.5.5修改像素数据
可以通过图像像素的坐标(x,y)来读取并改变像素值,坐标系为左上角(0,0)的。
getpixel((x,y))和putpixel((x,y),value)可用于获取和设置单一像素。其中value为合适的图像模式。例如如下给图像加上黑条:
>>> i=Image.open('xxx.bmp')
>>> i.getpixel((10,25))
(156,111,56)
>>> for y in xrange(50,60):
... for x in xrange(i.size[0]):
... i.putpixel((x,y),(0,0,0))
>>> i.show()
getdata()将返回当前图像各个像素的一列字节组,而putdata(data [,scale][,offset])将一系列字节数组存入图像,offset默认为图像的开始部位,scale默认为1.0。
PIL的ImageDraw模块支持Draw对象,可用于在图像上添加图形和文字。可用于生成随时要改变的图像。
25.5.6其他PIL功能
新版本的PIL包含更多的功能,如:
-
ImageEnhance:包含可用于调整图像颜色、亮度、对比度和清晰度的类。
-
ImageChops:支持算术图像操作,加亮、变暗、转换图。
-
提供对动画(多帧)GIF和FLI/FLC图像的支持。
-
转换,包括以任意角度旋转图像和在每一个像素上应用Python函数。
-
用于模糊图像和发现边界的图像过滤程序。
-
具有添加面向新文件格式的解码起的能力。
25.6小结
略...
完成...