主要目的:搞清楚使用pyside6-Qt Designer(widgets) 设计gui的从0到1的大致思路。
Python, Qt Quick - MODERN GUI / FLAT STYLE / BANK CONCEPT - EDIT [ FREE ON GITHUB NOW ]
软件架构说明
1、使用pycharm下载pyside6, 安装pyside6 package
python3.9
+ pyside6
开发环境python3.9
虚拟环境 venv_pyside6
创建
PyCharm
命令终端中,输入: conda create venv_pyside6 python==3.9
conda activate venv_pyside6
; 使用pycharm ide需要手动切换python解释器为此虚拟环境conda env list
; PyCharm
切换命令终端为 Command Prompt
, 可以看到括号内的环境为刚切换的虚拟环境。Command Prompt
中,安装库
pyside6
, 输入: pip install pyside6
pyinstaller
, 输入: pip install pyinstaller
main.py
:启动程序qt_core.py
:qt库导入管理程序gui folder
:UI设计文件存放处
windows folder
:窗口设计
main_window
:主窗口设计文件夹
ui_main_window.py
: 用代码拼出UI主界面PyCharm
自带的创建py文件自动加抬头注释。# ///////////////////////////////////////////////////////////////
#
# BY: Author name
# PROJECT MADE WITH: Qt Designer and PySide6
# V: 1.0.0
#
# This project can be used freely for all uses, as long as they maintain the
# respective credits only in the Python scripts, any information in the visual
# interface (GUI) can be modified without any implication.
#
# There are limitations on Qt licenses if you want to use your products
# commercially, I recommend reading them on the official website:
# https://doc.qt.io/qtforpython/licenses.html
#
# 这个项目可以被所有用户自由使用,只要他们只在Python脚本中保留各自的原作者信息,
# 可视界面(GUI)中的任何信息都可以被修改而不需要任何暗示。
# 如果你想在商业上使用你的产品,Qt许可证是有限制的,我建议你在官方网站上阅读它们
# ///////////////////////////////////////////////////////////////
pyside6
运行的最小框架qt_core.py
:增加其他程序需要调用qt库的所有功能。from PySide6.QtCore import *
from PySide6.QtGui import *
from PySide6.QtWidgets import *
main.py
: app 主程序,创建MainWindow
类,继承自QMainWindow
。# IMPORT MODULES
import sys
import os
# IMPORT QT CORE
from qt_core import *
# MAIN WINDOW
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Python3.9 PySide6 Learning")
# SHOW APP
self.show()
if __name__ == "__main__":
app = QApplication(sys.argv)
app.setWindowIcon(QIcon("icon.ico"))
window = MainWindow()
# window.show() # 展示我们的app 窗口,或者在MainWindow()构造函数中写self.show()
sys.exit(app.exec())
4.工程文件结构优化
ui_main_window.py
,定义UI_MainWindow
类,并且定义setup_ui
方法。在setup_ui
方法中在使用pyside6创建的UI界面类pyside6
创建&layout一系列控件)或复制来自 Qt Designer
自动生成的UI界面代码# IMPORT QT CORE
from qt_core import *
# MAIN WINDOW
class UI_MainWindow(object):
def setup_ui(self, parent):
'''parent: 一般由 main.py文件中MainWindow类的实例self变量作为父类,目的是UI_MainWindow实例做mai文件中的UI界面。
通常,在main.py,这样调用setup_ui方法
# SETUP MAIN WINDOW
self.ui = UI_MainWindow()
self.ui.setup_ui(self)
'''
if not parent.objectName(): # 此判断,经测试,一定会进入
parent.setObjectName("MainWindow") #TODO(wangjh-xm): 暂时不清楚setObjectName的作用效果。
# print('test')
# SET INITIAL PARAMETERS
# ///////////////////////////////////////////////////////////////
parent.resize(1200, 720) # 重新设启动时的界面大小
parent.setMinimumSize(960, 540) # 设置手动调整的最小尺寸
main.py
文件中的调用方式self.ui
实例变量。通过 self.ui.setup_ui(self)
让self.ui作为MainWindow实例变量self的界面。# IMPORT MODULES
import sys
import os
# IMPORT QT CORE
from qt_core import *
# IMPORT MAIN WINDOW
from gui.windows.main_window.ui_main_window import UI_MainWindow
# MAIN WINDOW
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Python3.9 PySide6 Learning")
# SETUP MAIN WINDOW
self.ui = UI_MainWindow()
self.ui.setup_ui(self)
# SHOW APP
self.show()
if __name__ == "__main__":
app = QApplication(sys.argv)
app.setWindowIcon(QIcon("icon.ico"))
window = MainWindow()
# window.show() # 展示我们的app 窗口,或者在MainWindow()构造函数中写self.show()
sys.exit(app.exec())
ui_main_window.py
代码增加:实现典型UI布局设计: menu(左侧) + content(右侧)
central_frame
main_layout
,设定中央画布中的控件布局方式是水平(H,推荐)/垂直(V)布局left_menu
,同时创建一个右侧内容画布content
,通过CCS格式,设定其背景颜色,便于区分不同画布main_layout.addWidget
,将左右侧画布依次加到主布局中parent.setCentralWidgets
是中央画布central_frame
setContentsMargins
设定成0,左右画布中间间隔setSpacing
也设置成0
setMaximumWidth/setMaximumHeight
,设定成50
# IMPORT QT CORE
from qt_core import *
# MAIN WINDOW
class UI_MainWindow(object):
def setup_ui(self, parent):
'''parent: 一般由 main.py文件中MainWindow类的实例self变量作为父类,目的是UI_MainWindow实例做mai文件中的UI界面。
通常,在main.py,这样调用setup_ui方法
# SETUP MAIN WINDOW
self.ui = UI_MainWindow()
self.ui.setup_ui(self)
'''
if not parent.objectName(): # 此判断,经测试,一定会进入
parent.setObjectName("MainWindow") # TODO(wangjh-xm): 暂时不清楚setObjectName的作用效果。
# print('test')
# SET INITIAL PARAMETERS
# ///////////////////////////////////////////////////////////////
parent.resize(1200, 720) # 重新设启动时的界面大小
parent.setMinimumSize(960, 540) # 设置手动调整的最小尺寸
# CREATE CENTRAL WIDGET
self.central_frame = QFrame()
# CREATE MAIN LAYOUT
self.main_layout = QHBoxLayout(self.central_frame)
self.main_layout.setContentsMargins(0, 0, 0, 0)
self.main_layout.setSpacing(0)
# LEFT MANU
self.left_menu = QFrame()
self.left_menu.setStyleSheet("background-color: #44475a")
self.left_menu.setMaximumWidth(50)
# CONTENT
self.content = QFrame()
self.content.setStyleSheet("background-color: #282a36")
# ADD WIDGETS TO APP
self.main_layout.addWidget(self.left_menu)
self.main_layout.addWidget(self.content)
# SET CENTRAL
parent.setCentralWidget(self.central_frame)
# IMPORT QT CORE
from qt_core import *
# MAIN WINDOW
class UI_MainWindow(object):
def setup_ui(self, parent):
'''parent: 一般由 main.py文件中MainWindow类的实例self变量作为父类,目的是UI_MainWindow实例做mai文件中的UI界面。
通常,在main.py,这样调用setup_ui方法
# SETUP MAIN WINDOW
self.ui = UI_MainWindow()
self.ui.setup_ui(self)
'''
if not parent.objectName(): # 此判断,经测试,一定会进入
parent.setObjectName("MainWindow") # TODO(wangjh-xm): 暂时不清楚setObjectName的作用效果。
# print('test')
# SET INITIAL PARAMETERS
# ///////////////////////////////////////////////////////////////
parent.resize(1200, 720) # 重新设启动时的界面大小
parent.setMinimumSize(960, 540) # 设置手动调整的最小尺寸
# CREATE CENTRAL WIDGET
self.central_frame = QFrame()
# CREATE MAIN LAYOUT
self.main_layout = QVBoxLayout(self.central_frame)
self.main_layout.setContentsMargins(0, 0, 0, 0)
self.main_layout.setSpacing(0)
# LEFT MANU
self.left_menu = QFrame()
self.left_menu.setStyleSheet("background-color: #44475a")
self.left_menu.setMaximumHeight(50)
# CONTENT
self.content = QFrame()
self.content.setStyleSheet("background-color: #282a36")
# ADD WIDGETS TO APP
self.main_layout.addWidget(self.left_menu)
self.main_layout.addWidget(self.content)
# SET CENTRAL
parent.setCentralWidget(self.central_frame)
content
画面中,创建小部件和页面(堆栈窗口控件) @2021.10.23, 24在第二课的基础上,继续在ui_main_window.py文件中,为 content
画面增加UI控件
1:增加 Top Bar
Top Bar
,设置固定尺寸以及背景色和前景色。content
画面的布局方式为垂直布局,设置内容间隔和控件之间的间隔值为0Top Bar
画面增加到 content_layout
布局中。content
画布加上 Top Bar
画布):
2:增加 pages
QS tackedWidget
实例 self.pages
;可实现页面切换(索引)效果,比选项卡控件 QTabWidget
操作的自由度更高(切换控件可以自由定制,通过页面索引控制页面切换)。content_layout
布局中left_menu
的最小尺寸宽度设定3:模仿 Top Bar
,增加一个 Bottom Bar
# TOP BAR
段,修改为 bottom_barself.bootom_bar
增加到 content_layout
布局中4:在 Top/Bottom Bar
画布中,增加 Label控件。
top_bar
代码的后面,新增 QLabel
控件实例 top_label_left
QSpacerItem
临时间隔区控件,目的是将顶部的左右2个 QLabel
分在2侧。QLabel
标签控件 top_label_right
, 并设置字体风格。top_bar
画面中的布局规格 self.top_bar_layout
:
QHBoxLayout(self.top_bar)
, 并且设置边缘间隔 Margins
为合适的距离,10。QLabel
以及中间的 QSpacerItem
依次放入上述水平布局中。
# ADD TO TOP BAR LAYOUT
self.top_bar_layout.addWidget(self.top_label_left)
self.top_bar_layout.addItem(self.top_spacer) # 注意 spacer 是Item,所以用addItem
self.top_bar_layout.addWidget(self.top_label_right)
top_bar
的 label
添加方法,新增 label
到 bottom_bar
5: 在 pages
画布中,添加多个 page
(借助 QStackedWidget
堆栈窗口控件实现,使用 Qt-Designer
自动生成相对应的 python
代码)
Qt-Designer
路径:...envs\venv_new_hmi\Lib\site-packages\PySide6\designer.exe
PyCharm
IDE中将此应用添加外部工具中,便于后续使用。PyCharm
IDE中添加 uic工具。
QStackedWidget
QStackedWidget
), 保存到 gui\pages
文件夹中
application_pages
。
uic.exe
生成 ui_pages.py
文件
self.pages
后面,继续添加 ui设计文件中类的实例化 self.ui_pages
self.ui_pages.setupUi
方法,与 self.pages
连接起来。QPushButton
在第三课的基础上,继续在ui_main_window.py文件中,为 左侧菜单
self.left_menu
画面增加UI控件。 思路总结如下,在左侧菜单frame
画面中,增加垂直布局
,然后在布局中依次增加顶部画面frame
、底部画面frame
、最底部标签label
。 然后,在顶部/底部frame画面
中,增加垂直布局
,在布局中依次增加pushButton
按钮。顶部
与底部
之间,增加spacer间隔
项分割。
总结手动创建控件的思路就是:先创建
frame画布
;然后规定画布中的布局方式为水平/垂直...布局
;在布局中,添加需要增加的控件,如pushButton
、label
等, 画面/子控件之间可以使用spacer间隔
隔开。
1、增加最底层的4个大控件
left_menu
的布局方式是垂直布局,注意将布局与左菜单画面绑定起来的语句是self.left_menu_layout = QVBoxLayout(self.left_menu)
。self.left_menu_top_frame
、中间分隔项self.left_menu_spacer
、底部画面菜单self.left_menu_bottom_frame
以及 版本标签self.left_menu_label_version
。2、在顶部/底部画面中增加按钮
self.left_menu_top_frame.setObjectName("left_menu_top_frame")
self.left_menu_top_frame.setStyleSheet("#left_menu_top_frame { background-color: red; }")
- 效果:
animation
,做出动态效果main.py
中,定义按钮的响应事件
QPropertyAnimation
功能,定义一个属性更改动画
PyPushButton
,继承自 QPushButton
,修改按钮显示图标。PyPushButton
类, 继承自 QPushButton
gui/widgets
中ui_main_window.py
文件中,修改按钮控件为 PyPushButton
的实例。
text
形参设置按钮的显示名称,is_active
设置当前控件为激活状态。.svg
gui/images/icons
中draw_icon(self,qp,image,color)
, 写死图标搜索路径就在gui/images/icons
下paintEvent(self, event)
方法,使用 QPainter
、QRect
,调用 draw_icon
方法ui_main_window.py
文件中,按钮初始化形参中调用 icon_path
形参,填入图标路径(仅填图标名以及后缀)1、添加按钮的单击事件
clicked.connect
方法,连接槽函数。实现显示 pages
的不同页面reset_selection
方法,复位按钮被激活效果,实现点击哪个按钮,只那个按钮会有选中的效果。2、使用QT Designer
,在主页中添加单行文本框 QLineEdit
和 按钮 PushButton
QFrame
最小/大尺寸
:500*70水平居中
栅格布局
垂直布局
layout
中的margin, 9->0placehoderText
中,填写文本框提示性文字。main.py
文件中, 增加 单行文本框的字符串,以及按钮单击事件。ui_pages.ui
中,增加输入完文本行后,按enter
键直接发送文本,需要重新uic
3、效果
pyinstaller
打包 python+pyside6
程序pyinstaller -D main.py
,执行完之后,到dist文件夹中找到 main.exe
运行。pyinstaller
生成的 spec
规格文件中,exe命令内容中添加 icon='icon.ico'
spec
文件中,exe命令内容中修改 console=True
-> console=False
pyinstaller main.spec
, 测试新生成的 main.exe
,然后把icon文件复制一份到与 main.exe
同目录下,观察图片和控制台是否成功设置。pyinstaller -F main.py
spec
文件中,设置打包时一并将资源文件复制到指定目录下。 datas=[('.\\*.ico','.\\'),('gui\\images\\icons','gui\\images\\icons')], # 增加程序图标以及控件图标文件
pip install auto-py-to-exe -i https://pypi.tuna.tsinghua.edu.cn/simple
安装过程
单目录EXE输出,启动效果:
单文件模式
的原因(从参考资料中copy)之所以强调这一点,并不是因为单文件模式存在什么无法解决的问题。如果你非常清楚该模式的运行机制,并且在写代码的时候小心避开这些坑的话,那么所有问题都是可以避免的。但实际上,可以说 PyInstaller 的用户 99% 都达不到这个要求,而只要你写的程序有点规模的话,几乎无一例外会踩到坑里。基于这种考虑,我从来不推荐用户使用单文件模式。如果你认真看过本文,并非常肯定自己能避开下面提到的问题,那么请使用单文件模式无妨。否则,还是老老实实的使用默认模式吧。
有个问题你不妨考虑一下:我们把程序编译成了单一的可执行文件,但是从上面的单目录模式结果可以知道,要让程序运行还需要其他很多的辅助文件,此外我们自己也可以添加数据文件(--add-data)和二进制文件(--add-binary),那么这些文件哪里去了?你如何访问这些文件?
这才是秘密所在!本质上,Python 是解释程序,而不是 native 的编译程序,它并不能真正产生出真正单一的可执行文件。PyInstaller 这里变了个小戏法,如果我们使用单文件模式的话,那么 PyInstaller 生成的实际上类似于 WinZIP/WinRAR 生成的自动解压程序。它需要先把所有文件解压到一个临时目录(通常名为_MEIxxxx,xxxx是随机数字),再从临时目录加载解释器和附属文件。程序运行完毕后,如果一切正常,那么它会把临时目录再悄悄删除掉。
为了让这个过程顺利执行,PyInstaller 会对运行时的 Python 解释器做一些修改,特别是下面两个变量:
sys.frozen 如果你直接运行 Python 脚本的话,那么该变量是不存在的。但 PyInstaller 则会设置它为 True(不论单目录还是单文件模式)。因此,你可以用它来判断程序是手工运行的,还是通过 PyInstaller 生成的可执行文件运行的;
sys._MEIPASS 如果使用单文件模式,该变量包含了 PyInstaller 自动创建的临时目录名。你可以用 --runtime-tmpdir 命令行开关来强制使用特定的目录,但是鉴于最终用户有哪些目录不在程序员控制范围内,通常还是应该避免使用它。 我们可以自己写一个程序来验证:
import sys
import os
print('__file__:', __file__)
print('sys.executable:', sys.executable)
print('sys.argv[0]:', sys.argv[0])
print('os.getcwd():', os.getcwd())
print('sys.frozen:', getattr(sys, 'frozen', False))
print('sys._MEIPASS:', getattr(sys, '_MEIPASS', None))
input('Press any key to exit...')
把该脚本编译到单文件模式,然后执行。注意,先不要按任何键(否则程序退出,临时目录就不存在了),然后根据输出结果,可以到资源管理器中找到对应的临时目录:
单文件模式临时目录
你可以看到临时目录包含了运行输出所需的各种辅助文件,除了主程序.EXE 之外。仔细分析一下,我们也能明白为什么单文件模式下容易出错了。尽管 PyInstaller 努力使得各种输出和直接运行脚本的结果尽可能相似,但差别还是很明显的:
file 指向的脚本名不变,但该文件已经不存在于磁盘上了。这使得依赖于 file 去解析相对文件位置的代码非常容易出错。这也是绝大多数错误的来源,请务必注意! sys.executable 不再指向 Python.exe,而是指向生成的文件位置了。如果你使用该变量判断系统库位置的话,那么也请小心; os.getcwd() 指向执行文件的位置(双击运行的话是这样,但如果从命令行启动的话则未必)。但请注意,你添加的数据/二进制文件并非位于此目录,而是在临时目录上,不明白这一点的话,也很容易出现找不到文件的问题。 需要说明的是,上述问题不只存在于你自己写的代码里。有相当多的库没有考虑到在 PyInstaller 打包后下执行的场景,它们在使用这些变量的时候很有可能会出问题。事实上这也是 PyInstaller 添加 Runtime Hook 机制的一个重要原因。
如果你的脚本需要引用辅助文件路径的话,那么一种可能的形式如下:
if getattr(sys, 'frozen', False):
tmpdir = getattr(sys, '_MEIPASS', None)
if tmpdir:
filepath = os.path.join(tmpdir, 'README.txt')
else:
filepath = os.path.join(os.getcwd(), 'README.txt')
else:
filepath = os.path.join(os.path.dirname(__file__), 'README.txt')
上述代码并不是唯一可行的代码,或许也不是最简洁的,但是你应当明白了,要正确处理该过程并不是轻而易举的事情。很多用户之所以出错又找不到问题,就是因为他们根本不清楚临时目录这回事,也不知道上哪里去找这些文件。如果使用单目录模式的话,那么文件在哪里是可以直接看到的,出现问题的可能性就小多了,即使有问题也很容易排查。这就是我为什么强烈推荐用户不要使用单文件模式的原因————除了看起来比较清爽之外,单文件模式基本上没有其他好处,而且它带来的麻烦比这一点好处要多太多了。
除此之外,单文件模式还带来了其他一些负面效应:
因为有临时目录和解压文件这个过程,所以单文件模式的程序启动速度会比较慢。对于稍大的程序,这个延迟是肉眼可以感觉到的; 如果你的程序运行到一半崩溃了,那么临时目录将没有机会被删除。日积月累的话,可能会在临时目录下遗留一大堆 _MEIxxxx 目录,占用大量磁盘空间。 或许对你来说上面这两个问题并不是特别重要,但知道它们的存在还是有好处的。
pages
中此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。