第3章 Python GUI工具包: PyQt6 PyQt是一个创建Python GUI应用程序的工具包,是Qt和Python结合的产物,可以认为是为了将Qt的功能用于Python开发的一个Qt的Python包装器。PyQt由Riverbank Computing公司开发,是最强大的GUI库之一。PyQt6是基于Digia公司Qt6的Python接口,是PyQt6的最新版本,也是目前最流行的基于Python的GUI工具包。本章将介绍PyQt6的主要功能以及一些常用API的使用方法。本书中所有的GUI应用都使用PyQt6实现,所以本章的内容非常重要。 3.1Python中主要的GUI工具包 Python中有非常多的GUI工具包,这些工具包主要有Tkinter、PyQT/PySide、wxPython、Kivy、PySimpleGui、Flexx、Toga等,它们的优缺点如下。 (1) Tkinter。 优点: 是Python的默认标准GUI库,使用简单。 缺点: 设计的界面比较简陋。 (2) PyQT/PySide。 优点: 作为Python的插件实现,功能非常强大,可以用Qt开发多个漂亮的界面。 缺点: 商业授权上存在一些问题。 (3) wxPython。 优点: 跨平台支持性很好。 缺点: 使用相对较少。 (4) Kivy。 优点: 跨平台支持性很好,可以用于移动端开发。 缺点: 学习曲线较陡峭。 (5) PySimpleGui。 优点: 使用简单,易于上手。 缺点: 功能相对较少。 (6) Flexx。 优点: 纯Python工具箱,使用Web技术进行渲染,更适合应用于Web应用中。 缺点: 使用相对较少。 (7) Toga。 优点: 适用于macOS、Windows、Linux(GTK)以及Android和iOS等移动平台。 缺点: 使用相对较少。 PyQT和PySide之所以放在一起,是因为它们都是Python的QT绑定库,它们的API非常相似,最大的区别是License。PyQT采用了GPL协议,QT公司拥有商业许可证; 而PySide则使用LGPL协议,可以免费使用,不需要开源代码。目前PyQT由Riverbank Computing公司维护,比较稳定,开发社区也比较大;而PySide(又名Qt for Python)现由Qt公司维护,比PyQt更“年轻”一些。 本章会主要讲解PyQt6,因为其功能非常强大,几乎可以完成任何工作,而且是目前最流行的基于Python的GUI工具包,技术社区非常繁荣。 如果读者没有安装PyQt6,可以使用下面的命令进行安装。 pip3 install PyQt6 3.2创建窗口 创建窗口时需要使用QWidget类。QWidget类是所有用户界面对象的基类。它提供了一个默认的构造方法,可以创建一个空的窗口。QWidget类提供了一些基本的功能,例如设置窗口标题、设置窗口图标、设置窗口大小、设置窗口位置等。此外,QWidget类还提供了一些事件处理函数,如鼠标事件、键盘事件、绘制事件等。 如果想创建自己的用户界面对象,可以从QWidget类派生出一个新的类。在这个新的类中,可以添加自己的成员变量和成员函数,并且可以重载QWidget类中的一些函数来实现自己的功能。 本节的例子会利用QWidget类创建一个窗口,并设置窗口的尺寸、位置、窗口标题、窗口图标以及红色的背景颜色。完成这个例子需要使用QWidget类的setGeometry方法设置窗口的尺寸和位置,使用setWindowTitle方法设置窗口标题,使用setWindowIcon方法设置窗口图标,使用setStyleSheet方法设置背景颜色。这些方法的原型和参数含义如下。 1. setGeometry方法 该方法的原型如下: setGeometry(self, x: int, y: int, w: int, h: int) -> None 参数含义如下。 (1) x: 窗口左上角的横坐标。 (2) y: 窗口左上角的纵坐标。 (3) w: 窗口的宽度。 (4) h: 窗口的高度。 2. setWindowTitle方法 该方法的原型如下: setWindowTitle(self, str) -> None str参数表示窗口的标题。 3. setWindowIcon方法 该方法的原型如下: setWindowIcon(self, icon) -> None icon参数表示窗口图标,为QIcon对象。 注意: QWidget类和QApplication类都有setWindowIcon方法,使用方法完全相同。不过由于macOS并不支持窗口标题,所以在macOS下使用QWidget.setWindowIcon方法是无效的,只能使用QApplication.setWindowIcon方法将图标放到macOS最下方的Dock(程序坞)中。而在Windows和Linux中,使用QWidget.setWindowIcon和QApplication.setWindowIcon都可以,为了让程序可以同时在macOS、Windows和Linux下正常运行,所以本例使用QApplication.setWindowIcon方法设置标题图标。 4. setStyleSheet方法 setStyleSheet方法用于设置PyQt6应用程序中的样式表。样式表是一种用于设置组件外观的机制,类似于CSS。可以使用setStyleSheet方法来设置组件的背景颜色、字体、边框等。例如,以下代码将QPushButton组件的背景颜色设置为蓝色: QPushButton { background-color: blue; } 可以在样式表中使用CSS选择器来选择要应用样式的组件。例如,以下代码将所有QLabel组件的字体大小设置为30像素: QLabel { font-size: 30px; } setStyleSheet方法的原型如下: setStyleSheet(self, str,) -> None str参数表示样式字符串。 下面的代码完整地演示了这个例子的实现过程。 代码位置: src/gui/pyqt6/create_window.py import sys from PyQt6.QtWidgets import QApplication, QWidget from PyQt6.QtGui import QIcon class Example(QWidget): def __init__(self): super().__init__() self.initUI() def initUI(self): # 设置窗口位置和尺寸 self.setGeometry(300, 300, 300, 220) # 设置窗口标题 self.setWindowTitle('窗口标题') # 设置窗口背景色 self.setStyleSheet("background-color: red") self.show() if __name__ == '__main__': app = QApplication(sys.argv) # 设置窗口图标 app.setWindowIcon(QIcon('images/tray.png')) ex = Example() sys.exit(app.exec()) 运行程序,会看到如图31所示的窗口。在macOS中,标题栏图标并不显示在标题栏中,而是将图像显示在Dock上,如图31所示窗口下方最右侧的图标就是本例创建的窗口的标题栏图标。Windows窗口的标题栏图标会显示在标题栏的左侧,如图32所示。Linux比较复杂,使用不同的窗口管理器时,会有不同的效果。有的显示图标,有的不显示图标。 图31在macOS下创建的窗口 图32在Windows下创建的窗口 注意: 运行代码之前,要保证images目录中有tray.png文件。 3.3布局 PyQt6提供了多种布局方式,包括水平布局、垂直布局、网格布局和表单布局。水平布局和垂直布局是最常用的两种布局方式,它们分别将组件从左到右或从上到下排列。网格布局将组件放置在一个网格中,而表单布局可以在窗口中放置多个组件,并且可以自动调整组件的大小和位置。在PyQt6中,可以使用QHBoxLayout和QVBoxLayout创建水平布局和垂直布局,使用QGridLayout创建网格布局,使用QFormLayout创建表单布局。这些布局管理器都是QWidget的子类,可以通过addWidget方法将组件添加到其中。如果需要在一个布局中添加另一个布局,则可以使用addLayout方法。 下面的例子演示了如何使用这4种布局。首先用PyQt6创建一个窗口,然后将窗口垂直分成4个区域,分别演示水平布局、垂直布局、网格布局和表单布局。第1个区域使用水平布局,水平放置5个按钮。第2个区域使用垂直布局,垂直放置3个按钮。第3个区域使用网格布局,放置9个按钮(文本从1到9),排成3×3形式。第4个区域使用表单布局放几个组件,用于演示使用表单布局。 代码位置: src/gui/pyqt6/layout.py import sys from PyQt6.QtWidgets import QApplication, QWidget, QGridLayout, QVBoxLayout, QHBoxLayout, QFormLayout, QPushButton class Example(QWidget): def __init__(self): super().__init__() self.initUI() def initUI(self): # 水平布局 hbox = QHBoxLayout() for i in range(5): hbox.addWidget(QPushButton(str(i))) # 垂直布局 vbox = QVBoxLayout() for i in range(3): vbox.addWidget(QPushButton(str(i))) # 网格布局 grid = QGridLayout() positions = [(i,j) for i in range(3) for j in range(3)] for position, name in zip(positions, ['1', '2', '3', '4', '5', '6', '7', '8', '9']): button = QPushButton(name) grid.addWidget(button, *position) # 表单布局 form = QFormLayout() form.addRow('Name:', QPushButton('Button')) form.addRow('Age:', QPushButton('Button')) form.addRow('Job:', QPushButton('Button')) # 整体垂直布局 vbox_all = QVBoxLayout() vbox_all.addLayout(hbox) vbox_all.addLayout(vbox) vbox_all.addLayout(grid) vbox_all.addLayout(form) self.setLayout(vbox_all) self.setGeometry(300, 300, 300, 150) self.setWindowTitle('布局演示') self.show() if __name__ == '__main__': app = QApplication(sys.argv) ex = Example() sys.exit(app.exec()) 运行程序,会看到如图33所示的窗口。 图33布局 3.4常用组件 PyQt6提供了一些常用的组件,如按钮、标签、文本框等,方便用户进行界面设计和交互。PyQt6中常用的组件包括QCheckBox(复选框)、QPushButton(按钮)、QSlider(滑杆)、QProgressBar(进度条)、QCalendarWidget(日历)、QComboBox(下拉列表框)、QLabel(标签)、QLineEdit(行文本编辑器)、QTextEdit(文本编辑器)、QSpinBox(整数步长调节器)、QDoubleSpinBox(浮点数步长调节器)、QDateEdit(日期编辑器)、QTimeEdit(时间编辑器)、QDateTimeEdit(日期和时间编辑器)等。 大多数组件的使用方法类似,每一个组件都对应于一个类,类名就是前面给出的组件英文名,如QLineEdit、QPushButton等。创建一个组件,其实就是创建这个组件类的对象,然后将组件放到窗口上,例如,下面的代码创建一个QPushButton组件。 button = QPushButton('Click me', window) 下面的例子会创建一个窗口,用PyQt6创建一个窗口,并放置3个QLineEdit组件、3个QLabel组件和3个复选框组件。根据这些复选框组件是否选中,控制前面的QLineEdit组件是否可以输入文本,如果不可输入文本,将QLineEdit组件的背景色设置为红色,否则恢复为白色。在组件的下方还会放置一个按钮组件和一个QLabel组件,单击按钮组件,会将这3个QLineEdit组件中输入的内容组合成一个字符串,显示在最下方的QLabel组件中。 代码位置: src/gui/pyqt6/components.py from PyQt6 import QtWidgets, QtCore from PyQt6.QtWidgets import QApplication, QWidget, QLabel, QLineEdit, QCheckBox, QVBoxLayout, QHBoxLayout, QPushButton from PyQt6.QtGui import QPalette, QColor class Window(QWidget): def __init__(self): super().__init__() self.setGeometry(300, 300, 300, 220) self.setWindowTitle("组件演示") # 创建布局 layout = QVBoxLayout() # 创建姓名行 name_layout = QHBoxLayout() name_label = QLabel('姓名') self.name_line_edit = QLineEdit() self.name_check_box = QCheckBox() self.name_check_box.setChecked(True) self.name_check_box.stateChanged.connect(self.on_name_check_box_state_changed) name_layout.addWidget(name_label) name_layout.addWidget(self.name_line_edit) name_layout.addWidget(self.name_check_box) # 创建年龄行 age_layout = QHBoxLayout() age_label = QLabel('年龄') self.age_line_edit = QLineEdit() self.age_check_box = QCheckBox() self.age_check_box.setChecked(True) self.age_check_box.stateChanged.connect(self.on_age_check_box_state_changed) age_layout.addWidget(age_label) age_layout.addWidget(self.age_line_edit) age_layout.addWidget(self.age_check_box) # 创建收入行 income_layout = QHBoxLayout() income_label = QLabel('收入') self.income_line_edit = QLineEdit() self.income_check_box = QCheckBox() self.income_check_box.setChecked(True) self.income_check_box.stateChanged.connect(self.on_income_check_box_state_changed) income_layout.addWidget(income_label) income_layout.addWidget(self.income_line_edit) income_layout.addWidget(self.income_check_box) # 添加姓名、年龄、收入到布局中 layout.addLayout(name_layout) layout.addLayout(age_layout) layout.addLayout(income_layout) # 创建按钮和显示标签 button = QPushButton('显示') button.clicked.connect(self.on_button_clicked) self.display_label = QLabel() # 添加按钮和显示标签到布局中 layout.addWidget(button) layout.addWidget(self.display_label) # 设置窗口布局 self.setLayout(layout) # 姓名复选框的槽函数 def on_name_check_box_state_changed(self): if self.name_check_box.isChecked(): self.name_line_edit.setReadOnly(False) palette = self.name_line_edit.palette() palette.setColor(QPalette.ColorRole.Base, QColor('white')) self.name_line_edit.setPalette(palette) else: self.name_line_edit.setReadOnly(True) palette = self.name_line_edit.palette() palette.setColor(QPalette.ColorRole.Base, QColor('red')) self.name_line_edit.setPalette(palette) # 年龄复选框的槽函数 def on_age_check_box_state_changed(self): if self.age_check_box.isChecked(): self.age_line_edit.setReadOnly(False) palette = self.age_line_edit.palette() palette.setColor(QPalette.ColorRole.Base, QColor('white')) self.age_line_edit.setPalette(palette) else: self.age_line_edit.setReadOnly(True) palette = self.age_line_edit.palette() palette.setColor(QPalette.ColorRole.Base, QColor('red')) self.age_line_edit.setPalette(palette) # 收入复选框的槽函数 def on_income_check_box_state_changed(self): if self.income_check_box.isChecked(): self.income_line_edit.setReadOnly(False) palette = self.income_line_edit.palette() palette.setColor(QPalette.ColorRole.Base, QColor('white')) self.income_line_edit.setPalette(palette) else: self.income_line_edit.setReadOnly(True) palette = self.income_line_edit.palette() palette.setColor(QPalette.ColorRole.Base, QColor('red')) self.income_line_edit.setPalette(palette) def on_button_clicked(self): name = self.name_line_edit.text() age = self.age_line_edit.text() income = self.income_line_edit.text() display_text = f'姓名:{name},年龄:{age},收入:{income}' self.display_label.setText(display_text) if __name__ == '__main__': app = QApplication([]) window = Window() window.show() app.exec() 运行代码,显示如图34所示的窗口,在QLineEdit组件中输入一些文本,单击“显示”按钮,这3个TLineEdit组件中输入的内容就会显示在按钮下方。 图34组件演示 3.5列表组件(QListWidget) PyQt6有两个列表组件——QListView和QListWidget。QListView是一个用于显示列表的组件,采用了MVC模式。与QListWidget不同,它不会自动创建一个内部模型来管理列表项。相反地,需要手动创建一个模型,并将其与视图关联。这使得QListView更加灵活,因为可以使用任何类型的模型来管理数据。向QListView列表中添加列表项的代码如下: list_view = QListView() # 创建字符串列表模型,用于向列表中添加字符串列表项 model = QStringListModel() model.setStringList(['Item 1', 'Item 2', 'Item 3']) # 为QListView组件设置模型 list_view.setModel(model) QListWidget是QListView的升级版本,已经建立了一个数据存储模型QListWidgetItem,操作方便,直接调用addItem方法即可添加列表项。而QListView是基于Model的,需要自己建模(如建立QStringListModel、QSqlTableModel等),保存数据,这样就大大降低了数据冗余,提高了程序的效率,但是需要我们对数据建模有一定的了解。向QListWidget列表中添加列表项的代码如下: list_widget = QListWidget() item1 = QListWidgetItem("item1") list_widget.addItem(item1) item2 = QListWidgetItem("item2") list_widget.addItem(item2) 下面的例子在窗口上放置一个QListWidget组件和两个按钮(“添加”按钮和“删除”按钮),QListWidget组件默认添加6个列表项。单击“添加”按钮,会向QListWidget组件中添加1个新的列表项,如果某一个列表项被选中,单击“删除”按钮,会删除当前的列表项。 代码位置: src/gui/pyqt6/list.py import sys from PyQt6.QtWidgets import QApplication, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QListWidget, QListWidgetItem class Example(QWidget): def __init__(self): super().__init__() self.initUI() def initUI(self): # 创建一个QListWidget组件 self.listWidget = QListWidget(self) # 创建两个按钮 self.addButton = QPushButton('添加', self) self.delButton = QPushButton('删除', self) # 将按钮添加到水平布局中 hbox = QHBoxLayout() hbox.addWidget(self.addButton) hbox.addWidget(self.delButton) # 将QListWidget和水平布局添加到垂直布局中 vbox = QVBoxLayout() vbox.addWidget(self.listWidget) vbox.addLayout(hbox) # 设置窗口布局 self.setLayout(vbox) # 添加6个列表项 for i in range(6): item = QListWidgetItem('列表项 %d' % i) self.listWidget.addItem(item) # 连接按钮的单击事件 self.addButton.clicked.connect(self.addItem) self.delButton.clicked.connect(self.delItem) # 设置窗口大小和标题 self.setGeometry(300, 300, 350, 300) self.setWindowTitle('QListWidget') def addItem(self): # 向QListWidget中添加一个列表项 item = QListWidgetItem('新列表项') self.listWidget.addItem(item) def delItem(self): # 获取当前选中的列表项 currentItem = self.listWidget.currentItem() if currentItem is not None: # 删除当前选中的列表项 row = self.listWidget.row(currentItem) self.listWidget.takeItem(row) if __name__ == '__main__': app = QApplication(sys.argv) ex = Example() ex.show() sys.exit(app.exec()) 运行程序,会显示如图35所示的窗口,读者可以单击“添加”按钮和“删除”按钮来体验QListWidget组件插入和删除列表项的效果。 图35列表组件 3.6下拉列表组件(QComboBox) QComboBox是一个下拉列表组件,允许用户从预定义的选项中选择一个值。使用QComboBox.addItem方法可以向该组件中添加列表项,代码如下: combo_box.addItem('New Item') 如果想删除当前选定的选项,可以使用removeItem方法,代码如下: combo_box.removeItem(combo_box.currentIndex()) 下面的例子在窗口上放置一个QComboBox组件,里面默认添加3个item。QComboxBox组件下方水平放置两个组件(Add按钮和Delete按钮),单击Add按钮,向QComboxBox组件添加一个列表项。单击Delete按钮,会删除当前选中的列表项。 代码位置: src/gui/pyqt6/combobox.py from PyQt6.QtWidgets import QApplication, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QComboBox import sys class Example(QWidget): def __init__(self): super().__init__() self.initUI() def initUI(self): vbox = QVBoxLayout() hbox = QHBoxLayout() # 创建QComboBox组件 combo_box = QComboBox() # 向QComboBox组件添加3个默认列表项 combo_box.addItems(['Item 1', 'Item 2', 'Item 3']) vbox.addWidget(combo_box) add_button = QPushButton('Add') add_button.clicked.connect(lambda: combo_box.addItem('New Item')) hbox.addWidget(add_button) remove_button = QPushButton('Remove') remove_button.clicked.connect(lambda: combo_box.removeItem(combo_box.currentIndex())) hbox.addWidget(remove_button) vbox.addLayout(hbox) self.setLayout(vbox) self.setGeometry(300, 300, 350, 250) self.setWindowTitle('QComboBox Example') self.show() if __name__ == '__main__': app = QApplication(sys.argv) ex = Example() sys.exit(app.exec()) 运行程序,单击Add按钮会添加新的列表项,单击Remove按钮会删除当前列表项。如图36所示。 图36下拉列表组件 3.7表格组件(QTableWidget) QTableWidget是PyQt6中的一个表格组件,可以用于显示和编辑表格数据。QTableWidget是一个基于QTableView的类,提供了一些额外的功能,如行和列的插入和删除,以及单元格的合并。若要添加表头,可以使用QTableWidget的setHorizontalHeaderLabels方法来设置水平表头标签,使用setVerticalHeaderLabels方法来设置垂直表头标签。若要添加表格项,可以使用setItem方法来设置单元格的内容。若要删除表格项,可以使用removeRow或removeColumn方法来删除行或列。 在下面的例子中,在窗口放置一个QTableWidget组件和两个按钮(“添加”按钮和“删除”按钮)。两个按钮要放到QTableWidget组件的下面,默认添加4列的两个表格项。单击“添加”按钮,会添加一个新表格行; 单击“删除”按钮,会删除当前选中的表格行。 代码位置: src/gui/pyqt6/tablewidget.py from PyQt6.QtWidgets import * from PyQt6.QtGui import * class Table(QWidget): def __init__(self): super().__init__() self.setWindowTitle('QTableWidget组件演示') self.resize(500, 300) # 创建表格 self.table = QTableWidget() self.table.setColumnCount(4) self.table.setHorizontalHeaderLabels(['header1', 'header2', 'header3', 'header4']) self.table.setRowCount(2) for i in range(self.table.rowCount()): for j in range(self.table.columnCount()): item = QTableWidgetItem(str(i+j)) self.table.setItem(i, j, item) # 创建添加和删除按钮 add_button = QPushButton('添加') add_button.clicked.connect(self.add_row) delete_button = QPushButton('删除') delete_button.clicked.connect(self.delete_row) # 创建布局 button_layout = QHBoxLayout() button_layout.addWidget(add_button) button_layout.addWidget(delete_button) table_layout = QVBoxLayout() table_layout.addWidget(self.table) table_layout.addLayout(button_layout) # 设置布局 self.setLayout(table_layout) def add_row(self): row_count = self.table.rowCount() self.table.insertRow(row_count) for j in range(self.table.columnCount()): item = QTableWidgetItem(str(row_count+j)) self.table.setItem(row_count, j, item) def delete_row(self): row = self.table.currentRow() if row != -1: self.table.removeRow(row) if __name__ == '__main__': app = QApplication([]) app.setFont(QFont('Helvetica')) table = Table() table.show() app.exec() 运行程序,会看到如图37所示的界面,单击“添加”按钮会添加新的行,单击“删除”按钮,会删除当前行。 图37表格组件 3.8树形组件(QTreeWidget) QTreeWidget是PyQt6中的一个树形组件,用于显示树形结构的数据。QTreeWidget类根据预设的模型显示树中的数据。QTreeWidget使用类似于QListView类的方式提供一种典型的基于item的树形交互类,该类基于QT的“模型/视图”结构,提供了默认的模型来支撑item的显示,这些item类为QTreeWidgetItem类。如果不需要灵活的“模型/视图”框架,可以使用QTreeWidget来创建有层级关系的树形结构。如果把标准item模型结合QTreeView使用,可以得到更灵活的使用方法,从而把“数据”和“显示”分离开。QTreeWidget组件可以添加多列: 使用setColumnCount方法设置列数,使用setHeaderLabels方法设置列标签。 下面的例子在窗口上放置一个QTreeWidget组件和两个按钮(Add按钮和Delete按钮)。QTreeWidget组件默认会随机添加一些节点。单击Add按钮,会在当前选中的节点添加一个子节点,单击Delete按钮,会删除当前选中的节点。 代码位置: src/gui/pyqt6/treewidget.py import sys from PyQt6.QtWidgets import * from PyQt6.QtGui import QFont class MainWindow(QWidget): def __init__(self): super().__init__() self.setWindowTitle("QTreeWidget组件演示") # 创建树形组件 self.tree_widget = QTreeWidget() self.tree_widget.setColumnCount(2) self.tree_widget.setHeaderLabels(["Name", "Value"]) # 创建Add按钮 self.add_button = QPushButton("Add") self.add_button.clicked.connect(self.add_node) # 创建Delete按钮 self.delete_button = QPushButton("Delete") self.delete_button.clicked.connect(self.delete_node) # 创建水平布局 self.layout = QHBoxLayout() self.layout.addWidget(self.tree_widget) self.layout.addWidget(self.add_button) self.layout.addWidget(self.delete_button) # 设置布局 self.setLayout(self.layout) # 随机添加一些节点 for i in range(10): item = QTreeWidgetItem([f"Node {i}", f"Value {i}"]) self.tree_widget.addTopLevelItem(item) # 添加节点 def add_node(self): # 获取当前选中的节点 current_item = self.tree_widget.currentItem() if not current_item: return # 创建新的子节点 new_item = QTreeWidgetItem(["New Node", "New Value"]) current_item.addChild(new_item) # 删除节点 def delete_node(self): # 获取当前选中的节点 current_item = self.tree_widget.currentItem() if not current_item: return # 删除节点 current_item.parent().removeChild(current_item) if __name__ == '__main__': app = QApplication(sys.argv) app.setFont(QFont('Helvetica')) main_window = MainWindow() main_window.show() sys.exit(app.exec()) 运行程序,会显示如图38所示的窗口,单击Add按钮,会在当前选中的节点下面添加子节点; 单击Delete按钮,会将选中的节点(包括该节点的子节点)删除。 图38树形组件 3.9菜单 PyQt6支持的主要菜单类型是主菜单和弹出菜单。其中,主菜单是应用程序的主要导航方式,通常包含文件、编辑、视图、帮助等菜单项。弹出菜单是在用户请求时显示的菜单,通常包含与当前上下文相关的操作,一般在组件上右击鼠标,弹出菜单就会显示出来。 要实现这两种菜单,可以使用QMenuBar和QMenu类。QMenuBar类提供了一个水平菜单,可以在其中添加QMenu对象。QMenu类提供了一个垂直菜单,可以在其中添加QAction对象。 下面的例子在窗口放置一个标签组件,然后创建一个主菜单,菜单项包括新建、打开、保存、分割线、退出。单击“退出”菜单项,会退出程序; 单击其他菜单项,会将菜单项文本显示在标签组件中。右击窗口或标签,会显示与主菜单一模一样的弹出菜单。菜单项的动作也完全相同。 代码位置: src/gui/pyqt6/menu_demo.py import sys from PyQt6.QtWidgets import * from PyQt6.QtGui import * from PyQt6.QtCore import * class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setGeometry(200,200,200,200) # 创建标签组件 self.label = QLabel(self) self.label.setText("这是一个标签") self.label.setGeometry(50, 50, self.width(), 30) self.label.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.label.customContextMenuRequested.connect(self.showMenu) # 创建主菜单 menu_bar = self.menuBar() file_menu = menu_bar.addMenu("文件") # 向主菜单添加 self.addMenuItem(file_menu) # 显示弹出菜单 def showMenu(self, pos): menu = self.createPopupMenu() menu.exec(self.mapToGlobal(pos)) # 添加菜单项 def addMenuItem(self,menu): # 创建弹出菜单的菜单项 new_action = QAction("新建", self) open_action = QAction("打开", self) save_action = QAction("保存", self) separator_action = QAction(self) separator_action.setSeparator(True) exit_action = QAction("退出", self) # 将菜单项添加到弹出菜单中 menu.addAction(new_action) menu.addAction(open_action) menu.addAction(save_action) menu.addAction(separator_action) menu.addAction(exit_action) # 连接退出菜单项的信号和槽函数 exit_action.triggered.connect(self.close) # 连接其他菜单项的信号和槽函数 new_action.triggered.connect(lambda: self.label.setText("新建菜单项被单击")) open_action.triggered.connect(lambda: self.label.setText("打开菜单项被单击")) save_action.triggered.connect(lambda: self.label.setText("保存菜单项被单击")) # 创建弹出菜单 def createPopupMenu(self): menu = QMenu(self) # 向弹出菜单添加菜单项 self.addMenuItem(menu) return menu # 关闭应用程序 def closeEvent(self, event): event.accept() if __name__ == '__main__': app = QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec()) 运行程序,会显示一个窗口。如果在macOS系统中,主菜单会显示在macOS的菜单栏上,如图39所示。如果在Windows系统中或Linux系统中,主菜单会直接显示在窗口上。 在窗口或标签组件上右击鼠标,会显示如图310所示的弹出菜单。 图39主菜单和弹出菜单 图310弹出菜单 在前面的代码中,使用QMainWindow.createPopupMenu方法创建窗口的弹出菜单。当主窗口接收到上下文菜单事件时,会自动调用此方法生成一个菜单,并返回这个菜单(Menu对象)。通常会在QMainWindow的子类中重写createPopupMenu方法来创建弹出菜单。 另外,由于主菜单与弹出菜单完全相同,所以本例使用addMenuItem函数为主菜单和弹出菜单添加菜单项。 3.10对话框 PyQt6支持多种对话框类型,常用的对话框包括QMessageBox、QInputDialog、QFileDialog、QColorDialog、QFontDialog、QPrintDialog和QProgressDialog等。下面分别介绍这些对话框的功能和使用方法。 1. QMessageBox QMessageBox是最常用的对话框之一,可以用于显示消息、警告和错误等信息。使用QMessageBox的方法如下: msgBox = QMessageBox() msgBox.setIcon(QMessageBox.Information) msgBox.setText("This is a message box") msgBox.setWindowTitle("Message Box") msgBox.setStandardButtons(QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel) msgBox.exec() 上述代码中,setIcon方法用于设置图标,setText方法用于设置文本,setWindowTitle方法用于设置标题,setStandardButtons方法用于设置消息对话框的按钮种类,exec方法用于显示对话框。 setStandardButtons方法可以设置的按钮种类如下。 (1) QMessageBox.StandardButton.Ok: 确定。 (2) QMessageBox.StandardButton.Open: 打开。 (3) QMessageBox.StandardButton.Save: 保存。 (4) QMessageBox.StandardButton.Cancel: 取消。 (5) QMessageBox.StandardButton.Close: 关闭。 (6) QMessageBox.StandardButton.Discard: 放弃。 (7) QMessageBox.StandardButton.Apply: 应用。 (8) QMessageBox.StandardButton.Reset: 重置。 (9) QMessageBox.StandardButton.RestoreDefaults: 恢复默认值。 (10) QMessageBox.StandardButton.Help: 帮助。 (11) QMessageBox.StandardButton.SaveAll: 全部保存。 (12) QMessageBox.StandardButton.Yes: 是。 (13) QMessageBox.StandardButton.YesToAll: 全部是。 (14) QMessageBox.StandardButton.No: 否。 (15) QMessageBox.StandardButton.NoToAll: 全部否。 (16) QMessageBox.StandardButton.Abort: 中止。 (17) QMessageBox.StandardButton.Retry: 重试。 (18) QMessageBox.StandardButton.Ignore: 忽略。 上述代码中,setStandardButtons方法设置了两个按钮——OK和Cancel。这些按钮的作用是由按钮的角色决定的,例如,OK按钮的角色是AcceptRole,Cancel按钮的角色是RejectRole。如果用户单击了OK按钮,则返回QMessageBox.Ok;如果用户单击了Cancel按钮,则返回QMessageBox.Cancel。 2. QInputDialog QInputDialog可以用于获取用户输入的信息。使用QInputDialog的方法如下: text, ok = QInputDialog.getText(None, "Input Dialog", "Enter your name:") if ok: print(f"Your name is {text}") 上述代码中,getText方法用于弹出一个输入对话框,第1个参数为父窗口,第2个参数为标题,第3个参数为提示信息。如果用户单击OK按钮,则返回输入的文本和True;否则,返回空字符串和False。 3. QFileDialog QFileDialog可以用于打开和保存文件。使用QFileDialog的方法如下: filename, _=QFileDialog.getOpenFileName(None, "Open File", "", "Text Files (*.txt);;All Files (*)") if filename: print(f"Filename is {filename}") 上述代码中,getOpenFileName方法用于弹出一个打开文件对话框,第1个参数为父窗口,第2个参数为标题,第3个参数为默认目录,第4个参数为文件过滤器。如果用户选择了文件,则返回文件名和True;否则,返回空字符串和False。 4. QColorDialog QColorDialog可以用于选择颜色。使用QColorDialog的方法如下: color = QColorDialog.getColor() if color.isValid(): print(f"Selected color: {color.name()}") 上述代码中,getColor方法用于弹出一个颜色选择对话框。如果用户选择了颜色,则返回颜色对象;否则,返回无效的颜色对象。 5. QFontDialog QFontDialog可以用于选择字体。使用QFontDialog的方法如下: font, ok = QFontDialog.getFont() if ok: print(f"Selected font: {font.family()}, {font.pointSize()}pt") 上述代码中,getFont方法用于弹出一个字体选择对话框。如果用户选择了字体,则返回字体对象和True;否则,返回默认字体对象和False。 6. QPrintDialog QPrintDialog类提供了一个打印对话框,用于选择打印机和打印选项。它的构造方法原型如下: QPrintDialog(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags()) 其中,parent参数表示可选的QWidget对象,flags参数表示可选的窗口标志。使用QPrintDialog类的方法如下: printer = QPrinter(QPrinter.HighResolution) dialog = QPrintDialog(printer) if dialog.exec_() == QDialog.Accepted: # 打印文档 7. QProgressDialog QProgressDialog类提供了一个进度对话框,用于显示长时间运行的操作的进度。它的构造方法如下: QProgressDialog(labelText: str, cancelButton: Union[QWidget, NoneType] = None, minimum: int = 0, maximum: int = 100, parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags()) 其中,labelText参数是进度对话框中显示的文本; cancelButton参数是可选的QWidget对象,用于取消操作; minimum和maximum参数分别是进度条的最小值和最大值; parent参数是可选的QWidget对象; flags参数是可选的窗口标志。使用QProgressDialog类的方法如下: progress_dialog = QProgressDialog("Copying files...", "Cancel", 0, len(files), self) progress_dialog.setWindowModality(Qt.WindowModal) for i, file in enumerate(files): if progress_dialog.wasCanceled(): break progress_dialog.setValue(i) # 复制文件 progress_dialog.close() 下面的例子完整地演示了这7种对话框的使用方法,单击相应的按钮,会有对应的对话框弹出,选择后,会在按钮下方的标签组件中看到选择的结果。 代码位置: src/gui/pyqt6/dialogs.py import sys from PyQt6.QtWidgets import * from PyQt6.QtPrintSupport import QPrintDialog class Example(QWidget): def __init__(self): super().__init__() self.initUI() def initUI(self): vbox = QVBoxLayout() # 添加4个按钮 btn1 = QPushButton('QMessageBox', self) btn2 = QPushButton('QInputDialog', self) btn3 = QPushButton('QFileDialog', self) btn4 = QPushButton('QColorDialog', self) btn5 = QPushButton('QFontDialog', self) btn6 = QPushButton('QPrintDialog', self) btn7 = QPushButton('QProgressDialog', self) # 添加4个标签 self.label1 = QLabel(self) self.label2 = QLabel(self) self.label3 = QLabel(self) self.label4 = QLabel(self) self.label5 = QLabel(self) self.label6 = QLabel(self) # 将4个标签添加到垂直布局中 vbox.addWidget(btn1) vbox.addWidget(self.label1) vbox.addWidget(btn2) vbox.addWidget(self.label2) vbox.addWidget(btn3) vbox.addWidget(self.label3) vbox.addWidget(btn4) vbox.addWidget(self.label4) vbox.addWidget(btn5) vbox.addWidget(self.label5) vbox.addWidget(btn6) vbox.addWidget(self.label6) vbox.addWidget(btn7) # 连接4个按钮到槽函数 btn1.clicked.connect(lambda: self.showMessageBox("messagebox")) btn2.clicked.connect(lambda: self.showInputDialog()) btn3.clicked.connect(lambda: self.showOpenFileDialog()) btn4.clicked.connect(lambda: self.showColorDialog()) btn5.clicked.connect(lambda: self.showFontDialog()) btn6.clicked.connect(lambda: self.showPrintDialog()) btn7.clicked.connect(lambda: self.showProgressDialog()) self.setLayout(vbox) # 显示消息对话框 def showMessageBox(self, text): sender = self.sender() msgBox = QMessageBox() msgBox.setWindowTitle("Message Box") msgBox.setText(f"The {sender.text()} button has been pressed.") msgBox.setInformativeText(f"You selected {text}") msgBox.setStandardButtons(QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel) msgBox.setDefaultButton(QMessageBox.StandardButton.Ok) ret = msgBox.exec() if ret == QMessageBox.StandardButton.Ok: self.label1.setText("ok") else: self.label1.setText("cancel") # 显示输入对话框 def showInputDialog(self): ret = QInputDialog.getText(self, 'Input Dialog', 'Enter your name:')[0] self.label2.setText(ret) # 显示打开文件对话框 def showOpenFileDialog(self): ret = QFileDialog.getOpenFileName(self)[0] self.label3.setText(ret) # 显示颜色对话框 def showColorDialog(self): col = QColorDialog.getColor() if col.isValid(): self.label4.setStyleSheet(f"background-color: {col.name()}") # 显示字体对话框 def showFontDialog(self): font, okPressed = QFontDialog.getFont() if okPressed: self.label5.setText(font.toString()) # 显示打印对话框 def showPrintDialog(self): printDialog = QPrintDialog() if printDialog.exec() == QDialog.DialogCode.Accepted: self.label6.setText("Success") # 显示进度条对话框 def showProgressDialog(self): progressDialog = QProgressDialog(self) progressDialog.setLabelText("Copying files...") progressDialog.setRange(0, 100) progressDialog.setValue(47) progressDialog.show() if __name__ == '__main__': app = QApplication(sys.argv) ex = Example() ex.show() sys.exit(app.exec()) 运行程序,会显示如图311所示的窗口。如果单击某一个按钮,会有相应的对话框弹出,完成选择(或输入)后,就会在对应按钮下方看到相应的内容。 图311对话框示例 这7种对话框在macOS、Windows和Linux下的显示效果并不相同,但其功能是一样的。例如,图312是macOS的“打开文件”对话框。图313是macOS的“选择颜色”对话框。 图312“打开文件”对话框 图313“选择颜色”对话框 3.11自由绘画 本节会使用Canvas组件实现一个有趣的功能: 自由绘画。基本功能是可以通过鼠标在窗口上画出任意平滑的曲线,并可以设置曲线的颜色,还可以将绘制的曲线保存成图像文件。 可以使用PyQt6的QPainter和QPen类来实现绘图功能。QPainter类用于在QWidget组件上执行绘图操作,其功能类似于一个绘图工具,为大部分图形界面提供了高度优化的函数,使QPainter类可以绘制从简单的直线到复杂的图形等。QPen类则用于设置画笔的颜色、宽度等属性。使用 QColorDialog 类来弹出“选择颜色”对话框,以设置画笔的颜色。可以在QWidget组件上重载paintEvent函数,并在该函数中使用QPainter类进行绘制。使用 mousePressEvent和 mouseMoveEvent函数来实现鼠标自由绘制线条。要实现平滑曲线,可以使用贝塞尔曲线或样条曲线等算法。 下面的例子将完整地实现这个绘图程序,通过单击窗口下方的“画笔颜色”按钮,可以弹出“选择颜色”对话框,并选择画笔的颜色。移动鼠标就可以在按钮下方的区域自由绘制曲线。单击“保存图像”按钮,会弹出“保存文件”对话框,输入文件名,就能将绘制的图形保存成图像文件。 代码位置: src/gui/pyqt6/drawing.py import sys from PyQt6.QtWidgets import * from PyQt6.QtGui import * from PyQt6.QtCore import * class Window(QWidget): def __init__(self): super().__init__() self.setWindowTitle("画图程序") self.setGeometry(100, 100, 500, 500) self.setFixedSize(500, 500) layout = QVBoxLayout() self.setLayout(layout) self.pen_color = Qt.GlobalColor.black self.pen_width = 3 self.canvas = QLabel() layout.addWidget(self.canvas) pixmap = QPixmap(500, 450) pixmap.fill(Qt.GlobalColor.white) self.canvas.setPixmap(pixmap) color_button = QPushButton("画笔颜色") color_button.clicked.connect(self.select_color) layout.addWidget(color_button, alignment=Qt.AlignmentFlag.AlignHCenter) save_button = QPushButton("保存图像") save_button.clicked.connect(self.save_image) layout.addWidget(save_button, alignment=Qt.AlignmentFlag.AlignHCenter) self.last_x, self.last_y = None, None # 选择颜色 def select_color(self): color = QColorDialog.getColor(initial=self.pen_color) if color.isValid(): self.pen_color = color # 鼠标移动时触发 def mouseMoveEvent(self, e): if self.last_x is None: self.last_x = e.position().x() self.last_y = e.position().y() return pixmap = self.canvas.pixmap().copy() painter = QPainter(pixmap) pen = QPen(self.pen_color, self.pen_width, Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap, Qt.PenJoinStyle.RoundJoin) painter.setPen(pen) painter.drawLine(int(self.last_x - 15), int(self.last_y ), int(e.position().x() - 15), int(e.position().y())) painter.end() self.canvas.setPixmap(pixmap) self.last_x = e.position().x() self.last_y = e.position().y() def mouseReleaseEvent(self, e): self.last_x = None self.last_y = None def save_image(self): file_name, _ = QFileDialog.getSaveFileName(self, "保存图像", "", "PNG Image (*.png)") if file_name: self.canvas.pixmap().save(file_name) app = QApplication(sys.argv) window = Window() window.show() app.exec() 运行程序,在显示的窗口上绘制一些曲线,并设置画笔的颜色,效果如图314所示。 图314画图程序 3.12图像旋转器 本节会通过PyQt6实现一个可以打开图像、旋转图像和保存旋转结果的项目。实现这个项目涉及如下4种技术。 打开图像; 显示图像; 旋转图像; 保存图像。 这4种技术可以完全通过PyQt6提供的API实现。 1. 打开图像 使用QPixmap对象可以打开图像文件,代码如下: pixmap = QtGui.QPixmap('/resources/file.png') 2. 显示图像 可以使用QLabel组件显示图像,通过QLabel.setPixmap方法为标签组件指定QPixmap对象,代码如下: image_label = QtWidgets.QLabel(self) image_label.setPixmap(self.pixmap) 3. 旋转图像 使用QTransform.rotate方法可以设置图像的旋转角度。如果rotate方法的参数值是正数,为顺时针旋转; 如果为负数,则为逆时针旋转。其实QTransform.rotate方法只是设置旋转角度,需要调用QPixmap.transformed方法,并传入QTransform对象,才会通过QPixmap.transformed方法返回旋转后的图像,最后再将旋转后的图像重新显示在QLabel组件上,代码如下: # 顺时针旋转40度 transform = QtGui.QTransform().rotate(40) pixmap = pixmap.transformed(transform) image_label.setPixmap(pixmap) 4. 保存图像 通过QPixmap.save方法可以保存图像,代码如下: pixmap = image_label.pixmap() pixmap.save('/resources/myimage.png') 下面是这个例子的完整代码,单击“打开图像”按钮会弹出“打开图像”对话框,选择图像文件后,会在窗口下方显示该图像,如图315所示。单击“逆时针旋转”按钮,会弹出一个“输入文本”对话框,输入旋转角度后,图像会向逆时针旋转一定角度,如图316所示。“顺时针旋转”按钮也有类似的操作。最后,单击“保存图像”按钮,会弹 出“保存图像”对话框,输入文件名后,会将旋转后的图像保存为新的图像文件。读者可以打开旋转后的图像,效果如图317所示。 图315打开图像 图316逆时针旋转图像 图317旋转后的图像 代码位置: src/gui/pyqt6/rotate_image.py import sys from PyQt6 import QtWidgets, QtGui, QtCore from PyQt6.QtWidgets import QFileDialog class Window(QtWidgets.QWidget): def __init__(self): super().__init__() # 设置窗口大小 self.resize(500, 500) # 创建打开图像按钮 self.open_button = QtWidgets.QPushButton('打开图像', self) self.open_button.clicked.connect(self.open_image) # 创建逆时针旋转按钮 self.rotate_ccw_button = QtWidgets.QPushButton('逆时针旋转', self) self.rotate_ccw_button.clicked.connect(self.rotate_ccw) # 创建顺时针旋转按钮 self.rotate_cw_button = QtWidgets.QPushButton('顺时针旋转', self) self.rotate_cw_button.clicked.connect(self.rotate_cw) # 创建保存图像按钮 self.save_button = QtWidgets.QPushButton('保存图像', self) self.save_button.clicked.connect(self.save_image) # 创建用于显示图像的组件 self.image_label = QtWidgets.QLabel(self) self.image_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) # 创建布局并添加组件 layout = QtWidgets.QVBoxLayout(self) layout.addWidget(self.open_button) layout.addWidget(self.rotate_ccw_button) layout.addWidget(self.rotate_cw_button) layout.addWidget(self.save_button) layout.addWidget(self.image_label) def open_image(self): # 打开文件对话框选择图像文件 file_name, _ = QFileDialog.getOpenFileName(self, '打开图像', '', '图像文件 (*.png *.jpg *.jpeg *.bmp)') if file_name: # 加载并显示图像 self.pixmap = QtGui.QPixmap(file_name) self.image_label.setPixmap(self.pixmap) def rotate_ccw(self): # 弹出输入对话框获取旋转角度 angle, ok = QtWidgets.QInputDialog.getInt(self, '逆时针旋转', '输入旋转角度(度)') if ok: # 逆时针旋转图像并显示结果 transform = QtGui.QTransform().rotate(-angle) pixmap = self.pixmap.transformed(transform) self.image_label.setPixmap(pixmap) def rotate_cw(self): # 弹出输入对话框获取旋转角度 angle, ok = QtWidgets.QInputDialog.getInt(self, '顺时针旋转', '输入旋转角度(度)') if ok: # 顺时针旋转图像并显示结果 transform = QtGui.QTransform().rotate(angle) pixmap = self.pixmap.transformed(transform) self.image_label.setPixmap(pixmap) def save_image(self): # 打开保存文件对话框选择保存位置 file_name, _ = QFileDialog.getSaveFileName(self, '保存图像', '', 'PNG 图像 (*.png);;JPEG 图像 (*.jpg *.jpeg);;BMP 图像 (*.bmp)') if file_name: # 保存图像 pixmap = self.image_label.pixmap() pixmap.save(file_name) if __name__ == '__main__': app = QtWidgets.QApplication(sys.argv) window = Window() window.show() sys.exit(app.exec()) 3.13点对点聊天 点对点聊天的关键点就是建立两端的通信,Python通过Socket在两台机器之间建立连接和互相传递文本数据的基本步骤如下。 (1) 一台机器作为服务器端,创建一个Socket对象,绑定一个IP地址和端口号,然后调用listen方法开始监听客户端的连接请求。 示例代码如下: s = socket.socket() # 创建socket对象 host = socket.gethostname() # 获取本机名称 port = 12345 # 设置端口号 s.bind((host, port)) # 绑定IP地址和端口号 s.listen(1) # 开始监听客户端连接请求,最多允许一个连接 (2) 另一台机器作为客户端,创建一个Socket对象,指定服务器端的IP地址和端口号,然后调用connect方法尝试连接服务器。 示例代码如下: s = socket.socket() # 创建socket对象 host = '192.168.1.100' # 设置服务器端的IP地址 port = 12345 # 设置服务器端的端口号 s.connect((host, port)) # 尝试连接服务器 (3) 如果连接成功,服务器端的Socket对象会返回一个新的Socket对象,用于与客户端进行通信。客户端的Socket对象也可以与服务器端的新Socket对象进行通信。 (4) 通信过程中,可以使用send和recv方法发送和接收文本数据。也可以使用sendall和makefile方法发送和接收大量数据。 发送数据示例代码如下: s.send('hello world') # 将内容编码后发送给客户端 接收数据示例代码如下: while True: # 循环接收消息 data = s.recv(1024) # 接收客户端发送的数据,最多1024字节 if data: # 如果接收到数据 s = data.decode() # 在文本框中显示客户端发送的消息 print(s) else: # 如果没有接收到数据 break # 跳出循环 (5) 通信结束后,可以使用close方法关闭Socket对象。 示例代码如下: s.close() #关闭socket对象 下面的例子会实现一个点对点的聊天程序,在窗口上方大部分区域是一个列表组件,用于显示聊天记录和状态信息。最下方从左到右分别是文本输入框、“发送”按钮和“连接”按钮。单击“连接”按钮,会弹出IP输入框,输入IP后,如果连接成功,会在另一台机器上同样程序的列表组件一开始显示连接成功信息。如果两台机器的点对点聊天程序连接成功,可以在文本输入框中要发送的内容,然后单击“发送”按钮,将输入的内容发送到另一台机器的聊天程序中,并在列表中显示,效果如图318所示。 图318点对点聊天 代码位置: src/gui/pyqt6/p2pchat.py import sys import socket from PyQt6.QtWidgets import * from PyQt6.QtNetwork import QTcpServer, QTcpSocket import threading class ChatWindow(QWidget): def __init__(self): super().__init__() self.initUI() # 创建服务端socket self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 获取本机ip地址 self.host_ip = socket.gethostbyname(socket.gethostname()) # 设置通信端口号 self.port =8888 # 绑定ip地址和端口号 self.server_socket.bind((self.host_ip, self.port)) # 开始监听客户端连接请求 self.server_socket.listen() # 创建客户端socket self.client_socket = None # 启动服务端线程 threading.Thread(target=self.start_server).start() def initUI(self): self.setGeometry(300, 300, 400, 700) self.setFixedSize(400, 600) self.setWindowTitle('P2P Chat') vbox = QVBoxLayout() self.setLayout(vbox) self.listWidget = QListWidget() vbox.addWidget(self.listWidget) hbox = QHBoxLayout() vbox.addLayout(hbox) self.lineEdit = QLineEdit() hbox.addWidget(self.lineEdit) connectButton = QPushButton('发送') connectButton.clicked.connect(self.send_message) hbox.addWidget(connectButton) sendButton = QPushButton('连接') sendButton.clicked.connect(self.connect_to_server) hbox.addWidget(sendButton) def start_server(self): # 等待客户端连接 self.client_socket, address = self.server_socket.accept() # 显示客户端地址 self.listWidget.addItem( f"客户端 {address} 已连接") # 接收客户端消息 self.receive_message() def connect_to_server(self): ip, ok = QInputDialog.getText(self, '连接', '请输入服务端的ip地址') if ok: try: # 创建客户端socket self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 连接到服务端socket self.client_socket.connect((ip, self.port)) # 显示服务端地址 self.listWidget.addItem(f"服务端 {ip} 已连接") # 接收服务端消息 threading.Thread(target=self.receive_message).start() except: # 显示连接失败的信息 self.listWidget.addItem(f"无法连接到 {ip}") def send_message(self): # 获取文本输入框中的文本 message = self.lineEdit.text() # 如果文本不为空 if message: try: # 通过socket发送文本给对方 self.client_socket.send(message.encode('utf-8')) # 在聊天记录中显示自己发送的消息 self.listWidget.addItem( f"我: {message}") # 清空文本输入框中的文本 self.lineEdit.clear() except: # 显示发送失败的信息 self.listWidget.addItem(f"无法发送消息") def receive_message(self): while True: try: # 从socket中接收对方发送的消息 message = self.client_socket.recv(1024).decode() # 如果消息不为空 if message: # 在聊天记录中显示对方发送的消息 self.listWidget.addItem(f"对方: {message}") except: # 显示接收失败的信息 self.listWidget.addItem(f"无法接收消息") break if __name__ == '__main__': app = QApplication(sys.argv) chatWindow = ChatWindow() chatWindow.show() sys.exit(app.exec()) 本例需要在两台机器上运行,在其中一台机器上单击“连接”按钮,会弹出输入IP对话框; 输入IP后,如果IP是正确的,会成功连接到另一台机器运行的点对点聊天程序,然后就可以进行聊天了。 3.14小结 PyQt6是目前最流行的Python GUI工具包,而且功能相当强大。PyQt6中不仅提供了与GUI相关的API,还提供了很多非GUI的API。例如,QtMultimedia提供了与多媒体相关的API;QtSql提供了与数据库相关的API;QtWebSockets提供了与WebSocket相关的API。在本书后面的部分,还会有多处使用PyQt6以及其他第三方库创建各种有趣的应用。因此,学好PyQt6是能否顺利理解这些案例的基础。