第 15 章

目录与文件

本章共 6 个小节 · PySide6 Basic Tutorial
本章要点:
  1. QDir;
  2. QFile 与 QSaveFile;
  3. QTextStream;
  4. QDataStream。
15.1

QDir

QDir类可以对目录进行常用操作,如创建、删除、移动、重命名等。

可以通过向QDir类的构造函数传递目录路径来创建实例。QDir实例初始化时不会检查路径是否存在,若需要检查目录是否存在,请调用exists方法。实例化时提供的路径既可以是相对路径,也可以是绝对路径。相对路径通常以目录名(或文件名)开头,绝对路径包含驱动器号(Windows)或挂载路径(Linux、UNIX)。例如,绝对路径有:

C:\Windows\System32 /usr/local/abc E:\server\univs\docs /home/kitty/medias

相对路径有:

admin\123\maps data/cache models

路径分隔符使用“\”“/”都是允许的,Qt内部会根据正在运行的系统平台自动转换。如果需要获取当前平台所使用的分隔符,可以访问QDir.separator静态方法。该方法会返回当前系统平台所使用的分隔符。因此,在Windows系统中也可以使用“/”来连接路径分段,如C:/Windows。

current和 currentPath方法将返回当前应用程序的工作目录,前者返回的是QDir类型的实例,而后者直接返回路径字符串。应用程序默认的工作目录是可执行文件(或Python脚本文件)所在的目录。

程序代码可以调用setCurrent方法修改应用程序的工作目录。演示代码如下:

QDir.setCurrent("D:\\Demo")

值得注意的是,指定的工作目录必须是存在的,否则setCurrent方法不会生效。如非必要,不建议修改。如果程序改变了工作目录而未告知用户,那么运行过程中所产生的重要文件可能会丢失——用户不知道工作目录的位置,可能会找不到所需要的文件。

15.1.1 示例:QDir 类的基本用法

QDir类公开了3个重载的构造函数:

def __init__(arg__1: Union[QDir, str]) def __init__(path: Union[str, bytes, os.PathLike, NoneType]) def __init__( path: Union[str, bytes, os.PathLike], nameFilter: str, sort: QDir.SortFlag = ..., filter: QDir.Filter = ...)

第一个重载是通过现有的QDir对象来创建新的实例;第二个重载用得比较多,直接指定目录路径来实例化 QDir类,可以指定不存在的路径;第三个重载指定了筛选和排序的设置,nameFilter和 filter参数结合起来,约束path参数所提供的目录能够列出哪些子目录和文件。本示例将使用第二个重载版本的构造函数。

首先用绝对路径构造 QDir实例:

d = QDir("F:/Demo/kits/Qt")

上述代码中的路径也可以写成F:\\Demo\\kits\\Qt,这是 Windows系统中的表示方式。若是在Linux系统中,可以使用形如/mnt/Demo/kits/Qt的路径,假设/mnt是一个挂载点。

下面的代码打印出目录的名称和绝对路径,以及目录是否存在:

print(f"路径:{d.path()}") print(f"目录名:{d.dirName()}") print(f"绝对路径:{d.absolutePath()}") print(f"目录是否存在?{'是'if d.exists() else'否'}")

代码运行后,控制台输出以下文本:

路径:F:/Demo/kits/Qt 目录名:Qt 绝对路径:F:/Demo/kits/Qt 目录是否存在?否

path方法所返回的就是代码指定的路径,如上述代码中通过 QDir构造函数指定的F:/Demo/kits/Qt。

dirName方法返回目录名,即路径中最后一段名称。exists方法返回False,说明目录并不存在。

调用QDir类的构造函数时也可以不传递路径,实例化后可调用setPath方法设置,如下面代码所示:

d = QDir() d.setPath("../../../") print(f"\n路径:{d.path()}") print(f"目录名:{d.dirName()}") print(f"绝对路径:{d.absolutePath()}") print(f"目录是否存在?{'是'if d.exists() else'否'}")

上述代码在执行后会输出这样的文本:

路径:../../.. 目录名:.. 绝对路径:C:/Users 目录是否存在?是

上述代码提供的是相对路径。“..”表示上一层目录,以当前应用程序所在的目录为参考,../../../就是由当前目录向上回退三层的目录。

15.1.2 示例:复制和移动目录

要使用QDir类移动目录,可以使用rename方法。该方法既可以重命名目录,也可以移动目录。当源路径和目标路径在同一目录下时,rename方法将起到重命名的作用,例如:

dirObj.rename("/home/abc/pdfs/tools","/home/abc/pdfs/goods")

上述代码将 tools目录重命名为goods。如果源路径和目标路径不在同一目录下,那么rename方法实现的移动功能。例如:

dirObj.rename("/home/abc/pdfs/tools", "/home/abc/links/tools")

上述代码将 tools 目录移动到links 目录下。

本示例将实现目录的复制与移动功能。自定义窗口的基本布局如下:

class MyWindow(QWidget): def __init__(self): super().__init__() #文本框 self.mTxtSrc = QLineEdit(self) self.mTxtDest = QLineEdit(self) #使用表单布局 self.mTxtLayout = QFormLayout() self.mTxtLayout.addRow("源路径:", self.mTxtSrc) self.mTxtLayout.addRow("目标路径:",self.mTxtDest) #按钮 self.mBtnCopy = QPushButton("复制", self) self.mBtnMove = QPushButton("移动", self) #使用水平布局 self.mBtnLayout = QHBoxLayout() self.mBtnLayout.addWidget(self.mBtnCopy) self.mBtnLayout.addWidget(self.mBtnMove) #窗口整体布局 self.rootLayout = QVBoxLayout() #添加子布局 self.rootLayout.addLayout(self.mTxtLayout, 1) self.rootLayout.addLayout(self.mBtnLayout) #将布局应用于窗口 self.setLayout(self.rootLayout) #连接信号 self.mBtnCopy.clicked.connect(self.onDirCopy) self.mBtnMove.clicked.connect(self.onDirMove)

两个QLineEdit组件分别用于输入源路径和目标路径。两个按钮依次实现复制和移动功能。下面的代码实现 onDirCopy 方法:

def onDirCopy(self): srcPath = self.mTxtSrc.text() destPath = self.mTxtDest.text() if len(srcPath) == 0 or len(destPath) == 0: return #源目录必须存在 dir = QDir(srcPath) if dir.exists() == False: return #复制目录 try: copytree(srcPath, destPath) except FileExistsError: print("目标已存在")

QDir类未提供复制目录的功能,本示例将使用 shutil模块中的 copytree函数。该函数会递归复制目录和文件。

下面的代码实现移动目录功能:

def onDirMove(self): srcPath = self.mTxtSrc.text() destPath = self.mTxtDest.text() if len(srcPath) == 0 or len(destPath) == 0: return dir = QDir() #源目录必须存在 if not dir.exists(srcPath): return #移动目录 res = dir.rename(srcPath, destPath) dir.rename("/home/abc/pdfs/tools","/home/abc/links/tools") if res: print("目录移动成功")

示例运行后,在第一个文本框中输入源路径,在第二个文本框中输入目标路径。注意此处要输入完整路径。

如图15-1所示,源路径为 D:\Example\Full,目标路径为 D:\Example\Out\Full。如果执行复制操作,那就是将Full目录复制到Out目录下;若执行的是移动操作,那么Full目录将移动到Out目录下。

图 15-1
图 15-1 输入目录路径
图 15-1 输入目录路径

15.1.3 枚举目录和文件

QDir类有两种方法列出子目录和文件。

  1. entryList方法。该方法将以字符串列表的形式返回目录或文件,即仅包含路径。entryList方法的声明如下:
def entryList( filters: Filter = ..., sort: SortFlag = ... ) -> List[str] def entryList( nameFilters: Sequence[str], filters: Filter = ..., sort: SortFlag = ... ) -> List[str]
  1. entryInfoList 方法。该方法返回的是 QFileInfo 对象列表。QFileInfo 类包含列表详细的目录或文件信息,如绝对路径、创建时间、文件大小等。entryInfoList方法的声明如下:
def entryInfoList( filters: Filter = ..., sort: SortFlag = ... ) -> List[QFileInfo] def entryInfoList( nameFilters: Sequence[str], filters: Filter = ..., sort: SortFlag = ... ) -> List[QFileInfo]

nameFilters、filters和 sort 参数都是可选的,如果未指定,则默认使用 setNameFilters、setFilter 和setSorting方法所设置的值。

nameFilters参数是字符串列表,可通过名称来过滤目录和文件列表。名称过滤可以使用通配符“*”和“?”。“*”可代表一个或多个字符,“?”代表一个字符。例如,“*.mp4”表示返回某个目录下的所有扩展名为.mp4的文件;“yd*jpg”则表示返回某目录下所有以“yd”开头的.jpg文件。

