python GUI实战项目——tkinter库的简单实例

一、项目说明:

  本次通过实现一个小的功能模块对Python GUI进行实践学习。项目来源于软件制造工程的作业。记录在这里以复习下思路和总结编码过程。所有的源代码和文件放在这里:

    链接: https://pan.baidu.com/s/1qXGVRB2 密码: 4a4r

    内置四个文件,分别是ora.sql, dataBaseOpr.py, guiPy.py, test.py

二、效果预览:

    python GUI实战项目——tkinter库的简单实例

                      主界面

    python GUI实战项目——tkinter库的简单实例

                      新增界面(更新界面一致)

    功能很简单,就是做一张表的增删改查,借此简单的熟悉下python,前几天才看了看相关的语法。

三、环境说明:

  数据库采用oracle12c,使用命令行进行操作。Python版本为3.6.2,命令行+Pycharm社区版2017.1.4。Python库使用了

    cx_Oracle: 连接oracle数据库

         tkinter: 简单入门的GUI库

  cx_Oracle库的安装我直接使用IDE自带的包管理进行下载安装的,tkinter是Python3.2以后自带的标准库,后面会讲。

四、编码过程实现:

 1、数据库表实现(ora.sql):

    python GUI实战项目——tkinter库的简单实例

    conn username/pass 根据本机的用户名和密码修改,后面的数据库连接统一都用我自己密码,不再赘述。

    python GUI实战项目——tkinter库的简单实例

    python GUI实战项目——tkinter库的简单实例

    为了简化Python代码和实践sql能力,写了两个简单的存储过程,分别是插入和更新,成功创建后只需调用存储过程和传递参数列表即可。代码详情在ora.sql中。

    代码折叠:

    

 conn c##bai/bai123
--建表
create or replace table groupinfo (
no varchar(12) not null,
name varchar(20),
headername varchar(20),
tel varchar(15),
constraint pk_groupinfo primary key(no)); --创建过程,直接传入参数即可插入
create or replace procedure insert_groupinfo
(no groupinfo.no%type,
name groupinfo.name%type,
headername groupinfo.headername%type,
tel groupinfo.tel%type
)
is
begin
insert into groupinfo values(no,name,headername,tel);
commit;
end; --创建过程,直接传入参数即可完成更新,第一个字段为原纪录no。必须有。
create or replace procedure update_groupinfo
(oldno groupinfo.no%type,
no groupinfo.no%type,
name groupinfo.name%type,
headername groupinfo.headername%type,
tel groupinfo.tel%type
)
is
n_no groupinfo.no%type;
n_name groupinfo.name%type;
n_headername groupinfo.headername%type;
n_tel groupinfo.tel%type;
grow groupinfo%rowtype;
ex_oldnoisnull exception;
begin
select * into grow from groupinfo g where g.no=oldno;
if oldno is null or grow.no is null then
raise ex_oldnoisnull;
end if;
if no is null then
n_no:= oldno;
else
n_no:= no;
end if;
if name is null then
n_name:= grow.name;
else
n_name:= name;
end if;
if headername is null then
n_headername:= grow.headername;
else
n_headername:= headername;
end if;
if tel is null then
n_tel:= grow.tel;
else
n_tel:= tel;
end if;
--dbms_output.put_line(n_no||n_name||n_headername||n_tel);
update groupinfo g set g.no = n_no, g.name = n_name, g.headername = n_headername, g.tel = n_tel where g.no = oldno;
commit;
exception
when ex_oldnoisnull then
dbms_output.out_line('选择的行不存在')
end;

ora.sql

 2、数据库操作类(dataBaseOpr.py):

    先贴源码,折叠起来:

 #!/usr/bin/env python
