pyQt5(QScrollArea+QGridLayout):图片合成gif功能,快速开发与实战

第一用pyQt5做了图片合成gif动图的小功能,将知识点做个记录。界面使用pyQt5的designer.exe 应该能好看些,但我全部敲代码,所以,布局很乱,界面很糙。虽然糙,但也是第一次用pyQt5的成果,先看下效果功能


QQ图片20211029094025.png

添加图片后,点击合成gif,将这些图片合成成gif动图。
代码分两部分整理,界面部分和事件部分

界面部分

模块整理

使用了pyQt5三个模块:

  • QtWidgets:包含创造经典桌面风格的用户界面提供了一套UI元素的类。
  • QtGui:包含类窗口系统集成、事件集成、二维图形、基本成像、字体和文本。
  • QtCore:包含了核心的非GUI功能。此模块用于处理时间、文件和目录、各种数据类型、流、URL、MIME类型、线程或进程。

以下为该界面用到的模块方法

QtWidgets:

QApplication

用于管理图形用户界面应用程序的控制流和主要设置。
获取屏幕大小

# 屏幕大小
self.desktop = QApplication.desktop()
self.height = self.desktop.height()
self.width = self.desktop.width()
QDesktopWidget

提供屏幕的有关信息。
窗口居中

qr = self.frameGeometry()
cp = QDesktopWidget().availableGeometry().center()  # desktop的availableGeometry()获取可用位置和尺寸(除去任务栏等)
qr.moveCenter(cp)
self.move(qr.topLeft())
QFileDialog

打开和保存文件的标准对话框

方法 描述
getOpenFileNames() 多选文件
getOpenFileName() 单选文件
getExistingDirectory() 返回用户选择的文件路径
getOpenFileName() 返回用户选择的文件名
QGridLayout

布局管理器,以网格的形式对组件进行定位

QHBoxLayout

水平布局

方法 描述
setSpacing() 布局中控件间距
addWidget() 添加控件
addStretch() 添加伸缩空间,在第一个控件之前添加,所有控件达到居右的效果、最后一个控件后添加,所有控件居左、第一个控件之前,最后一个控件之后分别添加则居中
QLable

文本标签

方法 描述
setPixmap() 设置图片,使用QPixmap设置,如:setPixmap(QPixmap(':/pic/add.png'))
QLineEdit

文本输入框

方法 描述
setVisible() 设置可见
setText() 设置显示内容
text() 返回显示内容
setValidator() 设置文本框校验器
QMainWindow

因为要用到状态栏,所以我选择继承QMainWindow。

方法 描述
setWindowTitle() 设置窗口标题
setWindowIcon() 设置窗口图标
resize() 设置窗口大小
setStatusBar() 设置状态栏
QProgressBar

进度条

方法 描述
setRange() 设置进度条范围
setValue() 进度条值
QPushButton
方法 描述
setMaximumSize() 设置大小,使用QSize()设置,如:setMaximumSize(QSize(100, 50))
setIcon() 添加图片,使用QIcon设置,如:setIcon(QIcon(':/pic/add.png'))
setStyleSheet() 设置按钮样式
setCursor() 设置鼠标手型
setEnabled() 设置可用
QRadioButton
方法 描述
setChecked() 设置选中
QScrollArea

滚动区域。

方法 描述
setAutoFillBackground() 填充背景色
setWidgetResizable() 自动调整窗口小部件的大小,以避免在可以避免滚动条的地方使用滚动条。在该例子中,我使用了改变窗口的大小,所以该方法没效果。
setGeometry() 设置所在位置和大小,(x, y, w, h) 即 (X坐标、Y坐标、宽、高),其中x,y坐标是参照它所在的继承的窗口,非屏幕坐标,单位px
QStatusBar

窗口状态栏

方法 描述
setStyleSheet() 设置样式
showMessage() 状态栏显示信息
QWidget

