PyQt实现SQLite数据库操作案例详解

✅作者简介:2022年博客新星 第八。热爱国学的Java后端开发者,修心和技术同步精进。

🍎个人主页:Java Fans的博客

🍊个人信条:不迁怒,不贰过。小知识,大智慧。

💞当前专栏:Python案例分享专栏

✨特色专栏:国学周更-心性养成之路

🥭本文内容:Qt Designer 和 PyQt 开发教程

文章目录

一、环境准备

  • Python 3.x(建议3.6及以上)
  • PyQt5(或PyQt6,本文以PyQt5为例)
  • SQLite(Python内置,无需额外安装)

安装PyQt5命令:

bash 复制代码
pip install PyQt5

二、核心模块介绍

  • sqlite3:Python内置模块,适合简单数据库操作,适合脚本或小项目。
  • PyQt5.QtSql:Qt框架提供的数据库模块,支持多种数据库驱动,能与Qt的MVC模型无缝结合,适合GUI应用。
  • QSqlDatabase:数据库连接管理类。
  • QSqlTableModel:基于数据库表的模型,支持数据的显示、编辑、插入、删除。
  • QTableView:视图控件,用于显示模型数据,支持排序、选择等功能。

三、案例需求

设计一个联系人管理程序,功能包括:

  • 显示联系人列表(姓名、电话)
  • 添加联系人
  • 删除联系人
  • 修改联系人信息(直接在表格中编辑)

四、详细代码实现及解析

1. 创建数据库和表

python 复制代码
import sqlite3

def create_database():
    # 连接数据库,文件不存在则自动创建
    conn = sqlite3.connect('contacts.db')
    cursor = conn.cursor()
    # 创建联系人表,包含id(主键自增)、name和phone字段
    cursor.execute('''
        CREATE TABLE IF NOT EXISTS contacts (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            name TEXT NOT NULL,
            phone TEXT NOT NULL
        )
    ''')
    conn.commit()
    conn.close()

if __name__ == '__main__':
    create_database()

解析:

  • 使用sqlite3.connect()连接数据库,文件不存在时自动创建。
  • CREATE TABLE IF NOT EXISTS保证表只创建一次,避免重复创建错误。
  • id字段设置为主键且自增,方便唯一标识每条记录。
  • namephone字段均设置为非空,保证数据完整性。

2. PyQt主窗口设计

python 复制代码
import sys
from PyQt5.QtWidgets import (
    QApplication, QWidget, QVBoxLayout, QPushButton, QHBoxLayout,
    QLineEdit, QMessageBox, QTableView, QLabel
)
from PyQt5.QtSql import QSqlDatabase, QSqlTableModel

