让 QMediaPlayer 使用代理
2025-02-21 22:57:25+08:00

让 QMediaPlayer 使用代理

QMediaPlayer 使用简介

使用 QMediaPlayer::setSource 设置媒体源,即可进行播放:

player = new QMediaPlayer;
audioOutput = new QAudioOutput;
player->setAudioOutput(audioOutput);
connect(player, &QMediaPlayer::positionChanged, this, &MediaExample::positionChanged);
player->setSource(QUrl("https://vjs.zencdn.net/v/oceans.mp4"));
audioOutput->setVolume(50);
player->play();

上述代码使用 Qt6,Qt6 对 Multimedia 模块做了较大改动,与 Qt5 不兼容。

问题描述

QMediaPlayer 在播放网络源时,会预先加载一段时间,然后开始播放,预先加载的部分是流畅的,但如果网速慢于播放速度后续就会产生卡顿。

上述示例中使用的视频源来自 video.js,播放时卡顿非常严重,经分析应是网速太慢导致的。

应用代理无效

通过 QNetworkProxy::setApplicationProxy 可以设置应用程序的全局代理,相应的 QNetworkProxy::applicationProxy 可以查看应用程序的全局代理。

当系统设置了代理是,Qt 会自动设置应用代理,通过 QNetworkProxy::applicationProxy 查看即可可以确认这一点,不需要手动设置。

但经过测试,QMediaPlayer 完全无视了应用代理,仍然卡顿。

解决办法

手动使用 QtNetwork 设置代理并请求数据源

可以手动使用 QNetworkAccessManager 请求网络源,并将返回的 QNetworkReply 设为 QMediaPlayer 的源:

manager = new QNetworkAccessManager;
manager.setProxy(QNetworkProxy()); // QNetworkAccessManager 会使用应用代理,这行可省略
reply = manager->get(QNetworkRequest(QUrl("https://vjs.zencdn.net/v/oceans.mp4")));

player->setSourceDevice(reply);
player->play();

预加载

上述示例并不能直接运行,因为返回时 QNetworkReply 还没有拿到数据。而 QMediaPlayer 不支持阻塞式读取。

因此需要等待 QNetworkReply 预加载一部分数据后再将其设为 QMediaPlayer 的数据源。

#define PRELOAD_SIZE 1024 * 1024

manager = new QNetworkAccessManager;
manager.setProxy(QNetworkProxy()); // QNetworkAccessManager 会使用应用代理,这行可省略
reply = manager->get(QNetworkRequest(QUrl("https://vjs.zencdn.net/v/oceans.mp4")));

// 等待 readyRead 并检查数据长度,达到一定的数据量时开始播放
connect(reply, &QNetworkReply::readyRead, [reply, player](){
    if (reply->size() >= PRELOAD_SIZE)
    {
        player->setSourceDevice(reply);
        player->play();
        disconnect(reply, &QNetworkReply::readyRead); // 断开与 readyRead 信号连接的所有槽
    }
});

// 如果媒体源非常小,总数据两都小于 PRELOAD_SIZE,则在 finished 触发时开始播放
connect(reply, &QNetworkReply::finished, [reply, player](){
    if (reply->size() >= PRELOAD_SIZE)
    {
        player->setSourceDevice(reply);
        player->play();
        disconnect(reply, &QNetworkReply::finished); // 断开与 finished 信号连接的所有槽
    }
});

流媒体

对于 m3u8 之类的流媒体,实际数据被切分为多个文件,m3u8 仅仅是文件索引。

使用 QNetworkAccessManager 发起请求时,仅仅获得了索引文件本身,需要手动解析并请求后续内容。

Qt Theme 纯 qss 的 Qt 主题
2025-02-20 15:29:19+08:00

Qt Theme 纯 qss 的 Qt 主题

简介

源码地址:https://github.com/hubenchang0515/QtTheme/

预览

