当前位置:首页>python>豆豆的Python生活 18 万卷书库(三)讲究而不将就

豆豆的Python生活 18 万卷书库(三)讲究而不将就

  • 2026-07-02 16:44:18
豆豆的Python生活 18 万卷书库(三)讲究而不将就

一、缘起

北风呼啸,豆豆看着窗外随风摇摆的大树,若有所思。

“豆豆,想什么呢?”

“我在想,多亏有这么一层窗户啊,别看阳光这么好,其实外面挺冷的。”

“你这句话让我想起一本书,叫《建筑的永恒之道》” 马老师说,“在这本书中,作者克里斯托弗·亚历山大通过整体建筑设计的视角,强调了和谐与无名特质的重要性。书中提到,飘窗可以作为一个过渡空间,消除室内外空间的张力,从而创造和谐。飘窗的设计使得人们既能够享受室内的舒适,又能感受到室外的开阔,就像你现在既可以看窗外的风景,又不受外面寒风的困扰。”

“马老师,您还研究建筑学吗?” 豆豆不解地问。

“不不,你想错了,《建筑的永恒之道》虽然是一本建筑学的书,其实也是软件架构师的案头经典,我们设计软件也像建设设计一样,好的建筑要消除室内外空间的张力,让人们住起来舒适,好的软件也应该消除软件与用户之间的张力,让人用起来顺心呀!”

“软件与用户之间的张力?我不太理解啊!”

“软件与用户之间的张力是指用户在使用软件时感受到的不匹配、不适或冲突,一般是因为软件的设计、功能、性能或用户体验与用户的期望、习惯或需求之间存在差距造成的。简单地说,不能用,不好用,体验差!”

“好像有点明白了……那怎么才能消除软件与用户之间的张力呢?”

“这可不是一两句话能说清楚的。” 马老师沉吟了一下,“比如我们要做用户体验设计、可用性测试、性能优化……举个最简单的例子吧,上节课我们做的【万卷书库】程序,它也就是做到了能用,离好用还差得远呢——比如这个用户交互的方式就不友好,你明明只想修改个分类,还要把其它的字段都过一遍……”

“我明白了马老师,您的意思就是现在应该赶紧用PyQt实现新版本的【万卷书库】程序了呗。”

“哈哈,既然话都说到这了,我们开始吧!”

二、建立程序框架

你可能发现,我们要实现的程序代码量变得越来越多,逻辑也相对之前更复杂了。接下来我们要实现的Windows版本程序,可能还会更复杂一些。为了避免软件复杂性给后边的开发带来困扰,我们有必要先了解一些架构设计的知识。

什么是软件架构设计?

软件的架构设计就像是建造房子之前的蓝图。它定义了软件的骨架和结构,包括软件的各个部分如何组织、如何交互,以及它们如何满足用户的需求。简而言之,软件架构设计就是提前规划软件的构建方式,确保它既稳固又好用。

为什么需要软件架构设计?

因为我们要管理软件的复杂性。举个例子,如果你想给家里的小狗盖一座狗窝,其实不需要专门的建筑师,你自己动手找几块木板订起来就差不多了。如果你懂一些木工,这个狗窝会很漂亮;如果不懂,最多是外观或实用性上有点问题,但至少不会有什么严重的后果。但如果是盖一座大楼呢?没有专门的设计,可能没盖两层就倒塌了。如果复杂一点的软件没有架构设计,后期就会产生许多麻烦的问题,难以修改、维护,甚至连功能和性能都无法保证。

我们现在做的程序还称不上是大楼级别,但是懂一些软件架构设计,会让我们的程序结构清晰,易于理解和修改。

对于本程序来说,我们需要重新建立一个更合适的程序结构,将构建用户操作界面的代码、实现业务逻辑的代码、实际进行数据库操作的代码分开,形成所谓的“三层架构”:

  • 表示层,负责实现用户界面,与程序的使用者直接交互,展示数据、接收输入,比如常见的网页、终端、或移动APP界面,这一层的代码不负责业务逻辑实现,它主要是把用户的请求转发到下面的业务逻辑层,并展示业务逻辑层的返回结果
  • 业务逻辑层:包含应用程序的核心业务规则和逻辑,负责处理界面请求,执行必要的操作,并与下面的数据访问层交互获得或存储数据;
  • 数据访问层:负责与数据库或其它存储数据的系统交互,执行数据增删改查操作,为业务逻辑层提供服务。

使用这种程序架构的好处很多,比如: - 由于各层的职责清晰,所以我们知道在什么情况下去修改哪一层,降低开发和维护的难度; - 可以独立地修改或扩展某一层的功能,不影响其它层; - 可以重用某一层的代码。比如在一个程序中写好的数据访问层,可以在另一个具有相同数据结构的程序中使用。我们之所以要在上一节课把数据访问抽离成单独的类,正是这种考虑。

软件架构设计是一门复杂的学问,目前我们现在只要知道它的目标就是为了让开发的软件结构合理、易于修改维护就行。我们会参考三层架构的设计思想去考虑万卷书库的程序设计,但不会追求实现完整严格的三层架构,那样反而成了“过度设计”,毕竟这还只是一个简单的图书管理程序嘛。

现在新建立一个文件夹book_manager_win,使用VSCode打开,新建一个文件dal.py,这就是我们的“数据访问层”,负责和数据库的交互。

我们可以直接把上一个版本中books_new.pyBookDatabase类复制过来,注意import sqlite3也是需要的:

import sqlite3class BookDatabase:    def __init__(self, db_name):        self.conn = sqlite3.connect(db_name)        self.cursor = self.conn.cursor()        self.create_table()    def create_table(self):        self.cursor.execute(            """            CREATE TABLE IF NOT EXISTS books (                id INTEGER PRIMARY KEY AUTOINCREMENT,                ISBN TEXT NOT NULL,                title TEXT NOT NULL,                author TEXT,                category TEXT,                purchase_date DATE,                is_read BOOLEAN,                completion_date DATE,                notes TEXT            )            """        )        self.conn.commit()    def add_book(        self,        ISBN,        title,        author,        category,        purchase_date,        is_read,        completion_date,        notes,    ):        query = """            INSERT INTO books (ISBN, title, author, category, purchase_date, is_read, completion_date, notes)            VALUES (?, ?, ?, ?, ?, ?, ?, ?)        """        self.cursor.execute(            query,            (                ISBN,                title,                author,                category,                purchase_date,                is_read,                completion_date,                notes,            ),        )        self.conn.commit()        print("图书添加成功!")    def update_book(        self,        book_id,        new_ISBN,        new_title,        new_author,        new_category,        new_purchase_date,        new_is_read,        new_completion_date,        new_notes,    ):        query = """            UPDATE books            SET ISBN = ?, title = ?, author = ?, category = ?, purchase_date = ?, is_read = ?, completion_date = ?, notes = ?            WHERE id = ?        """        self.cursor.execute(            query,            (                new_ISBN,                new_title,                new_author,                new_category,                new_purchase_date,                new_is_read,                new_completion_date,                new_notes,                book_id,            ),        )        self.conn.commit()        print("图书信息更新成功!")    def delete_book(self, book_id):        query = "DELETE FROM books WHERE id = ?"        self.cursor.execute(query, (book_id,))        self.conn.commit()        print("图书删除成功!")    def list_books(self):        query = "SELECT * FROM books"        self.cursor.execute(query)        return self.cursor.fetchall()    def search_book_by_title(self, title):        query = "SELECT * FROM books WHERE title LIKE ?"        self.cursor.execute(query, ("%" + title + "%",))        return self.cursor.fetchall()    def search_book_by_id(self, book_id):        query = "SELECT * FROM books WHERE id = ?"        self.cursor.execute(query, (book_id,))        return self.cursor.fetchone()    def close(self):        self.conn.close()

在实际开发中,我们会把与数据访问有关的类都放在dal.py中。它已经实现了我们需要的大部分数据访问功能,不过为了更好地实现后面的开发,我们对它做一些小小的改造:

  • 增加初始化默认参数

修改__init__方法,让db_name这个参数带有默认值books.db。这样在调用类的时候,我们不必要从业务逻辑层再传递数据库名称过来,因为这不是业务逻辑层应该考虑的问题,它只要初始化数据类就能直接使用。

def __init__(self, db_name="books.db"):        self.conn = sqlite3.connect(db_name)        self.cursor = self.conn.cursor()        self.create_table()

更好的做法是用配置文件保存要连接的数据库名称,在初始化的时候加载配置文件。不过目前这种方式已经实现了数据访问层和业务逻辑层的职责独立,以后我们再学习配置文件的用法。

  • 完善数据库表结构

我们对表结构进行小调整,在purchase_date(采购日期)下增加location字段,用于保存图书的存放位置(比如你家有个大书架,你可以给不同的位置编号便于检索,这也是马老师的做法)。再把原来的is_read替换为status,用文本类型保存图书的已读或未读状态更便于查看。

def create_table(self):        self.cursor.execute(            """            CREATE TABLE IF NOT EXISTS books (                id INTEGER PRIMARY KEY AUTOINCREMENT,                ISBN TEXT NOT NULL,                title TEXT NOT NULL,                author TEXT,                category TEXT,                purchase_date DATE,                location TEXT,                status TEXT,                completion_date DATE,                notes TEXT            )            """        )        self.conn.commit()
  • 简化参数传递方式

仔细查看,add_book()update_book()这两个方法传递的参数都特别多,因为数据库中字段每个都要作为参数传递过来。我们可以简化一下,把后面的数据库中字段统一放到一个字典类型的参数中,这样传递的时候就不用写太长的参数列表,在函数内部我们直接按字典的关键字取数即可。这样做还有一个好处就是当数据库中的字段发生变化时,函数参数不用变,修改调用函数的代码和函数中有关字段的代码就可以。

修改后的add_book()update_book()方法如下,我们顺便修改了方法最后的print语句打印内容,这样打印的信息可以作为程序日志记录使用:

def add_book(self,book_data):        query = """            INSERT INTO books (ISBN, title, author, category, purchase_date, location, status, completion_date, notes)            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)        """        self.cursor.execute(            query,            (                book_data["ISBN"],                book_data["title"],                book_data["author"],                book_data["category"],                book_data["purchase_date"],                book_data["location"],                book_data["status"],                book_data["completion_date"],                book_data["notes"],            ),        )        self.conn.commit()        print(f"图书添加成功:{book_data}")    def update_book(self,book_id,updated_data):        query = """            UPDATE books            SET ISBN = ?, title = ?, author = ?, category = ?, purchase_date = ?, location = ?, status = ?, completion_date = ?, notes = ?            WHERE id = ?        """        self.cursor.execute(            query,            (                updated_data["ISBN"],                updated_data["title"],                updated_data["author"],                updated_data["category"],                updated_data["purchase_date"],                updated_data["location"],                updated_data["status"],                updated_data["completion_date"],                updated_data["notes"],                book_id,            ),        )        self.conn.commit()        print(f"编号为{book_id}的图书信息更新成功:{updated_data}")
  • delete()方法修改

delete()方法修改就是最后的打印语句:

def delete_book(self, book_id):        query = "DELETE FROM books WHERE id = ?"        self.cursor.execute(query, (book_id,))        self.conn.commit()        print(f"编号为{book_id}的图书删除成功!")
  • 方法名修改

由于我们要实现的是windows版本程序,而且用三层架构,list_books()search_book_by_title()search_book_by_id()这三个方法的名称不太合适了,数据访问层应该只“返回”数据而不是“显示”数据。因此,我们把函数名称修改如下:

def get_all_books(self):        query = "SELECT * FROM books"        self.cursor.execute(query)        return self.cursor.fetchall()    def get_book_by_title(self, title):        query = "SELECT * FROM books WHERE title LIKE ?"        self.cursor.execute(query, ("%" + title + "%",))        return self.cursor.fetchall()    def get_book_by_id(self, book_id):        query = "SELECT * FROM books WHERE id = ?"        self.cursor.execute(query, (book_id,))        return self.cursor.fetchone()

其它方面基本上不用修改了。整体代码如下:

import sqlite3class BookDatabase:    def __init__(self, db_name="books.db"):        self.conn = sqlite3.connect(db_name)        self.cursor = self.conn.cursor()        self.create_table()    def create_table(self):        self.cursor.execute(            """            CREATE TABLE IF NOT EXISTS books (                id INTEGER PRIMARY KEY AUTOINCREMENT,                ISBN TEXT NOT NULL,                title TEXT NOT NULL,                author TEXT,                category TEXT,                purchase_date DATE,                location TEXT,                status TEXT,                completion_date DATE,                notes TEXT            )            """        )        self.conn.commit()    def add_book(self, book_data):        query = """            INSERT INTO books (ISBN, title, author, category, purchase_date, location, status, completion_date, notes)            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)        """        self.cursor.execute(            query,            (                book_data["ISBN"],                book_data["title"],                book_data["author"],                book_data["category"],                book_data["purchase_date"],                book_data["location"],                book_data["status"],                book_data["completion_date"],                book_data["notes"],            ),        )        self.conn.commit()        print(f"图书添加成功:{book_data}")    def update_book(self, book_id, updated_data):        query = """            UPDATE books            SET ISBN = ?, title = ?, author = ?, category = ?, purchase_date = ?, location = ?, status = ?, completion_date = ?, notes = ?            WHERE id = ?        """        self.cursor.execute(            query,            (                updated_data["ISBN"],                updated_data["title"],                updated_data["author"],                updated_data["category"],                updated_data["purchase_date"],                updated_data["location"],                updated_data["status"],                updated_data["completion_date"],                updated_data["notes"],                book_id,            ),        )        self.conn.commit()        print(f"编号为{book_id}的图书信息更新成功:{updated_data}")    def delete_book(self, book_id):        query = "DELETE FROM books WHERE id = ?"        self.cursor.execute(query, (book_id,))        self.conn.commit()        print(f"编号为{book_id}的图书删除成功!")    def get_all_books(self):        query = "SELECT * FROM books"        self.cursor.execute(query)        return self.cursor.fetchall()    def get_book_by_title(self, title):        query = "SELECT * FROM books WHERE title LIKE ?"        self.cursor.execute(query, ("%" + title + "%",))        return self.cursor.fetchall()    def get_book_by_id(self, book_id):        query = "SELECT * FROM books WHERE id = ?"        self.cursor.execute(query, (book_id,))        return self.cursor.fetchone()    def close(self):        self.conn.close()

你可能会问,我们对数据访问的类修改这么多,这还是“重用”吗?实际上,之所以要修改这么多,是因为我们在开始做第一个版本程序的时候并没有考虑将它作为windows版本程序也可以使用的数据访问层,而是从最基本的增删改查函数一步步修改而来。如果我们有了三层架构的设计经验,以后设计的东西复用性就会更强,甚至可以不加修改地重用。

即使如此,我们一步步演化的代码也节约了大量的精力,这也是我们将数据访问代码独立成类的好处。

现在,该实现用户界面了,现在我们知道,它属于“表示层”

三、UI实现

使用PyQt Designer设计一个窗口main_window

具体的窗口设计过程我们就不详述了,毕竟你应该已经熟悉了QtDesigner的可视化操作。这个窗口整体采用垂直布局,对界面上的组件自上而下包括:

  • 最上方的QLabel组件显示了一个Banner图片,图片使用资源文件加载;
  • 第二行是一组水平布局的操作组件,从左至右为:
  • keywordEdit:用于输入查询的关键字。
  • searchButton:用于点击执行查询。
  • horizontalSpacer:这个控件像一个弹簧,用于把查找和后面的按钮分开,它会随着窗口尺寸伸展或压缩,使后面的按钮始终位于窗口右侧。
  • addButton:新增图书。
  • updateButton:修改图书。
  • deleteButton:删除图书。
  • 最后一行是booksTable:用于显示图书信息。

上面的组件中,唯一你可能不太熟悉的就是booksTable,它是一个QTableWidget控件:

QTableWidget控件提供了一个基于表格的界面,允许我们以行和列的形式展示数据,使用起来比较简单。我们一般会先设置好表格中的行和列,再使用setItem方法来修改数据即可,表格也可能通过连接信号和槽实现用户交互(比如双击某一行数据弹出修改对话框)。

要修改表中的列,你可以在QTableWidget控件上点击鼠标右键,选择“编辑项目”,弹出对话框:

通过这个列表可以添加表格中的列,点击“属性”按钮可以显示选中列的详细属性,进行更进一步的修改。这里我们只需添加与数据库中字段对应的列即可,不需要单独设置每列的属性。

完成控件设置后需要设置相关的信号和槽函数,如下图所示:

完成上述设置,返回VSCode,编辑窗口文件和对应的资源。现在我们的项目文件应该是下面这样:

下面可以开始实现业务逻辑层了。

四、实现业务逻辑

新建book_manager.py,利用我们前面所学的PyQt知识,初始化应用,显示窗口。其中添加、删除、修改、查询按钮的实现都是空函数,相当于前一节课我们搭建的初步程序结构。

import sysimport Ui_main_windowfrom PyQt5.QtWidgets import (    QApplication,    QMainWindow,    QTableWidgetItem,    QTableWidget,    QMessageBox,    QDialog,)from dal import BookDatabaseclass BookManager(QMainWindow, Ui_main_window.Ui_MainWindow):    def __init__(self):        super(BookManager, self).__init__()        self.setupUi(self)        self.db = BookDatabase()  # 实例化数据库对象    # 添加书籍    def add_book(self):        pass    # 删除书籍    def delete_book(self):        pass    # 更新书籍    def update_book(self):        pass    # 搜索书籍    def search_book(self):        passif __name__ == "__main__":    app = QApplication(sys.argv)    manager = BookManager()    manager.show()    sys.exit(app.exec_())

此时运行程序,可以显示一个空白表格的主界面:

4.1 实现新增方法

要实现新增,我们不能再使用控制台的input()函数输入数据,而是会显示一个新增的窗口,接受用户输入,再把输入的信息传递给BookManager对象处理。要实现这个新增窗口,可以使用PyQtDesigner新建,也可以使用代码创建对话框。这里我们选择后一种方法,因为它的界面相对简单,就是一系列编辑控件。

新建一个dialogs.py文件,并添加以下内容:

from PyQt5.QtWidgets import (    QPushButton,    QVBoxLayout,    QLineEdit,    QDateEdit,    QComboBox,    QDialog,    QLabel,    QApplication,)from PyQt5.QtCore import QDate# 添加书籍对话框类class AddBookDialog(QDialog):    def __init__(self, parent=None):        super().__init__(parent)        self.setWindowTitle("添加书籍")        self.setGeometry(150150400300)        layout = QVBoxLayout()        self.isbnEdit = QLineEdit(self)        self.isbnEdit.setPlaceholderText("ISBN")        layout.addWidget(self.isbnEdit)        self.titleEdit = QLineEdit(self)        self.titleEdit.setPlaceholderText("书名")        layout.addWidget(self.titleEdit)        self.authorEdit = QLineEdit(self)        self.authorEdit.setPlaceholderText("作者")        layout.addWidget(self.authorEdit)        self.categoryEdit = QLineEdit(self)        self.categoryEdit.setPlaceholderText("分类")        layout.addWidget(self.categoryEdit)        self.purchaseDateEdit = QDateEdit(self)        layout.addWidget(self.purchaseDateEdit)        self.locationCombo = QComboBox(self)        self.locationCombo.addItems(["1-1""1-2""1-3""2-1""2-2""2-3"])        layout.addWidget(self.locationCombo)        self.readStatusCombo = QComboBox(self)        self.readStatusCombo.addItems(["未读""已读"])        layout.addWidget(self.readStatusCombo)        self.completionDateEdit = QDateEdit(self)        layout.addWidget(self.completionDateEdit)        self.notesEdit = QLineEdit(self)        self.notesEdit.setPlaceholderText("备注")        layout.addWidget(self.notesEdit)        self.addButton = QPushButton("添加书籍")        self.addButton.clicked.connect(self.accept)        layout.addWidget(self.addButton)        self.setLayout(layout)        self.center()    def center(self):        # 获取主窗口的位置和大小        qr = self.parent().geometry()        # 获取屏幕的分辨率        screen = QApplication.desktop().screenNumber(self)        centerPoint = QApplication.desktop().screenGeometry(screen).center()        # 计算对话框的位置        x = qr.x() + qr.width() / 2 - self.width() / 2        y = qr.y() + qr.height() / 2 - self.height() / 2        self.move(            centerPoint.x() - self.width() / 2, centerPoint.y() - self.height() / 2        )    def get_book_data(self):        return {            "ISBN"self.isbnEdit.text(),            "title"self.titleEdit.text(),            "author"self.authorEdit.text(),            "category"self.categoryEdit.text(),            "purchase_date"self.purchaseDateEdit.date().toString("yyyy-MM-dd"),            "location"self.locationCombo.currentText(),            "status"self.readStatusCombo.currentText(),            "completion_date": (                self.completionDateEdit.date().toString("yyyy-MM-dd")                if self.completionDateEdit.date().isValid()                else ""            ),            "notes"self.notesEdit.text(),        }

这段代码很长,但很易理解。__init__()方法创建了一系列与数据库中字段对应的编辑组件,并按垂直布局显示在窗口中。调用center()方法让窗口居中。最后一个get_book_data()方法则是将组件上的信息组合成字典返回。

日期信息返回的时候稍微做了一下处理,如果日期组件的日期有效就返回日期,否则返回空字符串。

接下来,我们在book_manager.py中导入添加对话框:

from dialogs import AddBookDialog

add_book()方法编写代码:

# 添加书籍    def add_book(self):        dialog = AddBookDialog(self)        if dialog.exec_() == QDialog.Accepted:            book_data = dialog.get_book_data()            self.db.add_book(book_data)            # 提示添加成功            QMessageBox.information(self"提示""图书添加成功!")

现在运行程序,点击新增按钮,会显示对话框,供用户输入图书信息,完成后按“添加书籍”按钮会提示图书添加成功。

