2010年7月22日星期四

web framework - Django

Django

Intro.

http://www.djangoproject.com/
Django框架最初是被开发来用于管理劳伦斯出版集团旗下的一些以新闻内容为主的网站,
其原则是DRY(Don't Repeat Yourself),注重组件的重用性和“可插拔性”以方便敏捷开发。

教程和文档请参考:
http://docs.djangoproject.com/
http://www.djangobook.com/
http://www.ibm.com/developerworks/cn/linux/l-django/
http://blog.ericsk.org/archives/815
下面内容也仅是设法从中整理出若然片段,以供额外的参考。

这里使用的版本是 python 2.6 和 Django 1.2 。


Start

如果不考虑部署仅仅是安装一个开发环境的话,把Django作为一个标准的第三方模块来对待便可以,让"import django"能够成功的载入对应的模块。
执行"django-admin.py startproject mysite"创建一个名为"mysite"的项目目录。
其中有四个文件:
__init__.py 空文件,这里对应"mysite"这个包(可在其他文件中import)
manage.py 命令行工具,提供了若干项目相关的功能
settings.py 该项目的配置文件,可手工编辑
urls.py 用于定义URL的调度,是一个站点接口的组成

下面来在项目中创建一个应用,执行"manage.py startapp polls"。
这样则在项目目录中得到一个名为"polls"的目录,其中有四个文件是:
__init__.py,models.py,tests.py,views.py。
这个包以"import mysite.polls"的方式在项目中使用。

先暂且放开刚才创建的应用回到我们项目的根目录,
通过"manage.py runserver"便可以执行测试服务器。
这样在浏览器中访问"http://127.0.0.1:8000/",
就已经能看到这个Django的应用已经顺利运行了。
并且在测试模式下它会为每次请求重新载入代码,
这样我们就可以随即看到对代码的修改产生的效果。

Django自身的测试代码可以在"tests"目录下中找到。
可作为模块使用的参考,或者复制出来使用。


ORM

首先编辑"settings.py"中"DATABASES"和"INSTALLED_APPS"连接数据库并添加所创建的App,
比如这里的是"sqlite3"和'mysite.polls'。
这个准备工作可以让我们能立即试验接下来要为应用所编写的模型。
执行"manage.py syncdb"如果已安装应用的模型对应的数据库空缺的话,
会执行所需的SQL中的"CREATE"操作。
也提供有其他命令(如"manage.py sql"等)来只生成SQL然后手工执行,
以便于语句本身以及对数据库的调整。

使用对象关系映射模块可以通过声明的方式来使用数据库,
这会为我们自动定义对象的若干方法来方便操作数据并执行对应的SQL语句。
类似的模块,比如ROR中的ActiveRecord实现。


Models

模型声明在app的models.py文件中,例如:
class Category(models.Model):
name = models.CharField(max_length=32)
def __unicode__(self):
return self.name
class Article(models.Model):
title = models.CharField(max_length=64)
published_at = models.DateTimeField('date published')
content = models.TextField()
category = models.ForeignKey(Category)
def __unicode__(self):
return self.title

模型类型派生自django.db.models,并提供若干定义好的Field类型。
对每种类型有若干选项,有的不在调用构造方法时指定也提供了获得默认的值(如null=False,blank,choices)。
在未指定主键的情况下"id = models.AutoField(primary_key=True)"将被自动定义。

模型间的关系用ForeignKey,ManyToManyField以及OneToOneField,
分别用于表示
many-to-one|has_one|belongs_to,
has_and_belongs_to_many 以及
对模型的继承扩展。
虽然ManyToManyField表示的关系是相互的,
也只要在相关联的模型中指定一次。

为使用方便,通常重载模型的__unicode__方法。
还可以通过定义"class Meta"为模型提供定义字段之外的信息,
比如"ordering = ['-published_at']"定义获取对象列表时的默认排序。


QuerySets

在数据库被创建之后,可以执行"manage.py shell"以相互方式使用模型,
首先执行"from mysite.polls.models import Poll, Choice"导入。

除了自己在模型中定义方法,对于CRUD操作Django生成了:

INSERT
from mysite.blog.models import Blog
b = Blog(name='Beatles Blog', tagline='All the latest Beatles news.')
b.save()
save方法调用才会执行对应的SQL语句。

UPDATE
b.name = 'New name'
b.save()
与INSERT操作会自动区分。
ForeignKey也是使用赋值语句,而ManyToManyField用add等方法。

DELETE
e.delete()
也可对list类型操作。

SELECT操作通过类型的objects成员进行,例如
Entry.objects.all()
这是一个惰性类型,可以无多余性能消耗的Python切片运算。
其他提供的方法有 order_by ,filterexclude, get 以及 count ,可以链式使用。
由于参数是以Python中dict类型传入的,会以文本上"field__lookuptype=value"的形式解析。
例如
Entry.objects.filter(pub_date__lte='2006-01-01')
p = Poll.objects.get(pk=1)
关联类型如果非一个单独的属性(如ForeignKey类似CharField使用),
则提供实例的FOO_set成员(用法同模型的objects)进行操作。


MVC

在Django中称为MTV,
原理略。


URL

URL调度在"mysite/urls.py"中指定,修改其中的"urlpatterns"变量。
Django将根据模式匹配的结果调用对应的函数:
urlpatterns = patterns('',
(r'^polls/$', 'mysite.polls.views.index'),
(r'^polls/(?P\d+)/$', 'mysite.polls.views.detail'),
(r'^polls/(?P\d+)/results/$', 'mysite.polls.views.results'),
(r'^polls/(?P\d+)/vote/$', 'mysite.polls.views.vote'),
)
这里访问"/polls/23/"则会调用"detail(request=, poll_id='23')"。
patterns个第一的参数用于指定通用的前缀,
比如这里若是"mysite.polls.views"下面就可省略只写"index"了。
模式的格式是:
(regular expression, Python callback function [, optional dictionary [, optional name]])

也可以把urls.py复制到每个应用的目录下,
然后以类似"(r'^polls/', include('mysite.polls.urls')"的形式引入原urls.py文件中。
这样能够让URL调度的层次变得清晰。


Views

View函数传入一个HttpRequest并返回一个HttpResponse,例如:
from django.http import HttpResponse
def detail(request, poll_id):
return HttpResponse("You're looking at poll %s." % poll_id)
将提供给匹配"/polls/23/"时调用。

HttpRequest的实例通常命名为request,为View函数的第一个参数
if request.method == 'GET':
do_something()
elif request.method == 'POST':
do_something_else()
通过method获得HTTP方法。
为获得HTTP头,
还提供有GET,POST,REQUEST,COOKIES,FILES,META成员按照字典的方式来使用。

HttpResponse的实例被View函数作为返回值,包括其中的文本以及传送的HTTP头:
response = HttpResponse(my_data, mimetype='application/vnd.ms-excel')
response['Content-Disposition'] = 'attachment; filename=foo.xls'
需要时也可返回它的子类如HttpResponseNotFound的实例,另一种类似的写法是:
from django.http import Http404
def detail(request, poll_id):
try:
p = Poll.objects.get(pk=poll_id)
except Poll.DoesNotExist:
raise Http404
return render_to_response('polls/detail.html', {'poll': p})
通过raise抛出错误而没有用return。

代码执行错误是500错误,它们的默认行为是渲染模板目录下404.html或500.html文件。

HttpResponseRedirect则是302重定向。


Template

Django提供了一套自己的模板语言,它是站点前端和后端的交汇处。
虽然塔和Python并不一致,不过也是以这样的限制区分不同的职责。
它的长相如下:
from django.template import Context, loader
from mysite.polls.models import Poll
from django.http import HttpResponse
def index(request):
latest_poll_list = Poll.objects.all().order_by('-pub_date')[:5]
t = loader.get_template('polls/index.html')
c = Context({
'latest_poll_list': latest_poll_list,
})
return HttpResponse(t.render(c))

/polls/index.html
{% if latest_poll_list %}
<ul>
{% for poll in latest_poll_list %}
<li><a href="/polls/{{ poll.id }}/">{{ poll.question }}</a></li>
{% endfor %}
</ul>
{% else %}
<p>No polls are available.</p>
{% endif %
使用前,需要先去 settings.py 指定 TEMPLATE_DIRS 的值。

在模板中使用{{ variable }}嵌入其返回值,可以是变量或方法调用。
Filters以类似{{ bio|truncatewords:30 }}的形式使用,
用以处理变量的值,常用的例如default,length,striptags。
Tags以类似{% tag %}...{% endtag %}的形式使用,
用以生成输出的内用,例如for循环,if and else条件处理,block and extends继承和重写。
Comments以类似{# greeting #}的形式使用,用以书写注释或注释掉临时不需要的代码。

特殊符号默认会被自动转义,以增强安全性。载入其他库以{% load comments %}的形式。

模板的渲染则是调用Template的实例的render方法,并以Context的实例为参数。


Shortcut

在django.shortcuts定义了若干混合的MVC来方便使用的函数。
from django.shortcuts import render_to_response
def my_view(request):
# View code here...
return render_to_response('myapp/index.html', {"foo": "bar"},
mimetype="application/xhtml+xml")

等价于:

from django.http import HttpResponse
from django.template import Context, loader
def my_view(request):
# View code here...
t = loader.get_template('myapp/template.html')
c = Context({'foo': 'bar'})
return HttpResponse(t.render(c),
mimetype="application/xhtml+xml")
这是render_to_response(template[, dictionary][, context_instance][, mimetype])。

还有若干其他的:
redirect(to[, permanent=False], *args, **kwargs)
表示重定向。

get_object_or_404(klass, *args, **kwargs)
from django.shortcuts import get_object_or_404
def my_view(request):
my_object = get_object_or_404(MyModel, pk=1)

等价于

from django.http import Http404
def my_view(request):
try:
my_object = MyModel.objects.get(pk=1)
except MyModel.DoesNotExist:
raise Http404
类似的是get_list_or_404,使用filter()而不是get()。


Next

关于Django的可重用组件以后贴里再说。

----
Django的风格太自己了,在这点上有些别扭。

2010年7月21日星期三

Web界面的桌面应用程序

标题起空泛了,而且还跑题。。。

qooxdoo

qooxdoo是一个开源AjaxWeb应用程序框架。

其官方说明是
qooxdoo is a comprehensive and innovative framework for creating rich internet applications (RIAs). Leveraging object-oriented JavaScript allows developers to build impressive cross-browser applications. No HTML, CSS nor DOM knowledge is needed.

以个人的熟悉的话,会拿qooxdoo和qt做类比(其实是像Java库)。同样是应用程序框架,不过运行的环境并不相同。
话说qooxdoo可作为swt的前端,那么类比而言做Qt的前端的也是可以的。只不过显示交给了浏览器,而一些功能实现要由Ajax交给服务端(Qt有Network模块)来处理。
说浏览器中的库的话,自然会想到jQuery。而如果仅在Web中呈现GUI的话,jquery-easyui那样从内容展示出发的模块显然会更好用一点。
这么来说2005年便成立项目的qooxdoo,其本意浏览器中实现供JavaScript使用的应用程序开发框架了。


Hello World

使用create-application.py生成一个项目目录,例如
create-application.py --name=demo

在生成的目录中,/source/class/demo/Application.js便是需要编辑的文件。
此外有一个generate.py文件,需要先执行
./generate.py source-all
把全部的Class都导入,这样编写时就可以不必再用它来分析代码的依赖。
之后
./generate.py build
生成发布用的代码,就已经有近500k之巨了。

这里的Application.js已经包含最基本的模板,其中涉及qx的对象机制以及GUI工具箱的使用。
看起来有Java代码的感觉,在sdk中demobrowser有示例代码,playground里可以立即执行代码,"generate.py api"用以为当前项目生成文档,Manual则是首先必须了解到的。

此外,qx提供了Debug和Unit Testing的支持,以及一个Inspector。


Remote Procedure Call

基本的Ajax的话是使用qx.io.remote.Request,高层一点的qx.data.store.Json用于读取json,然后是基于JSON-RPC的qx.io.remote.Rpc。
它们都是按照先创建实例然后设置属性在调用的方式使用。
Rpc提供callSync和callAsync远程调用服务端提供的函数。
对服务端的语言没有限制,调用时需要对数据类型进行转换。

qx第三方提供的RPC_Python模块提供的接口看起还是挺优雅的,也实现了简单session机制。
作为实现的参考还是不错的,可以年久并不能实际使用。
这么来看在RPC方面还是需要来自己实现一点东西,不过这反而可以去使用一些已经熟悉的网络框架吧。
Python自带的话,有个xmlrpc模块。一般的话选个Web开发框架就可以了。

还有一个类似GWT的东西,由Java代码生成qx代码。


Web Interface

一些下载软件有提供本机或远程在浏览器中使用的Web界面。
例如emule,mldonkey,它们将自身作为一个Web服务器。
同过URI或者Form来以GET或POST的方式,使用HTTP协议(CURL也可用)。

如果单纯说HTTP的话,就可以是一个RESTFUL的API了。
不过对于以上所列的两款软件来说,并没有使用Ajax的方式。
只是单纯的信息的展示与提交,不过对这样的应用来说也足够了。

在Python中,webbrowser.open_new(url)会在浏览器中打开地址。
服务端的话派生BaseHTTPRequestHandler并实现do_GET和do_POST方法。
通过wfile回数据时先发head,就像SimpleHTTPRequestHandler实现的那样,
在Py的安装目录下可以看到代码。

不过这显然是以上若干节所说的有区别的,这里偏向于API。
而之前在说的是rich internet applications (RIAs),算Web application的范畴。


pyjs

一个Python Javascript Compiler,由Python代码编译出javascript代码。
属于Pyjamas项目,是一个GWT的Python实现,还包含AJAX framework和Widget Set API。
pyjs包括两部分,一是通过Py的语法树生成js代码,另一个则是用js实现了py的基本对象类型。
有个Pyjamas Desktop子版本用于类似Air的桌面程序。
暂且提及一下,略过。


Code Examples

最后堆点不完整的复制来的代码供参考,来源是各自官方。


qx.helloworld.js
qx.Class.define("demo.Application",
{
extend : qx.application.Standalone,
members :
{
main : function()
{
this.base(arguments);
var button1 = new qx.ui.form.Button("First Button");
var doc = this.getRoot();
doc.add(button1, {left: 100, top: 50});
button1.addListener("execute", function(e) {
alert("Hello World!");
});
}
}
});
或创建窗口:
 var win = new qx.ui.window.Window(
"First Window");

win.setPadding(10);
win.setLayout(new qx.ui.layout.VBox(10));
win.add(new qx.ui.form.Button("Hello World"));

win.open();
继承层次结构是
Object>qx.core.Object>qx.ui.core.LayoutItem>qx.ui.core.Widget>qx.ui.window.Window


qx.io.remote.Rpc
var rpc = new qx.io.remote.Rpc(
"http://localhost:8080/qooxdoo/.qxrpc",
"qooxdoo.test"
);

// synchronous call
try {
var result = rpc.callSync("echo", "Test");
alert("Result of sync call: " + result);
} catch (exc) {
alert("Exception during sync call: " + exc);
}

// asynchronous call
var handler = function(result, exc) {
if (exc == null) {
alert("Result of async call: " + result);
} else {
alert("Exception during async call: " + exc);
}
};
rpc.callAsync(handler, "echo", "Test");

//Aborting a call
//var callref = rpc.callAsync(handler, "echo", "Test");
//rpc.abort(callref);
RPC对象,以及两种调用方法。


RPC with a Python server
#!/usr/bin/python
# -*- coding: ascii -*-

import qxjsonrpc
import qxjsonrpc.http

class ExampleWikiService(object):
def __init__(self):
self.total=0
@qxjsonrpc.public
def add(self, *args):
for value in args:
self.total+=value
return self.total

def main():
server=qxjsonrpc.http.HTTPServer()
server.setService('example.wiki', ExampleWikiService())
server.serve_forever()

if __name__=='__main__': main()
加@qxjsonrpc.session话会传入一个session参数。


Python.BaseHTTPServer
def run(server_class=BaseHTTPServer.HTTPServer,
handler_class=BaseHTTPServer.BaseHTTPRequestHandler):
server_address = ('', 8000)
httpd = server_class(server_address, handler_class)
while True:
httpd.handle_request()
这里需要实现自己的handler_class子类,while语句后面可控制是否退出。


...

标题就是,写了个本地应用,然后拿浏览器渲染界面的意思。。。

2010年7月12日星期一

web framework - web.py

http://webpy.org/
web.py is a web framework for python that is as simple as it is powerful.

web.py写起来简单,所以就写它了,算个引子。
况且这里本就把它当作一个拿来用,而不是拿来学习的东西。
写对性能有要求较高的服务端的话,有tornado和twisted作为选择。
而在Py的Web框架中,相比于DjanGo和TurboGears以及功能更强大的Zope/Plone,web.py则显得比较简单。
web.py资料的量不打,在其官网上有提供。tutorial用于对整体概念的学习,documentation是对模块和接口的描述,cookbook和Code samples是使用代码和代码示例,也提供有若干用于交流的途径。
其自身定位于anti-framework,意思是不去约束用户的使用方式,这样使得web.py用起来更加灵活。不过web.py的可复用的组件不够多,所以说web.py简单也是因为他将复杂度移向了其他的地方。
灵活与单薄可以算是一个权衡web.py的选择依据,也就是不论喜欢或讨厌其风格是都可以有其实用场合。
说网络应用的话php是专门的语言了,如今也有诸多框架可选择,而像drupal那样的cms,也可看作一个功能丰富类型固定的框架了。
web.py中如果再增加一些的约定的话,使用起来代码量会少一点。
以上的简短的介绍,不展开说了。

web.py的模块
当前版本0.34

部署


web.application

官网上已经简明地列出了web.py的基本用法了
import web

urls = (
'/(.*)', 'hello'
)
app = web.application(urls, globals())

class hello:
def GET(self, name):
if not name:
name = 'World'
return 'Hello, ' + name + '!'

if __name__ == "__main__":
app.run()
这段代码中涉及了URL处理,是使用正则表达匹配并捕获字符串并映射到一个命名空间中创建的类型(实现GET或POST方法,并返回要显示的字符串)上。
代码执行后默认来debug模式下,代码的修改会随即生效,也可指定web.config.debug = False。
默认使用http://127.0.0.1:8080/来通过浏览器执行效果的预览,也有提供了API的形式方便调试。
可以修改app.notfound以自定义404错误时所显示内容,创建的app也有Subdir和Subdomain的类型。


web.webapi

这部分是在Web命名空间下提供的一组函数,用于Http协议相关。
在实现GET或POST方法时会用到。

header
web.header('Content-Type', 'text/plain')
web.header("Content-Type","text/html; charset=utf-8")
输出Http头。

input
class hello:
def GET(self):
i = web.input(name = 'web')
return 'Hello, ' + web.websafe(i.name) + '!'
获得GET或POST的输入数据。

setcookie与cookies
web.setcookie('age', i.age, 3600)
参数是名称,值,有效时间。这对函数用于操作cookie。


另一组函数用户返回Http状态,使用raise语句调用。
例如
raise web.seeother("/foo")
是303重定向。
还有若干HTTPError派生的不同类型以表示不同的状态,例如NotFound(message=None)。


web.httpserver

目录static/下用于存放静态内容


web.template

首先是程序部分

render = web.template.render('templates', base='layout')
print render.hello('world')
模板文件放在'templates'目录下,可选参数base表示共用部分。
这里以'world'为参数渲染了'hello.html'文件并返回字符串结果,可以进行其他的处理之后再执行return等来使用。
通过设置globals或builtins属性(dict类型,默认None。),可以提供模板代码可以访问的变量和可以使用的函数。

另一方面则是模板代码的语法了,
它的长相是
$#hello.html
$def with (name)
$var title:"hello"
$name
$用于使用Python语句,例如访问变量(会自动web.websafe)执行函数或者for/while/if及其他语句。
$#layout.html
$def with (page)
$page.title
$:page
这里的title是子模板中定义的。


web.contrib.template

实现了对其他模板引擎的接口。


web.form

该helper模块用于渲染表单和处理验证。
signup = form.Form(
form.Textbox('username'),
form.Password('password'),
form.Password('password_again'),
validators = [form.Validator("Passwords didn't match.", lambda i: i.password == i.password_again)]
)
也可在创建Widget时指定验证条件
form.Textbox("bax", 
form.notnull,
form.regexp('\d+', 'Must be a digit'),
form.Validator('Must be more than 5', lambda x:int(x)>5)),
然后可以在控制器中或者模板中创建拷贝实例再执行.render()方法。

方法.validates()用于让form处理input,之后可以获取输入值
class hello:
def GET(self):
my_form = number_form()
return render.hello(my_form)

def POST(self):
my_form = number_form()
if not my_form.validates():
return render.hello(my_form)
else:
number = my_form['number'].value
if int(number) % 2:
return "Your number %s is odd." % number
else:
return "Your number %s is even." % number

这里的hello.html
$def with (form)
<form name="test" method="POST">
$if not form.valid: <p>Sorry, your input was invalid.</p>
$:form.render()
<input type="submit" value="Check" />
</form>
可以获得表单状态。


web.session

机制session用于保存客户端的状态
if web.config.get('_session') is None:
session = web.session.Session(app, web.session.DiskStore('sessions'), {'count': 0})
web.config._session = session
else:
session = web.config._session
在debug模式执行结果不可靠。
然后使用
class hello:
def GET(self):
session.count += 1
return "You visited " + str(session.count) + " pages."

class bye:
def GET(self):
session.kill()
return ("Bye, web!")

也可把session传递给模板
web.template.Template.globals['session'] = session

web.session的配置在web.config.session_parameters,例如timeout属性。
session可使用DiskStore或者DBStore。


web.db

这是操作数据库的一个接口,有postgresql/mysql/sqlite,需安装Py下的支持模块。
也可用sqlite3,sqlalchemy或一个文件来替换这个模块。
以下代码纯粘贴
db = web.database(dbn='postgres', user='username', pw='password', db='dbname')

entries = db.select('mytable')
db.update('mytable', where="id = 10", value1 = "foo")
db.delete('mytable', where="id=10")
sequence_id = db.insert('mytable', firstname="Bob",lastname="Smith",joindate=web.SQLLiteral("NOW()"))
results = db.query("SELECT COUNT(*) AS total_users FROM users")
print results[0].total_users
results = db.query("SELECT * FROM entries JOIN users WHERE entries.author_id = users.id")
results = db.query("SELECT * FROM users WHERE id=$id", vars={'id':10})
def post(title, body, tags):
t = db.transaction()
try:
post_id = db.insert('post', title=title, body=body)
add_tags(post_id, tags)
except:
t.rollback()
else:
t.commit()
def GET(self):
todos = db.select('todo')
return render.index(todos)
class add:
def POST(self):
i = web.input()
n = db.insert('todo', title=i.title)
raise web.seeother('/')
>>> web.db.sqlwhere({'a': 1, 'b': 2}, grouping=' OR ')
'a = 1 OR b = 2'
select方法可用的参数(传入字符串)有vars,what,where,order,group,limit,offset。
通常在整个应用中把数据库操作包装为model模块。


web.utils

该模块提供若干通用用具,在web命名空间下。
例如:
safestr转换编码
Memoize用于让函数对属于缓存一定时间的返回值
datestr将datetime对象转换为字符串
sendmail发送邮件的函数,相关配置在web.config中
safemarkdown使用Markdown标记

在这里实现了web.py的基础数据类型和相关函数,其他的在输出中可用的实用过滤器也放在这里。

web.net

这里提供了若干网络相关的组件
validaddr从字符串解析出ip地址和端口
httpdate/parsehttpdate字符串编码形式与datetime对象间的转换
urlquote将字符串转换为URL地址中可用
websafe将文本转换为网页中用于显示的UTF-8 HTML

web.http

Http相关组件,用于产生header或url。


other


这里并没有列出web.py的全部组件,并且内容以后会受到API变动的影响的。
其他还是有一些会用到的模块的:
通过web.ctx可以获得客户端信息,类型是ThreadDict。
Python的内置模块中已经有了json的支持了,做ajax时比xml用起来简单一些。
web.webopenid使用openid。
web.wsgi是WSGI组件。

web.py的使用

demo

一个最简陋的计数器
import web

urls = (
'/index.htm','index',
'/.*', 'toidx'
)
web.config.debug = False
app = web.application(urls, globals())

counter=0

class index:
def GET(self):
web.header('Content-Type', 'text/html')
global counter
counter += 1
return str(counter)

class toidx:
def GET(self):
raise web.SeeOther("/index.htm")

if __name__ == "__main__":
app.run()
这个算个引子,表示下面的内容是一些示例来着:

Guestbook,User authentication,Wiki,Blog,Test。

代码就不贴了(坑),也是想变成模块的话复用会更方便一点。
所以去用一些复用程度更高的框架的话,就可以把事情完成的更好更简单一点。


第三方组件
sqlalchemy
模板引擎
CherryPy

然后,这篇就这么简单一些吧。。。

2010年7月9日星期五

Python中的交互

先对内容说明一下,手头是2.6的版本,考虑兼容2.5。
主要参考与来源是Python的文档,只选择性地涉及了部分库,而且没有去标注版本变动。
以下内容只是做了一个相当简略的归类性质的东西,也算是概览之类的吧。
本帖算是选定了一个话题,就是列举一下Py对常见的交互方式的实现。
暂时只列举了一些目前容易想到的方式,并略做了说明。


command

sys.argv是一个list类型的实例,argv[0]是脚本的名称的字符串。
如果要处理流的重定向的话用FileInput
import fileinput;for line in fileinput.input():process(line)

getopt(或optparse)则用于解析Unix风格的命令行参数。


stdio

print和input这一组的表达为:
"print" ([expression ("," expression)* [","] | ">>" expression [("," expression)+ [","])
raw_input([prompt])

较底层的话,stdin/stdout/stderr都在sys里面,依照File对象使用。
有getpass用于输入密码。


file

内置的open函数返回一个File对象。
StringIO模块可用于实现内存文件。
其他的文件操作在os模块中。
还有一个io模块是后来增加的。
一些交换用的文件格式有专门的模块。


process

原本在os模块里,后来单独为subprocess模块了。


socket

这是较底层的可用于网络通讯接口,以下也将仅涉及这一方面。

客户端
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("www.google.com", 80))

服务器
serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serversocket.bind((socket.gethostname(), 80))
serversocket.listen(5)
while 1:
....(clientsocket, address) = serversocket.accept()
....ct = client_thread(clientsocket)
....ct.run()

数据读写都是用到send()/recv()方法

如果TCP上的HTTP协议的话,在性能要求不高的条件下,
客户端可以用urllib2模块,服务器则可用BaseHTTPServer模块提供的框架(多线程则用ThreadingMixIn结合threading模块)。
其他协议也有单独的模块来着。


curses

这是绑定了一个Unix平台下可以使用的终端界面
代码是像这个样子的:
import curses
scr=curses.initscr()
curses.echo()
curses.cbreak()
scr.keypad(1)
scr.clear()
curses.start_color()
curses.init_pair(1, curses.COLOR_RED, curses.COLOR_GREEN)
scr.attron(curses.color_pair(1))
scr.box(curses.ACS_VLINE, curses.ACS_HLINE)
scr.move(curses.LINES/2, curses.COLS/2)
scr.addstr("hello, world!")
scr.addstr(5,33, " "*43, curses.A_UNDERLINE)
scr.attroff(curses.color_pair(1))
scr.refresh()
win=curses.newwin(10,10,2,2)
win.refresh()
s=win.getstr(0,0,5)
scr.addstr(s)
text=curses.newwin(50,50,10,5)
import curses.textpad
curses.textpad.Textbox(text).edit()
c=scr.getch()
if c==ord('q'):pass
scr.keypad(0)
curses.endwin()
API主要在curses命名空间上和Window对象上。
其中curses上改变状态的操作要手动恢复,而Window对象有若干种类型。


tkinter

在Python中内置了Tk的绑定,用于显示图形界面。
先来一个Hello World
#!/usr/bin/env python
#coding:utf-8
import Tkinter as tk
#http://www.pythonware.com/library/tkinter/introduction/
#http://infohost.nmt.edu/tcc/help/pubs/tkinter/
root=tk.Tk()
w=tk.Label(root,text="Hello World")
w.pack(side=tk.LEFT)
root.mainloop()

开始主程序后,就可以创建Widget了,然后放置。
Widget属性可以用其config方法来设置。
放置Widget可以用的方法是pack,grid以及place。
然后就是事件绑定。
还一些提供有其他的东西,例如更多的Widget和一些Dialog。
以下是一个随意的示例:
#!/usr/bin/env python
#coding:utf-8
import Tkinter as tk

class App(tk.Frame):
def __init__(self,root):
tk.Frame.__init__(self,root)
self.pack()
root.protocol("WM_DELETE_WINDOW", self.closeEvent)
top = tk.Toplevel()
self.w=tk.Label(self,text="Hello World")
self.w.pack(side=tk.LEFT)
self.txt=tk.StringVar()
self.e=tk.Entry(self,textvariable=self.txt)
self.e.pack(side=tk.LEFT)
f=tk.Frame(self)
f.pack(side=tk.LEFT)
self.btn=tk.Button(f,text="change",command=self.change)
self.btn.bind("",self.change)
self.btn.pack(side=tk.LEFT)

def change(self,event=None):
self.w["text"]=self.e.get()+"what?"#self.txt.get()
def closeEvent(self):
import tkMessageBox
if tkMessageBox.askokcancel("Quit", "Do you really wish to quit?"):
root.destroy()

if __name__ == '__main__':
root=tk.Tk()

a=App(root)
a.master.title("App Hello")
a.master.maxsize(1000, 400)
root.mainloop()

个人对Tk不熟,而Py的文档里重点在绑定的部分了。
不过想比与PyQt等更现代的Gui库来说,Tkinter使用起来更加简单一些。


test

《重构》里有一张说明了为什么要自动测试,另一种解释是测试驱动开发。
其实手工测试是很常见的事情,这里的区别是在是否自动上面,并获得如何的好处。
Py中模块有用 if __name__ == '__main__': 写测试兼示例的习惯。

Python中提供的若干开发工具,例如:
pydoc用于从源代码生成文档,文本来自__doc__部分。
doctest是基于文本的测试,匹配命令交互方式下的执行结果。
unittest的功能是自动单元测试,测试代码由TestCase类型派生,并添加给TestSuite。
test模块提供回归测试,用于在代码就该后执行之前的全部或部分测试代码。


thread

通常使用threading模块,而较低层的是thread。
所以简单的做法便是派生threading.Thread(或者直接创建并指定target属性)并重写run方法,
并使用若干synchronization对象。

other


还可以写python内部的机制,比如import或者parse什么的。


----
说明一下,上文只是简单列举语言对接口的实现。
重点不在语言上面也不在接口上面,而建立起的一份整体印象。
不过全篇看来也没列到什么特别的东西,都算是基础性的吧。

2010年7月8日星期四

使用Python编写Qt程序

使用Python编写Qt程序

Qt

Qt是一个C++的跨平台框架。
它最基本的是一个跨平台的Gui库。可以在Windows下X11下Mac OS X下以及若干嵌入设备中使用,Qt提供了对平台相关的Api的封装。
不过随着发展功能变得日渐丰富,Qt已经包含了众多跨平台的实用库与组件。除了QtCore和QtGui,还有特定用途的模块提供。
同时实现了对C++语言的扩充,一方是Qt自身的模块提供了比C++标准库更丰富的功能为开发带来便捷,另一方面是通过预处理机制下的QObject实现了Signals and Slots和Meta-Object System增强了对象的功能。
在Qt框架中也包含了对设计模式的重用,使用树状结构实现对象的管理,MVC在Gui中的运用,以及在其他Api的设计中体现出的。这可以提高新代码的编写速度与质量。
Qt中也提供了若干实用的工具程序。


PyQt

Python是一个通用的动态类型语言。
它的特性使得它可以提供更高层次的抽象,并且可以直接解释运行,让专注点侧重到所要处理的事务上,以获得高效的开发进展。
Py设计为一种胶水语言,很便于第三方库的绑定。PyQt便是目前主要的在Python中使用Qt的方式。
在使用PyQt中依然能够使用Qt强大的功能,但是通过Python的环境这种使用变得更加方便和高效。
不过Qt本身目前也有使用JavaScript进行脚本化的趋势,这个暂时观望态度,等4.7出来再看看。
毕竟这里仍是在使用Qt,PyQt只是下文将使用的一个界面接口而已,它能够为使用提供一定的便捷便足够了。
也就是说这里所关注的仍然如何更好的使用Qt这一方面的话题。
在版本的选择上这里将以Python2.5和Qt4.5中向后兼容的部分为参照来使用PyQt。


避免

目前PyQt是一个商业的开源项目,不过也有一个LGPL的实现叫PySide也在迅速开发中,它们使用了相一致的接口。
不过因为是Qt上又包装了一层而且Qt中各种零散的对象相当多,感觉使用PyQt4现在是比较容易撞到Bug的,所以使用时至少了解一下开发版的changelog,免得不知所措。
同时一般使用Python是在一些对执行性能要求不严格的场合。同样的C++代码转为Py写有后,在某些操作上是会有可察觉的界面上的迟钝的,而结构相同的C++代码却表现良好,这表明可以一些优化的手段。
此外对于一些特定平台来说,Qt自身的C++会是一个更合适的语言。
总体来说,这些不足不影响这里在使用Python上获得的好处。而且在Python的众多Gui绑定中,目前PyQt是一个可取的选择。


开始

这里的主要参考文档是 PyQt4的参考指南 以及 Qt的参考文档。
可以通过以下在线地址查阅PyQt4/Qt
不过安装好PyQt和Qt的话就会获得一份离线的副本,查看起来会更方便一些(比如使用QtAssistant)。
还有本实体书叫《C++ GUI Programming with Qt 4, Second Edition》,虽然不是必备的文档,但是其编排的体系还是有参考价值的。
也有本专门讲PyQt的实体书来着,那个叫什么来着的,我也不清楚版本的使用情况了。
目录什么还是可以看一下吧,看看说到了那些东西,这样查文档也会方向明确一点吧。
本文的定位是对Qt的基本概念已经有所了解,使用Python也能编写简单的脚本,而只是希望有更快捷高效的尝试。
像Qt这样的东西是了解一些原理后可以拿来使用而非学习的东西,其中一些组件是用到了才会产生使用价值的,不过呢只有简单尝试对它更加熟悉之后才能更好运用出它的价值吧。
下面文本将简略一些,不过文本展开思路什么的就按照这样了。


模板

这是一个模板代码:
#!/usr/bin/env python
#coding:utf-8
from PyQt4.QtCore import *
from PyQt4.QtGui import *

if __name__ == '__main__':
import sys
a = QApplication(sys.argv)
w = QWidget()
w.show()
sys.exit(a.exec_())
可以复制下来作为新的代码的开始。

另一种尝试的方式是在交互环境中依次执行下列语句:
>>> from PyQt4.QtGui import *
>>> a = QApplication([])
>>> w = QWidget()
>>> w.show()
>>> w.resize(200,200)
>>> w.hide()
这样每一步执行后便能立刻看到结果。

这里的代码部分使用了标签
<pre></pre>
代码的缩进会正常显示的

Qt的Python绑定

进化

下面从QWidget派生出新的窗口类型
#!/usr/bin/env python
#coding:utf-8
from PyQt4.QtCore import *
from PyQt4.QtGui import *

class mainwindow(QWidget):
def __init__(self,p=None):
QWidget.__init__(self,p)

if __name__ == '__main__':
import sys
a = QApplication(sys.argv)
w = mainwindow()
w.show()
sys.exit(a.exec_())
如果不实现对特定成员函数的OverRide的话,这里的派生不是必需的。
但是出于一种Qt惯例与一种可取的设计的角度,仍然建议这样做。
总之我们将用OverRide和Connect来为我们的QWidget的子类增加功能。

仅仅是添加一个新Widget的话,这样的代码就能工作了
#import missing
a = QApplication([])
w = QWidget()
QLineEdit(w)
w.show()
a.exec_()
这个w.show()包含了隐式的对匿名QLineEdit实例的show消息。交互环境下试这样的代码会方便一些。

于是再放两个文本框,就得到这样的代码
#!/usr/bin/env python
#coding:utf-8
from PyQt4.QtCore import *
from PyQt4.QtGui import *

class mainwindow(QWidget):
def __init__(self):
QWidget.__init__(self)

self.txt1=QLineEdit()
self.txt2=QLineEdit()
Layout=QHBoxLayout()
Layout.addWidget(self.txt1)
Layout.addWidget(self.txt2)
self.setLayout(Layout)

if __name__ == '__main__':
import sys
a = QApplication(sys.argv)
w = mainwindow()
w.show()
sys.exit(a.exec_())
这里为了让界面排列的好看一点,使用了一下Qt的布局功能。


属性

下面在代码添加一段简单的事件处理
#!/usr/bin/env python
#coding:utf-8
from PyQt4.QtCore import *
from PyQt4.QtGui import *

class mainwindow(QWidget):
def __init__(self,p=None):
QWidget.__init__(self,p)

self.txt1=QLineEdit()
self.txt2=QLineEdit()
Layout=QHBoxLayout()
Layout.addWidget(self.txt1)
Layout.addWidget(self.txt2)
self.setLayout(Layout)

self.txt1.textChanged.connect(self.on_txt1_changed)
def on_txt1_changed(self):
self.txt2.setText(self.txt1.text())

if __name__ == '__main__':
import sys
a = QApplication(sys.argv)
w = mainwindow()
w.show()
sys.exit(a.exec_())
这里使用了Signals and Slots机制,不过是使用了PyQt提供的额外的接口,传统的Qt方式也是可以的。
在Qt中Slot属于QObject的成员,PyQt中无此限定,所以这里的继承依然不是必须的。

这段代码如果写成属性操作的形式的话也行
#!/usr/bin/env python
#coding:utf-8
from PyQt4.QtCore import *
from PyQt4.QtGui import *

class mainwindow(QWidget):
def __init__(self):
QWidget.__init__(self)
self.resize(200,300)
self.txt1=QLineEdit(self,textChanged=self.on_txt1_changed,text=QString("hello world") )
self.txt3=QLineEdit(self)
self.txt3.move(20,20)

#@pyqtSlot()
def on_txt1_changed(self):
self.txt3.pyqtConfigure(text=self.txt1.text())

if __name__ == '__main__':
import sys
a = QApplication(sys.argv)
w = mainwindow()
w.show()
sys.exit(a.exec_())
对属性的操作涉及读与写两种操作,既可以使用Qt的setter/getter接口,也可以使用PyQt提供了额外的方式。


PyQt的接口

对Qt中提供的数据类型,PyQt提供了两种转换方式,比如QString就可作为一个例子:
一种是风格是将Qt中的类型的示例保存为单独的对象,仍然按照Qt中提供的对象的方法来操作,而Python中的类型则可以不作转换按PyQt_PyObject传给Qt。
这种风格就是按照Qt原本的接口来操作的,PyQt中默认便使用这种风格的接口。
另一种新的风格则是尽量将Qt中的类型转换为Python的类型,可以在Python提供的功能中完成对数据的操作,更贴近了Python的使用。
此外一些传引用的函数(需要多个返回值的函数),在PyQt绑定中改为了返回一个tuple对象。
本文这里依然以使用Qt为主,所以选择了目前默认使用的传统风格,而pickled对若干PyQt类型是可以工作的。
在使用Signals and Slots时,PyQt也提供了两种风格。一种传统风格接近Qt本身的方式,方便移植。另一种新的风格则有点像Tkinter,更加简洁易用。
不过这两种方式并不冲突,而对于新写代码使用新的方式显然会使代码更加直观与清晰。
所以这里一般就直接使用新的风格,其区别会在下一节展示。


Signals and Slots

Signals and Slots可以算是Qt中最明显的一个特性,其原理其参看Qt文档自身的描述。

首先是一个新风格的演示
#!/usr/bin/env python
#coding:utf-8
from PyQt4.QtCore import *

class O(QObject):
sig=pyqtSignal(str)
def __init__(self):
QObject.__init__(self)
self.sig.connect(self.handle)
#@pyqtSlot(str)
def handle(self,text):
print "hello %s"%text
def trigger(self):
self.sig.emit("world")
self.sig.disconnect()
self.sig.emit("black")

if __name__ == '__main__':
import sys
a = QCoreApplication(sys.argv)
O().trigger()
sys.exit(a.exec_())

这里显示的创建pyqtSignal是为了使用它的connect和emit方法。
可以显示的创建pyqtSlot指定参数类型,以实现C++中根据不同类型的overload。

传统风格用起来有些麻烦,Qt的预处理机制在这里无效
#!/usr/bin/env python
#coding:utf-8
from PyQt4.QtCore import *

class O(QObject):
def __init__(self):
QObject.__init__(self)
self.connect(self,SIGNAL("sig"),self.handle)
def handle(self,text):
print "hello %s"%text
def trigger(self):
self.emit(SIGNAL("sig"),"world")

if __name__ == '__main__':
import sys
a = QCoreApplication(sys.argv)
O().trigger()
sys.exit(a.exec_())
这段代码使用了Unbound Signals,并直接用了callable对象作了槽,已经有点新风格的特征了。
这段代码风格上不是很纯粹了,没有加pyqtSlot修饰并指定name属性(这个属性Qt中是预处理负责的,但是PyQt中创建的QObject实例是不知道自己的QMetaObject信息的。QMetaObject.connectSlotsByName()无法直接工作也是这个原因)。
属性则通过pyqtProperty创建,这里只写一种实用的方案,不是PyQt提供的全部。


Qt Designer

Qt Designer是Qt工具中可视化的图形界面创建工具。
在Qt中它编辑一个XML格式的ui文件,在由uic编译为C++文件。

在PyQt中也可以通过类似的方式使用
pyuic4 demo.ui -o ui_demo.py
可以在makefile中写
ui_%.py: %.ui
pyuic4 $< -o $@
这样可以简化版本上的依赖的管理

接下就是引入生成的文件并使用它
#!/usr/bin/env python
#coding:utf-8
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from ui_demo import Ui_Form
class M(QMainWindow):
def __init__(self,parent=None):
QMainWindow.__init__(self)
self.ui = Ui_Form()
self.ui.setupUi(self)

if __name__ == '__main__':
import sys
a = QApplication(sys.argv)
w = M()
w.show()
sys.exit(a.exec_())
这里创建的窗口上的QWidget在self.ui这个命名空间下。

不过去看pyuic4生成的代码的话,里面是一个有setupUi成员方法的对象,并添加一些代码让connectSlotsByName可以工作。
这样的就可以使用Qt的Auto-Connection机制,而免去一些繁琐手工操作。

使用名为on_<object name>_<signal name>的成员函数来添加事件相关的代码
#!/usr/bin/env python
#coding:utf-8
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from ui_demo import Ui_Form
class M(QMainWindow):
def __init__(self,parent=None):
QMainWindow.__init__(self)
self.ui = Ui_Form()
self.ui.setupUi(self)
#self.ui.lineEdit.textChanged.connect(self.on_lineEdit_textChanged)
def on_lineEdit_textChanged(self):
print "hello world"
if __name__ == '__main__':
import sys
a = QApplication(sys.argv)
w = M()
w.show()
sys.exit(a.exec_())
这里注释掉的那个语句是不可以保留的了,不然就被connect了两次。

或者直接只用uic模块也是可以的
#!/usr/bin/env python
#coding:utf-8
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from PyQt4.uic import *
if __name__ == '__main__':
import sys
a = QApplication(sys.argv)
ui=loadUi("demo.ui")
ui.show()
sys.exit(a.exec_())
uic.loadUi返回的是一个QWidget实例,没有setupUi方法了。
不过这不影响connect的使用,跨对象或跨线程都是可以的。

其他工具

pyrcc4用于处理rcc文件生成类似于资源文件qrc的py文件,使用时import便可。
pylupdate4是用于i18n的工具,从源代码中生成.ts文件。

Qt的组件示例

QMainWindow
The Application Main Window是QtGui程序的主框架
#!/usr/bin/env python
#coding:utf-8
from PyQt4.QtCore import *
from PyQt4.QtGui import *

class M(QMainWindow):
list=[]
def __init__(self):
QMainWindow.__init__(self)
self.textEdit=QTextEdit()
self.setCentralWidget(self.textEdit)
self.createActions()
self.createMenus()
self.createContextMenu()
self.createToolBars()
self.createStatusBar()
self.createDockWidget()

self.readSettings()
def closeEvent(self,e):
r = QMessageBox.question(self,self.tr("Title"), self.tr("Close?"),
QMessageBox.Yes | QMessageBox.No)
if r == QMessageBox.Yes:
self.writeSettings()
return e.accept()
elif r == QMessageBox.No:
return e.ignore()

def contextMenuEvent(self,e):
m=QMenu(self)
m.addAction(self.closeAct)
m.exec_(e.globalPos())

def createActions(self):
self.closeAct = QAction("Cl&ose", self, shortcut="Ctrl+F4",
statusTip="Close the active window",
triggered=self.close)
def createMenus(self):
self.fileMenu = self.menuBar().addMenu("&File")
self.fileMenu.addAction(self.closeAct)
self.fileMenu.addSeparator()
action = self.fileMenu.addAction("&New")
action.triggered.connect(self.newWindow)
def createToolBars(self):
self.fileToolBar = self.addToolBar("File")
self.fileToolBar.addAction(self.closeAct)
def createStatusBar(self):
self.statusBar().showMessage("Ready")
l = QLabel("hello world")
self.statusBar().addPermanentWidget(l)
def createContextMenu(self):
self.textEdit.addAction(self.closeAct);
self.textEdit.setContextMenuPolicy(Qt.ActionsContextMenu);
def createDockWidget(self):
dockWidget = QDockWidget(self.tr("Dock Widget"),self)
dockWidget.setAllowedAreas(Qt.LeftDockWidgetArea |
Qt.RightDockWidgetArea);
#dockWidget->setWidget(dockWidgetContents);
self.addDockWidget(Qt.LeftDockWidgetArea, dockWidget);

def newWindow(self):
w=M()
M.list.append(w)
w.show()
def readSettings(self):
settings = QSettings('ee.zsy', 'HelloWorld')
pos = settings.value('pos', QPoint(200, 200)).toPoint()
size = settings.value('size', QSize(400, 400)).toSize()
self.move(pos)
self.resize(size)

def writeSettings(self):
settings = QSettings('ee.zsy', 'HelloWorld')
settings.setValue('pos', self.pos())
settings.setValue('size', self.size())

if __name__ == '__main__':
import sys
a = QApplication(sys.argv)
w = M()
w.show()
sys.exit(a.exec_())
涉及到的Qt类有:QAction,QActionGroup,QDockWidget,QMainWindow,QMdiArea,QMdiSubWindow,QMenu,QMenuBar,SizeGrip,QStatusBar,QToolBar,QWidgetAction。
用于构建窗口自身,以及菜单工具栏状态栏等等。


Dialog Windows

对话框是QWidget的子类,可以是有模式的,也可以是无模式的。
通常用于和用户简短的交互,以达成特定任务或获得返回值。
#!/usr/bin/env python
#coding:utf-8
from PyQt4.QtCore import *
from PyQt4.QtGui import *

class D(QDialog):
def __init__(self,parent=None):
QDialog.__init__(self,parent)
self.bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel,
Qt.Horizontal,self,
accepted=self.accept,rejected=self.reject)
self.bb.show()
QPushButton("return",self,
clicked=self.on_btn_clicked,pos=QPoint(200,200)
).setDefault(False)
def on_btn_clicked(self):
self.done(99)

class M(QWidget):
def __init__(self):
QWidget.__init__(self)
self.setWindowTitle("Hello World")
self.btn1=QPushButton("modal dialog",clicked=self.on_btn1_clicked)
self.btn2=QPushButton("modelless dialog",clicked=self.on_btn2_clicked)
self.btn3=QPushButton("messageBox",clicked=self.on_btn3_clicked)
Layout=QVBoxLayout()
Layout.addWidget(self.btn1)
Layout.addWidget(self.btn2)
Layout.addWidget(self.btn3)
self.setLayout(Layout)

def on_btn1_clicked(self):
d = D(self)
r = d.exec_()
if r == QDialog.Accepted:
print "A"
elif r == QDialog.Rejected:
print "B"
def on_btn2_clicked(self):
d = QDialog(self)
d.show()
d.raise_()
d.activateWindow()
def on_btn3_clicked(self):
msgBox=QMessageBox()
msgBox.setText("The document has been modified.")
ret = msgBox.exec_()
ret = QMessageBox.warning(self, "My Application",
"The document has been modified.\n"
"Do you want to save your changes?",
QMessageBox.Save|QMessageBox.Discard
|QMessageBox.Cancel,QMessageBox.Save)
fileName = QFileDialog.getOpenFileName(self,
"Open Image", "/", "Image Files (*.png *.jpg *.bmp)")
if __name__ == '__main__':
import sys
a = QApplication(sys.argv)
w = M()
w.show()
sys.exit(a.exec_())
目前QDialog的派生类有QAbstractPrintDialog,QColorDialog, QErrorMessage,QFileDialog,QFontDialog,QInputDialog, QMessageBox,QPageSetupDialog,QPrintPreviewDialog,QProgressDialog以及QWizard。
注意connect是可以跨QObject进行的,对这里的QDialog也同样可以。


Layout Management

Layout和Widget这两样东西可以用来控制子Widget的摆放
#!/usr/bin/env python
#coding:utf-8
from PyQt4.QtCore import *
from PyQt4.QtGui import *


class P(QWidget):
def __init__(self):
QWidget.__init__(self)
self.sa = QScrollArea()
self.sp = QSplitter(Qt.Horizontal);
self.sp.addWidget(QTextEdit())
self.sp.addWidget(QTextEdit())
self.sp.setStretchFactor(1,7)
self.l = QListWidget()
self.l.addItem("A")
self.l.addItem("B")
self.l.addItem("C")
self.l.addItem("D")
self.l.setMaximumHeight(50)
self.l.setSizePolicy(QSizePolicy.Maximum,QSizePolicy.Maximum)
self.s = QStackedLayout()
self.s.addWidget(QLabel("A"))
self.s.addWidget(QLabel("B"))
self.s.addWidget(QLabel("C"))
self.s.addWidget(QLabel("D"))
self.l.currentRowChanged.connect(self.s.setCurrentIndex)
self.l.setCurrentRow(0)
self.f = QFormLayout()
self.f.addRow("&Index",self.l)
self.f.addRow("Page", self.s)
self.sa.setWidget(self.sp)
self.sa.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn);
self.sa.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn);
self.f.addRow("SP",self.sa)
self.setLayout(self.f)
if __name__ == '__main__':
import sys
a = QApplication(sys.argv)
w = P()
w.show()
sys.exit(a.exec_())
这里两种方式混合着用了,有点乱乱的呵。
另外呢,QAbstractScrollArea有个子类叫QMdiArea,用于MDI布局。
这里QFormLayout和QGridLayout有点类似之处,都是网格状的布局。


Drag and Drop

拖放操作涉及程序本身的鼠标事件处理,以及用于程序间的信息传递
#!/usr/bin/env python
#coding:utf-8
from PyQt4.QtCore import *
from PyQt4.QtGui import *

class M(QMainWindow):
def __init__(self):
QWidget.__init__(self)
self.clipboard = QApplication.clipboard()
self.textedit = QTextEdit(self)
self.label = QLabel("Hello World",self)
self.label.move(0,50)
self.textedit.setAcceptDrops(False)
self.setAcceptDrops(True)
def dragEnterEvent(self,event):
if event.mimeData().hasFormat("text/plain"):
event.acceptProposedAction()
def dropEvent(self,event):
text=event.mimeData().text()
self.setWindowTitle(text)
self.clipboard.setText(text)

def mousePressEvent(self,event):
if (event.button() == Qt.LeftButton and
self.label.geometry().contains(event.pos())):

drag = QDrag(self)
mimeData = QMimeData()
mimeData.setText("hello world")
drag.setMimeData(mimeData)
dropAction = drag.exec_(Qt.CopyAction | Qt.MoveAction)

if dropAction == Qt.MoveAction :
self.label.hide()
def dragEnterEvent(self,event):
if event.mimeData().hasFormat("text/plain"):
event.accept()
else:
event.ignore()
def mouseMoveEvent(self,event):
pass

if __name__ == '__main__':
import sys
a = QApplication(sys.argv)
w = M()
w.show()
sys.exit(a.exec_())
有QDropEvent::pos可获得方位。
拖放也可以结合Item Views使用。


Graphics.

这一段是说2D图像的绘制
#!/usr/bin/env python
#coding:utf-8
from PyQt4.QtCore import *
from PyQt4.QtGui import *
class M(QWidget):
def paintEvent(self,event):
img=QPixmap(self.size())
p=QPainter(img)
p.initFrom(self)
p.drawRect(10,10,50,50)
p=QPainter(self)
p.setRenderHint(QPainter.Antialiasing, True)
p.setWindow(-200, -10, 800, 600);
transform=QTransform()
transform.rotate(+45.0)
p.setWorldTransform(transform)
p.setPen(QPen(Qt.black, 5, Qt.DashDotLine, Qt.RoundCap))
p.setBrush(QBrush(Qt.green, Qt.Dense2Pattern))
p.drawEllipse(80, 80, 400, 240)
gradient=QLinearGradient(50, 100, 300, 350)
gradient.setColorAt(0.0, Qt.white)
gradient.setColorAt(0.2, Qt.green)
gradient.setColorAt(1.0, Qt.black)
p.save()
p.setBrush(gradient)
p.drawPie(80, 80, 400, 240, 60 * 16, 270 * 16)
p.restore()
p.drawPixmap(100,200,img)
p.setCompositionMode(QPainter.CompositionMode_Xor)
path=QPainterPath()
path.moveTo(8, 32)
path.cubicTo(200, 80, 320, 80, 480, 320)
p.drawPath(path)
p.scale(3,3)
p.translate(-50.0, -50.0)
p.drawText(100,100,900,500,Qt.AlignLeft,"Hello world")

if __name__ == '__main__':
import sys
a = QApplication(sys.argv)
w = M()
w.show()
sys.exit(a.exec_())
屏幕,图像,打印都可以用来绘制。


Binary Data

这里来说Qt中IO相关的
#!/usr/bin/env python
#coding:utf-8
from PyQt4.QtCore import *

class O(QObject):
def __init__(self):
QObject.__init__(self)
def run(self):
print "hello world"
l=QStringList()
l.append("A")
l.append("B")
file=QFile("output.dat")
if not file.open(QIODevice.WriteOnly):
print "Error: %s"%file.errorString()
return

i=QDataStream(file);
i.setVersion(QDataStream.Qt_4_5);
i << l

if __name__ == '__main__':
import sys
a = QCoreApplication(sys.argv)
O().run()
sys.exit(a.exec_())
此外QDir用于操作目录,QProcess用于操作进程。
不过这个例子有点怪怪的,应为在PyQt中以Python的方式在读存文件会更直接一些。
对Qt中要保存的数据去使用Python的pickle模块。


事件

要处理事件的话Reimplementing或者Installing Filters都可以
#!/usr/bin/env python
#coding:utf-8
from PyQt4.QtCore import *
from PyQt4.QtGui import *
class M(QWidget):
def __init__(self):
QWidget.__init__(self)
self.btn = QPushButton("start",self,clicked=self.run)
self.btn.installEventFilter(self)
def run(self):
QApplication.setOverrideCursor(Qt.WaitCursor)
for i in xrange(20000):
self.setWindowTitle(str(i))
qApp.processEvents(QEventLoop.ExcludeUserInputEvents)
QApplication.restoreOverrideCursor()

def keyPressEvent(self,event):
if event.key() == Qt.Key_Up:
self.setWindowTitle("Hello world")
else:
QWidget.keyPressEvent(self,event)

def eventFilter(self,target,event):
if target == self.btn:
if event.type() == QEvent.KeyPress:
if event.key() == Qt.Key_Down:
self.setWindowTitle("Hello world")
return True
return QWidget.eventFilter(self,target, event)

if __name__ == '__main__':
import sys
a = QApplication(sys.argv)
w = M()
w.show()
sys.exit(a.exec_())
直接操纵QApplication也是可以的。
这里的事件包括鼠标键盘事件窗口时间等。


Qt Designer Plugins

见/examples/designer/plugins
    zoom = QtCore.pyqtProperty(int, getZoom, setZoom, resetZoom)
会看到这样的东西,
并由QtDesigner.QPyDesignerCustomWidgetPlugin派生,实现特定的成员函数。


Container

Qt中STL的容器和算法的实现,可用Python中类似特性混合使用。


框架与特性

Graphics View

这是基于项目的2D Graphics的管理与显示
#!/usr/bin/env python
#coding:utf-8
from PyQt4.QtCore import *
from PyQt4.QtGui import *

class N(QGraphicsRectItem):
def __init__(self):
QGraphicsRectItem.__init__(self)
self.setRect(0,0,100,100)
self.setFlags(QGraphicsItem.ItemIsSelectable|
QGraphicsItem.ItemIsMovable|
QGraphicsItem.ItemSendsGeometryChanges)
self.setPen(QPen(Qt.darkRed, 1.0))
def itemChange(self,change,value):
if change == self.ItemPositionChange:
print value.toPointF()
return QGraphicsItem.itemChange(self,change, value)
def mouseDoubleClickEvent(event):
pass

class M(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.scene = QGraphicsScene(0, 0, 600, 500)
view = QGraphicsView()
view.setScene(self.scene)
view.setDragMode(QGraphicsView.RubberBandDrag)
view.setRenderHints(QPainter.Antialiasing
| QPainter.TextAntialiasing)
view.setContextMenuPolicy(Qt.ActionsContextMenu)
self.setCentralWidget(view)
self.createNode()
def createNode(self):
n=N()
self.scene.addItem(n)
self.scene.clearSelection()
n.setSelected(True)


if __name__ == '__main__':
import sys
a = QApplication(sys.argv)
w = M()
w.show()
sys.exit(a.exec_())
这里示例没能展现出Graphics View的整体。
Graphics View在结构上有Scene,View,Item,并有坐标转换机制。
还涉及到其包含的变换,拖拽,光标,动画,Groups等特性上的要点。


Item View

这部分是用来展示数据的
#!/usr/bin/env python
#coding:utf-8
from PyQt4.QtCore import *
from PyQt4.QtGui import *

class M(QWidget):
def __init__(self):
QWidget.__init__(self)
self.m=QDirModel()#QStandardItemModel
self.v=QTreeView(self,pos=QPoint(250,0))
self.v.setModel(self.m)
self.v.setSortingEnabled(True)
self.m.setNameFilters(QStringList()<<"*.*")

self.listWidget = QListWidget(self)
self.listWidget.setIconSize(QSize(60, 60))

self.listWidget.setEditTriggers(QAbstractItemView.NoEditTriggers)
for i in range(1,10):
item=QListWidgetItem(str(i),self.listWidget)
item.setToolTip("this is "+str(i))
self.listWidget.item(0).setText("hello")

btn1 = QPushButton(self,pos=QPoint(0,200),clicked=self.lwm)

def lwm(self):
item=self.listWidget.currentItem()
if item:
QMessageBox.information(self,"",item.text())
self.v.header().swapSections(0,3)

if __name__ == '__main__':
import sys
a = QApplication(sys.argv)
w = M()
w.show()
sys.exit(a.exec_())
MVC模式是模型视图控制器的组合,M将改变通知给V,而V上的操作可以使M得到改变。
以上是
不涉及代理类型,自定义模型/视图 以及委托。


Databases

数据库
#!/usr/bin/env python
#coding:utf-8

from PyQt4.QtCore import *
from PyQt4.QtGui import *
from PyQt4.QtSql import *

class O(QObject):

def __init__(self):
QObject.__init__(self)
db = QSqlDatabase.addDatabase("QSQLITE")
db.setDatabaseName("db.sqlite")
if not db.open():
QMessageBox.critical(0,"Database Error",db.lastError().text())
def run(self):
print "hello world"
QSqlDatabase.database().transaction()

query=QSqlQuery()
query.exec_("SELECT name, salary FROM employee WHERE salary > 50000")
while query.next() :
r = query.value(0).toString()
QSqlDatabase.database().commit()

model=QSqlTableModel()
model.setTable("cd")
model.setFilter("year >= 1998")
model.select()
#model.setQuery("SELECT * FROM cd WHERE year >= 1998");
for i in range(model.rowCount()):
record = model.record(i)
title = record.value("title").toString()

if __name__ == '__main__':
import sys
a = QCoreApplication(sys.argv)
O().run()
sys.exit(a.exec_())
这段代码非常不完整。
涉及到了使用SQL语句和SQL模型。
对应模型有一种关联模型,View用于显示和编辑,QDataWidgetMapper用于生成窗口。

Multithreading

多线程
#!/usr/bin/env python
#coding:utf-8

from PyQt4.QtCore import *
from PyQt4.QtGui import *
from PyQt4.QtSql import *

class T(QThread):
def __init__(self):
QThread.__init__(self)
self.mutex=QMutex()

def run(self):
self.mutex.lock()
while 1:
print "t"
self.mutex.unlock()
#QMetaObject.invokeMethod

class M(QWidget):
def __init__(self):
QWidget.__init__(self)
self.t=T()
QPushButton("start",self,clicked=self.start)
QPushButton("stop",self,clicked=self.stop,pos=QPoint(0,50))
def start(self):
self.t.start()
def stop(self):
self.t.terminate()

if __name__ == '__main__':
import sys
a = QApplication(sys.argv)
w = M()
w.show()
sys.exit(a.exec_())
涉及同步问题和通讯问题。
可以混合signal–slot使用,涉及到Qt提供的组件是否是可重入的。


Networking

网络通讯
#!/usr/bin/env python
#coding:utf-8

from PyQt4.QtCore import *
from PyQt4.QtGui import *
from PyQt4.QtNetwork import *

class O(QObject):
def __init__(self):
QObject.__init__(self)
self.h=QHttp(done=self.httpDone)
def run(self):
self.h.get("http://google.com")
def httpDone(self,error):
print "hello world"

if __name__ == '__main__':
import sys
a = QCoreApplication(sys.argv)
O().run()
sys.exit(a.exec_())
这是QHttp,还有链接的各种相关属性与状态。
另一种用法是QTcpSocket和QTcpServer的组合,是有固定用法的,QtDemo里的很完整了。
这个模块与Python的功能重叠。


XML

读取的话,可以三种方式
QXmlStreamReader,QDomDocument,QXmlContentHandler。
写XML的话用QXmlStreamWriter,保存DOM,或字符串

其他

其他
QDesktopServices::openUrl(url);
i18n
窗口样式

实用类
正则
验证
进程
测试

...

----
以上就是烂尾,其实也较待续。。。

这篇看样子是随手笔记式的类型了,
关于Qt的主要参考是其文档以及官方的那本教程。
在以上的文字里,仅仅是大致的去涉及了一些Qt中常见的模块。
而Qt本身也是在不断扩充的,其中一些功能重写的代价也不高,不过Qt中包含的话就可以重用。
所以这里就先大略说这些了。