Showing posts with label QImage. Show all posts
Showing posts with label QImage. Show all posts

Wednesday, April 22, 2009

做一个简单的显示图片的 widget

需要明白的是 Qt 实现图片等显示需要一个所谓的 QPaintDevice,我们通过 QPaintEngine 在上面作图,而 QPainter 实现比较高层次的作图动作。对于一些特殊的应用,比如需要渲染 OpenGL 场景,原始的 QPaintEngine 并不适用,注意一般说来 QPaintEngine 是一个 static 对象,我们看看相关一些定义。首先了解一下下面这一段代码,来自 qglobal.h

#define Q_GLOBAL_STATIC(TYPE, NAME)                              \
    static TYPE *NAME()                                          \
    {                                                            \
        static TYPE this_##NAME;                                 \
        static QGlobalStatic<TYPE > global_##NAME(&this_##NAME); \
        return global_##NAME.pointer;                            \
    }
template <typename T>
class QGlobalStatic
{
public:
    T *pointer;
    inline QGlobalStatic(T *p) : pointer(p) { }
    inline ~QGlobalStatic() { pointer = 0; }
};
可见这定义了一个函数,该函数内有一个 static 对象,叫 this_NAME,然后通过模板类产生一个指向该对象的指针。我们在 qwidget.h 里面看到
Q_GLOBAL_STATIC(QX11PaintEngine, qt_widget_paintengine)
,在 qgl.cpp 中发现
Q_GLOBAL_STATIC(QGL2PaintEngineEx, qt_gl_engine)
Q_GLOBAL_STATIC(QOpenGLPaintEngine, qt_gl_engine)
因此,实际上我们平时在 Linux 下使用的 Engine 是 qt_widget_paintengine(QX11PaintEngine),而有两种 OpenGL 的 Engine。

那么使用不同的 Engine 获益主要在于某些 engine 可以通过特殊的硬件,如显卡,对特别的应用实现高速的渲染。因此,普通的 QWidget 如果想具有 OpenGL 的渲染能力需要将其 paint engine 换成 qt_gl_engine,这是通过构造函数里面

QGLWidget::QGLWidget(QWidget *parent, const QGLWidget* shareWidget, Qt::WindowFlags f)
    : QWidget(*(new QGLWidgetPrivate), parent, f | Qt::MSWindowsOwnDC)
{
    Q_D(QGLWidget);
    setAttribute(Qt::WA_PaintOnScreen);
    setAttribute(Qt::WA_NoSystemBackground);
    setAutoFillBackground(true); // for compatibility
    d->init(new QGLContext(QGLFormat::defaultFormat(), this), shareWidget);
}
实现的,这里 QGLContext 在该 QPainterDevice 上建立了 OpenGL 的 context,这是 enable OpenGL 的重要步骤,这个实现也是通过 QGLContextPrivate 的 init() 方法实现的。

有了这些底层的准备工作之后,我们后面还需要做很多事情。虽然 QWidget 提供了一个基本的舞台,但是并没有很多专门性的用途,因此,很多我们使用的 component 都是从 QWidget 里面继承的,比如 QLabel,它本身是一个可以显示 image 的 component。我们可以借鉴它的代码,

void QLabel::setPixmap(const QPixmap &pixmap)
{
    Q_D(QLabel);
    if (!d->pixmap || d->pixmap->cacheKey() != pixmap.cacheKey()) {
        d->clearContents();
        d->pixmap = new QPixmap(pixmap);
    }

    if (d->pixmap->depth() == 1 && !d->pixmap->mask())
        d->pixmap->setMask(*((QBitmap *)d->pixmap));

    d->updateLabel();
}
注意这里使用了一个 QLablePrivate 类作为 QPixmap 的存储,最后调用 QLabelPrivate::updateLabel() 实际上是重画这个 QWidget。那么如何重画呢,其实这时候需要响应所谓的 QPaintEvent,这会调用 QWidget::paintEvent(),我们后面来看看所谓的 event 到底是什么。因此实现一个自己需要的样子的 widget 就只需要把这个 protected 虚函数 overriden 即可。

那么,对于比如说 QGLWidget 而言,如果需要渲染场景、改变场景,是否应该对应到对应到 paint event 呢?我们注意到该 widget 声明中,

protected:
    virtual void initializeGL();
    virtual void resizeGL(int w, int h);
    virtual void paintGL();

    void paintEvent(QPaintEvent*);
我们来看看是怎么回事,在实现文件中,
void QGLWidget::paintEvent(QPaintEvent *)
{
    if (updatesEnabled()) {
        glDraw();
        updateOverlayGL();
    }
}
可见直接调用 OpenGL 函数重绘场景,而另外三个函数提供给需要自己创建自己的 widget 的用户继承该类进行 overridden。因此,我们得出一个基本的事情,就是当我们收到 paint event 时,正常情况是调用 paint engine 重绘,一旦我们需要改变一个 QWidget 对其 paintEvent() 进行重载,QGLWidget 提供的另外三个函数只是为了方便实现一些简单的功能留下来的“钩子”,比如 initializeGL 将在 QGLWidget::glInit() 中调用,初始化 GL 环境后创建初始场景,QGLWidget::glDraw() 中调用 paintGL(),因此响应 paint event 的时候我们可以通过 override paintGL() 更改场景。那么实现 OpenGL 动画的关键就是通过几个参数确定状态,然后在 paintGL() 里面将场景更新,而在一个 QTimer 中依照一定的时间间隔更新参数。

现在我们实现一个简单的显示图片的的 widget,根据前面的知识,我们创建如下的 qmake 配置文件,

TEMPLATE = app
CONFIG += qt debug_and_release
DEPENDPATH += .
INCLUDEPATH += .

TARGET = test_imagewidget
SOURCES = main.cpp imagewidget.cpp
HEADERS = imagewidget.hpp

debug {
  DESTDIR = debug
}

release {
  DESTDIR = release
}

DESTDIR_TARGET = $$DESTDIR
首先我们看看我们的 ImageWidget 类,
#ifndef IMAGEWIDGET_HPP
#define IMAGEWIDGET_HPP

#include <QWidget>
#include <QImage>

class ImageWidget : public QWidget {
  Q_OBJECT
  QImage *img ;
protected:
  void paintEvent( QPaintEvent * ) ;
public:
  ImageWidget( QWidget* = 0 ) ;
  void setImage( QImage * ) ;
  const QImage *getImage() const ;
} ;

#endif
我们通过一个 QImage 的指针用于作图,其实用 QPixmap 可能更好。下面是实现的代码,根据以上分析,我们修改 QWidget::paintEvent() 实现显示功能,
#include "imagewidget.hpp"
#include <QPainter>

ImageWidget::ImageWidget( QWidget* pa ) : QWidget::QWidget( pa )
{
  img = NULL ;
}

void
ImageWidget::setImage( QImage *image )
{
  img = image ;
}

const QImage*
ImageWidget::getImage() const
{
  return img ;
}

void
ImageWidget::paintEvent( QPaintEvent * )
{
  if( img != NULL ) {
    QPainter p( this ) ;
    p.drawImage( QPoint( 0, 0 ), img -> scaled( size() ) ) ;
  }
}
值得注意的是如果我们考虑实现 seam carving 这种东西,我们可能需要对 resizeEvent 进行 override。最后下面是主调文件,
#include <QApplication>
#include <QImage>
#include "imagewidget.hpp"

int
main( int argc, char *argv[] )
{
  QApplication app( argc, argv ) ;
  QImage img ;
  if( argc == 2 )
    img.load( argv[1] ) ;
  else
    return 1 ;

  ImageWidget widget ;
  widget.setImage( &img ) ;
  widget.show() ;

  return app.exec() ;
}

那么很显然,如果我们有一个图片序列,可以用 QTimer 调用 ImageWidget::setImage() 设置图片,然后就可以获得我们需要的结果了。因此从一个高层角度来看,我们去实现一个 image buffer,然后通过 QTimer 从中获得需要的图片,另一个线程更新该 buffer 即可。但是事实上 video 是这样的么?