class ContactManager(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("联系人管理")
        self.resize(500, 400)
        self.init_ui()
        self.create_connection()
        self.init_model()
        self.init_view()

    def init_ui(self):
        layout = QVBoxLayout()

        # 输入区域,水平布局
        form_layout = QHBoxLayout()
        self.name_edit = QLineEdit()
        self.name_edit.setPlaceholderText("姓名")
        self.phone_edit = QLineEdit()
        self.phone_edit.setPlaceholderText("电话")
        self.add_btn = QPushButton("添加联系人")
        self.add_btn.clicked.connect(self.add_contact)

        # 添加控件到form_layout
        form_layout.addWidget(QLabel("姓名:"))
        form_layout.addWidget(self.name_edit)
        form_layout.addWidget(QLabel("电话:"))
        form_layout.addWidget(self.phone_edit)
        form_layout.addWidget(self.add_btn)

        # 表格视图显示联系人列表
        self.table_view = QTableView()

        # 删除按钮
        self.delete_btn = QPushButton("删除选中联系人")
        self.delete_btn.clicked.connect(self.delete_contact)

        # 将所有控件添加到主布局
        layout.addLayout(form_layout)
        layout.addWidget(self.table_view)
        layout.addWidget(self.delete_btn)

        self.setLayout(layout)

    def create_connection(self):
        # 创建数据库连接,使用QSQLITE驱动
        self.db = QSqlDatabase.addDatabase('QSQLITE')
        self.db.setDatabaseName('contacts.db')
        if not self.db.open():
            QMessageBox.critical(self, "数据库错误", self.db.lastError().text())
            sys.exit(1)

    def init_model(self):
        # 初始化数据模型,绑定contacts表
        self.model = QSqlTableModel(self, self.db)
        self.model.setTable('contacts')
        # 设置编辑策略为OnFieldChange,编辑后自动提交
        self.model.setEditStrategy(QSqlTableModel.OnFieldChange)
        self.model.select()  # 加载数据
        # 设置表头显示文字
        self.model.setHeaderData(1, 0, "姓名")
        self.model.setHeaderData(2, 0, "电话")

    def init_view(self):
        # 绑定模型到视图
        self.table_view.setModel(self.model)
        # 隐藏id列,用户无需看到主键
        self.table_view.hideColumn(0)
        # 自动调整列宽
        self.table_view.resizeColumnsToContents()
        # 设置选择行为为整行选择
        self.table_view.setSelectionBehavior(QTableView.SelectRows)
        # 设置只能单选
        self.table_view.setSelectionMode(QTableView.SingleSelection)

    def add_contact(self):
        # 获取输入框内容
        name = self.name_edit.text().strip()
        phone = self.phone_edit.text().strip()
        if not name or not phone:
            QMessageBox.warning(self, "输入错误", "姓名和电话不能为空")
            return
        # 创建空记录
        record = self.model.record()
        record.setValue("name", name)
        record.setValue("phone", phone)
        # 插入记录到模型
        if not self.model.insertRecord(-1, record):
            QMessageBox.critical(self, "添加失败", self.model.lastError().text())
        else:
            self.model.select()  # 刷新视图
            self.name_edit.clear()
            self.phone_edit.clear()

    def delete_contact(self):
        # 获取当前选中行索引
        index = self.table_view.currentIndex()
        if not index.isValid():
            QMessageBox.warning(self, "删除错误", "请选择要删除的联系人")
            return
        # 删除选中行
        self.model.removeRow(index.row())
        # 提交删除操作
        if not self.model.submitAll():
            QMessageBox.critical(self, "删除失败", self.model.lastError().text())
        else:
            self.model.select()  # 刷新视图

if __name__ == '__main__':
    create_database()  # 确保数据库和表存在
    app = QApplication(sys.argv)
    window = ContactManager()
    window.show()
    sys.exit(app.exec_())

五、代码详细解析

1. 界面设计

  • 使用QVBoxLayout作为主布局,垂直排列输入区域、表格视图和删除按钮。
  • 输入区域用QHBoxLayout水平排列姓名输入框、电话输入框和添加按钮。
  • QLineEdit设置setPlaceholderText提示用户输入内容。
  • QTableView用于显示联系人列表,绑定QSqlTableModel模型。
  • 删除按钮绑定删除功能。

2. 数据库连接

  • 使用QSqlDatabase.addDatabase('QSQLITE')创建SQLite数据库连接。
  • 设置数据库文件名为contacts.db
  • 调用open()打开数据库,失败则弹出错误并退出程序。

3. 数据模型

  • QSqlTableModel绑定contacts表。
  • 设置编辑策略为OnFieldChange,即用户在表格中编辑数据后,自动提交到数据库,无需手动调用submitAll()
  • 调用select()加载数据。
  • 使用setHeaderData设置表头显示的列名,参数12分别对应namephone字段的列索引(第0列是id)。

4. 视图设置

  • 将模型绑定到QTableView
  • 隐藏id列,避免显示无意义的主键。
  • 调用resizeColumnsToContents()自动调整列宽,保证内容完整显示。
  • 设置选择行为为整行选择,方便删除操作。
  • 设置单选,避免多选删除复杂度。

5. 添加联系人

  • 从输入框获取姓名和电话,去除空白字符。
  • 校验输入不能为空,避免插入无效数据。
  • 通过model.record()创建空记录,设置字段值。
  • 调用insertRecord(-1, record)在末尾插入新记录。
  • 插入失败弹出错误提示。
  • 插入成功后刷新模型,清空输入框。

6. 删除联系人

  • 获取当前选中行的索引。
  • 校验是否选中有效行。
  • 调用removeRow(row)删除对应行。
  • 调用submitAll()提交删除操作,失败则弹出错误。
  • 成功后刷新模型。

六、功能扩展建议

好的,下面我将基于之前的联系人管理程序,逐项补充功能扩展的示例代码和详细说明,帮助你快速实现搜索、数据验证、批量删除、CSV导入导出和分页显示功能。

1. 搜索功能 ------ 利用QSqlTableModel.setFilter()实现模糊查询

思路
  • 在界面顶部添加一个搜索输入框。
  • 用户输入关键词后,调用setFilter()设置SQL过滤条件,实现按姓名或电话模糊查询。
  • 过滤条件示例:name LIKE '%keyword%' OR phone LIKE '%keyword%'
代码示例
python 复制代码
from PyQt5.QtWidgets import QLineEdit

class ContactManager(QWidget):
    def init_ui(self):
        layout = QVBoxLayout()

        # 搜索框
        self.search_edit = QLineEdit()
        self.search_edit.setPlaceholderText("搜索姓名或电话")
        self.search_edit.textChanged.connect(self.search_contacts)
        layout.addWidget(self.search_edit)

        # 之前的输入区域和按钮
        # ...(保持不变)

    def search_contacts(self, text):
        # 构造过滤条件,防止SQL注入,简单替换单引号
        keyword = text.replace("'", "''")
        filter_str = f"name LIKE '%{keyword}%' OR phone LIKE '%{keyword}%'"
        self.model.setFilter(filter_str)
        self.model.select()

说明:

  • textChanged信号实时响应用户输入。
  • setFilter()设置SQL WHERE子句,自动刷新显示符合条件的记录。

2. 数据验证 ------ 电话格式正则校验

思路
  • 使用Python内置re模块定义电话格式正则表达式。
  • 在添加联系人和编辑提交前校验电话格式,格式不正确则提示用户。
代码示例
python 复制代码
import re

class ContactManager(QWidget):
    def __init__(self):
        # ...
        self.phone_regex = re.compile(r'^\+?\d{7,15}$')  # 简单示例:允许+开头,7-15位数字

    def add_contact(self):
        name = self.name_edit.text().strip()
        phone = self.phone_edit.text().strip()
        if not name or not phone:
            QMessageBox.warning(self, "输入错误", "姓名和电话不能为空")
            return
        if not self.phone_regex.match(phone):
            QMessageBox.warning(self, "输入错误", "电话格式不正确,应为7-15位数字,可带+号")
            return
        # 其余代码保持不变

    def init_model(self):
        # 绑定编辑提交信号,校验电话格式
        self.model = QSqlTableModel(self, self.db)
        self.model.setTable('contacts')
        self.model.setEditStrategy(QSqlTableModel.OnFieldChange)
        self.model.select()
        self.model.setHeaderData(1, 0, "姓名")
        self.model.setHeaderData(2, 0, "电话")

        # 监听数据更改,校验电话格式
        self.model.dataChanged.connect(self.validate_phone_on_edit)

    def validate_phone_on_edit(self, topLeft, bottomRight, roles):
        # 获取编辑的行和列
        row = topLeft.row()
        col = topLeft.column()
        if col == 2:  # 电话列索引为2
            phone_index = self.model.index(row, col)
            phone = self.model.data(phone_index)
            if not self.phone_regex.match(phone):
                QMessageBox.warning(self, "输入错误", "电话格式不正确,应为7-15位数字,可带+号")
                # 恢复旧值,简单做法是重新select刷新
                self.model.select()

说明:

  • 电话格式示例正则:允许以+开头,后面7-15位数字。
  • 添加联系人时校验输入。
  • 编辑表格时监听dataChanged信号,校验电话列数据。

3. 批量删除 ------ 支持多选删除

思路
  • QTableView的选择模式改为多选。
  • 删除按钮点击时,获取所有选中行,逐行删除。
  • 提交删除操作后刷新视图。
代码示例
python 复制代码
class ContactManager(QWidget):
    def init_view(self):
        self.table_view.setModel(self.model)
        self.table_view.hideColumn(0)
        self.table_view.resizeColumnsToContents()
        self.table_view.setSelectionBehavior(QTableView.SelectRows)
        self.table_view.setSelectionMode(QTableView.MultiSelection)  # 多选模式

    def delete_contact(self):
        selected_indexes = self.table_view.selectionModel().selectedRows()
        if not selected_indexes:
            QMessageBox.warning(self, "删除错误", "请选择要删除的联系人")
            return
        # 逆序删除,避免索引错乱
        rows = sorted([index.row() for index in selected_indexes], reverse=True)
        for row in rows:
            self.model.removeRow(row)
        if not self.model.submitAll():
            QMessageBox.critical(self, "删除失败", self.model.lastError().text())
        else:
            self.model.select()

说明:

  • selectedRows()返回所有选中行的QModelIndex列表。
  • 逆序删除避免删除时索引变化导致错误。
  • 删除后调用submitAll()提交数据库操作。

4. 导入导出 ------ 支持CSV格式

思路
  • 导入:读取CSV文件,逐行插入数据库。
  • 导出:将当前模型数据写入CSV文件。
  • 使用Python内置csv模块。
  • 使用QFileDialog选择文件路径。
代码示例
python 复制代码
import csv
from PyQt5.QtWidgets import QFileDialog

class ContactManager(QWidget):
    def init_ui(self):
        # ... 之前代码
        # 添加导入导出按钮
        self.import_btn = QPushButton("导入CSV")
        self.import_btn.clicked.connect(self.import_csv)
        self.export_btn = QPushButton("导出CSV")
        self.export_btn.clicked.connect(self.export_csv)

        # 将按钮添加到布局,比如放在删除按钮旁边
        btn_layout = QHBoxLayout()
        btn_layout.addWidget(self.delete_btn)
        btn_layout.addWidget(self.import_btn)
        btn_layout.addWidget(self.export_btn)
        layout.addLayout(btn_layout)

    def import_csv(self):
        path, _ = QFileDialog.getOpenFileName(self, "选择CSV文件", "", "CSV文件 (*.csv)")
        if not path:
            return
        with open(path, newline='', encoding='utf-8') as f:
            reader = csv.DictReader(f)
            for row in reader:
                name = row.get('name') or row.get('姓名')
                phone = row.get('phone') or row.get('电话')
                if not name or not phone:
                    continue
                record = self.model.record()
                record.setValue("name", name)
                record.setValue("phone", phone)
                self.model.insertRecord(-1, record)
            if not self.model.submitAll():
                QMessageBox.critical(self, "导入失败", self.model.lastError().text())
            else:
                self.model.select()
                QMessageBox.information(self, "导入成功", "CSV导入完成")

    def export_csv(self):
        path, _ = QFileDialog.getSaveFileName(self, "保存CSV文件", "", "CSV文件 (*.csv)")
        if not path:
            return
        with open(path, 'w', newline='', encoding='utf-8') as f:
            writer = csv.writer(f)
            # 写表头
            writer.writerow(['姓名', '电话'])
            for row in range(self.model.rowCount()):
                name = self.model.data(self.model.index(row, 1))
                phone = self.model.data(self.model.index(row, 2))
                writer.writerow([name, phone])
        QMessageBox.information(self, "导出成功", "CSV导出完成")

说明:

  • 导入时使用csv.DictReader方便按列名读取。
  • 导出时写入表头和所有数据。
  • 通过文件对话框选择文件路径。
  • 导入后调用submitAll()提交批量插入。

5. 分页显示 ------ 适合大数据量时提升性能

思路
  • SQLite不支持直接分页的模型接口,需要手动控制SQL查询。
  • 利用QSqlQueryModel自定义分页查询。
  • 设计"上一页""下一页"按钮控制页码。
  • 每页显示固定条数,比如20条。
代码示例
python 复制代码
from PyQt5.QtSql import QSqlQueryModel, QSqlQuery
from PyQt5.QtWidgets import QPushButton, QLabel

class ContactManager(QWidget):
    def __init__(self):
        super().__init__()
        self.page_size = 20
        self.current_page = 0
        self.total_records = 0
        self.init_ui()
        self.create_connection()
        self.init_paging_model()
        self.init_view()

    def init_ui(self):
        layout = QVBoxLayout()

        # 分页控制按钮和标签
        page_layout = QHBoxLayout()
        self.prev_btn = QPushButton("上一页")
        self.next_btn = QPushButton("下一页")
        self.page_label = QLabel()
        self.prev_btn.clicked.connect(self.prev_page)
        self.next_btn.clicked.connect(self.next_page)
        page_layout.addWidget(self.prev_btn)
        page_layout.addWidget(self.page_label)
        page_layout.addWidget(self.next_btn)

        # 其余UI保持不变,添加分页布局
        # 例如在表格下方添加分页控件
        # ...

        layout.addLayout(page_layout)
        # 其他控件添加到layout
        self.setLayout(layout)

    def init_paging_model(self):
        self.model = QSqlQueryModel(self)
        self.update_total_records()
        self.load_page()

    def update_total_records(self):
        query = QSqlQuery(self.db)
        query.exec("SELECT COUNT(*) FROM contacts")
        if query.next():
            self.total_records = query.value(0)

    def load_page(self):
        offset = self.current_page * self.page_size
        sql = f"SELECT id, name, phone FROM contacts LIMIT {self.page_size} OFFSET {offset}"
        self.model.setQuery(sql, self.db)
        self.page_label.setText(f"第 {self.current_page + 1} 页 / 共 {((self.total_records - 1) // self.page_size) + 1} 页")
        # 绑定模型到视图
        self.table_view.setModel(self.model)
        self.table_view.hideColumn(0)
        self.table_view.resizeColumnsToContents()

    def next_page(self):
        if (self.current_page + 1) * self.page_size >= self.total_records:
            return  # 已经是最后一页
        self.current_page += 1
        self.load_page()

    def prev_page(self):
        if self.current_page == 0:
            return  # 已经是第一页
        self.current_page -= 1
        self.load_page()

说明:

  • 使用QSqlQueryModel执行自定义SQL查询,支持分页。
  • LIMITOFFSET实现分页查询。
  • 维护当前页码和总记录数,更新分页标签。
  • "上一页""下一页"按钮控制页码切换。
  • 由于QSqlQueryModel是只读模型,若需要编辑功能,分页实现会更复杂,需要自定义模型。

码文不易,本篇文章就介绍到这里,如果想要学习更多Java系列知识点击关注博主,博主带你零基础学习Java知识。与此同时,对于日常生活有困扰的朋友,欢迎阅读我的第四栏目《国学周更---心性养成之路》,学习技术的同时,我们也注重了心性的养成。

相关推荐
子夜江寒2 小时前
MySQL 学习
数据库·mysql
SAP小崔说事儿2 小时前
SAP B1 库龄分析报表(SQL版本&非批次管理)
数据库·sql·sap·sap b1·business one·批次管理·库龄分析
不穿格子的程序员2 小时前
Redis篇7——Redis深度剖析:主从数据同步原理与实践优化
数据库·redis·缓存·数据同步
廋到被风吹走2 小时前
【数据库】【Redis】监控与告警体系构建
数据库·redis·缓存
老华带你飞2 小时前
个人网盘管理|基于springboot + vue个人网盘管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端
代码or搬砖2 小时前
悲观锁讲解
开发语言·数据库
soft20015252 小时前
MySQL Buffer Pool深度解析:冷热数据分离下的LRU链表工作机制
数据库·mysql·链表
whn19772 小时前
磁盘空间不足导致oracle的system01.dbf损坏
数据库·oracle
此生只爱蛋2 小时前
【Redis】Hash 哈希
数据库·redis·哈希算法