Monday, September 25, 2006

Drill Down View

When dealing with data that is in a hierarchy, one way to present the data is inside of a drill down view. Some examples of what a drill down view are include the recent Start menu redesign put out by SuSe, the TiVo menus, and the iPod interface. It has the fantastic features of taking up almost no space and being very intuative. For more in depth discussion on this view check out One-Window Drilldown on the UI Patters website. Creating this in Qt 4 was very simple, here is a animated gif screengrab of the results.



The implimention was done by subclassing QListView and its moveCursor function. Before changing the root index I would store the current view image. When the current index changes I start the animation. One feature found in drill down views is animations. In Qt 4.2 there is a new class called QTimeLine. This class is perfect for helping you add animations to widgets such as the above one. Below you will find the code that you can use to have a drill down view in your application. The only code I didn't show is a item delegate that I wrote to draw the triangles on the right hand side, but i'll leave that part as an exercise for the reader.


#include "QtGui/QtGui"

class DrillDownView : public QListView {
Q_OBJECT

public:
DrillDownView(QWidget *parent = 0) : QListView(parent) {
connect(&animation, SIGNAL(frameChanged(int)), this, SLOT(slide(int)));
animation.setDuration(200);
};
QModelIndex moveCursor(CursorAction cursorAction, Qt::KeyboardModifiers modifiers);

public slots:
void currentChanged( const QModelIndex ¤t, const QModelIndex &previous );
void slide(int x);

protected:
void paintEvent(QPaintEvent * event);

private:
QTimeLine animation;
QPixmap oldView;
QPixmap newView;
int lastPosition;
};

void DrillDownView::paintEvent(QPaintEvent *event) {
if (animation.state() == QTimeLine::Running) {
QPainter painter(viewport());
if (animation.direction() == QTimeLine::Backward) {
painter.drawPixmap(-animation.currentFrame(), 0, newView);
painter.drawPixmap(-animation.currentFrame() + animation.endFrame(), 0, oldView);
} else {
painter.drawPixmap(-animation.currentFrame(), 0, oldView);
painter.drawPixmap(-animation.currentFrame() + animation.endFrame(), 0, newView);
}
} else {
QListView::paintEvent(event);
}
}

void DrillDownView::slide(int x){
viewport()->scroll(lastPosition - x, 0);
lastPosition = x;
}

QModelIndex DrillDownView::moveCursor(CursorAction cursorAction, Qt::KeyboardModifiers modifiers)
{
if (animation.state() == QTimeLine::Running)
return QListView::moveCursor(cursorAction, modifiers);

QModelIndex current = currentIndex();
if (cursorAction == MoveLeft && current.parent().isValid()) {
oldView = QPixmap::grabWidget(viewport());
return current.parent();
}

if (cursorAction == MoveRight && model() && model()->hasChildren(current)) {
oldView = QPixmap::grabWidget(viewport());
return model()->index(0, 0, current);
}

return QListView::moveCursor(cursorAction, modifiers);
}

void DrillDownView::currentChanged(const QModelIndex ¤t, const QModelIndex &previous) {
if ((current.isValid() && previous.isValid())
&& (current.parent() == previous || previous.parent() == current)) {
setUpdatesEnabled(false);
setRootIndex(currentIndex().parent());
setCurrentIndex(currentIndex());
executeDelayedItemsLayout();
// Force the hiding/showing of scrollbars
setVerticalScrollBarPolicy(verticalScrollBarPolicy());
newView = QPixmap::grabWidget(viewport());
setUpdatesEnabled(true);

int length = qMax(oldView.width(), newView.width());
lastPosition = (previous.parent() == current) ? length : 0;
animation.setFrameRange(0, length);
animation.stop();
animation.setDirection(previous.parent() == current ? QTimeLine::Backward : QTimeLine::Forward);
animation.start();
} else {
QListView::currentChanged(current, previous);
}
}

int main(int argc, char *argv[])
{
QApplication app(argc, argv);
DrillDownView view;
view.setWindowTitle("Use arrow keys");
QDirModel dirmodel;
view.setModel(&dirmodel);
view.show();
return app.exec();
}

#include "main.moc"

Popular Posts