DIP|PyQt5界面设计

一 前言

本次学习聚焦于 PyQt5—— 这是一个基于 Python 的强大 GUI(图形用户界面)框架,由 Qt 库的 Python 封装版本实现。PyQt 体系包含 超 300 个类与近 6000 个函数及方法,作为跨平台工具包,其支持 Windows、Mac OS、Linux 等主流操作系统,真正实现 “一次编码,多端运行”。

PyQt5 的核心特性解析

  1. 跨平台兼容能力
    PyQt5 具备卓越的系统适配性,可无缝运行于 Windows、Mac OS、Linux 等操作系统,开发者仅需编写一套代码,即可在不同平台上部署应用,大幅降低跨平台开发成本。

  2. 丰富的功能生态

    • 多元 UI 组件库:提供按钮、文本框、菜单、对话框、表格、树形结构等基础组件,支持复杂界面布局设计。
    • 全场景扩展支持:内置多媒体播放、数据库连接(如 SQLite、MySQL)、网络编程(TCP/IP、HTTP)、多线程处理等高级功能模块,满足从简单工具到复杂系统的开发需求。
  3. 面向对象的设计范式
    基于 Qt 的面向对象架构,PyQt5 将功能封装为模块化类(如 QWidget 界面基类、QMainWindow 主窗口类),通过继承与组合构建层次化代码结构,提升代码复用性与可维护性。

  4. 信号与槽机制
    作为 PyQt5 的核心交互机制,信号(Signal)与槽(Slot)实现了事件驱动编程:

    • 当组件触发事件(如按钮点击、文本框内容变更)时,会发出对应的信号;
    • 预先绑定的槽函数(普通 Python 函数)将自动响应信号,完成逻辑处理。
      这种解耦式设计使界面逻辑与业务逻辑分离,简化事件处理流程。
  5. 深度 Python 集成
    完美兼容 Python 语法与生态,支持与 NumPy、Pandas 等数据处理库、Matplotlib 可视化库及机器学习框架(如 TensorFlow)无缝协作,可快速开发数据可视化工具、科学计算软件等复合场景应用。

二 代码

import sys
import cv2
import numpy as np
from PyQt5.QtWidgets import (
    QApplication, QMainWindow, QWidget, QLabel, QPushButton,
    QFileDialog, QMessageBox, QVBoxLayout, QHBoxLayout, QFrame,
    QGridLayout, QCheckBox, QGroupBox, QScrollArea
)
from PyQt5.QtGui import QPixmap, QImage
from PyQt5.QtCore import Qt


