本文由 简悦 SimpRead 转码, 原文地址 www.zhihu.com 韦易笑
早先看一堆人说 PyQt 打包麻烦,部署困难的,打出来的包大(几十兆起步),而且启动贼慢,其实 Python+PyQt 打包非常容易,根本不需要用什么 PyInstaller,我手工打包出来的纯 Python 环境只有 5MB,加上 PyQt 也才 14MB。
很多人用 PyInstaller 喜欢加一个 -F 参数,打包成一个单文件:
这样的单文件看起来似乎很爽,其实他们不知道,这其实是一个自解压程序,每次运行时需要把自己解压到 temp 目录,然后再去用实际的方式运行一遍解压出来的东西:
Process Explorer 把雷达图标拖动到 pyqt_hello.exe 的窗口上,可以看到有两个 pyqt_hello.exe 的文件,外面那个是你打包出来的,里面那个才是真正的程序(虽然可执行都是一个),看看它下面依赖的 python310.dll 是在哪里?这不就是一个临时解压出来的目录么:
看到没?这就是你 PyInstaller 打包出来的 30MB 的程序,每次运行都要临时解压出 71MB 的文件,运行完又删除了,那么如果打包出来的可执行有 100MB,每次运行都要释放出 200-300 MB 的东西出来,所以为什么 PyInstaller 出来的单文件运行那么慢的原因除了每次要解压外,还有杀毒软件碰到新的二进制都要扫描一遍,你每次新增一堆 .dll , .pyd, .exe,每次都要扫描,不慢可能么?
其实 PyInstaller 如果不打包成单文件可执行(-F
参数),用起来问题不大,唯一不足有两个,首先是很多动态库其实我没用比如上面的 _socket,_ ssl, QtQuick 等,但都被打包的时候打进去了,大小会偏大;其次是目录看起来很乱,上百个文件一个目录,找主程序都找不到。
正确的打包姿势
当然是手工打包,现在 Python 3.5 以后,官方都会发布一个嵌入式 Python 包:
链接在这里:Python Release Python 3.8.10
现在不是都到 Python 3.10 了么,为什么选择 3.8 ? 因为 3.8 是最后一个支持 Win7 的版本,3.9 以后就不支持了。那么为什么选择 32 位?因为打包出来 32 位是最紧凑的,64 位会大很多,除非你要一次性在内存里 load 2GB 以上的数据,否则基本就选择 32 位的。
这个 32 位的包很小:
本身也只有 7MB,解压出来是一些必要的文件:
那么你在项目路径里建立一个新的 runtime
文件夹,把这些文件放进去,外层写个批处理,调用一下里面的 python.exe 基本就可以跑个命令行的程序了。当然这样看起来很原始,所以精细一点的话,为这个 embedded python 做一个壳,直接加载里面 python3.dll 或者 python38.dll 来运行程序。
嵌入式 Python 加壳
上面说的加壳我写了个例子了,叫做 PyStand:
https://github.com/skywind3000/PyStand
到 Release 下载下来是这样:
选择第一个 PyStand-py38-pyqt5-lite 这个包,下载下来 14MB,解压后:
目录非常清爽,比 PyInstaller 非单文件那种上百个 dll 的目录干净多了,就几个文件:
- runtime:之前官方包 embedded python 解压后的内容。
- site-packages:第三方依赖
- PyStand.exe:主程序入口。
- http://PyStand.int:脚本入口。
这个 PyStand.exe 就是可以直接运行的程序,双击:
运行成功,打包文件只有 14MB,就能跑一个完整 PyQt5 的项目了,比 PyInstaller 的 30MB 小不少,你就是解压开也才 40MB,比 70MB 的 PyInstaller 解压后大小精简很多。
主要代码就是写在 PyStand.int 里,这个 PyStand.exe 启动后会自动加载同名的 .int 文件:
import sys, os
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
app = QApplication([])
win = QWidget()
win.setWindowTitle('PyStand')
layout = QVBoxLayout()
label = QLabel('Hello, World !!')
label.setAlignment(Qt.AlignCenter)
layout.addWidget(label)
btn = QPushButton(text = 'PUSH ME')
layout.addWidget(btn)
win.setLayout(layout)
win.resize(400, 300)
btn.clicked.connect(lambda : [
print('exit'),
sys.exit(0),
])
win.show()
app.exec_()
也就是说可执行叫做 PyStand.exe ,它会加载 http://PyStand.int;如果改名叫做 MyDemo.exe 它就会加载 MyDemo.int 里的代码。
那么其实大家可以直接使用了,把程序名字改成你想要的编写对应的 .int 即可,换图标的话,可以自己重新编译 PyStand 项目,或者直接 Resource Hacker 更换图标:
根本不需要配置 C/C++ 编译环境。
安装依赖
我们需要一个对应版本号的 32 位的完整 python 3.8,然后新建个干净的虚拟环境:
\path\to\py38\python.exe venv test
生成 test 目录里,大概目录是这样:
用 cmd.exe 进入 Scripts 目录,运行 activate
后,用 pip 安装你需要的包,然后到上面虚拟环境的 Lib/site-packages
里,把你需要的包找出来:
拷贝到 PyStand.exe 所在目录的 site-packages 里面即可使用,注意多余的,没有依赖的东西无需拷贝,比如上图的 pip 包。
裁剪依赖
现在你已经把依赖的包拷贝到 PyStand 的 site-packages 里了,比如 PyQt5 这个包:
进去看一眼,把你不要的模块全部删除,什么 OpenGL,AxContainer,Multimedia,Position, Location, RemoteObject, DBus, QtQuick, QtWebEngine 之类的,不确定的可以删除了运行下你的程序测试下行不行,不行的又拷贝回来。
然后继续进入上图 Qt5 的目录里的 bin 目录:
这里有很多尺寸很大的模块,继续删除多余的,什么 QML,Test,Help,OpenGL ,GLES,Sensor, Bluetooth 之类的全部干掉,再到上层:
接着精简 plugins 目录里不要的东西,删除 qml/qsci, 然后到 translations 里把不要的语言删除掉,这么一圈下来,整个目录从原来的 134MB:
精简到 46.8 MB:
比你 PyInstaller 解压出来的 70MB 小了一大半,打包出来就是 14MB:
我已经帮你裁剪好了,你可以直接使用,还有好多可以根据你程序的需要进行裁剪,比如 openssl,sqlite 这些都非常大,总之还有很多压缩空间,按照你的程序需要还可以二次裁剪。
手工裁剪比无脑 PyInstaller 可靠的多,不但可以精细裁剪,每一步你都清晰的知道是怎么来的,出了问题你也知道该怎么回退。
还可以用 upx 压缩一些比较大的文件,但 runtime 下面的 python3.dll, python38.dll, vcruntime140.dll 不能压缩,而 PyQt5 里的 QtCore, QtWidgets, QtGUI 不能压缩,一边测试一边压缩,还可以进一步精简。
代码组织
你有很多 py 代码,可以在 PyStand 下面新建一个 script 目录:
在里面放一个 main.py,实现一个 main 方法,然后改写 http://PyStand.int:
import sys, os
os.chdir(os.path.dirname(__file__))
sys.path.append(os.path.abspath('script'))
sys.path.append(os.path.abspath('script.egg'))
import main
main.main()
这个代码就是做了三件事情:矫正当前运行目录,设置 sys.path
,然后导入 main 模块并执行 main 方法。注意后面 sys.path 里追加了一个 script.egg
,意思是你调试好了,发布时把 script 目录里面的代码或者 pyc 压缩成要给 zip 文件,叫做 script.egg 放在 PyStand 那里删除 script 目录即可:
发布出来大概是这样,运行 PyStand.exe 成功的 import 到了 main.main() 函数:
主目录下面就三个文件,打包放到其他机器上解压就运行,不喜欢 PyStand.exe 这个名字可以随便改,同时修改 PyStand.int 的名字即可:
比如这样,运行 PyQt-Demo.exe 它会根据自身的名字,正确的找到 PyQt-Demo.int 文件并执行。
基础加密
要求不高的话,上面你将 script 目录内的 .py 文件打包成 script.egg,直接就可以发布了,至少不会满目录的 .py 文件。要求高一点的话,把 .py 先转换成 .pyc 再压缩成 script.egg,然后把关键几个模块用 cython 之类的工具转换成 .pyd 即可。
上面基础加密基本够用了,个人开发者可以就此止步,如果你是一个团队,要发布面向百万以上用户产品级的东西,追求比 PyInstaller 更安全的加密方式可以继续往下。
高级加密
接下来技巧我在 Python 2 时代都做过,你可以视精力酌情添加:
第一层:pyc 加密,自己写一个 importer,放到 PyStand.int 里初始化,作用是加载自定义的 .pz
文件,而 .pz
文件是根据 .pyo
文件加密得到,你的 importer 负责解密并加载这些字节码,把这个 importer 添加到 sys.path_hooks 里面,这样 python 就能 import .pz
文件了,再写个批处理,把项目文件全部编译转换成 .pz
压缩成 script.egg。
第二层:zip 文件加密码,参考 python 自带的 zipimporter 实现一个 zipimporter2,支持 zip 文件加密码,只要在 sys.path.append('script.egg@12345') 类似这样的路径,就可以按给定密码 import zip 内的东西,当然密码可以写的不那么明显,还可以支持 7zip 导入。
第三层:将 .dll/.pyd 封装近 python38.zip 或者 script.egg 内,这里你会用到 py2exe 的两个子模块:MemoryModule:
地址:https://github.com/py2exe/py2exe/tree/master/source
可以用来从内存加载 dll/pyd,然后还有一个 zipdllimporter 的脚本,可以从内存 / zip 文件直接加载 pyd/dll,这样你的所有的 pyd/dll 都可以塞到 .zip/.egg 文件里了,根本不用暴露。
第四层:源代码重新编译 Python,将很多东西直接编译进去,比如上面说的各种 importer 实现,memory importer,加密 zip 文件之类的,并且支持加密的 http://PyStand.int。
第五层:修改字节码,找到 python 源代码的 include/opcode.h:
自己魔改一遍,基本反编译的程序都蒙圈了,再到 Include/internal/pycore_ast.h 下面修改一些结构体的内部顺序,这样只要对方没有你的头文件,想从进程内存级别 intercept 进来获取字节码或者 ast 的都会非常麻烦。
第六层:静态编译,把所有第三方库和 python 自己静态编译成一个 exe 或者 dll,没有任何依赖,不暴露任何 dll 接口,集成上面说过的所有功能。由于你全部依赖都静态编译了,所以可以给 PyObject 里加两个无关的成员,调整一下已有成员顺序,别人就是进程截取 PyObject 的指针,由于没有头文件,内部结构不知道,所以它也没有任何办法。
第七层:你的可执行每次启动会检测可执行文件末尾,是否有添加的内容,如果有,把他视为一个加密的压缩包,在内存里解密并 import 对应模块,这样你上面的单个程序就可以和具体逻辑相分离,有了新的逻辑代码,压缩加密后添加在唯一可执行尾部即可。
。。。。。
还有很多类似的方法,这里仅仅抛砖引玉,没有绝对的安全,就是看你愿意投入多少人力,可以做到哪一层,个人的话,上面基础加密足够了,公司团队的话,安排人搞个一两个月,基本也就搞定了。
错误调试
这个 PyStand.exe 是窗口程序,那么出错了怎么看 exception 呢?可以打开一个 cmd.exe,用 cmd.exe 启动 PyStand,就能看到错误了,你自己也可以记录下日志,catch 一下内部的 exception。
--
更多阅读:为什么很多 Python 开发者写 GUI 不用 Tkinter,而要选择 PyQt 和 wxPython 或其他?
知乎用户
之前追求极致 exe 启动速度, 用 py2exe, 独立打包后, 启动 EXE 飞快, 应该是内存虚拟解压, 不像 pyinstaller 文件方式解压到 temp. 但最近买了代码签名证书后发现 py2exe 的 exe 加了签名后会无法启动, 老外论坛说签名会破坏 exe 的 MD5. 只能再次用 pyisntaller. 无意间发现有 enigma 这种软件, 也是内存虚拟映射解压, 速度飞起, 还支持代码签名. 唯一缺点就是压缩比还是有点低, 比 rar 同等压缩要大 20% 左右.
我感觉这种方式是挺好的, 能完美保留开发环境配置, 不用刻意去精简什么. 速度还快, 稳定最重要.
还有一点:
互联网产品一般还有安装器, 卸载器, 更新器的需求, 而这三个完全都能通过自身复制独立 exe 和独立 DLL, 让安装包缩小三倍.
你不认识我
我业余写 python 玩,最近写了个小工具,同事们也想要用,总不能让他们也下载 python 然后安装各种包吧,所以需要进行打包。
先参考了韦神的帖子:https://www.zhihu.com/question/48776632/answer/2336654649
他这种手动打包的方式我很喜欢,但他自己写了个程序,作为外壳进行程序的加载。我这儿不需要加密,也希望能够深入的做一些了解,就自己做了一些工作。这儿做个记录,也给大家一个参考。
第一步:下载嵌入式 python,并解压。 注意,这个 python 的版本和架构必须和你用的版本是一模一样的。
第二步:建立如下目录结构
- 主目录名称:test
- 主目录下面存放自己写的程序,main.py 和 other.py
- 主目录下建立一个子目录:runtime,并将解压后的嵌入式 python 拷贝到这个目录下面。
- runtime 下建立子目录 Lib,用来存放第三方库。
第三步:把普通 python 下的库直接拷贝到嵌入式 python 的 Lib 目录下。
普通 python 的第三方库在 \ Python39\Lib\site-packages 目录下面。再次申明,两个 python 的架构和版本必须一致。
第四步:关键步骤,编辑 python39._pth
嵌入式 python 下载并解压之后,里面有个很重要的文件:python39._pth,(你的文件名因为 python 版本的问题可能和我不同)这个文件规定了嵌入式 python 加载库文件的路径。打开可以看到其内容:
python39.zip
.
# Uncomment to run site.main() automatically
#import site
- 第一行:python39.zip 的意思是从这个压缩文件里面加载库,我们打开这个压缩文件可以看到这个压缩文件下面就是各种标准库。
- 第二行:当前目录
- 后面都是注释,不用关注
我们需要增加两行
- 增加 runtime 下的 Lib 目录,用于加载第三方库。
- 增加主目录,用于加载自己的程序,比如 other.py 之类
此时,此文件内容如下
python39.zip
.
Lib
..\ #主目录
# Uncomment to run site.main() automatically
#import site
第五步:建立一个 go.bat 文件,用来进行启动。
在主目录 test 下,建立一个 go.bat,其内容很简单,就一行。
.\runtime\python.exe main.py
到此,就基本成功了,但我碰到了几个问题,有知道的请评论区解答。
- 理想情况下,如果能把第三方库都打包成 zip,那可以大大减小打包文件的大小,我这样做了,也把路径加到了 pth 文件里面。但是出错了,不能加载 wxpython。我不清楚是 wxpython 的问题还是其他问题造成的。
- 我的程序里面用了 prettytable,我把它拷贝到 lib 目录下之后,加载失败,还需要把另外一个目录 “prettytable-3.2.0.dist-info” 也加进去才行。Jessi
可以看看这个:
python 如何连同依赖打包发布以及 python 的构建工具? - Python
可能打包依赖文件会方便点。
峰哥 python 笔记
布局管理是指我们在应用程序窗口上放置小部件的方式。我们可以使用绝对定位或布局类来放置小部件。使用布局管理器来管理布局是组织小部件的首选方式。
绝对定位
程序员指定每个小部件的位置和大小(以像素为单位)。当使用绝对定位时,我们需要理解以下限制:
- 如果调整窗口大小,小部件的大小和位置不会改变。
- 应用程序在不同平台上可能显示不同。
- 更改应用程序中的字体可能破坏布局。
- 如果决定更改布局,必须完全重新设计布局,这是繁琐且耗时的工作。 以下示例在绝对坐标中放置小部件。
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
self.setWindowTitle("桥然:布局测试")
self.resize(600, 400)
self.setWindowIcon(PyQt6.QtGui.QIcon("logo.jpg"))
def initUI(self):
lbl1 = QLabel('桥然:', self)
lbl1.move(150, 60)
lbl2 = QLabel('tutorials', self)
lbl2.move(350, 80)
lbl3 = QLabel('for programmers', self)
lbl3.move(450, 100)
self.setGeometry(300, 300, 350, 250)
self.setWindowTitle('Absolute')
self.show()
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec())
if __name__ == '__main__':
main()
我们使用 move
方法来定位我们的小部件。在我们的例子中,这些是标签。我们通过提供 x 和 y 坐标来定位它们。坐标系统的起点位于左上角。x
值从左到右增长。y
值从上到下增长。
PyQt6 QHBoxLayout 和 QVBoxLayout
QHBoxLayout
和 QVBoxLayout
是 PyQt6
中常用的布局管理器类,用于在应用程序中水平和垂直地排列小部件。
QHBoxLayout
(水平布局)将小部件水平排列,从左到右摆放,可以根据需要添加伸缩因子。QVBoxLayout
(垂直布局)将小部件垂直排列,从上到下摆放,同样可以根据需要添加伸缩因子。
这两个布局管理器类的用法非常类似,以下是它们的一般用法和常用参数:
QHBoxLayout 的用法:
layout = QHBoxLayout()
QVBoxLayout 的用法:
layout = QVBoxLayout()
使用以上代码,我们可以创建一个空的水平布局或垂直布局对象。接下来,我们可以通过以下方法将小部件添加到布局中:
-
addWidget(widget, stretch=0, alignment=Qt.Alignment)
-
widget
:要添加的小部件对象。 -
[stretch](https://www.zhihu.com/search?q=stretch&search_source=Entity&hybrid_search_source=Entity&hybrid_search_extra=%7B%22sourceType%22%3A%22answer%22%2C%22sourceId%22%3A3334746121%7D)
:可选参数,指定伸缩因子,默认为 0。伸缩因子决定了小部件在布局中的占用空间比例。 -
alignment
:可选参数,指定对齐方式,默认为 Qt.Alignment。用于控制小部件在布局中的对齐方式。 例如,添加一个按钮到水平布局中:
button = QPushButton("Button")
layout.addWidget(button)
还可以添加多个小部件,并使用伸缩因子进行布局控制:
button1 = QPushButton("Button 1")
button2 = QPushButton("Button 2")
layout.addWidget(button1, 1) # 伸缩因子为1
layout.addWidget(button2, 2) # 伸缩因子为2
在以上示例中,button1 将占据 1/3 的水平空间,button2 将占据 2/3 的水平空间。
除了 addWidget 方法之外,还可以使用其他方法对布局进行进一步设置和操作,例如:
addLayout(layout, stretch=0)
:添加另一个布局对象到当前布局中。 addSpacing(spacing)
:添加指定的间距(以像素为单位)。 addStretch(stretch=0)
:添加伸缩因子,用于平均分配剩余空间。 setSpacing(spacing)
:设置小部件之间的间距(以像素为单位)。 以上是 QHBoxLayout
和 QVBoxLayout
的基本用法和常用参数。根据实际需求,你可以根据文档进一步了解它们的更多方法和属性。
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
self.setWindowTitle("桥然:布局测试")
self.resize(600, 400)
self.setWindowIcon(PyQt6.QtGui.QIcon("logo.jpg"))
def initUI(self):
okButton = QPushButton("OK")
cancelButton = QPushButton("Cancel")
hbox = QHBoxLayout()
hbox.addStretch(1)
hbox.addWidget(okButton)
hbox.addWidget(cancelButton)
vbox = QVBoxLayout()
vbox.addStretch(1)
vbox.addLayout(hbox)
self.setLayout(vbox)
self.setGeometry(300, 300, 350, 250)
self.setWindowTitle('Buttons')
self.show()
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec())
if __name__ == '__main__':
main()
定义了一个名为 Example
的类,继承自 QWidget
类,用于创建窗口应用程序。
在 Example
类的__init__
方法中,调用了父类的构造函数,并初始化了窗口的 UI 界面。
initUI
方法用于初始化用户界面,包括创建按钮、设置布局和添加按钮到布局中。
首先,创建了两个按钮对象:okButton
和 cancelButton
。
然后,创建一个水平布局对象 hbox
,通过 addStretch
方法添加一个弹性空间,使按钮能够水平居中显示,并使用 addWidget
方法将两个按钮添加到水平布局中。
接下来,创建一个垂直布局对象 vbox
,通过 addStretch
方法添加一个弹性空间,使按钮能够垂直居中显示,并使用 addLayout
方法将水平布局添加到垂直布局中。
最后,通过 setLayout
方法将垂直布局设置为窗口的布局。
在 main
函数中,创建了一个 QApplication 对象 app
,实例化了 Example
类,并通过 sys.exit(app.exec())
进入应用程序的主循环。
运行该代码,会显示一个窗口,窗口中包含 "OK" 和 "Cancel" 两个按钮,按钮水平居中显示,同时垂直居中于窗口。
QGridLayout 案例
QGridLayout
是 PyQt6
中的一个布局类,它将窗口 / 对话框分割为行和列,我们可以指定控件的位置和跨越的行列数。
创建 QGridLayout
对象的基本语法如下:
layout=QGridLayout()
一旦创建了 QGridLayout
对象,就可以使用 addWidget()
方法将控件添加到网格中。addWidget()
方法的参数是要添加的控件和控件的行号和列号。例如:
layout.addWidget(button,0,0)
这将 button 控件添加到网格的第 0
行和第 0
列。
如果你想让一个控件跨越多行或多列,可以在 addWidget()
方法中添加两个额外的参数,分别表示控件所跨越的行数和列数。例如:
layout.addWidget(textEdit,1,1,3,3)
这将 textEdit
控件添加到网格的第 1
行和第 1
列,且 textEdit
控件跨越了 3
行和 3
列。
QGridLayout 还有一个 setSpacing()
方法,用于设置网格中的单元格之间的间距。例如:
layout.setSpacing(10)
这将设置网格中的单元格之间的间距为 10 像素。
QGridLayout
会尽可能平均地分配空间给每个控件,但是如果某个控件需要更多的空间(例如,因为它的内容较多),那么 QGridLayout
会自动调整空间分配。
最后,可以使用 setLayout()
方法将 QGridLayout
对象设置为窗口或对话框的布局。例如:
self.setLayout(layout)
这将设置窗口的布局为 layout
。
计算器
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
self.setWindowTitle("桥然:Calculator")
self.resize(300, 200)
self.setWindowIcon(PyQt6.QtGui.QIcon("logo.jpg"))
def initUI(self):
grid = QGridLayout()
self.setLayout(grid)
names = ['Cls', 'Bck', '', 'Close',
'7', '8', '9', '/',
'4', '5', '6', '*',
'1', '2', '3', '-',
'0', '.', '=', '+']
positions = [(i, j) for i in range(5) for j in range(4)]
for position, name in zip(positions, names):
if name == '':
continue
button = QPushButton(name)
grid.addWidget(button, *position)
self.move(300, 150)
self.setWindowTitle('Calculator')
self.show()
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec())
if __name__ == '__main__':
main()
意见反馈表
import sys,PyQt6
from PyQt6.QtWidgets import (QWidget, QLabel, QLineEdit,
QTextEdit, QGridLayout, QApplication)
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
self.setWindowTitle("桥然:GridLayout布局测试")
self.resize(600, 400)
self.setWindowIcon(PyQt6.QtGui.QIcon("logo.jpg"))
def initUI(self):
title = QLabel('Title')
author = QLabel('Author')
review = QLabel('Review')
titleEdit = QLineEdit()
authorEdit = QLineEdit()
reviewEdit = QTextEdit()
grid = QGridLayout()
grid.setSpacing(10)
grid.addWidget(title, 1, 0)
grid.addWidget(titleEdit, 1, 1)
grid.addWidget(author, 2, 0)
grid.addWidget(authorEdit, 2, 1)
grid.addWidget(review, 3, 0)
grid.addWidget(reviewEdit, 3, 1, 5, 1)
self.setLayout(grid)
self.setGeometry(300, 300, 350, 300)
#self.setWindowTitle('Review')
self.show()
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec())
if __name__ == '__main__':
main()
使用 PyQt6 创建一个简单的 GUI 应用程序,该应用程序具有一个网格布局(QGridLayout)和一些基本的控件(标签、文本编辑框和文本输入框)。
-
从 PyQt6.QtWidgets 模块中导入了必要的类。
-
定义了一个名为 Example 的类,它继承自 QWidget 类。QWidget 类是所有用户界面对象的基类。
-
在
Example
类的构造函数中,调用了父类的构造函数,并调用了 initUI 方法来初始化用户界面。 -
在
initUI
方法中,首先创建了一些QLabel
、QLineEdit
和QTextEdit
对象。 -
创建了一个 QGridLayout 对象,并设置了网格间的间距。
-
使用 addWidget 方法将控件添加到网格布局中。addWidget 方法的前两个参数是要添加的控件和控件所在的行号。第三个参数是控件所在的列号。最后两个参数(可选)是控件所占的行数和列数。
-
将网格布局设置为窗口的布局。
-
设置了窗口的几何形状(位置和大小)、标题,并显示了窗口。
-
在 main 函数中,创建了一个 QApplication 对象和一个 Example 对象,并进入了应用程序的主循环。
当运行这个脚本时,如果它是作为主脚本运行的(也就是说,它不是被其他脚本导入的),那么就会调用 main 函数。
上面代码简单修改,就可以实现发送邮件的功能。在原来的基础上,填选择附件,发送的按钮,余下就是把发邮件的功能与相应按钮绑定。
小修改
修改后的界面如下:
标题、收件人,可以手动输入,也可以加载时,直接自动填加,只需要选择附件,实现特定的邮件发送任务。
修改过程:
加载程序时,会把预先设置好的邮件标题,收件人,编写好的邮件内容,都填到相应的位置,加载程序后,可以给邮件标题,邮件内容进行修改。点击发送邮件时,读取标题的 QEditLine 的内容,做为邮件的标题,内容部分的内容,转化成 html 格式,作为邮件的内容。读取附件内容,作为信息的附件。
加载程序时,读取本地的 smtp 的授权码 (密码) 和收件人列表。
代码部分
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
self.setWindowTitle("桥然:GridLayout布局测试--发送邮件")
self.resize(600, 400)
self.setWindowIcon(PyQt6.QtGui.QIcon("logo.jpg"))
#########################
with open("pwd.ini",mode='r') as f:
self.pwd = f.readline()
print(self.pwd)
df = pd.read_csv('smtp.ini' , sep = ',' , parse_dates=['email'])
self.emails = []
for i in range(0,df.shape[0]):
ss = df['email'][i]
self.emails.append(ss)
my_string = ','.join(self.emails)
self.authorEdit.setText(my_string)
self.smtp_server = 'smtp.163.com'
self.from_addr = '1****@163.com'
html_msg = """
<p>尊敬的领导:</p>
<p>这是黑龙江省************专报,请查收!</p>
"""
# 使用正则表达式去除HTML标签
plain_text = re.sub('<.*?>', '', html_msg)
print(plain_text.strip())
self.reviewEdit.setText(plain_text)
self.titleEdit.setText("黑龙江省***********专报")
############################################
def initUI(self):
title = QLabel('标题')
author = QLabel('收件人')
review = QLabel('内容')
fujian = QLabel('附件路径')
self.titleEdit = QLineEdit() #标题
self.authorEdit = QLineEdit() #收件信箱
self.reviewEdit = QTextEdit() #内容
self.fujianEdit = QLineEdit() #附件路径
grid = QGridLayout()
grid.setSpacing(10)
grid.addWidget(title, 1, 0)
grid.addWidget(self.titleEdit, 1, 1)
grid.addWidget(author, 2, 0)
grid.addWidget(self.authorEdit, 2, 1)
grid.addWidget(fujian, 3, 0)
grid.addWidget(self.fujianEdit, 3, 1)
grid.addWidget(review, 4, 0)
grid.addWidget(self.reviewEdit, 4, 1, 5, 1)
sent = QPushButton("发送邮件")
sent.clicked.connect(self.sent)
grid.addWidget(sent, 9, 1)
select = QPushButton("选择文件")
select.clicked.connect(self.select)
grid.addWidget(select, 9, 0)
self.setLayout(grid)
self.setGeometry(300, 300, 350, 300)
#self.setWindowTitle('Review')
self.show()
def sent(self):
html_msg = self.reviewEdit.toHtml()
print(html_msg)
# 创建一个带附件的实例msg
msg = MIMEMultipart()
msg['From'] = Header("黑龙江******<1********@163.com>", 'utf-8') # 发送者
msg['To'] = Header('********', 'utf-8').encode() # 接收者
subject = self.titleEdit.text()
msg['Subject'] = Header(subject, 'utf-8') # 邮件主题
# 邮件正文内容
msg.attach(MIMEText(html_msg, 'html', 'utf-8'))
att = MIMEBase('application', 'octet-stream')
att.set_payload(open(self.fujianEdit.text(), 'rb').read())
ss = self.fujianEdit.text()
filename = ss.split("/")[-1]
att.add_header('Content-Disposition', 'attachment', filename=('gbk', '', filename) )
encoders.encode_base64(att)
msg.attach(att)
try:
smtpobj = smtplib.SMTP_SSL(self.smtp_server)
smtpobj.connect(self.smtp_server, 465) # 建立连接--qq邮箱服务和端口号
smtpobj.login(self.from_addr, self.pwd) # 登录--发送者账号和口令
smtpobj.sendmail(self.from_addr, self.emails, msg.as_string())
print("邮件发送成功")
time.sleep(30)
except smtplib.SMTPException as e:
print(f"邮件发送失败,原因:{e}")
def select(self):
options = QFileDialog.Option(QFileDialog.Option.ReadOnly)
file_dialog = QFileDialog()
file_dialog.setOptions(options)
file_dialog.setFileMode(QFileDialog.FileMode.ExistingFiles)
file_dialog.setNameFilter("Documents files (*.doc *.docx)")
if file_dialog.exec() == QFileDialog.DialogCode.Accepted:
file_paths = file_dialog.selectedFiles()
self.fujianEdit.setText(file_paths[0])
#fileName, _ = QFileDialog.getOpenFileName(self,"QFileDialog.getOpenFileName()", "","All Files (*);;Python Files (*.py)", options=options)
#if fileName:
# self.fujianEdit.setText(fileName)
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec())
if __name__ == '__main__':
main()
技术细节
这段代码是一个使用 PyQt6 编写的简单的邮件发送应用程序。以下是对代码中主要功能和关键技术细节的解析:
- 导入所需的模块:
-
sys:系统相关的功能。
-
PyQt6:PyQt6 模块。
-
其他相关模块和类,如 QWidget、QLabel、QLineEdit、QTextEdit、QGridLayout、QApplication、QPushButton、QFileDialog 等。
-
导入 smtplib 和 email 库:
-
smtplib:用于 SMTP 邮件发送。
-
email.mime.text:用于构建纯文本邮件内容。
-
email.mime.multipart:用于构建多部分邮件内容。
-
email.header:用于构建邮件头部信息。
-
email.encoders:用于编码附件数据。
-
创建 Example 类,继承自 QWidget:
-
在
__init__
方法中初始化 UI 界面,包括设置窗口标题、图标、大小等,读取配置文件和数据,并将数据填充到界面的对应部件上。 -
实现 initUI 方法:
-
创建标签和输入框部件,并使用 QGridLayout 布局管理器进行布局。
-
实现 sent 方法:
-
获取用户在界面上填写的邮件标题、收件人、内容和附件路径等信息。
-
使用 MIMEMultipart 创建一个带附件的邮件对象,并设置发件人、收件人、主题和正文内容。
-
将附件添加到邮件对象中。
-
连接 SMTP 服务器,登录发送者账号,发送邮件。
-
实现 select 方法:
-
创建 QFileDialog 对话框,允许用户选择附件文件。
-
获取用户选择的附件文件路径,并将路径填充到界面上对应的输入框中。
-
创建 QApplication 实例,实例化 Example 类,并运行应用程序。
关键技术细节包括:
- 使用 PyQt6 创建 GUI 界面,并使用布局管理器(QGridLayout)来安排小部件的位置。
- 使用 smtplib 库和 SMTP 协议发送邮件。
- 使用 email 库构建邮件内容,包括邮件头部信息、纯文本或 HTML 格式的邮件正文、带附件的邮件等。
- 通过 QFileDialog 选择文件,并将选择的文件路径填充到输入框中。
该应用程序的主要功能是允许用户填写邮件标题、收件人、内容和附件路径,并发送邮件。使用了 PyQt6 进行界面设计和布局管理,以及 smtplib 和 email 库来处理邮件发送和构建邮件内容。
存在的问题
可能邮件头的某些编码设置有问题,通过此方法发送的邮件,发送者信息不能正常显示,打开邮件,收件人部位也显示乱码。目前还不知道怎么解决。这个名称问题已解决,后续的文章会介绍方法。
Comments | NOTHING