前面我们有说过一篇,Qt自定义表格的文章,主要集中体现在自定义表头上,其实在Qt的很多界面开发上,自定义表格是很常见的一种操作。今天我们就看看怎么样自定义一个表格。
首先我们看下效果吧,没有找到复选框的图片,所以复选框的样式还差点意思。
同样的,我们沿用上一个自定义的方式,使用两个影藏了表头的QTableWidget
进行组合表格。而为了能够实现表头复选框的选中(半选中)和未选中三种状态的切换,并且能够缓存每行表格的数据,这次我们使用了自定义的复选框。
如下,我们自定义CheckBox
,继承自QCheckBox
,并且重写他的mouseReleaseEvent
函数。重写该函数的目的是因为button
的click
事件是在该函数中调用的,这样我们就可以重写他的click
操作,达到我们想要的目的。
另外我们定义了一个QVariant
类型的成员变量,用来存储一些比较简单的数据。当然,如果存储的数据有多个,推荐选择QVariantMap
。
class CheckBox : public QCheckBox
{
Q_OBJECT
public:
using QCheckBox::QCheckBox;
QVariant data() const;
void setData(const QVariant& data);
protected:
void mouseReleaseEvent(QMouseEvent* e);
private:
void btnClick();
QVariant m_data{ QVariant() };
};
函数的实现是很简单的,如下所示:
void CheckBox::mouseReleaseEvent(QMouseEvent *e)
{
btnClick();
}
QVariant CheckBox::data() const
{
return m_data;
}
void CheckBox::setData(const QVariant& data)
{
m_data = data;
}
void CheckBox::btnClick()
{
if (checkState() == Qt::Unchecked)
{
setCheckState(Qt::PartiallyChecked);
}
click();
}
在mouseReleaseEvent
函数中调用了btnClick
函数,而btnClick
的目的是为了控制按钮状态的点击,因为按钮在Qt::Unchecked
状态下,点击则会进入Qt::PartiallyChecked
状态,但是了一半情况下,我们在点击按钮的时候是不想要这种状态的,只希望有UnChecked
和Checked
状态切换,所以实现该函数的目的也就是实现上面的功能。
完成了上面的准备工作,下面我们就需要具体看一看实现的控件的定义。
class TableWidget : public QWidget
{
Q_OBJECT
public:
explicit TableWidget(QWidget *parent = 0);
~TableWidget();
void initTableHeader(const QStringList& header);
void initTableColumnWidth(const QList<int>& width);
void initTableData(const QList<QVariantList>& data);
void data(QList<QVariantList>& data, bool header = true, bool index = false);
void setTableCellData(int r, int c, const QVariant& value);
void setTableColumnData(int c, const QVariant& value);
private:
void initPage();
QWidget* newBtnCheckBox(const QString& strText);
private slots:
void on_btnCheckClicked();
signals:
void signal_rowData(const QVariantList& list);
private:
Ui::TableWidget *ui;
};
如上,该类我们对外提供了6个接口。
- 初始化表头,初始化表头的时候,表体有部分内容也是需要初始化的
- 设置每列的宽度
- 初始化表体数据,显示数据
- 获取表体数据,是否包含表头和序号
- 设置某个单元格的数据
- 设置表体某一列的数据
同时为了防止界面显示不完全,表格单元格数据太长,导致显示不完全,因此提供了提示功能。
同时当选中其中某行时,可将该行数据通过信号槽的形式发送出去。
接下来我们看下其中每个函数的具体实现。
首先在构造的时候需要设置一些参数。同时有两个信号槽的连接,提示功能和发送选中行的数据。
void TableWidget::initPage()
{
setWindowFlags(Qt::FramelessWindowHint | Qt::SubWindow);
ui->tbBody->setMouseTracking(true);
connect(ui->tbBody, &QTableWidget::cellPressed, this, [this](int r, int c) void
{
auto item = ui->tbBody->item(r, c);
if (item == Q_NULLPTR)
{
return;
}
QToolTip::showText(QCursor::pos(), item->text());
});
connect(ui->tbBody, &QTableWidget::clicked, this, [this] {
QList<QTableWidgetItem*> items = ui->tbBody->selectedItems();
if (items.isEmpty())
{
return;
}
int row = ui->tbBody->row(items.at(0));
QVariantList body;
body.append(QString::number(row + 1));
for (int index= 1, column = ui->tbHeader->columnCount(); index< column; ++index)
{
body.append(ui->tbBody->item(row , index)->data(Qt::UserRole));
}
emit signal_rowData(body);
});
}
接下来是表头的设置,这块地方的设置跟以前讲的那篇文章是一样的。
void TableWidget::initTableHeader(const QStringList& header)
{
ui->tbHeader->setColumnCount(header.size() + 1);
ui->tbBody->setColumnCount(header.size() + 1);
auto pBtnAll = new CheckBox("All");
pBtnAll->setObjectName(QString("btnCheck"));
pBtnAll->setFixedSize(72, 36);
pBtnAll->setProperty("buttontext", "field");
auto pWidget = new QWidget;
auto layout = new QHBoxLayout;
layout->setSpacing(0);
layout->setMargin(0);
layout->addWidget(pBtnAll);
pWidget->setLayout(layout);
ui->tbHeader->setCellWidget(0, 0, pWidget);
for (int nIndex = 1; nIndex <= header.size(); ++nIndex)
{
ui->tbHeader->setItem(0, nIndex, new QTableWidgetItem(header.at(nIndex - 1)));
ui->tbHeader->item(0, nIndex)->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
}
}
上面的代码中我们给表头的第一个单元格中设置了一个QWidget
类型的窗口,设置这个的主要目的是为了后面在调用的时候,能够通过QWidget
类的成员方法 findChild
快速准确的找到子控件CheckBox
。
修改表格每列宽度的功能就更简单了。
void TableWidget::initTableColumnWidth(const QList<int>& width)
{
if (width.size() != ui->tbHeader->columnCount())
{
return;
}
for (int index = 0; index < width.size(); ++index)
{
ui->tbHeader->setColumnWidth(index, width[index]);
ui->tbBody->setColumnWidth(index, width[index]);
}
}
接下来是对表格进行数据的填充。步骤也是很简单的进行设置text,唯一需要注意的点是,如果数据量较大的话,如果在对每个单元格进行数据填充之后,都进行了UI的绘制,则可能会造成界面明显的卡顿,因此我们选择先进行数据填充,等数据填充完毕后再一次性刷新界面。而这一点的控制是由QTabelWidget
的属性UpdatesEnabled
控制的。
void TableWidget::initTableData(const QList<QVariantList>& data)
{
auto btn = static_cast<QWidget*>(ui->tbHeader->cellWidget(0, 0))->findChild<CheckBox*>();
if (btn != Q_NULLPTR)
{
btn->setChecked(false);
}
ui->tbBody->setRowCount(data.size());
ui->tbBody->setUpdatesEnabled(false);
for (int nIndex = 0, size = data.size(); nIndex < size; ++nIndex)
{
// 序号 从1 开始
QWidget* pWidgetItem = newBtnCheckBox(QString::number(nIndex + 1));
ui->tbBody->setCellWidget(nIndex, 0, pWidgetItem);
QVariantList list = data.at(nIndex);
for (int nRet = 0, s = list.size(); nRet < s; ++nRet)
{
ui->tbBody->setItem(nIndex, nRet + 1, new QTableWidgetItem(list.at(nRet).toString()));
ui->tbBody->item(nIndex, nRet + 1)->setData(Qt::UserRole, list.at(nRet));
ui->tbBody->item(nIndex, nRet + 1)->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
}
static_cast<QWidget*>(ui->tbBody->cellWidget(nIndex, 0))->findChild<CheckBox*>()->setData(list);
}
ui->tbBody->setUpdatesEnabled(true);
}
同样的下面是其他两个设置数据的接口。
void TableWidget::setTableCellData(int r, int c, const QVariant& value)
{
ui->tbBody->item(r, c)->setText(value.toString());
}
void TableWidget::setTableColumnData(int c, const QVariant& value)
{
for (int nIndex = 0, nRet = ui->tbBody->rowCount(); nIndex < nRet; ++nIndex)
{
ui->tbBody->item(nIndex, c)->setText(value.toString());
}
}
下面是对表格中数据的读取,我们可以看到,我们对表头,对序号都进行了判断,是否读取,并且从读取数据的时候是读取了我们设置表格数据时设置给每个单元格用以Qt::UserRole
为键存储的数据,如果表格是可编辑的,则该数据可能未被修改,因此,如果表格是可编辑的状态,则需要获取修改后的数值。
void TableWidget::data(QList<QVariantList>& data, bool header, bool index)
{
if (header) //是否需要表头
{
QVariantList head;
if (index)
{
head.append("index");
}
for (int nRet = 1, column = ui->tbHeader->columnCount(); nRet < column; ++nRet)
{
head.append(ui->tbHeader->item(0, nRet)->text());
}
data << head;
}
for (int nIndex = 0, size = ui->tbBody->rowCount(); nIndex < size; ++nIndex)
{
auto pWidget = static_cast<QWidget*>(ui->tbBody->cellWidget(nIndex, 0));
auto pBtn = pWidget->findChild<QCheckBox*>(QString("btnCheck"));
if (pBtn == Q_NULLPTR || !pBtn->isChecked())
{
continue;
}
QVariantList body;
if (index)
{
body.append(pBtn->text());
}
for (int nRet = 1, column = ui->tbHeader->columnCount(); nRet < column; ++nRet)
{
body.append(ui->tbBody->item(nIndex, nRet)->data(Qt::UserRole));
// body.append(ui->tbBody->item(nIndex, nRet)->text());
}
data.append(body);
}
}
下面的操作实现了每行index列的check_box的实现和其三种状态的切换。
这个在前面的文章中也有涉猎到,如果想细询,请参考Qt自定义控件实现一些不重要但很膈应人的功能。
而我们今天主要说的是表格和表头的复选框之间的牵扯,所以主要看下面的代码便可。
QWidget* TableWidget::newBtnCheckBox(const QString& strText)
{
auto pBtnCheck = new CheckBox(strText);
pBtnCheck->setObjectName(QString("btnCheck"));
pBtnCheck->setFixedSize(72, 36);
pBtnCheck->setProperty("buttontext", "field");
auto pWidget = new QWidget;
auto layout = new QHBoxLayout;
layout->setSpacing(0);
layout->setMargin(0);
layout->addWidget(pBtnCheck);
pWidget->setLayout(layout);
connect(pBtnCheck, &QCheckBox::clicked, this, &TableWidget::on_btnCheckClicked);
auto pWidgetAll = static_cast<QWidget*>(ui->tbHeader->cellWidget(0, 0));
auto pBtnAll = pWidgetAll->findChild<QCheckBox*>(QString("btnCheck"));
connect(pBtnAll, &QCheckBox::clicked, pBtnCheck, &QCheckBox::setChecked);
return pWidget;
}
void TableWidget::on_btnCheckClicked()
{
auto pWidget = static_cast<QWidget*>(ui->tbHeader->cellWidget(0, 0));
auto pBtnAll = pWidget->findChild<QCheckBox*>(QString("btnCheck"));
if (Q_NULLPTR == pBtnAll)
{
return;
}
auto pcurBtn = qobject_cast<QCheckBox*>(QObject::sender());
if (Q_NULLPTR == pcurBtn)
{
return;
}
pBtnAll->setCheckState(pcurBtn->checkState());
for (int nIndex = 0; nIndex < ui->tbBody->rowCount(); ++nIndex)
{
auto pWidget = static_cast<QWidget*>(ui->tbBody->cellWidget(nIndex, 0));
auto pBtn = pWidget->findChild<QCheckBox*>(QString("btnCheck"));
if (pBtn == Q_NULLPTR)
{
continue;
}
if (pBtn->checkState() != pBtnAll->checkState())
{
pBtnAll->setCheckState(Qt::PartiallyChecked);
break;
}
}
}
至此,功能也就全部实现了。 测试代码。