PyQt5自定义图片组件:同时显示多张图片

前言:大概在去年 10 月,笔者想做一个有 GUI 的词云生成器,可通过界面上的各种控件选择制图参数,定制化词云图的形状、颜色、字体等。在形状定制方面,希望有一个组件能够加载多张图片同时显示,通过点击图片来选择对应的形状。一开始搜索了半天,也没发现可用的轮子,而后因为要忙工作上的项目,这个小玩意儿的开发就搁置了,至今仍未完成(究竟会鸽到什么时候呢…)。但对于这个图片组件,当时还是写了一个可复用的雏形,如今回顾并优化一番,以便在后续补完词云生成器时能更加方便运用。那么先来看看效果吧!

一、自定义图片组件ImageWidget用例

首先笔者在 QtDesigner 中拖了两个 LineEdit 分别用于显示图片组页数和单个图片的路径,以及两个 PushButton 用于控制翻页。
PyQt5自定义图片组件:同时显示多张图片_第1张图片

接着,在这个测试窗体中添加笔者自定义的组件 ImageWidget ,当前设计该组件可显示一行 N 列的图片,每个图片自动缩放以正方形显示,因此仅需指定组件的列数、总宽度,以及需读取图片的文件目录。
以下用例笔者添加了宽 600、一次显示 4 张图片(即 4 列)、从 shape_images 文件夹读取图片的组件,因组件继承 QWidegt,所以可用父类方法 move() 将组件移动到主窗体中的合适位置。
将按钮的点击信号与组件的翻页方法 turn_page() 绑定, 传入 1 为下一页, -1 为上一页。
再将组件内部的图片点击信号与翻页信号,与修改 LineEdit 内容的方法绑定。

class testForm(QWidget, untitled.Ui_Form):

    def __init__(self):
        super(testForm, self).__init__()
        self.setupUi(self)
        # 添加自定义图像组件
        self.image_widget = ImageWidget(self, dir='./shape_images', col=4, w=600)
        self.image_widget.move(20, 100)
        self.pB_previous.clicked.connect(lambda: self.image_widget.turn_page(-1))
        self.pB_next.clicked.connect(lambda: self.image_widget.turn_page(1))  # 图像列表翻页
        self.image_widget.signal_order.connect(self.change_path)
        self.image_widget.signal_page.connect(self.change_page)

    def change_path(self, path):
        self.lineEdit_path.setText(path)

    def change_page(self, index):
        self.lineEdit_page.setText(f"第{index}页")

看看在界面上实际操作的效果,通过传递翻页和图片点击的信号,我们可以执行更多的操作。例如,点击了圆形,词云生成器就获取圆形图像作为 mask 参数。
PyQt5自定义图片组件:同时显示多张图片_第2张图片

此外,ImageWidget 还有一些可选参数,先分别修改 ImageWidget 的列数为 2 和 6 ,我们可以看到图片都以正方形显示,而高度会自动调整,但不保持原比例。
PyQt5自定义图片组件:同时显示多张图片_第3张图片
PyQt5自定义图片组件:同时显示多张图片_第4张图片

如果不想以正方形显示,我们可以通过参数 h 指定组件的高度,还可通过参数 suit 指定图片尺寸的适应方式,如下为设置 3 列、高度为 330、保持原比例按高适应的图片组。

self.image_widget = ImageWidget(self, dir='./shape_images', col=3, w=600, h=330, suit=1)

PyQt5自定义图片组件:同时显示多张图片_第5张图片

设置 1 列、高度为 330、保持原比例按宽适应的图片组。

self.image_widget = ImageWidget(self, dir='./shape_images', col=1, w=600, h=330, suit=2)

PyQt5自定义图片组件:同时显示多张图片_第6张图片

在此基础上,我们可以继续强化 ImageWidget 的功能和效果,比如可设置显示多行多列的图片矩阵、美化组件样式、自动轮播等。下面我们看看当前的组件是如何实现的。

