├── .gitignore
├── Addition
├── Layout.png
├── Layout_Size11.png
├── Lesson1.png
├── Lesson2Design.png
├── Object_window.png
├── QtDesigner.png
├── SizePolicy.png
├── V_layout_self.png
├── mainwindow.png
└── mainwindow_dock.png
├── EzQtTools.py
├── Lesson_01.环境配置与入门
├── README.md
└── helloworld.py
├── Lesson_02.使用QtDesigner
├── README.md
├── main.py
├── mainwindow.ui
└── ui_mainwindow.py
├── Lesson_03.使用布局管理
├── README.md
├── main.py
├── mainwindow.ui
└── ui_mainwindow.py
├── Lesson_04.使用QSS美化界面
├── README.md
├── helloworld.qss
├── main.py
└── ui_mainwindow.py
├── Lesson_05.结合OpenCV实现视频播放器
├── README.md
├── main.py
├── mainwindow.ui
└── ui_mainwindow.py
├── Lesson_06.另一种槽连接机制
├── Slot.py
└── readme.md
├── Lesson_07.主窗口的构成
├── readme.md
└── struct_main.py
├── Lesson_08.窗口嵌套
├── readme.md
└── subwindow.py
├── Lesson_09.EzQtTools
├── main.py
└── readme.md
├── Lesson_10.AutoSizeImage
├── main.py
└── readme.md
├── Lesson_11.MultimediaPlayer
├── icon.png
├── main.py
└── readme.md
├── Lesson_12.贪吃蛇
├── readme.md
├── resources
│ ├── a.svg
│ ├── candy.svg
│ ├── d.svg
│ ├── ico.svg
│ ├── s.svg
│ └── w.svg
└── snake.py
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .*
2 | !.gitignore
3 | /Lesson_5.结合OpenCV实现视频播放器/*.png
4 | *.mp4
5 | *.mp3
6 | __pycache__
7 | !Lesson_11.MultimediaPlayer/icon.png
--------------------------------------------------------------------------------
/Addition/Layout.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/se7enXF/pyside2/81fedb79f14507aff631b3f3b3d27102e5208bd9/Addition/Layout.png
--------------------------------------------------------------------------------
/Addition/Layout_Size11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/se7enXF/pyside2/81fedb79f14507aff631b3f3b3d27102e5208bd9/Addition/Layout_Size11.png
--------------------------------------------------------------------------------
/Addition/Lesson1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/se7enXF/pyside2/81fedb79f14507aff631b3f3b3d27102e5208bd9/Addition/Lesson1.png
--------------------------------------------------------------------------------
/Addition/Lesson2Design.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/se7enXF/pyside2/81fedb79f14507aff631b3f3b3d27102e5208bd9/Addition/Lesson2Design.png
--------------------------------------------------------------------------------
/Addition/Object_window.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/se7enXF/pyside2/81fedb79f14507aff631b3f3b3d27102e5208bd9/Addition/Object_window.png
--------------------------------------------------------------------------------
/Addition/QtDesigner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/se7enXF/pyside2/81fedb79f14507aff631b3f3b3d27102e5208bd9/Addition/QtDesigner.png
--------------------------------------------------------------------------------
/Addition/SizePolicy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/se7enXF/pyside2/81fedb79f14507aff631b3f3b3d27102e5208bd9/Addition/SizePolicy.png
--------------------------------------------------------------------------------
/Addition/V_layout_self.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/se7enXF/pyside2/81fedb79f14507aff631b3f3b3d27102e5208bd9/Addition/V_layout_self.png
--------------------------------------------------------------------------------
/Addition/mainwindow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/se7enXF/pyside2/81fedb79f14507aff631b3f3b3d27102e5208bd9/Addition/mainwindow.png
--------------------------------------------------------------------------------
/Addition/mainwindow_dock.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/se7enXF/pyside2/81fedb79f14507aff631b3f3b3d27102e5208bd9/Addition/mainwindow_dock.png
--------------------------------------------------------------------------------
/EzQtTools.py:
--------------------------------------------------------------------------------
1 | # -*- coding:utf-8 -*-
2 | """
3 | @Author: Fei Xue
4 | @E-Mail: FeiXue@nuaa.edu.cn
5 | @File: EzQtTools.py
6 | @Time: 2020/7/3 12:37
7 | @Introduction: 简便的Qt工具
8 | """
9 |
10 |
11 | from PySide2 import QtWidgets, QtCore, QtGui
12 | __NAME__ = 'EzQtTools'
13 |
14 |
15 | # 快捷生成基础窗口和添加布局控件工具
16 | class EzMainWindow(QtWidgets.QMainWindow):
17 |
18 | def __init__(self,
19 | icon: str = None,
20 | title: str = __NAME__,
21 | size: tuple = (960, 540),
22 | fixed: bool = False,
23 | max_window: bool = False,
24 | show_statusbar: bool = False,
25 | default_layout: str = 'V',
26 | default_layout_stretch: int = 0,
27 | default_layout_margins: int = 9,
28 | default_layout_space: int = 9
29 | ):
30 | """
31 | 直接生成基础的主窗口,并设置默认布局参数
32 | :param icon: 窗口图标URL
33 | :param title: 窗口标题
34 | :param size: 窗口大小(w,h)
35 | :param fixed: 窗口大小不可改变?
36 | :param max_window: 打开窗口后全屏?
37 | :param default_layout: 默认布局,V表示垂直布局,H表示水平布局
38 | :param default_layout_stretch: 当前控件在布局中的伸缩量
39 | :param default_layout_margins: 布局四周的边界量,可以是整数四元组,也可以是一个整数来表示同值的四元组
40 | :param default_layout_space: 布局中控件之间的间隔量
41 | """
42 | super(EzMainWindow, self).__init__()
43 |
44 | self.default_layout = default_layout
45 | self.default_layout_stretch = default_layout_stretch
46 | self.default_layout_margins = default_layout_margins
47 | self.default_layout_space = default_layout_space
48 |
49 | # -- 初始化默认参数 -- #
50 | if icon and QtCore.QFile.exists(icon):
51 | icon = QtGui.QIcon(icon)
52 | self.setWindowIcon(icon)
53 | self.setWindowTitle(title)
54 | if fixed:
55 | self.setFixedSize(size[0], size[1])
56 | else:
57 | self.resize(size[0], size[1])
58 | if max_window:
59 | self.showMaximized()
60 |
61 | # -- 初始化中心控件 -- #
62 | self.central_widget = QtWidgets.QWidget()
63 | self.setCentralWidget(self.central_widget)
64 |
65 | # -- 添加菜单栏和状态栏 -- #
66 | # 菜单栏添加菜单后自动显示
67 | if show_statusbar:
68 | self.statusBar().show()
69 |
70 | # -- 在此函数中设置自己的布局控件 -- #
71 | self.__init__widget()
72 |
73 | def add_layout_widget(self, parent: QtWidgets.QWidget,
74 | widget: QtWidgets.QWidget,
75 | layout: QtWidgets.QLayout = None,
76 | stretch: int = None,
77 | margins: [int, list] = None,
78 | space: int = None
79 | ) -> QtWidgets.QWidget:
80 | """
81 | 给parent控件上添加widget。当parent首次添加widget,需要指定其布局格式。
82 | :param parent: 父控件
83 | :param widget: 当前添加的控件
84 | :param layout: 布局
85 | :param stretch: 伸缩量
86 | :param margins: 边界量
87 | :param space: 间隔量
88 | :return: 当前添加的控件
89 | """
90 |
91 | current_layout = parent.layout()
92 | if not stretch:
93 | stretch = self.default_layout_stretch
94 |
95 | if current_layout:
96 | current_layout.addWidget(widget)
97 | else:
98 | if not layout:
99 | if self.default_layout == 'H':
100 | layout = QtWidgets.QHBoxLayout()
101 | else:
102 | layout = QtWidgets.QVBoxLayout()
103 | if not margins:
104 | margins = self.default_layout_margins
105 | if not space:
106 | space = self.default_layout_space
107 |
108 | layout.addWidget(widget, stretch=stretch)
109 | layout.setSpacing(space)
110 | parent.setLayout(layout)
111 |
112 | if isinstance(margins, int):
113 | layout.setContentsMargins(margins, margins, margins, margins)
114 | else:
115 | layout.setContentsMargins(margins[0], margins[1], margins[2], margins[3])
116 |
117 | return widget
118 |
119 | def status_bar_write(self, words, timeout=0):
120 | """
121 | 在状态栏上显示信息
122 | :param keep: 停留时间(毫秒),0表示一致停留直到触发清除或显示信息事件
123 | :param words: 信息
124 | """
125 | self.statusBar().showMessage(words)
126 |
127 | def open_file_dialog(self, default_dir='.', types: list = None) -> str:
128 | """
129 | 打开文件或文件夹路径并返回
130 | :param default_dir: 打开文件夹对话框默认的目录
131 | :param types: 打开文件类型列表,例如 types=['jpg', 'bmp', ...],当types=None时打开文件夹目录
132 | :return: dir
133 | """
134 | if types is None:
135 | src = QtWidgets.QFileDialog.getExistingDirectory(self,
136 | '打开目录',
137 | default_dir,
138 | QtWidgets.QFileDialog.ShowDirsOnly)
139 | else:
140 | type_str = f'*.{types}'
141 | if isinstance(types, list):
142 | type_str = f'*.{types[0]}'
143 | for t in types[1:]:
144 | type_str += f' *.{t}'
145 | src, _ = QtWidgets.QFileDialog.getOpenFileName(self, '打开文件', default_dir, f'File type ({type_str})')
146 |
147 | return src
148 |
149 | def __init__widget(self):
150 | """
151 | 在自定义方法中重写该函数以设置自己的布局;
152 | 在中心控件上设置布局,首要布局的parent必须指定为self.central_widget;
153 |
154 | 例1:只在中心控件上放置标签
155 | label = self.add_layout_widget(self.central_widget, QtWidgets.QLabel())
156 | 例2:在中心空间上放置容器,然后在其中添加两个标签
157 | widget = self.add_layout_widget(self.central_widget, QtWidgets.QWidget())
158 | label1 = self.add_layout_widget(widget, QtWidgets.QLabel())
159 | label2 = self.add_layout_widget(widget, QtWidgets.QLabel())
160 | 例3:在中心空间上放置两个容器,然后分别在其中添加标签
161 | widget1 = self.add_layout_widget(self.central_widget, QtWidgets.QWidget())
162 | widget2 = self.add_layout_widget(self.central_widget, QtWidgets.QWidget())
163 | label1 = self.add_layout_widget(widget1, QtWidgets.QLabel())
164 | label2 = self.add_layout_widget(widget2, QtWidgets.QLabel())
165 | """
166 | pass
167 |
168 |
169 | # 自适应窗口变化的显示图像的标签
170 | class AutoSizeLabel(QtWidgets.QWidget):
171 |
172 | def __init__(self, parent=None):
173 | super(AutoSizeLabel, self).__init__(parent=parent)
174 |
175 | self.image = QtWidgets.QLabel(self)
176 | self.image.move(0, 0)
177 | self.image.setScaledContents(True)
178 | self.resize(parent.size())
179 |
180 | def SetPixmap(self, pix_img):
181 | self.image.setPixmap(pix_img)
182 |
183 | def resizeEvent(self, *args, **kwargs):
184 | self.image.resize(self.size())
185 |
186 | def Clear(self):
187 | self.image.clear()
188 |
--------------------------------------------------------------------------------
/Lesson_01.环境配置与入门/README.md:
--------------------------------------------------------------------------------
1 | # 环境配置与入门
2 | 第一课,讲述使用Python和PySide2的软件开发环境的配置和显示简单的helloworld界面。
3 |
4 | ## 软件开发环境
5 | * Windows10(其他操作系统亦可)
6 | * Python3.6
7 | * PySide2
8 |
9 | ## 安装PySide2
10 | 关于Python的安装请自行学习,这里不再累述。
11 | 安装PySide2的PyPI,命令行:
12 | ```bash
13 | pip install pyside2
14 | ```
15 |
16 | ## Hello world!
17 | 万程开头helloworld!作者编写的的这个helloworld.py是根据官网教程改编的,里面也写了详细的注释。
18 | 此方法,只使用编写代码来实现Qt窗口。当我们设计复杂且美观的窗口时,只使用代码来编写必定是非常繁琐的。
19 | 因此,在下一讲中,会介绍一种结合QtDesigner的用户界面绘制方法,能让我们更高效的实现QtUI。
20 | 当你配置好环境,可以直接运行helloworld.py,如果成功运行并显示如下窗口,那么恭喜你入坑!
21 | 
22 | * [第二课](../Lesson_02.使用QtDesigner/README.md)
23 | ## 参考文档
24 | [PySide官方文档 Qt for Python](https://doc-snapshots.qt.io/qtforpython/index.html )
--------------------------------------------------------------------------------
/Lesson_01.环境配置与入门/helloworld.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-#
2 | # File: helloworld.py
3 | # Author: se7enXF
4 | # Github: se7enXF
5 | # Date: 2019/2/19
6 | # Note: 此程序源自Qt官网文档!使用PySide2显示窗口,点击按钮随机显示不同语言的“hello world”
7 |
8 | import sys
9 | import random
10 | from PySide2 import QtGui, QtWidgets, QtCore
11 |
12 |
13 | # 定义主窗口
14 | class MainWindow(QtWidgets.QWidget):
15 | def __init__(self):
16 | super().__init__()
17 | # 设置主窗口大小
18 | self.resize(400, 300)
19 | # 设置主窗口标题
20 | self.setWindowTitle("hello world")
21 |
22 | # 定义不同语言
23 | self.hello = ["Hallo Welt", "你好世界!", "Hola Mundo", "Привет мир", "Hello world"]
24 |
25 | # 定义按钮
26 | self.button = QtWidgets.QPushButton("Click me!")
27 |
28 | # 定义标签
29 | self.text = QtWidgets.QLabel("Hello World")
30 | # 文字居中对齐
31 | self.text.setAlignment(QtCore.Qt.AlignCenter)
32 |
33 | # 定义布局,垂直分布
34 | self.layout = QtWidgets.QVBoxLayout()
35 |
36 | # 在布局上添加文字
37 | self.layout.addWidget(self.text)
38 | # 在布局上添加按钮
39 | self.layout.addWidget(self.button)
40 | # 在主窗口上布置布局
41 | self.setLayout(self.layout)
42 |
43 | # 添加槽链接
44 | self.button.clicked.connect(self.magic)
45 |
46 | # 定义槽函数
47 | def magic(self):
48 | self.text.setText(random.choice(self.hello))
49 |
50 |
51 | # 以下是主程序入口,格式基本固定,无须修改
52 | if __name__ == '__main__':
53 | app = QtWidgets.QApplication(sys.argv)
54 | window = MainWindow()
55 | window.show()
56 | sys.exit(app.exec_())
57 |
--------------------------------------------------------------------------------
/Lesson_02.使用QtDesigner/README.md:
--------------------------------------------------------------------------------
1 | # 高效绘制用户界面
2 | ## 概要
3 | 刚一开始使用Qt,大家基本上都是直接在代码里设置控件类,大小,位置。这样虽然繁琐,但是更可以让大家熟悉各
4 | 个控件的属性和方法。为了高效的绘制界面,这里介绍一种使用QtDesigner结合PySide的编程方法。主要分为三个步骤:
5 | 1. 打开QtDesigner绘制窗口;
6 | 2. 将UI文件转换为py文件供主程序调用;
7 | 3. 主窗口全局初始化。
8 |
9 | ## 1.绘制窗口
10 | 使用过C++版本Qt的老玩家(用户)都知道,QtDesigner是一个非常好用的窗口绘制工具。我们使用PySide编程,难道还需要
11 | 安装Qt?答案是不用。Qt团队在PySide的安装包里已经为我们准备好了QtDesigner。
12 | 找到你的python安装目录,打开Python\Lib\site-packages\PySide2\designer.exe,这便是QtDesigner。为了打开方便,
13 | 你可以设置快捷方式。作者使用PyCharm编程,直接将该程序添加到了工具里。界面如下图:
14 | 
15 |
16 | ## 2.文件转换
17 | 打开QtDesigner后,新建一个空白的主窗口文件,按照Lesson_1显示的窗口的样子放置一个Label和一个pushButton,
18 | 并设置好大小等属性,保存在当前工程目录下。
19 | 
20 | 然后打开命令行,切换工作路径到当前工程目录下,输入:
21 | ```bash
22 | pyside2-uic [你保存的文件名].ui > ui_mainwindow.py
23 | ```
24 | 注意文件名,可以修改,但切记与后面主文件调用的文件名要一致。毕竟很多人写代码只用三个键。此时,你可以打开生
25 | 成的py文件查看,很详细的记录了窗口的各种属性。
26 | ```
27 | # 生成.py文件中部分关键代码
28 | class Ui_MainWindow(object):
29 | def setupUi(self, MainWindow):
30 | MainWindow.setObjectName("MainWindow")
31 | ```
32 |
33 | ## 3.调用显示窗口
34 | 首先,把生成的py文件中的窗口类import进来,把QMainWindow类也import进来。然后定义主窗口,代码如下;
35 | ```
36 | # 从生成的.py文件导入定义的窗口类
37 | from ui_mainwindow import Ui_MainWindow
38 |
39 | # 定义主窗体
40 | class MainWindow(QMainWindow):
41 | def __init__(self):
42 | super(MainWindow, self).__init__()
43 | self.ui = Ui_MainWindow()
44 | self.ui.setupUi(self)
45 | ```
46 | 代码很简单,切记文件名和类名要对应。其他简单的功能实现部分相差不多,具体请参照main.py。
47 | 到此,使用PySide创建用户界面的基本流程你都学会了。但是你会发现,这样生成的窗口很不美观,并且拖动大小后,
48 | 内部控件没有变化,和Lesson_1显示截然的不同,显得很不自然。想要更好的设置界面,请看下一课。
49 | * [第三课](../Lesson_03.使用布局管理/README.md)
--------------------------------------------------------------------------------
/Lesson_02.使用QtDesigner/main.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-#
2 | # File: main.py
3 | # Author: se7enXF
4 | # Github: se7enXF
5 | # Date: 2019/2/19
6 | # Note: 使用QtDesigner高效绘制界面,并使用PySide调用
7 |
8 | import sys
9 | import random
10 | from PySide2 import QtGui, QtWidgets, QtCore
11 | from PySide2.QtWidgets import QApplication, QMainWindow
12 | from ui_mainwindow import Ui_MainWindow
13 |
14 |
15 | # 主窗体类
16 | class MainWindow(QMainWindow, Ui_MainWindow):
17 | def __init__(self):
18 | super(MainWindow, self).__init__()
19 | self.setupUi(self)
20 | # 定义字符
21 | self.hello = ["Hallo Welt", "Hei maailma", "Hola Mundo", "Привет мир", "Hello world"]
22 |
23 | # 添加槽链接
24 | self.pushButton.clicked.connect(self.magic)
25 | '''
26 | 关于槽链接,在PySide中调用的格式是:
27 | 发送者.信号.connect(槽函数)
28 |
29 | Qt是面向对象的程序设计,只有某个动作才会触发某个效果。使用槽链接的方式,可以实现复杂的操作。
30 | 使用槽链接时,一定要在官方文档查找发送者有哪些信号,例如pushButton有clicked信号可以激活槽链接。
31 | '''
32 |
33 | def magic(self):
34 | # 随机选取
35 | self.label.setText(random.choice(self.hello))
36 |
37 |
38 | if __name__ == '__main__':
39 | app = QtWidgets.QApplication(sys.argv)
40 | window = MainWindow()
41 | window.show()
42 | sys.exit(app.exec_())
43 |
--------------------------------------------------------------------------------
/Lesson_02.使用QtDesigner/mainwindow.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | MainWindow
4 |
5 |
6 |
7 | 0
8 | 0
9 | 400
10 | 300
11 |
12 |
13 |
14 | MainWindow
15 |
16 |
17 |
18 |
19 |
20 | 110
21 | 120
22 | 161
23 | 41
24 |
25 |
26 |
27 | Hello World
28 |
29 |
30 | Qt::AlignCenter
31 |
32 |
33 |
34 |
35 |
36 | 20
37 | 270
38 | 361
39 | 23
40 |
41 |
42 |
43 | Click me!
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/Lesson_02.使用QtDesigner/ui_mainwindow.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Form implementation generated from reading ui file 'mainwindow.ui',
4 | # licensing of 'mainwindow.ui' applies.
5 | #
6 | # Created: Tue Feb 19 16:37:20 2019
7 | # by: pyside2-uic running on PySide2 5.11.4a1.dev1546291887
8 | #
9 | # WARNING! All changes made in this file will be lost!
10 |
11 | from PySide2 import QtCore, QtGui, QtWidgets
12 |
13 | class Ui_MainWindow(object):
14 | def setupUi(self, MainWindow):
15 | MainWindow.setObjectName("MainWindow")
16 | MainWindow.resize(400, 300)
17 | self.centralwidget = QtWidgets.QWidget(MainWindow)
18 | self.centralwidget.setObjectName("centralwidget")
19 | self.label = QtWidgets.QLabel(self.centralwidget)
20 | self.label.setGeometry(QtCore.QRect(110, 120, 161, 41))
21 | self.label.setAlignment(QtCore.Qt.AlignCenter)
22 | self.label.setObjectName("label")
23 | self.pushButton = QtWidgets.QPushButton(self.centralwidget)
24 | self.pushButton.setGeometry(QtCore.QRect(20, 270, 361, 23))
25 | self.pushButton.setObjectName("pushButton")
26 | MainWindow.setCentralWidget(self.centralwidget)
27 |
28 | self.retranslateUi(MainWindow)
29 | QtCore.QMetaObject.connectSlotsByName(MainWindow)
30 |
31 | def retranslateUi(self, MainWindow):
32 | MainWindow.setWindowTitle(QtWidgets.QApplication.translate("MainWindow", "MainWindow", None, -1))
33 | self.label.setText(QtWidgets.QApplication.translate("MainWindow", "Hello World", None, -1))
34 | self.pushButton.setText(QtWidgets.QApplication.translate("MainWindow", "Click me!", None, -1))
35 |
36 |
--------------------------------------------------------------------------------
/Lesson_03.使用布局管理/README.md:
--------------------------------------------------------------------------------
1 | # 布局:控件自适窗口大小变化
2 | ## 概要
3 | 在Qt中,提供了多种布局,我们常用的是两种:水平和垂直。为什么要使用布局?有些人可能遇到过,
4 | 窗口绘制好后,运行时偶然拖动窗口或控件位置大小,会破坏整个布局的美观。在Qt中使用布局功能,
5 | 能很好的控制控件的大小和位置随窗孔变化而变化。
6 |
7 | ## 绘制Lesson_1窗口
8 | 打开QtDesigner,新建一个空白窗口,在主窗口中放置一个垂直布局,然后在主窗口空白处单击鼠标右键,任意选择一
9 | 种布局。此时放置的垂直布局框会布满主窗口。然后在该布局中放置一个Label和一个Push Button,他们会自动上下分布。
10 | (Qt中一般使用Label来显示文字和图像)然后修改Label的文字对齐属性为居中。主程序中,要注意窗口控件所属关系,
11 | 否则使用“.”方式调用会出现问题。到此处程序能正常执行的话,你就得到了与Lesson_1相同的窗口。
12 | 
13 |
14 | ## 布局参数
15 | 上面窗口的绘制使用了默认参数,美观的布局依赖于参数的设置。
16 | 一个布局的属性有两部分,自己与别人的关系和自己内部的关系。
17 | 在QtDesigner中,默认情况下,右上角是对象查看器。
18 | 
19 | 1. 在对象查看器中,我们可以很方便的选中控件,也可以看出所属关系。选中
20 | QVBoxLayout,下方出现属性编辑器。
21 | 
22 | 这部分属性是设置该布局与相邻布局的距离的,即自己与别人关系。
23 | 2. 在对象查看器中,选中布局下的某一个控件,然后找到“水平伸展”和“垂直伸展”两个属性,他们的数值表示了当前布局中
24 | 不同控件大小所占的比例,即自己内部关系。
25 | 
26 | 当前是QVBoxLayout,因此只考虑垂直比例,如果我们将比例设置为1:1,
27 | 即将label的“垂直伸展”和pushButton的“垂直伸展”都设置为1,那么在窗口变化中,上下两个控件垂直高度会一样。
28 | (当使用动态布局,切记分布策略选择“preferred")
29 | 
30 |
31 | * [第四课](../Lesson_04.使用QSS美化界面/README.md)
32 |
--------------------------------------------------------------------------------
/Lesson_03.使用布局管理/main.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-#
2 | # File: main.py
3 | # Author: se7enXF
4 | # Github: se7enXF
5 | # Date: 2019/2/19
6 | # Note: 使用布局。此程序在之前2讲已有注释,此处为简洁,不再注释
7 |
8 | import sys
9 | import random
10 | from PySide2 import QtGui, QtWidgets, QtCore
11 | from PySide2.QtWidgets import QApplication, QMainWindow
12 | from ui_mainwindow import Ui_MainWindow
13 |
14 |
15 | class MainWindow(QMainWindow, Ui_MainWindow):
16 | def __init__(self):
17 | super(MainWindow, self).__init__()
18 | self.setupUi(self)
19 | self.hello = ["Hallo Welt", "Hei maailma", "Hola Mundo", "Привет мир", "Hello world"]
20 | self.pushButton.clicked.connect(self.magic)
21 |
22 | def magic(self):
23 | self.label.setText(random.choice(self.hello))
24 |
25 |
26 | if __name__ == '__main__':
27 | app = QtWidgets.QApplication(sys.argv)
28 | window = MainWindow()
29 | window.show()
30 | sys.exit(app.exec_())
31 |
--------------------------------------------------------------------------------
/Lesson_03.使用布局管理/mainwindow.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | MainWindow
4 |
5 |
6 |
7 | 0
8 | 0
9 | 400
10 | 300
11 |
12 |
13 |
14 | MainWindow
15 |
16 |
17 | Qt::LeftToRight
18 |
19 |
20 |
21 | Qt::NoFocus
22 |
23 |
24 | false
25 |
26 |
27 | -
28 |
29 |
30 | 1
31 |
32 |
33 | QLayout::SetMaximumSize
34 |
35 |
36 | 1
37 |
38 |
39 | 1
40 |
41 |
42 | 1
43 |
44 |
45 | 1
46 |
47 |
-
48 |
49 |
50 |
51 | 0
52 | 1
53 |
54 |
55 |
56 | Hello World
57 |
58 |
59 | Qt::AlignCenter
60 |
61 |
62 |
63 | -
64 |
65 |
66 |
67 | 0
68 | 1
69 |
70 |
71 |
72 | Click me!
73 |
74 |
75 | false
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
--------------------------------------------------------------------------------
/Lesson_03.使用布局管理/ui_mainwindow.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Form implementation generated from reading ui file 'mainwindow.ui',
4 | # licensing of 'mainwindow.ui' applies.
5 | #
6 | # Created: Tue Feb 19 17:28:46 2019
7 | # by: pyside2-uic running on PySide2 5.11.4a1.dev1546291887
8 | #
9 | # WARNING! All changes made in this file will be lost!
10 |
11 | from PySide2 import QtCore, QtGui, QtWidgets
12 |
13 | class Ui_MainWindow(object):
14 | def setupUi(self, MainWindow):
15 | MainWindow.setObjectName("MainWindow")
16 | MainWindow.resize(400, 300)
17 | MainWindow.setLayoutDirection(QtCore.Qt.LeftToRight)
18 | self.centralwidget = QtWidgets.QWidget(MainWindow)
19 | self.centralwidget.setFocusPolicy(QtCore.Qt.NoFocus)
20 | self.centralwidget.setAutoFillBackground(False)
21 | self.centralwidget.setObjectName("centralwidget")
22 | self.horizontalLayout = QtWidgets.QHBoxLayout(self.centralwidget)
23 | self.horizontalLayout.setObjectName("horizontalLayout")
24 | self.verticalLayout = QtWidgets.QVBoxLayout()
25 | self.verticalLayout.setSpacing(1)
26 | self.verticalLayout.setSizeConstraint(QtWidgets.QLayout.SetMaximumSize)
27 | self.verticalLayout.setContentsMargins(1, 1, 1, 1)
28 | self.verticalLayout.setObjectName("verticalLayout")
29 | self.label = QtWidgets.QLabel(self.centralwidget)
30 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
31 | sizePolicy.setHorizontalStretch(0)
32 | sizePolicy.setVerticalStretch(1)
33 | sizePolicy.setHeightForWidth(self.label.sizePolicy().hasHeightForWidth())
34 | self.label.setSizePolicy(sizePolicy)
35 | self.label.setAlignment(QtCore.Qt.AlignCenter)
36 | self.label.setObjectName("label")
37 | self.verticalLayout.addWidget(self.label)
38 | self.pushButton = QtWidgets.QPushButton(self.centralwidget)
39 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
40 | sizePolicy.setHorizontalStretch(0)
41 | sizePolicy.setVerticalStretch(1)
42 | sizePolicy.setHeightForWidth(self.pushButton.sizePolicy().hasHeightForWidth())
43 | self.pushButton.setSizePolicy(sizePolicy)
44 | self.pushButton.setAutoDefault(False)
45 | self.pushButton.setObjectName("pushButton")
46 | self.verticalLayout.addWidget(self.pushButton)
47 | self.horizontalLayout.addLayout(self.verticalLayout)
48 | MainWindow.setCentralWidget(self.centralwidget)
49 |
50 | self.retranslateUi(MainWindow)
51 | QtCore.QMetaObject.connectSlotsByName(MainWindow)
52 |
53 | def retranslateUi(self, MainWindow):
54 | MainWindow.setWindowTitle(QtWidgets.QApplication.translate("MainWindow", "MainWindow", None, -1))
55 | self.label.setText(QtWidgets.QApplication.translate("MainWindow", "Hello World", None, -1))
56 | self.pushButton.setText(QtWidgets.QApplication.translate("MainWindow", "Click me!", None, -1))
57 |
58 |
--------------------------------------------------------------------------------
/Lesson_04.使用QSS美化界面/README.md:
--------------------------------------------------------------------------------
1 | # Qt界面美化
2 | ## 概要
3 | 上一课程,讲述了如何使用布局来控制控件的位置和大小,让他们随着窗口变化而变化,显得美观可靠。只有这些变化是不能满足
4 | 人们对一个友好的用户界面的要求,还需要配色来点缀,才可以使得它完美无缺。几乎在每个控件的属性中,都可以设置颜色和
5 | 文字大小,这也是配色的主要表现形式。Qt作为一个强大的用户界面绘制工具,它支持QSS脚本来辅助控制窗口控件属性,兼容网页
6 | 格式控制的CSS脚本,即其脚本的格式和属性。这使得之前写过网页的爱好者更容易美化界面,其他同学也不必担心,因为CSS很容易
7 | 掌握。
8 |
9 | ## 要点
10 | 具体的参数我就不必讲解了,自行查阅资料学习,相信大家看过我的例子程序就很容易理解。文件名为[名称].qss,
11 | 文件语法参考CSS。控制某个控件,简写格式如下:
12 | ```
13 | [控件类型]#[控件名称],[...]
14 | {
15 | [属性]:[值]
16 | ...
17 | }
18 |
19 | # 例:
20 | QStatusBar
21 | {
22 | color: #DCDCDC; # 文字颜色
23 | background: #484848; # 背景颜色
24 | }
25 | ```
26 | 控件名称可以缺省,缺省时表示这一类控件都设置属性。多个控件可以用逗号分隔,然后统一设置属性。
27 | 当某个控件是QWidget的继承类时,可以使用QWidget#[名称]来替代(例如Label)。具体细则请网络查找QSS,
28 | 有非常详细的介绍。
29 |
30 | * [第五课](../Lesson_05.结合OpenCV实现视频播放器/README.md)
--------------------------------------------------------------------------------
/Lesson_04.使用QSS美化界面/helloworld.qss:
--------------------------------------------------------------------------------
1 | QWidget#label
2 | {
3 | color: #dc0700;
4 | background: #46cbdc;
5 | }
6 |
7 | QPushButton:hover#pushButton
8 | {
9 | background-color:#DCDCDC;
10 | color: #484848;
11 | }
12 |
13 | QPushButton:pressed#pushButton
14 | {
15 | background-color:deepskyblue;border-style: inset;
16 | }
17 |
--------------------------------------------------------------------------------
/Lesson_04.使用QSS美化界面/main.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-#
2 | # File: main.py
3 | # Author: se7enXF
4 | # Github: se7enXF
5 | # Date: 2019/2/19
6 | # Note: 使用布局。此程序在之前2讲已有注释,此处为简洁,不再注释
7 |
8 | import sys
9 | import random
10 | from PySide2 import QtGui, QtWidgets, QtCore
11 | from PySide2.QtWidgets import QApplication, QMainWindow
12 | from ui_mainwindow import Ui_MainWindow
13 |
14 |
15 | class MainWindow(QMainWindow, Ui_MainWindow):
16 | def __init__(self):
17 | super(MainWindow, self).__init__()
18 | self.setupUi(self)
19 | self.hello = ["Hallo Welt", "Hei maailma", "Hola Mundo", "Привет мир", "Hello world"]
20 | self.pushButton.clicked.connect(self.magic)
21 |
22 | # 加载QSS
23 | with open("helloworld.qss", "r") as qs:
24 | self.setStyleSheet(qs.read())
25 |
26 | def magic(self):
27 | self.label.setText(random.choice(self.hello))
28 |
29 |
30 | if __name__ == '__main__':
31 | app = QtWidgets.QApplication(sys.argv)
32 | window = MainWindow()
33 | window.show()
34 | sys.exit(app.exec_())
35 |
--------------------------------------------------------------------------------
/Lesson_04.使用QSS美化界面/ui_mainwindow.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Form implementation generated from reading ui file 'mainwindow.ui',
4 | # licensing of 'mainwindow.ui' applies.
5 | #
6 | # Created: Tue Feb 19 17:28:46 2019
7 | # by: pyside2-uic running on PySide2 5.11.4a1.dev1546291887
8 | #
9 | # WARNING! All changes made in this file will be lost!
10 |
11 | from PySide2 import QtCore, QtGui, QtWidgets
12 |
13 | class Ui_MainWindow(object):
14 | def setupUi(self, MainWindow):
15 | MainWindow.setObjectName("MainWindow")
16 | MainWindow.resize(400, 300)
17 | MainWindow.setLayoutDirection(QtCore.Qt.LeftToRight)
18 | self.centralwidget = QtWidgets.QWidget(MainWindow)
19 | self.centralwidget.setFocusPolicy(QtCore.Qt.NoFocus)
20 | self.centralwidget.setAutoFillBackground(False)
21 | self.centralwidget.setObjectName("centralwidget")
22 | self.horizontalLayout = QtWidgets.QHBoxLayout(self.centralwidget)
23 | self.horizontalLayout.setObjectName("horizontalLayout")
24 | self.verticalLayout = QtWidgets.QVBoxLayout()
25 | self.verticalLayout.setSpacing(1)
26 | self.verticalLayout.setSizeConstraint(QtWidgets.QLayout.SetMaximumSize)
27 | self.verticalLayout.setContentsMargins(1, 1, 1, 1)
28 | self.verticalLayout.setObjectName("verticalLayout")
29 | self.label = QtWidgets.QLabel(self.centralwidget)
30 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
31 | sizePolicy.setHorizontalStretch(0)
32 | sizePolicy.setVerticalStretch(1)
33 | sizePolicy.setHeightForWidth(self.label.sizePolicy().hasHeightForWidth())
34 | self.label.setSizePolicy(sizePolicy)
35 | self.label.setAlignment(QtCore.Qt.AlignCenter)
36 | self.label.setObjectName("label")
37 | self.verticalLayout.addWidget(self.label)
38 | self.pushButton = QtWidgets.QPushButton(self.centralwidget)
39 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
40 | sizePolicy.setHorizontalStretch(0)
41 | sizePolicy.setVerticalStretch(1)
42 | sizePolicy.setHeightForWidth(self.pushButton.sizePolicy().hasHeightForWidth())
43 | self.pushButton.setSizePolicy(sizePolicy)
44 | self.pushButton.setAutoDefault(False)
45 | self.pushButton.setObjectName("pushButton")
46 | self.verticalLayout.addWidget(self.pushButton)
47 | self.horizontalLayout.addLayout(self.verticalLayout)
48 | MainWindow.setCentralWidget(self.centralwidget)
49 |
50 | self.retranslateUi(MainWindow)
51 | QtCore.QMetaObject.connectSlotsByName(MainWindow)
52 |
53 | def retranslateUi(self, MainWindow):
54 | MainWindow.setWindowTitle(QtWidgets.QApplication.translate("MainWindow", "MainWindow", None, -1))
55 | self.label.setText(QtWidgets.QApplication.translate("MainWindow", "Hello World", None, -1))
56 | self.pushButton.setText(QtWidgets.QApplication.translate("MainWindow", "Click me!", None, -1))
57 |
58 |
--------------------------------------------------------------------------------
/Lesson_05.结合OpenCV实现视频播放器/README.md:
--------------------------------------------------------------------------------
1 | # 图片的显示与视频的播放
2 | ## 概要
3 | 在Qt中,一般使用QLabel显示图像。视频播放,就是多张连续的图像快速的播放,超过人眼识别的速度。
4 | 一般的视频和电影是30FPS,而人眼的识别频率是24FPS,也就是每秒24张图片闪过人眼是感受不到闪烁的。
5 | 这次课程,将从以下两个方面讲述Qt图像的处理:
6 | 1. 直接显示图像。(不使用第三方图像读取,处理库)
7 | 2. 使用OpenCV读取视频并播放。
8 |
9 | ## 1.直接显示图像
10 | 在本次的例程中,我是用Qt自带的图像读取与显示方法,大家一看程序便知。
11 |
12 | ## 2.使用OpenCV读取视频,在Qt界面上播放
13 | 正如之前所说,播放视频就是很快的播放连续的图片,而播放频率就靠视频的FPS控制。
14 | 使用OpenCV读取视频后,会得到视频的一系列参数,其中重要的有以下几个:
15 | * 视频FPS:cv2.CAP_PROP_FPS
16 | * 视频总帧数:cv2.CAP_PROP_FRAME_COUNT
17 | * 视频当前帧:cv2.CAP_PROP_POS_FRAMES
18 | 通过简单的计算,我们可以得到视频播放的时间等信息。
19 |
20 | ### 要点
21 | 1. 为了循环的播放一帧一帧的图像,使用了QTimer(定时器),让他定时循环溢出,
22 | 然后将溢出信号与显示一帧图像的槽程序连接。定时器的循环时间为1000/FPS,单位为毫秒。
23 | 2. 为了实现快进,快退等功能,使用了槽连接的连接与断开,让大家更容易理解如何使用槽。
24 | 3. 在Qt中显示Mat格式的图像(OpenCV的图像格式),需要相应的转换。
25 |
26 | * [第六课](../Lesson_06.另一种槽连接机制/readme.md)
27 |
--------------------------------------------------------------------------------
/Lesson_05.结合OpenCV实现视频播放器/main.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-#
2 | # File: main.py
3 | # Author: se7enXF
4 | # Github: se7enXF
5 | # Date: 2019/2/19
6 | # Note: 使用布局。此程序在之前2讲已有注释,此处为简洁,不再注释
7 |
8 | import sys
9 | import os
10 | from glob import glob
11 | from PySide2 import QtWidgets
12 | from PySide2.QtWidgets import QMainWindow, QFileDialog, QMessageBox
13 | from PySide2.QtCore import QDir, QTimer, QSize
14 | from PySide2.QtGui import QPixmap, QImage
15 | from ui_mainwindow import Ui_MainWindow
16 | import cv2
17 |
18 |
19 | class MainWindow(QMainWindow, Ui_MainWindow):
20 | def __init__(self):
21 | super(MainWindow, self).__init__()
22 | self.setupUi(self)
23 | # 打开文件类型,用于类的定义
24 | self.f_type = 0
25 |
26 | def window_init(self):
27 | # 设置控件属性
28 | self.label.setText("请打开文件")
29 | self.menu.setTitle("打开")
30 | self.actionOpen_image.setText("打开图片")
31 | self.actionOpen_video.setText("打开视频")
32 | # 按钮使能(否)
33 | self.pushButton.setEnabled(False)
34 | self.pushButton_2.setEnabled(False)
35 | self.pushButton_3.setEnabled(False)
36 | # 菜单按钮 槽连接 到函数
37 | self.actionOpen_image.triggered.connect(ImgConfig_init)
38 | self.actionOpen_video.triggered.connect(VdoConfig_init)
39 | # 自适应窗口缩放
40 | self.label.setScaledContents(True)
41 |
42 |
43 | # 定义图片类
44 | class ImgConfig:
45 | # 初始化
46 | def __init__(self):
47 | window.pushButton.setEnabled(False)
48 | window.pushButton_2.setEnabled(True)
49 | window.pushButton_3.setEnabled(True)
50 | # 获取打开文件列表,当前选中的文件
51 | self.files, self.d_file = open_image()
52 | # 判断是否正常打开
53 | if not self.files:
54 | return
55 | # 文件个数
56 | self.n = len(self.files)
57 | window.pushButton_2.setText("上一张")
58 | window.pushButton_3.setText("下一张")
59 |
60 | # 获取当前列表文件名
61 | file_names = []
62 | for _ in range(self.n):
63 | (dir_name, full_file_name) = os.path.split(self.files[_])
64 | file_names.append(full_file_name)
65 | # 获取当前文件在列表中的位置
66 | self.counter = file_names.index(self.d_file)
67 | # 显示当前图片
68 | direct_show_image(self.files[self.counter])
69 | # 连接槽函数
70 | window.pushButton_2.clicked.connect(self.last_img)
71 | window.pushButton_3.clicked.connect(self.next_img)
72 |
73 | # 上一张
74 | def last_img(self):
75 | if self.counter > 0:
76 | self.counter -= 1
77 | else:
78 | self.counter = self.n-1
79 | # 显示图片
80 | direct_show_image(self.files[self.counter])
81 |
82 | # 下一张
83 | def next_img(self):
84 | if self.counter < self.n-1:
85 | self.counter += 1
86 | else:
87 | self.counter = 0
88 | direct_show_image(self.files[self.counter])
89 |
90 |
91 | # 图像类初始化,槽函数
92 | def ImgConfig_init():
93 | window.f_type = ImgConfig()
94 |
95 |
96 | def open_image():
97 | # 打开文件对话框
98 | file_dir, _ = QFileDialog.getOpenFileName(window.pushButton, "打开图片", QDir.currentPath(),
99 | "图片文件(*.jpg *.png *.bmp);;所有文件(*)")
100 | # 判断是否正确打开文件
101 | if not file_dir:
102 | QMessageBox.warning(window.pushButton, "警告", "文件不存在或打开文件失败!", QMessageBox.Yes)
103 | return None, None
104 | print("读入文件成功")
105 | # 分离路径和文件名
106 | (dir_name, full_file_name) = os.path.split(file_dir)
107 | # 分离文件主名和扩展名
108 | (file_name, file_type) = os.path.splitext(full_file_name)
109 | # 获取文件路径下 所有以当前文件扩展名结尾的文件
110 | files = glob(os.path.join(dir_name, "*{}".format(file_type)))
111 | # 返回 文件列表,当前选中的文件名
112 | return files, full_file_name
113 |
114 |
115 | # 直接显示图像,该函数使用Qt的函数读取图片,
116 | # 没有结合OpenCv或者Numpy
117 | def direct_show_image(img):
118 | # 使用Qt自带的图像格式读取
119 | pixmap = QPixmap(img)
120 | # 缩放以适应窗口大小,由于label.size()包含了显示区域和边界,因此要减去两边各1像素的边界
121 | pixmap = pixmap.scaled(window.label.size() - QSize(2, 2))
122 | # 在label上显示图像
123 | window.label.setPixmap(pixmap)
124 | # 状态栏显示
125 | (f_dir, f_name) = os.path.split(img)
126 | window.statusbar.showMessage("文件名:{}".format(f_name))
127 |
128 |
129 | def open_video():
130 | # 打开文件对话框
131 | file_dir, _ = QFileDialog.getOpenFileName(window.pushButton, "打开视频", QDir.currentPath(),
132 | "视频文件(*.mp4 *.avi );;所有文件(*)")
133 | # 判断是否正确打开文件
134 | if not file_dir:
135 | QMessageBox.warning(window.pushButton, "警告", "文件不存在或打开文件失败!", QMessageBox.Yes)
136 | return None
137 | print("读入文件成功")
138 | # 返回视频路径
139 | return file_dir
140 |
141 |
142 | # 定义视频类
143 | class VdoConfig:
144 | def __init__(self):
145 | window.pushButton.setEnabled(False)
146 | window.pushButton_2.setEnabled(False)
147 | window.pushButton_3.setEnabled(False)
148 | self.file = open_video()
149 | if not self.file:
150 | return
151 | window.label.setText("正在读取请稍后...")
152 |
153 | # 设置时钟
154 | self.v_timer = QTimer()
155 | # 读取视频
156 | self.cap = cv2.VideoCapture(self.file)
157 | if not self.cap:
158 | print("打开视频失败")
159 | return
160 | # 获取视频FPS
161 | self.fps = self.cap.get(cv2.CAP_PROP_FPS)
162 | # 获取视频总帧数
163 | self.total_f = self.cap.get(cv2.CAP_PROP_FRAME_COUNT)
164 | # 获取视频当前帧所在的帧数
165 | self.current_f = self.cap.get(cv2.CAP_PROP_POS_FRAMES)
166 | # 设置定时器周期,单位毫秒
167 | self.v_timer.start(int(1000/self.fps))
168 | print("FPS:".format(self.fps))
169 |
170 | window.pushButton.setEnabled(True)
171 | window.pushButton_2.setEnabled(True)
172 | window.pushButton_3.setEnabled(True)
173 | window.pushButton.setText("播放")
174 | window.pushButton_2.setText("快退")
175 | window.pushButton_3.setText("快进")
176 |
177 | # 连接定时器周期溢出的槽函数,用于显示一帧视频
178 | self.v_timer.timeout.connect(self.show_pic)
179 | # 连接按钮和对应槽函数,lambda表达式用于传参
180 | window.pushButton.clicked.connect(self.go_pause)
181 | window.pushButton_2.pressed.connect(lambda: self.last_img(True))
182 | window.pushButton_2.clicked.connect(lambda: self.last_img(False))
183 | window.pushButton_3.pressed.connect(lambda: self.next_img(True))
184 | window.pushButton_3.clicked.connect(lambda: self.next_img(False))
185 | print("init OK")
186 |
187 | def show_pic(self):
188 | # 读取一帧
189 | success, frame = self.cap.read()
190 | if success:
191 | # Mat格式图像转Qt中图像的方法
192 | show = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
193 | showImage = QImage(show.data, show.shape[1], show.shape[0], QImage.Format_RGB888)
194 | window.label.setPixmap(QPixmap.fromImage(showImage))
195 |
196 | # 状态栏显示信息
197 | self.current_f = self.cap.get(cv2.CAP_PROP_POS_FRAMES)
198 | current_t, total_t = self.calculate_time(self.current_f, self.total_f, self.fps)
199 | window.statusbar.showMessage("文件名:{} {}({})".format(self.file, current_t, total_t))
200 |
201 | def show_pic_back(self):
202 | # 获取视频当前帧所在的帧数
203 | self.current_f = self.cap.get(cv2.CAP_PROP_POS_FRAMES)
204 | # 设置下一次帧为当前帧-2
205 | self.cap.set(cv2.CAP_PROP_POS_FRAMES, self.current_f-2)
206 | # 读取一帧
207 | success, frame = self.cap.read()
208 | if success:
209 | show = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
210 | showImage = QImage(show.data, show.shape[1], show.shape[0], QImage.Format_RGB888)
211 | window.label.setPixmap(QPixmap.fromImage(showImage))
212 |
213 | # 状态栏显示信息
214 | current_t, total_t = self.calculate_time(self.current_f-1, self.total_f, self.fps)
215 | window.statusbar.showMessage("文件名:{} {}({})".format(self.file, current_t, total_t))
216 |
217 |
218 | # 快退
219 | def last_img(self, t):
220 | window.pushButton.setText("播放")
221 | if t:
222 | # 断开槽连接
223 | self.v_timer.timeout.disconnect(self.show_pic)
224 | # 连接槽连接
225 | self.v_timer.timeout.connect(self.show_pic_back)
226 | self.v_timer.start(int(1000/self.fps)/2)
227 | else:
228 | self.v_timer.timeout.disconnect(self.show_pic_back)
229 | self.v_timer.timeout.connect(self.show_pic)
230 | self.v_timer.start(int(1000/self.fps))
231 |
232 | # 快进
233 | def next_img(self, t):
234 | window.pushButton.setText("播放")
235 | if t:
236 | self.v_timer.start(int(1000/self.fps)/2)
237 | else:
238 | self.v_timer.start(int(1000/self.fps))
239 |
240 | # 暂停播放
241 | def go_pause(self):
242 | if window.pushButton.text() == "播放":
243 | self.v_timer.stop()
244 | window.pushButton.setText("暂停")
245 | elif window.pushButton.text() == "暂停":
246 | self.v_timer.start(int(1000/self.fps))
247 | window.pushButton.setText("播放")
248 |
249 | def calculate_time(self, c_f, t_f, fps):
250 | total_seconds = int(t_f/fps)
251 | current_sec = int(c_f/fps)
252 | c_time = "{}:{}:{}".format(int(current_sec/3600), int((current_sec % 3600)/60), int(current_sec % 60))
253 | t_time = "{}:{}:{}".format(int(total_seconds / 3600), int((total_seconds % 3600) / 60), int(total_seconds % 60))
254 | return c_time, t_time
255 |
256 |
257 | def VdoConfig_init():
258 | window.f_type = VdoConfig()
259 |
260 |
261 | if __name__ == '__main__':
262 | app = QtWidgets.QApplication(sys.argv)
263 | window = MainWindow()
264 | window.window_init()
265 | window.show()
266 | sys.exit(app.exec_())
267 |
--------------------------------------------------------------------------------
/Lesson_05.结合OpenCV实现视频播放器/mainwindow.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | MainWindow
4 |
5 |
6 |
7 | 0
8 | 0
9 | 800
10 | 600
11 |
12 |
13 |
14 | MainWindow
15 |
16 |
17 |
18 | -
19 |
20 |
-
21 |
22 |
23 |
24 | 0
25 | 1
26 |
27 |
28 |
29 | QFrame::Box
30 |
31 |
32 | Open file
33 |
34 |
35 | Qt::AlignCenter
36 |
37 |
38 |
39 | -
40 |
41 |
-
42 |
43 |
44 | 上一张/快退10S
45 |
46 |
47 |
48 | -
49 |
50 |
51 | 播放/暂停
52 |
53 |
54 |
55 | -
56 |
57 |
58 | 下一张/快进10秒
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
86 |
87 |
88 |
89 | Open image
90 |
91 |
92 |
93 |
94 | Open video
95 |
96 |
97 |
98 |
99 |
100 |
101 |
--------------------------------------------------------------------------------
/Lesson_05.结合OpenCV实现视频播放器/ui_mainwindow.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Form implementation generated from reading ui file 'mainwindow.ui',
4 | # licensing of 'mainwindow.ui' applies.
5 | #
6 | # Created: Sun Mar 3 19:33:31 2019
7 | # by: pyside2-uic running on PySide2 5.11.4a1.dev1546291887
8 | #
9 | # WARNING! All changes made in this file will be lost!
10 |
11 | from PySide2 import QtCore, QtGui, QtWidgets
12 |
13 | class Ui_MainWindow(object):
14 | def setupUi(self, MainWindow):
15 | MainWindow.setObjectName("MainWindow")
16 | MainWindow.resize(800, 600)
17 | self.centralwidget = QtWidgets.QWidget(MainWindow)
18 | self.centralwidget.setObjectName("centralwidget")
19 | self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.centralwidget)
20 | self.verticalLayout_2.setObjectName("verticalLayout_2")
21 | self.verticalLayout = QtWidgets.QVBoxLayout()
22 | self.verticalLayout.setObjectName("verticalLayout")
23 | self.label = QtWidgets.QLabel(self.centralwidget)
24 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
25 | sizePolicy.setHorizontalStretch(0)
26 | sizePolicy.setVerticalStretch(1)
27 | sizePolicy.setHeightForWidth(self.label.sizePolicy().hasHeightForWidth())
28 | self.label.setSizePolicy(sizePolicy)
29 | self.label.setFrameShape(QtWidgets.QFrame.Box)
30 | self.label.setAlignment(QtCore.Qt.AlignCenter)
31 | self.label.setObjectName("label")
32 | self.verticalLayout.addWidget(self.label)
33 | self.horizontalLayout = QtWidgets.QHBoxLayout()
34 | self.horizontalLayout.setObjectName("horizontalLayout")
35 | self.pushButton_2 = QtWidgets.QPushButton(self.centralwidget)
36 | self.pushButton_2.setObjectName("pushButton_2")
37 | self.horizontalLayout.addWidget(self.pushButton_2)
38 | self.pushButton = QtWidgets.QPushButton(self.centralwidget)
39 | self.pushButton.setObjectName("pushButton")
40 | self.horizontalLayout.addWidget(self.pushButton)
41 | self.pushButton_3 = QtWidgets.QPushButton(self.centralwidget)
42 | self.pushButton_3.setObjectName("pushButton_3")
43 | self.horizontalLayout.addWidget(self.pushButton_3)
44 | self.verticalLayout.addLayout(self.horizontalLayout)
45 | self.verticalLayout_2.addLayout(self.verticalLayout)
46 | MainWindow.setCentralWidget(self.centralwidget)
47 | self.menubar = QtWidgets.QMenuBar(MainWindow)
48 | self.menubar.setGeometry(QtCore.QRect(0, 0, 800, 23))
49 | self.menubar.setObjectName("menubar")
50 | self.menu = QtWidgets.QMenu(self.menubar)
51 | self.menu.setObjectName("menu")
52 | MainWindow.setMenuBar(self.menubar)
53 | self.statusbar = QtWidgets.QStatusBar(MainWindow)
54 | self.statusbar.setObjectName("statusbar")
55 | MainWindow.setStatusBar(self.statusbar)
56 | self.actionOpen_image = QtWidgets.QAction(MainWindow)
57 | self.actionOpen_image.setObjectName("actionOpen_image")
58 | self.actionOpen_video = QtWidgets.QAction(MainWindow)
59 | self.actionOpen_video.setObjectName("actionOpen_video")
60 | self.menu.addAction(self.actionOpen_image)
61 | self.menu.addAction(self.actionOpen_video)
62 | self.menubar.addAction(self.menu.menuAction())
63 |
64 | self.retranslateUi(MainWindow)
65 | QtCore.QMetaObject.connectSlotsByName(MainWindow)
66 |
67 | def retranslateUi(self, MainWindow):
68 | MainWindow.setWindowTitle(QtWidgets.QApplication.translate("MainWindow", "MainWindow", None, -1))
69 | self.label.setText(QtWidgets.QApplication.translate("MainWindow", "Open file", None, -1))
70 | self.pushButton_2.setText(QtWidgets.QApplication.translate("MainWindow", "上一张/快退10S", None, -1))
71 | self.pushButton.setText(QtWidgets.QApplication.translate("MainWindow", "播放/暂停", None, -1))
72 | self.pushButton_3.setText(QtWidgets.QApplication.translate("MainWindow", "下一张/快进10秒", None, -1))
73 | self.menu.setTitle(QtWidgets.QApplication.translate("MainWindow", "Open", None, -1))
74 | self.actionOpen_image.setText(QtWidgets.QApplication.translate("MainWindow", "Open image", None, -1))
75 | self.actionOpen_video.setText(QtWidgets.QApplication.translate("MainWindow", "Open video", None, -1))
76 |
77 |
--------------------------------------------------------------------------------
/Lesson_06.另一种槽连接机制/Slot.py:
--------------------------------------------------------------------------------
1 | # -*- coding:utf-8 -*-
2 | """
3 | @Author: Fei Xue
4 | @E-Mail: FeiXue@nuaa.edu.cn
5 | @File: Slot.py
6 | @Time: 2020/6/11 15:41
7 | @Introduction: 另一种槽机制
8 | """
9 |
10 | import sys
11 | import random
12 | from PySide2 import QtWidgets, QtCore
13 |
14 |
15 | class MainWindow(QtWidgets.QMainWindow):
16 | def __init__(self):
17 | super(MainWindow, self).__init__()
18 |
19 | self.hello = ["Hallo Welt", "Hei maailma", "Hola Mundo", "Привет мир", "Hello world"]
20 | self.resize(400, 300)
21 | self.setWindowTitle('Slot')
22 |
23 | # --- 布局和控件 ---#
24 |
25 | # 中心控件
26 | self.center_widget = QtWidgets.QWidget()
27 | # 中心控件布局
28 | self.center_widget_layout = QtWidgets.QVBoxLayout()
29 | # 标签
30 | self.show_word = QtWidgets.QLabel('Hello world')
31 | # 设置标签居中
32 | self.show_word.setAlignment(QtCore.Qt.AlignCenter)
33 | # 按钮
34 | self.push_word = QtWidgets.QPushButton('Random word')
35 | # 设置按钮ObjectName,使用connectSlotsByName必须设定
36 | self.push_word.setObjectName('push_word')
37 | # 将标签添加到布局里
38 | self.center_widget_layout.addWidget(self.show_word)
39 | # 将按钮添加到布局里
40 | self.center_widget_layout.addWidget(self.push_word)
41 | # 将布放置到中心控件上
42 | self.center_widget.setLayout(self.center_widget_layout)
43 | # 将中心控件放置到主窗口内
44 | self.setCentralWidget(self.center_widget)
45 |
46 | # 设置通过ObjectName来连接槽函数
47 | QtCore.QMetaObject.connectSlotsByName(self)
48 |
49 | # 指定下面的函数是槽函数
50 | @QtCore.Slot()
51 | # on_ObjectName_信号 来定义槽函数名
52 | def on_push_word_clicked(self):
53 | self.show_word.setText(random.choice(self.hello))
54 |
55 |
56 | if __name__ == '__main__':
57 | app = QtWidgets.QApplication(sys.argv)
58 | window = MainWindow()
59 | window.show()
60 | sys.exit(app.exec_())
61 |
--------------------------------------------------------------------------------
/Lesson_06.另一种槽连接机制/readme.md:
--------------------------------------------------------------------------------
1 | # 信号与槽连接
2 |
3 | PyQt5或者PySide2中提供了两种槽连接方式,它们各有优点。
4 |
5 | ### 1. 通过connect方式连接
6 |
7 | * 优点:代码和逻辑关联紧密,槽函数可以复用
8 | * 缺点:代码繁琐(每个信号连接都需要使用connect语句)
9 | * 示例:
10 |
11 | ```python
12 | # 定义槽函数
13 | def pushButton_slot_fun(self, x):
14 | print(x)
15 |
16 | # 定义控件
17 | push_word = QtWidgets.QPushButton('Random word')
18 | push_word2 = QtWidgets.QPushButton('Random word')
19 | # 槽连接,使用lambda表达式传参
20 | push_word.clicked.connect(lambda: pushButton_slot_fun('参数1'))
21 | push_word2.clicked.connect(lambda: pushButton_slot_fun('参数2'))
22 | ```
23 |
24 | ### 2. 通过connectSlotsByName方式连接
25 |
26 | * 优点:代码整洁美观
27 | * 缺点:槽函数唯一,只能传信号自带参数 [Issue](https://github.com/se7enXF/pyside2/issues/2#issuecomment-664003084)
28 | * 示例:
29 |
30 | ```python
31 | # 定义控件
32 | push_word = QtWidgets.QPushButton('Random word')
33 | push_word2 = QtWidgets.QPushButton('Random word')
34 | # 必须设置控件的名称,即ObjectName
35 | push_word.setObjectName('push_word')
36 | push_word2.setObjectName('push_word2')
37 |
38 | # parent为发出槽信号的控件的parent,如是当前窗口的控件发出信号,这里填self
39 | # 这句话必须在所有setObjectName之后才会生效
40 | QtCore.QMetaObject.connectSlotsByName(parent)
41 |
42 | # 使用@QtCore.Slot()修饰下面的函数,说明该函数是槽函数
43 | @QtCore.Slot()
44 | # 槽函数名的格式为:on_[ObjectName]_[信号]
45 | def on_push_word_clicked():
46 | pass
47 |
48 | @QtCore.Slot()
49 | def on_push_word2_clicked():
50 | pass
51 | ```
52 |
53 | ### 最后
54 | * 之前的代码都用【designer生成+py脚本调用】的方式,认为这种方式编程快速。
55 | 在我真正部署了大项目之后,才发现完全键入的代码才更有生命力。增删改除控件在
56 | 两步走的方式中存在很大问题,而实践中这些动作是必不可少的。
57 | * 说明一下我的代码方式。首先写出框架和基本控件,如果哪个控件的某个熟悉怎么
58 | 设置不知道,我会打开designer然后随便放置一个去查看,然后尝试使用set等命令。
59 | 如果上述方法不通,我还会打开[Qt文档](https://doc.qt.io/qtforpython/modules.html)
60 | 去查看控件的方法,函数,信号,例子。
61 | * 之后的代码将不再使用两步走的方案,我会提供一个快捷方便的函数来构建布局。
62 | * 下一节([第七课](../Lesson_07.主窗口的构成/readme.md))会介绍Qt主窗口的构成。
--------------------------------------------------------------------------------
/Lesson_07.主窗口的构成/readme.md:
--------------------------------------------------------------------------------
1 | # 主窗口构成
2 | 
3 | 
4 |
5 | * 上图是窗口初始化截图,下图是将dock控件浮动后的截图。
6 | * 从上例分析得知主窗口构成:窗口从上往下由菜单栏,中心区域和状态栏构成。
7 |
8 | ### 中心区域
9 | * 中心区域可以任意布置控件的区域,主要由中心控件,dock窗口,工具栏组成。
10 | * 之前使用QtDesigner的例子,当新建一个窗口时,相当于创建一个窗口包含菜单栏,
11 | 中心区域和状态栏,然后在中心区域放入一个中心控件。我们的操作都是在这个中心
12 | 控件上部署的。
13 | * 只有主窗口可以添加中心控件,工具栏和dock控件。
14 |
15 | ### 最后
16 | * 熟悉掌握窗口的结构,以后控件部署会有条不紊。
17 | * 下一讲([第八课](../Lesson_08.窗口嵌套/readme.md))会介绍多窗口的继承和嵌套。
--------------------------------------------------------------------------------
/Lesson_07.主窗口的构成/struct_main.py:
--------------------------------------------------------------------------------
1 | # -*- coding:utf-8 -*-
2 | """
3 | @Author: Fei Xue
4 | @E-Mail: FeiXue@nuaa.edu.cn
5 | @File: struct_main.py
6 | @Time: 2020/6/11 16:58
7 | @Introduction:
8 | """
9 |
10 |
11 | import sys
12 | from PySide2 import QtWidgets, QtCore
13 |
14 |
15 | class MainWindow(QtWidgets.QMainWindow):
16 | def __init__(self):
17 | super(MainWindow, self).__init__()
18 |
19 | self.resize(1024, 768)
20 | self.setWindowTitle('Main window struct')
21 |
22 | # --- 布局和控件 ---#
23 |
24 | # 中心控件
25 | self.center_widget = QtWidgets.QWidget()
26 | self.center_widget.setStyleSheet('*{background: Gold; color: Black}')
27 | # 中心控件布局
28 | self.center_widget_layout = QtWidgets.QVBoxLayout()
29 | # 标签
30 | self.show_word = QtWidgets.QLabel('这是放在中心控件布局中的标签')
31 | # 设置标签居中
32 | self.show_word.setAlignment(QtCore.Qt.AlignCenter)
33 | # 将标签添加到布局里
34 | self.center_widget_layout.addWidget(self.show_word)
35 | # 将布放置到中心控件上
36 | self.center_widget.setLayout(self.center_widget_layout)
37 |
38 | # Dock窗口1
39 | self.dock = QtWidgets.QDockWidget('这是Dock1的标题')
40 | self.dock_container = QtWidgets.QWidget()
41 | self.dock_layout = QtWidgets.QVBoxLayout()
42 | self.dock_label = QtWidgets.QLabel('这是放在Dock1中的标签')
43 | self.dock_layout.addWidget(self.dock_label)
44 | self.dock_container.setLayout(self.dock_layout)
45 | self.dock.setWidget(self.dock_container)
46 |
47 | # Dock窗口2
48 | self.dock2 = QtWidgets.QDockWidget('这是Dock2的标题')
49 | self.dock2_container = QtWidgets.QWidget()
50 | self.dock2_layout = QtWidgets.QVBoxLayout()
51 | self.dock2_label = QtWidgets.QLabel('这是放在Dock2中的标签')
52 | self.dock2_layout.addWidget(self.dock2_label)
53 | self.dock2_container.setLayout(self.dock2_layout)
54 | self.dock2.setWidget(self.dock2_container)
55 |
56 | # 设置工具栏
57 | self.toolBar_top = QtWidgets.QToolBar()
58 | self.tool_top = QtWidgets.QLabel('这是放在工具栏中的一个标签')
59 | self.toolBar_top.addWidget(self.tool_top)
60 |
61 | # 将中心控件放置到主窗口内
62 | self.setCentralWidget(self.center_widget)
63 | # 将Dock控件放置到主窗口内
64 | self.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.dock)
65 | self.addDockWidget(QtCore.Qt.LeftDockWidgetArea, self.dock2)
66 | # 将工具栏控件放置到主窗口内
67 | self.addToolBar(self.toolBar_top)
68 |
69 | # 设置状态栏永久显示信息
70 | self.statusBar_Permanent = QtWidgets.QLabel('这是放在状态栏中的永久标签')
71 | self.statusBar().addPermanentWidget(self.statusBar_Permanent)
72 | self.statusBar().setStyleSheet('*{background: Aqua; color: Black}')
73 |
74 | # 设置菜单栏
75 | self.menu_setup = QtWidgets.QMenu('这是菜单栏中的一个菜单')
76 | self.menuBar().addMenu(self.menu_setup)
77 | self.menuBar().setStyleSheet('*{background: Aqua; color: Black}')
78 |
79 |
80 | if __name__ == '__main__':
81 | app = QtWidgets.QApplication(sys.argv)
82 | window = MainWindow()
83 | window.show()
84 | sys.exit(app.exec_())
85 |
--------------------------------------------------------------------------------
/Lesson_08.窗口嵌套/readme.md:
--------------------------------------------------------------------------------
1 | # 窗口嵌套
2 |
3 | Qt 中几乎所有控件都是继承于 QtWidgets,所以它们之间可以相互嵌套和继承。本讲演示了两种窗口嵌套后传参的方法。
4 |
5 | ## 第一种:指定子窗口parent
6 |
7 | 此方法要注意以下要点,具体实现请看subwindow.py中第一种窗口示例:
8 | 1. 为了避免一些变量冲突的问题,将主窗口和子窗口的所有变量避免重复定义
9 | 2. 子窗口调用主窗口变量,要用 self.parent().window().[控件或控件]
10 | 3. 主窗口中初始化子窗口要指定parent为自己
11 | 4. 主窗口直接可以调用子窗口变量
12 | 5. 我不建议使用此方法,因为不同窗口之间的变量的耦合度太高了。
13 |
14 | ## 第二种:自定义信号
15 |
16 | 给 QtWidgets 添加自动定义信号,通过信号来实现不同窗口之间的的通信,这种方法是我推荐的。此方法要注意
17 | 以下要点,具体实现请看subwindow.py中第二种窗口示例:
18 |
19 | 1. 此方法不用指定parent,主窗口和子窗口是独立的
20 | 2. 子窗口中,要在def __init__ 函数之前定义信号,函数中通过[自定义信号].emit()来发出信号。
21 | 自定义信号要指定信号类型,可以是str,int等,emit的参数类型要和信号定义的类型一致
22 | 3. 主窗口中,初始化子窗口后要连接信号
23 | `TODO: 此处的信号连接使用connectSlotsByName无效?`
24 | 4. 信号的槽函数参数和发出新信号的类型要一致
25 |
26 | * 下一讲([第九课](../Lesson_09.EzQtTools/readme.md))会介绍我自己定义的Qt工具,让控件设置不再繁琐。
--------------------------------------------------------------------------------
/Lesson_08.窗口嵌套/subwindow.py:
--------------------------------------------------------------------------------
1 | # -*- coding:utf-8 -*-
2 | """
3 | @Author: Fei Xue
4 | @E-Mail: FeiXue@nuaa.edu.cn
5 | @File: subwindow.py
6 | @Time: 2020/7/1 15:58
7 | @Introduction:
8 | """
9 |
10 |
11 | import sys
12 | import random
13 | from PySide2 import QtWidgets, QtCore
14 |
15 |
16 | class MainWindow(QtWidgets.QMainWindow):
17 | def __init__(self):
18 | super(MainWindow, self).__init__()
19 |
20 | self.hello = ["Hallo Welt", "Hei maailma", "Hola Mundo", "Привет мир", "Hello world"]
21 | self.resize(1024, 768)
22 | self.setWindowTitle('Main window struct')
23 |
24 | # --- 布局和控件 ---#
25 |
26 | # 中心控件
27 | self.center_widget = QtWidgets.QWidget()
28 | # 中心控件布局
29 | self.center_widget_layout = QtWidgets.QVBoxLayout()
30 | # 标签
31 | self.show_word = QtWidgets.QLabel('Hello world')
32 | self.show_word.setAlignment(QtCore.Qt.AlignTop | QtCore.Qt.AlignHCenter)
33 | self.select = QtWidgets.QPushButton('Click to open sub-window1')
34 | self.select.setObjectName('subwindow')
35 | self.select2 = QtWidgets.QPushButton('Click to open sub-window2')
36 | self.select2.setObjectName('subwindow2')
37 | # 将标签添加到布局里
38 | self.center_widget_layout.addWidget(self.show_word)
39 | self.center_widget_layout.addWidget(self.select)
40 | self.center_widget_layout.addWidget(self.select2)
41 | # 将布放置到中心控件上
42 | self.center_widget.setLayout(self.center_widget_layout)
43 | # 将中心控件设定到主窗口
44 | self.setCentralWidget(self.center_widget)
45 |
46 | # 初始化子窗口
47 | self.subwindow = SubWindow1(parent=self)
48 | self.subwindow2 = SubWindow2()
49 | # 连接子窗口的自定义信号和槽函数
50 | self.subwindow2.signal_.connect(self.on_sub2_signal_)
51 |
52 | # 设置通过ObjectName来连接槽函数
53 | QtCore.QMetaObject.connectSlotsByName(self)
54 |
55 |
56 | @QtCore.Slot()
57 | def on_subwindow_clicked(self):
58 | self.subwindow.show()
59 |
60 | @QtCore.Slot()
61 | def on_subwindow2_clicked(self):
62 | self.subwindow2.show()
63 |
64 | def on_sub2_signal_(self, num):
65 | self.show_word.setText(random.choice(self.hello))
66 |
67 |
68 | class SubWindow1(QtWidgets.QMainWindow):
69 | def __init__(self, parent=None):
70 | """
71 | 第一种传参的方法:
72 | 1. 为了避免一些问题的出现,避免所有变量重复定义
73 | 2. 子窗口调用主窗口变量,要用 self.parent().window().[控件]
74 | 3. 主窗口中初始化子窗口要指定parent为自己
75 | 4. 主窗口直接可以调用子窗口变量
76 | :param parent:
77 | """
78 | super(SubWindow1, self).__init__(parent=parent)
79 |
80 | self.setWindowTitle('Sub window')
81 | self.setFixedSize(600, 250)
82 | self.setWindowModality(QtCore.Qt.ApplicationModal)
83 |
84 | # --- 布局和控件 ---#
85 |
86 | # 中心控件
87 | self.center_widget_sub = QtWidgets.QWidget()
88 | # 中心控件布局
89 | self.center_widget_layout_sub = QtWidgets.QVBoxLayout()
90 | # 按钮
91 | self.select_sub = QtWidgets.QPushButton('Click to change word in MainWindow')
92 | self.select_sub.setObjectName('words')
93 | # 将标签添加到布局里
94 | self.center_widget_layout_sub.addWidget(self.select_sub)
95 | # 将布放置到中心控件上
96 | self.center_widget_sub.setLayout(self.center_widget_layout_sub)
97 | # 将中心控件设定到主窗口
98 | self.setCentralWidget(self.center_widget_sub)
99 |
100 | # 设置通过ObjectName来连接槽函数
101 | QtCore.QMetaObject.connectSlotsByName(self)
102 |
103 | @QtCore.Slot()
104 | def on_words_clicked(self):
105 | self.parent().window().show_word.setText(random.choice(self.parent().window().hello))
106 |
107 |
108 | class SubWindow2(QtWidgets.QMainWindow):
109 | signal_ = QtCore.Signal(int)
110 |
111 | def __init__(self, parent=None):
112 | """
113 | 第二种传参的方法,自定义信号:
114 | 1. 此方法不用指定parent,主窗口和子窗口是独立的
115 | 2. 子窗口中,要在def init 函数之前定义信号,函数中通过[自定义信号].emit()来发出信号
116 | 3. 主窗口中,初始化子窗口后要连接信号
117 | 4. 信号的槽函数参数和发出新信号的类型要一致
118 | :param parent:
119 | """
120 | super(SubWindow2, self).__init__(parent=parent)
121 |
122 | self.setWindowTitle('Sub window')
123 | self.setFixedSize(600, 250)
124 | self.setWindowModality(QtCore.Qt.ApplicationModal)
125 |
126 | # --- 布局和控件 ---#
127 |
128 | # 中心控件
129 | self.center_widget_sub = QtWidgets.QWidget()
130 | # 中心控件布局
131 | self.center_widget_layout_sub = QtWidgets.QVBoxLayout()
132 | # 按钮
133 | self.select_sub = QtWidgets.QPushButton('Click to change word in MainWindow')
134 | self.select_sub.setObjectName('words')
135 | # 将标签添加到布局里
136 | self.center_widget_layout_sub.addWidget(self.select_sub)
137 | # 将布放置到中心控件上
138 | self.center_widget_sub.setLayout(self.center_widget_layout_sub)
139 | # 将中心控件设定到主窗口
140 | self.setCentralWidget(self.center_widget_sub)
141 |
142 | # 设置通过ObjectName来连接槽函数
143 | QtCore.QMetaObject.connectSlotsByName(self)
144 |
145 | @QtCore.Slot()
146 | def on_words_clicked(self):
147 | self.signal_.emit(1)
148 |
149 |
150 | if __name__ == '__main__':
151 | app = QtWidgets.QApplication(sys.argv)
152 | window = MainWindow()
153 | window.show()
154 | sys.exit(app.exec_())
155 |
--------------------------------------------------------------------------------
/Lesson_09.EzQtTools/main.py:
--------------------------------------------------------------------------------
1 | # -*- coding:utf-8 -*-
2 | """
3 | @Author: Fei Xue
4 | @E-Mail: FeiXue@nuaa.edu.cn
5 | @File: main.py.py
6 | @Time: 2020/7/1 17:10
7 | @Introduction: 此例只适用于点击链接直接跳转的网页,不适用于点击链接打开新tab或者窗口的网页
8 | """
9 |
10 | import sys
11 | from EzQtTools import *
12 | from PySide2.QtWebEngineWidgets import QWebEngineView
13 |
14 |
15 | class MainWindow(EzMainWindow):
16 |
17 | def __init__(self, **kwargs):
18 | EzMainWindow.__init__(self, **kwargs)
19 | self.__init__widget()
20 |
21 | def __init__widget(self):
22 | """
23 | 界面大概组成如下:
24 |
25 | -------------------------------------
26 | 后退|前进|刷新|主页| URL
27 | -------------------------------------
28 | 显示区域
29 | -------------------------------------
30 |
31 | 根据上述格式,我们从大到小分块构成布局
32 | 1. 垂直分两块
33 | 2. 第一块水平分五块
34 | :return:
35 | """
36 |
37 | # 首先是从上往下的两个个大区域
38 | self.title_area = self.add_layout_widget(self.central_widget, QtWidgets.QWidget(), space=1)
39 | self.pages_area = self.add_layout_widget(self.central_widget, QtWidgets.QWidget(), stretch=1)
40 |
41 | # 给最上面的区域添加项目
42 | self.back = self.add_layout_widget(self.title_area, QtWidgets.QPushButton('后退'), QtWidgets.QHBoxLayout())
43 | self.ford = self.add_layout_widget(self.title_area, QtWidgets.QPushButton('前进'))
44 | self.refs = self.add_layout_widget(self.title_area, QtWidgets.QPushButton('刷新'))
45 | self.home = self.add_layout_widget(self.title_area, QtWidgets.QPushButton('主页'))
46 | self.urls = self.add_layout_widget(self.title_area, QtWidgets.QLineEdit())
47 | self.goto = self.add_layout_widget(self.title_area, QtWidgets.QPushButton('打开'))
48 |
49 | # 给显示区域添加浏览控件
50 | self.web_browser = self.add_layout_widget(self.pages_area, QWebEngineView(), stretch=1)
51 |
52 | # 定义槽函数
53 | def open_home():
54 | url_home = 'https://gitee.com/se7enXF/pyside2'
55 | self.web_browser.load(QtCore.QUrl(url_home))
56 | self.urls.setText(url_home)
57 |
58 | def open_last():
59 | self.web_browser.back()
60 |
61 | def open_next():
62 | self.web_browser.forward()
63 |
64 | def open_refs():
65 | self.web_browser.reload()
66 |
67 | def open_goto():
68 | url = self.urls.text()
69 | self.web_browser.load(QtCore.QUrl(url))
70 |
71 | def correct_title():
72 | title = self.web_browser.title()
73 | self.setWindowTitle(title)
74 |
75 | # connections
76 | self.home.clicked.connect(open_home)
77 | self.back.clicked.connect(open_last)
78 | self.ford.clicked.connect(open_next)
79 | self.refs.clicked.connect(open_refs)
80 | self.goto.clicked.connect(open_goto)
81 | self.urls.returnPressed.connect(open_goto)
82 | self.web_browser.loadFinished.connect(correct_title)
83 |
84 |
85 | if __name__ == '__main__':
86 | app = QtWidgets.QApplication(sys.argv)
87 | window = MainWindow(title='浏览器', default_layout_margins=2, default_layout_space=2, max_window=True)
88 | window.show()
89 | sys.exit(app.exec_())
90 |
--------------------------------------------------------------------------------
/Lesson_09.EzQtTools/readme.md:
--------------------------------------------------------------------------------
1 | # EzQtTools
2 |
3 | 为了统筹兼顾 QtDesigner 快捷的优点和使用代码灵活的好处,我定义了一个工具名为 EzQtTools,里面已经初始化了基本界面和
4 | 一些基本功能。具体使用方法在其中有详细定义。main.py 演示了如何使用此工具。
5 |
6 | ## 工具简介
7 |
8 | EzQtTools.py放置在此项目根目录下,方便调用。此工具暂时只有一个 python 类,名为 EzMainWindow。它继承于 QMainWindow,
9 | 因此 QMainWindow 所有属性和方法仍适用于 EzMainWindow。
10 |
11 | 1. 初始化
12 | 初始化的参数在工具中有详细介绍,主要完成了类似于 QtDesigner 新建一个窗口文件的工作。附加的关于布局属性的参数,可以方便
13 | 布局格式的布置。
14 |
15 | 2. 主要的函数 `add_layout_widget`
16 | 该函数是此工具的核心,在父控件上放置子控件并设置布局,避免了很多冗余的代码操作。具体方法请查看此函数。
17 |
18 | 3. 自定义布局设置
19 | 主窗口初始化的最后一步,是执行 `__init__widget` 函数,需要在自己的窗口类别中重写此函数来实现剩余布局。
20 |
21 | ## 使用方法
22 |
23 | 工具的使用方法已经在main.py中展示,相比之前的明显减少了代码量。
24 | 1. 首先引用此工具`from EzQtTools import *`
25 | 2. 然后定义自己的窗口类,它继承于`EzMainWindow`
26 | 3. 在自定义窗口类的初始化函数中,先初始化父类,然后调用 `__init__widget` 函数。
27 | 4. 在自定义窗口类中重写 `__init__widget` 函数以设置自己的布局。
28 | 5. 类的参数是关键词匹配传参
29 |
30 | ## 写在最后
31 |
32 | 工具只是初步完成,功能还不完善,使用中如有什么意见或建议,欢迎提出。
33 | * 下一讲[自适应大小图像Label](../Lesson_10.AutoSizeImage/readme.md)。
34 |
--------------------------------------------------------------------------------
/Lesson_10.AutoSizeImage/main.py:
--------------------------------------------------------------------------------
1 | # -*- coding:utf-8 -*-
2 | """
3 | @Author: Fei Xue
4 | @E-Mail: FeiXue@nuaa.edu.cn
5 | @File: main.py
6 | @Time: 2020/7/24 22:32
7 | @Introduction: 图变大小跟随窗口变化
8 | """
9 |
10 | from EzQtTools import *
11 | import sys
12 | import cv2
13 | from PySide2.QtCore import QTimer, Signal
14 | from PySide2.QtGui import QPixmap, QImage, QCursor
15 |
16 |
17 | class MainWindow(EzMainWindow):
18 |
19 | def __init__(self, **kwargs):
20 | EzMainWindow.__init__(self, **kwargs)
21 | self.__init__widget()
22 |
23 | def __init__widget(self):
24 | # up, down
25 | self.image_area = self.add_layout_widget(self.central_widget, QtWidgets.QWidget(), stretch=1)
26 | self.control_area = self.add_layout_widget(self.central_widget, QtWidgets.QWidget(), stretch=0)
27 |
28 | # image
29 | self.image = self.add_layout_widget(self.image_area, ImageLabel(self.image_area))
30 | # progress widget
31 | self.progress = self.add_layout_widget(self.control_area, QtWidgets.QWidget())
32 | # control widget
33 | self.control = self.add_layout_widget(self.control_area, QtWidgets.QWidget())
34 |
35 | # button in control
36 | self.back = self.add_layout_widget(self.control, QtWidgets.QPushButton('快退'), QtWidgets.QHBoxLayout())
37 | self.play = self.add_layout_widget(self.control, QtWidgets.QPushButton('播放'))
38 | self.next = self.add_layout_widget(self.control, QtWidgets.QPushButton('快进'))
39 | self.back.setEnabled(False)
40 | self.play.setEnabled(False)
41 | self.next.setEnabled(False)
42 |
43 | # progress area
44 | self.progress_bar = self.add_layout_widget(self.progress, QtWidgets.QSlider(QtCore.Qt.Horizontal), QtWidgets.QHBoxLayout())
45 | self.progress_text = self.add_layout_widget(self.progress, QtWidgets.QLabel('00:00:00/00:00:00'))
46 | self.progress_bar.setEnabled(False)
47 |
48 | # menubar
49 | self.open = QtWidgets.QMenu('打开')
50 | self.menuBar().addMenu(self.open)
51 | # menu
52 | self.open_video = QtWidgets.QAction('打开视频')
53 | self.open.addAction(self.open_video)
54 |
55 | # video player
56 | self.video_player = Video()
57 |
58 | # connections
59 | self.open_video.triggered.connect(self.open_video_play)
60 | self.video_player.signal_image_show.connect(self.show_image_label)
61 | self.back.clicked.connect(lambda: self.video_player.play(1))
62 | self.back.pressed.connect(lambda: self.video_player.play(-2))
63 | self.next.clicked.connect(lambda: self.video_player.play(1))
64 | self.next.pressed.connect(lambda: self.video_player.play(2))
65 | self.play.clicked.connect(self.play_or_pause)
66 | self.video_player.signal_play_done.connect(self.reset_all)
67 | self.progress_bar.sliderMoved.connect(self.slider_move)
68 | self.progress_bar.sliderPressed.connect(lambda: self.video_player.play(0))
69 | self.progress_bar.sliderReleased.connect(lambda: self.video_player.play(1))
70 | self.image.signal_speed_changed.connect(self.speed_changed)
71 |
72 | # open file and load video into Opencv
73 | def open_video_play(self):
74 | video_path = self.open_file_dialog(default_dir='', types=['mp4', 'flv', 'avi'])
75 | if video_path:
76 | self.video_player.load(video_path)
77 | self.video_player.play(1)
78 | self.play.setText('暂停')
79 |
80 | self.back.setEnabled(True)
81 | self.play.setEnabled(True)
82 | self.next.setEnabled(True)
83 | self.progress_bar.setEnabled(True)
84 |
85 | max_frame = self.video_player.total_f
86 | self.progress_bar.setMaximum(max_frame)
87 | self.progress_text.setText(f'{self.video_player.now_time()}/{self.video_player.total_time()}')
88 |
89 | # show QPixmap into autosize label
90 | def show_image_label(self, img: QPixmap):
91 | if img:
92 | self.image.SetPixmap(img)
93 | self.progress_text.setText(f'{self.video_player.now_time()}/{self.video_player.total_time()}')
94 | self.progress_bar.setValue(self.video_player.current_f)
95 |
96 | def play_or_pause(self):
97 | if self.play.text() == '播放':
98 | self.video_player.play(1)
99 | self.play.setText('暂停')
100 | else:
101 | self.video_player.play(0)
102 | self.play.setText('播放')
103 |
104 | def reset_all(self):
105 | self.video_player.play(0)
106 | self.video_player.cap = None
107 | self.video_player.current_f = 0
108 | self.video_player.total_f = 0
109 | self.video_player.fps = 0
110 | self.back.setEnabled(False)
111 | self.play.setEnabled(False)
112 | self.next.setEnabled(False)
113 | self.progress_bar.setEnabled(False)
114 | self.play.setText('播放')
115 | self.progress_text.setText('00:00:00/00:00:00')
116 | self.progress_bar.setValue(self.video_player.current_f)
117 | self.image.Clear()
118 |
119 | def slider_move(self, pos: int):
120 | self.video_player.move_to(pos)
121 | self.video_player.current_f = pos
122 | self.progress_text.setText(f'{self.video_player.now_time()}/{self.video_player.total_time()}')
123 | self.video_player.read_next_frame()
124 |
125 | def speed_changed(self, speed: int):
126 | if speed == 0:
127 | self.video_player.play(0.5)
128 | else:
129 | self.video_player.play(speed)
130 |
131 |
132 | class ImageLabel(AutoSizeLabel):
133 | """
134 | 这里为了打开AutoSizeLabel的鼠标右键菜单,重写了它的某些事件。
135 | 如果不使用右键菜单,可以将主窗口函数中的
136 | self.image = self.add_layout_widget(self.image_area, ImageLabel(self.image_area))
137 | 替换为
138 | self.image = self.add_layout_widget(self.image_area, AutoSizeLabel(self.image_area))
139 | """
140 | signal_speed_changed = Signal(int)
141 |
142 | def __init__(self, *args, **kwargs):
143 | super(ImageLabel, self).__init__(*args, **kwargs)
144 |
145 | # setup context menu
146 | self.setContextMenuPolicy(QtCore.Qt.DefaultContextMenu)
147 | # define menu
148 | self.contextMenu = QtWidgets.QMenu(self)
149 | self.opt1 = self.contextMenu.addAction('0.5倍速播放')
150 | self.opt2 = self.contextMenu.addAction('1倍速播放')
151 | self.opt3 = self.contextMenu.addAction('2倍速播放')
152 | self.opt4 = self.contextMenu.addAction('4倍速播放')
153 | self.contextMenu.triggered.connect(self.menuSlot)
154 | self.contextMenu.sizeHint()
155 |
156 | def contextMenuEvent(self, *args, **kwargs):
157 | self.contextMenu.popup(QCursor.pos())
158 |
159 | def menuSlot(self, act):
160 | act_text = act.text()
161 | speed = int(act_text[0])
162 | self.signal_speed_changed.emit(speed)
163 |
164 |
165 | class Video(QtCore.QObject):
166 |
167 | signal_image_show = Signal(QPixmap)
168 | signal_play_done = Signal()
169 |
170 | def __init__(self):
171 | super(Video, self).__init__()
172 |
173 | self.v_timer = QTimer()
174 | self.v_timer.timeout.connect(lambda: self.play(1))
175 | self.cap = None
176 | self.current_f = 0
177 | self.total_f = 0
178 | self.fps = 0
179 |
180 | def load(self, video_path):
181 | self.cap = cv2.VideoCapture(video_path)
182 | assert self.cap, f"Open file {video_path} error."
183 | self.fps = self.cap.get(cv2.CAP_PROP_FPS)
184 | self.total_f = self.cap.get(cv2.CAP_PROP_FRAME_COUNT)
185 | self.current_f = self.cap.get(cv2.CAP_PROP_POS_FRAMES)
186 | self.v_timer.start(int(1000 / self.fps))
187 |
188 | def play(self, play_speed: float):
189 | """
190 | :param play_speed: 0停止,1正常速度播放,-1,正常速度后退
191 | :return:
192 | """
193 | if not self.cap:
194 | return
195 |
196 | if play_speed == 0:
197 | self.v_timer.stop()
198 | elif play_speed > 0:
199 | self.v_timer.timeout.disconnect()
200 | self.v_timer.timeout.connect(self.read_next_frame)
201 | self.v_timer.start(int(1000 / self.fps) / play_speed)
202 | elif play_speed < 0:
203 | self.v_timer.timeout.disconnect()
204 | self.v_timer.timeout.connect(self.__read_last_frame)
205 | self.v_timer.start(int(1000 / self.fps) / abs(play_speed))
206 |
207 | def move_to(self, pos):
208 | if pos < self.total_f:
209 | self.cap.set(cv2.CAP_PROP_POS_FRAMES, pos)
210 |
211 | def read_next_frame(self):
212 | success, frame = self.cap.read()
213 | if success:
214 | show = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
215 | showImage = QImage(show.data, show.shape[1], show.shape[0], QImage.Format_RGB888)
216 | next_pixmap = QPixmap.fromImage(showImage)
217 | self.signal_image_show.emit(next_pixmap)
218 | self.current_f = self.cap.get(cv2.CAP_PROP_POS_FRAMES)
219 | else:
220 | self.signal_play_done.emit()
221 |
222 | def __read_last_frame(self):
223 | self.current_f = self.cap.get(cv2.CAP_PROP_POS_FRAMES)
224 | self.cap.set(cv2.CAP_PROP_POS_FRAMES, self.current_f-2)
225 | return self.read_next_frame()
226 |
227 | def __frame_time(self, frame_n: int) -> str:
228 | sec = int(frame_n/self.fps)
229 | c_time = f"{int(sec/3600):02d}:{int((sec % 3600)/60):02d}:{int(sec % 60):02d}"
230 | return c_time
231 |
232 | def total_time(self) -> str:
233 | return self.__frame_time(self.total_f)
234 |
235 | def now_time(self) -> str:
236 | return self.__frame_time(self.current_f)
237 |
238 |
239 | if __name__ == '__main__':
240 | app = QtWidgets.QApplication(sys.argv)
241 | window = MainWindow(icon='icon.png', title='播放器', default_layout_margins=2, default_layout_space=2)
242 | window.show()
243 | sys.exit(app.exec_())
244 |
--------------------------------------------------------------------------------
/Lesson_10.AutoSizeImage/readme.md:
--------------------------------------------------------------------------------
1 | # 图片自适应窗口大小变化
2 |
3 | 此例是在Lesson5的基础上改进的,解决了窗口中图片过大时不能缩小窗口的问题。
4 |
5 | ## 问题分析
6 |
7 | Qt布局的缩放不是无限制的,默认大小是0~16777215,即 0x00000000 ~ 0x00FFFFFF。
8 | 当布局中存在不能将尺寸缩小到0的控件时,布局缩小的最小的控件大小后将不再缩小。
9 | Lesson5 的布局中直接放置了一个label,虽然设置了属性 scaledContents 为真,但还是
10 | 存在窗口不能缩小的问题。下面详细解释其中的工作流程:
11 |
12 | 1. 布局中的控件会随窗口变化而改变大小,但其最小大小受控件最小大小影响;
13 | 2. scaledContents 为真时,图片会自适应窗口大小;
14 | 3. 窗口变化导致 label 大于图像本身时,label根据布局变大,图像根据label变大;
15 | 4. 尝试窗口小于图片时,图片写入label,图片本身的大小成为了 label 的大小,
16 | 即限制了 label 的最小大小,进而布局不能进一步变小,导致窗口不能变小。
17 |
18 | ## 解决办法
19 |
20 | 给label不设置布局,而是把它放入一个widget,让他跟随parent尺寸变化。由于没有
21 | 布局的限制,widget大小不受child的影响。
22 |
23 | ## 程序说明
24 |
25 | 1. EzQtTools 中新增了 AutoSizeLabel 类。
26 | 2. main.py 中 重新定义了 Video 类用于解析视频图像,示例了如何自定义上下文菜
27 | 单和信号参数。
28 |
29 | ## 写在最后
30 |
31 | 终于写完毕业论文了,可以好好研究以下Qt的各种问题。已经解决了多媒体播放的问题,请看
32 | 下一讲([第十一课](../Lesson_11.MultimediaPlayer/readme.md))。更新时间为
33 | 2020年12月27日。
34 |
35 |
--------------------------------------------------------------------------------
/Lesson_11.MultimediaPlayer/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/se7enXF/pyside2/81fedb79f14507aff631b3f3b3d27102e5208bd9/Lesson_11.MultimediaPlayer/icon.png
--------------------------------------------------------------------------------
/Lesson_11.MultimediaPlayer/main.py:
--------------------------------------------------------------------------------
1 | # -*- coding:utf-8 -*-
2 | """
3 | @Author: Fei Xue
4 | @E-mail: feixue@nuaa.edu.cn
5 | @File : main.py.py
6 | @Date : 2020/10/15
7 | @Info : 多媒体播放器,支持视频,音频,必须安装本地媒体解码器
8 | 作者使用解码器:https://github.com/Nevcairiel/LAVFilters
9 | """
10 |
11 | from EzQtTools import *
12 | import sys
13 | from PySide2.QtMultimedia import *
14 | from PySide2.QtMultimediaWidgets import QVideoWidget
15 | from PySide2.QtCore import QUrl
16 |
17 |
18 | class MainWindow(EzMainWindow):
19 |
20 | def __init__(self, **kwargs):
21 | EzMainWindow.__init__(self, **kwargs)
22 | self.player_widget = VideoPlayer()
23 | self.__init__widget()
24 | self.total_time = 0
25 | self.now_time = 0
26 | self.play_rate = 1
27 |
28 | def __init__widget(self):
29 | # up, down
30 | self.image_area = self.add_layout_widget(self.central_widget, QtWidgets.QWidget(), stretch=1)
31 | self.control_area = self.add_layout_widget(self.central_widget, QtWidgets.QWidget(), stretch=0)
32 |
33 | # image
34 | self.image = self.add_layout_widget(self.image_area, self.player_widget.video_widget)
35 | # progress widget
36 | self.progress = self.add_layout_widget(self.control_area, QtWidgets.QWidget())
37 | # control widget
38 | self.control = self.add_layout_widget(self.control_area, QtWidgets.QWidget())
39 |
40 | # button in control
41 | self.back = self.add_layout_widget(self.control, QtWidgets.QPushButton('正常速度'), QtWidgets.QHBoxLayout())
42 | self.play = self.add_layout_widget(self.control, QtWidgets.QPushButton('播放'))
43 | self.next = self.add_layout_widget(self.control, QtWidgets.QPushButton('加速播放'))
44 | self.back.setEnabled(False)
45 | self.play.setEnabled(False)
46 | self.next.setEnabled(False)
47 |
48 | # progress area
49 | self.progress_bar = self.add_layout_widget(self.progress, QtWidgets.QSlider(QtCore.Qt.Horizontal),
50 | QtWidgets.QHBoxLayout(), space=20)
51 | self.progress_text = self.add_layout_widget(self.progress, QtWidgets.QLabel('00:00:00/00:00:00'))
52 | self.progress_bar.setEnabled(False)
53 | # volume control
54 | self.volume = self.add_layout_widget(self.progress, QtWidgets.QSlider(QtCore.Qt.Horizontal),
55 | QtWidgets.QHBoxLayout())
56 | self.volume.setFixedWidth(100)
57 | self.volume.setMinimum(0)
58 | self.volume.setMaximum(100)
59 | self.volume.setSingleStep(1)
60 | self.volume.setValue(50)
61 |
62 | # menubar
63 | self.open = QtWidgets.QMenu('打开')
64 | self.menuBar().addMenu(self.open)
65 | # menu
66 | self.open_video = QtWidgets.QAction('打开视频')
67 | self.open.addAction(self.open_video)
68 |
69 | # connections
70 | self.open_video.triggered.connect(self.open_video_play)
71 | self.play.clicked.connect(self.play_or_pause)
72 | self.progress_bar.sliderMoved.connect(self.img_slider_move)
73 | self.volume.sliderMoved.connect(self.volume_slider_move)
74 | self.player_widget.dur_changed.connect(self.auto_set_img_slider_max_pos)
75 | self.player_widget.pos_changed.connect(self.auto_move_img_slider)
76 | self.player_widget.state_changed.connect(self.auto_reset_play_end)
77 | self.back.clicked.connect(self.play_rate_reset)
78 | self.next.clicked.connect(self.play_rate_raise)
79 |
80 | def open_video_play(self):
81 | video_path = self.open_file_dialog(default_dir='', types=['mp4', 'flv', 'avi', 'mp3'])
82 | if video_path:
83 | self.player_widget.video_widget.show()
84 | self.player_widget.set_media(video_path)
85 | self.player_widget.play()
86 |
87 | self.play.setText('暂停')
88 | self.back.setEnabled(True)
89 | self.play.setEnabled(True)
90 | self.next.setEnabled(True)
91 | self.progress_bar.setEnabled(True)
92 |
93 | def play_or_pause(self):
94 | if self.play.text() == '暂停':
95 | self.play.setText('播放')
96 | self.player_widget.pause()
97 | else:
98 | self.play.setText('暂停')
99 | self.player_widget.play()
100 |
101 | def reset_all(self):
102 | self.back.setEnabled(False)
103 | self.play.setEnabled(False)
104 | self.next.setEnabled(False)
105 | self.progress_bar.setEnabled(False)
106 | self.play.setText('播放')
107 | self.progress_text.setText('00:00:00 / 00:00:00')
108 | self.progress_bar.setValue(0)
109 | self.player_widget.video_widget.hide()
110 |
111 | def img_slider_move(self, pos: int):
112 | self.player_widget.set_pos(pos)
113 |
114 | def volume_slider_move(self, pos: int):
115 | self.player_widget.set_volume(pos)
116 |
117 | def auto_move_img_slider(self, pos: int):
118 | self.progress_bar.setValue(pos)
119 | self.now_time = pos
120 | now, total = self.calculate_time(self.now_time, self.total_time)
121 | self.progress_text.setText(f'{now} / {total}')
122 |
123 | def auto_set_img_slider_max_pos(self, dur: int):
124 | self.progress_bar.setMaximum(dur)
125 | self.total_time = dur
126 |
127 | def auto_reset_play_end(self, state: int):
128 | if state == 0:
129 | self.reset_all()
130 |
131 | def calculate_time(self, now: int, total: int):
132 | current_sec = now // 1000
133 | total_seconds = total // 1000
134 | c_time = "{}:{}:{}".format(int(current_sec/3600), int((current_sec % 3600)/60), int(current_sec % 60))
135 | t_time = "{}:{}:{}".format(int(total_seconds / 3600), int((total_seconds % 3600) / 60), int(total_seconds % 60))
136 | return c_time, t_time
137 |
138 | def play_rate_reset(self):
139 | self.play_rate = 1
140 | self.player_widget.set_play_rate(self.play_rate)
141 |
142 | def play_rate_raise(self):
143 | self.play_rate += 1
144 | self.player_widget.set_play_rate(self.play_rate)
145 |
146 |
147 | class VideoPlayer(QtWidgets.QWidget):
148 |
149 | dur_changed = QtCore.Signal(int)
150 | pos_changed = QtCore.Signal(int)
151 | state_changed = QtCore.Signal(int)
152 |
153 | def __init__(self, parent=None):
154 | super(VideoPlayer, self).__init__(parent=parent)
155 | """
156 | 需要安装媒体解码器才可以正常使用QMediaPlayer
157 | 作者使用 LAVFilters 来解码视频和音频。 URL:https://github.com/Nevcairiel/LAVFilters
158 | """
159 | self.video_player = QMediaPlayer()
160 | self.video_widget = QVideoWidget()
161 | self.video_player.setVideoOutput(self.video_widget)
162 | self.video_player.setVolume(50)
163 |
164 | # 信号连接
165 | self.video_player.durationChanged.connect(self.__dur_changed)
166 | self.video_player.positionChanged.connect(self.__pos_changed)
167 | self.video_player.stateChanged.connect(self.__state_changed)
168 |
169 | def set_media(self, media_path: str):
170 | media = QMediaContent(QUrl.fromLocalFile(media_path))
171 | media = QMediaContent(media)
172 | self.video_player.setMedia(media)
173 |
174 | def set_volume(self, v: int = 50):
175 | self.video_player.setVolume(v)
176 |
177 | def set_play_rate(self, speed: int = 1):
178 | self.video_player.setPlaybackRate(speed)
179 |
180 | def set_pos(self, pos: int):
181 | self.video_player.setPosition(pos)
182 |
183 | def get_max_pos(self):
184 | return self.video_player.duration()
185 |
186 | def get_now_pos(self):
187 | return self.video_player.position()
188 |
189 | def get_video_widget(self):
190 | return self.video_widget
191 |
192 | def pause(self):
193 | self.video_player.pause()
194 |
195 | def play(self):
196 | self.video_player.play()
197 |
198 | def __dur_changed(self, dur: int):
199 | self.dur_changed.emit(dur)
200 |
201 | def __pos_changed(self, pos: int):
202 | self.pos_changed.emit(pos)
203 |
204 | def __state_changed(self, state: int):
205 | self.state_changed.emit(state)
206 |
207 |
208 | if __name__ == '__main__':
209 | app = QtWidgets.QApplication(sys.argv)
210 | window = MainWindow(icon='icon.png', title='音视频播放器', default_layout_margins=2, default_layout_space=2)
211 | window.show()
212 | sys.exit(app.exec_())
213 |
--------------------------------------------------------------------------------
/Lesson_11.MultimediaPlayer/readme.md:
--------------------------------------------------------------------------------
1 | # 多媒体播放器
2 |
3 | 之前尝试使用 Qt 的 QMediaPlayer 来播放视频或者音乐,一直没有成功。最近有时间了,
4 | 研究了一下,发现是Qt没有多媒体解码器。加载文件一直卡在 loading media 或者
5 | format error 的状态。网络上搜到了一个很简洁的解码器,下载安装各种问题都解决了。
6 | 具体方法请看 main.py 中的说明。
7 |
8 | ## 设计方法
9 |
10 | 音频播放可以没有窗体,视频播放需要有图像的载体,QtMultimediaWidgets 中已经
11 | 包含了图像的载体窗口 QVideoWidget,我们只需要将其放置到主窗口的某个容器中即可。
12 | 我已经定义好了 VideoPlayer 类作为媒体播放器,这个类中定义了两个信号,一个是
13 | 媒体播放进度的信号,另一个是播放状态的信号。通过对这两个信号处理,可以控制播放
14 | 进度条自动移动。
15 |
16 | ## 程序说明
17 |
18 | 1. 所有程序放置在 main.py 中
19 | 2. 我计划将 EzQt 工具规范化,之后设计为一个使用 pip 安装的库。借鉴 Django
20 | 的方法来自动生成工程文件,减少 Qt 开发的代码量。(打工人太累,懒得搞了-_-!)
21 | * [十二课-贪吃蛇](../Lesson_12.贪吃蛇/readme.md)
22 |
23 |
24 |
--------------------------------------------------------------------------------
/Lesson_12.贪吃蛇/readme.md:
--------------------------------------------------------------------------------
1 | # 贪吃蛇
2 |
3 | 打工人忙里偷闲,按照自己的思路写了贪吃蛇。没有看网上的经典代码,可能存在很多BUG。
4 | 本程序的主要目的是帮助大家理解Qt编程思路。
5 |
6 | -----
7 |
8 | ## 概述
9 |
10 | * 目录下resources为资源文件,保存了贪吃蛇所需的贴图
11 | * 使用按钮来表示贪吃蛇的每个单元,按钮贴图来区分蛇头、蛇身和食物
12 | * 定时器溢出事件驱动按钮移动
13 | * 将二维坐标转换为一维数组来方便判定是否位于蛇身上
14 | * Tips:PyCharm 插件 Rainbow Brackets 真好用
15 |
--------------------------------------------------------------------------------
/Lesson_12.贪吃蛇/resources/a.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Lesson_12.贪吃蛇/resources/candy.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Lesson_12.贪吃蛇/resources/d.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Lesson_12.贪吃蛇/resources/ico.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Lesson_12.贪吃蛇/resources/s.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Lesson_12.贪吃蛇/resources/w.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Lesson_12.贪吃蛇/snake.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | @ author: se7enXF
4 | @ software: PyCharm
5 | @ file: snake.py
6 | @ time: 2021/9/25
7 | @ Note: python3.9 code for Qt Greedy snake
8 | """
9 |
10 | from EzQtTools import *
11 | import sys
12 | import random
13 | from PySide2.QtCore import Qt
14 | from PySide2.QtCore import QTimer
15 |
16 |
17 | class Snake(QtWidgets.QPushButton):
18 | def __init__(self,
19 | parent,
20 | icon: str = 'resources/ico.svg',
21 | direction: int = 0,
22 | size: int = 10,
23 | position: QtCore.QPoint = QtCore.QPoint(0, 0)
24 | ):
25 | """
26 | Using QPushButton as a single snake part, head or body unit
27 | :param parent:must set up MainWindow as parent for embedding
28 | :param icon:head or body has different icon, default for body
29 | :param direction:move direction, WASD for up left down right
30 | :param size:box size in pixel
31 | :param position:QPushButton left up corner position
32 | """
33 | super().__init__(parent=parent)
34 | self.__icon = icon
35 | self.__direction = direction
36 | self.__size = size
37 | self.__position = position
38 | # setup snake body
39 | self.setFixedSize(QtCore.QSize(self.__size, self.__size))
40 | self.setIcon(QtGui.QIcon(self.__icon))
41 | self.setIconSize(QtCore.QSize(self.__size, self.__size))
42 | self.move(self.__position)
43 | # set button no border
44 | self.setFlat(True)
45 | self.show()
46 |
47 | def direct(self):
48 | return self.__direction
49 |
50 | def remove(self):
51 | """
52 | Hide current unit and delete obj
53 | :return:
54 | """
55 | self.hide()
56 | self.deleteLater()
57 |
58 |
59 | class MainWindow(EzMainWindow):
60 | __ticker = QTimer()
61 | __directions: list[str] = ['w', 'a', 's', 'd']
62 | __snake: list[Snake] = []
63 | __h_direction: int = 0
64 | __enable_key = False
65 | __candy = None
66 |
67 | def __init__(self,
68 | title: str = '贪吃蛇',
69 | col_row_num: tuple = (10, 10),
70 | icon: str = 'resources/ico.svg',
71 | default_len: int = 3,
72 | cell_edge: int = 50,
73 | step: int = 500,
74 | acc_rate: float = 2.,
75 | **kwargs):
76 | """
77 | Qt MainWindow for snake control
78 | :param col_row_num: columns, rows, must greater than 5
79 | :param default_len:start snake length, must greater than 3
80 | :param cell_edge:snake unit edge size, must greater than 50
81 | :param step:snake move interval time, m_second, must greater than 100
82 | :param kwargs: other EzMainWindow args
83 | """
84 | w_size = list(col_row_num)
85 | if col_row_num[0] < 10:
86 | w_size[0] = 10
87 | if col_row_num[1] < 10:
88 | w_size[1] = 10
89 | if cell_edge < 50:
90 | cell_edge = 50
91 | w_size = (w_size[0] * cell_edge, w_size[1] * cell_edge)
92 | EzMainWindow.__init__(self, title=title, size=w_size, icon=icon, fixed=True, **kwargs)
93 | self.__cell_edge = cell_edge
94 | self.__col_row_num = col_row_num
95 | if default_len < 3:
96 | default_len = 3
97 | self.__default_len = default_len
98 | if step < 100:
99 | step = 100
100 | self.__step = step
101 | self.__init__widget()
102 | self.__ticker.setInterval(self.__step)
103 | self.__acc_step = int(self.__step / acc_rate)
104 |
105 | def __init__widget(self):
106 | """
107 | Init layout and connection
108 | """
109 | self.__introduction = QtWidgets.QPushButton('使用“WSAD”对应“上下左右”控制\n贪吃蛇,点击开始游戏!')
110 | self.add_layout_widget(self.central_widget, self.__introduction)
111 | self.__introduction.clicked.connect(self.__run)
112 | self.__ticker.timeout.connect(self.__snake_move)
113 |
114 | def __init_snake(self):
115 | """
116 | Init snake head, bodies
117 | """
118 | if len(self.__snake) > 0:
119 | for s in self.__snake:
120 | s.remove()
121 | self.__snake = []
122 | self.__ticker.setInterval(self.__step)
123 | self.__h_direction = random.choice(range(4))
124 | max_len_x = self.width() // self.__cell_edge
125 | max_len_y = self.height() // self.__cell_edge
126 | pos_x = random.choice(range(self.__default_len + 2, max_len_x - 2)) * self.__cell_edge
127 | pos_y = random.choice(range(self.__default_len + 2, max_len_y - 2)) * self.__cell_edge
128 | icon = f'resources/{self.__directions[self.__h_direction]}.svg'
129 | s = Snake(self, icon, self.__h_direction, self.__cell_edge, QtCore.QPoint(pos_x, pos_y))
130 | self.__snake.append(s)
131 | for i in range(self.__default_len - 1):
132 | self.__add_node()
133 | if not self.__candy:
134 | self.__candy = Snake(self, 'resources/candy.svg', size=self.__cell_edge)
135 | self.__new_candy()
136 |
137 | def __run(self):
138 | """
139 | Start move and show snake
140 | """
141 | # init snake show
142 | self.__init_snake()
143 | self.__introduction.hide()
144 | # start ticktock for snake moving
145 | self.__ticker.start()
146 | # enable key press
147 | self.__enable_key = True
148 |
149 | def __snake_move(self):
150 | """
151 | Body follow head, calculate head new pos
152 | Eating and crash check
153 | """
154 | self.__eat_candy()
155 | # move tail and body
156 | n_snake = len(self.__snake)
157 | for i in range(1, n_snake):
158 | s2 = self.__snake[n_snake - i]
159 | s1 = self.__snake[n_snake - i - 1]
160 | s2.move(s1.pos())
161 | # move head
162 | pos = self.__snake[0].pos()
163 | tmp_snake = Snake(self, direction=self.__h_direction, position=pos)
164 | h_pos = self.__get_next_head_pos(tmp_snake)
165 | tmp_snake.remove()
166 | icon = f'resources/{self.__directions[self.__h_direction]}.svg'
167 | new_head = Snake(self, icon, self.__h_direction, self.__cell_edge, h_pos)
168 | old_head = self.__snake[0]
169 | self.__snake[0] = new_head
170 | old_head.remove()
171 | self.__crash_check()
172 |
173 | def __restart(self):
174 | """
175 | Reset variables
176 | :return:
177 | """
178 | self.__ticker.stop()
179 | self.__enable_key = False
180 | self.__introduction.setText(f'游戏结束!\n得分:{len(self.__snake)}\n点击重新开始游戏!')
181 | self.__introduction.show()
182 |
183 | def __crash_check(self):
184 | # head out of window
185 | if self.__snake[0].x() < 0 or self.__snake[0].y() < 0 \
186 | or (self.__snake[0].x() + self.__cell_edge) > self.window().width() \
187 | or (self.__snake[0].y() + self.__cell_edge) > self.window().height():
188 | self.__restart()
189 | # head on body
190 | array_set = []
191 | for s in self.__snake:
192 | step_x = s.x() // self.__cell_edge
193 | step_y = s.y() // self.__cell_edge
194 | array_set.append(step_y * self.__col_row_num[0] + step_x)
195 | if array_set[0] in array_set[1:]:
196 | self.__restart()
197 |
198 | def __add_node(self):
199 | """
200 | Add one node based on last node
201 | """
202 | next_pos = self.__get_next_tail_pos(self.__snake[-1])
203 | next_d = self.__snake[-1].direct()
204 | node = Snake(self, direction=next_d, size=self.__cell_edge, position=next_pos)
205 | self.__snake.append(node)
206 |
207 | def __get_next_tail_pos(self, snake: Snake) -> QtCore.QPoint:
208 | """
209 | Mapping dir_code to next position
210 | """
211 | dir_code = snake.direct()
212 | pos = snake.pos()
213 | if dir_code == 0: # up
214 | next_pos = QtCore.QPoint(pos.x(), pos.y() + self.__cell_edge)
215 | elif dir_code == 2: # down
216 | next_pos = QtCore.QPoint(pos.x(), pos.y() - self.__cell_edge)
217 | elif dir_code == 1: # left
218 | next_pos = QtCore.QPoint(pos.x() + self.__cell_edge, pos.y())
219 | elif dir_code == 3: # right
220 | next_pos = QtCore.QPoint(pos.x() - self.__cell_edge, pos.y())
221 | else:
222 | raise ValueError(f'dir_code must in [0, 1, 2, 3], but get {dir_code}')
223 | return next_pos
224 |
225 | def __get_next_head_pos(self, snake: Snake) -> QtCore.QPoint:
226 | """
227 | Switch up and down, left and right
228 | :param snake: head of snake
229 | :return: next head position QPoint
230 | """
231 | pos = snake.pos()
232 | direction = (snake.direct() + 2) % 4
233 | tmp_snake = Snake(self, direction=direction, position=pos)
234 | pos = self.__get_next_tail_pos(tmp_snake)
235 | tmp_snake.remove()
236 | return pos
237 |
238 | def __change_speed(self, speed):
239 | if speed != self.__ticker.interval():
240 | self.__ticker.setInterval(speed)
241 | self.__snake_move()
242 |
243 | def __new_candy(self):
244 | """
245 | Random raise a candy in window but not on snake
246 | Mapping x,y to 1-d array
247 | :return:
248 | """
249 | array_set = list(range(self.__col_row_num[0] * self.__col_row_num[1]))
250 | for s in self.__snake:
251 | step_x = s.x() // self.__cell_edge
252 | step_y = s.y() // self.__cell_edge
253 | array_set.remove(step_y * self.__col_row_num[0] + step_x)
254 | pos = random.choice(array_set)
255 | x_pos = pos % self.__col_row_num[0] * self.__cell_edge
256 | y_pos = pos // self.__col_row_num[0] * self.__cell_edge
257 | self.__candy.move(QtCore.QPoint(x_pos, y_pos))
258 |
259 | def __eat_candy(self):
260 | """
261 | Judge eating candy
262 | :return:
263 | """
264 | head = self.__snake[0]
265 | if head.x() == self.__candy.x() and head.y() == self.__candy.y():
266 | self.__new_candy()
267 | self.__add_node()
268 |
269 | def keyReleaseEvent(self, event: QtGui.QKeyEvent) -> None:
270 | """
271 | Keep pressing key for accelerating speed, release for normal speed
272 | :param event:
273 | :return:
274 | """
275 | if event.key() in [Qt.Key_W, Qt.Key_S, Qt.Key_A, Qt.Key_D] and self.__enable_key:
276 | new_direction = self.__directions.index(event.text())
277 | # ignore opposite direction
278 | if (new_direction + 2) % 4 == self.__h_direction:
279 | return
280 | self.__h_direction = new_direction
281 | if event.isAutoRepeat():
282 | self.__change_speed(self.__acc_step)
283 | print(f'{event.text().capitalize()}:accelerate speed')
284 | else:
285 | self.__change_speed(self.__step)
286 | print(f'{event.text().capitalize()}:normal speed')
287 |
288 |
289 | if __name__ == '__main__':
290 | app = QtWidgets.QApplication(sys.argv)
291 | window = MainWindow()
292 | window.show()
293 | sys.exit(app.exec_())
294 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PySide2 入门教程
2 |
3 | Qt是一种强大的图形用户界面构造工具,如今它对于Python也有很好的接口支持,
4 | 分别是PyQt和PySide。PyQt采用需购买版权的商业及GPL许可,而PySide采用无需
5 | 购买版权的LGPL许可。PySide与PyQt有非常相容的API,因此无需版权的PySide更
6 | 适合广大Python爱好者的学习。PySide2支持Qt5框架,兼容Python2.7以上版本及
7 | Python3.5以上版本。本教程以PySide2为例,讲述如何从显示一个简单的
8 | hello world窗口到设置井然有序窗口布局。介于作者时间有限,此教程只讲述如何
9 | 入门并高效使用Qt,至于每个控件如何使用,各位爱好者自己学习吧,网络上各种资
10 | 源一大把。俗话说,师傅领进门,修行靠个人,我的每个教程都有详细的代码供大家
11 | 参考,有问题可以直接在github上提出,共同进步!
12 |
13 | ## 致谢
14 |
15 | 截至2021年9月,本项目在github有上百star,在gitee也有上百star,感谢各位码友的支持!
16 |
--------------------------------------------------------------------------------