4.2 查询和显示图书

新增的图书并没有显示在表格中,因为我们没有将数据加载到表格。现在在BookManager类添加一个方法

def load_books(self, title=""):        self.booksTable.setRowCount(0)        books = self.db.get_book_by_title(title)  # 从数据库获取书籍        for row in books:            rowPosition = self.booksTable.rowCount()            self.booksTable.insertRow(rowPosition)            for column, item in enumerate(row):                self.booksTable.setItem(                    rowPosition, column, QTableWidgetItem(str(item))                )

这个方法先设置表格行数为0,用于清空表格,然后调用数据访问层的search_book_by_title()方法获得数据。如果title参数为空相当于获得全部数据,否则就是查询书名包含title参数的图书,然后通过遍历向表格控件插入数据。

这样,搜索按钮的代码就很简单了,直接使用关键字输入控件的内容来调用load_books()方法即可:

# 搜索书籍    def search_book(self):        self.load_books(self.keywordEdit.text())

为了让窗口显示的时候就加载数据,可以在__init()__方法中调用一次load_books()方法:

def __init__(self):        super(BookManagerself).__init__()        self.setupUi(self)        self.db = BookDatabase()  # 实例化数据库对象        self.load_books()

同样,新增完成后也要调用一次load_books()方法:

# 添加书籍    def add_book(self):        dialog = AddBookDialog(self)        if dialog.exec_() == QDialog.Accepted:            book_data = dialog.get_book_data()            self.db.add_book(book_data)            self.load_books(self.keywordEdit.text())            # 提示添加成功            QMessageBox.information(self"提示""图书添加成功!")

再次运行程序,会加载全部图书。通过书名也可以查询:

4.3 修改图书

修改图书和新增是相似的。在dialogs.py中再添加一个类UpdateBookDialog

class UpdateBookDialog(QDialog):    def __init__(self, book_data, parent=None):        super().__init__(parent)        self.setWindowTitle("更新书籍信息")        self.setGeometry(150150400350)  # 增加窗口高度,以适应标签和输入框        layout = QVBoxLayout()        # ISBN        self.isbnLabel = QLabel("ISBN:"self)        layout.addWidget(self.isbnLabel)        self.isbnEdit = QLineEdit(self)        self.isbnEdit.setText(book_data["ISBN"])        layout.addWidget(self.isbnEdit)        # 书名        self.titleLabel = QLabel("书名:"self)        layout.addWidget(self.titleLabel)        self.titleEdit = QLineEdit(self)        self.titleEdit.setText(book_data["title"])        layout.addWidget(self.titleEdit)        # 作者        self.authorLabel = QLabel("作者:"self)        layout.addWidget(self.authorLabel)        self.authorEdit = QLineEdit(self)        self.authorEdit.setText(book_data["author"])        layout.addWidget(self.authorEdit)        # 分类        self.categoryLabel = QLabel("分类:"self)        layout.addWidget(self.categoryLabel)        self.categoryEdit = QLineEdit(self)        self.categoryEdit.setText(book_data["category"])        layout.addWidget(self.categoryEdit)        # 购买日期        self.purchaseDateLabel = QLabel("购买日期:"self)        layout.addWidget(self.purchaseDateLabel)        self.purchaseDateEdit = QDateEdit(self)        self.purchaseDateEdit.setDate(            QDate.fromString(book_data["purchase_date"], "yyyy-MM-dd")        )        layout.addWidget(self.purchaseDateEdit)        # 位置        self.locationLabel = QLabel("位置:"self)        layout.addWidget(self.locationLabel)        self.locationCombo = QComboBox(self)        self.locationCombo.addItems(["1-1""1-2""1-3""2-1""2-2""2-3"])        self.locationCombo.setCurrentText(book_data["location"])        layout.addWidget(self.locationCombo)        # 是否已读        self.readStatusLabel = QLabel("是否已读:"self)        layout.addWidget(self.readStatusLabel)        self.readStatusCombo = QComboBox(self)        self.readStatusCombo.addItems(["未读""已读"])        self.readStatusCombo.setCurrentText(book_data["status"])        layout.addWidget(self.readStatusCombo)        # 完成阅读时间        self.completionDateLabel = QLabel("完成阅读时间:"self)        layout.addWidget(self.completionDateLabel)        self.completionDateEdit = QDateEdit(self)        finish_date = book_data["completion_date"]        if finish_date:            self.completionDateEdit.setDate(QDate.fromString(finish_date, "yyyy-MM-dd"))        layout.addWidget(self.completionDateEdit)        # 备注        self.notesLabel = QLabel("备注:"self)        layout.addWidget(self.notesLabel)        self.notesEdit = QLineEdit(self)        self.notesEdit.setText(book_data["notes"])        layout.addWidget(self.notesEdit)        # 更新按钮        self.updateButton = QPushButton("更新书籍")        self.updateButton.clicked.connect(self.accept)        layout.addWidget(self.updateButton)        self.setLayout(layout)        self.center()    def center(self):        # 获取主窗口的位置和大小        qr = self.parent().geometry()        # 获取屏幕的分辨率        screen = QApplication.desktop().screenNumber(self)        centerPoint = QApplication.desktop().screenGeometry(screen).center()        # 计算对话框的位置        x = qr.x() + qr.width() / 2 - self.width() / 2        y = qr.y() + qr.height() / 2 - self.height() / 2        self.move(            centerPoint.x() - self.width() / 2, centerPoint.y() - self.height() / 2        )    def get_updated_data(self):        return {            "ISBN"self.isbnEdit.text(),            "title"self.titleEdit.text(),            "author"self.authorEdit.text(),            "category"self.categoryEdit.text(),            "purchase_date"self.purchaseDateEdit.date().toString("yyyy-MM-dd"),            "location"self.locationCombo.currentText(),            "status"self.readStatusCombo.currentText(),            "completion_date": (                self.completionDateEdit.date().toString("yyyy-MM-dd")                if self.completionDateEdit.date().isValid()                else ""            ),            "notes"self.notesEdit.text(),        }

