概述

QPython 支持 WEB APP,这能让所有熟悉 WEB 开发模式的工程师快速地进入移动开发。

目前已有不少成熟的 WEB APP 开发方案,比如 Cordova,Phonegap 等。

相比于它们, QPython 的 WEB APP 方案有以下优势:

  • 强大的本地逻辑处理解析器 Python 脚本解析器 QPython 有一个强大的本地逻辑处理解析器,不需要像它们一样需要将大量的工作放置在服务端才能完成,QPython 能有效地解决网络传输带来的延迟影响。

  • 海量的库支持,更快实现 Python 世界中积累了多年的大部分的类库都能够被快速引入到 QPython 体系中,因此您可以不必重复造轮子。

  • 掌上即可完成开发 进行 QPython 开发,您只需要有一台安卓手机即可。当然您也可以在 PC 端完成开发,再将程序上传到 QPython 上,您也可以直接在 QPython 中进行开发。

原理

通过 QPython,你可以在手机上启动自定义的的 Python 服务,包括 WEB 服务。在脚本中按照 QPython 的 WEB APP 的规则加入 WEB APP 所需要的定义信息,即可让 QPython 在运行时载入 WebView 控件,访问对应 Web 服务。

接下来,我们开始以 Bottle 开发框架为例,带你进入 QPython 第一个 WEB APP 工程开发。

项目:我在哪里

这是一个需要你一步步动手的教学项目,它主要是使用了 QPython 开发的 WEB APP。它能在地图上显示我的位置坐标,并分享出去。

在项目的过程中,你可以学习使用 WEB APP 建立起来的 WEB APP 的框架,以及如何使用 SL4A 接口获取地理位置信息的知识;最后能学习调用安卓体系内其他 APP 的公开接口的方法。

开始动手:1 新建项目 & WebApp 初探

你可以通过以下步骤,创建一个 WebApp 项目

  • 1 打开编辑器,点击右上角的新建
  • 2 多个项目类型的对话框就会弹出,选择 WebApp
  • 3 弹出对话框,你需要为项目取个名字
  • 4 正确输入项目名称后, QPython 会自动按照项目名称在项目目录中创建目录,并且按照 WebApp 的模版创建一个 main.py 文件。

完成上述步骤后,你就可以开始编辑 main.py 了。下面介绍下默认创建好的 main.py。

#-*-coding:utf8;-*-
#qpy:2
#qpy:webapp:Sample 
#qpy://localhost:8080/
"""
This is a sample for qpython webapp
"""
from bottle import route, run

@route('/')
def hello():
    return "Hello World!"

run(host='localhost', port=8080)

下面是一步步解释:

从 Bottle 中引入 route 和 run 方法:

from bottle import route, run

引入了 route 方法之后,代码中通过 @route("/") 即可将 hello 方法注册为用户在访问 "http://(主机地址):(端口)/" 时默认响应的处理函数。而 hello 的动作:返回 "Hello World!"。

@route('/')
def hello():
    return "Hello World!"

完成上述的定义后,Bottle 框架就完成了对 WEB 服务器的行为定义,然后通过 run 指令就启动了 WEB 服务。

run(host='localhost', port=8080)

WEB 服务启动的同时,QPython 会根据 main.py 文件头部的声明去确定 App 启动的模式,根据 WebApp 头部定义的协议,QPython 会启动 WebView 控件, 标题为: Sample,同时载入 //localhost:8080/。 因此,用户就看到了 "Hello World!" 。

#qpy:webapp:Sample 
#qpy://localhost:8080/

开始动手:2 深入 WebApp

通过新建项目而创建的 WebApp 仅给出了 QPython 中 WebApp 的最小框架体系,但是在实际中,需要考虑下列两个关键的功能点才算一个完整的 QPython WebApp。

健康监测

QPython 在启动 WebApp 时,会访问 /__ping 路径,根据 HTTP 返回值来检测服务是否准备好,会在服务准备好时载入对应的 URL。因此,你可以在__ping 来中自定义健康检查。在这里,我们仅仅返回 "ok",因为当服务 OK 时,其 HTTP 返回值正是 200。

包含自我结束功能的完整服务

在通过点 QPython 的退出键来关闭 QPython WebApp 时,最后都会通过调用 /__exit URL,如果你想要让 QPython WebApp 在退出时也关闭,则需要在该 URL 部分定义好退出函数。

由于默认的 bottle 在处理退出时比较难出来,所以我们引入了自定义的 MyWSGIRefServer,这能很好实现自我关闭。

看最后的启动 MyWSGIRefServer 的指令。

    app = Bottle()
    app.route('/', method='GET')(index)
    app.route('/hello', method='GET')(hello)
    app.route('/__exit', method=['GET','HEAD'])(__exit)
    app.route('/__ping', method=['GET','HEAD'])(__ping)

    try:
        server = MyWSGIRefServer(host="127.0.0.1", port="8080")
        app.run(server=server,reloader=False)
    except Exception,ex:
        print "Exception: %s" % repr(ex)

