QtGUI /QScrollArea + QLabel /实现一个可水平滑动的多图片缩略图预览组件

文章介绍了如何在Qt环境中通过派生QScrollArea实现水平滚动的图片缩略图组件。利用QLabel和QPixmap显示图片,通过事件过滤器处理焦点事件来管理选中状态。此外,还讨论了Qt的键盘焦点机制,包括键盘和鼠标事件。最后,展示了如何在实际代码中动态添加和设置图片,并给出了预览组件的使用示例。
摘要由CSDN通过智能技术生成

概述

本文目标是实现一个系列图片缩略图展示控件,要求可以水平滚动缩略图。主要样式参考,VisonPro 软件中的图像源配置窗口。最终我通过派生 QScrollArea 组件实现了水平滚动功能,使用 QImage和QLabel 实现图片加载和显示功能,并通过过滤 QLabel的‘焦点获得事件’实现了缩略图的选中效果。
在这里插入图片描述

滚动效果的实现

正式着手前,我幻想这QcrollArea会有addWidget和setScrollDirection之类的遍历接口,可以使得我们直接在其中添加任意多的窗口。但实际上,我它更简单些,它只提供了setWidget接口,而,把布局交由用户自定义的Widget来实现。重点关注下 QScrollArea 的组成
在这里插入图片描述
如上Qt Designer中的截图,当拖入 scrollArea 组件时,scrollAreaWidgetContents 窗口会被自动创建。我们新添加的 QLabel 会自动的以 scrollAreaWidgetContents 为父窗口,而不是 scrollArea。其实不用做太多工作便能实现水平滚动,在QScrollArea 控件 + Qt 布局机制的作用下,使用 Qt Designer 就可以绘制出如下效果。 如下,scrollArea 和 label_image_x 设置 固定高度,并添加水平弹簧,在scrollAreaWidgetContents 上应用水平布局。
请添加图片描述
如上并不是最终的实现,只是为了说明,使用QScrollArea 很容易就可以实现水平滚动效果。实际实现中,我们提升QScrollArea为自定义类,使用代码动态的添加label_image_x缩略图显示窗,设置scrollArea垂直滚动关闭,水平滚动打开…

实现选中状态管理

但QLabel及其父类中,并没有接口或属性可以实现选中状态selected的管理。我们的需求之一是,某时刻必须有一张Image处于选中状态,QWidget的焦点状态是不会长久被维持的,它可以被其他任何可获得焦点的其他窗体抢走。但是我们可以利用窗口获得焦点这一事件,来实现Label_Image选中状态的功能。实现方案有两种:重写focusInEvent函数、重写eventFilter函数。

插曲,
使用 focusInEvent 函数,帮助文档中对focusInEvent的描述,

void QWidget::focusInEvent(QFocusEvent *event)

This event handler can be reimplemented in a subclass to receive keyboard focus events (focus received) for the widget. The event is passed in the event parameter。我有个疑问,focusInEvent 只接收 keyboard focus ? 不接受鼠标事件 ? 于是我又查询了 什么是 Qt的 Keyboard Focus ? 参考 《Qt 5.12 Qt Widgets Keyboard Focus in Widgets》帮助中的描述,
The customs which have evolved for directing keyboard focus to a particular widget are these:
哈哈,原谅我正在准备英语考试,这里的customs本意是风俗、习惯,此引申为约定。which 引导的定语从句修饰 customs 这个主语,谓语是are系动词。
The user presses Tab (or Shift+Tab). The user clicks a widget. The user presses a keyboard shortcut. The user uses the mouse wheel.
The user moves the focus to a window, and the application must determine which widget within the window should get the focus.
也就是说,Qt 的 Keyboard Focus 包含键盘事件和鼠标事件。

重载 focusInEvent 函数,需要派生 QLabel 类,我一般不太喜欢这样。个人更新欢 使用 eventFilter 函数,怎么也能少写一个类是不。具体实现参见后续章节。

其他,使用QSS样式表实现选中效果。

实现QLabel显示图片

参见 《机器视觉 /使用QLabel显示QImage的实现方法》中提及的两种方式,本文使用的是 QPixmap 方案,具体实现参见后续章节。

#include <QApplication>
#include <QLabel>
#include <QImage>

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);

    // 从文件加载 QImage(也可以从其他来源获取 QImage)
    QImage image("path/to/your/image.png");

    // 将 QImage 转换为 QPixmap
    QPixmap pixmap = QPixmap::fromImage(image);

    // 创建 QLabel,并设置 QPixmap
    QLabel label;
    label.setPixmap(pixmap);

    // 设置 QLabel 自动适应大小
    label.setScaledContents(true);

    // 显示 QLabel
    label.show();

    return app.exec();
}

缩略图预览组件实现

//previewimage.h
#pragma once

#include <map>
#include <vector>
#include <QScrollArea>

class PreviewImage  : public QScrollArea
{
    Q_OBJECT

public:
    PreviewImage(QWidget *parent);
    ~PreviewImage();

public:
    //添加缩略图位置
    bool AddLabelImage(int iCntOfLabelImage);

    //指定位置设置图像数据
    bool SetLabelImage(QImage *pImage, int indexOfLabel);

signals:
    void SignalCurrentSelected(int iLabelIndex);

protected:
    bool eventFilter(QObject *obj, QEvent *ev) override;

private:
    //当前被选中的Label
    int m_iCurrentLabelImage;

    //记录各Label的序号
    std::map <void*, int> m_mapLabelAddrToInnerID;
    //记录序号对应的Label指针
    std::vector <void*> m_vecLabelAddr;
};
//previewimage.cpp
#include <QEvent>
#include <QLabel>
#include <QImage>
#include <QPixmap>
#include <QHBoxLayout>
#include "previewimage.h"

PreviewImage::PreviewImage(QWidget *parent)
    : QScrollArea(parent)
{
    m_vecLabelAddr.clear();
    m_mapLabelAddrToInnerID.clear();
}

PreviewImage::~PreviewImage()
{

}

bool PreviewImage::AddLabelImage(int iCntOfLabelImage)
{
    QHBoxLayout *pHBoxLayout = new QHBoxLayout();
    //
    this->widget()->setLayout(pHBoxLayout);
    //
    pHBoxLayout->setContentsMargins(2, 2, 2, 2);

    //create
    for (int index = 0; index < iCntOfLabelImage; index++)
    {
        QLabel *plabel = new QLabel(QString("ImagePos <%1>").arg(index), this->widget());
        //添加到水平布局
        pHBoxLayout->addWidget(plabel);

        //设置label允许选中
        plabel->setFocusPolicy(Qt::StrongFocus);
        //满足1.8倍的比例关系 //且高度上为水平滚动条预留空间
        plabel->setFixedSize((this->height() - 30)/0.618, this->height() - 30);
        //设置获得焦点时的样式 /focus样式无法持久保持
        //plabel->setStyleSheet("QLabel:focus { border: 2px solid red; }" );
        //无Image填充时以此显示
        plabel->setStyleSheet("QLabel { border: none; background-color: rgb(170, 255, 255);}");
        //否则填充image后无法看到border
        plabel->setMargin(3);

        //设置 QImage 自动适应QLabel大小 //must
        plabel->setScaledContents(true);

        //记录映射关系
        m_vecLabelAddr.push_back(plabel);
        //记录映射关系
        m_mapLabelAddrToInnerID.insert(std::make_pair(plabel, index));

        //注册Qt事件过滤器
        plabel->installEventFilter(this);
    }

    return true;
}

bool PreviewImage::SetLabelImage(QImage * pImage, int indexOfLabel)
{
    QPixmap pixmap = QPixmap::fromImage(*pImage);

    if (indexOfLabel >= m_vecLabelAddr.size())
        return false;

    ((QLabel*)m_vecLabelAddr[indexOfLabel])->setPixmap(pixmap);

    return true;
}