# encoding: utf-8
"""
:author: xiaoxiaobai :contact: 865816863@qq.com :file: dataBaseOpr.py :time: 2017/10/3 12:04 :@Software: PyCharm Community Edition :desc: 连接oracle数据库,并封装了增删改查全部操作。 """
import cx_Oracle class OracleOpr: def __init__(self, username='c##bai', passname='bai123', ip='localhost', datebasename='orcl', ipport=''):
"""
:param username: 连接数据库的用户名
:param passname: 连接数据库的密码
:param ip: 数据库ip
:param datebasename:数据库名
:param ipport: 数据库端口
:desc: 初始化函数用于完成数据库连接,可以通过self.connStatus判断是否连接成功,成功则参数为0,不成功则返回错误详情
"""
try:
self.connStatus = '未连接' # 连接状态
self.queryStatus = 0 # 查询状态
self.updateStatus = 0 # 更新状态
self.deleteStatus = 0 # 删除状态
self.insertStatus = 0 # 插入状态
self.__conn = ''
self.__conStr = username+'/'+passname+'@'+ip+':'+ipport+'/'+datebasename
self.__conn = cx_Oracle.connect(self.__conStr)
self.connStatus = 0
except cx_Oracle.Error as e:
self.connStatus = e def closeconnection(self):
try:
if self.__conn:
self.__conn.close()
self.connStatus = '连接已断开'
except cx_Oracle.Error as e:
self.connStatus = e def query(self, table='groupinfo', queryby=''):
"""
:param table: 查询表名
:param queryby: 查询条件,支持完整where, order by, group by 字句
:return:返回数据集,列名
"""
self.queryStatus = 0
result = ''
cursor = ''
title = ''
try:
sql = 'select * from '+table+' '+queryby
print(sql)
cursor = self.__conn.cursor()
cursor.execute(sql)
result = cursor.fetchall()
title = [i[0] for i in cursor.description]
cursor.close()
cursor = ''
except cx_Oracle.Error as e:
self.queryStatus = e
finally:
if cursor:
cursor.close()
return result, title def insert(self, proc='insert_groupinfo', insertlist=[]):
"""
:param proc: 过程名
:param insertlist: 参数集合,主键不能为空,参数必须与列对应,数量一致
:desc: 此方法通过调用过程完成插入,需要在sql上完成存储过程,可以通过insertstatus的值判断是否成功
"""
self.insertStatus = 0
cursor = ''
try:
cursor = self.__conn.cursor()
cursor.callproc(proc, insertlist)
cursor.close()
cursor = ''
except cx_Oracle.Error as e:
self.insertStatus = e
finally:
if cursor:
cursor.close() def update(self, proc='update_groupinfo', updatelist=[]):
"""
:param proc: 存储过程名
:param updatelist: 更新的集合,第一个为查询主键,后面的参数为对应的列,可以更新主键。
:desc: 此方法通过调用存储过程完成更新操作,可以通过updatestatus的值判断是否成功
"""
self.updateStatus = 0
cursor = ''
try:
cursor = self.__conn.cursor()
cursor.callproc(proc, updatelist)
cursor.close()
cursor = ''
except cx_Oracle.Error as e:
self.updateStatus = e
finally:
if cursor:
cursor.close() def delete(self, deleteby: '删除条件,where关键词后面的内容,即列名=列值(可多个组合)', table='groupinfo'):
"""
:param deleteby: 删除的条件,除where关键字以外的内容
:param table: 要删除的表名
:desc:可以通过deletestatus判断是否成功删除
"""
self.deleteStatus = 0
cursor = ''
try:
sql = 'delete ' + table + ' where ' + deleteby
cursor = self.__conn.cursor()
cursor.execute(sql)
cursor.close()
cursor = ''
except cx_Oracle.Error as e:
self.deleteStatus = e
finally:
if cursor:
cursor.close()

dataBaseOpr.py

    源码注释基本很清晰了,对关键点进行说明:数据库连接的数据全部用默认参数的形式给出了,可根据实际情况进行移植。关于调用存储过程,只需要使用connect(**).cursor.callproc(存储过程名, 参数列表)即可,方便高效。

 3、GUI界面搭建(tkinter):

    因为界面和逻辑我都写在guiPy.py中的,没有使用特别的设计模式。所以这一部分主要讲tkinter的用法,下一部分说明具体的实现。

    关于安装:Python3.2后自带本库,若引用没有,很可能是安装的时候没有选。解决方案嘛找到安装文件修改安装python GUI实战项目——tkinter库的简单实例即可,如下图:

    python GUI实战项目——tkinter库的简单实例

    python GUI实战项目——tkinter库的简单实例

    下一步打上勾即可,完成安装就能引用tkinter了。

  使用教程简单介绍:

  我这次用的时候就是在网上随便搜了一下教程,发现内容都很浅显,而且不系统,当然我也没法系统的讲清楚,但官方文档可以啊,提醒自己,以后一定先看官方文档!

  http://effbot.org/tkinterbook/tkinter-index.htm

 4、逻辑实现(guiPy.py):

    先上代码,基本注释都有:

 #!/usr/bin/env python
