第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())




运行程序,会看到如图31所示的窗口。在macOS中,标题栏图标并不显示在标题栏中,而是将图像显示在Dock上,如图31所示窗口下方最右侧的图标就是本例创建的窗口的标题栏图标。Windows窗口的标题栏图标会显示在标题栏的左侧,如图32所示。Linux比较复杂,使用不同的窗口管理器时,会有不同的效果。有的显示图标,有的不显示图标。



图31在macOS下创建的窗口




图32在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())




运行程序,会看到如图33所示的窗口。



图33布局


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()




运行代码,显示如图34所示的窗口,在QLineEdit组件中输入一些文本,单击“显示”按钮,这3个TLineEdit组件中输入的内容就会显示在按钮下方。



图34组件演示


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())




运行程序,会显示如图35所示的窗口,读者可以单击“添加”按钮和“删除”按钮来体验QListWidget组件插入和删除列表项的效果。



图35列表组件


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按钮会删除当前列表项。如图36所示。



图36下拉列表组件


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()




运行程序,会看到如图37所示的界面,单击“添加”按钮会添加新的行,单击“删除”按钮,会删除当前行。



图37表格组件


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())




运行程序,会显示如图38所示的窗口,单击Add按钮,会在当前选中的节点下面添加子节点; 单击Delete按钮,会将选中的节点(包括该节点的子节点)删除。



图38树形组件


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的菜单栏上,如图39所示。如果在Windows系统中或Linux系统中,主菜单会直接显示在窗口上。

在窗口或标签组件上右击鼠标,会显示如图310所示的弹出菜单。



图39主菜单和弹出菜单




图310弹出菜单



在前面的代码中,使用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())




运行程序,会显示如图311所示的窗口。如果单击某一个按钮,会有相应的对话框弹出,完成选择(或输入)后,就会在对应按钮下方看到相应的内容。



图311对话框示例


这7种对话框在macOS、Windows和Linux下的显示效果并不相同,但其功能是一样的。例如,图312是macOS的“打开文件”对话框。图313是macOS的“选择颜色”对话框。



图312“打开文件”对话框




图313“选择颜色”对话框


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()




运行程序,在显示的窗口上绘制一些曲线,并设置画笔的颜色,效果如图314所示。



图314画图程序


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')




下面是这个例子的完整代码,单击“打开图像”按钮会弹出“打开图像”对话框,选择图像文件后,会在窗口下方显示该图像,如图315所示。单击“逆时针旋转”按钮,会弹出一个“输入文本”对话框,输入旋转角度后,图像会向逆时针旋转一定角度,如图316所示。“顺时针旋转”按钮也有类似的操作。最后,单击“保存图像”按钮,会弹
出“保存图像”对话框,输入文件名后,会将旋转后的图像保存为新的图像文件。读者可以打开旋转后的图像,效果如图317所示。



图315打开图像




图316逆时针旋转图像





图317旋转后的图像



代码位置: 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后,如果连接成功,会在另一台机器上同样程序的列表组件一开始显示连接成功信息。如果两台机器的点对点聊天程序连接成功,可以在文本输入框中要发送的内容,然后单击“发送”按钮,将输入的内容发送到另一台机器的聊天程序中,并在列表中显示,效果如图318所示。



图318点对点聊天


代码位置: 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是能否顺利理解这些案例的基础。