以下为 MyWSGIRefServer 定义以及 __exit 方法定义:

class MyWSGIRefServer(ServerAdapter):
    server = None
    def run(self, handler):
        from wsgiref.simple_server import make_server, WSGIRequestHandler
        if self.quiet:
            class QuietHandler(WSGIRequestHandler):
                def log_request(*args, **kw): pass
            self.options['handler_class'] = QuietHandler
        self.server = make_server(self.host, self.port, handler, **self.options)
        self.server.serve_forever()

    def stop(self):
        #sys.stderr.close()
        import threading
        threading.Thread(target=self.server.shutdown).start()
        #self.server.shutdown()
        self.server.server_close() #<--- alternative but causes bad fd exception
        print "# qpyhttpd stop"

def __exit():
    Droid.stopLocating()
    global server
    server.stop()

开始动手:3 调用 SL4A 获取地理位置

完成上述步骤后,想必你也对 QPython 的 WEB APP 有了一个基本的了解,接下来我们将要实现如何使用 SL4A 接口来获得地理位置,并用图形展示出来。

根据 SL4A 的文档,开始查询地理位置的接口如下:

import androidhelper
Droid = androidhelper.Android()
Droid.startLocating()

而想要获得地理位置的接口如下:

location = Droid.getLastKnownLocation().result
location = location.get('network', location.get('gps'))

将此代码片段在后面增加 print location 后运行,发现得到 None,因此首先检查地理位置开关是否打开?WIFI 是否打卡?确定打开之后再运行尝试。用 Python 的接口是不是很简单 ?

