PyQt5桌面应用开发(1):需求分析

PyQt5桌面应用系列

前言

事到如今,我也不得不承认。没错,那个还在使用Python编Qt的人就是我。(社区匿名大佬)

除了无聊搞点Kotlin+JavaFX这么没有前途的玩意之外,我也不能免俗。说什么Python乱七八糟、Python毁天灭地、学Python人类💊、PHP是最好的语言……

“真香!”

写作目标

  1. 做个模板放着,以后可以直接拷贝;
  2. 同好来讨论讨论。

结构

  1. 需求分析
  2. 开发实例
  3. 主要知识点

需求分析

需求分析就像是内裤。每个人都要穿。有些人老不洗。有些人天天换。脏内裤烂内裤呢?穿还是可以穿的。但是呢,穿久了之后啊!走起路来就有点对不起自己的小宝贝。

市面上关于需求分析的书籍,文献和文章,汗牛充栋。看是不可能看的,这辈子都不会看的。但是会还是要会的,哪怕是一个初级程序员,大佬给你把任务都分解好了,你只需要编一个很小的函数或者是很小的一部分,也需要对大佬的需求分析的东西心里有数才能做好。

只有很清晰地了解开发需求,才能保证开发出来的东西是实际上有用的和有需要的。

那么对于一个桌面信息化系统或者是一个桌面应用来说,需求分析核心的内容究竟是什么呢?其实非常简单,就是功能。那你们有没有注意到啊?一般大佬说什么东西非常简单之后呢就会开始说一些完全听不懂的东西。还好我不是大佬,所以我接下来讲的东西依然很简单。

借鉴信息系统的需求分析过程来分析这个问题。大型信息系统,分为几种类型,从管理信息系统和最最常见的联机事务处理系统,到决策支持系统、专家系统、办公自动化。一个程序核心有两个点,第一是如何使用数据,就是我们呈现数据给用户提供信息(正规的名字是报表,包括图表、图、自动生成的文档等)。第二个就是信息从哪,数据从哪来?常规的,比如说用户的输入(这就是软件实现的交互工作流程),系统传感器采集,网络获取,从外部文件中读入等。
软件需求分析的核心
在使用一个程序时,是从获取数据-使用数据的顺序;但是,设计一个程序时,一般是倒着来做。首先我们看我们怎么使用数据?就是最后我们给用户呈现什么数据。从呈现数据这里呢,我们就可以明确程序中最重要的信息:数据。数据是通过什么方法处理得到的?原始的数据是从哪里得到?

通过原始数据的获取过程,就可以进一步的设计用户输入的部分和系统采集的这两个部分的功能。

例子

功能需求和基本界面

这里假设准备开发一个能够显示一段音频的时域和频域特征。

很显然,报表就是两个:一个是音频的时域图形,一个是频域图形。另外一个呢,既然是音频,还可以额外增加一个播放功能(也是一种报表:声波)。这是在同一个数据的三种表现形式。

报表:

  1. 时域图形
  2. 频域图形
  3. 播放为声音

数据表现形式

数据定义

从这个上面三种表现报表形式很直接的就可以分析的得到需要的数据:音频。

音频信号本质上是一个(等间距、也就是一定采样频率)时间序列,是一个一维数组或者二维数组。一段音频信号有几个参数。一个参数就是多长时间?是一秒钟两秒钟还是十秒钟?第二个参数呢,就是采样频率,一般我们叫做音频质量,CD音质,是无损音质,还是一些比较低分辨率的音质?英文就是叫做sample rate。最后一个是声道,左声道还是右声道组成的双声道,还是一个声道,声道数决定了音频信号是一维数组(单声道)还是二维数组。那么音频信号的一个维数就是时长乘以采样频率,一个维数就是声道数。

音频数据

  1. 持续时间
  2. 采样频率
  3. 声道

那么这个数据怎么得到呢?最直观的想法就是通过电脑的麦克风拾取外界的声音信号。

用户输入

从上面的分析还知道,还需要从用户输入一些信息。

  1. 录音相关参数;
  2. 开始录音的信号;
  3. 切换绘制时间域还是频率域(如果不是一起显示的话);
  4. 播放开始的信号。

这里就决定sample rate就直接取常规的48000就是高音质。需要用户输入的参数是时长.

界面设计。

用户界面主要分为两个部分。第一个部分是参数设置和用户输入。第二个部分是显示的部分。如下图,上面是输入,下面是显示,通过文本框输入时间,通过CheckBox来切换时域频域。按钮输入包括:录音和播放。

录制的声音信号
频谱特征

代码

PyQt5主程序:

import sys

from PyQt5.QtWidgets import QApplication, QMainWindow

from ui.qsoundwdiget import QSoundWidget

if __name__ == '__main__':
    app = QApplication(sys.argv)
    w = QMainWindow()
    w.resize(1024, 768)
    w.move(100, 100)
    w.setWindowTitle('Record and show sound')

    w.setCentralWidget(QSoundWidget())

    w.show()
    sys.exit(app.exec_())