filters参数是QDir.Filter枚举,以目录或文件的属性来过滤。其定义的成员如下。

  1. NoFilter:不使用属性过滤。
  1. Dirs:只列出目录。
  1. AllDirs:所有目录,包括隐藏的目录。
  1. Files:列出文件。
  1. Drives:列出驱动器/分区列表(Windows下可用)。
  1. NoSymLinks:忽略(不列出)符号链接。
  1. NoDot:忽略“.”相对路径。
  1. NoDotDot:忽略“..”相对路径。
  1. NoDotAndDotDot:忽略“.”和“..”相对路径,即 NoDot 和NoDotDot 的组合。
  1. AllEntries:列出分区、目录和文件。
  1. Readable:只列出当前应用程序能读取的对象,可与Files、Dirs等值组合。
  1. Writable:列出当前应用程序可写的对象,可与Files等值组合。
  1. Executable:列出当前应用程序能够执行的文件。通常是可执行文件或脚本文件。
  1. Executable:列出被修改过的文件(UNIX上无效)。
  1. Hidden:列出时包含隐藏文件。
  1. System:列出系统文件。
  1. CaseSensitive:在列出目录或文件时,区分大小写。

sort参数为 QDir.SortFlag枚举类型的值,指定返回的目录或文件列表的排序选项。SortFlag的成员如下。

  1. NoSort:默认不进行排序。
  1. Name:按名称排序。
  1. Time:按修改时间排序。
  1. Size:按文件大小排序。
  1. Unsorted:不排序。
  1. DirsFirst:先列出目录,再列出文件。
  1. Reversed:反向排序。
  1. IgnoreCase:忽略大小写。
  1. LocaleAware:根据当前平台的本地化设置进行排序。

15.1.4 示例:列出动态链接库文件并按大小排序

本示例将调用 entryInfoList 方法列出 C:\Windows\System32 目录下的.dll 文件,然后按照文件大小排序。代码如下:

from PySide6.QtCore import QDir #初始化 winDir = QDir("C:\\Windows\\system32") #枚举文件 fileList = winDir.entryInfoList( ["*.dll"], QDir.Filter.Files | QDir.Filter.Readable, QDir.SortFlag.Size ) if len(fileList) > 0: print(f'{"文件名":^70s}{"大小":^20s}') print('-' * 100) #打印文件信息 for f in fileList: print("{0:<70s}{1:15d}".format(f.fileName(), f.size())) else: print("未列出任何文件")

示例运行后,控制台窗口将输出如图15-2所示的文本。

图 15-2
图 15-2 列出.dll 文件
图 15-2 列出.dll 文件

15.1.5 示例:创建和删除目录

本示例将演示在应用程序的工作目录下创建和删除子目录。QDir类提供了用于创建目录的mkdir方法,以及删除目录用的rmdir方法。这两个方法均返回布尔值,True表示操作成功,否则返回False。

示例的实现步骤如下。

  1. 从QWidget类派生出自定义窗口类。
class DemoWindow(QWidget): ....
  1. 在__init__方法中初始化用户界面。
def __init__(self): super().__init__() #窗口布局 rootLayout = QVBoxLayout() self.setLayout(rootLayout) #标签 rootLayout.addWidget(QLabel("请输入目录名称:", self)) #输入框 self.txtInput = QLineEdit(self) rootLayout.addWidget(self.txtInput) rootLayout.addStretch(1) #按钮 btnMkDir = QPushButton("创建目录", self) btnRmDir = QPushButton("删除目录", self) rootLayout.addWidget(btnMkDir) rootLayout.addWidget(btnRmDir) #连接信号 btnMkDir.clicked.connect(self.onMkDir) btnRmDir.clicked.connect(self.onRmDir)
  1. 实现onMkDir方法,当“创建目录”按钮被单击后执行。
def onMkDir(self): #获取输入的目录名称 name = self.txtInput.text() if len(name) == 0: return #获取工作目录 cd = QDir.current() #判断目录是否已存在 if cd.exists(name): QMessageBox.warning(self, "警告", f"目录{name}已存在") return #创建新目录 res = cd.mkdir(name) msgbox = QMessageBox( QMessageBox.Icon.Information if res else QMessageBox.Icon.Warning, "操作结果", "目录创建成功" if res else "目录创建失败", QMessageBox.StandardButton.Ok, self ) msgbox.show()
  1. 实现onRmDir方法,“删除目录”按钮被单击后会被调用。
def onRmDir(self): #获取输入的目录名称 dirname = self.txtInput.text() #获取当前目录 currdir = QDir.current() #确定输入的目录有效且目录是存在的 if len(dirname) == 0 or not currdir.exists(dirname): return #删除目录 res = currdir.rmdir(dirname) if res: QMessageBox.information(self, "操作结果", "目录已删除") else: QMessageBox.warning(self, "操作结果", "删除目录失败")

运行示例程序,如图15-3所示。

图 15-3
图 15-3 创建与删除目录
图 15-3 创建与删除目录

输入目录名称,如Nice,然后单击“创建目录”按钮新建目录;单击“删除目录”按钮即可删除创建的目录。

15.2

QFile

QFile类提供一组支持文本和二进制方式读写文件的 API。通常在调用 QFile构造函数时传递要处理的文件名(绝对路径或相对路径),也可以在实例化 QFile对象后调用 setFileName方法设置文件名。

