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"

3 comments:

mentat said...

Great post! I was doing a similar thing with separate QWidgets, but this is much nicer. Now the question is, how do we preview a item that has no children within the QListView?

Benjamin Meyer said...

Taking the iPod as the example when you want to view the file (play the mp3) you could have it switch away from the QListView and to the Player widget. One way to do it would be to have another widget that animated the transition between the listview and the player, another way would be to use a proxy to give the item a child index and set a widget on that index.

mentat said...

The proxy idea is clean--I ended up just hacking the model to give the last item a dummy child and the delegate paint it differently. I'm not sure what you mean by "set a widget on that index"--I'm instead opening the dummy item for editing and creating a widget in the delegate.

Popular Posts