二、ImageWidget源码解析

1、制作相框

熟悉 Qt 的同学可以想到,有一个控件可以用来显示图片,那就是 QLabel。该控件支持文本、图片、动态图,甚至是视频的加载,是个十分便利的容器。但 QLabel 本身没有实现点击事件,也就无法像按钮一样传达点击信号,那我们可以自己实现它。
首先自定义一个 MyLabel 类,继承自 QLabel。笔者添加了一个整型信号,并重载了鼠标点击事件,用于在其自身被点击时传递标识,而标识则在创建对象时通过 order 参数设置。此外,笔者设置了一个简单的样式,加上了灰色边框。
如此一来,单张图片的“塑封膜"或者说"相框”,就设计好了。

class MyLabel(QLabel):  # 自定义label,用于传递是哪个label被点击了
    signal_order = pyqtSignal(int)

    def __init__(self, order=None):
        super(MyLabel, self).__init__()
        self.order = order
        self.setStyleSheet("border-width: 2px; border-style: solid; border-color: gray")

    def mousePressEvent(self, e):  # 重载鼠标点击事件
        self.signal_order.emit(self.order)

2、制作相册-定制页

“相框”有了,还缺一个“相册”,我们可以通过布局来考虑图片是如何在相册中摆放的。因为布局大小跟随其父类容器,所以笔者选择了 Qwidegt 来作为相册的框架。
首先,打开相册当然是在第一页嘛,然后它还有一个目录 list_files,并且选择图片和翻页时它会通过两种信号来告诉你。

class ImageWidget(QWidget):
    group_num = 1  # 图像列表当前组数(页数)
    list_files = []  # 图像文件路径集
    signal_order = pyqtSignal(str)  # 图像项目信号
    signal_page = pyqtSignal(int)  # 页数信号

我们的这个相册虽然简单,但它也是可定制的。你要告诉它从哪儿收集图片(dir),一页显示几张图片(col),图片的宽/高(w/h)和自适应模式(suit)。

def __init__(self, parent=None, dir='./', col=1, w=10, h=None, suit=0):
    super(ImageWidget, self).__init__(parent)
    self.get_files(dir)
    self.col = col
    self.w = w
    self.suit = suit
    if h == None:
        self.h = self.w / self.col
    else:
        self.h = h
    self.setFixedSize(self.w, self.h)
    self.hbox = QHBoxLayout(self)
    self.hbox.setContentsMargins(0, 0, 0, 0)
    self.show_images_list()  # 初次加载图像列表
    
def get_files(self, dir):  # 储存需加载的所有图像路径
    for file in os.listdir(path=dir): 
        if file.endswith('jpg') or file.endswith('png'):
            self.list_files.append(dir + "/" + file)

这个相册的风格比较复古,是一卷胶带的形式,它每页展示的都是一行图片,所以我们采用水平布局 QHBoxLayout。
PyQt5自定义图片组件:同时显示多张图片_第7张图片

3、制作相册-翻页

在放入图片之前,先想想这个相册要怎么翻页。由于不能虚空翻页,所以在首页不可往前翻,在末页不可往后翻,无图时也没必要翻空气。
flag 表示目录中的图片数, math.ceil(flag/self.col) 即表示翻完装满了图片的页面后,多出来的图片所在的页数(末页)。比如每页 4 张图,一共 9 张图,计算得末页数为 3,即第一页、第二页都放满了 4 张,而第三页(末页)只放了 1 张。翻完页通过信号说一声到第几页了,最后把该页的图片展示出来。

def turn_page(self, num):  # 图像列表翻页
    flag = len(self.list_files)
    if self.group_num == 1 and num == -1:  # 到首页时停止上翻
        QMessageBox.about(self, "Remind", "This is the first page!")
    elif (self.group_num == math.ceil(flag/self.col) and num == 1) or flag==0:  # 到末页页时停止下翻
        QMessageBox.about(self, "Remind", "No more image! ")
    else:
        self.group_num +=  num  # 翻页
    self.signal_page.emit(self.group_num)
    self.show_images_list()  # 重新加载图像列表