QFile类在初始化过程不检测文件存在性,可以调用exists方法判断文件是否存在。

在进行读写操作之前,必须调用 open 方法打开文件。调用时需要向该方法传递 OpenModeFlag枚举的值。该枚举类型的值可以用“或”运算符(|)组合使用。OpenModeFlag枚举的常用成员如下。

  1. NotOpen:文件未打开,设置该值会使isOpen方法返回False。
  1. ReadOnly:以只读方式打开。
  1. ExistingOnly:要求文件必须存在,否则 open方法会失败。如果指定了ReadOnly,就不需要指定ExistingOnly了,因为ReadOnly模式打开不存在的文件也会导致失败。
  1. WriteOnly:仅用于写操作。此值会清空文件原有的内容。
  1. NewOnly:创建新的文件,如果文件已存在,open方法将失败。
  1. Text:读写文本,主要是处理换行符。当写入文件时,换行符由平台决定(如 Windows 上会使用“\r\n”);当读取文件时,换行符会统一替换为“\n”。
  1. Append:追加模式,即文件原有的内容不会丢失,数据将从文件末尾开始写入。

文件打开后,调用write方法写入数据,或调用 read方法(包括 readAll、readLine方法)读取数据。

在读取数据时,还可以通过atEnd方法判断当前位置是否到达文件末尾。若atEnd方法返回True,将无有效数据可读。读写完成后,需要调用close方法关闭文件。如果数据不是一次性写入文件(两次写入之间相隔一段时间),可在每次写入数据后调用一下flush方法清空缓存数据并写入文件,等所有写入操作都结束后再调用close方法关闭文件。

15.2.1 示例:追加文件内容

本示例将分两次将数据写入文件:第一次写入5字节,第二次写入3字节。两次写入操作都是独立进行的——写入后马上关闭文件。为了保证第一次写入的数据不丢失,第二次打开文件时要使用Append模式。

首先实例化 QFile对象,代码如下:

file = QFile("demo.data")

以WriteOnly模式打开文件,写入 5字节。

if file.open(QFile.OpenModeFlag.WriteOnly): #5字节 content = b'\x13\x2f\x57\xe7\x16' #写入 file.write(content) #关闭文件 file.close()

以Append模式打开文件,写入3字节。此处不能用 WriteOnly模式,那样会把前面写的 5字节清空,造成数据丢失。代码如下:

if file.open(QFile.OpenModeFlag.Append): #3字节 content = b'\xa5\x83\x27' #写入 file.write(content) #关闭文件 file.close()

把文件的所有内容一次性读出。

if file.open(QFile.OpenModeFlag.ReadOnly): #全部读出 content = file.readAll() #关闭文件 file.close() #输出读到的数据 print("读到的文件内容:", content.data().hex())

readAll方法返回的对象是 QByteArray 类型,其 data 方法将返回 Python 的内置类型 bytes,然后调用hex方法返回十六进制的表示形式,即“132f57e716a58327”。

15.2.2 示例:读写文本文件

本示例将演示对文本文件的读写。在调用 open方法时使用OpenModeFlag.Text标志,使QFile 对象能自动处理换行符。写入文件时可以使用Python字符串内置的 encode方法进行编码;读取文本时,可以用 bytes 对象的 decode 方法进行解码。encode 和 decode 方法默认使用 UTF-8 编码。

示例实现步骤如下。

  1. 定义窗口类 DemoWindow,基类是 QWidget。
class DemoWindow(QWidget): ......
  1. 在__init__方法中初始化应用程序界面。
def __init__(self): super().__init__() #文本框 self.mEdit = QPlainTextEdit(self) #按钮 btnSave = QPushButton("保存", self) btnLoad = QPushButton("加载", self) btnClear = QPushButton("清空", self) #网格布局 layout = QGridLayout() self.setLayout(layout) layout.addWidget(self.mEdit, 0, 0, 1, 3) layout.addWidget(btnSave, 1, 0) layout.addWidget(btnLoad, 1,.1) layout.addWidget(btnClear, 1, 2) layout.setRowStretch(0, 1) #连接信号 btnSave.clicked.connect(self.onSave) btnLoad.clicked.connect(self.onLoad) btnClear.clicked.connect(self.onClear)

示例窗口使用网格布局。第一行是QPlainTextEdit组件,可输入多行文本;第二行是3个按钮,“保存”按钮将文本内容写入文件,“加载”按钮从文本文件中读取内容并显示在QPlainTextEdit组件中,“清空”按钮用于清除QPlainTextEdit组件中的文本。

  1. 实现onSave方法,将文本内容保存到文件中。
