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

一、项目说明:

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

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

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

二、效果预览:

    

                      主界面

 

    

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

    功能很简单,就是做一张表的增删改查,借此简单的熟悉下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):

    

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

    

 

    

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

    代码折叠:

    

 1 conn c##bai/bai123
 2 --建表
 3 create or replace table groupinfo (
 4 no varchar(12) not null,
 5 name varchar(20),
 6 headername varchar(20),
 7 tel varchar(15),
 8 constraint pk_groupinfo primary key(no));
 9 
10 --创建过程,直接传入参数即可插入
11 create or replace procedure insert_groupinfo
12 (no groupinfo.no%type,
13 name groupinfo.name%type,
14 headername groupinfo.headername%type,
15 tel groupinfo.tel%type
16 )
17 is
18 begin
19 insert into groupinfo values(no,name,headername,tel);
20 commit;
21 end;
22 
23 --创建过程,直接传入参数即可完成更新,第一个字段为原纪录no。必须有。
24 create or replace procedure update_groupinfo
25 (oldno groupinfo.no%type,
26 no groupinfo.no%type,
27 name groupinfo.name%type,
28 headername groupinfo.headername%type,
29 tel groupinfo.tel%type
30 )
31 is
32 n_no groupinfo.no%type;
33 n_name groupinfo.name%type;
34 n_headername groupinfo.headername%type;
35 n_tel groupinfo.tel%type;
36 grow groupinfo%rowtype;
37 ex_oldnoisnull exception;
38 begin
39 select * into grow from groupinfo g where g.no=oldno;
40 if oldno is null or grow.no is null then
41     raise ex_oldnoisnull;
42 end if;
43 if no is null then
44     n_no:= oldno;
45 else
46     n_no:= no;
47 end if;
48 if name is null then
49     n_name:= grow.name;
50 else
51     n_name:= name;
52 end if;
53 if headername is null then
54     n_headername:= grow.headername;
55 else
56     n_headername:= headername;
57 end if;
58 if tel is null then
59     n_tel:= grow.tel;
60 else
61     n_tel:= tel;
62 end if; 
63 --dbms_output.put_line(n_no||n_name||n_headername||n_tel);
64 update groupinfo g set g.no = n_no, g.name = n_name, g.headername = n_headername, g.tel = n_tel where g.no = oldno;
65 commit;
66 exception
67     when ex_oldnoisnull then
68 dbms_output.out_line('选择的行不存在')
69 end;
ora.sql

 

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

     先贴源码,折叠起来:

  1 #!/usr/bin/env python
  2 # encoding: utf-8
  3 """
  4 :author: xiaoxiaobai
  5 
  6 :contact: 865816863@qq.com
  7 
  8 :file: dataBaseOpr.py
  9 
 10 :time: 2017/10/3 12:04
 11 
 12 :@Software: PyCharm Community Edition
 13 
 14 :desc: 连接oracle数据库,并封装了增删改查全部操作。
 15 
 16 """
 17 import cx_Oracle
 18 
 19 
 20 class OracleOpr:
 21 
 22     def __init__(self, username='c##bai', passname='bai123', ip='localhost', datebasename='orcl', ipport='1521'):
 23         """
 24         :param username: 连接数据库的用户名
 25         :param passname: 连接数据库的密码
 26         :param ip: 数据库ip
 27         :param datebasename:数据库名
 28         :param ipport: 数据库端口
 29         :desc: 初始化函数用于完成数据库连接,可以通过self.connStatus判断是否连接成功,成功则参数为0,不成功则返回错误详情
 30         """
 31         try:
 32             self.connStatus = '未连接'    # 连接状态
 33             self.queryStatus = 0    # 查询状态
 34             self.updateStatus = 0   # 更新状态
 35             self.deleteStatus = 0   # 删除状态
 36             self.insertStatus = 0   # 插入状态
 37             self.__conn = ''
 38             self.__conStr = username+'/'+passname+'@'+ip+':'+ipport+'/'+datebasename
 39             self.__conn = cx_Oracle.connect(self.__conStr)
 40             self.connStatus = 0
 41         except cx_Oracle.Error as e:
 42             self.connStatus = e
 43 
 44     def closeconnection(self):
 45         try:
 46             if self.__conn:
 47                 self.__conn.close()
 48                 self.connStatus = '连接已断开'
 49         except cx_Oracle.Error as e:
 50             self.connStatus = e
 51 
 52     def query(self, table='groupinfo', queryby=''):
 53         """
 54         :param table: 查询表名
 55         :param queryby: 查询条件,支持完整where, order by, group by 字句
 56         :return:返回数据集,列名
 57         """
 58         self.queryStatus = 0
 59         result = ''
 60         cursor = ''
 61         title = ''
 62         try:
 63             sql = 'select * from '+table+' '+queryby
 64             print(sql)
 65             cursor = self.__conn.cursor()
 66             cursor.execute(sql)
 67             result = cursor.fetchall()
 68             title = [i[0] for i in cursor.description]
 69             cursor.close()
 70             cursor = ''
 71         except cx_Oracle.Error as e:
 72             self.queryStatus = e
 73         finally:
 74             if cursor:
 75                 cursor.close()
 76             return result, title
 77 
 78     def insert(self, proc='insert_groupinfo', insertlist=[]):
 79         """
 80         :param proc: 过程名
 81         :param insertlist: 参数集合,主键不能为空,参数必须与列对应,数量一致
 82         :desc: 此方法通过调用过程完成插入,需要在sql上完成存储过程,可以通过insertstatus的值判断是否成功
 83         """
 84         self.insertStatus = 0
 85         cursor = ''
 86         try:
 87             cursor = self.__conn.cursor()
 88             cursor.callproc(proc, insertlist)
 89             cursor.close()
 90             cursor = ''
 91         except cx_Oracle.Error as e:
 92             self.insertStatus = e
 93         finally:
 94             if cursor:
 95                 cursor.close()
 96 
 97     def update(self, proc='update_groupinfo', updatelist=[]):
 98         """
 99         :param proc: 存储过程名
100         :param updatelist: 更新的集合,第一个为查询主键,后面的参数为对应的列,可以更新主键。
101         :desc: 此方法通过调用存储过程完成更新操作,可以通过updatestatus的值判断是否成功
102         """
103         self.updateStatus = 0
104         cursor = ''
105         try:
106             cursor = self.__conn.cursor()
107             cursor.callproc(proc, updatelist)
108             cursor.close()
109             cursor = ''
110         except cx_Oracle.Error as e:
111             self.updateStatus = e
112         finally:
113             if cursor:
114                 cursor.close()
115 
116     def delete(self, deleteby: '删除条件,where关键词后面的内容,即列名=列值(可多个组合)', table='groupinfo'):
117         """
118         :param deleteby: 删除的条件,除where关键字以外的内容
119         :param table: 要删除的表名
120         :desc:可以通过deletestatus判断是否成功删除
121         """
122         self.deleteStatus = 0
123         cursor = ''
124         try:
125             sql = 'delete ' + table + ' where ' + deleteby
126             cursor = self.__conn.cursor()
127             cursor.execute(sql)
128             cursor.close()
129             cursor = ''
130         except cx_Oracle.Error as e:
131             self.deleteStatus = e
132         finally:
133             if cursor:
134                 cursor.close()
dataBaseOpr.py

 

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

 3、GUI界面搭建(tkinter):

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

    关于安装:Python3.2后自带本库,若引用没有,很可能是安装的时候没有选。解决方案嘛找到安装文件修改安装即可,如下图:

    

    

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

 

  使用教程简单介绍:

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

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

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

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

  1 #!/usr/bin/env python
  2 # encoding: utf-8
  3 """
  4 :author: xiaoxiaobai
  5 
  6 :contact: 865816863@qq.com
  7 
  8 :file: guiPy.py
  9 
 10 :time: 2017/10/3 19:42
 11 
 12 :@Software: PyCharm Community Edition
 13 
 14 :desc: 该文件完成了主要窗体设计,和数据获取,呈现等操作。调用时,运行主类MainWindow即可
 15 
 16 """
 17 import tkinter as tk
 18 from tkinter import ttk
 19 from dataBaseOpr import *
 20 import tkinter.messagebox
 21 
 22 
 23 class MainWindow(tk.Tk):
 24     def __init__(self):
 25         super().__init__()
 26 
 27         # 变量定义
 28         self.opr = OracleOpr()
 29         self.list = self.init_data()
 30         self.item_selection = ''
 31         self.data = []
 32 
 33         # 定义区域,把全局分为上中下三部分
 34         self.frame_top = tk.Frame(width=600, height=90)
 35         self.frame_center = tk.Frame(width=600, height=180)
 36         self.frame_bottom = tk.Frame(width=600, height=90)
 37 
 38         # 定义上部分区域
 39         self.lb_tip = tk.Label(self.frame_top, text="评议小组名称")
 40         self.string = tk.StringVar()
 41         self.string.set('')
 42         self.ent_find_name = tk.Entry(self.frame_top, textvariable=self.string)
 43         self.btn_query = tk.Button(self.frame_top, text="查询", command=self.query)
 44         self.lb_tip.grid(row=0, column=0, padx=15, pady=30)
 45         self.ent_find_name.grid(row=0, column=1, padx=45, pady=30)
 46         self.btn_query.grid(row=0, column=2, padx=45, pady=30)
 47 
 48         # 定义下部分区域
 49         self.btn_delete = tk.Button(self.frame_bottom, text="删除", command=self.delete)
 50         self.btn_update = tk.Button(self.frame_bottom, text="修改", command=self.update)
 51         self.btn_add = tk.Button(self.frame_bottom, text="添加", command=self.add)
 52         self.btn_delete.grid(row=0, column=0, padx=20, pady=30)
 53         self.btn_update.grid(row=0, column=1, padx=120, pady=30)
 54         self.btn_add.grid(row=0, column=2, padx=30, pady=30)
 55 
 56         # 定义中心列表区域
 57         self.tree = ttk.Treeview(self.frame_center, show="headings", height=8, columns=("a", "b", "c", "d"))
 58         self.vbar = ttk.Scrollbar(self.frame_center, orient=tk.VERTICAL, command=self.tree.yview)
 59         # 定义树形结构与滚动条
 60         self.tree.configure(yscrollcommand=self.vbar.set)
 61         # 表格的标题
 62         self.tree.column("a", width=80, anchor="center")
 63         self.tree.column("b", width=120, anchor="center")
 64         self.tree.column("c", width=120, anchor="center")
 65         self.tree.column("d", width=120, anchor="center")
 66         self.tree.heading("a", text="小组编号")
 67         self.tree.heading("b", text="小组名称")
 68         self.tree.heading("c", text="负责人")
 69         self.tree.heading("d", text="联系方式")
 70         # 调用方法获取表格内容插入及树基本属性设置
 71         self.tree["selectmode"] = "browse"
 72         self.get_tree()
 73         self.tree.grid(row=0, column=0, sticky=tk.NSEW, ipadx=10)
 74         self.vbar.grid(row=0, column=1, sticky=tk.NS)
 75 
 76         # 定义整体区域
 77         self.frame_top.grid(row=0, column=0, padx=60)
 78         self.frame_center.grid(row=1, column=0, padx=60, ipady=1)
 79         self.frame_bottom.grid(row=2, column=0, padx=60)
 80         self.frame_top.grid_propagate(0)
 81         self.frame_center.grid_propagate(0)
 82         self.frame_bottom.grid_propagate(0)
 83 
 84         # 窗体设置
 85         self.center_window(600, 360)
 86         self.title('评议小组管理')
 87         self.resizable(False, False)
 88         self.mainloop()
 89 
 90     # 窗体居中
 91     def center_window(self, width, height):
 92         screenwidth = self.winfo_screenwidth()
 93         screenheight = self.winfo_screenheight()
 94         # 宽高及宽高的初始点坐标
 95         size = '%dx%d+%d+%d' % (width, height, (screenwidth - width) / 2, (screenheight - height) / 2)
 96         self.geometry(size)
 97 
 98     # 数据初始化获取
 99     def init_data(self):
