前言:大概在去年 10 月,笔者想做一个有 GUI 的词云生成器,可通过界面上的各种控件选择制图参数,定制化词云图的形状、颜色、字体等。在形状定制方面,希望有一个组件能够加载多张图片同时显示,通过点击图片来选择对应的形状。一开始搜索了半天,也没发现可用的轮子,而后因为要忙工作上的项目,这个小玩意儿的开发就搁置了,至今仍未完成(究竟会鸽到什么时候呢…)。但对于这个图片组件,当时还是写了一个可复用的雏形,如今回顾并优化一番,以便在后续补完词云生成器时能更加方便运用。那么先来看看效果吧!
首先笔者在 QtDesigner 中拖了两个 LineEdit 分别用于显示图片组页数和单个图片的路径,以及两个 PushButton 用于控制翻页。
接着,在这个测试窗体中添加笔者自定义的组件 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 参数。
此外,ImageWidget 还有一些可选参数,先分别修改 ImageWidget 的列数为 2 和 6 ,我们可以看到图片都以正方形显示,而高度会自动调整,但不保持原比例。
如果不想以正方形显示,我们可以通过参数 h
指定组件的高度,还可通过参数 suit
指定图片尺寸的适应方式,如下为设置 3 列、高度为 330、保持原比例按高适应的图片组。
self.image_widget = ImageWidget(self, dir='./shape_images', col=3, w=600, h=330, suit=1)
设置 1 列、高度为 330、保持原比例按宽适应的图片组。
self.image_widget = ImageWidget(self, dir='./shape_images', col=1, w=600, h=330, suit=2)
在此基础上,我们可以继续强化 ImageWidget 的功能和效果,比如可设置显示多行多列的图片矩阵、美化组件样式、自动轮播等。下面我们看看当前的组件是如何实现的。
熟悉 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)
“相框”有了,还缺一个“相册”,我们可以通过布局来考虑图片是如何在相册中摆放的。因为布局大小跟随其父类容器,所以笔者选择了 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。
在放入图片之前,先想想这个相册要怎么翻页。由于不能虚空翻页,所以在首页不可往前翻,在末页不可往后翻,无图时也没必要翻空气。
用 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() # 重新加载图像列表
相册的基本框架有了,开始整理图片往里塞吧!先确保新的每一页都是空的,不能放重了。
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补位
不然有空缺的相册页就会变形移位了,当然,没有强迫症不加也没关系。
另外再瞅瞅刚才说的点读机,先将 MyLabel
中的点击信号传至 choose_image()
,再利用 ImageWidget
中的点击信号发射出去,由此便可实现点击任意图片后对该图片进行的一些操作。
def choose_image(self, index): # 选择图像
self.signal_order.emit(self.list_files[index])
至此,可定制的图片组件 ImageWidget 就初步完成了。源码可通过以下方式自取,可供学习参考,欢迎进行二次开发或提出改进版本。在写这篇之前笔者又去搜索了一番,还发现了一些新方案,待尝试研究后将继续码文分享(咕咕)。这里是放自己鸽子的塞翁,下一篇再见!
Github地址
Gitee地址