Qt Theme 是一个纯 qss 的 Qt 主题项目,能够极为简单对已有项目的风格进行改进。

支持 C++、PyQt5、PyQt6、PySide2、PySide6,并以 WebAssembly 的方式在 GitHub Pages 上发布。

示例

安装

这里演示一下在 Python 上的使用,首先进行安装:

pip install QtTheme

原生样式及代码

让 Deep Seek 随便帮我写个界面作为示例:

deepseek

import sys
from PyQt5.QtWidgets import *
from PyQt5.QtGui import QPixmap, QFont
from PyQt5.QtCore import Qt, QTimer

class DemoWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("PyQt Widget 演示界面")
        self.setGeometry(100, 100, 800, 600)
        
        # 创建中心widget和主布局
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        main_layout = QVBoxLayout(central_widget)

        # 添加功能区域
        self.create_input_section(main_layout)
        self.create_selection_section(main_layout)
        self.create_display_section(main_layout)
        self.create_progress_section(main_layout)
        
        # 添加状态栏
        self.statusBar().showMessage("就绪")

        # 初始化进度条
        self.progress_value = 0
        self.update_progress()

    def create_input_section(self, layout):
        group = QGroupBox("输入控件")
        grid = QGridLayout()

        # 文本输入
        self.line_edit = QLineEdit()
        self.line_edit.setPlaceholderText("单行文本输入...")
        grid.addWidget(QLabel("单行文本:"), 0, 0)
        grid.addWidget(self.line_edit, 0, 1)

        # 多行文本
        self.text_edit = QTextEdit()
        self.text_edit.setPlaceholderText("多行文本输入...")
        grid.addWidget(QLabel("多行文本:"), 1, 0)
        grid.addWidget(self.text_edit, 1, 1)

        # 数字输入
        self.spin_box = QSpinBox()
        self.spin_box.setRange(0, 100)
        grid.addWidget(QLabel("数字输入:"), 2, 0)
        grid.addWidget(self.spin_box, 2, 1)

        group.setLayout(grid)
        layout.addWidget(group)

    def create_selection_section(self, layout):
        group = QGroupBox("选择控件")
        hbox = QHBoxLayout()

        # 复选框
        vbox = QVBoxLayout()
        self.check1 = QCheckBox("选项1")
        self.check2 = QCheckBox("选项2")
        vbox.addWidget(self.check1)
        vbox.addWidget(self.check2)
        hbox.addLayout(vbox)

        # 单选框
        vbox = QVBoxLayout()
        self.radio1 = QRadioButton("单选1")
        self.radio2 = QRadioButton("单选2")
        self.radio1.setChecked(True)
        vbox.addWidget(self.radio1)
        vbox.addWidget(self.radio2)
        hbox.addLayout(vbox)

        # 下拉列表
        self.combo = QComboBox()
        self.combo.addItems(["选项A", "选项B", "选项C"])
        hbox.addWidget(self.combo)

        group.setLayout(hbox)
        layout.addWidget(group)

    def create_display_section(self, layout):
        group = QGroupBox("显示控件")
        hbox = QHBoxLayout()

        # 标签
        self.label = QLabel("这是一个标签")
        self.label.setAlignment(Qt.AlignCenter)
        self.label.setStyleSheet("border: 1px solid gray; padding: 10px;")
        hbox.addWidget(self.label)

        # 图片显示
        pixmap = QPixmap(100, 50)
        pixmap.fill(Qt.blue)
        image_label = QLabel()
        image_label.setPixmap(pixmap)
        hbox.addWidget(image_label)

        # 列表控件
        self.list_widget = QListWidget()
        self.list_widget.addItems(["项目1", "项目2", "项目3"])
        hbox.addWidget(self.list_widget)

        group.setLayout(hbox)
        layout.addWidget(group)

    def create_progress_section(self, layout):
        group = QGroupBox("进度控件")
        vbox = QVBoxLayout()

        # 进度条
        self.progress_bar = QProgressBar()
        self.progress_bar.setValue(0)
        vbox.addWidget(self.progress_bar)

        # 滑块
        self.slider = QSlider(Qt.Horizontal)
        self.slider.setRange(0, 100)
        self.slider.valueChanged.connect(self.on_slider_changed)
        vbox.addWidget(self.slider)

        # 控制按钮
        btn_layout = QHBoxLayout()
        self.start_btn = QPushButton("开始进度")
        self.start_btn.clicked.connect(self.start_progress)
        self.reset_btn = QPushButton("重置")
        self.reset_btn.clicked.connect(self.reset_progress)
        btn_layout.addWidget(self.start_btn)
        btn_layout.addWidget(self.reset_btn)

        vbox.addLayout(btn_layout)
        group.setLayout(vbox)
        layout.addWidget(group)

    def on_slider_changed(self, value):
        self.progress_bar.setValue(value)
        self.statusBar().showMessage(f"滑块值: {value}")

    def start_progress(self):
        self.timer = QTimer()
        self.timer.timeout.connect(self.update_progress)
        self.timer.start(100)

    def update_progress(self):
        self.progress_value += 1
        if self.progress_value > 100:
            self.timer.stop()
            return
        self.progress_bar.setValue(self.progress_value)
        self.slider.setValue(self.progress_value)

    def reset_progress(self):
        self.progress_value = 0
        self.progress_bar.setValue(0)
        self.slider.setValue(0)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = DemoWindow()
    window.show()
    sys.exit(app.exec_())