这个类和AddBookDialog类的区别就是在__init__()方法中要加载book_data,这是一个字典类型的图书数据,其它返回数据的操作都一样。

book_manager.py中再添加对UpdateBookDialog的引用:

from dialogs import AddBookDialogUpdateBookDialog

然后实现update_book方法 :

# 更新书籍    def update_book(self):        selected_row = self.booksTable.currentRow()        if selected_row >= 0:            book_id = self.booksTable.item(selected_row, 0).text()            book_data = self.db.get_book_by_id(book_id)  # 通过 book_id 获取书籍信息            update_dialog = UpdateBookDialog(                {                    "ISBN": book_data[1],                    "title": book_data[2],                    "author": book_data[3],                    "category": book_data[4],                    "purchase_date": book_data[5],                    "location": book_data[6],                    "status": book_data[7],                    "completion_date": book_data[8],                    "notes": book_data[9],                },                self,            )            if update_dialog.exec_() == QDialog.Accepted:                updated_data = update_dialog.get_updated_data()                self.db.update_book(book_id, updated_data)                self.load_books(self.keywordEdit.text())  # 刷新书籍列表        else:            QMessageBox.warning(self"错误""请选择要更新的书籍")

这个方法要我们首先判断表格中有没有选中的行,如果有,获得当前选中行的图书id,用这个id调用数据访问层获得图书信息,将获得的信息填充到update_dialog中,并处理窗口返回的信息,调用数据访问层实现数据保存。最后再使用load_books()方法重新检索数据。

运行后点击“修改”按钮,即可修改当前图书。由于我们还在QtDesigner中关联了表格的双击信号到update_book,所以双击表格中的行也可以调出这个对话框:

输入新的数据后点击“更新书籍”按钮保存数据,表格随之刷新。

4.4 删除图书

删除的代码相对简单,如下 :

def delete_book(self):        selected_row = self.booksTable.currentRow()        if selected_row >= 0:            book_id = self.booksTable.item(selected_row, 0).text()            book_title = self.booksTable.item(selected_row, 2).text()            try:                book_id = int(book_id)                if (                    QMessageBox.question(                        self,                        "确认删除",                        "确认删除书籍 {}?".format(book_title),                        QMessageBox.Yes | QMessageBox.No,                        QMessageBox.No,                    )                    == QMessageBox.Yes                ):                    self.db.delete_book(book_id)  # 从数据库删除书籍                    self.load_books(self.keywordEdit.text())  # 刷新书籍列表            except ValueError:                QMessageBox.warning(self"错误""无效的书籍编号")        else:            QMessageBox.warning(self"提示""请选择要删除的书籍")

删除之前我们先去数据库中检索相应编号的图书,并在删除之前让用户确认一下(这是为了防止用户不小心点击删除按钮,给用户一次后悔的机会),对删除操作来说这是很有必要的。之后调用数据访问层删除图书。

4.5 显示效果调整

现在通过添加功能多增加几条数据,你可能会发现显示效果有一些不尽人意的地方:

  • 首先是表格每行显示了一个行号,似乎与我们的图书编号冲突,不美观;
  • 其次是书名和作者列不够宽,信息显示不完整;
  • 最后一列备注是固定的,不会拉长,如果能随表格拉伸更好
  • 如果你双击某个单元格,在弹出修改框后如果你不修改,这个单元格会变成可编辑状态,但这个编辑并不起什么作用。

我们需要一些代码来调整表格控件的显示效果,编写一个set_table方法:

def set_table(self):        # 禁止编辑单元格        self.booksTable.setEditTriggers(QTableWidget.NoEditTriggers)        # 隐藏垂直标题        self.booksTable.verticalHeader().setVisible(False)        # 设置表头宽度        self.booksTable.setColumnWidth(1150)        self.booksTable.setColumnWidth(2200)        self.booksTable.setColumnWidth(3200)        # 设置最后一列自动伸缩        self.booksTable.horizontalHeader().setStretchLastSection(True)        # 设置隔行变色        self.booksTable.setAlternatingRowColors(True)

在窗口__init__()方法中添加对set_table()方法的调用:

def __init__(self):        super(BookManagerself).__init__()        self.setupUi(self)        self.set_table()    # 设置表格显示效果        self.db = BookDatabase()  # 实例化数据库对象        self.load_books()

经过调整再次运行,显示效果是不是好多了?

五、小结

“豆豆,你觉得这个版本的【万卷书库】,是不是好用多了?” 马老师问。

“是的,至少在更新的时候我不用把不需要修改的字段也过一遍了,显示的效果也更好了。” 豆豆说。

“在控制台中我们的操作更像是线性的,所以你只能一个属性一个属性地过一遍。” 马老师说,“现在用windows版本,你可以跳出来可以直接选择自己关注的信息处理,感觉自然不同——这就是在一定程度上消除了软件与用户之间的【张力】,用起来不那么难受了。”

“看来做软件不能只考虑功能实现,还得考虑许多易用性方面的问题呀 ” 豆豆若有所思地说。

“甚至说,你的软件好用会产生很强的【粘性】,从而留住用户”,马老师说,“比如苹果公司的IOS系统,很多用户习惯了它的用户体验,再用其它一些体验不那么优秀的系统,就感觉很不舒服,自然就成了忠实的【果粉】”。

“怪不得我妈现在除了苹果手机别的都不喜欢用了!” 豆豆恍然大悟地说。

马老师笑着说:“是的。当然用户体验可不仅仅是界面设计的问题,它还包括了软件的性能、稳定性、安全性等多个方面。程序员要不断地去了解用户,满足他们的需求,而不是将程序作为展示技术水平的试验品。有时候,我经常说有些新手做的程序,充满了一种实验室产品的味道,根本不考虑使用者的感觉,这就是一种将就,这样的作品,包括程序员自己在内,都是不愿意用的。”

“做软件,还是要讲究一点啊,但凡有了一点从用户角度出发去设计的意识,程序做出来就会明显不一样。”

“明白了马老师”,豆豆说,“总结下来七个字——要讲究,不要将就!”

最新文章

随机文章

基本 文件 流程 错误 SQL 调试
  1. 请求信息 : 2026-07-03 11:16:52 HTTP/2.0 GET : https://f.mffb.com.cn/a/494553.html
  2. 运行时间 : 0.094974s [ 吞吐率:10.53req/s ] 内存消耗:4,688.27kb 文件加载:140
  3. 缓存信息 : 0 reads,0 writes
  4. 会话信息 : SESSION_ID=6a958742cccfc89f05a82470504730b7
  1. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/public/index.php ( 0.79 KB )
  2. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/autoload.php ( 0.17 KB )
  3. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/autoload_real.php ( 2.49 KB )
  4. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/platform_check.php ( 0.90 KB )
  5. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/ClassLoader.php ( 14.03 KB )
  6. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/autoload_static.php ( 4.90 KB )
  7. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper.php ( 8.34 KB )
  8. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-validate/src/helper.php ( 2.19 KB )
  9. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/helper.php ( 1.47 KB )
  10. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/stubs/load_stubs.php ( 0.16 KB )
  11. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Exception.php ( 1.69 KB )
  12. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-container/src/Facade.php ( 2.71 KB )
  13. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/deprecation-contracts/function.php ( 0.99 KB )
  14. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/polyfill-mbstring/bootstrap.php ( 8.26 KB )
  15. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/polyfill-mbstring/bootstrap80.php ( 9.78 KB )
  16. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/var-dumper/Resources/functions/dump.php ( 1.49 KB )
  17. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-dumper/src/helper.php ( 0.18 KB )
  18. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/var-dumper/VarDumper.php ( 4.30 KB )
  19. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/App.php ( 15.30 KB )
  20. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-container/src/Container.php ( 15.76 KB )
  21. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/container/src/ContainerInterface.php ( 1.02 KB )
  22. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/provider.php ( 0.19 KB )
  23. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Http.php ( 6.04 KB )
  24. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper/Str.php ( 7.29 KB )
  25. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Env.php ( 4.68 KB )
  26. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/common.php ( 0.03 KB )
  27. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/helper.php ( 18.78 KB )
  28. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Config.php ( 5.54 KB )
  29. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/app.php ( 0.95 KB )
  30. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/cache.php ( 0.78 KB )
  31. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/console.php ( 0.23 KB )
  32. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/cookie.php ( 0.56 KB )
  33. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/database.php ( 2.48 KB )
  34. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/Env.php ( 1.67 KB )
  35. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/filesystem.php ( 0.61 KB )
  36. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/lang.php ( 0.91 KB )
  37. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/log.php ( 1.35 KB )
  38. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/middleware.php ( 0.19 KB )
  39. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/route.php ( 1.89 KB )
  40. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/session.php ( 0.57 KB )
  41. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/trace.php ( 0.34 KB )
  42. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/view.php ( 0.82 KB )
  43. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/event.php ( 0.25 KB )
  44. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Event.php ( 7.67 KB )
  45. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/service.php ( 0.13 KB )
  46. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/AppService.php ( 0.26 KB )
  47. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Service.php ( 1.64 KB )
  48. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Lang.php ( 7.35 KB )
  49. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/lang/zh-cn.php ( 13.70 KB )
  50. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/Error.php ( 3.31 KB )
  51. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/RegisterService.php ( 1.33 KB )
  52. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/services.php ( 0.14 KB )
  53. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/PaginatorService.php ( 1.52 KB )
  54. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/ValidateService.php ( 0.99 KB )
  55. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/ModelService.php ( 2.04 KB )
  56. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/Service.php ( 0.77 KB )
  57. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Middleware.php ( 6.72 KB )
  58. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/BootService.php ( 0.77 KB )
  59. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/Paginator.php ( 11.86 KB )
  60. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-validate/src/Validate.php ( 63.20 KB )
  61. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/Model.php ( 23.55 KB )
  62. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/Attribute.php ( 21.05 KB )
  63. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/AutoWriteData.php ( 4.21 KB )
  64. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/Conversion.php ( 6.44 KB )
  65. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/DbConnect.php ( 5.16 KB )
  66. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/ModelEvent.php ( 2.33 KB )
  67. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/RelationShip.php ( 28.29 KB )
  68. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/contract/Arrayable.php ( 0.09 KB )
  69. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/contract/Jsonable.php ( 0.13 KB )
  70. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/contract/Modelable.php ( 0.09 KB )
  71. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Db.php ( 2.88 KB )
  72. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/DbManager.php ( 8.52 KB )
  73. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Log.php ( 6.28 KB )
  74. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Manager.php ( 3.92 KB )
  75. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/log/src/LoggerTrait.php ( 2.69 KB )
  76. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/log/src/LoggerInterface.php ( 2.71 KB )
  77. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Cache.php ( 4.92 KB )
  78. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/simple-cache/src/CacheInterface.php ( 4.71 KB )
  79. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper/Arr.php ( 16.63 KB )
  80. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/cache/driver/File.php ( 7.84 KB )
  81. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/cache/Driver.php ( 9.03 KB )
  82. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/CacheHandlerInterface.php ( 1.99 KB )
  83. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/Request.php ( 0.09 KB )
  84. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Request.php ( 55.78 KB )
  85. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/middleware.php ( 0.25 KB )
  86. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Pipeline.php ( 2.61 KB )
  87. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/TraceDebug.php ( 3.40 KB )
  88. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/middleware/SessionInit.php ( 1.94 KB )
  89. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Session.php ( 1.80 KB )
  90. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/session/driver/File.php ( 6.27 KB )
  91. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/SessionHandlerInterface.php ( 0.87 KB )
  92. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/session/Store.php ( 7.12 KB )
  93. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Route.php ( 23.73 KB )
  94. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleName.php ( 5.75 KB )
  95. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Domain.php ( 2.53 KB )
  96. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleGroup.php ( 22.43 KB )
  97. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Rule.php ( 26.95 KB )
  98. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleItem.php ( 9.78 KB )
  99. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/route/app.php ( 1.72 KB )
  100. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/Route.php ( 4.70 KB )
  101. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/dispatch/Controller.php ( 4.74 KB )
  102. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Dispatch.php ( 10.44 KB )
  103. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/controller/Index.php ( 4.81 KB )
  104. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/BaseController.php ( 2.05 KB )
  105. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/facade/Db.php ( 0.93 KB )
  106. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/connector/Mysql.php ( 5.44 KB )
  107. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/PDOConnection.php ( 52.47 KB )
  108. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Connection.php ( 8.39 KB )
  109. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/ConnectionInterface.php ( 4.57 KB )
  110. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/builder/Mysql.php ( 16.58 KB )
  111. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Builder.php ( 24.06 KB )
  112. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/BaseBuilder.php ( 27.50 KB )
  113. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Query.php ( 15.71 KB )
  114. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/BaseQuery.php ( 45.13 KB )
  115. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/TimeFieldQuery.php ( 7.43 KB )
  116. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/AggregateQuery.php ( 3.26 KB )
  117. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php ( 20.07 KB )
  118. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ParamsBind.php ( 3.66 KB )
  119. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ResultOperation.php ( 7.01 KB )
  120. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/WhereQuery.php ( 19.37 KB )
  121. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/JoinAndViewQuery.php ( 7.11 KB )
  122. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/TableFieldInfo.php ( 2.63 KB )
  123. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/Transaction.php ( 2.77 KB )
  124. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/log/driver/File.php ( 5.96 KB )
  125. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/LogHandlerInterface.php ( 0.86 KB )
  126. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/log/Channel.php ( 3.89 KB )
  127. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/event/LogRecord.php ( 1.02 KB )
  128. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/Collection.php ( 16.47 KB )
  129. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/View.php ( 1.70 KB )
  130. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/View.php ( 4.39 KB )
  131. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Response.php ( 8.81 KB )
  132. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/response/View.php ( 3.29 KB )
  133. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Cookie.php ( 6.06 KB )
  134. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-view/src/Think.php ( 8.38 KB )
  135. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/TemplateHandlerInterface.php ( 1.60 KB )
  136. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/Template.php ( 46.61 KB )
  137. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/template/driver/File.php ( 2.41 KB )
  138. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/template/contract/DriverInterface.php ( 0.86 KB )
  139. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/runtime/temp/067d451b9a0c665040f3f1bdd3293d68.php ( 11.98 KB )
  140. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/Html.php ( 4.42 KB )
  1. CONNECT:[ UseTime:0.000586s ] mysql:host=127.0.0.1;port=3306;dbname=f_mffb;charset=utf8mb4
  2. SHOW FULL COLUMNS FROM `fenlei` [ RunTime:0.000748s ]
  3. SELECT * FROM `fenlei` WHERE `fid` = 0 [ RunTime:0.000334s ]
  4. SELECT * FROM `fenlei` WHERE `fid` = 63 [ RunTime:0.000345s ]
  5. SHOW FULL COLUMNS FROM `set` [ RunTime:0.000583s ]
  6. SELECT * FROM `set` [ RunTime:0.000205s ]
  7. SHOW FULL COLUMNS FROM `article` [ RunTime:0.000532s ]
  8. SELECT * FROM `article` WHERE `id` = 494553 LIMIT 1 [ RunTime:0.000553s ]
  9. UPDATE `article` SET `lasttime` = 1783048612 WHERE `id` = 494553 [ RunTime:0.007337s ]
  10. SELECT * FROM `fenlei` WHERE `id` = 66 LIMIT 1 [ RunTime:0.000390s ]
  11. SELECT * FROM `article` WHERE `id` < 494553 ORDER BY `id` DESC LIMIT 1 [ RunTime:0.000573s ]
  12. SELECT * FROM `article` WHERE `id` > 494553 ORDER BY `id` ASC LIMIT 1 [ RunTime:0.000490s ]
  13. SELECT * FROM `article` WHERE `id` < 494553 ORDER BY `id` DESC LIMIT 10 [ RunTime:0.001009s ]
  14. SELECT * FROM `article` WHERE `id` < 494553 ORDER BY `id` DESC LIMIT 10,10 [ RunTime:0.000689s ]
  15. SELECT * FROM `article` WHERE `id` < 494553 ORDER BY `id` DESC LIMIT 20,10 [ RunTime:0.006281s ]
0.096662s