def onSave(self): fileName, filter = QFileDialog.getSaveFileName( self, "保存文件", QDir.currentPath(), "文本文件(*.txt)" ) if len(fileName) == 0: return #获取输入框中的文本 text = self.mEdit.toPlainText() if len(text) == 0: return #准备写入文件 fileObj = QFile(fileName) #打开文件 if fileObj.open(QFile.OpenModeFlag.WriteOnly | QFile.OpenModeFlag.Text): #写入数据 fileObj.write(text.encode()) #关闭文件 fileObj.close()

调用 QFileDialog 类的 getSaveFileName 静态方法,打开文件对话框并选择要保存文件的路径。随后创建 QFile 实例,在调用 open 方法时使用 WriteOnly 和 Text 标志。写入数据时需要调用 encode 方法进行编码。

  1. 实现 onLoad方法,从文件中加载文本。
def onLoad(self): #浏览文件 fileName, filter = QFileDialog.getOpenFileName( self, "打开文件", QDir.currentPath(), "文本文件(*.txt)" ) if len(fileName) == 0: return #准备读取文件 theFile = QFile(fileName) #打开文件 if theFile.open(QFile.OpenModeFlag.ReadOnly | QFile.OpenModeFlag.Text): #读取所有数据 readData = theFile.readAll() #还原文本内容 text = readData.data().decode() self.mEdit.setPlainText(text) #关闭文件 theFile.close()

QFileDialog.getOpenFileName静态方法可通过文件对话框选择要打开的文件。在调用 open 方法时,同样需要加上 Text 标志。使用 readAll方法读取所有内容,需要用 decode 方法将字节序列解码,返回文本内容,之后才能显示在QPlainTextEdit组件中。

  1. 实现 onClear 方法,清空 QPlainTextEdit 组件中所有文本。
def onClear(self): self.mEdit.clear()

运行示例程序后,在文本框中输入测试文本。单击“保存”按钮保存为test.txt文件。随后单击“清空”按钮清除文本框中的内容。最后单击“加载”按钮,选择保存的test.txt文件,文本内容被读出,重新显示在文本框中,如图15-4所示。

图 15-4
图 15-4 保存和加载文本文件
图 15-4 保存和加载文本文件

15.2.3 示例:创建符号链接

调用 QFile 对象的 link 方法可以创建当前文件的符号链接。在 Windows 系统中,链接名称必须加上扩展名.lnk(快捷方式)。

下面的代码先在当前工作目录下创建demo.tda文件,然后创建一个指向demo.tda文件的符号链接test。

#创建文件 file = QFile("demo.tda") #如果文件已存在,先删除 if file.exists(): file.remove() #打开文件 if file.open(QFile.OpenModeFlag.WriteOnly): #写入一些数据 data=b'\x91\x20\xa4\x48\xf7\x60\x11\x9b\xd2\x24\x36' file.write(data) #关闭文件 file.close() #检测当前平台 from platform import system if system() == "Windows": linkName = "test.lnk" else: linkName = "test" #为文件创建链接 file.link(linkName)

由于 Windows 上的快捷方式需要扩展名.lnk,因此要用到 platform 模块下的 system 函数。如果该函数返回“Windows”,那么链接名称需要带扩展名(test.lnk)。

15.3

QSaveFile

QSaveFile类与 QFile类的用法一样,但QSaveFile在写文件时相对安全一些。

在数据写入时,QSaveFile类会创建一个临时文件用于存放数据。当所有数据写入完毕且未发生错误后,再将临时文件重命名为目标文件。也就是说,如果在写入过程中发生了错误,这些数据会被丢弃。

QSaveFile类在写完文件后不要调用close方法关闭文件,而是改用commit方法进行确认。commit方法会检查写入过程中是否存在错误,如果未发生错误,就把临时文件重命名为要保存的文件;如果发生错误,则删除临时文件。commit方法会自动关闭文件,如果确认保存成功会返回True,否则返回False。

由于QSaveFile对象会在目标文件的同级目录下创建临时文件,若遇到只读目录,将无法创建临时文件,open 方法返回 False。此时可以调用 setDirectWriteFallback 方法并将参数设置为 True,使得QSaveFile类直接向目标文件写数据(此时就与QFile对象差不多)。这样做会使写入操作失去安全性,一旦发生错误,数据就会丢失。

如果在commit方法调用之前调用了cancelWriting方法,那么写入操作就会被取消,临时文件的内容被丢弃,就算后面再调用commit方法也无法保存文件。所以,调用了cancelWriting方法就不再需要调用 commit 方法。

下面的代码演示了QSaveFile类的使用。