运行起来看看:

native ui

设置全局样式

导入 QtTheme,并通过 Qt 资源系统读取并设置样式即可:

from PyQt5.QtCore import QFile
import QtTheme.PyQt5

class DemoWindow(QMainWindow):
    def __init__(self):
        # 省略...

        qss = QFile(":/QtTheme/theme/Flat/Dark/Blue/Pink.qss")
        qss.open(QFile.OpenModeFlag.ReadOnly)
        self.setStyleSheet(qss.readAll().data().decode())

dark ui

设置颜色

最后根据需要,通过 QWidget.setProperty 对 widgets 设置颜色:

    def create_progress_section(self, layout):
        # 省略 ...
        self.start_btn.setProperty("Color", "Primary")
        self.reset_btn.setProperty("Color", "Danger")

color ui

导出资源

你也可以不安装 QtTheme,而是通过 在线页面 导出资源文件, 通过 RCC 将其加入你的项目:

pyrcc5 -o resource.py QtTheme.qrc

只需要修改导入方式,其余代码一致:

from PyQt5.QtCore import QFile
import resource   # 改为导入生成的 resource.py

class DemoWindow(QMainWindow):
    def __init__(self):
        # 省略...

        qss = QFile(":/QtTheme/theme/Flat/Dark/Blue/Pink.qss")
        qss.open(QFile.OpenModeFlag.ReadOnly)
        self.setStyleSheet(qss.readAll().data().decode())

QAudioOutput 无法播放声音的问题
2024-12-18 21:18:31+08:00

QAudioOutput 无法播放声音的问题

在 Windows 上使用 MSVC 编译的 Qt 程序,无法播放声音,QAudioDeviceInfo::availableDevices 返回空列表。

这是因为没能正确加载 Qt 插件。需要将 Qt\msvc2017_64\plugins 目录下的 audio 目录复制到可执行文件所在目录。

使用 windeployqt 部署后即可正确运行。

插件加载正确后,仍然发生无声音的问题,且 Qt 无任何报错。通过 QAudioDeviceInfo 查看支持的音频格式为:

("audio/pcm")
(8000, 11025, 16000, 22050, 32000, 44100, 48000, 88200, 96000, 192000)
(8, 16, 24, 32, 48, 64)
(LittleEndian)
(SignedInt, UnSignedInt, Float)

程序设置的音频格式为 32 位、小端、浮点数,在支持的范围内。

这个问题上的回答提到:

Qt 音频在 Windows 上,8 位数据只支持 UnSignedInt,16 位数据支持 SignedInt

但是没有提到 float 类型,怀疑可能不支持,改用 16 位 SignedInt 后可以正常播放。

另外,使用其支持列表返回的 64 位会报错,可以判断支持列表是假的。

QCustomPlot 启用 OpenGL
2024-12-18 21:18:31+08:00

QCustomPlot 启用 OpenGL

编译时添加预编译宏 QCUSTOMPLOT_USE_OPENGL,用绘图的 QCustomPlot 子类调用 setOpenGl(true),并链接 OpenGL32

在 Windows 上使用 OpenGL32 存在很严重的性能问题,可以改为使用 freeglut

图像错乱问题

修改 QCPPaintBufferGlFbo::draw,添加上下文切换的代码。

void QCPPaintBufferGlFbo::draw(QCPPainter *painter) const
{
  if (!painter || !painter->isActive())
  {
    qDebug() << Q_FUNC_INFO << "invalid or inactive painter passed";
    return;
  }
  if (!mGlFrameBuffer)
  {
    qDebug() << Q_FUNC_INFO << "OpenGL frame buffer object doesn't exist, reallocateBuffer was not called?";
    return;
  }
  
  // 添加
  if (QOpenGLContext::currentContext() != mGlContext.data()) {
    mGlContext.data()->makeCurrent(mGlContext.data()->surface());
  }
  
  painter->drawImage(0, 0, mGlFrameBuffer->toImage());
}
编译 Qt 源码,没有生成 libqxcb.so 的问题
2024-12-18 21:18:31+08:00

编译 Qt 源码,没有生成 libqxcb.so 的问题

Qt 通过不同的插件,在不同的平台上进行显示,例如:

  • libqlinuxfb.so 用于 Frame Buffer
  • libqxcb.so 用于 X11
  • libqwayland-generic.so 用于 Wayland

构建 X11 插件需要依赖许多 xcb 的开发包,可以通过下面的命令安装:

Qt 5.15.2:

sudo apt install libfontconfig1-dev \
                 libfreetype6-dev \
                 libx11-dev \
                 libx11-xcb-dev \
                 libxext-dev \
                 libxfixes-dev \
                 libxi-dev \
                 libxrender-dev \
                 libxcb1-dev \
                 libxcb-glx0-dev \
                 libxcb-keysyms1-dev \
                 libxcb-image0-dev \
                 libxcb-shm0-dev \
                 libxcb-icccm4-dev \
                 libxcb-sync0-dev \
                 libxcb-xfixes0-dev \
                 libxcb-shape0-dev \
                 libxcb-randr0-dev \
                 libxcb-render-util0-dev \
                 libxcb-xinerama0-dev \
                 libxkbcommon-dev \
                 libxkbcommon-x11-dev

Qt 6.5:

sudo apt install libfontconfig1-dev \
                 libfreetype6-dev \
                 libx11-dev \
                 libx11-xcb-dev \
                 libxext-dev \
                 libxfixes-dev \
                 libxi-dev \
                 libxrender-dev \
                 libxcb1-dev \
                 libxcb-cursor-dev \
                 libxcb-glx0-dev \
                 libxcb-keysyms1-dev \
                 libxcb-image0-dev \
                 libxcb-shm0-dev \
                 libxcb-icccm4-dev \
                 libxcb-sync-dev \
                 libxcb-xfixes0-dev \
                 libxcb-shape0-dev \
                 libxcb-randr0-dev \
                 libxcb-render-util0-dev \
                 libxcb-util-dev \
                 libxcb-xinerama0-dev \
                 libxcb-xkb-dev \
                 libxkbcommon-dev \
                 libxkbcommon-x11-dev

安装后重新构建即可:

rm config.cache
./configure -nomake examples -prefix /opt/qt-6.5.2 -opensource -confirm-license -release -xcb
make
make install

