引言
本身Qt有下拉菜单的实现QComboBox,但其中有相当多的限制,简单界面使用没有问题,但复杂的界面就经常达不到设计效果。
第一个问题就是QComboBox内部有单独的View和Model,在setView时会将原有View的Model剥离替换为内部Model,导致需要分别设置。在本人想法中View和Model本身就是一体的,而下拉菜单只是View一个容器,View被安装在其中,不应该改变原有的东西。
第二个问题在于showPopup,当下拉菜单被点击后就会关闭,这适用于单选操作,但多余多选情况并不有效。在前文《QTreeView复选框的实现》中,如果将自定义的View放在QComboBox就会遇到上述困扰,组合框的实现本身并不难,因此索性自定义一个。
ComboBox本身就是编辑框、下拉按钮以及选项框的组合,交互逻辑大概如下:
- 正常状态是编辑框以及下拉按钮
- 点击下拉按钮后弹出选项框
- 点击选项框中子项后,编辑框根据选中状态变化
- 展开选项框子项后,选项框显示高度可自动拉伸,但不能超过最大展示数量
- 点击选项框外任意范围,选项框恢复收起状态
效果如下:
代码实现
代码中以CustomTreeView为例子,但对于View并没有特殊要求,将其替换为任意View都是可行的,实现思路和方法不会发生变化。
选项框的高度调整adjustHeight函数参考了QComboBox::showPopup源码,大概思路是从根节点开始便利子项,visualRect(…)若存在高度,则证明为非隐藏节点,将其纳入listHeight中,当个数超过maxVisibleItems()时则跳出循环结束遍历。
关于上述交互逻辑的第四点"点击选项框外任意范围,选项框恢复收起状态"的实现无需过分关注,将popupWidget的窗口标志设置为Qt::Popup即可。
样式表:
QAbstractItemView#general_popup_list {
border-radius: 4px;
padding-top: 10px;
padding-bottom: 10px;
background: rgb(50,50,53);
alternate-background-color: rgb(50,50,53);
border: 2px solid rgb(11,11,12);
/*阴影*/
margin-bottom: 3px;
}
QAbstractItemView#general_popup_list::item {
height: 34px;
padding-left: 10px;
background-color: transparent;
border: 2px solid transparent;
}
QAbstractItemView#general_popup_list::item:selected{
border: 2px solid transparent;
background-color: rgb(11,11,12);
}
QAbstractItemView#general_popup_list::item:hover {
color: rgb(255,255,255);
border: 2px solid rgb(89,89,89);
background-color: transparent;
}
QAbstractItemView#general_popup_list::item:selected:hover {
color: rgb(255,255,255);
border: 2px solid rgb(89,89,89);
background-color: rgb(11,11,12);
}
头文件:
class CustomComboBox : public QWidget
{
Q_OBJECT
public:
CustomComboBox(QWidget *parent = nullptr);
virtual ~CustomComboBox();
CustomTreeView* view();
int maxVisibleItems();
void setMaxVisibleItems(int maxItems);
private:
void adjustHeight();// 参考QComboBox::showPopup源码实现
void adjustPostion();
private slots:
void slot_showPopup();
void slot_refreshLineEdit();
void slot_expanded();
private:
QLineEdit* m_lineEdit;
QPushButton* m_pushButton;
CustomTreeView* m_view;
QFrame* m_popupWidget;
int m_maxVisibleItems;
};
具体实现:
#include <QApplication>
#include <QDesktopWidget>
#include <QStack>
#include <QScrollBar>
CustomComboBox::CustomComboBox(QWidget *parent)
: QWidget(parent)
, m_maxVisibleItems(8)
{
m_lineEdit = new QLineEdit(this);
m_pushButton = new QPushButton("v", this);
connect(m_pushButton, &QPushButton::clicked, this, &CustomComboBox::slot_showPopup);
m_view = new CustomTreeView(this);
m_view->setObjectName("general_popup_list");
m_view->setHeaderHidden(true);
m_view->setCheckable();
connect(m_view, &QTreeView::clicked, this, &CustomComboBox::slot_refreshLineEdit);
connect(m_view, &QTreeView::expanded, this, &CustomComboBox::slot_expanded);
//connect(m_view, &QTreeView::collapsed, this, &CustomComboBox::slot_expanded);
m_popupWidget = new QFrame(this);
m_popupWidget->setWindowFlags(Qt::Popup | Qt::FramelessWindowHint);
m_popupWidget->setAttribute(Qt::WA_TranslucentBackground);
m_popupWidget->hide();
// 布局
auto popupLayout = new QHBoxLayout(m_popupWidget);
popupLayout->setMargin(0);
popupLayout->addWidget(m_view);
auto mainLayout = new QHBoxLayout(this);
mainLayout->setMargin(0);
mainLayout->setSpacing(0);
mainLayout->addWidget(m_lineEdit);
mainLayout->addWidget(m_pushButton);
}
CustomComboBox::~CustomComboBox()
{
}
CustomTreeView *CustomComboBox::view()
{
return m_view;
}
int CustomComboBox::maxVisibleItems()
{
return m_maxVisibleItems;
}
void CustomComboBox::setMaxVisibleItems(int maxItems)
{
m_maxVisibleItems = maxItems;
}
void CustomComboBox::adjustHeight()
{
auto tmpModel = view()->model();
int listHeight = 0;
int count = 0;
QStack<QModelIndex> toCheck;
toCheck.push(view()->rootIndex());
QTreeView *treeView = qobject_cast<QTreeView*>(view());
if (treeView && treeView->header() && !treeView->header()->isHidden())
listHeight += treeView->header()->height();
while (!toCheck.isEmpty()) {
QModelIndex parent = toCheck.pop();
for (int i = 0, end = tmpModel->rowCount(parent); i < end; ++i) {
QModelIndex idx = tmpModel->index(i, 0, parent);
if (!idx.isValid())
continue;
int tmpHeight = view()->visualRect(idx).height();
if (tmpHeight) {
listHeight += tmpHeight;
++count;
if (count >= maxVisibleItems()) {
toCheck.clear();
break;
}
}
if (tmpModel->hasChildren(idx) && treeView && treeView->isExpanded(idx))
toCheck.push(idx);
}
}
int spaceing = 28;// 与样式表有关
if(m_popupWidget->isVisible() && m_popupWidget->height() > spaceing + listHeight){
return;
}
m_popupWidget->setFixedHeight(spaceing + listHeight);
}
void CustomComboBox::adjustPostion()
{
QPoint pos = mapToGlobal(QPoint(0, 0));
pos.setY(pos.y() + height() + 5);
//屏幕越界判断
QRect screenRect = QApplication::desktop()->screenGeometry();
if (pos.y() + m_popupWidget->height() > screenRect.height())
pos.setY(mapToGlobal(QPoint(0, 0)).y() - m_popupWidget->height() - 5);
m_popupWidget->move(pos);
}
void CustomComboBox::slot_showPopup()
{
m_popupWidget->setFixedWidth(width());
if (m_popupWidget->isHidden()) {
adjustHeight();
adjustPostion();
m_popupWidget->show();
}
else {
m_popupWidget->hide();
}
}
void CustomComboBox::slot_refreshLineEdit()
{
QStringList strList;
for (QModelIndex tmpIndex : m_view->currentIndexes()) {
strList << tmpIndex.data(Qt::DisplayRole).toString();
}
m_lineEdit->setText(strList.join(","));
m_lineEdit->setToolTip(strList.join("\n"));
}
void CustomComboBox::slot_expanded()
{
adjustHeight();
adjustPostion();
}