概述
本文目标是实现一个系列图片缩略图展示控件,要求可以水平滚动缩略图。主要样式参考,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...
//
图片加载效果,
选中效果: