Home · Examples 


Item View Chart Example

Code:

The Item View Chart example shows how to create a custom view for the model/view framework.

In this example, the items in a table model are represented as slices in a pie chart, relying on the flexibility of the model/view architecture to handle custom editing and selection features.

Note that you only need to create a new view class if your data requires a specialized representation. You should first consider using a standard QListView, QTableView, or QTreeView with a custom QItemDelegate subclass if you need to represent data in a special way.

Many alternative representations of the same data, such as bar charts, could more easily be obtained using custom delegates with a QTableView.

The example provides a custom QAbstractItemView class, PieView, that is displayed side-by-side with a QTableView in a main window provided by the ItemviewChart class, a subclass of QMainWindow.

ItemviewChart Class Implementation

The ItemviewChart class contains methods to handle the user interface and set up a model with data obtained from a file. It also contains the implementation of the PieView class (shown later).
public class ItemviewChart extends QMainWindow {

    private QAbstractItemModel model;
We define private variables for the model so that we can access and modify it as required.

The constructor sets up the user interface, connecting actions used in the menus to slots in the class, constructs a model, and arranges the views in the window:

    public ItemviewChart() {
        QMenu fileMenu = new QMenu(tr("&File"), this);

        QAction openAction = new QAction(tr("&Open..."), this);
        openAction.setShortcut(new QKeySequence(tr("Ctrl+O")));
        openAction.triggered.connect(this, "openFile()");
        fileMenu.addAction(openAction);

        QAction saveAction = new QAction(tr("&Save As..."), this);
        saveAction.setShortcut(new QKeySequence(tr("Ctrl+S")));
        saveAction.triggered.connect(this, "saveFile()");
        fileMenu.addAction(saveAction);

        QAction quitAction = new QAction(tr("&Quit"), this);
        quitAction.setShortcut(new QKeySequence(tr("Ctrl+Q")));
        quitAction.triggered.connect(this, "close()");
        fileMenu.addAction(quitAction);

        setupModel();
        setupViews();

        menuBar().addMenu(fileMenu);
        statusBar();

        openFile("classpath:com/trolltech/examples/qtdata.cht");

        setWindowTitle(tr("Chart"));
        setWindowIcon(new QIcon("classpath:com/trolltech/images/qt-logo.png"));
        resize(750, 500);
    }
The data in the qtdata.cht file is accessed from the classpath via the resource system.

The setupModel() method provides an empty model with a reasonable number of rows and columns:

    private void setupModel() {
        model = new QStandardItemModel(8, 2, this);
        model.setHeaderData(0, Qt.Orientation.Horizontal, tr("Label"));
        model.setHeaderData(1, Qt.Orientation.Horizontal, tr("Quantity"));
    }
Since the data used with the example is arranged in the form of a two-column table with an arbitrary number of rows, it is useful to set the number of columns when the model is constructed. The number of rows defined here is a useful minimum value, allowing the user to experiment by adding additional data to small data sets.

The setupViews() method arranges a table view alongside the custom pie view in a QSplitter widget, enabling the amount of space assigned to each to be adjusted by the user:

    private void setupViews() {
        QSplitter splitter = new QSplitter();
        QTableView table = new QTableView();
        QAbstractItemView pieChart = new PieView(this);
        splitter.addWidget(table);
        splitter.addWidget(pieChart);
        splitter.setStretchFactor(0, 0);
        splitter.setStretchFactor(1, 1);

        table.setModel(model);
        pieChart.setModel(model);

        QItemSelectionModel selectionModel = new QItemSelectionModel(model);
        table.setSelectionModel(selectionModel);
        pieChart.setSelectionModel(selectionModel);

        setCentralWidget(splitter);
    }
We construct a selection model for both views to share, before setting the splitter as the central widget for the main window.

The openFile() and saveFile() methods are called when the user selects the respective File|Open... and File|Save As... menu items. openFile() reads and parses data in a simple format, and populates the model with items. saveFile() exports the contents of the model to a file in the same format.

PieView Class Overview

There are a number of different groups of methods in the PieView class. Some methods provide functionality defined in QAbstractItemView, and are required for the view to method correctly. These typically handle interaction between the view and its model, or indicate the mapping between model indexes and the visual location of items within the view. Each view class needs to provide implementations of methods that specify how it arranges items in a view. These relate model indexes to the positions of corresponding visible items and the regions they occupy. Three methods are needed to control which part of the view is shown in the viewport: The view also needs to handle keyboard input to allow navigation between items, and provide support for selections. A selection of event handlers are reimplemented to provide support for item selection and painting: For convenience, we also implement a rows() method specific to this view that provides a convenient way to obtain the number of sibling of the item corresponding to a given model index.

PieView Class Implementation

The PieView class is subclassed from
QAbstractItemView rather than one of the standard view classes because it represents data obtained from a model in a way that is quite different from those views.

To operate correctly, the view needs to record certain pieces of information about the data supplied by the model, including the sum of all values in the second column of the model and the number of valid items:

