本次經過實現一個小的功能模塊對Python GUI進行實踐學習。項目來源於軟件製造工程的做業。記錄在這裏以複習下思路和總結編碼過程。全部的源代碼和文件放在這裏:python
連接: https://pan.baidu.com/s/1qXGVRB2 密碼: 4a4rsql
內置四個文件,分別是ora.sql, dataBaseOpr.py, guiPy.py, test.py數據庫
主界面c#
新增界面(更新界面一致)設計模式
功能很簡單,就是作一張表的增刪改查,藉此簡單的熟悉下python,前幾天纔看了看相關的語法。oracle
數據庫採用oracle12c,使用命令行進行操做。Python版本爲3.6.2,命令行+Pycharm社區版2017.1.4。Python庫使用了 ide
cx_Oracle: 鏈接oracle數據庫函數
tkinter: 簡單入門的GUI庫oop
cx_Oracle庫的安裝我直接使用IDE自帶的包管理進行下載安裝的,tkinter是Python3.2之後自帶的標準庫,後面會講。佈局
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;
先貼源碼,摺疊起來:
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()
源碼註釋基本很清晰了,對關鍵點進行說明:數據庫鏈接的數據所有用默認參數的形式給出了,可根據實際狀況進行移植。關於調用存儲過程,只須要使用connect(**).cursor.callproc(存儲過程名, 參數列表)便可,方便高效。
由於界面和邏輯我都寫在guiPy.py中的,沒有使用特別的設計模式。因此這一部分主要講tkinter的用法,下一部分說明具體的實現。
關於安裝:Python3.2後自帶本庫,若引用沒有,極可能是安裝的時候沒有選。解決方案嘛找到安裝文件修改安裝便可,以下圖:
下一步打上勾便可,完成安裝就能引用tkinter了。
使用教程簡單介紹:
我此次用的時候就是在網上隨便搜了一下教程,發現內容都很淺顯,並且不繫統,固然我也無法系統的講清楚,但官方文檔能夠啊,提醒本身,之後必定先看官方文檔!
http://effbot.org/tkinterbook/tkinter-index.htm
先上代碼,基本註釋都有:
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
能夠看的出,窗體類繼承自tkinter.TK()能夠直接經過self.x對主窗體添加控件和修改屬性。而後在初始化函數中須要聲明須要的成員變量,完成總體佈局以及控件的事件綁定,以及數據初始化,最後self.mainloop()使窗體完成自動刷新。咱們全部的邏輯處理都是在事件綁定方法中完成的,這樣感受就像是針對用戶的每個操做作出對應的邏輯處理和反應,同時須要考慮可能出現的異常以及全部的可能性,達到用戶友好的設計要求。
運行此實例,可使用test,py中的測試方法,也能夠把guiPy.py和dataBaseOpr.py兩個類放在同一個文件夾,在本機安裝好上述兩個庫和完成數據庫建立的狀況下,直接在py解釋器下導入guiPy.py文件下全部的包,MainWindow()便可。