获取地理位置信息没问题后,我们再来看下百度的文档,[掌握在其 WEB 版本的地图上根据已知的坐标显示位置的接口](http://developer.baidu.com/map/index.php?title=uri/api/web):

因此确定其接口类似:

http://api.map.baidu.com/marker?location=39.916979519873,116.41004950566&title=我的位置&content=我在这里&output=html

开始动手:4 完成运行

在了解了 QPython WebApp 的框架、SL4A 获得地理位置的方法、百度地图 API 的使用之后,结合你以掌握的 Web 开发知识,你就可以轻松地实现对应的目标了。最终代码如下:

#-*-coding:utf8;-*-
#qpy:2
#qpy:webapp:Sample
#qpy://localhost:8080/
"""
This is a sample for qpython webapp
"""
import os.path
from bottle import Bottle, ServerAdapter
from bottle import template,request,response,redirect,HTTPResponse
root = os.path.dirname(os.path.abspath(__file__))

import androidhelper
Droid = androidhelper.Android()
Droid.startLocating(5000,5)

class MyWSGIRefServer(ServerAdapter):
    server = None
    def run(self, handler):
        from wsgiref.simple_server import make_server, WSGIRequestHandler
        if self.quiet:
            class QuietHandler(WSGIRequestHandler):
                def log_request(*args, **kw): pass
            self.options['handler_class'] = QuietHandler
        self.server = make_server(self.host, self.port, handler, **self.options)
        self.server.serve_forever()

    def stop(self):
        #sys.stderr.close()
        import threading
        threading.Thread(target=self.server.shutdown).start()
        #self.server.shutdown()
        self.server.server_close() #<--- alternative but causes bad fd exception
        print "# qpyhttpd stop"

def __exit():
    Droid.stopLocating()
    global server
    server.stop()

def __ping():
    return "ok"


def index():
    Droid.vibrate()
    return """<html><body><button onclick='location.href="/hello"'>显示我在哪里</button></body></html>"""

def hello():
    location = Droid.getLastKnownLocation().result
    location = location.get('network', location.get('gps'))
    #location = {"latitude":"116.387884","longitude":"39.929986"}
    return template(root+'/baidu.tpl',lat=location['latitude'],lng=location['longitude'])

if __name__ == '__main__':
    app = Bottle()
    app.route('/', method='GET')(index)
    app.route('/hello', method='GET')(hello)
    app.route('/__exit', method=['GET','HEAD'])(__exit)
    app.route('/__ping', method=['GET','HEAD'])(__ping)

    try:
        server = MyWSGIRefServer(host="127.0.0.1", port="8080")
        app.run(server=server,reloader=False)
    except Exception,ex:
        print "Exception: %s" % repr(ex)

其中 hello 页面所使用的 baidu.tpl 模板,由百度地图生成,注意需要替换 var markerArr = [{title:"我在这里",content:"我的位置",point:"xxx|yyy",isOpen:1,icon:{w:21,h:21,l:0,t:0,x:6,lb:5}} 中的 xxx 和 yyy 为 {{lng}} 及 {{lat}} 即可

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
<meta name="keywords" content="百度地图,百度地图API,百度地图自定义工具,百度地图所见即所得工具" />
<meta name="description" content="百度地图API自定义地图,帮助用户在可视化操作下生成百度地图" />
<title>百度地图API自定义地图</title>
<!--引用百度地图API-->
<style type="text/css">
    html,body{margin:0;padding:0;}
    .iw_poi_title {color:#CC5522;font-size:14px;font-weight:bold;overflow:hidden;padding-right:13px;white-space:nowrap}
    .iw_poi_content {font:12px arial,sans-serif;overflow:visible;padding-top:4px;white-space:-moz-pre-wrap;word-wrap:break-word}
</style>
<script type="text/javascript" src="http://api.map.baidu.com/api?key=&v=1.1&services=true"></script>
</head>

<body>
  <!--百度地图容器-->
  <div style="width:480px;height:550px;border:#ccc solid 1px;" id="dituContent"></div>
</body>
<script type="text/javascript">
    //创建和初始化地图函数:
    function initMap(){
        createMap();//创建地图
        setMapEvent();//设置地图事件
        addMapControl();//向地图添加控件
        addMarker();//向地图中添加marker
    }

    //创建地图函数:
    function createMap(){
        var map = new BMap.Map("dituContent");//在百度地图容器中创建一个地图
        var point = new BMap.Point(116.387884,39.929986);//定义一个中心点坐标
        map.centerAndZoom(point,12);//设定地图的中心点和坐标并将地图显示在地图容器中
        window.map = map;//将map变量存储在全局
    }

    //地图事件设置函数:
    function setMapEvent(){
        map.enableDragging();//启用地图拖拽事件,默认启用(可不写)
        map.enableScrollWheelZoom();//启用地图滚轮放大缩小
        map.enableDoubleClickZoom();//启用鼠标双击放大,默认启用(可不写)
        map.enableKeyboard();//启用键盘上下左右键移动地图
    }

    //地图控件添加函数:
    function addMapControl(){
        //向地图中添加缩放控件
    var ctrl_nav = new BMap.NavigationControl({anchor:BMAP_ANCHOR_TOP_LEFT,type:BMAP_NAVIGATION_CONTROL_LARGE});
    map.addControl(ctrl_nav);
        //向地图中添加缩略图控件
    var ctrl_ove = new BMap.OverviewMapControl({anchor:BMAP_ANCHOR_TOP_LEFT,isOpen:1});
    map.addControl(ctrl_ove);

        //向地图中添加比例尺控件
    var ctrl_sca = new BMap.ScaleControl({anchor:BMAP_ANCHOR_BOTTOM_LEFT});
    map.addControl(ctrl_sca);
    }

    //标注点数组
    var markerArr = [{title:"我在这里",content:"我的位置",point:"{{lng}}|{{lat}}",isOpen:1,icon:{w:21,h:21,l:0,t:0,x:6,lb:5}}
         ];
    //创建marker
    function addMarker(){
        for(var i=0;i<markerArr.length;i++){
            var json = markerArr[i];
            var p0 = json.point.split("|")[0];
            var p1 = json.point.split("|")[1];
            var point = new BMap.Point(p0,p1);
            var iconImg = createIcon(json.icon);
            var marker = new BMap.Marker(point,{icon:iconImg});
            var iw = createInfoWindow(i);
            var label = new BMap.Label(json.title,{"offset":new BMap.Size(json.icon.lb-json.icon.x+10,-20)});
            marker.setLabel(label);
            map.addOverlay(marker);
            label.setStyle({
                        borderColor:"#808080",
                        color:"#333",
                        cursor:"pointer"
            });

            (function(){
                var index = i;
                var _iw = createInfoWindow(i);
                var _marker = marker;

                _marker.addEventListener("click",function(){
                    this.openInfoWindow(_iw);
                });
                _iw.addEventListener("open",function(){
                    _marker.getLabel().hide();
                })
                _iw.addEventListener("close",function(){
                    _marker.getLabel().show();
                })
                label.addEventListener("click",function(){
                    _marker.openInfoWindow(_iw);
                })
                if(!!json.isOpen){
                    label.hide();
                    _marker.openInfoWindow(_iw);
                }
            })()
        }
    }
    //创建InfoWindow
    function createInfoWindow(i){
        var json = markerArr[i];
        var iw = new BMap.InfoWindow("<b class='iw_poi_title' title='" + json.title + "'>" + json.title + "</b><div class='iw_poi_content'>"+json.content+"</div>");
        return iw;
    }
    //创建一个Icon
    function createIcon(json){
        var icon = new BMap.Icon("http://app.baidu.com/map/images/us_mk_icon.png", new BMap.Size(json.w,json.h),{imageOffset: new BMap.Size(-json.l,-json.t),infoWindowOffset:new BMap.Size(json.lb+5,1),offset:new BMap.Size(json.x,json.h)})
        return icon;
    }

    initMap();//创建和初始化地图
</script>
</html>

有了 QPython 之后, 是不是开发 Mobile App 就和 Web 开发一样简单?