Chart Example
图表示例展示了如何为模型/视图框架创建自定义视图。
在本例中,表模型中的项表示为饼图中的切片,依赖于模型/视图架构的灵活性来处理自定义编辑和选择特征。
请注意,只有在数据需要专门化表示的情况下,才需要创建新的视图类。如果你需要用一种特殊的方式表示数据,你应该首先考虑使用标准的QListView, QTableView,或者QTreeView和自定义的QItemDelegate子类。
main.cpp
#include "MainWindow.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
MainWindow类
MainWindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
QT_BEGIN_NAMESPACE
class QAbstractItemModel;
class QAbstractItemView;
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
private slots:
void openFile();
void saveFile();
private:
void setupModel();
void setupViews();
void loadFile(const QString &path);
QAbstractItemModel *model = nullptr;
QAbstractItemView *pieChart = nullptr;
};
#endif // MAINWINDOW_H
MainWindow.cpp
#include "PieView.h"
#include "MainWindow.h"
#include <QtWidgets>
// 构造函数
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
// 文件菜单
QMenu *fileMenu = new QMenu(tr("&File"), this);
QAction *openAction = fileMenu->addAction(tr("&Open..."));// 打开项
openAction->setShortcuts(QKeySequence::Open); // 打开动作快捷键
QAction *saveAction = fileMenu->addAction(tr("&Save As...")); // 保存项
saveAction->setShortcuts(QKeySequence::SaveAs); // 保存项快捷键
QAction *quitAction = fileMenu->addAction(tr("E&xit")); // 退出项
quitAction->setShortcuts(QKeySequence::Quit); // 退出项快捷键
setupModel(); // 设置模型
setupViews(); // 设置视图
connect(openAction, &QAction::triggered, this, &MainWindow::openFile);
connect(saveAction, &QAction::triggered, this, &MainWindow::saveFile);
connect(quitAction, &QAction::triggered, qApp, &QCoreApplication::quit);
menuBar()->addMenu(fileMenu);
statusBar();
loadFile(":/Charts/qtdata.cht");
setWindowTitle(tr("Chart"));
resize(870, 550);
}
// 设置模型
void MainWindow::setupModel()
{
// 标准项目模型
model = new QStandardItemModel(8, 2, this);
model->setHeaderData(0, Qt::Horizontal, tr("Label")); //设置头数据0列
model->setHeaderData(1, Qt::Horizontal, tr("Quantity")); //设置头数据1列
}
// 设置界面
void MainWindow::setupViews()
{
// 分裂期作为中央控件
QSplitter *splitter = new QSplitter;
QTableView *table = new QTableView; // 表视图
pieChart = new PieView; // 自定义视图
splitter->addWidget(table); // 左边是表格
splitter->addWidget(pieChart); // 右边是饼视图
splitter->setStretchFactor(0, 0); // 0列不拉伸
splitter->setStretchFactor(1, 1); // 1列拉伸
table->setModel(model); // 两个视图设置同一个数据模型
pieChart->setModel(model);
QItemSelectionModel *selectionModel = new QItemSelectionModel(model); // 选择模型
table->setSelectionModel(selectionModel); // 两个视图设置同一个选择模型
pieChart->setSelectionModel(selectionModel);
QHeaderView *headerView = table->horizontalHeader(); // 表视图最后一列拉伸
headerView->setStretchLastSection(true);
setCentralWidget(splitter);
}
// 打开文件
void MainWindow::openFile()
{
const QString fileName =
QFileDialog::getOpenFileName(this, tr("Choose a data file"), "", "*.cht");
if (!fileName.isEmpty())
loadFile(fileName);
}
// 加载文件
void MainWindow::loadFile(const QString &fileName)
{
QFile file(fileName);
if (!file.open(QFile::ReadOnly | QFile::Text))
return;
QTextStream stream(&file);
// 清空模型
model->removeRows(0, model->rowCount(QModelIndex()), QModelIndex());
int row = 0;
while (!stream.atEnd()) {
const QString line = stream.readLine(); // 读取文件中一行字符串
if (!line.isEmpty()) {
model->insertRows(row, 1, QModelIndex()); // 模型添加一列
const QStringList pieces = line.split(QLatin1Char(','), Qt::SkipEmptyParts); // 分割字符串
if (pieces.size() < 3)
continue;
// 模型设置项数据
model->setData(model->index(row, 0, QModelIndex()),pieces.value(0));//默认Qt::EditRole
model->setData(model->index(row, 1, QModelIndex()),pieces.value(1));//默认Qt::EditRole
model->setData(model->index(row, 0, QModelIndex()),
QColor(pieces.value(2)), Qt::DecorationRole);
row++;
}
};
file.close();
statusBar()->showMessage(tr("Loaded %1").arg(fileName), 2000); //状态栏显示信息
}
// 保存文件
void MainWindow::saveFile()
{
QString fileName = QFileDialog::getSaveFileName(this,
tr("Save file as"), "", "*.cht");
if (fileName.isEmpty())
return;
QFile file(fileName);
if (!file.open(QFile::WriteOnly | QFile::Text))
return;
QTextStream stream(&file);
for (int row = 0; row < model->rowCount(QModelIndex()); ++row) {
QStringList pieces;
pieces.append(model->data(model->index(row, 0, QModelIndex()),
Qt::DisplayRole).toString());
pieces.append(model->data(model->index(row, 1, QModelIndex()),
Qt::DisplayRole).toString());
pieces.append(model->data(model->index(row, 0, QModelIndex()),
Qt::DecorationRole).toString());
stream << pieces.join(',') << "\n";
}
file.close();
statusBar()->showMessage(tr("Saved %1").arg(fileName), 2000);
}
PieView类
PieView.h
自定义视图比较复杂,很多函数都不知道为啥要使用。注释了一点
#ifndef PIEVIEW_H
#define PIEVIEW_H
#include <QAbstractItemView>
class PieView : public QAbstractItemView
{
public:
PieView(QWidget *parent = nullptr);
QRect visualRect(const QModelIndex &index) const override;
void scrollTo(const QModelIndex &index, ScrollHint hint = EnsureVisible) override;
QModelIndex indexAt(const QPoint &point) const override;
protected slots:
void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles = QVector<int>()) override;
void rowsInserted(const QModelIndex &parent, int start, int end) override;
void rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end) override;
protected:
bool edit(const QModelIndex &index, EditTrigger trigger, QEvent *event) override;
QModelIndex moveCursor(CursorAction cursorAction, Qt::KeyboardModifiers modifiers) override;
int horizontalOffset() const override;
int verticalOffset() const override;
bool isIndexHidden(const QModelIndex &index) const override;
void setSelection(const QRect &rect, QItemSelectionModel::SelectionFlags command) override;
void mousePressEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
void paintEvent(QPaintEvent *event) override;
void resizeEvent(QResizeEvent *event) override;
void scrollContentsBy(int dx,int dy) override;
QRegion visualRegionForSelection(const QItemSelection &selection) const override;
private:
QRect itemRect(const QModelIndex &index) const;
QRegion itemRegion(const QModelIndex &index) const;
int rows(const QModelIndex &index = QModelIndex()) const;
void updateGeometries() override;
int margin = 0;
int totalSize = 300;
int pieSize = totalSize - 2 * margin;
int validItems = 0;
double totalValue = 0.0;
QRubberBand *rubberBand = nullptr;
QPoint origin;
};
#endif // PIEVIEW_H
PieView.cpp
#include "PieView.h"
#include <QtWidgets>
// 构造函数
PieView::PieView(QWidget *parent)
: QAbstractItemView(parent)
{
horizontalScrollBar()->setRange(0,0);
verticalScrollBar()->setRange(0,0);
}
// 滚动视图保证索引执行的项可见
void PieView::scrollTo(const QModelIndex &index, QAbstractItemView::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() + qMin(
// rect.right() - area.right(), rect.left() - area.left()));
// }
horizontalScrollBar()->setValue(
horizontalScrollBar()->value() + 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() + qMin(
// rect.bottom() - area.bottom(), rect.top() - area.top()));
// }
verticalScrollBar()->setValue(
verticalScrollBar()->value() + rect.top() - area.top());
update();
}
// 获取point所在的项目索引
QModelIndex PieView::indexAt(const QPoint &point) const
{
if(validItems == 0)
return QModelIndex();
// 将视图坐标转化为widget的内容坐标
int wx = point.x() + horizontalScrollBar()->value();
int wy = point.y() + verticalScrollBar()->value();
if(wx < totalSize ) {
double cx = wx - totalSize / 2;
double cy = totalSize / 2 - wy;
double d = std::sqrt(std::pow(cx,2) + std::pow(cy,2));
if(d == 0 || d > pieSize / 2)
return QModelIndex();
double angle = qRadiansToDegrees(std::atan2(cy,cx));
if(angle < 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 = model()->data(index).toDouble();
if(value > 0.0) {
double sliceAngle = 360 * value / totalValue;
if(angle >= startAngle && angle < (startAngle + sliceAngle))
return model()->index(row,1,rootIndex());
startAngle += sliceAngle;
}
}
} else {
double itemHeight = QFontMetrics(viewOptions().font).height();
int listItem = (int)((wy - margin) / itemHeight);
int validRow = 0;
for (int row = 0; row < model()->rowCount(rootIndex()); ++row) {
QModelIndex index = model()->index(row,1,rootIndex());
if(model()->data(index).toDouble() > 0.0){
if(listItem==validRow)
return model()->index(row,0,rootIndex());
++validRow;
}
}
}
return QModelIndex();
}
// 项数据变动
void PieView::dataChanged(const QModelIndex &topLeft,
const QModelIndex &bottomRight, const QVector<int> &roles)
{
QAbstractItemView::dataChanged(topLeft,bottomRight,roles);
if(! roles.contains(Qt::DisplayRole))
return;
validItems = 0;
totalValue = 0.0;
for (int row = 0; row < model()->rowCount(rootIndex()); ++row) {
QModelIndex index = model()->index(row,1,rootIndex());
double value = model()->data(index,Qt::DisplayRole).toDouble();
if(value > 0.0){
totalValue += value;
validItems++;
}
}
viewport()->update();
}
// 视图插入新行
void PieView::rowsInserted(const QModelIndex &parent, int start, int end)
{
for (int row = start; row <= end; ++row) {
QModelIndex index = model()->index(row, 1, rootIndex());
double value = model()->data(index).toDouble();
if (value > 0.0) {
totalValue += value;
++validItems;
}
}
QAbstractItemView::rowsInserted(parent, start, end);
}
// 视图将要删除行时
void PieView::rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end)
{
for (int row = start; row <= end; ++row) {
QModelIndex index = model()->index(row, 1, rootIndex());
double value = model()->data(index).toDouble();
if (value > 0.0) {
totalValue -= value;
--validItems;
}
}
QAbstractItemView::rowsAboutToBeRemoved(parent, start, end);
}
// 视图编辑
bool PieView::edit(const QModelIndex &index, QAbstractItemView::EditTrigger trigger, QEvent *event)
{
if(index.column() == 0)
return QAbstractItemView::edit(index,trigger,event);
else
return false;
}
// 鼠标移动返回下一个模型索引,纯虚
QModelIndex PieView::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;
}
// 水平偏移
int PieView::horizontalOffset() const
{
return horizontalScrollBar()->value();
}
// 索引项是否隐藏
bool PieView::isIndexHidden(const QModelIndex &/*index*/) const
{
return false;
}
// 鼠标按下,创建一个橡皮筋
void PieView::mousePressEvent(QMouseEvent *event)
{
QAbstractItemView::mousePressEvent(event);
origin = event->pos();
if (!rubberBand)
rubberBand = new QRubberBand(QRubberBand::Rectangle, viewport());
rubberBand->setGeometry(QRect(origin, QSize()));
rubberBand->show();
}
// 鼠标按下移动,更新橡皮筋
void PieView::mouseMoveEvent(QMouseEvent *event)
{
if (rubberBand)
rubberBand->setGeometry(QRect(origin, event->pos()).normalized());
QAbstractItemView::mouseMoveEvent(event);
}
// 鼠标按下移动,隐藏橡皮筋
void PieView::mouseReleaseEvent(QMouseEvent *event)
{
QAbstractItemView::mouseReleaseEvent(event);
if (rubberBand)
rubberBand->hide();
viewport()->update();
}
// 重绘事件
void PieView::paintEvent(QPaintEvent *event)
{
QItemSelectionModel *selections = selectionModel();
QStyleOptionViewItem option = viewOptions();
QBrush background = option.palette.base();
QPen foreground(option.palette.color(QPalette::WindowText));
QPainter painter(viewport());
painter.setRenderHint(QPainter::Antialiasing);
painter.fillRect(event->rect(), background);
painter.setPen(foreground);
// Viewport rectangles
QRect pieRect = QRect(margin, margin, pieSize, pieSize);
if (validItems <= 0)
return;
painter.save();
painter.translate(pieRect.x() - horizontalScrollBar()->value(),
pieRect.y() - verticalScrollBar()->value());
painter.drawEllipse(0, 0, pieSize, pieSize);
double startAngle = 0.0;
int row;
for (row = 0; row < model()->rowCount(rootIndex()); ++row) {
QModelIndex index = model()->index(row, 1, rootIndex());
double value = model()->data(index).toDouble();
if (value > 0.0) {
double angle = 360 * value / totalValue;
QModelIndex colorIndex = model()->index(row, 0, rootIndex());
QColor color = QColor(model()->data(colorIndex, Qt::DecorationRole).toString());
if (currentIndex() == index)
painter.setBrush(QBrush(color, Qt::Dense4Pattern));
else if (selections->isSelected(index))
painter.setBrush(QBrush(color, Qt::Dense3Pattern));
else
painter.setBrush(QBrush(color));
painter.drawPie(0, 0, pieSize, pieSize, int(startAngle*16), int(angle*16));
startAngle += angle;
}
}
painter.restore();
int keyNumber = 0;
for (row = 0; row < model()->rowCount(rootIndex()); ++row) {
QModelIndex index = model()->index(row, 1, rootIndex());
double value = model()->data(index).toDouble();
if (value > 0.0) {
QModelIndex labelIndex = model()->index(row, 0, rootIndex());
QStyleOptionViewItem option = viewOptions();
option.rect = visualRect(labelIndex);
if (selections->isSelected(labelIndex))
option.state |= QStyle::State_Selected;
if (currentIndex() == labelIndex)
option.state |= QStyle::State_HasFocus;
itemDelegate()->paint(&painter, option, labelIndex);
++keyNumber;
}
}
}
void PieView::resizeEvent(QResizeEvent */*event*/)
{
updateGeometries();
}
void PieView::scrollContentsBy(int dx, int dy)
{
viewport()->scroll(dx,dy);
}
QRect PieView::itemRect(const QModelIndex &index) const
{
if (!index.isValid())
return QRect();
// Check whether the index's row is in the list of rows represented
// by slices.
QModelIndex valueIndex;
if (index.column() != 1)
valueIndex = model()->index(index.row(), 1, rootIndex());
else
valueIndex = index;
if (model()->data(valueIndex).toDouble() <= 0.0)
return QRect();
int listItem = 0;
for (int row = index.row()-1; row >= 0; --row) {
if (model()->data(model()->index(row, 1, rootIndex())).toDouble() > 0.0)
listItem++;
}
switch (index.column()) {
case 0: {
const qreal itemHeight = QFontMetricsF(viewOptions().font).height();
return QRect(totalSize,
qRound(margin + listItem * itemHeight),
totalSize - margin, qRound(itemHeight));
}
case 1:
return viewport()->rect();
}
return QRect();
}
QRegion PieView::itemRegion(const QModelIndex &index) const
{
if (!index.isValid())
return QRegion();
if (index.column() != 1)
return itemRect(index);
if (model()->data(index).toDouble() <= 0.0)
return QRegion();
double startAngle = 0.0;
for (int row = 0; row < model()->rowCount(rootIndex()); ++row) {
QModelIndex sliceIndex = model()->index(row, 1, rootIndex());
double value = model()->data(sliceIndex).toDouble();
if (value > 0.0) {
double angle = 360 * value / totalValue;
if (sliceIndex == index) {
QPainterPath slicePath;
slicePath.moveTo(totalSize / 2, totalSize / 2);
slicePath.arcTo(margin, margin, margin + pieSize, margin + pieSize,
startAngle, angle);
slicePath.closeSubpath();
return QRegion(slicePath.toFillPolygon().toPolygon());
}
startAngle += angle;
}
}
return QRegion();
}
int PieView::rows(const QModelIndex &index) const
{
return model()->rowCount(model()->parent(index));
}
void PieView::setSelection(const QRect &rect, QItemSelectionModel::SelectionFlags command)
{
// Use content widget coordinates because we will use the itemRegion()
// function to check for intersections.
QRect contentsRect = rect.translated(
horizontalScrollBar()->value(),
verticalScrollBar()->value()).normalized();
int rows = model()->rowCount(rootIndex());
int columns = model()->columnCount(rootIndex());
QModelIndexList indexes;
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.intersects(contentsRect))
indexes.append(index);
}
}
if (indexes.size() > 0) {
int firstRow = indexes.at(0).row();
int lastRow = firstRow;
int firstColumn = indexes.at(0).column();
int lastColumn = firstColumn;
for (int i = 1; i < indexes.size(); ++i) {
firstRow = qMin(firstRow, indexes.at(i).row());
lastRow = qMax(lastRow, indexes.at(i).row());
firstColumn = qMin(firstColumn, indexes.at(i).column());
lastColumn = qMax(lastColumn, indexes.at(i).column());
}
QItemSelection selection(
model()->index(firstRow, firstColumn, rootIndex()),
model()->index(lastRow, lastColumn, rootIndex()));
selectionModel()->select(selection, command);
} else {
QModelIndex noIndex;
QItemSelection selection(noIndex, noIndex);
selectionModel()->select(selection, command);
}
update();
}
void PieView::updateGeometries()
{
horizontalScrollBar()->setPageStep(viewport()->width());
horizontalScrollBar()->setRange(0, qMax(0, 2 * totalSize - viewport()->width()));
verticalScrollBar()->setPageStep(viewport()->height());
verticalScrollBar()->setRange(0, qMax(0, totalSize - viewport()->height()));
}
int PieView::verticalOffset() const
{
return verticalScrollBar()->value();
}
/*
Returns the position of the item in viewport coordinates.
*/
QRect PieView::visualRect(const QModelIndex &index) const
{
QRect rect = itemRect(index);
if (!rect.isValid())
return rect;
return QRect(rect.left() - horizontalScrollBar()->value(),
rect.top() - verticalScrollBar()->value(),
rect.width(), rect.height());
}
/*
Returns a region corresponding to the selection in viewport coordinates.
以视口坐标返回与所选内容对应的区域。
*/
QRegion PieView::visualRegionForSelection(const QItemSelection &selection) const
{
int ranges = selection.count();
if (ranges == 0)
return QRect();
QRegion region;
for (int i = 0; i < ranges; ++i) {
const 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 += visualRect(index);
}
}
}
return region;
}