单独编译某个模块的命令为:

make module-qtbase                     # 单独编译 qtbase
make module-qtbase-install_subtargets  # 单独安装 qtbase

另外,如果需要 OpenGL 的话,可以安装以下包:

sudo apt install libgl1-mesa-dev \
                 libglu1-mesa-dev \
                 libx11-dev

如果不需要 OpenGL 的话,在 configure 步骤添加 -no-opengl 选项即可。

如果需要使用 SSL 的话,可以安装以下包:

sudo apt install libssl-dev \
                 openssl

如果不需要 OpenGL 的话,在 configure 步骤添加 -no-ssl 选项即可。

缓存问题:

执行过 configure 之后再安装依赖,再次执行 configure 仍会提示缺少依赖,可以删除 config.cache 来解决。

查找路径问题:

执行 configure 时可以使用 -I-L 选项添加查找路径。

参考:

QObject 与 dllimport 的相关问题
2024-12-18 20:03:48+08:00

QObject 与 dllimport 的相关问题

Qt 的元对象编译(MOC)系统会根据标记了 Q_OBJECT 的类生成代码。由于类通常在头文件中,因此通常需要将头文件加入编译。

add_library(lib SHARED lib.h lib.cpp)

在 Windows 上引用 lib 时,可能会遇到 staticMetaObject 符号未定义的错误。

add_executable(demo main.cpp)

即使创建动态库时,将 CMAKE_EXPORT_ALL_SYMBOLS 设为 ON 也不行。

set(CMAKE_EXPORT_ALL_SYMBOLS ON)
add_library(lib SHARED lib.h lib.cpp)

这是因为只有函数符号可以自动导入,而staticMetaObject 是静态数据成员,全局数据必须通过 __declspec(dllimport) 手动导入。

参考 CMake 文档

正确的解决办法是声明 dllexport 和 dllimport

#ifdef BUILD_SHARED
    #define DLL_EXPORT __declspec(dllexport)
#else
    #define DLL_EXPORT __declscpe(dllimport)
#endif

class DLL_EXPORT XXX : public QObject
{
    Q_OBJECT
};

不清楚这个机制的人可能会在外部项目中将 lib.h 加入编译,这样错误也会消失,因为 lib.h 重新 moc 并编译了一份。

add_executable(demo main.cpp lib.h)

但这是错误的解决办法,这将导致外部项目和库中的 staticMetaObject 是不同的对象。

并且,如果引用的头文件中已经声明了 DLL_EXPORT,同时外部项目不是 BUILD_SHARED(例如构建可执行程序)。此时 DLL_EXPORT 将被解析为 __declscpe(dllimport),从而导致头文件报 不允许 dllimport 静态数据成员 staticMetaObject 的错误。

Qt 布局不更新的问题
2024-12-18 20:03:48+08:00

Qt 布局不更新的问题

修改 QWidget

在 QWidget 已经加入布局之后,修改其大小,布局不会自动更新。需要调用 QWidget::updateGeometry 方法。

类似的,修改子布局时需要调用 QLayout::updateGeometry 方法。

修改 QSpacerItem

在 QSpacerItem 已经加入布局之后,修改其大小,布局不会自动更新。需要调用 QLayout::invalidate 方法。

另外还可以调用 QWidget::adjustSize 方法调整大小.

Qt 日志模块
2024-12-18 20:03:48+08:00

Qt 日志模块

旧的方式:

#include <QDebug>

qDebug() << "hello world"

现在默认所有日志都不打印,因此这种方式看不到打印

QT_LOGGING_RULES="*.debug=false"

新的方式:

#include <QLoggingCategory>

QLoggingCategory category("my.module");
qCDebug(category) << "hello world";

需要配置打印哪些模块的日志

QT_LOGGING_RULES="*.debug=false;my.module.debug=true"

其中日志类型支持 debug, info, warning, critical 四种类型

qDebug 等旧接口所属模块名为 default, 因此如下配置可以使旧的方式打印可见:

QT_LOGGING_RULES="*.debug=false;default.debug=true"

QtQuick/QML 中 console.log 属于 qmljs 模块,因此使用如下配置可以使其打印可见:

QT_LOGGING_RULES="*.debug=false;qml.debug=true"
Qt 资源文件打开失败的问题
2024-12-18 20:03:48+08:00

Qt 资源文件打开失败的问题

首先,Qt 的资源文件需要使用 Q_INIT_RESOURCE 宏进行初始化,参数为 qrc 文件的文件名,例如:

Q_INIT_RESOURCE(theme);   // 初始化 theme.qrc
Q_INIT_RESOURCE(icon);    // 初始化 icon.qrc

并且,这个宏必须在全局命名空间下调用,例如:

static inline void initResource()
{
    Q_INIT_RESOURCE(theme);   // 初始化 theme.qrc
    Q_INIT_RESOURCE(icon);    // 初始化 icon.qrc
}

namespace DemoNamespace
{

class DemoClass
{

public:
    DemoClass()
    {
        initResource(); // 调用初始化函数
    }

};

};

并且,可以使用 Q_CLEANUP_RESOURCE 宏来显式删除资源。

Q_INIT_RESOURCE 仅在将资源构建为静态库时是必须的,在构建动态库和应用程序中时可以省略。

但是我遇到的是另一个问题 —— Qt 的资源集合文件不能重名

在库中创建了名为 theme.qrc 的资源集合文件,之后在应用程序中再次创建一个名为 theme.qrc 的资源集合文件。库的 theme.qrc 会失效。

这个问题仅在 Linux 上存在,而在 Windows 上不存在。因此无法确定是 Feature 还是 Bug。

Ubuntu 上配置 QtQuick(QML) 开发环境
2024-12-18 20:03:48+08:00

Ubuntu 上配置 QtQuick/QML 开发环境

安装以下包:

sudo apt install qtbase5-dev              # Qt5 基础开发包
sudo apt install qtquickcontrols2-5-dev   # QtQuick 基础开发包
sudo apt install qtdeclarative5-dev       # Qt5 的 CMake 模块

sudo apt install qtcreator                # Qt Creator
sudo apt install qttools5-dev             # Qt5 工具,包含 Qt5Designer、Qt5LinguistTools 等
sudo apt install qmlscene                 # QML 预览工具

QML 导入失败时,表示没有安装对应的模块,deb 包名为 qml-module-*

例如 QtQuick.Dialogs 的 deb 包名为 qml-module-qtquick-dialogs

遇到的一些问题

最初安装的包:

sudo apt install qtbase5-dev
sudo apt install qtquickcontrols2-5-dev
sudo apt install qtcreator

执行 CMake 失败

  Could not find a package configuration file provided by "Qt5Quick" 
  with any of the following names:

    Qt5QuickConfig.cmake
    qt5quick-config.cmake

原因: 没有安装 Qt 的 CMake 模块
解决: sudo apt install qtdeclarative5-dev

  Could not find a package configuration file provided by "Qt5LinguistTools"
  with any of the following names:

    Qt5LinguistToolsConfig.cmake
    qt5linguisttools-config.cmake

原因: 没有安装 Qt5 Linguist Tools 的 CMake 模块 解决: sudo apt install qttools5-dev

导入 QtQuick.Controls 2 失败

QQmlApplicationEngine failed to load component
qrc:/main.qml:2:1: plugin cannot be loaded for module "QtQuick.Controls": Cannot protect module QtQuick.Controls 2 as it was never registered

原因: 没有安装 QtQuick.Controls 2 的运行时环境
解决: sudo apt install qml-module-qtquick-controls2

导入 QtQuick.Dialogs 失败

module "QtQuick.Dialogs" is not installed

原因: 没有安装 QtQuick.Dialogs 解决: sudo apt install qml-module-qtquick-dialogs