file = QSaveFile("mydata") #打开文件 if file.open(QSaveFile.OpenModeFlag.WriteOnly): #第一次写入 data = bytes([25, 118, 24, 9, 46, 200, 105]) file.write(data) sleep(6) #第二次写入 data = bytes([89, 13, 12, 58, 109, 37, 81]) file.write(data) sleep(6) #第三次写入 data = bytes([75, 86, 144, 214, 30, 68, 96, 153]) file.write(data) #确认已写入完毕 file.commit()

上述代码分3次向文件写入数据,每次写入之前都调用sleep函数让程序暂停一段时间。这样当代码执行时,就能看到当前工作目录下创建的临时文件了。临时文件的命名是在目标文件名称的基础上添加随机扩展名,如 mydata.tinXfg、mydata.ktkywY等。

15.4

QBuffer

QBuffer类允许以类似文件的方式读写 QByteArray 对象。构造QBuffer 实例时,默认会创建一个内部使用的 QByteArray 对象。也可以通过构造函数或 setBuffer 方法引用其他 QByteArray 对象。

下面的代码演示QBuffer类的基本用法。

buffer = QBuffer() #打开buffer if buffer.open(QBuffer.OpenModeFlag.WriteOnly): #写入数据 bts = bytes([0x05, 0x4e, 0x91, 0xf4, 0x77, 0x6b, 0x38, 0x31, 0xaf, 0x9e, 0x7d, 0x19, 0x42, 0xf8, 0x04, 0x12, 0x83, 0x37, 0x72, 0x6a, 0x88, 0x55, 0xff, 0x70, 0x06, 0x18, 0xa9, 0x8d, 0x68]) buffer.write(bts) #关闭buffer buffer.close #以只读方式打开buffer if buffer.open(QBuffer.OpenModeFlag.ReadOnly): #读取数据 while not buffer.atEnd(): bs = buffer.read(5).data() #打印 for i in range(len(bs)): print(f'{bs[i]:02x}', end=' ') #关闭buffer buffer.close()

与QFile类一样,QBuffer对象在读写之前必须先调用open方法,操作结束后应调用 close方法。上述代码中,读取数据时使用了read方法。该方法通过参数设置一个最大值,每次调用时所读入的字节数不会超过最大值(例如 5)。由于read方法不能一次性读取所有字节,因此使用了while循环,当atEnd方法返回 True 时结束循环。

如果已创建QByteArray实例,可以将其传递给QBuffer类的构造函数,请参考下面的代码。

#先实例化 QByteArray 对象 arr = QByteArray() #再实例化 QBuffer 对象 buf = QBuffer(arr) #打开buffer if buf.open(QBuffer.OpenModeFlag.WriteOnly): #写入数据 data ='镜水无风也自波'.encode() buf.write(data) #关闭buffer buf.close() #写入后检查QByteArray对象的内容 print(f'数据大小:{arr.size()}') print(f'数据内容:{arr.data().decode()}')

上述代码先创建 QByteArray 实例,再传给 QBuffer 类的构造函数,随后通过 QBuffer 对象写入的内容会存储在 QByteArray 对象中。

15.5

QTextStream

QTextStream类支持以流的方式读写文本内容。该类公开了一组可设置文本格式的方法成员,并通过“<<”运算符实现快速写入字符串。

QTextStream 类的构造函数可以传入 QIODevice类型的实例引用(如 QFile、QBuffer等),或者QByteArray、bytes类型的对象引用。

15.5.1 示例:QTextStream 类的简单使用

本示例将演示QTextStream类的基本写入操作。程序会在当前工作目录下创建文本文件,然后使用QTextStream类写入3行文本,代码如下:

from PySide6.QtCore import QTextStream, QFile, Qt file = QFile("test.txt") #如果文件已存在,就删除 if file.exists(): file.remove() #打开文件 if file.open(QFile.OpenModeFlag.NewOnly | QFile.OpenModeFlag.WriteOnly): #实例化 QTextStream 对象 txtStream = QTextStream(file) #写入文本 txtStream << "这是第一行文本\n" txtStream << "这是第二行文本\n" txtStream << "这是" << "第三行" << "文本" #关闭文件 file.close()

“<<”运算符的左边是QTextStream实例,右边是要写入的文本。由于该运算符的结果也是QTextStream实例,因此可以将多个文本实例连起来写,如上述代码中的第三行文本,被分成了3个片段写入。

当示例代码执行完毕后,打开test.txt文件,其内容如下:

这是第一行文本 这是第二行文本 这是第三行文本

15.5.2 示例:字段宽度与对齐方式

QTextStream类可以设置文本字段的宽度(字符数量)和对齐方式(左对齐、居中对齐等)。对应的成员是 setFieldWidth 和 setFieldAlignment 方法。

在QTextStream类中,单次写入的文本被视为一个字段,例如:

textStream << "dog" << "fox"

