多个 QGLWidget 同时不适用于视频使用
More than one QGLWidget simultaneous doesn't work for a video usage
如果我想使用 QGLWidget
显示视频,我会遇到问题。在一个实例中它可以工作,但它没有,并且小部件是黑色的且多次出现(例如 QGridLayout
中的用法)。
我子class QGLWidget
那样播放视频(通过刷新QPixmap
我想用我的setCurrentImage
方法显示):
class GLWidget : public QGLWidget
{
Q_OBJECT
public:
GLWidget(QGLWidget* shareWidget = 0, QWidget* parent = 0)
: QGLWidget(parent, shareWidget)
{
//...
// I do this because the QPixmap to refresh is produced in a different thread than UI thread.
connect(this, SIGNAL(updated()), this SLOT(update()));
}
//...
void setCurrentImage(const QPixmap& pixmap)
{
m_mutex.lock();
m_pixmap = pixmap;
m_mutex.unlock();
emit updated();
}
protected:
void initializeGL()
{
static const int coords[4][2] = {{ +1, -1 }, { -1, -1 }, { -1, +1 }, { +1, +1 }};
for(int j = 0; j < 4; ++j){
m_texCoords.append(QVector2D(j == 0 || j == 3, j == 0 || j == 1));
m_vertices.append(QVector2D(0.5 * coords[j][0], 0.5 * coords[j][1]));
}
glEnable(GL_DEPTH_TEST);
glEnable(GL_CULL_FACE);
glEnable(GL_TEXTURE_2D);
#define PROGRAM_VERTEX_ATTRIBUTE 0
#define PROGRAM_TEXCOORD_ATTRIBUTE 1
QGLShader* vShader = new QGLShader(QGLShader::Vertex, this);
const char* szVShaderCode = /*...*/;
vShader->compileSourceCode(szVShaderCode);
QGLShader *fShader = new QGLShader(QGLShader::Fragment, this);
const char* szFShaderCode = /*...*/;
fShader->compileSourceCode(szFShaderCode);
m_pProgram = new QGLShaderProgram(this);
m_pProgram->addShader(vShader);
m_pProgram->addShader(fShader);
m_pProgram->bindAttributeLocation("vertex", PROGRAM_VERTEX_ATTRIBUTE);
m_pProgram->bindAttributeLocation("texCoord", PROGRAM_TEXCOORD_ATTRIBUTE);
m_pProgram->link();
m_pProgram->bind();
m_pProgram->setUniformValue("texture", 0);
}
void paintGL()
{
qglClearColor(Qt::black);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// I use some uniform values to "modify" image
m_pProgram->setUniformValue(/*...*/);
//...
QMatrix4x4 m;
m.ortho(-0.5f, +0.5f, +0.5f, -0.5f, 4.0f, 15.0f);
m.translate(0.0f, 0.0f, -10.0f);
m_pProgram->setUniformValue("matrix", m);
m_pProgram->enableAttributeArray(PROGRAM_VERTEX_ATTRIBUTE);
m_pProgram->enableAttributeArray(PROGRAM_TEXCOORD_ATTRIBUTE);
m_pProgram->setAttributeArray(PROGRAM_VERTEX_ATTRIBUTE, m_vertices.constData());
m_pProgram->setAttributeArray(PROGRAM_TEXCOORD_ATTRIBUTE, m_texCoords.constData());
glBindTexture(GL_TEXTURE_2D, m_texture);
glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
}
void resizeGL(int iWidth, int iHeight)
{
glViewport(0, 0, iWidth, iHeight);
}
private slots:
void update()
{
QPixmap pixmap;
m_mutex.lock();
pixmap = m_pixmap;
m_mutex.unlock();
m_texture = bindTexture(pixmap, GL_TEXTURE_2D);
updateGL();
}
private:
QMutex m_mutex;
QPixmap m_pixmap;
QVector<QVector2D> m_vertices;
QVector<QVector2D> m_texCoords;
QGLShaderProgram* m_pProgram;
};
然后,为了模拟多线程渲染(每个线程产生自己的视频图像),我写了这些 classes。
图片制作者class:
class ImageProducer : public QThread
{
Q_OBJECT
public:
ImageProducer(QGLWidget* pGLWidget)
: QThread(pGLWidget), m_pGLWidget(pGLWidget)
{
m_pixmap = QPixmap("fileName.jpg");
m_bMustStop = false;
}
protected:
void run()
{
while(!m_bMustStop){
static_cast<GLWidget*>(m_pGLWidget)->setCurrentImage(m_pixmap);
// Simulate a frame rate
msleep(1000 / /*FRAME_RATE*/);
}
}
private:
QGLWidget* m_pGLWidget;
QPixmap m_pixmap;
bool m_bMustStop;
};
和渲染class:
#define ROWS 1
#define COLS 1
class Window : public QWidget
{
public:
Window()
{
QGridLayout* pMainLayout = new QGridLayout(this);
setLayout(pMainLayout);
for(int i = 0; i < ROWS; ++i){
for(int j = 0; j < COLS; ++j){
QGLWidget* pGLWidget = new GLWidget();
pMainLayout->addWidget(pGLWidget, i, j);
ImageProducer* pImageProducer = new ImageProducer(pGLWidget);
pImageProducer->start();
}
}
}
};
好的,停止使用代码示例^^ 问题在于 ROWS = 1 和 COLS = 1(请参阅 Window
class) 它有效,但我有其他值的黑色小部件...我迷路了,我想念什么?
谢谢!
编辑:(上下文总是多 GLWidget 实例,只有一个就可以正常工作)
我刚刚发现的奇怪的事情:我覆盖了 QGLWidget
的 mouseMoveEvent
(因此,在我的 GLWidget
class 中)它只是调用 updateGL();
。事实上,当我用当前代码按下并移动鼠标时,什么也没有发生。但是如果我替换(在我的 ImageProducer
的 run()
方法中):
static_cast<GLWidget*>(m_pGLWidget)->setCurrentImage(m_pixmap);
由
static_cast<GLWidget*>(m_pGLWidget)->setCurrentImage(QPixmap("fileName.jpg"));
只要我移动鼠标,图像就会在当前组件中刷新。当我释放它时,或者在其他组件中执行它时,背景又变黑了。
为了其他人的利益,我在与 OP 聊天后写下答案。以下是导致问题得到解决的主要主题。
注意:在撰写本文时,Qt OpenGL 模块已弃用,不鼓励使用它。根据官方 documentation 建议的方法是在 GUI 模块中使用 OpenGL* classes。
纹理
上面代码的主要问题本质上是由于纹理的处理方式。
首先,bindTexture() 方法在 OpenGL 术语中并不是 bind,而是 create OpenGL 纹理并上传作为参数传递给它的数据(QImage 或 QPixmap)。这令人困惑,并可能导致严重的问题。在 OP 发布的代码中,他本质上是 泄漏 内存,在每个帧更新时分配一个新纹理。
为确保不会泄漏内存,您至少应该释放之前分配的纹理,调用 QGLWidget::deleteTexture.
但是,值得注意的是,有更好的方法可以避免不必要的内存碎片和效率低下。在这种情况下,最好的方法是分配纹理 一次 并在必要时简单地更新其内容。这可能无法直接使用旧 Qt OpenGL 模块提供的 API,但您始终可以混合使用 Qt 和 native OpenGL 代码。
上下文处理
另一个问题是 OpenGL 上下文的处理方式。根据经验,在向 OpenGL 实现发出命令之前,应始终确保正确的上下文是 current。有时,Qt 会自动为您执行此操作(即在调用 paintGL() 方法之前)。
在这种情况下,我们需要在调用 bindTexture 方法之前明确地使 QGLWidget 的上下文成为当前上下文,否则它将影响成为当前的 last 上下文。这就是为什么只有最后一个小部件显示一些东西,直到有人通过与其他小部件交互来触发 makeCurrent 调用。
线程
这里有几个问题。首先,在 GUI 线程以外的线程中使用 QPixmap 对象是不安全的。 QPixmap 旨在优化屏幕上的像素图块传输。在这种情况下,像素图实际上只是要上传到处理所有渲染的 OpenGL 实现的帧。所以使用 QImage 是安全的。
另一个问题是 GLWidget::setCurrentImage() 以及 bindTexture 方法直接从 运行() ImageProducer 线程的方法。这不可能是因为我们需要使小部件上下文成为当前的,但是不可能从 GUI 线程以外的线程调用 makeCurrent()(更多细节 here).
解决此问题的一种可能方法是向 ImageProducer 添加一个信号以通知帧已更新,并将此信号连接到 setCurrentImage() 插槽。 Qt的signal-slot机制会保证setCurrentImage在GLWidget线程中执行,也就是GUI线程。
代码
以下是对上面发布的代码的建议(和测试)修改。
ImageProduer class
添加信号:
void imageUpdated(const QImage &image);
并在框架更新时发出它:
void ImageProducer::run()
{
while(!m_bMustStop){
QImage frame(m_pixmap.width(), m_pixmap.height(), m_pixmap.format());
QPainter painter(&frame);
painter.drawImage(QPoint(), m_pixmap);
painter.setFont(QFont("sans-serif", 22));
painter.setPen(Qt::white);
painter.drawText(20, 50, QString("Frame: %1").arg(QString::number(_frameCount)));
painter.end();
emit imageUpdated(frame);
msleep(1000 / FRAME_RATE);
_frameCount++;
}
}
GLWidget class
确保 setCurrentImage 方法处理 OpenGL 上下文并销毁旧纹理:
void GLWidget::setCurrentImage(const QImage& pixmap)
{
m_mutex.lock();
m_pixmap = pixmap;
m_mutex.unlock();
makeCurrent();
deleteTexture(m_texture);
m_texture = bindTexture(pixmap, GL_TEXTURE_2D);
doneCurrent();
emit updated();
}
如果我想使用 QGLWidget
显示视频,我会遇到问题。在一个实例中它可以工作,但它没有,并且小部件是黑色的且多次出现(例如 QGridLayout
中的用法)。
我子class QGLWidget
那样播放视频(通过刷新QPixmap
我想用我的setCurrentImage
方法显示):
class GLWidget : public QGLWidget
{
Q_OBJECT
public:
GLWidget(QGLWidget* shareWidget = 0, QWidget* parent = 0)
: QGLWidget(parent, shareWidget)
{
//...
// I do this because the QPixmap to refresh is produced in a different thread than UI thread.
connect(this, SIGNAL(updated()), this SLOT(update()));
}
//...
void setCurrentImage(const QPixmap& pixmap)
{
m_mutex.lock();
m_pixmap = pixmap;
m_mutex.unlock();
emit updated();
}
protected:
void initializeGL()
{
static const int coords[4][2] = {{ +1, -1 }, { -1, -1 }, { -1, +1 }, { +1, +1 }};
for(int j = 0; j < 4; ++j){
m_texCoords.append(QVector2D(j == 0 || j == 3, j == 0 || j == 1));
m_vertices.append(QVector2D(0.5 * coords[j][0], 0.5 * coords[j][1]));
}
glEnable(GL_DEPTH_TEST);
glEnable(GL_CULL_FACE);
glEnable(GL_TEXTURE_2D);
#define PROGRAM_VERTEX_ATTRIBUTE 0
#define PROGRAM_TEXCOORD_ATTRIBUTE 1
QGLShader* vShader = new QGLShader(QGLShader::Vertex, this);
const char* szVShaderCode = /*...*/;
vShader->compileSourceCode(szVShaderCode);
QGLShader *fShader = new QGLShader(QGLShader::Fragment, this);
const char* szFShaderCode = /*...*/;
fShader->compileSourceCode(szFShaderCode);
m_pProgram = new QGLShaderProgram(this);
m_pProgram->addShader(vShader);
m_pProgram->addShader(fShader);
m_pProgram->bindAttributeLocation("vertex", PROGRAM_VERTEX_ATTRIBUTE);
m_pProgram->bindAttributeLocation("texCoord", PROGRAM_TEXCOORD_ATTRIBUTE);
m_pProgram->link();
m_pProgram->bind();
m_pProgram->setUniformValue("texture", 0);
}
void paintGL()
{
qglClearColor(Qt::black);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// I use some uniform values to "modify" image
m_pProgram->setUniformValue(/*...*/);
//...
QMatrix4x4 m;
m.ortho(-0.5f, +0.5f, +0.5f, -0.5f, 4.0f, 15.0f);
m.translate(0.0f, 0.0f, -10.0f);
m_pProgram->setUniformValue("matrix", m);
m_pProgram->enableAttributeArray(PROGRAM_VERTEX_ATTRIBUTE);
m_pProgram->enableAttributeArray(PROGRAM_TEXCOORD_ATTRIBUTE);
m_pProgram->setAttributeArray(PROGRAM_VERTEX_ATTRIBUTE, m_vertices.constData());
m_pProgram->setAttributeArray(PROGRAM_TEXCOORD_ATTRIBUTE, m_texCoords.constData());
glBindTexture(GL_TEXTURE_2D, m_texture);
glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
}
void resizeGL(int iWidth, int iHeight)
{
glViewport(0, 0, iWidth, iHeight);
}
private slots:
void update()
{
QPixmap pixmap;
m_mutex.lock();
pixmap = m_pixmap;
m_mutex.unlock();
m_texture = bindTexture(pixmap, GL_TEXTURE_2D);
updateGL();
}
private:
QMutex m_mutex;
QPixmap m_pixmap;
QVector<QVector2D> m_vertices;
QVector<QVector2D> m_texCoords;
QGLShaderProgram* m_pProgram;
};
然后,为了模拟多线程渲染(每个线程产生自己的视频图像),我写了这些 classes。
图片制作者class:
class ImageProducer : public QThread
{
Q_OBJECT
public:
ImageProducer(QGLWidget* pGLWidget)
: QThread(pGLWidget), m_pGLWidget(pGLWidget)
{
m_pixmap = QPixmap("fileName.jpg");
m_bMustStop = false;
}
protected:
void run()
{
while(!m_bMustStop){
static_cast<GLWidget*>(m_pGLWidget)->setCurrentImage(m_pixmap);
// Simulate a frame rate
msleep(1000 / /*FRAME_RATE*/);
}
}
private:
QGLWidget* m_pGLWidget;
QPixmap m_pixmap;
bool m_bMustStop;
};
和渲染class:
#define ROWS 1
#define COLS 1
class Window : public QWidget
{
public:
Window()
{
QGridLayout* pMainLayout = new QGridLayout(this);
setLayout(pMainLayout);
for(int i = 0; i < ROWS; ++i){
for(int j = 0; j < COLS; ++j){
QGLWidget* pGLWidget = new GLWidget();
pMainLayout->addWidget(pGLWidget, i, j);
ImageProducer* pImageProducer = new ImageProducer(pGLWidget);
pImageProducer->start();
}
}
}
};
好的,停止使用代码示例^^ 问题在于 ROWS = 1 和 COLS = 1(请参阅 Window
class) 它有效,但我有其他值的黑色小部件...我迷路了,我想念什么?
谢谢!
编辑:(上下文总是多 GLWidget 实例,只有一个就可以正常工作)
我刚刚发现的奇怪的事情:我覆盖了 QGLWidget
的 mouseMoveEvent
(因此,在我的 GLWidget
class 中)它只是调用 updateGL();
。事实上,当我用当前代码按下并移动鼠标时,什么也没有发生。但是如果我替换(在我的 ImageProducer
的 run()
方法中):
static_cast<GLWidget*>(m_pGLWidget)->setCurrentImage(m_pixmap);
由
static_cast<GLWidget*>(m_pGLWidget)->setCurrentImage(QPixmap("fileName.jpg"));
只要我移动鼠标,图像就会在当前组件中刷新。当我释放它时,或者在其他组件中执行它时,背景又变黑了。
为了其他人的利益,我在与 OP 聊天后写下答案。以下是导致问题得到解决的主要主题。
注意:在撰写本文时,Qt OpenGL 模块已弃用,不鼓励使用它。根据官方 documentation 建议的方法是在 GUI 模块中使用 OpenGL* classes。
纹理
上面代码的主要问题本质上是由于纹理的处理方式。
首先,bindTexture() 方法在 OpenGL 术语中并不是 bind,而是 create OpenGL 纹理并上传作为参数传递给它的数据(QImage 或 QPixmap)。这令人困惑,并可能导致严重的问题。在 OP 发布的代码中,他本质上是 泄漏 内存,在每个帧更新时分配一个新纹理。
为确保不会泄漏内存,您至少应该释放之前分配的纹理,调用 QGLWidget::deleteTexture.
但是,值得注意的是,有更好的方法可以避免不必要的内存碎片和效率低下。在这种情况下,最好的方法是分配纹理 一次 并在必要时简单地更新其内容。这可能无法直接使用旧 Qt OpenGL 模块提供的 API,但您始终可以混合使用 Qt 和 native OpenGL 代码。
上下文处理
另一个问题是 OpenGL 上下文的处理方式。根据经验,在向 OpenGL 实现发出命令之前,应始终确保正确的上下文是 current。有时,Qt 会自动为您执行此操作(即在调用 paintGL() 方法之前)。
在这种情况下,我们需要在调用 bindTexture 方法之前明确地使 QGLWidget 的上下文成为当前上下文,否则它将影响成为当前的 last 上下文。这就是为什么只有最后一个小部件显示一些东西,直到有人通过与其他小部件交互来触发 makeCurrent 调用。
线程
这里有几个问题。首先,在 GUI 线程以外的线程中使用 QPixmap 对象是不安全的。 QPixmap 旨在优化屏幕上的像素图块传输。在这种情况下,像素图实际上只是要上传到处理所有渲染的 OpenGL 实现的帧。所以使用 QImage 是安全的。
另一个问题是 GLWidget::setCurrentImage() 以及 bindTexture 方法直接从 运行() ImageProducer 线程的方法。这不可能是因为我们需要使小部件上下文成为当前的,但是不可能从 GUI 线程以外的线程调用 makeCurrent()(更多细节 here).
解决此问题的一种可能方法是向 ImageProducer 添加一个信号以通知帧已更新,并将此信号连接到 setCurrentImage() 插槽。 Qt的signal-slot机制会保证setCurrentImage在GLWidget线程中执行,也就是GUI线程。
代码
以下是对上面发布的代码的建议(和测试)修改。
ImageProduer class
添加信号:
void imageUpdated(const QImage &image);
并在框架更新时发出它:
void ImageProducer::run()
{
while(!m_bMustStop){
QImage frame(m_pixmap.width(), m_pixmap.height(), m_pixmap.format());
QPainter painter(&frame);
painter.drawImage(QPoint(), m_pixmap);
painter.setFont(QFont("sans-serif", 22));
painter.setPen(Qt::white);
painter.drawText(20, 50, QString("Frame: %1").arg(QString::number(_frameCount)));
painter.end();
emit imageUpdated(frame);
msleep(1000 / FRAME_RATE);
_frameCount++;
}
}
GLWidget class
确保 setCurrentImage 方法处理 OpenGL 上下文并销毁旧纹理:
void GLWidget::setCurrentImage(const QImage& pixmap)
{
m_mutex.lock();
m_pixmap = pixmap;
m_mutex.unlock();
makeCurrent();
deleteTexture(m_texture);
m_texture = bindTexture(pixmap, GL_TEXTURE_2D);
doneCurrent();
emit updated();
}