基础窗口控件。

QtGui:

QDoubleValidator

校验器。

方法 描述
setRange() 值范围
setDecimals() 精度位数
QIcon

设置图标。

self.setWindowIcon(QIcon(':/pic/aehvi-0m7h6-001.ico'))
QPalette

对话框或控件调色板。

scroll_area_palette = self.scroll_area.palette()  # 获取已有的调色板
scroll_area_palette.setColor(QPalette.Window, Qt.white)  # 修改背景色为白色
self.scroll_area.setPalette(scroll_area_palette)  # 为组件设置调色板
self.scroll_area.setAutoFillBackground(True)  # 组件生效调色板

QtCore:

QSize

设置尺寸。

self.add_file_btn.setMaximumSize(QSize(113, 38))
QRect

设置左、上、宽、高。

代码整理

分两个类文件,界面类和事件类。

class Ui_Form(QMainWindow):
    def __init__(self):
        # 初始化继承父类(QMainWindow)
        super(Ui_Form, self).__init__()
        # 获取屏幕大小
        self.desktop = QApplication.desktop()
        self.height = self.desktop.height()
        self.width = self.desktop.width()

        # 设置窗口大小,并显示在屏幕中间
        self.resize(int(self.width * 0.5), int(self.height * 0.65))
        self.setWindowTitle("合成gif")
        self.setWindowIcon(QIcon(':/pic/aehvi-0m7h6-001.ico'))

        self.set_center()
        self.initUI()

窗口居中

    def set_center(self):
        qr = self.frameGeometry()
        cp = QDesktopWidget().availableGeometry().center()  # desktop的availableGeometry()获取可用位置和尺寸(除去任务栏等)
        qr.moveCenter(cp)
        self.move(qr.topLeft())

添加控件,主要是QScrollArea控件的滚动条,当区域内的内容超过滚动区域大小时显示滚动条。QScrollArea内部有个QGridLayout(QWidget子组件),因为QScrollArea只能添加widget,先实例一个QWidget,再把QGridLayout添加到QWidget中,就可以添加到QScrollArea里。计算好QGridLayout列数的大小不要超过QScrollArea的宽度,就不会显示横向滚动条,当图片加入QGridLayout的个数总长度超过QScrollArea的高度时,自动出现纵向滚动条。个人觉得这个方法最简单

滚动区域的大小,我按屏幕的大小比例计算,如果写死大小,如果程序打包出来,发给屏幕尺寸不一样的电脑,界面上控件位置就会惨不忍睹。我这个办法个人觉得很low,但是一时没找到更好的方法,先这么将就用了。

        # 滚动区域,存放图片
        self.scroll_area = QScrollArea(self)
        self.scroll_area_contentwidget = QWidget(self)
        self.gridLayout = QGridLayout(self.scroll_area_contentwidget)
        self.scroll_area.setWidget(self.scroll_area_contentwidget)
        # 设置滚动区域的位置和大小
        self.scroll_area.setGeometry(10, 10, int(self.width*0.5-20), int(self.height*0.6/3*2))  

设置滚动区域背景色,setAutoFillBackground 方法一定要设置为Ture,否则背景色不生效

        scroll_area_palette = self.scroll_area.palette()  # 获取已有的调色板
        scroll_area_palette.setColor(QPalette.Window, Qt.white)  # 修改背景色为白色
        self.scroll_area.setPalette(scroll_area_palette)  # 为组件设置调色板
        self.scroll_area.setAutoFillBackground(True)  # 组件生效调色板