    private class PieView extends QAbstractItemView {

        private int margin;
        private int totalSize;
        private int pieSize;
        private int validItems;
        private double totalValue;
        private QPoint origin;
        private QRubberBand rubberBand;
The constructor provides default values for these variables:
        public PieView(QWidget parent) {
            super(parent);
            horizontalScrollBar().setRange(0, 0);
            verticalScrollBar().setRange(0, 0);

            margin = 8;
            totalSize = 300;
            pieSize = totalSize - 2 * margin;
            validItems = 0;
            totalValue = 0.0;
        }
We also set constant values for the total size of the view and its margin, calculating the pie chart's size from these values.

Handling Changes to Data

When data in the model is changed, the dataChanged() slot is called:
        protected void dataChanged(final QModelIndex topLeft, final QModelIndex bottomRight) {
            super.dataChanged(topLeft, bottomRight);

            validItems = 0;
            totalValue = 0.0;

            for (int row = 0; row < model().rowCount(rootIndex()); ++row) {

                QModelIndex index = model().index(row, 1, rootIndex());
                double value = toDouble(model().data(index));

                if (value > 0.0) {
                    totalValue += value;
                    validItems++;
                }
            }
            viewport().update();

        }
Since the view needs to know the sum of all values in the second column in order to correctly represent each individual item in the pie chart, it is useful to keep a running total of all values. Since we do not know the previous values of the items of data changed, we simply recalculate the total and number of valid items.

The rowsInserted() and rowsAboutToBeRemoved() slots are able to modify the running total and update the number of valid items:

        protected void rowsInserted(final QModelIndex parent, int start, int end) {
            for (int row = start; row <= end; ++row) {

                QModelIndex index = model().index(row, 1, rootIndex());
                double value = toDouble(model().data(index));

                if (value > 0.0) {
                    totalValue += value;
                    validItems++;
                }
            }

            super.rowsInserted(parent, start, end);
        }

        @Override
        protected void rowsAboutToBeRemoved(final QModelIndex parent, int start, int end) {

            for (int row = start; row <= end; ++row) {

                QModelIndex index = model().index(row, 1, rootIndex());
                double value = toDouble(model().data(index));
                if (value > 0.0) {
                    totalValue -= value;
                    validItems--;
                }
            }

            super.rowsAboutToBeRemoved(parent, start, end);

        }
The edit() slot handles item editing:
        protected boolean edit(final QModelIndex index, EditTrigger trigger, QEvent event) {
            return false;
        }
We only allow the user to edit the labels for each item; these are stored in the first column of the model.

Relating Model Indexes to the View

When the user interacts with the view using the mouse cursor, the position of the cursor needs to be related to the relevant model index so that the appropriate editing or selection actions can be performed.

Since custom views manage the geometries of the items they display, the indexAt() method must be implemented to perform this relation for each view, returning a model index corresponding to the item in which the given point lies, or an invalid model index if the point does not intersect with an item.

        public QModelIndex indexAt(final QPoint point) {
            if (validItems == 0)
                return null;

            int wx = point.x() + horizontalScrollBar().value();
            int wy = point.y() + verticalScrollBar().value();
The initial conversion between viewport and view (contents) coordinates is straightforward for this view, requiring a simple translation based on the scroll offsets.

Since the view displays a pie chart in the left half of its area and a key on the right, we need to handle the point differently depending on its location. We compare a point that lies within the pie chart area against each of the slices in the pie and, if it lies within a slice, we return the corresponding model index:

            if (wx < totalSize) {
                double cx = wx - totalSize / 2;
                double cy = totalSize / 2 - wy;
                double d = Math.pow(Math.pow(cx, 2) + Math.pow(cy, 2), 0.5);

                if (d == 0 || d > pieSize / 2)
                    return null;

                double angle = (180 / Math.PI) * Math.acos(cx / d);
                if (cy < 0)
                    angle = 360 - angle;

                double startAngle = 0.0;

                for (int row = 0; row < model().rowCount(rootIndex()); ++row) {

                    QModelIndex index = model().index(row, 1, rootIndex());
                    double value = toDouble(model().data(index));

                    if (value > 0.0) {
                        double sliceAngle = 360 * value / totalValue;

                        if (angle >= startAngle && angle < (startAngle + sliceAngle))
                            return model().index(row, 1, rootIndex());

                        startAngle += sliceAngle;

                    }
                }
            }

            return null;

        }
If the point did not lie within any parts of the view that correspond to model indexes, null is returned.

The itemRect() method is used to obtain rectangles defined in view coordinates that correspond to model indexes:

        QRect itemRect(final QModelIndex index) {
            if (index == null)
                return new QRect();

            if (index.column() != 1)
                return new QRect();

            if (toDouble(model().data(index)) > 0.0) {
                return new QRect(margin, margin, pieSize, pieSize);

            }
            return new QRect();

        }
In this example, we only return useful information for the model indexes that are used to obtain information about the key labels in the right half of the view. For the pie chart, we simply return a
QRect that covers the entire pie chart. We return a null QRect for invalid model indexes.

For the itemRegion() implementation, we can provide more accurate information about the geometry of each slice in the pie chart that corresponds to the specified model index:

        QRegion itemRegion(final QModelIndex index) {
            if (index == null)
                return null;


            if (index.column() != 1)
                return null;


            if (toDouble(model().data(index)) <= 0.0)
                return null;
We are only interested in items in the second column in the model that contain positive double values, so we return null for all other model indexes.

For each suitable model index, we use a QPainterPath to define the geometry of the item, and convert it to a filled polygon to construct a new QRegion object:

            double startAngle = 0.0;
            for (int row = 0; row < model().rowCount(rootIndex()); ++row) {

                QModelIndex sliceIndex = model().index(row, 1, rootIndex());
                double value = toDouble(model().data(sliceIndex));

                if (value > 0.0) {
                    double angle = 360 * value / totalValue;

                    if (sliceIndex.equals(index)) {
                        QPainterPath slicePath = new QPainterPath();
                        slicePath.moveTo(totalSize / 2, totalSize / 2);
                        slicePath.arcTo(margin, margin, margin + pieSize, margin + pieSize, startAngle, angle);
                        slicePath.closeSubpath();

                        return new QRegion(slicePath.toFillPolygon().toPolygon());
                    }
                    startAngle += angle;

                }
            }

            return null;

        }
If the model index supplied was invalid, null is returned.

The visualRect() method returns the rectangle that corresponds to a given model index in viewport coordinates, calling the itemRect() method to avoid duplicating effort:

        public QRect visualRect(final QModelIndex index) {
            QRect rect = itemRect(index);
            if (rect.isValid())
                return new QRect(rect.left() - horizontalScrollBar().value(), rect.top() - verticalScrollBar().value(), rect.width(), rect.height());
            else
                return rect;
        }
The visualRegionForSelection() method is similar to visualRect() but, where visualRect(), itemRect(), and itemRegion() are only used to obtain geometric information about individual items, this method is used to obtain regions that correspond to a number of selected items in viewport coordinates:
        protected QRegion visualRegionForSelection(final QItemSelection selection) {
            int ranges = selection.size();

            if (ranges == 0)
                return new QRegion(new QRect());

            QRegion region = new QRegion();
            for (int i = 0; i < ranges; ++i) {
                QItemSelectionRange range = selection.at(i);
                for (int row = range.top(); row <= range.bottom(); ++row) {
                    for (int col = range.left(); col <= range.right(); ++col) {
                        QModelIndex index = model().index(row, col, rootIndex());
                        region = region.united(new QRegion(visualRect(index)));
                    }
                }
            }
            return region;

        }
We start with an empty region, and cumulatively find its union with the region that corresponds to each selected item. The result we return is the union of all selected items.

Scrolling and Viewport Handling

We need to ensure that the viewport only shows the part of the visible part of the view's contents, represented by the positions of the scroll bars if they are shown. To do this, we need to implement the horizontalOffset() and verticalOffset() methods to return the position of the top-left corner of the visible area relative to the top-left corner of the view's contents:
        protected int horizontalOffset() {
            return horizontalScrollBar().value();
        }

protected int verticalOffset() { return verticalScrollBar().value(); }
Since the view does not contain scaled content, these methods simply return the corresponding scroll bar values.

The scrollTo() method is used to navigate to the item that corresponds to a particular model index:

        public void scrollTo(final QModelIndex index, ScrollHint hint) {
            QRect area = viewport().rect();
            QRect rect = visualRect(index);

            if (rect.left() < area.left())
                horizontalScrollBar().setValue(
                    horizontalScrollBar().value() + rect.left() - area.left());
            else if (rect.right() > area.right())
                horizontalScrollBar().setValue(
                    horizontalScrollBar().value() + Math.min(
                        rect.right() - area.right(), rect.left() - area.left()));

            if (rect.top() < area.top())
                verticalScrollBar().setValue(
                    verticalScrollBar().value() + rect.top() - area.top());
            else if (rect.bottom() > area.bottom())
                verticalScrollBar().setValue(
                    verticalScrollBar().value() + Math.min(
                        rect.bottom() - area.bottom(), rect.top() - area.top()));

            update();
        }
We find the position of the item in viewport coordinates, and calculate the displacement from the top-left of the viewport. The result is used to provide new values for the horizontal and vertical scroll bars.

The updateGeometries() method ensures that the scroll bars have the correct size when the size of the viewport changes:

        protected void updateGeometries() {
            horizontalScrollBar().setPageStep(viewport().width());
            horizontalScrollBar().setRange(0, Math.max(0, totalSize - viewport().width()));
            verticalScrollBar().setPageStep(viewport().height());
            verticalScrollBar().setRange(0, Math.max(0, totalSize - viewport().height()));
        }
This method is called by the implementation of resizeEvent() handler method.

Keyboard Navigation and Selections

In the moveCursor() method, we interpret standard keyboard actions entered by the user, and update the current model index to refer to the corresponding current item in the view.

In this model, we only handle actions for the cursor keys:

        protected QModelIndex moveCursor(QAbstractItemView.CursorAction cursorAction, Qt.KeyboardModifiers modifiers) {
            QModelIndex current = currentIndex();

            switch (cursorAction) {
            case MoveLeft:
            case MoveUp:
                if (current.row() > 0)
                    current = model().index(current.row() - 1, current.column(), rootIndex());
                else
                    current = model().index(0, current.column(), rootIndex());
                break;
            case MoveRight:
            case MoveDown:
                if (current.row() < rows(current) - 1)
                    current = model().index(current.row() + 1, current.column(), rootIndex());
                else
                    current = model().index(rows(current) - 1, current.column(), rootIndex());
                break;
            default:
                break;
            }

            viewport().update();
            return current;

        }
Here, we interpret the left and up cursor keys in the same way, setting the current model index to the index in the row above it in the model. Similarly, the right and down cursor keys cause the current model index to be set to the index in the row below it in the model. Although the view represents the items of data in the form of a pie chart, we do not allow the current index to "wrap around" from one end of the data set to the other.

Selections of items are handled by the setSelection() method, which translates a QRect, given in viewport coordinates, to the corresponding QRect in view contents coordinates, and determines which items lie within this selection rectangle.

First, we obtain model indexes from the model for each item of data, using itemRegion() to obtain a region for each item.

