首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 數據庫知識 >  >> MySQL 資料庫 >> MySQL 綜合教學

PyQt5 + MySQL 實戰:可視化數據庫查詢工具開發全記錄 (界面設計、動態 SQL、結果渲染)

     編輯:MySQL 綜合教學

PyQt5 + MySQL 實戰:可視化數據庫查詢工具開發全記錄 (界面設計、動態 SQL、結果渲染)

使用 PyQt5 (Qt Designer) 快速構建 GUI,配合 mysqlclient 實現動態 SQL 構建、參數化查詢、結果集表格渲染。本文記錄從 .ui 設計到 .py 轉換、信號槽綁定、動態 WHERE 條件生成、QTableWidget 數據填充的完整流程。

一、技術棧與環境

  • Python 3.8+
  • PyQt5 (pip install PyQt5)
  • mysqlclient (pip install mysqlclient)
  • Qt Designer (隨 PyQt5 安裝或單獨安裝 pyqt5-tools)

二、Qt Designer 界面設計

  1. 新建 Main Window
  2. 左側 Widget Box 拖入控件:
    • 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 列,不可編輯)
  3. 設置 QCheckBox 文本為欄位中文名,QLineEdit placeholderText 提示輸入格式
  4. 保存為 query_tool.ui

三、.ui 轉 .py

# 命令行
pyuic5 -x query_tool.ui -o ui_query_tool.py
# -x 生成可直接運行的主程序入口 (含 if __name__ == '__main__')

四、主邏輯代碼 (query_tool.py)

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() 確保寫操作持久化 (本例唯讀)

六、進化方向:從教學版到生產級

  1. 參數化查詢重構:動態構建 WHERE col = %s 佔位符與 params 列表,徹底防注入
  2. 連接池:DBUtils.PooledDB 管理連接,避免頻繁建連
  3. 異步查詢:QThread 或 QRunnable 執行耗時查詢,避免 UI 卡死,pyqtSignal 回傳結果
  4. 分頁加載:大結果集分批 LIMIT offset, size,滾動加載
  5. 導出功能:CSV/Excel 導出 (pandas.DataFrame.to_excel 或 openpyxl)
  6. 配置管理:DB 連接信息外置 config.ini/環境變量,非硬編碼
  7. 多表支持:下拉選表、自動獲取列元數據動態生成條件區

七、表結構初始化腳本 (MySQL)

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 監控等內部工具,極大提升運維開發效率。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved