|  | Home · Examples

# Item View Chart Example

Code:

- ItemviewChart.java

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.

- The dataChanged(), rowsInserted(), and rowsAboutToBeRemoved() methods are the couterparts of the signals with the same signatures in the QAbstractItemModel class. These handle changes to the model's data and underlying structure specified by model indexes.
- The edit() method is called whenever the user attempts to edit an item. Since the base class implements the basic mechanism to do this, we don't need to provide more than a simple reimplementation of this method.

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.

- The indexAt() method is used to relate a position in the view to a model index. Since views manage their own item placement, this method must be implemented.
- visualRect() and visualRegionForSelection() relate model indexes to rectangles and regions specified in the viewport's coordinate system, so that visible items can be displayed.
- The isIndexHidden() method is used in certain types of view to support hidden items. We need to provide a default implementation even if he do not provide this feature.
- `itemRect()` and `itemRegion()` relate model indexes to rectangles and regions specified in the view's coordinate system. These are private helper methods that make implementating other methods easier.

Three methods are needed to control which part of the view is shown in the viewport:

- horizontalOffset() and verticalOffset() are used to indicate the horizontal and vertical displacements of the area in the view that is shown in the viewport. These can usually be related to values provided by the view's scroll bars.
- The scrollTo() method is used to navigate to a particular item in the view by scrolling the viewport. Although useful for many views, we only provide a minimal implementation of this method.
- The updateGeometries() method is called whenever the user scrolls the viewport.

The view also needs to handle keyboard input to allow navigation between items, and provide support for selections.

- The moveCursor() method is called when the user presses a key corresponding to a standard action. It is up to the view to provide behavior that is appropriate for its visual representation of model data.
- setSelection() must be implemented to relate a rectangle in view coordinates to a range of model indexes, and adjust the underlying selection model according to the specified selection mode. The `itemRect()` and `itemRegion()` methods can be used to help implement this method.

A selection of event handlers are reimplemented to provide support for item selection and painting:

- The mousePressEvent(), mouseMoveEvent(), and mouseReleaseEvent() methods handle item selection, but can also be used to provide support for drag and drop.
- We render the visible part of the view in the paintEvent() method.
- The resizeEvent() handler method is called whenever the view changes size. This method is reimplemented to ensure that the region shown in the viewport corresponds to the scroll bar positions.

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.