zoukankan      html  css  js  c++  java
  • 狗书(flask基础)

      为什么选择使用flask?

      和其他框架相比, Flask 之所以能脱颖而出,原因在于它让开发者做主,使其能对程序具有全面的创意控制。

      Flask 中,你可以自主选择程序的组件,如果找不到合适的,还可以自己开发。
      Flask 提供了一个强健的核心, 其中包含每个 Web 程序都需要的基本功能,而其他功能则交给行业系统中的众多第三方扩展。
      一句话概括就是flask不是一个高度定制化的web框架,你可以做到随心所欲,使用任何可能的扩展来完成你的项目。

      狗书的代码已上传GitHub:Companion code to my O'Reilly book "Flask Web Development

      Flask 有两个主要依赖:路由、调试和 Web 服务器网关接口(Web Server Gateway InterfaceWSGI)子系统由 Werkzeughttp://werkzeug.pocoo.org/)提供;模板系统由 Jinja2http://jinja.pocoo.org/)提供。 Werkzeug Jinjia2 都是由 Flask 的核心开发者开发而成。

      Flask 并不原生支持数据库访问、 Web 表单验证和用户认证等高级功能。这些功能以及其他大多数 Web 程序中需要的核心服务都以扩展的形式实现, 然后再与核心包集成。 
      安装

    pip install flask

      初始化

      所有 Flask 程序都必须创建一个程序实例Web 服务器使用一种名为 Web 服务器网关接口(Web Server Gateway InterfaceWSGI)的协议,把接收自客户端的所有请求都转交给这个对象处理。

    from flask import Flask
    app = Flask(__name__)

      Flask接受一个字符串作为参数,这个参数决定程序的根目录,以便于能找到相对于程序根目录的资源文件的位置,通常这种情况下都使用  __name__作为Flask参数。

      也就是说,此时web框架接收的请求都会通过flask实例化的对象进行处理。

      这里的初始化方式是最简单的初始化方式,后面会使用到更为复杂的初始化方式。

      路由和视图函数

      程序实例需要知道对每个 URL 请求运行哪些代码,所以保存了一个 URL Python 函数的映射关系。处理 URL 和函数之间关系的程序称为路由

      在 Flask 程序中定义路由的最简便方式,是使用程序实例提供的 app.route 修饰器,把修饰的函数注册为路由。

    @app.route('/')
    def index():
        return '<h1>Hello World!</h1>'

      这个函数的返回值称为响应,是客户端接收到的内容。如果客户端是 Web 浏览器, 响应就是显示给用户查看的文档(一般就是html页面)。

      程序实例用 run 方法启动 Flask 集成的开发 Web 服务器:

    if __name__ == '__main__':
        app.run(debug=True)

      有一些选项参数可被 app.run() 函数接受用于设置 Web 服务器的操作模式。在开发过程中启用调试模式会带来一些便利, 比如说激活调试器重载程序。要想启用调试模式, 我们可以把 debug 参数设为 True

      第一个程序:

    from flask import Flask
    app = Flask(__name__)
    
    
    @app.route('/')
    def index():
        return '<h1>Hello World!</h1>'
    
    
    @app.route('/user/<name>')
    def user(name):
        return '<h1>Hello, {}!</h1>'.format(name)
    if __name__ == '__main__':
        app.run()

      为了避免大量可有可无的参数把视图函数弄得一团糟, Flask 使用上下文临时把某些对象变为全局可访问。有了上下文,就可以写出下面的视图函数:

    from flask import request
    @app.route('/')
    def index():
        user_agent = request.headers.get('User-Agent')
        return '<p>Your browser is %s</p>' % user_agent

      这里我们把request当作全局变量使用,实际生产中每个线程都处理不同的请求,那么他们的request必然是不同的。Falsk 使用上下文让特定的变量在一个线程中全局可访问,与此同时却不会干扰其他线程。

      request就是一种python中的ThreadLocal对象。

      只在线程中是全局变量。

    #转载至csdn,http://blog.csdn.net/hyman_c/article/details/52548540
    import threading
    
    localobj = threading.local()#flask中的request就是这样的概念
    
    
    def threadfunc(name):
        localobj.name = name
        print('localobj.name is %s' % name)
    
    
    if __name__ == '__main__':
        t1 = threading.Thread(target=threadfunc, args=('Hyman',))
        t2 = threading.Thread(target=threadfunc, args=('liuzhihui',))
        t1.start()
        t2.start()
        t1.join()
        t2.join()

      在多线程服务器中客户端每建立一个链接,服务器就创建一个线程,每个线程中就会有一个request来表示客户端的链接请求信息。

      flask上下文全局变量

      没激活程序上下文之前就调用 current_app.name 会导致错误,但推送完上下文之后就可以调用了。 注意,在程序实例上调用 app.app_context() 可获得一个程序上下文。ctx.push()是激活上下文的操作,类似的,如果我们想要回收上下文,用ctx.pop()。

    from hello import app
    from flask import current_app
    app_ctx = app.app_context()
    app_ctx.push()
    current_app.name
    'hello'
    from flask import g
    g.name='manno'

      g作为flask程序全局的一个临时变量,充当者中间媒介的作用,我们可以通过它传递一些数据。

      request请求对象,封装了客户端发送的HTTP请求的内容。

      session用户会话,用来记住请求(比如前后一个GET请求和一个POST请求)之间的值,从数据格式上来说它是字典类型。它存在于连接到服务器的每个客户端中,属于私有存储,会保存在客户端的cookie中。

    session['name']=form.name.data

      URL 映射是 URL 和视图函数之间的对应关系。Flask 使用 app.route 修饰器或者非修饰器形式的 pp.add_url_rule() 生成映射。

    app.url_map
    Map([<Rule '/' (GET, HEAD, OPTIONS) -> index>,
     <Rule '/static/<filename>' (GET, HEAD, OPTIONS) -> static>,
     <Rule '/user/<name>' (GET, HEAD, OPTIONS) -> user>])

      / /user/<name> 路由在程序中使用 app.route 修饰器定义。 /static/<filename> 路由是Flask 添加的特殊路由,用于访问静态文件。

      请求钩子

      Flask 支持以下 4 种钩子。

      before_first_request:注册一个函数,在处理第一个请求之前运行。
      before_request:注册一个函数,在每次请求之前运行。
      after_request:注册一个函数,如果没有未处理的异常抛出,在每次请求之后运行。
      teardown_request:注册一个函数,即使有未处理的异常抛出,也在每次请求之后运行。

      注:在请求钩子函数和视图函数之间共享数据一般使用上下文全局变量 g。例如, before_request 处理程序可以从数据库中加载已登录用户,并将其保存到 g.user 中。随后调用视
    图函数时,视图函数再使用 g.user 获取用户。

      flask中具有四种钩子被做成了修饰器,我们在后端可以进行调用做相关的操作.使用钩子函数时,我们需要借助flask的全局变量g.g作为中间变量,在钩子函数和视图函数中间传递数据.我们先引入全局变量g。

      第一步,引入全局变量g

    from flask import g

      第二步, 然后注册一个视图函数,用来显示g中的数据

    @app.route('/test')  
    def test():  
        return g.string  

      这里我们使用request之前的钩子

    @app.before_first_request  
    def bf_first_request():  
        g.string = 'before_first_request'  

      此时运行此路由显示的是g中传递的string变量(当然也可以不返回进行其他操作)。

      响应

      flask的响应包括模板(本质是字符串)和状态码。

    @app.route('/')
    def index():
        return '<h1>Bad Request</h1>', 400

      如果不想返回由 1 个、 2 个或 3 个值组成的元组, Flask 视图函数还可以返回 Response 对象。 make_response() 函数可接受 1 个、 2 个或 3 个参数(和视图函数的返回值一样),并
    返回一个 Response 对象。

    from flask import make_response
    @app.route('/')
    def index():
        response = make_response('<h1>This document carries a cookie!</h1>')
        response.set_cookie('answer', '42')#创建对象,给对象加cookie
        return response

      flask重定向

    from flask import redirect
    @app.route('/')
    def index():
        return redirect('http://www.example.com')

      abort生成响应404,abort 不会把控制权交还给调用它的函数,而是抛出异常把控制权交给 Web 服务器。

    from flask import abort
    @app.route('/user/<id>')
    def get_user(id):
        user = load_user(id)
        if not user:
            abort(404)
        return '<h1>Hello, %s</h1>' % user.name

      使用Flask-Script支持命令行选项

      Flask 的开发 Web 服务器支持很多启动设置选项,但只能在脚本中作为参数传给 app.run()函数。这种方式并不十分方便,传递设置选项的理想方式是使用命令行参数。

      Flask-Script 是一个 Flask 扩展,为 Flask 程序添加了一个命令行解析器。 Flask-Script 自带了一组常用选项,而且还支持自定义命令。

    pip install flask-script
    from flask.ext.script import Manager
    manager = Manager(app)
    
    if __name__ == '__main__':
    manager.run()

      专为 Flask 开发的扩展都暴漏在 flask.ext 命名空间下。 Flask-Script 输出了一个名为Manager 的类,可从 flask.ext.script 中引入。

      这个扩展的初始化方法也适用于其他很多扩展: 把程序实例作为参数传给构造函数,初始化主类的实例。 创建的对象可以在各个扩展中使用。在这里,服务器由 manager.run()
    动,启动后就能解析命令行了。

    from flask import Flask
    from flask_script import Manager
    
    app = Flask(__name__)
    
    manager = Manager(app)
    
    
    @app.route('/')
    def index():
        return '<h1>Hello World!</h1>'
    
    
    @app.route('/user/<name>')
    def user(name):
        return '<h1>Hello, %s!</h1>' % name
    
    
    if __name__ == '__main__':
        manager.run()

       使用manager可以增加自定义命令

    from flask_script import Manager
    app = Flask(__name__)
    manager=Manager(app)
    
    
    @manager.command
    def print_str():
        print('hello world')
    
    if __name__ == '__main__':
        manager.run()

      设置cookie

    @app.route('/set_cookie')  
    def set_cookie():  
        response=make_response('Hello World');  
        response.set_cookie('Name','Hyman')  
        return response 

      我们还可以指定cookie的有效时长,下面的代码把有效时长设置成了30天.通常情况下,我们还可以在浏览器上设置cookie的有效时长,而且浏览器上配置的有效时长优先级要高于我们在代码中设置的。

    outdate=datetime.datetime.today() + datetime.timedelta(days=30)  
    response.set_cookie('Name','Hyman',expires=outdate) 

      获取cookie

    @app.route('/get_cookie')  
    def get_cookie():  
        name=request.cookies.get('Name')  
        return name 
    <h1>My name is {{request.cookies.get('Name')}}</h1>
    {#html模板中获取cookie#}

      删除cookie(三种方式)

      (1) 可以通过在浏览器中设置来清除cookie

      (2) 使用Response的set_cookie进行清除

    @app.route('/del_cookie')
    def del_cookie():
        response=make_response('delete cookie')
        response.set_cookie('Name','',expires=0)
        return response

      (3)使用Response的 delete_cookie方法

    @app.route('/del_cookie2')  
    def del_cookie2():  
        response=make_response('delete cookie2')  
        response.delete_cookie('Name')  
        return response  

      Jinja2模板引擎

      Jinja2模板引擎是flask默认的模板引擎。

       Flask 提供的 render_template 函数把 Jinja2 模板引擎集成到了程序中 。使用方式与django的render基本一致。

      Jinja2 能识别所有类型的变量, 甚至是一些复杂的类型,例如列表、字典和对象。

    <p>A value from a dictionary: {{ mydict['key'] }}.</p>
    <p>A value from a list: {{ mylist[3] }}.</p>
    <p>A value from a list, with a variable index: {{ mylist[myintvar] }}.</p>
    <p>A value from an object's method: {{ myobj.somemethod() }}.</p>

      jinja2很大程度上和django的模板语言很类似,包括过滤器的使用,判断,循环,模板继承等。

      来说点特别的,jinja2支持宏(类似于函数,使用也和函数很像)。

      定义及使用宏:

    {% macro render_comment(comment) %}
        <li>{{ comment }}</li>
    {% endmacro %}
    <ul>
        {% for comment in comments %}
            {{ render_comment(comment) }}
        {% endfor %}
    </ul>

      重复使用宏需要将其保存在单独的文件中,然后在需要使用的模板中导入

    {% import 'macros.html' as macros %}
    <ul>
        {% for comment in comments %}
            {{ macros.render_comment(comment) }}
        {% endfor %}
    </ul>

      除了重复使用宏的方式还可以重复导入代码块。

    common.html
    <h1>我是重复的代码片</h1>

      导入这个代码块

    {% include 'common.html' %}
    {% include 'common.html' %}
    {% include 'common.html' %}
    {% include 'common.html' %}
    <h1>Hello World</h1>

      jinja2还支持与django模板一样的模板继承功能。

      要想在flask程序中集成 Bootstrap, 显然要对模板做所有必要的改动。不过,更简单的方法是使用一个名为 Flask-Bootstrap Flask 扩展(这也是我喜欢flask的原因之一吧,扩展性强,插件还很多),简化集成的过程。 

    pip install flask-bootstrap

      初始化 Flask-Bootstrap

    from flask.ext.bootstrap import Bootstrap
    bootstrap = Bootstrap(app)

      初始化 Flask-Bootstrap 之后,就可以在程序中使用一个包含所有 Bootstrap 文件的基模板。

    {% extends "bootstrap/base.html" %}
    {% block title %}Flasky{% endblock %}

    {% extends "bootstrap/base.html" %}
    
    {% block title %}Flasky{% endblock %}
    
    {% block navbar %}
    <div class="navbar navbar-inverse" role="navigation">
        <div class="container">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                    <span class="sr-only">Toggle navigation</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a class="navbar-brand" href="/">Flasky</a>
            </div>
            <div class="navbar-collapse collapse">
                <ul class="nav navbar-nav">
                    <li><a href="/">Home</a></li>
                </ul>
            </div>
        </div>
    </div>
    {% endblock %}
    
    {% block content %}
    <div class="container">
        <div class="page-header">
            <h1>Hello, {{ name }}!</h1>
        </div>
    </div>
    {% endblock %}

      自定制错误页面

    @app.errorhandler(404)
    def page_not_found(e):
        return render_template('404.html'), 404
    @app.errorhandler(500)
    def internal_server_error(e):
        return render_template('500.html'), 500

       url_for() 函数最简单的用法是以视图函数名(或者 app.add_url_route() 定义路由时使用的端点名)作为参数, 返回对应的 URL。例如,在当前版本的 hello.py 程序中调用 url_for('index') 得到的结果是 /。调用 url_for('index', _external=True) 返回的则是绝对地址,在这个示例中是 http://localhost:5000/
      使用 url_for() 生成动态地址时,将动态部分作为关键字参数传入。例如, url_for('user', name='john', _external=True) 的返回结果是 http://localhost:5000/user/john
      如果 Web 程序的用户来自世界各地,那么处理日期和时间可不是一个简单的任务。
      有一个使用 JavaScript 开发的优秀客户端开源代码库,名为 moment.jshttp://momentjs.com/),它可以在浏览器中渲染日期和时间。 Flask-Moment 是一个 Flask 程序扩展,能把moment.js 集成到 Jinja2 模板中。

    pip install flask-moment

      此模块依赖于moment.js 和jquery.js

    from datetime import datetime
    @app.route('/')
    def index():
        return render_template('index.html',current_time=datetime.utcnow())

      渲染当前时间:

    {% block scripts %}
    {{ super() }}
    {{ moment.include_moment() }}
    {% endblock %}
    <p>The local date and time is {{ moment(current_time).format('LLL') }}.</p>
    <p>That was {{ moment(current_time).fromNow(refresh=True) }}</p>

    {{moment.lang("zh-CN")}}//设置语言
    {{moment().format('YYYY-MM-DD,h:mm:ss a')}}//设置时间格式
    常用格式化参数
    YYYY    2014    年份
    YY    14    2个字符表示的年份
    Q    1..4    季度
    M MM    4..04    月份
    MMM MMMM    4月..四月    根据moment.locale()中的设置显示月份
    D DD    1..31    一月中的第几天
    Do    1日..31日    一月中的第几天
    DDD DDDD    1..365    一年中的第几天
    X    1410715640.579    时间戳
    x    1410715640579    时间戳

      flask表单处理

    pip install flask-wtf

      为了实现 CSRF 保护, Flask-WTF 需要程序设置一个密钥。 Flask-WTF 使用这个密钥生成加密令牌,再用令牌验证请求中表单数据的真伪。设置密钥的方法 

    app = Flask(__name__)
    app.config['SECRET_KEY'] = 'hard to guess string'

      定义表单类

    from flask.ext.wtf import Form
    from wtforms import StringField, SubmitField
    from wtforms.validators import Required
    class NameForm(Form):
        name = StringField('What is your name?', validators=[Required()])
        submit = SubmitField('Submit')

      这点flask-wtf要比django的form强大,类似于model form的功能。

      flash一般写在逻辑函数中,比如登陆信息改变时作为提醒在页面输出。在前端页面flask开放函数get_flashed_messages给模板使用,用于取出flash信息。

      例,两次登陆的人姓名改变了,使用flash进行提示。

    @app.route('/',methods=['GET','POST'])
    def index():
        form = NameForm()
        if form.validate_on_submit():
            old_name=session.get('name')
            if old_name is not None and old_name != form.name.data:
                flash('name has been changed')
                return redirect(url_for('index'))
            session['name']=form.name.data
            return render_template('index2.html',form=form)
        return render_template('index2.html',form=form)
    views.py
    <form method="POST">  
        {{form.hidden_tag()}}  
        <p>{{form.name.label}}</p>
        {{form.name()}}  
        <br>{{form.submit }}  
    </form>  
    <h6>flashed message</h6>
     <p>
        {% for message in get_flashed_messages() %}
            {{ message }}
        {% endfor %} 
     </p>
    index2.html

     

      使用 Flask-Bootstrap方式渲染

    {% import "bootstrap/wtf.html" as wtf %}
    {{ wtf.quick_form(form) }}

      wtf.quick_form() 函数的参数为 Flask-WTF 表单对象,使用 Bootstrap 的默认样式渲染传入的表单。

      用户提交表单后, 服务器收到一个包含数据的 POST 请求。 validate_on_submit() 会调用name 字段上附属的 Required() 验证函数。如果名字不为空,就能通过验证, validate_on_submit() 返回 True。现在,用户输入的名字可通过字段的 data 属性获取。
      Flask-SQLAlchemy 是一个 Flask 扩展,简化了在 Flask 程序中使用 SQLAlchemy 的操作。SQLAlchemy 是一个很强大的关系型数据库框架, 支持多种数据库后台。 SQLAlchemy 提供了高层 ORM,也提供了使用数据库原生 SQL 的低层功能。

    pip install flask-sqlalchemy

       配置数据库

    from flask.ext.sqlalchemy import SQLAlchemy
    basedir = os.path.abspath(os.path.dirname(__file__))
    app = Flask(__name__)
    app.config['SQLALCHEMY_DATABASE_URI'] =
    'sqlite:///' + os.path.join(basedir, 'data.sqlite')
    app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True
    db = SQLAlchemy(app)

       db 对象是 SQLAlchemy 类的实例,表示程序使用的数据库,同时还获得了 Flask-SQLAlchemy提供的所有功能。

       定义数据库模型

    class Role(db.Model):
        __tablename__ = 'roles'
        id = db.Column(db.Integer, primary_key=True)
        name = db.Column(db.String(64), unique=True)
        def __repr__(self):
            return '<Role %r>' % self.name

       创建数据库

    (venv) $ python hello.py shell
    >>> from hello import db
    >>> db.create_all()

      删除数据库

    db.drop_all()

      增加行数据

    >>> from hello import Role, User
    >>> admin_role = Role(name='Admin')
    >>> mod_role = Role(name='Moderator')
    >>> user_role = Role(name='User')
    >>> user_john = User(username='john', role=admin_role)
    >>> user_susan = User(username='susan', role=user_role)
    >>> user_david = User(username='david', role=user_role)

      此时,我们的对象还只是自己意淫出来的,并没有往数据库中提交,我们需要通过数据库会话管理对数据库做改动 。

      第一步,将要提交对象加入会话中:

    >>> db.session.add(admin_role)
    >>> db.session.add(mod_role)
    >>> db.session.add(user_role)
    >>> db.session.add(user_john)
    >>> db.session.add(user_susan)
    >>> db.session.add(user_david)

      或者:

    db.session.add_all([admin_role, mod_role, user_role,user_john, user_susan, user_david])

      为了把对象写入数据库,我们要调用 commit() 方法提交会话:

    db.session.commit()

      此时,我们的对象已经真正的存在数据库中,可以查询到了。

       数据库会话能保证数据库的一致性。提交操作使用原子方式把会话中的对象全部写入数据库。如果在写入会话的过程中发生了错误, 整个会话都会失效。

      修改数据行

    >>> admin_role.name = 'Administrator'
    >>> db.session.add(admin_role)
    >>> db.session.commit()

      删除数据库

    >>> db.session.delete(mod_role)
    >>> db.session.commit()

      查询行数据

      Flask-SQLAlchemy 为每个模型类都提供了 query 对象。最基本的模型查询是取回对应表中的所有记录:

    >>> Role.query.all()
    [<Role u'Administrator'>, <Role u'User'>]
    >>> User.query.all()
    [<User u'john'>, <User u'susan'>, <User u'david'>]

      过滤查询

    >>> User.query.filter_by(role=user_role).all()
    [<User u'susan'>, <User u'david'>]

      如果你想看他在底层到底执行的sql语句是什么。

    >>> str(User.query.filter_by(role=user_role))
    'SELECT users.id AS users_id, users.username AS users_username,
    users.role_id AS users_role_id FROM users WHERE :param_1 = users.role_id'

      为了避免一直重复导入,我们可以做些配置,让 Flask-Script shell 命令自动导入特定的对象。

    from flask.ext.script import Shell
    def make_shell_context():
        return dict(app=app, db=db, User=User, Role=Role)
    manager.add_command("shell", Shell(make_context=make_shell_context))

       使用Flask-Migrate实现数据库迁移

      使用Flsak-Migrate数据库迁移框架,可以保证数据库结构在发生变化时,改变数据库结构不至于丢失数据库的数据。

      首先第一步,创建数据仓库

      配置 Flask-Migrate 

    from flask.ext.migrate import Migrate, MigrateCommand
    migrate = Migrate(app, db)#创建一个Migrate对象并关联对应的应用程序类对象app和数据库管理类对象db
    manager.add_command('db', MigrateCommand)

      在维护数据库迁移之前,要使用 init 子命令创建迁移仓库:

    python hello.py db init

      'db'是在manager.add_command('db',MigrateComand)这句中我们声明的命令行对象名称,init是Migrate命令,表示初始化迁移仓库,运行完成之后,会在当前目录下创建一个migrations的文件夹,用于进行迁移的数据库脚本都放在这里。

      使用migarate子命令来创建数据库迁移脚本,在此之前我们先改动一下数据库的模型来验证迁移是否成功,我们在User模型中添加age属性。

      添加好字段后,使用下面命令自动添加迁移脚本

    python flask_blog.py db migrate

      upgrade() 函数把迁移中的改动应用到数据库中, downgrade() 函数则将改动删除。 

    python flask_blog.py db upgrade

       在视图函数中处理数据库管理。

    @app.route('/',methods=['GET','POST'])
    def index():
        form = NameForm()
        if form.validate_on_submit():
            user = User.query.filter_by(name=form.name.data).first()
            if user is None:
                user=User(name=form.name.data)
                db.session.add(user)#提交数据
            session['name']=form.name.data
            form.name.data=''
            return redirect(url_for('index'))
        return render_template('index.html',form=form,name=session['name'])
    View
    <form method="POST">  
         Hello {% if name %}{{name}} {% else %} stranger {% endif%}
        {{form.hidden_tag()}}  
        <p>{{form.name.label}}</p>
        {{form.name()}}  
        <br>{{form.submit }}  
    </form>  
    html

       这里数据提交与shell中不同,我们设置到配置信息里了。

    app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN']=True 

      之后我们的数据都会自动被提交了。

  • 相关阅读:
    Visual Studio 2010使用Visual Assist X的方法
    SQL Server 2000 评估版 升级到 SQL Server 2000 零售版
    双网卡多网络单主机同时访问
    开发即过程!立此纪念一个IT新名词的诞生
    delphi dxBarManager1 目录遍历 转为RzCheckTree2树
    5320 软件集合
    delphi tree 从一个表复制到另一个表
    DELPHI 排课系统课表
    长沙金思维 出现在GOOGLE的 金思维 相关搜索里啦!!
    如何在DBGrid的每一行前加一个单选框?
  • 原文地址:https://www.cnblogs.com/Jeffding/p/8313263.html
Copyright © 2011-2022 走看看