QListView 使用Delegate定制

  • 目标:

  1. 使用Model/View实现列表内容加载;
  2. 使用Delegate实现Item的设计;
  3. Item可以包含多个不同类型的控件;
  4. Item可处理多控件的鼠标hover,press事件;
  5. Item可支持输入控件;
  6. 实现列表内容随滚动条动态加载;
  7. 实现ListMode和IconMode动态切换;
  • 效果演示 

  •  Model/View原理

         Model:列表的数据集合,每一条Item是一个数据表,默认的key值是Qt::DisplayRole,用户可扩展以Qt::UserRole起始的自定义字段;数据提供者;
        Delegate:Item的样式实现,使用Model中的数据来创建Item的UI;数据使用者;
        View:List UI的载体,带有默认的Model和Delegate,也可设置自定义的Model及Delegate来实现自定义的ListView;

  •  QListView设置Model和Delegate

void ProjectInfoListView::init(QListView::ViewMode viewMode)
{
    //layout;
    auto layout = new QHBoxLayout();
    layout->setContentsMargins(0, 0, 0, 0);
    this->setLayout(layout);

    //listview;
    m_listView = new QListView(this);
    layout->addWidget(m_listView);

    //model
    m_model = new ProjectInfoModel(this);
    m_listView->setModel(m_model);

    //list delegate;
    m_delegate = new ProjectInfoDelegate(this);
    m_listView->setItemDelegate(m_delegate);
}

        分别创建QListView,ProjectInfoModel和ProjectInfoDelegate的对象,然后把Model和Delegate对象设置给ListView。 

  • ListModel和IconMode动态切换 

void ProjectInfoListView::setViewMode(QListView::ViewMode viewMode)
{
    m_viewMode = viewMode;
    if(viewMode == QListView::IconMode)
    {
        m_listView->setItemDelegate(m_iconDelegate);
        m_listView->setViewMode(QListView::IconMode);
        m_listView->setProperty("isListMode", false);

        m_listView->setSelectionBehavior(QAbstractItemView::SelectItems);
        m_listView->setResizeMode(QListView::Adjust); //ListView Resize时是否根据新的宽高重新排列;
        m_listView->setWrapping(true); //一行多个Item,直到占满一行;
        m_listView->setSpacing(10);
        m_listView->verticalScrollBar()->setSingleStep(100);
    }
    else
    {
        m_listView->setItemDelegate(m_listDelegate);
        m_listView->setViewMode(QListView::ListMode);
        m_listView->setProperty("isListMode", true);

        m_listView->setSelectionBehavior(QAbstractItemView::SelectRows);
        m_listView->setWrapping(false); //一行多个Item,直到占满一行;
        m_listView->setSpacing(0);
        m_listView->verticalScrollBar()->setSingleStep(1);
    }
    m_listView->scrollToTop();
    style()->unpolish(m_listView);
    style()->polish(m_listView);
}

        创建m_listDelegate和m_iconDelegate两个不同类型的Delegate对象,分别代表list mode和icon mode。 

  • Model的设计 

    • item数据的读写
namespace fly
{
struct ProjectDetailInfo
{
    bool checked = false;
    QString name = "";
    QDateTime modifyTime;
    QString size = "";
    int  hoverPosX= 0;
    ProjectDetailInfoKey clickedRole = PROJECT_NAME;
    QString iconPath = ":/icon/default.png";
};
}

class ProjectInfoModel : public QAbstractListModel
{
    Q_OBJECT
public:
    explicit ProjectInfoModel(QObject* parent=nullptr);
    explicit ProjectInfoModel(const QList& itemList, QObject*parent=nullptr);
    virtual ~ProjectInfoModel();

    void appendList(const QList& itemList);
    void append(const fly::ProjectDetailInfo& item);
    void insert(int row, const fly::ProjectDetailInfo& item);
    void remove(int row);

private:
    QList  m_list;
};

        用户数据实际上存储在m_list中,我们自定义了ProjectDetailInfod数据结构,增加了append,insert,remove等接口来操作m_list,实现Model的更新。

        Item的数据读写需要通过固定的接口来实现,分别是data()和setData(),这两个函数会在Delegate被使用到,我们需要对其进行重写,使它们支持自定义的数据结构ProjectDetailInfo。

QVariant ProjectInfoModel::data(const QModelIndex &index, int role) const
{
    QVariant var;
    do
    {
        if(!index.isValid())
            break;

        if(index.row()>= m_list.size() || index.row()< 0)
            break;
            
        const auto& info = m_list.at(index.row());
        switch(role)
        {
        case fly::CHECKED_STATE:
            var = info.checked;
            break;

        case fly::PROJECT_NAME:
            var = info.name;
            break;

        case fly::MODIFY_TIME:
            var = info.modifyTime;
            break;

        case fly::PROJECT_SIZE:
            var = info.size;
            break;

        case fly::HOVER_POS_X:
            var = info.hoverPosX;
            break;

        case fly::ICON_PATH:
            var = info.iconPath;
            break;

        case fly::CLICKED_ROLE:
            var = info.clickedRole;
            break;

        case Qt::DisplayRole:
            var = info.name;
            break;
        }
    }while(0);

    return var;
}

         index代表一条item数据,role则代表一条item数据下的一个字段(一条item可以有多个字段)。

bool ProjectInfoModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
    int row = index.row();

    if(row>= m_list.size() || row< 0)
        return false;

    auto info = m_list.at(row);
    switch(role)
    {
    case fly::CHECKED_STATE:
        info.checked = value.toBool();
        break;

    case fly::PROJECT_NAME:
        info.name = value.toString();
        break;

    case fly::MODIFY_TIME:
        info.modifyTime = value.toDateTime();
        break;

    case fly::PROJECT_SIZE:
        info.size = value.toString();
        break;

    case fly::HOVER_POS_X:
        info.hoverPosX = value.toInt();
        break;

    case fly::ICON_PATH:
        info.iconPath = value.toString();
        break;

    case fly::CLICKED_ROLE:
        info.clickedRole = (fly::ProjectDetailInfoKey)value.toInt();
        break;

    case Qt::DisplayRole:
        info.name = value.toString();
        break;
    }

    m_list.replace(row, info);
    return true;
}

        对应于data()访问数据函数,setData()函数则是用于通过key值更新数据,同样传入index定位到item,再通过role找到对应的字段。  

  • item随滚动条滑动动态加载
int ProjectInfoModel::rowCount(const QModelIndex &parent) const
{
    (void)parent;
    return m_itemCount;
}

bool ProjectInfoModel::canFetchMore(const QModelIndex &parent) const
{
    (void)parent;
    if(m_itemCount< m_list.size())
        return true;
    else
        return false;
}

void ProjectInfoModel::fetchMore(const QModelIndex &parent)
{
    (void)parent;
    int remainder = m_list.size()- m_itemCount;
    int itemToFetch = qMin(m_onceFetchNum, remainder);

    if(itemToFetch< 0)
        return;

    beginInsertRows(QModelIndex(), m_itemCount, m_itemCount+ itemToFetch- 1);
    m_itemCount += itemToFetch;
    endInsertRows();
}

        通过重写基类的rowCount(),canFetchMore(),fetchMore()三个函数实现item的动态加载,这三个函数默认由ListView底层去调用,我们只需关注实现。

        m_itemCount记录的是ListView当前加载的item数量,ListView会根据这个值去计算滚动条的位置。

        当滚动条到达底部时,底层会调用canFetchMore()函数,若返回true,则下一步调用fetchMore(),fetchMore()里面实现了item的加载。

  • Delegate的设计

    • 绘制多个不同的控件
void ProjectInfoDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
    fly::ProjectDetailInfo info;
    info.init(index);

    QRect rect = option.rect;
    QRect checkedRect;
    QRect iconRect;
    QRect nameRect;
    QRect timeRect;
    QRect sizeRect;
    QRect moreRect;
    getRects(rect, checkedRect, iconRect, nameRect, timeRect, sizeRect, moreRect);

    //checked button;
    if(info.checked)
    {
        QPixmap pix(":/icon/checked.png");
        painter->drawPixmap(checkedRect, pix);
    }
    else
    {
        QPixmap pix(":/icon/unchecked.png");
        painter->drawPixmap(checkedRect, pix);
    }

    //icon
    QPixmap iconPix(info.iconPath);
    painter->drawPixmap(iconRect, iconPix);

    {
        QFont font;
        font.setFamily("Microsoft YaHei");
        font.setPixelSize(18);
        painter->setFont(font);

        painter->drawText(nameRect, Qt::AlignLeft, info.name);
        painter->drawText(timeRect, Qt::AlignLeft, info.modifyTime.toString());
        painter->drawText(sizeRect, Qt::AlignLeft, info.size);
    }

    //more button;
    QPixmap morePix(":/icon/more.png");
    painter->drawPixmap(moreRect, morePix);
}

        ProjectInfoDelegate继承于QStyledItemDelegate,重写它的paint()函数,通过传入的index所带的数据来绘制item。所有的item都会通过paint()函数来创建,所以过程是统一的,差异在于每条index带的数据会不一样。 

  • 鼠标press事件实现