# encoding: utf-8
"""
:author: xiaoxiaobai :contact: 865816863@qq.com :file: guiPy.py :time: 2017/10/3 19:42 :@Software: PyCharm Community Edition :desc: 该文件完成了主要窗体设计,和数据获取,呈现等操作。调用时,运行主类MainWindow即可 """
import tkinter as tk
from tkinter import ttk
from dataBaseOpr import *
import tkinter.messagebox class MainWindow(tk.Tk):
def __init__(self):
super().__init__() # 变量定义
self.opr = OracleOpr()
self.list = self.init_data()
self.item_selection = ''
self.data = [] # 定义区域,把全局分为上中下三部分
self.frame_top = tk.Frame(width=600, height=90)
self.frame_center = tk.Frame(width=600, height=180)
self.frame_bottom = tk.Frame(width=600, height=90) # 定义上部分区域
self.lb_tip = tk.Label(self.frame_top, text="评议小组名称")
self.string = tk.StringVar()
self.string.set('')
self.ent_find_name = tk.Entry(self.frame_top, textvariable=self.string)
self.btn_query = tk.Button(self.frame_top, text="查询", command=self.query)
self.lb_tip.grid(row=0, column=0, padx=15, pady=30)
self.ent_find_name.grid(row=0, column=1, padx=45, pady=30)
self.btn_query.grid(row=0, column=2, padx=45, pady=30) # 定义下部分区域
self.btn_delete = tk.Button(self.frame_bottom, text="删除", command=self.delete)
self.btn_update = tk.Button(self.frame_bottom, text="修改", command=self.update)
self.btn_add = tk.Button(self.frame_bottom, text="添加", command=self.add)
self.btn_delete.grid(row=0, column=0, padx=20, pady=30)
self.btn_update.grid(row=0, column=1, padx=120, pady=30)
self.btn_add.grid(row=0, column=2, padx=30, pady=30) # 定义中心列表区域
self.tree = ttk.Treeview(self.frame_center, show="headings", height=8, columns=("a", "b", "c", "d"))
self.vbar = ttk.Scrollbar(self.frame_center, orient=tk.VERTICAL, command=self.tree.yview)
# 定义树形结构与滚动条
self.tree.configure(yscrollcommand=self.vbar.set)
# 表格的标题
self.tree.column("a", width=80, anchor="center")
self.tree.column("b", width=120, anchor="center")
self.tree.column("c", width=120, anchor="center")
self.tree.column("d", width=120, anchor="center")
self.tree.heading("a", text="小组编号")
self.tree.heading("b", text="小组名称")
self.tree.heading("c", text="负责人")
self.tree.heading("d", text="联系方式")
# 调用方法获取表格内容插入及树基本属性设置
self.tree["selectmode"] = "browse"
self.get_tree()
self.tree.grid(row=0, column=0, sticky=tk.NSEW, ipadx=10)
self.vbar.grid(row=0, column=1, sticky=tk.NS) # 定义整体区域
self.frame_top.grid(row=0, column=0, padx=60)
self.frame_center.grid(row=1, column=0, padx=60, ipady=1)
self.frame_bottom.grid(row=2, column=0, padx=60)
self.frame_top.grid_propagate(0)
self.frame_center.grid_propagate(0)
self.frame_bottom.grid_propagate(0) # 窗体设置
self.center_window(600, 360)
self.title('评议小组管理')
self.resizable(False, False)
self.mainloop() # 窗体居中
def center_window(self, width, height):
screenwidth = self.winfo_screenwidth()
screenheight = self.winfo_screenheight()
# 宽高及宽高的初始点坐标
size = '%dx%d+%d+%d' % (width, height, (screenwidth - width) / 2, (screenheight - height) / 2)
self.geometry(size) # 数据初始化获取
def init_data(self):
result, _ = self.opr.query()
if self.opr.queryStatus:
return 0
else:
return result # 表格内容插入
def get_tree(self):
if self.list == 0:
tkinter.messagebox.showinfo("错误提示", "数据获取失败")
else:
# 删除原节点
for _ in map(self.tree.delete, self.tree.get_children("")):
pass
# 更新插入新节点
for i in range(len(self.list)):
group = self.list[i]
self.tree.insert("", "end", values=(group[0],
group[1],
group[2],
group[3]), text=group[0])
# TODO 此处需解决因主程序自动刷新引起的列表项选中后重置的情况,我采用的折中方法是:把选中时的数据保存下来,作为记录 # 绑定列表项单击事件
self.tree.bind("<ButtonRelease-1>", self.tree_item_click)
self.tree.after(500, self.get_tree) # 单击查询按钮触发的事件方法
def query(self):
query_info = self.ent_find_name.get()
self.string.set('')
# print(query_info)
if query_info is None or query_info == '':
tkinter.messagebox.showinfo("警告", "查询条件不能为空!")
self.get_tree()
else:
result, _ = self.opr.query(queryby="where name like '%" + query_info + "%'")
self.get_tree()
if self.opr.queryStatus:
tkinter.messagebox.showinfo("警告", "查询出错,请检查数据库服务是否正常")
elif not result:
tkinter.messagebox.showinfo("查询结果", "该查询条件没有匹配项!")
else:
self.list = result
# TODO 此处需要解决弹框后代码列表刷新无法执行的问题 # 单击删除按钮触发的事件方法
def delete(self):
if self.item_selection is None or self.item_selection == '':
tkinter.messagebox.showinfo("删除警告", "未选中待删除值")
else:
# TODO: 删除提示
self.opr.delete(deleteby="no = '"+self.item_selection+"'")
if self.opr.deleteStatus:
tkinter.messagebox.showinfo("删除警告", "删除异常,可能是数据库服务意外关闭了。。。")
else:
self.list = self.init_data()
self.get_tree() # 为解决窗体自动刷新的问题,记录下单击项的内容
def tree_item_click(self, event):
try:
selection = self.tree.selection()[0]
self.data = self.tree.item(selection, "values")
self.item_selection = self.data[0]
except IndexError:
tkinter.messagebox.showinfo("单击警告", "单击结果范围异常,请重新选择!") # 单击更新按钮触发的事件方法
def update(self):
if self.item_selection is None or self.item_selection == '':
tkinter.messagebox.showinfo("更新警告", "未选中待更新项")
else:
data = [self.item_selection]
self.data = self.set_info(2)
if self.data is None or not self.data:
return
# 更改参数
data = data + self.data
self.opr.update(updatelist=data)
if self.opr.insertStatus:
tkinter.messagebox.showinfo("更新小组信息警告", "数据异常库连接异常,可能是服务关闭啦~")
# 更新界面,刷新数据
self.list = self.init_data()
self.get_tree() # 单击新增按钮触发的事件方法
def add(self):
# 接收弹窗的数据
self.data = self.set_info(1)
if self.data is None or not self.data:
return
# 更改参数
self.opr.insert(insertlist=self.data)
if self.opr.insertStatus:
tkinter.messagebox.showinfo("新增小组信息警告", "数据异常库连接异常,可能是服务关闭啦~")
# 更新界面,刷新数据
self.list = self.init_data()
self.get_tree() # 此方法调用弹窗传递参数,并返回弹窗的结果
def set_info(self, dia_type):
"""
:param dia_type:表示打开的是新增窗口还是更新窗口,新增则参数为1,其余参数为更新
:return: 返回用户填写的数据内容,出现异常则为None
"""
dialog = MyDialog(data=self.data, dia_type=dia_type)
# self.withdraw()
self.wait_window(dialog) # 这一句很重要!!!
return dialog.group_info # 新增窗口或者更新窗口
class MyDialog(tk.Toplevel):
def __init__(self, data, dia_type):
super().__init__() # 窗口初始化设置,设置大小,置顶等
self.center_window(600, 360)
self.wm_attributes("-topmost", 1)
self.resizable(False, False)
self.protocol("WM_DELETE_WINDOW", self.donothing) # 此语句用于捕获关闭窗口事件,用一个空方法禁止其窗口关闭。 # 根据参数类别进行初始化
if dia_type == 1:
self.title('新增小组信息')
else:
self.title('更新小组信息') # 数据变量定义
self.no = tk.StringVar()
self.name = tk.StringVar()
self.pname = tk.StringVar()
self.pnum = tk.StringVar()
if not data or dia_type == 1:
self.no.set('')
self.name.set('')
self.pname.set('')
self.pnum.set('')
else:
self.no.set(data[0])
self.name.set(data[1])
self.pname.set(data[2])
self.pnum.set(data[3]) # 错误提示定义
self.text_error_no = tk.StringVar()
self.text_error_name = tk.StringVar()
self.text_error_pname = tk.StringVar()
self.text_error_pnum = tk.StringVar()
self.error_null = '该项内容不能为空!'
self.error_exsit = '该小组编号已存在!' self.group_info = []
# 弹窗界面布局
self.setup_ui() # 窗体布局设置
def setup_ui(self):
# 第一行(两列)
row1 = tk.Frame(self)
row1.grid(row=0, column=0, padx=160, pady=20)
tk.Label(row1, text='小组编号:', width=8).pack(side=tk.LEFT)
tk.Entry(row1, textvariable=self.no, width=20).pack(side=tk.LEFT)
tk.Label(row1, textvariable=self.text_error_no, width=20, fg='red').pack(side=tk.LEFT)
# 第二行
row2 = tk.Frame(self)
row2.grid(row=1, column=0, padx=160, pady=20)
tk.Label(row2, text='小组名称:', width=8).pack(side=tk.LEFT)
tk.Entry(row2, textvariable=self.name, width=20).pack(side=tk.LEFT)
tk.Label(row2, textvariable=self.text_error_name, width=20, fg='red').pack(side=tk.LEFT)
# 第三行
row3 = tk.Frame(self)
row3.grid(row=2, column=0, padx=160, pady=20)
tk.Label(row3, text='负责人姓名:', width=10).pack(side=tk.LEFT)
tk.Entry(row3, textvariable=self.pname, width=18).pack(side=tk.LEFT)
tk.Label(row3, textvariable=self.text_error_pname, width=20, fg='red').pack(side=tk.LEFT)
# 第四行
row4 = tk.Frame(self)
row4.grid(row=3, column=0, padx=160, pady=20)
tk.Label(row4, text='手机号码:', width=8).pack(side=tk.LEFT)
tk.Entry(row4, textvariable=self.pnum, width=20).pack(side=tk.LEFT)
tk.Label(row4, textvariable=self.text_error_pnum, width=20, fg='red').pack(side=tk.LEFT)
# 第五行
row5 = tk.Frame(self)
row5.grid(row=4, column=0, padx=160, pady=20)
tk.Button(row5, text="取消", command=self.cancel).grid(row=0, column=0, padx=60)
tk.Button(row5, text="确定", command=self.ok).grid(row=0, column=1, padx=60) def center_window(self, width, height):
screenwidth = self.winfo_screenwidth()
screenheight = self.winfo_screenheight()
size = '%dx%d+%d+%d' % (width, height, (screenwidth - width) / 2, (screenheight - height) / 2)
self.geometry(size) # 点击确认按钮绑定事件方法
def ok(self): self.group_info = [self.no.get(), self.name.get(), self.pname.get(), self.pnum.get()] # 设置数据
if self.check_info() == 1: # 进行数据校验,失败则不关闭窗口
return
self.destroy() # 销毁窗口 # 点击取消按钮绑定事件方法
def cancel(self):
self.group_info = None # 空!
self.destroy() # 数据校验和用户友好性提示,校验失败返回1,成功返回0
def check_info(self):
is_null = 0
str_tmp = self.group_info
if str_tmp[0] == '':
self.text_error_no.set(self.error_null)
is_null = 1
if str_tmp[1] == '':
self.text_error_name.set(self.error_null)
is_null = 1
if str_tmp[2] == '':
self.text_error_pname.set(self.error_null)
is_null = 1
if str_tmp[3] == '':
self.text_error_pnum.set(self.error_null)
is_null = 1 if is_null == 1:
return 1
res, _ = OracleOpr().query(queryby="where no = '"+str_tmp[0]+"'")
print(res)
if res:
self.text_error_no.set(self.error_exsit)
return 1
return 0 # 空函数
def donothing(self):
pass

guiPy.py

  可以看的出,窗体类继承自tkinter.TK()可以直接通过self.x对主窗体添加控件和修改属性。然后在初始化函数中需要声明需要的成员变量,完成整体布局以及控件的事件绑定,以及数据初始化,最后self.mainloop()使窗体完成自动刷新。我们所有的逻辑处理都是在事件绑定方法中完成的,这样感觉就像是针对用户的每一个操作做出对应的逻辑处理和反应,同时需要考虑可能出现的异常以及所有的可能性,达到用户友好的设计要求。

  运行此实例,可以使用test,py中的测试方法,也可以把guiPy.py和dataBaseOpr.py两个类放在同一个文件夹,在本机安装好上述两个库和完成数据库创建的情况下,直接在py解释器下导入guiPy.py文件下所有的包,MainWindow()即可。

上一篇:IC卡存储器介绍


下一篇:python常用的内置函数哈哈