添加按钮,加入水平布局

        # 添加两个按钮
        self.btnwidget = QWidget(self)
        self.btnwidget.setGeometry(QRect(10, int(self.height*0.6/3*2)+20, 211, 50))  # 按钮控件布局及大小

        self.hlayout_btn = QHBoxLayout(self.btnwidget)  # 加水平布局
        self.hlayout_btn.setSpacing(10)  # 水平布局中控件的间隔
        
        # 添加文件按钮
        self.add_file_btn = QPushButton('添加文件', self.btnwidget)
        self.add_file_btn.setMaximumSize(QSize(113, 38))  # 设置按钮大小
        self.add_file_btn.setIcon(QIcon(':/pic/add.png')) # 按钮添加图片
        self.add_file_btn.setStyleSheet("border-radius:10px;border:1px groove gray;")  # 设置按钮样式
        self.add_file_btn.setCursor(Qt.PointingHandCursor)  # 鼠标移入显示手型

        self.empty_btn = QPushButton('清空列表', self.btnwidget)
        self.empty_btn.setMaximumSize(QSize(113, 38))
        self.empty_btn.setIcon(QIcon(':/pic/cal.png'))
        self.empty_btn.setStyleSheet("border-radius:10px;border:1px groove gray;")
        self.empty_btn.setCursor(Qt.PointingHandCursor)
        self.empty_btn.setEnabled(False)  # 设置按钮不可用
        
        # 水平布局加入按钮
        self.hlayout_btn.addWidget(self.add_file_btn) 
        self.hlayout_btn.addWidget(self.empty_btn)

输出信息块的代码,与按钮添加类似,主要是水平布局在最后添加的控件添加拉伸,使各控件局左,否则各个控件间就会隔的很开

        self.hlayout_out.addWidget(self.label_out)
        self.hlayout_out.addWidget(self.radio_btn1)
        self.hlayout_out.addWidget(self.radio_btn2)
        self.hlayout_out.addWidget(self.custom_edit)
        self.hlayout_out.addWidget(self.custom_btn)
        self.hlayout_out.addStretch()  # 设置拉伸

设置时间间隔的文本输入框,使用校验器

        self.hlayout_duration = QHBoxLayout(self.switchDuration)    
        pDoubleValidator = QDoubleValidator() # 数值校验器,只能输入数字
        pDoubleValidator.setRange(0, 100) # 输入数值范围
        pDoubleValidator.setNotation(QDoubleValidator.StandardNotation)
        pDoubleValidator.setDecimals(1)  # 输入精度位
        self.duration_edit.setValidator(pDoubleValidator)  # 文本输入框加入校验器

合成按钮,设置按钮样式

        self.com_btn = QPushButton('合成gif', self)
        self.com_btn.setStyleSheet("background-color: rgb(170, 170, 255);color: white;"
                                   "border-radius: 10px;  border: 2px groove gray;")

到此界面部分就完成!


事件部分

事件处理,新定义了一个类文件,继承界面类

class Ui_Event(Ui_Form):
    def __init__(self):
        super(Ui_Event, self).__init__()
        self.initUI()
        self.radio_btn1.toggled.connect(lambda: self.radio_btn_sign(self.radio_btn1)) # 输入信息单选框事件
        self.radio_btn2.toggled.connect(lambda: self.radio_btn_sign(self.radio_btn1))
        self.add_file_btn.clicked.connect(self.open)  # 添加文件按钮事件
        self.empty_btn.clicked.connect(self.empty)    # 清空图片列表事件
        self.custom_btn.clicked.connect(self.custom)  # 自定义输出按钮事件
        self.com_btn.clicked.connect(self.complex)    # 合成按钮事件

        self.col = -1
        self.row = 0
        self.gif_file = './result.gif'
        self.img_list = []

