一、项目说明:
本次通过实现一个小的功能模块对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()即可。