使用 PyQt5 (Qt Designer) 快速構建 GUI,配合 mysqlclient 實現動態 SQL 構建、參數化查詢、結果集表格渲染。本文記錄從 .ui 設計到 .py 轉換、信號槽綁定、動態 WHERE 條件生成、QTableWidget 數據填充的完整流程。
pip install PyQt5)pip install mysqlclient)pyqt5-tools)QGroupBox "查詢條件" → 內部網格佈局放入:QCheckBox (啟用欄位) + QLineEdit (輸入值) × 7 (id, name, age_range, sex, class, dept, addr)QLabel "至" (年齡範圍中間)QPushButton "查詢" (objectName: btn_query)QPushButton "退出" (objectName: btn_exit)QTextBrowser "生成 SQL" (objectName: txt_sql, 唯讀)QTableWidget "結果集" (objectName: tbl_result, 7 列,不可編輯)QCheckBox 文本為欄位中文名,QLineEdit placeholderText 提示輸入格式query_tool.ui# 命令行
pyuic5 -x query_tool.ui -o ui_query_tool.py
# -x 生成可直接運行的主程序入口 (含 if __name__ == '__main__')
import sys
import MySQLdb
from PyQt5 import QtWidgets, QtCore
from ui_query_tool import Ui_MainWindow
class QueryTool(QtWidgets.QMainWindow, Ui_MainWindow):
def __init__(self):
super().__init__()
self.setupUi(self)
# 數據庫連接 (生產環境用連接池)
self.conn = MySQLdb.connect(
host='localhost', user='root', passwd='', db='mysql',
charset='utf8mb4', cursorclass=MySQLdb.cursors.DictCursor
)
self.cur = self.conn.cursor()
# 信號槽綁定
self.btn_query.clicked.connect(self.on_query)
self.btn_exit.clicked.connect(self.on_exit)
# 初始化表格列標題
self.tbl_result.setColumnCount(7)
self.tbl_result.setHorizontalHeaderLabels(['Sid', 'Sname', 'Sage', 'Ssex', 'Sclass', 'Sdept', 'Saddr'])
self.tbl_result.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
self.tbl_result.horizontalHeader().setStretchLastSection(True)
# 基礎 SQL
self.base_sql = "SELECT Sid, Sname, Sage, Ssex, Sclass, Sdept, Saddr FROM students WHERE "
self.txt_sql.setPlainText(self.base_sql)
def build_condition(self, checkbox, lineedit, column, is_range=False, is_likeable=True):
"""構建單條 WHERE 子句"""
if not checkbox.isChecked():
return None, False
val = lineedit.text().strip()
if not val:
return None, False
used_like = is_likeable and ('%' in val or '_' in val)
if is_range:
# 年齡範圍:兩個 LineEdit (min, max)
min_val = self.first_Sage.text().strip()
max_val = self.last_Sage.text().strip()
parts = []
if min_val:
parts.append(f"{column} >= {min_val}")
if max_val:
parts.append(f"{column} <= {max_val}")
if not parts:
return None, False
return ' AND '.join(parts), True
else:
op = "LIKE" if used_like else "="
sql_val = f"'{val}'" if op == "LIKE" else (val if val.isdigit() else f"'{val}'")
return f"{column} {op} {sql_val}", True
def on_query(self):
conditions = []
first = True
# Sid
cond, ok = self.build_condition(self.check_Sid, self.Sid, 'Sid')
if ok: conditions.append(cond)
# Sname
cond, ok = self.build_condition(self.check_Sname, self.Sname, 'Sname')
if ok: conditions.append(cond)
# Sage (範圍)
if self.check_Sage.isChecked():
min_v = self.first_Sage.text().strip()
max_v = self.last_Sage.text().strip()
parts = []
if min_v: parts.append(f"Sage >= {min_v}")
if max_v: parts.append(f"Sage <= {max_v}")
if parts: conditions.append(' AND '.join(parts))
# Ssex
cond, ok = self.build_condition(self.check_Ssex, self.Ssex, 'Ssex', is_likeable=False)
if ok: conditions.append(cond)
# Sclass
cond, ok = self.build_condition(self.check_Sclass, self.Sclass, 'Sclass')
if ok: conditions.append(cond)
# Sdept
cond, ok = self.build_condition(self.check_Sdept, self.Sdept, 'Sdept')
if ok: conditions.append(cond)
# Saddr
cond, ok = self.build_condition(self.check_Saddr, self.Saddr, 'Saddr')
if ok: conditions.append(cond)
if not conditions:
QtWidgets.QMessageBox.warning(self, '提示', '請至少勾選一個查詢條件')
return
# 組裝完整 SQL
full_sql = self.base_sql + ' AND '.join(conditions)
self.txt_sql.setPlainText(full_sql)
# 執行查詢
try:
self.cur.execute(full_sql)
rows = self.cur.fetchall()
# 清空舊數據
self.tbl_result.setRowCount(0)
# 填充新數據
for row_idx, row in enumerate(rows):
self.tbl_result.insertRow(row_idx)
for col_idx, col_name in enumerate(['Sid', 'Sname', 'Sage', 'Ssex', 'Sclass', 'Sdept', 'Saddr']):
val = row[col_name]
# 數字類型轉字符串
display = '' if val is None else str(val)
item = QtWidgets.QTableWidgetItem(display)
self.tbl_result.setItem(row_idx, col_idx, item)
# 自適應列寬
self.tbl_result.resizeColumnsToContents()
except Exception as e:
QtWidgets.QMessageBox.critical(self, '查詢錯誤', f'SQL: {full_sql}\n錯誤: {e}')
def on_exit(self):
self.cur.close()
self.conn.close()
self.close()
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key_Escape:
self.on_exit()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
window = QueryTool()
window.show()
sys.exit(app.exec_())
| 技術點 | 實現細節 |
|---|---|
| 動態 SQL 構建 | 按勾選欄位收集條件,AND 連接,LIKE 檢測 %/_,範圍查詢拆分上下界 |
| 參數化查詢缺失 | 教學簡化直接拼接,生產必須用參數綁定: 收集 params 列表,構建佔位符, cur.execute(sql, params) |
| QTableWidget 渲染 | setRowCount(0) 清空 → insertRow() 逐行 → setItem() 逐單元格,只接受字符串,數字需 str() |
| 信號槽機制 | btn.clicked.connect(slot),keyPressEvent 捕獲 Esc 退出 |
| 異常處理 | try/except 捕獲 DB 錯誤,QMessageBox.critical 顯示 SQL 與錯誤詳情 |
| 資源釋放 | 退出時關閉游標、連接,conn.commit() 確保寫操作持久化 (本例唯讀) |
WHERE col = %s 佔位符與 params 列表,徹底防注入DBUtils.PooledDB 管理連接,避免頻繁建連QThread 或 QRunnable 執行耗時查詢,避免 UI 卡死,pyqtSignal 回傳結果LIMIT offset, size,滾動加載pandas.DataFrame.to_excel 或 openpyxl)config.ini/環境變量,非硬編碼CREATE DATABASE IF NOT EXISTS school DEFAULT CHARSET utf8mb4;
USE school;
CREATE TABLE IF NOT EXISTS `students` (
`Sid` CHAR(10) NOT NULL PRIMARY KEY COMMENT '學號',
`Sname` VARCHAR(20) DEFAULT NULL COMMENT '姓名',
`Sage` TINYINT UNSIGNED DEFAULT NULL COMMENT '年齡',
`Ssex` ENUM('男','女') DEFAULT NULL COMMENT '性別',
`Sclass` VARCHAR(30) DEFAULT NULL COMMENT '班級',
`Sdept` VARCHAR(50) DEFAULT NULL COMMENT '系別',
`Saddr` VARCHAR(100) DEFAULT NULL COMMENT '地址'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO `students` VALUES
('20230001', '張三', 20, '男', 'CS-1', '計算機科學', '北京市海淀區'),
('20230002', '李四', 19, '女', 'CS-1', '計算機科學', '上海市浦東區'),
('20230003', '王五', 21, '男', 'EE-2', '電子工程', '廣州市天河區'),
('20230004', '趙六', 20, '女', 'EE-2', '電子工程', '深圳市南山區');
PyQt5 + MySQL 可視化查詢工具開發核心流程:**Qt Designer 設計 .ui → pyuic5 轉 .py → 繼承 Ui 類綁定信號槽 → 動態構建 SQL (教學版拼接/生產版參數化) → 執行查詢 → QTableWidget 渲染結果 → 異常捕獲與資源釋放**。掌握此模式,可快速構建各類數據庫管理、報表展示、ETL 監控等內部工具,極大提升運維開發效率。