主要是添加用户选中的图片到QGridLayout里,这边用到的是先将图片设置到100px*100px的大小(这样方便计算出QGridLayout的一行的大小,QGridLayout不超过QScrollArea的宽度,就不会出现横向的滚动条),存到QLable中,然后把QLable添加到QGridLayout的一个个网格里。

    def open(self):
        imgfiles, _ = QFileDialog.getOpenFileNames(self, 'Open file', '.', 'Image files (*.jpg *.gif *.png *.jpeg)') # 打开文件目录,设置可选择的文件类型
        photo_num = len(imgfiles)
        if photo_num != 0:
            for i in range(photo_num):
                image_id = imgfiles[i]
                pixmap = QPixmap(image_id)
                self.img_list.append(image_id)
                pixmap_change = pixmap.scaled(100, 100, Qt.KeepAspectRatio)  # 改变展示图片的大小,保持等比例缩放
                label2 = QLabel()
                label2.setPixmap(QPixmap(pixmap_change))  # 图片加到lable上
                self.addImage(label2)
            self.empty_btn.setEnabled(True)
        else:
            QMessageBox.information(self, '提示', '未选择图片')

添加图片到QGridLayout,这里还是得注意计算QGridLayout各列总宽度不要超过QScrollArea的跨度。总列数*100px(我们前面设置的一张图片的大小)

    def get_nr_of_image_columns(self):
        # 展示图片的区域,计算每排显示图片数
        scroll_area_images_width = int(self.width*0.5-20)
        return scroll_area_images_width // 100 - 2  # 除100后可能有余数,可能会超过QScrollArea的宽度,-2 尽量减少列数,保证总列数不超过QScrollArea的宽度

    def addImage(self, label2):
        nr_of_columns = self.get_nr_of_image_columns()
        self.max_columns = nr_of_columns
        if self.col < self.max_columns:
            self.col += 1
        else:
            self.col = 0
            self.row += 1
        self.gridLayout.addWidget(label2, self.row, self.col)  # 添加图片的lable到gridLayout具体行列上

清空列表,清空列表要把图片img_list也一起清空了,img_list定义的是全局变量,否则再下次合成后,会把上次加入的图片也合成进去。

    def empty(self):
        print(self.gridLayout.count())
        for i in reversed(range(self.gridLayout.count())):
            move_widget = self.gridLayout.itemAt(i).widget()
            move_widget.setParent(None)
            move_widget.deleteLater()
        self.empty_btn.setEnabled(False)
        self.col = -1
        self.row = 0
        self.img_list.clear()
        self.label_bar1.setText('准备合成:')
        self.label_bar2.setText('准备合成:')
        self.pbar.setValue(0)

合成方法就很简单了,imageio模块imread,mimsave方法搞定,注意状态栏进度条的处理。

    def create_gif(self, image_list, gif_name, duration=0.35):
        frames = []
        image_num = len(image_list)
        step_size = 500//image_num + 1
        for i, image_name in enumerate(image_list):
            frames.append(imageio.imread(image_name))  # 加入合成图片
            step_sum = step_size*(i+1)  # 进度条值计算
            max = step_sum if step_sum<500 else 500
            self.pbar.setValue(max)  # 设置进度条值
        imageio.mimsave(gif_name, frames, 'GIF', duration=duration) # 合成gif
        return

    def complex(self):
        com_duration = self.duration_edit.text()
        if self.gridLayout.count() == 0:
            QMessageBox.information(self, '提示', '未添加合成的图片')
        elif not com_duration:
            QMessageBox.information(self, '提示', '切换间隔未填写')
        else:
            self.label_bar1.setText('开始合成')
            self.label_bar2.setText('正在合成:')
            if self.radio_btn2.isChecked() == True:
                self.gif_file = self.custom_edit.text() + '/result.gif'
            if not os.path.exists(os.path.dirname(self.gif_file)):
                QMessageBox.information(self, '提示', '输出目录不存在')
            else:
                self.create_gif(self.img_list, self.gif_file, com_duration)
                self.label_bar1.setText('完成合成')
                self.label_bar2.setText('完成合成:')

整体功能完全,界面优化的地方还有很多,比如窗口最大化,控件不会自适应、界面布局太丑、输入的文件名用户自己定义……

打算后续用designer.exe重新再做过。代码可打包成exe,如何打包exe后续再整理。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容