bool ProjectInfoDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index)
{
    ProjectInfoModel* conModel = static_cast(model);
    QRect rect = option.rect;
    QRect checkedRect;
    QRect iconRect;
    QRect nameRect;
    QRect timeRect;
    QRect sizeRect;
    QRect moreRect;
    getRects(rect, checkedRect, iconRect, nameRect, timeRect, sizeRect, moreRect);

    QMouseEvent* msEvent = static_cast(event);
    if(event->type() == QEvent::MouseButtonPress)
    {
        if(checkedRect.contains(msEvent->pos()))
        {
            conModel->setData(index, fly::CHECKED_STATE, fly::CLICKED_ROLE);
            emit conModel->clicked(index);
            conModel->setData(index, false, fly::CHECKED_STATE);
        }
        else if(moreRect.contains(msEvent->pos()))
        {
            emit conModel->moreBtnClick(index, QPoint(rect.x()+ moreRect.x()+ moreRect.width(), rect.y()+ moreRect.height()));
        }
    }

    return QStyledItemDelegate::editorEvent(event, model, option, index);
}

        editorEvent()函数会传入鼠标的Pressed事件,以及当前index和整个Model,我们在判断到MouseButtonPress事件时,可以根据鼠标当前所在的位置来判断属于哪个控件的区域,然后更新对应item的CLICKED_ROLE字段的值,该字段记录了鼠标事件属于哪个控件的。通过在Model中的自定义信号clicked发出信号,该信号可以在Delegate对象持有者中进行处理。

  •  鼠标hover事件实现
bool ProjectInfoListView::eventFilter(QObject *watched, QEvent *event)
{
    //捕获ListView的MouseMove事件,并把Mouse的position值记录在鼠标当前所在的item的数据里,用于实现item的多控件hover响应;
    if(watched == m_listView->viewport() && event->type() == QMouseEvent::MouseMove)
    {
        QMouseEvent* msEvent = static_cast(event);
        m_model->setData(m_listView->indexAt(msEvent->pos()), msEvent->x(), fly::HOVER_POS_X);
        update();
    }

    return QWidget::eventFilter(watched, event);
}
void ProjectInfoDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
    QRect rect = option.rect;
    QRect checkedRect;
    QRect iconRect;
    QRect nameRect;
    QRect timeRect;
    QRect sizeRect;
    QRect moreRect;
    getRects(rect, checkedRect, iconRect, nameRect, timeRect, sizeRect, moreRect);

    QPoint msPos = QPoint(index.data(fly::HOVER_POS_X).toInt(), rect.y());
    if(option.state & QStyle::State_MouseOver)
    {
        //more button hover
        if(moreRect.contains(msPos))
        {
            painter->fillRect(moreRect, QColor(100, 100, 100, 100));
        }
        else if(nameRect.contains(msPos))
        {
            painter->fillRect(nameRect, QColor(100, 100, 100, 100));
        }
    }
}

         item中的鼠标hover事件需要由QListView的viewport的的MouseMove来实现,Delegate的paint()函数中虽然有QStyle::State_MouseOver事件,但这个事件只能实现item的hover,无法实现item里面具体的控件的hover。结合QListView的viewport中捕获的Mouse 的位置就可以具体到哪个控件,同样也是通过把Mouse的位置记录到item的自定义的HOVER_POS_X字段中,再在paint()中去读取这个值。

  • 实现输入型控件动态创建 

        重写createEidtor(),setEditorData(),setModelData()和updateEditorGeometry()这四个函数可以实现在item上动态创建各种类型的控件,比如QLineEidt,QSlider,甚至自绘的控件都可以。

        1. QListView可以通过setEditTriggers()函数设置触发item进入Eidt模式的方式,分别有鼠标双击,选中单击等选项。假如设置了双击触发,那么在双击item时QListView底层会调用createEidtor()来获取要创建的控件对象,当我们在子类重写了createEditor(),使它返回我们自己创建的控件,那么动态创建控件的目的就达到了。

        2. 底层在调用完createEditor()后,会接着调用setEidtorData()来初始化控件的内容,比如说把item的内容设置在控件上。

        3. createEidtor()创建出来的控件默认位置是当前item的(0, 0) 位置,要想控件显示在不同的位置可以重写updateEditorGeometry()把控件移到希望显示的位置,同样这个函数也是由QListView底层调用。

        4. setModelData()的作用是在item推出edit状态后更新Model的,也是由QListView底层调用,比如输入框输入完成后,把输入框的内容更新到Model中。

【QListViewDemo】https://download.csdn.net/download/JellyLi2091/88068274

你可能感兴趣的:(Qt,qt,C++)