bool PreviewImage::eventFilter(QObject * obj, QEvent * ev)
{
    if (/*"QLabel" == obj->metaObject()->className()*/true)
    {
        if (QEvent::FocusIn == ev->type())
        {
            //被选中的Label
            int iLabelIndex = m_mapLabelAddrToInnerID[obj];

            //通知客户当前被选中的LabelID是什么
            if (m_mapLabelAddrToInnerID.find(obj) != m_mapLabelAddrToInnerID.end())
            {
                //取消原本被选中的控件的样式 //在成员中记录上次选中更好些
                for (auto &kone : m_vecLabelAddr)
                    ((QLabel*)kone)->setStyleSheet("QLabel { border: none; background-color: rgb(170, 255, 255);}");

                //设置自身为选中状态
                emit SignalCurrentSelected(iLabelIndex);

                //设置选中状态的样式
                ((QLabel*)obj)->setStyleSheet("QLabel { border: 2px solid red; background-color: rgb(170, 255, 255);}");
            }
                
        }
    }

    return QScrollArea::eventFilter(obj, ev);
}

缩略图预览组件使用和效果

//testimagepreview.ui
在这里插入图片描述
注意,要将 scrollArea 从 QScrollArea 原始类型 提升为 PreviewImage 自定义类型。当然也可以直接在mainWindow中心窗口上,自己定义布局进行添加,此处不再赘述这些基础知识。

//testimagepreview.h
#pragma once

#include <QtWidgets/QMainWindow>
#include "ui_testimagepreview.h"

QT_BEGIN_NAMESPACE
namespace Ui { class TestImagePreviewClass; };
QT_END_NAMESPACE

class TestImagePreview : public QMainWindow
{
    Q_OBJECT
public:
    TestImagePreview(QWidget *parent = nullptr);
    ~TestImagePreview();
private:
    Ui::TestImagePreviewClass *ui;
    //默认打开的文件夹
    QString m_strDefaultOpenLastDir;
    //被选出的文件列表
    QStringList m_strlstSelectedFiles;
    //当前被选中的数据的序号
    int m_iDataIndexOfSelected = -1;
};
//testimagepreview.cpp
#include <QFileDialog>
#include "testimagepreview.h"
#include "previewimage.h"

//定义预览label的最大个数
#define COUNT_MAX_LABEL_OF_IMAGE 20

TestImagePreview::TestImagePreview(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::TestImagePreviewClass())
{
    ui->setupUi(this);

    //预览控件实现
    ((PreviewImage*)ui->scrollArea)->AddLabelImage(COUNT_MAX_LABEL_OF_IMAGE);
    //预览组件的选中信息
    connect((PreviewImage*)ui->scrollArea, &PreviewImage::SignalCurrentSelected, [&](int _currentIndex) {
        if (_currentIndex >= m_strlstSelectedFiles.size())
            ui->label_selected_info->setText(QStringLiteral("#当前没有选中任何数据块..."));
        else
            ui->label_selected_info->setText(QString("Selected: %1").arg(m_strlstSelectedFiles.at(_currentIndex)));
		//do something about m_iDataIndexOfSelected ...
    });

    //选择图片数据
    connect(ui->pushButton_select_blocks, &QPushButton::clicked, [&]() {
        QStringList &files = m_strlstSelectedFiles;
        //打开文件选择窗口
        files = QFileDialog::getOpenFileNames(
            this,
            "Select one or more files to open",
            m_strDefaultOpenLastDir,
            "Images (*.png *.jpg *.bmp)");

        if (!files.isEmpty())
            //从index0全文件路径得到最终的目录 //更新默认打开路径 
            m_strDefaultOpenLastDir = files[0].left(files[0].lastIndexOf("/"));
        else
            return;

        std::vector<std::string> lstOfRawImageName;
        for (int i = 0; i < files.size(); i++)
            lstOfRawImageName.push_back(files.at(i).toLocal8Bit().toStdString());

        //图片缩略图展示(不同数据类型转Image的方式不同)
        for (int i = 0; i < lstOfRawImageName.size(); i++)
        {
            // 从文件加载 QImage(也可以从其他来源获取 QImage)
            QImage image(QString::fromLocal8Bit(lstOfRawImageName[i].c_str()));

            if (i < COUNT_MAX_LABEL_OF_IMAGE)
                ui->scrollArea->SetLabelImage(&image, i);
            else
                //WARN_...
                break;
        }
    });
}

//
//do somethingabout business...
//

图片加载效果,
在这里插入图片描述
选中效果:
在这里插入图片描述

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值