├── .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 | ![hello_world](../Addition/Lesson1.png) 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 | ![QtDesigner](../Addition/QtDesigner.png) 15 | 16 | ## 2.文件转换 17 | 打开QtDesigner后,新建一个空白的主窗口文件,按照Lesson_1显示的窗口的样子放置一个Label和一个pushButton, 18 | 并设置好大小等属性,保存在当前工程目录下。 19 | ![Design](../Addition/Lesson2Design.png) 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 | ![v_layout](../Addition/Layout.png) 13 | 14 | ## 布局参数 15 | 上面窗口的绘制使用了默认参数,美观的布局依赖于参数的设置。 16 | 一个布局的属性有两部分,自己与别人的关系和自己内部的关系。 17 | 在QtDesigner中,默认情况下,右上角是对象查看器。 18 | ![Object_window](../Addition/Object_window.png) 19 | 1. 在对象查看器中,我们可以很方便的选中控件,也可以看出所属关系。选中 20 | QVBoxLayout,下方出现属性编辑器。 21 | ![V_layout_self](../Addition/V_layout_self.png) 22 | 这部分属性是设置该布局与相邻布局的距离的,即自己与别人关系。 23 | 2. 在对象查看器中,选中布局下的某一个控件,然后找到“水平伸展”和“垂直伸展”两个属性,他们的数值表示了当前布局中 24 | 不同控件大小所占的比例,即自己内部关系。 25 | ![V_Size_policy](../Addition/SizePolicy.png) 26 | 当前是QVBoxLayout,因此只考虑垂直比例,如果我们将比例设置为1:1, 27 | 即将label的“垂直伸展”和pushButton的“垂直伸展”都设置为1,那么在窗口变化中,上下两个控件垂直高度会一样。 28 | (当使用动态布局,切记分布策略选择“preferred") 29 | ![v_layout_size11](../Addition/Layout_Size11.png) 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 | 69 | 70 | 71 | 0 72 | 0 73 | 800 74 | 23 75 | 76 | 77 | 78 | 79 | Open 80 | 81 | 82 | 83 | 84 | 85 | 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 | ![mainwindow](../Addition/mainwindow.png) 3 | ![maindock](../Addition/mainwindow_dock.png) 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 | --------------------------------------------------------------------------------