100         result, _ = self.opr.query()
101         if self.opr.queryStatus:
102             return 0
103         else:
104             return result
105 
106     # 表格内容插入
107     def get_tree(self):
108         if self.list == 0:
109             tkinter.messagebox.showinfo("错误提示", "数据获取失败")
110         else:
111             # 删除原节点
112             for _ in map(self.tree.delete, self.tree.get_children("")):
113                 pass
114             # 更新插入新节点
115             for i in range(len(self.list)):
116                 group = self.list[i]
117                 self.tree.insert("", "end", values=(group[0],
118                                                     group[1],
119                                                     group[2],
120                                                     group[3]), text=group[0])
121             # TODO 此处需解决因主程序自动刷新引起的列表项选中后重置的情况,我采用的折中方法是:把选中时的数据保存下来,作为记录
122 
123             # 绑定列表项单击事件
124             self.tree.bind("<ButtonRelease-1>", self.tree_item_click)
125             self.tree.after(500, self.get_tree)
126 
127     # 单击查询按钮触发的事件方法
128     def query(self):
129         query_info = self.ent_find_name.get()
130         self.string.set('')
131         # print(query_info)
132         if query_info is None or query_info == '':
133             tkinter.messagebox.showinfo("警告", "查询条件不能为空!")
134             self.get_tree()
135         else:
136             result, _ = self.opr.query(queryby="where name like '%" + query_info + "%'")
137             self.get_tree()
138             if self.opr.queryStatus:
139                 tkinter.messagebox.showinfo("警告", "查询出错,请检查数据库服务是否正常")
140             elif not result:
141                 tkinter.messagebox.showinfo("查询结果", "该查询条件没有匹配项!")
142             else:
143                 self.list = result
144                 # TODO 此处需要解决弹框后代码列表刷新无法执行的问题
145 
146     # 单击删除按钮触发的事件方法
147     def delete(self):
148         if self.item_selection is None or self.item_selection == '':
149             tkinter.messagebox.showinfo("删除警告", "未选中待删除值")
150         else:
151             # TODO: 删除提示
152             self.opr.delete(deleteby="no = '"+self.item_selection+"'")
153             if self.opr.deleteStatus:
154                 tkinter.messagebox.showinfo("删除警告", "删除异常,可能是数据库服务意外关闭了。。。")
155             else:
156                 self.list = self.init_data()
157                 self.get_tree()
158 
159     # 为解决窗体自动刷新的问题,记录下单击项的内容
160     def tree_item_click(self, event):
161         try:
162             selection = self.tree.selection()[0]
163             self.data = self.tree.item(selection, "values")
164             self.item_selection = self.data[0]
165         except IndexError:
166             tkinter.messagebox.showinfo("单击警告", "单击结果范围异常,请重新选择!")
167 
168     # 单击更新按钮触发的事件方法
169     def update(self):
170         if self.item_selection is None or self.item_selection == '':
171             tkinter.messagebox.showinfo("更新警告", "未选中待更新项")
172         else:
173             data = [self.item_selection]
174             self.data = self.set_info(2)
175             if self.data is None or not self.data:
176                 return
177             # 更改参数
178             data = data + self.data
179             self.opr.update(updatelist=data)
180             if self.opr.insertStatus:
181                 tkinter.messagebox.showinfo("更新小组信息警告", "数据异常库连接异常,可能是服务关闭啦~")
182             # 更新界面,刷新数据
183             self.list = self.init_data()
184             self.get_tree()
185 
186     # 单击新增按钮触发的事件方法
187     def add(self):
188         # 接收弹窗的数据
189         self.data = self.set_info(1)
190         if self.data is None or not self.data:
191             return
192         # 更改参数
193         self.opr.insert(insertlist=self.data)
194         if self.opr.insertStatus:
195             tkinter.messagebox.showinfo("新增小组信息警告", "数据异常库连接异常,可能是服务关闭啦~")
196         # 更新界面,刷新数据
197         self.list = self.init_data()
198         self.get_tree()
199 
200     # 此方法调用弹窗传递参数,并返回弹窗的结果
201     def set_info(self, dia_type):
202         """
203         :param dia_type:表示打开的是新增窗口还是更新窗口,新增则参数为1,其余参数为更新
204         :return: 返回用户填写的数据内容,出现异常则为None
205         """
206         dialog = MyDialog(data=self.data, dia_type=dia_type)
207         # self.withdraw()
208         self.wait_window(dialog)  # 这一句很重要!!!
209         return dialog.group_info
210 
211 
212 # 新增窗口或者更新窗口
213 class MyDialog(tk.Toplevel):
214     def __init__(self, data, dia_type):
215         super().__init__()
216 
217         # 窗口初始化设置,设置大小,置顶等
218         self.center_window(600, 360)
219         self.wm_attributes("-topmost", 1)
220         self.resizable(False, False)
221         self.protocol("WM_DELETE_WINDOW", self.donothing)   # 此语句用于捕获关闭窗口事件,用一个空方法禁止其窗口关闭。
222 
223         # 根据参数类别进行初始化
224         if dia_type == 1:
225             self.title('新增小组信息')
226         else:
227             self.title('更新小组信息')
228 
229         # 数据变量定义
230         self.no = tk.StringVar()
231         self.name = tk.StringVar()
232         self.pname = tk.StringVar()
233         self.pnum = tk.StringVar()
234         if not data or dia_type == 1:
235             self.no.set('')
236             self.name.set('')
237             self.pname.set('')
238             self.pnum.set('')
239         else:
240             self.no.set(data[0])
241             self.name.set(data[1])
242             self.pname.set(data[2])
243             self.pnum.set(data[3])
244 
245         # 错误提示定义
246         self.text_error_no = tk.StringVar()
247         self.text_error_name = tk.StringVar()
248         self.text_error_pname = tk.StringVar()
249         self.text_error_pnum = tk.StringVar()
250         self.error_null = '该项内容不能为空!'
251         self.error_exsit = '该小组编号已存在!'
252 
253         self.group_info = []
254         # 弹窗界面布局
255         self.setup_ui()
256 
257     # 窗体布局设置
258     def setup_ui(self):
259         # 第一行(两列)
260         row1 = tk.Frame(self)
261         row1.grid(row=0, column=0, padx=160, pady=20)
262         tk.Label(row1, text='小组编号:', width=8).pack(side=tk.LEFT)
263         tk.Entry(row1, textvariable=self.no, width=20).pack(side=tk.LEFT)
264         tk.Label(row1, textvariable=self.text_error_no, width=20, fg='red').pack(side=tk.LEFT)
265         # 第二行
266         row2 = tk.Frame(self)
267         row2.grid(row=1, column=0, padx=160, pady=20)
268         tk.Label(row2, text='小组名称:', width=8).pack(side=tk.LEFT)
269         tk.Entry(row2, textvariable=self.name, width=20).pack(side=tk.LEFT)
270         tk.Label(row2, textvariable=self.text_error_name, width=20, fg='red').pack(side=tk.LEFT)
271         # 第三行
272         row3 = tk.Frame(self)
273         row3.grid(row=2, column=0, padx=160, pady=20)
274         tk.Label(row3, text='负责人姓名:', width=10).pack(side=tk.LEFT)
275         tk.Entry(row3, textvariable=self.pname, width=18).pack(side=tk.LEFT)
276         tk.Label(row3, textvariable=self.text_error_pname, width=20, fg='red').pack(side=tk.LEFT)
277         # 第四行
278         row4 = tk.Frame(self)
279         row4.grid(row=3, column=0, padx=160, pady=20)
280         tk.Label(row4, text='手机号码:', width=8).pack(side=tk.LEFT)
281         tk.Entry(row4, textvariable=self.pnum, width=20).pack(side=tk.LEFT)
282         tk.Label(row4, textvariable=self.text_error_pnum, width=20, fg='red').pack(side=tk.LEFT)
283         # 第五行
284         row5 = tk.Frame(self)
285         row5.grid(row=4, column=0, padx=160, pady=20)
286         tk.Button(row5, text="取消", command=self.cancel).grid(row=0, column=0, padx=60)
287         tk.Button(row5, text="确定", command=self.ok).grid(row=0, column=1, padx=60)
288 
289     def center_window(self, width, height):
290         screenwidth = self.winfo_screenwidth()
291         screenheight = self.winfo_screenheight()
292         size = '%dx%d+%d+%d' % (width, height, (screenwidth - width) / 2, (screenheight - height) / 2)
293         self.geometry(size)
294 
295     # 点击确认按钮绑定事件方法
296     def ok(self):
297 
298         self.group_info = [self.no.get(), self.name.get(), self.pname.get(), self.pnum.get()]  # 设置数据
299         if self.check_info() == 1:  # 进行数据校验,失败则不关闭窗口
300             return
301         self.destroy()  # 销毁窗口
302 
303     # 点击取消按钮绑定事件方法
304     def cancel(self):
305         self.group_info = None  # 空!
306         self.destroy()
307 
308     # 数据校验和用户友好性提示,校验失败返回1,成功返回0
309     def check_info(self):
310         is_null = 0
311         str_tmp = self.group_info
312         if str_tmp[0] == '':
313             self.text_error_no.set(self.error_null)
314             is_null = 1
315         if str_tmp[1] == '':
316             self.text_error_name.set(self.error_null)
317             is_null = 1
318         if str_tmp[2] == '':
319             self.text_error_pname.set(self.error_null)
320             is_null = 1
321         if str_tmp[3] == '':
322             self.text_error_pnum.set(self.error_null)
323             is_null = 1
324 
325         if is_null == 1:
326             return 1
327         res, _ = OracleOpr().query(queryby="where no = '"+str_tmp[0]+"'")
328         print(res)
329         if res:
330             self.text_error_no.set(self.error_exsit)
331             return 1
332         return 0
333 
334     # 空函数
335     def donothing(self):
336         pass
guiPy.py

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

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

posted @ 2017-10-07 09:42  白常福  阅读(36828)  评论(6编辑  收藏  举报