一、项目说明:
本次通过实现一个小的功能模块对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;
2、数据库操作类(dataBaseOpr.py):
先贴源码,折叠起来:
1 #!/usr/bin/env python 2 # encoding: utf-8 3 """ 4 :author: xiaoxiaobai 5 6 :contact: [email protected] 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()
源码注释基本很清晰了,对关键点进行说明:数据库连接的数据全部用默认参数的形式给出了,可根据实际情况进行移植。关于调用存储过程,只需要使用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: [email protected] 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("", 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
可以看的出,窗体类继承自tkinter.TK()可以直接通过self.x对主窗体添加控件和修改属性。然后在初始化函数中需要声明需要的成员变量,完成整体布局以及控件的事件绑定,以及数据初始化,最后self.mainloop()使窗体完成自动刷新。我们所有的逻辑处理都是在事件绑定方法中完成的,这样感觉就像是针对用户的每一个操作做出对应的逻辑处理和反应,同时需要考虑可能出现的异常以及所有的可能性,达到用户友好的设计要求。
运行此实例,可以使用test,py中的测试方法,也可以把guiPy.py和dataBaseOpr.py两个类放在同一个文件夹,在本机安装好上述两个库和完成数据库创建的情况下,直接在py解释器下导入guiPy.py文件下所有的包,MainWindow()即可。