上述代码进行两次写入,QTextStream类会认为写入了两个字段—“dog”和“fox”。

setFieldWidth和 setFieldAlignment方法调用后,其设定的参数只对后续写入的文本起作用,已经写入的文本不受影响。而且一旦设置后,无论后续将进行多少次写入,都会应用该设定,除非再次调用setFieldWidth 或setFieldAlignment方法重新设置宽度和对齐方式。

本示例的主要代码如下:

arr = QByteArray() #实例化 QTextStream 类 stream = QTextStream(arr, QTextStream.OpenModeFlag.WriteOnly) #第一次设定 stream.setPadChar("=") stream.setFieldWidth(20) stream.setFieldAlignment(QTextStream.FieldAlignment.AlignRight) stream << "Hello" #第二次设定 stream.setPadChar("+") stream.setFieldWidth(30) stream.setFieldAlignment(QTextStream.FieldAlignment.AlignCenter) stream << "All" #清空缓冲区并将数据写入QByteArray对象 stream.flush() #打印文本 print(arr.toStdString())

setPadChar方法用于设置填充字符,默认是空格。为了能直观地看到文本对齐后的效果,上述代码在第一次写入时使用“=”字符填充剩余空白,而第二次则是用“+”字符进行填充。

写入“Hello”时,设定的字段宽度为20(个字符),右对齐;写入“All”时设定的字段宽度为30,居中对齐。

示例程序执行后,将得到这样的文本:

===============Hello+++++++++++++All++++++++++++++

15.5.3 示例:写入不同进制的整数

QTextStream类的 setIntegerBase方法可以设置整数的进制,当写入的内容是整数时,会自动进行转换。setIntegerBase方法可用的参数值有2、8、16、10。

下面的示例先以十六进制为基础写入3个整数值,再以二进制为基础写入两个整数值。详细代码如下:

arr = QByteArray() ts = QTextStream(arr) #设置显示方式 ts.setNumberFlags( QTextStream.NumberFlag.ShowBase ) #设置为十六进制 ts.setIntegerBase(16) #写入数字 ts << 127 << ', ' << 128 << ', ' << 65 << '\n' #改为二进制 ts.setIntegerBase(2) #写入数据 ts << 15 << ', ' << 16 ts.flush() #打印字符串 print(arr.toStdString())

调用 setNumberFlags方法设置ShowBase标志后,生成的字符串会带有前缀。如十六进制的数值将带有“0x”前缀,二进制数值带有“0b”前缀。

上述代码执行后,将得到以下结果:

0x7f, 0x80, 0x41 # 十六进制 0b1111, 0b10000 # 二进制
15.6

QDataStream

QDataStream类是以二进制方式读写数据的,其适用性与兼容性都比 QTextStream类要好(QTextStream类是专为读写文本而设计的)。因此,QDataStream类几乎能够处理所有数据类型,包括Qt自身的常用类型,如QSize、QRect、QLine、QPoint等。

QDataStream 类仅负责处理数据的输入输出,它需要关联一个 QIODevice对象(如QFile)或QByteArray 对象来存储内容,这些对象可以通过 QDataStream 类的构造函数传递。也可以在创建QDataStream 实例后调用 setDevice 方法设置 QIODevice 对象。

QDataStream类可以使用表15-1所列出的方法成员进行读写操作。

表 15-1 QDataStream 类用于读写数据的常用成员
类型说明
boolreadBoolwriteBool读写布尔类型的值(True 或 False)
bytesreadByteswriteBytes读写字节序列
floatreadDoublewriteDouble读写浮点数据值,如12.0085
readFloatwriteFloat
intreadInt8writeInt8读写整数值,支持8、16、32、64位整数值,包括有符号和无符号整数
readInt16writeInt16
readInt32writeInt32
readInt64writeInt64
readUInt8writeUInt8
readUInt16writeUInt16
readUInt32writeUInt32
readUInt64writeUInt64
strreadQCharwriteQChar读写单个字符或字符串
readQStringwriteQString
readQStringListwriteQStringList
readStringwriteString
任意类型readQVariantwriteQVariant读写上述基础类型以外的其他数据

15.6.1 示例:简单的读写操作

本示例将依次向文件写入8位无符号整数、字符串、浮点数,随后将数据读出来并打印在屏幕上。完整代码如下:

file = QFile("something.bin") #如果文件已存在,则删除 if file.exists(): file.remove() #打开文件,用于写操作 if file.open(QFile.OpenModeFlag.WriteOnly): #实例化 QDataStream 对象 stream = QDataStream(file) #1.写入8位无符号整数 stream.writeUInt8(127) #2. 写入字符串 stream.writeString("马到成功") #3. 写入浮点数 stream.writeFloat(192.0627) #关闭文件 file.close() #打开文件,用于读操作 if file.open(QFile.OpenModeFlag.ReadOnly): #实例化 QDataStream 对象 stream = QDataStream(file) #1. 读出8位无符号整数 val = stream.readUInt8() print(f"Uint8: {val}") #2. 读出字符串 val = stream.readString() print(f"String: {val}") #3. 读出浮点数 val = stream.readFloat() print(f"Float: {val}") #关闭文件 file.close()

QDataStream类使用起来并不复杂,写入时调用write*方法,读取时调用read*方法。但一定要注意的是:读出数据的顺序必须要与写入时相同。例如在上述代码中,先写入的是8位无符号整数,接着是字符串,在读取时也要先读出8位无符号整数,再读字符串。

示例代码执行后,屏幕将输出以下内容:

Uint8: 127 String:马到成功 Float:192.0626983642578

浮点数值在读出来的时候,由于二进制运算产生的误差,小数部分会与写入有点差异。

15.6.2 示例:保存和恢复调色板数据

本示例将演示使用 QDataStream类读写调色板(QPalette)数据。示例窗口上有3个按钮,单击后会修改窗口背景和按钮文本的颜色。在窗口即将关闭时将调色板的数据写入文件,当窗口重新初始化或再次运行应用程序时,将从文件中读出调色板的数据,还原窗口的外观。

示例的具体实现步骤如下。

  1. 创建 DemoWindow类,派生自 QWidget类。
class DemoWindow(QWidget):
  1. 在类中定义DATA_FILE字段,表示数据文件的名称。
DATA_FILE = "pldata.bin"
  1. 在__init__方法中,检查数据文件是否存在。如果存在,读取文件并重新设置窗口的调色板。
def __init__(self): super().__init__() dataFile = QFile(DemoWindow.DATA_FILE) if dataFile.exists(): #打开文件 if dataFile.open(QFile.OpenModeFlag.ReadOnly): #实例化 QDataStream 对象 stream = QDataStream(dataFile) #创建调色板实例 palette = self.palette() #还原调色板 stream >> palette #重新设置调色板 self.setPalette(palette)

QPalette类重载了“>>”运算符,使用它可以从QDataStream对象读取调色板数据,并赋值给palette变量。

  1. 初始化窗口布局。本示例窗口包含3个按钮,可以切换3种颜色。
#布局 layout = QVBoxLayout() self.setLayout(layout) #按钮 self.btn1 = QPushButton("样式1", self) self.btn2 = QPushButton("样式2", self) self.btn3 = QPushButton("样式3", self) layout.addWidget(self.btn1) layout.addWidget(self.btn2) layout.addWidget(self.btn3) layout.addStretch(1) #连接按钮的clicked信号 self.btn1.clicked.connect(self.onStyle1) self.btn2.clicked.connect(self.onStyle2) self.btn3.clicked.connect(self.onStyle3)
  1. 实现onStyle1等3个方法。这3个方法的代码逻辑是相同的,区别在于为窗口背景和按钮文本设置不同的颜色。
def onStyle1(self): p = self.palette() p.setColor(QPalette.ColorRole.Window, QColor("green")) p.setColor(QPalette.ColorRole.ButtonText, QColor("darkblue")) self.setPalette(p) def onStyle2(self): p = self.palette() p.setColor(QPalette.ColorRole.Window, QColor("blue")) p.setColor(QPalette.ColorRole.ButtonText, QColor("purple")) self.setPalette(p) def onStyle3(self): p = self.palette() p.setColor(QPalette.ColorRole.Window, QColor("gray")) p.setColor(QPalette.ColorRole.ButtonText, QColor("black")) self.setPalette(p)
  1. 重写closeEvent方法,在窗口即将关闭时,把调色板数据写入文件。
def closeEvent(self, event: QCloseEvent): #保存调色板数据 p = self.palette() file = QFile(DemoWindow.DATA_FILE) #如果文件存在,将其删除 if file.exists(): file.remove() #打开文件 if file.open(QFile.OpenModeFlag.WriteOnly): #实例化 QDataStream 对象 stream = QDataStream(file) #写入数据 stream << p #关闭文件 file.close() #调用基类成员 super().closeEvent(event)

QSataStream类重载了“<<”运算符,可用于写入数据。在写入文件后,应该调用基类(QWidget)的closeEvent方法,以便窗口在关闭前执行一行默认操作。

运行示例程序,在窗口上随机单击一个按钮,改变调色板的参数,然后关闭应用程序。重新运行示例程序,此时会看到窗口已将调色板还原至关闭前的状态,如图15-5所示。

图 15-5
图15-5 重启应用程序后将自动还原调色板
图15-5 重启应用程序后将自动还原调色板