class ImageProcessor(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("PyQt演示")
        self.resize(900, 800)

        # 存储原始图像和处理后的图像数据
        self.image_data = {}
        self.original_image = None

        # 创建主窗口小部件并设置布局
        main_widget = QWidget()
        self.setCentralWidget(main_widget)
        main_layout = QVBoxLayout(main_widget)

        # 设置主窗口的样式
        main_widget.setStyleSheet("""
            QWidget {
                background-color: #f0f4f8;
            }
            QLabel {
                border: 2px solid #aaa;
                border-radius: 10px;
                background-color: white;
                padding: 5px;
                box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1);
            }
            QPushButton {
                font-size: 15px;
                padding: 8px 18px;
                min-width: 100px;
            }
            QGroupBox {
                border: 1px solid #aaa;
                border-radius: 5px;
                margin-top: 10px;
            }
        """)

        # 创建顶部布局:加载按钮和保存按钮
        top_layout = QHBoxLayout()
        load_btn = QPushButton("加载图片")
        save_btn = QPushButton("保存图像")
        restore_btn = QPushButton("还原")
        load_btn.clicked.connect(self.load_image)  # 连接加载按钮的事件
        save_btn.clicked.connect(self.save_image)  # 连接保存按钮的事件
        restore_btn.clicked.connect(self.restore_image)  # 连接还原按钮的事件
        top_layout.addWidget(load_btn)
        top_layout.addWidget(save_btn)
        top_layout.addWidget(restore_btn)
        top_layout.addStretch()
        main_layout.addLayout(top_layout)
        # 添加水平分割线
        main_layout.addWidget(self._h_line())

        # 创建显示图片的 2x2 网格布局
        self.img_grid_layout = QGridLayout()
        self.image_labels = []
        for i in range(2):
            for j in range(2):
                label = QLabel()
                label.setFixedSize(400, 400)
                label.setAlignment(Qt.AlignmentFlag.AlignCenter)
                self.img_grid_layout.addWidget(label, i, j)
                self.image_labels.append(label)
        main_layout.addLayout(self.img_grid_layout)
        main_layout.addWidget(self._h_line())

        # 创建隐藏/显示图片框的复选框
        self.show_checkboxes = []
        checkbox_layout = QHBoxLayout()
        for i in range(4):
            checkbox = QCheckBox(f"显示图片框 {i + 1}")
            checkbox.setChecked(True)
            checkbox.stateChanged.connect(lambda state, idx=i: self.toggle_image_display(idx, state))
            checkbox_layout.addWidget(checkbox)
            self.show_checkboxes.append(checkbox)
        main_layout.addLayout(checkbox_layout)
        main_layout.addWidget(self._h_line())

        # 创建功能分类布局
        self.category_layout = QHBoxLayout()
        self.category_buttons = {}
        self.function_layouts = {}

        # 定义功能类别和具体功能
        categories = {
            "色彩调整": [("灰度化", "gray"), ("色调调整", "hue"), ("对比度调整", "contrast")],
            "滤波处理": [("去噪", "denoise"), ("模糊", "blur"), ("锐化", "sharpen")],
            "几何变换": [("旋转", "rotate"), ("缩放", "scale"), ("裁剪", "crop"), ("反转", "flip")],
            "形态学操作": [("腐蚀", "erode"), ("膨胀", "dilate"), ("开运算", "opening"), ("闭运算", "closing")]
        }

        for category, functions in categories.items():
            group_box = QGroupBox(category)
            function_layout = QVBoxLayout()
            self.function_layouts[category] = function_layout

            category_btn = QPushButton(f"展开 {category}")
            category_btn.clicked.connect(lambda _, cat=category: self.toggle_category(cat))
            self.category_buttons[category] = category_btn

            for text, func in functions:
                btn = QPushButton(text)
                btn.clicked.connect(lambda _, f=func: self.process(f))
                function_layout.addWidget(btn)

            group_box.setLayout(function_layout)
            self.category_layout.addWidget(category_btn)
            self.category_layout.addWidget(group_box)

        scroll_area = QScrollArea()
        scroll_area.setWidgetResizable(True)
        scroll_widget = QWidget()
        scroll_widget.setLayout(self.category_layout)
        scroll_area.setWidget(scroll_widget)

        main_layout.addWidget(scroll_area)

    def _h_line(self):
        line = QFrame()
        line.setFrameShape(QFrame.Shape.HLine)
        line.setFrameShadow(QFrame.Shadow.Sunken)
        line.setStyleSheet("color: #ccc;")
        return line

    def _v_line(self):
        line = QFrame()
        line.setFrameShape(QFrame.Shape.VLine)
        line.setFrameShadow(QFrame.Shadow.Sunken)
        line.setStyleSheet("color: #ccc;")
        return line

    def load_image(self):
        """加载图像"""
        file, _ = QFileDialog.getOpenFileName(self, "选择图片", "", "图片文件 (*.png *.jpg *.bmp)")
        if file:
            img = cv2.imread(file)
            if img is None:
                QMessageBox.warning(self, "错误", "无法加载图像")  # 显示错误消息
                return
            self.original_image = img.copy()  # 存储原始图像副本
            self.image_data['original'] = img  # 存储原始图像
            self.image_data['processed'] = img.copy()  # 存储处理后的图像(初始为原图)
            self.show_image(img, self.image_labels[0])  # 显示原图像在第一个框
            self.show_image(img, self.image_labels[1])  # 显示处理后图像(初始为原图)在第二个框

    def save_image(self):
        """保存图像"""
        if 'processed' not in self.image_data:
            QMessageBox.warning(self, "提示", "没有可保存的图像")  # 没有处理图像时提示
            return
        file, _ = QFileDialog.getSaveFileName(self, "保存图像", "", "PNG (*.png);;JPG (*.jpg)")
        if file:
            cv2.imwrite(file, self.image_data['processed'])  # 保存处理图像
            QMessageBox.information(self, "成功", f"图像已保存:{file}")  # 提示保存成功

    def process(self, mode):
        """根据选择的模式处理图像"""
        if 'original' not in self.image_data:
            QMessageBox.warning(self, "提示", "请先加载图片")  # 提示用户加载图像
            return

        img = self.image_data['processed']  # 获取上一次处理后的图像
        if mode == "gray":  # 灰度化处理
            result = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
            result = cv2.cvtColor(result, cv2.COLOR_GRAY2BGR)  # 转回三通道
        elif mode == "hue":  # 色调调整
            hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
            hsv[:, :, 0] = (hsv[:, :, 0] + 10) % 180
            result = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)
        elif mode == "contrast":  # 对比度调整
            alpha = 1.5
            beta = 0
            result = cv2.convertScaleAbs(img, alpha=alpha, beta=beta)
        elif mode == "denoise":  # 去噪处理
            result = cv2.fastNlMeansDenoisingColored(img, None, 10, 10, 7, 21)
        elif mode == "blur":  # 模糊处理
            result = cv2.GaussianBlur(img, (5, 5), 0)
        elif mode == "sharpen":  # 锐化处理
            kernel = np.array([[-1, -1, -1], [-1, 9, -1], [-1, -1, -1]])
            result = cv2.filter2D(img, -1, kernel)
        elif mode == "rotate":  # 旋转处理
            rows, cols = img.shape[:2]
            M = cv2.getRotationMatrix2D((cols / 2, rows / 2), 90, 1)
            result = cv2.warpAffine(img, M, (cols, rows))
        elif mode == "scale":  # 缩放处理
            result = cv2.resize(img, (int(img.shape[1] * 0.5), int(img.shape[0] * 0.5)))
        elif mode == "crop":  # 裁剪处理
            result = img[100:300, 100:300]  # 简单示例裁剪
        elif mode == "flip":  # 反转处理
            result = cv2.flip(img, 1)  # 水平反转
        elif mode == "erode":  # 腐蚀操作
            kernel = np.ones((5, 5), np.uint8)
            result = cv2.erode(img, kernel, iterations=1)
        elif mode == "dilate":  # 膨胀操作
            kernel = np.ones((5, 5), np.uint8)
            result = cv2.dilate(img, kernel, iterations=1)
        elif mode == "opening":  # 开运算
            kernel = np.ones((5, 5), np.uint8)
            result = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)
        elif mode == "closing":  # 闭运算
            kernel = np.ones((5, 5), np.uint8)
            result = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)
        else:
            return

        self.image_data['processed'] = result  # 存储处理后的图像
        self.show_image(result, self.image_labels[1])  # 显示处理后的图像在第二个框
        self.show_image(result, self.image_labels[2])  # 显示处理后的图像在第三个框
        self.show_image(result, self.image_labels[3])  # 显示处理后的图像在第四个框

    def restore_image(self):
        """还原图像到初始状态"""
        if self.original_image is not None:
            self.image_data['processed'] = self.original_image.copy()
            self.show_image(self.original_image, self.image_labels[1])
            self.show_image(self.original_image, self.image_labels[2])
            self.show_image(self.original_image, self.image_labels[3])

    def show_image(self, img, label):
        """显示图像"""
        rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)  # 将BGR转为RGB格式
        h, w, ch = rgb.shape
        bytes_per_line = ch * w
        q_img = QImage(rgb.data, w, h, bytes_per_line, QImage.Format.Format_RGB888)  # 转为QImage格式
        label.setPixmap(QPixmap.fromImage(q_img).scaled(400, 400, Qt.AspectRatioMode.KeepAspectRatio))  # 设置显示图像

    def toggle_image_display(self, index, state):
        """隐藏/显示图片框"""
        if state == Qt.Checked:
            self.image_labels[index].show()
        else:
            self.image_labels[index].hide()

    def toggle_category(self, category):
        """展开或收起功能类别"""
        layout = self.function_layouts[category]
        btn = self.category_buttons[category]
        if layout.count() > 0:
            for i in reversed(range(layout.count())):
                layout.itemAt(i).widget().setParent(None)
            btn.setText(f"展开 {category}")
        else:
            for i in range(len(self.function_layouts[category].children())):
                # 修正此处,使用 layout 来获取子部件
                widget = layout.itemAt(i).widget()
                layout.addWidget(widget)
            btn.setText(f"收起 {category}")


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = ImageProcessor()
    window.show()  # 显示主窗口
    sys.exit(app.exec())

三 总结

学习 PyQt5 时,掌握了核心框架与开发流程,通过创建窗口、管理布局、添加控件理解信号与槽机制,用 Qt Designer 提效。实践多线程、数据库等进阶功能,体会其扩展性,解决过布局冲突等问题,如今具备开发功能丰富、界面美观的桌面应用能力。 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值