4、制作相册-放入图片

相册的基本框架有了,开始整理图片往里塞吧!先确保新的每一页都是空的,不能放重了。

def show_images_list(self):  # 加载图像列表
    for i in range(self.hbox.count()):  # 每次加载先清空内容,避免layout里堆积label
        self.hbox.itemAt(i).widget().deleteLater()

(继续补充 show_images_list(),下同)接着我们想想在当前页,要放哪几张照片。假设 col 为 4,那么在第一页,我们就从第 1 张放到第 4 张;第二页,从第 5 张放到第 8 张,以此类推。再用 count 记录当前页已经放了几张了。别忘了我们的相册是可定制尺寸的,图片宽度是一页相册的总宽度除以该页的图片数。

# 设置分段显示图像,每col个一段
group_num = self.group_num
start = 0
end = self.col
if group_num > 1:
    start = self.col * (group_num - 1)
    end = self.col * group_num
count = 0  # 记录当前页载入的label数
width = int(self.w / self.col)  # 自定义label宽度
height = self.h  # 自定义label高度

想好了就开始放入图片吧,根据客户指定的适应模式(suit),我们还提供了裁剪尺寸的服务,可以按原始比例(0)、按相框高(1)、按相框宽(2)来裁剪,并且裁剪时会根据相框的厚度(border-width: 2px)进行微调(-2*self.col-4)以得到最佳效果。

for index, path in enumerate(self.list_files):  # group_num = 1 则加载前col个,以此类推
    if index < start:
        continue
    elif index == end:
        break
    # 按路径读取成QPixmap格式的图像,根据适应方式调整尺寸
    if self.suit==0:
        pix = QPixmap(path).scaled(width-2*self.col, height-4)
    elif self.suit==1:
        pix = QPixmap(path)
        pix = QPixmap(path).scaled(int(pix.width()*height/pix.height())-2*self.col, height-4)
    elif self.suit==2:
        pix = QPixmap(path)
        pix = QPixmap(path).scaled(width-2*self.col, int(pix.height()*width/pix.width())-4)

(继续补充 for 循环)裁剪好后就塑封装框啦,然后把带图片的相框放进相册里。我们这是个电子相册,绑定好每个相框的信号,这样触摸图片就有点读机的效果了。

label = MyLabel(index)
label.setPixmap(pix)  # 加载图片
self.hbox.addWidget(label)   # 在水平布局中添加自定义label
label.signal_order.connect(self.choose_image)  # 绑定自定义label点击信号
count += 1

(for 循环外)这个相册有脾气,说每页要放几张图就要放几张,不够就要放白纸进去,其实也是为了有实物填充才好撑门面嘛。

if not count==self.col:
    for i in range(self.col - count):
        label = QLabel()
        self.hbox.addWidget(label)  # 在水平布局中添加空label补位

不然有空缺的相册页就会变形移位了,当然,没有强迫症不加也没关系。
PyQt5自定义图片组件:同时显示多张图片_第8张图片
另外再瞅瞅刚才说的点读机,先将 MyLabel 中的点击信号传至 choose_image(),再利用 ImageWidget 中的点击信号发射出去,由此便可实现点击任意图片后对该图片进行的一些操作。

def choose_image(self, index):  # 选择图像
    self.signal_order.emit(self.list_files[index])

结语

至此,可定制的图片组件 ImageWidget 就初步完成了。源码可通过以下方式自取,可供学习参考,欢迎进行二次开发或提出改进版本。在写这篇之前笔者又去搜索了一番,还发现了一些新方案,待尝试研究后将继续码文分享(咕咕)。这里是放自己鸽子的塞翁,下一篇再见!
Github地址
Gitee地址

你可能感兴趣的:(PyQt5记录,qt,gui)