        protected void setSelection(final QRect rect, QItemSelectionModel.SelectionFlags command) {
            QRect contentsRect = rect.translated(horizontalScrollBar().value(), verticalScrollBar().value()).normalized();

            int rows = model().rowCount(rootIndex());
            int columns = model().columnCount(rootIndex());
            Vector<QModelIndex> indexes = new Vector<QModelIndex>();

            for (int row = 0; row < rows; ++row) {
                for (int column = 0; column < columns; ++column) {
                    QModelIndex index = model().index(row, column, rootIndex());
                    QRegion region = itemRegion(index);

                    if (region != null && region.intersects(contentsRect))
                        indexes.add(index);

                }

            }
If an item's region intersects the selection rectangle, we add the model index to a vector of indexes.

Once we have examined the regions of each item, we check whether any indexes were stored in the vector. If so, we construct a selection by extending the current selection to cover a range of indexes.


            if (indexes.size() > 0) {
                int firstRow = indexes.elementAt(0).row();
                int lastRow = indexes.elementAt(0).row();
                int firstColumn = indexes.elementAt(0).column();
                int lastColumn = indexes.elementAt(0).column();

                for (int i = 1; i < indexes.size(); ++i) {
                    firstRow = Math.min(firstRow, indexes.elementAt(i).row());
                    lastRow = Math.max(lastRow, indexes.elementAt(i).row());
                    firstColumn = Math.min(firstColumn, indexes.elementAt(i).column());
                    lastColumn = Math.max(lastColumn, indexes.elementAt(i).column());
                }

                QItemSelection selection = new QItemSelection(
                    model().index(firstRow, firstColumn, rootIndex()),
                    model().index(lastRow, lastColumn, rootIndex()));
                selectionModel().select(selection, command);
            } else {
                QModelIndex noIndex = null;
                QItemSelection selection = new QItemSelection(noIndex, noIndex);
                selectionModel().select(selection, command);
            }

            update();

        }
Note that we still apply a selection even if there are no model indexes to select; this ensures that selection commands such as Clear have an effect on the selection.

Event Handler Functions

We reimplement mouse event handlers for the view to control the way items are selected in the view.

In the mousePressEvent() method, we call the base class's method to ensure that various basic tasks are performed and, if necessary, we create a QRubberBand object that we will use to indicate the region the user has selected in the view:

        protected void mousePressEvent(QMouseEvent event) {
            super.mousePressEvent(event);
            origin = event.pos();
            if (rubberBand == null)
                rubberBand = new QRubberBand(QRubberBand.Shape.Rectangle, this);
            rubberBand.setRubberBandGeometry(new QRect(origin, new QSize()));
            rubberBand.show();
        }
We record the position of the mouse press in the instance's origin variable for later use. The rubber band initially has an invalid size; it will be resized if the user moves the mouse while the mouse button is held down.

The mouseMoveEvent() implementation resizes the rubber band using the value previously stored in origin, taking care to normalize the newly-constructed rectangle first, and calls the base class's implementation of the method:

        protected void mouseMoveEvent(QMouseEvent event) {
            QRect rect = new QRect(origin, event.pos()).normalized();
            rubberBand.setRubberBandGeometry(rect);
            super.mouseMoveEvent(event);

            QModelIndex underMouseIndex = indexAt(event.pos());
            if (underMouseIndex == null)
                setSelection(rect, selectionCommand(underMouseIndex, event));
            viewport().update();
        }
If the mouse is over an item in the view, we update the selection to include the corresponding model index. We also update the visible part of the view to show any changes.

In the mouseReleaseEvent() method, we call the base class's implementation, hide the rubber band, and update the visible part of the view:

        protected void mouseReleaseEvent(QMouseEvent event) {
            super.mouseReleaseEvent(event);
            rubberBand.hide();
            viewport().update();
        }
Although the selection handling provided by these event handlers is quite simple, it demonstrates a basic level of functionality that can be used as a starting point for more complex views.


Copyright © 2009 Nokia Corporation and/or its subsidiary(-ies) Trademarks
Qt Jambi 4.5.2_01