非常直白和平实,将一个QMainWindow的主Widget设置为QSoundWidget。下面就是QSoundWidget的代码。具体的PyQt5的知识点留待下回。

import multiprocessing as mp

import numpy as np
import sounddevice as sd
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QIntValidator, QIcon
from PyQt5.QtWidgets import QWidget, QPushButton, QCheckBox, QLineEdit, QVBoxLayout, QHBoxLayout, QLabel
from matplotlib import pyplot as plt
from matplotlib.axes import Axes
from matplotlib.backends.backend_qt5 import NavigationToolbar2QT as NavigationToolbar
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure


def toggle_button(button: QPushButton, enable: bool):
    button.setEnabled(enable)
    button.repaint()


class QSoundWidget(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.sound = None
        self.freq = None
        self.default_window_bg_color = [240.0 / 255] * 3
        # self.rate = 44100
        self.rate = 48000
        self._init_components()
        self._setup_layout()

        self.play_pool = mp.Pool(1)

    def start_button_clicked(self):

        text = self.start_button.text()
        self.start_button.setText("Recording...")
        toggle_button(self.start_button, False)

        self.record_sound(self.get_sound_duration())
        self.draw_series()

        self.start_button.setText(text)
        toggle_button(self.start_button, True)

    # 1. record sound
    def record_sound(self, time):

        samples = time * self.rate
        sound_recorded = sd.rec(samples, samplerate=self.rate, channels=1)
        sd.wait()

        n = len(sound_recorded)
        t = np.linspace(0, time, n)
        self.sound = t, sound_recorded
        fft = np.fft.fft(self.sound[1])
        fft_freq = np.fft.fftfreq(len(fft), 1 / self.rate)
        n = len(fft) // 2
        self.freq = fft_freq[:n], fft.real[:n]

    def draw_series(self):
        if self.sound is None:
            return
        self.figure.clear(True)

        ax: Axes = self.figure.add_subplot(111)
        (x, y), title, label, scale = (
            self.freq,
            "Recorded sound in frequency domain",
            "Frequency (Hz)",
            "log"
        ) if self.is_frequency_domain.isChecked() else (
            self.sound,
            "Recorded sound in time domain",
            "Time (s)",
            "linear"
        )

        ax.scatter(x, y)
        ax.set_title(title)
        ax.set_xlabel(label)
        ax.set_ylabel("Amplitude")
        ax.set_yscale(scale)
        ax.grid(True)

        self.canvas.draw()

    def get_sound_duration(self):
        try:
            return int(self.record_duration.text())
        except ValueError:
            return 2

    def play_sound(self):
        if self.sound is None:
            return
        toggle_button(self.play_button, False)
        self.play_pool.apply_async(sd.play,
                                   args=(self.sound[1], self.rate),
                                   callback=lambda _: toggle_button(self.play_button, True))

    def _init_components(self):
        self.figure: Figure = plt.figure()
        self.figure.tight_layout()
        self.figure.set_facecolor(self.default_window_bg_color)

        self.canvas = FigureCanvas(self.figure)
        self.toolbar = NavigationToolbar(self.canvas, self)

        self.start_button = QPushButton("Start recording", self)
        self.start_button.setIcon(QIcon("../icons/record.png"))

        self.is_frequency_domain = QCheckBox("Frequency domain", self)
        self.record_duration = QLineEdit("2", self)
        self.is_show_toolbar = QCheckBox("Show Toolbar", self)

        self.play_button = QPushButton("Play", self)
        self.play_button.setIcon(QIcon("../icons/play.png"))

        self.is_show_toolbar.setChecked(True)
        self.record_duration.setValidator(QIntValidator(1, 10))

        self.is_frequency_domain.setChecked(False)

        self.start_button.clicked.connect(self.start_button_clicked)
        self.is_frequency_domain.stateChanged.connect(self.draw_series)
        self.is_show_toolbar.stateChanged.connect(lambda: self.toolbar.setVisible(self.is_show_toolbar.isChecked()))
        self.play_button.clicked.connect(self.play_sound)

    def _setup_layout(self):
        self.layout = QVBoxLayout(self)

        buttons = QWidget(self)
        self.button_layout = QHBoxLayout(buttons)

        for widget in (QLabel("Record duration: ", self), self.record_duration,
                       QLabel("s", self), self.start_button,
                       self.is_frequency_domain, self.is_show_toolbar,
                       self.play_button):
            self.button_layout.addWidget(widget)

        self.layout.addWidget(buttons)
        self.layout.addWidget(self.canvas, stretch=1)
        self.layout.addWidget(self.toolbar)

        self.layout.setAlignment(self.toolbar, Qt.AlignCenter)

知识点总结

  1. 需求分析很简单;
  2. 用户如何使用信息(报表);
  3. 信息从哪里来(用户输入+系统采集)。
posted @ 2023-02-26 08:04  大福是小强  阅读(77)  评论(0编辑  收藏  举报  来源