Как сделать светофор действий выполняемого приложения?

Приложение, условно, имеет три шага действий:

  1. Выбор программ;
  2. Процесс установки;
  3. Завершение.

При выполнении каждого этапа, об этом сигнализируется выделением фона соответствующей метки (step1_label, step2_label, step3_label) красным цветом.

После выбора программ, для перехода ко второму шагу "Процесс установки"
необходимо нажать кнопку "Далее" и начинается процесс установки программ.
Этот процесс должен сопровождаться визуализацией, т.е. метка "2. Процесс установки" должна окрасится красным цветом, но этого не происходит.
Пожалуйста, помогите разобраться почему так получается?

main.py

import sys
import shutil
import os
import subprocess
from PyQt5.QtWidgets import (
    QApplication, QMainWindow, QLabel, QVBoxLayout, QPushButton,
    QWidget, QScrollArea, QHBoxLayout, QLineEdit, QGroupBox, 
    QFileDialog, QCheckBox, QSizePolicy, QSpacerItem, QStackedWidget)
from PyQt5.QtCore import Qt, QThread, pyqtSignal
from PyQt5.QtGui import QIcon


class InstallThread(QThread):
    completed = pyqtSignal(str)

    def __init__(self, installer_path, args):
        super().__init__()
        self.installer_path = installer_path
        self.args = args

    def run(self):
        try:
            # Выполняем установку в тихом режиме
            subprocess.run([self.installer_path] + self.args, check=True)
            self.completed.emit("Установка завершена")
        except subprocess.CalledProcessError as e:
            self.completed.emit(f"Ошибка установки: {e}")


class InstallerApp(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle('Установщик программ')
        self.setFixedSize(1100, 1100)  # Фиксированный размер окна
        self.setWindowIcon(QIcon(r'C:\Users\366abc\Documents\MultiSetup\icon\logo.png'))  # Иконка программы
        self.installation_path = "C:\\Program Files"  # Путь по умолчанию

        # Основная компоновка
        main_layout = QVBoxLayout()
        central_widget = QWidget()
        central_widget.setLayout(main_layout)
        self.setCentralWidget(central_widget)

        # Создание стека вкладок
        self.stacked_widget = QStackedWidget()
        main_layout.addWidget(self.stacked_widget)

        # Добавляем первую вкладку — выбор программ
        self.create_program_selection_tab()
        # Вторая вкладка — процесс установки
        self.create_installation_tab()
        # Третья вкладка — завершение установки
        self.create_completion_tab()

        # Добавляем вкладки в QStackedWidget
        self.stacked_widget.addWidget(self.program_selection_tab)
        self.stacked_widget.addWidget(self.installation_tab)
        self.stacked_widget.addWidget(self.completion_tab)

        # По умолчанию открыта первая вкладка
        self.stacked_widget.setCurrentIndex(0)

    def create_program_selection_tab(self):
        # Создаем первую вкладку — выбор программ
        self.program_selection_tab = QWidget()
        layout = QVBoxLayout(self.program_selection_tab)

        # Верхний текст и строка поиска
        header_layout = QHBoxLayout()
    
        self.header_label = QLabel('Чтобы продолжить, нажмите "Далее"')
        header_layout.addWidget(self.header_label)

        # Добавляем растяжку, чтобы переместить строку поиска в правый угол
        header_layout.addStretch()

        # Создаем строку поиска
        self.search_bar = QLineEdit()
        self.search_bar.setPlaceholderText("Поиск программ...")
        self.search_bar.setFixedWidth(300)  # Устанавливаем фиксированную ширину для поиска
        self.search_bar.textChanged.connect(self.filter_programs)
        header_layout.addWidget(self.search_bar)

        layout.addLayout(header_layout)

        # Индикатор вкладок (перемещен под текст)
        self.create_tab_indicator(layout)

        # Программы
        self.program_group = QGroupBox()
        self.program_group.setStyleSheet("QGroupBox { padding: 10px; margin: 0px; }")  # Уменьшаем padding и margin
        self.program_layout = QVBoxLayout()
        self.program_layout.setContentsMargins(0, 0, 0, 0)  # Убираем рамки
        self.program_layout.setSpacing(5)  # Уменьшаем расстояние между элементами
        self.program_group.setLayout(self.program_layout)
        self.program_layout.insertStretch(99, 1)

        self.all_programs = [
            ('Opera Browser', 'Новая версия самого быстрого браузера со встроенным VPN', 'Интернет', '2.45 MB'),
            ('Google Chrome', 'Новый браузер от Google, быстрая и безопасная работа', 'Интернет', '3.25 MB'),
            ('Mozilla Firefox', 'Открытый и бесплатный браузер с множеством возможностей', 'Интернет', '2.75 MB'),
            ('Microsoft Edge', 'Браузер от Microsoft с интеграцией с Windows', 'Интернет', '2.90 MB'),
            ('VLC Media Player', 'Проигрыватель мультимедиа для всех форматов', 'Мультимедиа', '4.15 MB'),
            ('7-Zip', 'Архиватор для работы с файлами', 'Система', '3.36 MB'),
            ('Notepad++', 'Продвинутый текстовый редактор для разработчиков', 'Разработка', '1.65 MB'),
            ('Skype', 'Программа для видеозвонков и сообщений', 'Коммуникации', '1.85 MB'),
            ('Spotify', 'Платформа для прослушивания музыки', 'Мультимедиа', '3.50 MB'),
            ('Visual Studio Code', 'Редактор кода от Microsoft для разработчиков', 'Разработка', '2.95 MB'),
            ('Google Chrome Setup', 'Установщик Google Chrome', 'Интернет', '10.0 MB'),
        ]

        # Добавляем пути к установщикам на сервере
        self.installers = {
            'Opera Browser': r'\\10.10.10.190\it\36602\Program\OperaSetup.exe',
            'Google Chrome': r'\\10.10.10.190\it\36602\Program\ChromeSetup.exe',  # MSI файл для тихой установки
            'Mozilla Firefox': r'\\10.10.10.190\it\36602\Program\FirefoxSetup.exe',
            'Microsoft Edge': r'\\10.10.10.190\it\36602\Program\EdgeSetup.exe',
            'VLC Media Player': r'\\10.10.10.190\it\36602\Program\VLCSetup.exe',
            '7-Zip': r'\\10.10.10.190\it\36602\Program\7zipSetup.exe',
            'Notepad++': r'\\10.10.10.190\it\36602\Program\Notepad++Setup.exe',
            'Skype': r'\\10.10.10.190\it\36602\Program\SkypeSetup.exe',
            'Spotify': r'\\10.10.10.190\it\36602\Program\SpotifySetup.exe',
            'Visual Studio Code': r'\\10.10.10.190\it\36602\Program\VSCodeSetup.exe',
            'Google Chrome Setup': r'\\10.10.10.190\it\36602\Program\ChromeSetup.msi'
        }

        self.programs = self.all_programs.copy()
        self.check_boxes = []
        self.create_program_list()

        self.scroll_area = QScrollArea()
        self.scroll_area.setWidgetResizable(True)
        self.scroll_area.setWidget(self.program_group)
        layout.addWidget(self.scroll_area)

        bottom_layout = QHBoxLayout()
        left_side_layout = QVBoxLayout()
        self.selected_size_label = QLabel('Выбрано для установки: 0 программ (0 MB)')
        self.selected_size_label.setFixedWidth(500)
        self.disk_space_label = QLabel('Доступно на диске: 36 ГБ')
        left_side_layout.addWidget(self.selected_size_label)
        left_side_layout.addWidget(self.disk_space_label)

        center_side_layout = QVBoxLayout()
        path_title_label = QLabel('Путь установки:')
        center_side_layout.addWidget(path_title_label, alignment=Qt.AlignLeft)
        self.installation_path_label = QLabel(self.format_path(self.installation_path))
        center_side_layout.addWidget(self.installation_path_label, alignment=Qt.AlignLeft)

        right_side_layout = QVBoxLayout()
        self.clear_selection_button = QPushButton('Очистить выбор программ')
        self.clear_selection_button.clicked.connect(self.clear_program_selection)
        self.install_path_button = QPushButton('Выбрать путь установки')
        self.install_path_button.clicked.connect(self.select_installation_path)
        right_side_layout.addWidget(self.clear_selection_button)
        right_side_layout.addWidget(self.install_path_button)

        bottom_layout.addLayout(left_side_layout)
        bottom_layout.addStretch()
        bottom_layout.addLayout(center_side_layout)
        bottom_layout.addStretch()
        bottom_layout.addLayout(right_side_layout)

        layout.addLayout(bottom_layout)

        self.next_button = QPushButton('Далее')
        self.next_button.clicked.connect(self.go_to_installation_tab)
        layout.addWidget(self.next_button)

    def filter_programs(self, search_text):
        search_text = search_text.lower()
        if search_text:
            # Фильтруем программы только по названию и проверяем, начинается ли название с введенного текста
            self.programs = [
                (name, description, category, size) for name, description, category, size in self.all_programs
                if name.lower().startswith(search_text)
            ]
        else:
            # Если текст пустой, показываем все программы
            self.programs = self.all_programs.copy()

        # Пересоздаем список программ после фильтрации
        self.create_program_list()
        self.program_group.adjustSize()
        self.scroll_area.verticalScrollBar().setValue(self.scroll_area.verticalScrollBar().minimum())

    def create_tab_indicator(self, layout):
        # Индикатор шагов
        steps_layout = QHBoxLayout()

        self.step1_label = QLabel('1. Выбор программ')
        self.step1_label.setAlignment(Qt.AlignCenter)
        self.step1_label.setStyleSheet("""
            background-color: red;
            color: white;
            padding: 10px;
            font-size: 14px;
            min-width: 150px;
        """)

        self.step2_label = QLabel('2. Процесс установки')
        self.step2_label.setAlignment(Qt.AlignCenter)
        self.step2_label.setStyleSheet("""
            background-color: lightgray;
            color: black;
            padding: 10px;
            font-size: 14px;
            min-width: 150px;
        """)

        self.step3_label = QLabel('3. Завершение')
        self.step3_label.setAlignment(Qt.AlignCenter)
        self.step3_label.setStyleSheet("""
            background-color: lightgray;
            color: black;
            padding: 10px;
            font-size: 14px;
            min-width: 150px;
        """)

        # Добавляем вкладки в горизонтальный компоновщик
        steps_layout.addWidget(self.step1_label)
        steps_layout.addWidget(self.step2_label)
        steps_layout.addWidget(self.step3_label)

        layout.addLayout(steps_layout)

    def update_tab_indicator(self, step):
        if step == 1:
            self.step1_label.setStyleSheet("background-color: red; color: white; padding: 10px;")
            self.step2_label.setStyleSheet("background-color: lightgray; color: black; padding: 10px;")
            self.step3_label.setStyleSheet("background-color: lightgray; color: black; padding: 10px;")
        elif step == 2:
            self.step1_label.setStyleSheet("background-color: lightgray; color: black; padding: 10px;")
            self.step2_label.setStyleSheet("background-color: red; color: white; padding: 10px;")
            self.step3_label.setStyleSheet("background-color: lightgray; color: black; padding: 10px;")
        elif step == 3:
            self.step1_label.setStyleSheet("background-color: lightgray; color: black; padding: 10px;")
            self.step2_label.setStyleSheet("background-color: lightgray; color: black; padding: 10px;")
            self.step3_label.setStyleSheet("background-color: red; color: white; padding: 10px;")

    def create_installation_tab(self):
        # Создаем вторую вкладку — процесс установки (такую же, как первая)
        self.installation_tab = QWidget()
        layout = QVBoxLayout(self.installation_tab)

        # Верхний текст и строка поиска (вместо строки поиска можно добавить описание процесса)
        header_layout = QHBoxLayout()
    
        self.header_label_install = QLabel('Идет процесс установки программ...')
        header_layout.addWidget(self.header_label_install)

        # Добавляем растяжку
        header_layout.addStretch()

        layout.addLayout(header_layout)

        # Индикатор вкладок
        self.create_tab_indicator(layout)

        # Прогресс процесса установки
        self.installation_group = QGroupBox()
        self.installation_group.setStyleSheet("QGroupBox { padding: 10px; margin: 0px; }")
        self.installation_layout = QVBoxLayout()
        self.installation_layout.setContentsMargins(0, 0, 0, 0)
        self.installation_layout.setSpacing(5)
        self.installation_group.setLayout(self.installation_layout)

        self.scroll_area_install = QScrollArea()
        self.scroll_area_install.setWidgetResizable(True)
        self.scroll_area_install.setWidget(self.installation_group)
        layout.addWidget(self.scroll_area_install)

        # Нижняя часть с кнопкой "Назад"
        bottom_layout = QHBoxLayout()
        self.back_button = QPushButton('Назад')
        self.back_button.clicked.connect(self.go_to_program_selection_tab)
        bottom_layout.addStretch()
        bottom_layout.addWidget(self.back_button)

        layout.addLayout(bottom_layout)

    def create_completion_tab(self):
        # Создаем третью вкладку — завершение установки
        self.completion_tab = QWidget()
        layout = QVBoxLayout(self.completion_tab)

        # Индикатор шагов (добавляем его и на эту вкладку)
        self.create_tab_indicator(layout)

        self.completion_label = QLabel('Установка завершена!')
        layout.addWidget(self.completion_label)

        self.finish_button = QPushButton('Завершить')
        self.finish_button.clicked.connect(self.close)
        layout.addWidget(self.finish_button)

    def go_to_installation_tab(self):
        # Сначала обновляем индикатор на второй шаг
        self.update_tab_indicator(2)
        # Переключаемся на вкладку процесса установки
        self.stacked_widget.setCurrentIndex(1)
        # Начинаем процесс установки программ
        self.install_programs()

    def go_to_program_selection_tab(self):
        # Возврат на вкладку выбора программ
        self.update_tab_indicator(1)
        self.stacked_widget.setCurrentIndex(0)

    def go_to_completion_tab(self):
        # Переход на вкладку завершения установки
        self.update_tab_indicator(3)
        self.stacked_widget.setCurrentIndex(2)

    def format_path(self, path):
        if len(path) > 20:
            return f"...\\{os.path.basename(path)}"
        return path

    def create_program_list(self):
        self.check_boxes.clear()

        for i in reversed(range(self.program_layout.count())):
            widget = self.program_layout.itemAt(i).widget()
            if widget:
                widget.deleteLater()

        if not self.programs:
            empty_label = QLabel("Программы не найдены")
            self.program_layout.addWidget(empty_label, alignment=Qt.AlignCenter)
        else:
            for i, (name, description, category, size) in enumerate(self.programs):
                row_widget = QWidget()
                row_layout = QHBoxLayout(row_widget)
                row_layout.setContentsMargins(0, 5, 0, 5)
                row_layout.setSpacing(10)
                row_layout.setAlignment(Qt.AlignVCenter)

                program_layout_widget = QWidget()
                program_layout_vbox = QVBoxLayout(program_layout_widget)
                program_layout_vbox.setContentsMargins(0, 0, 0, 0)
                program_layout_vbox.setSpacing(2)

                check_box = QCheckBox(name)
                check_box.setFixedWidth(200)

                desc_label = QLabel(description)
                desc_label.setStyleSheet("color: gray;")
                desc_label.setWordWrap(True)
                desc_label.setFixedWidth(650)

                program_layout_vbox.addWidget(check_box, alignment=Qt.AlignLeft)
                program_layout_vbox.addWidget(desc_label, alignment=Qt.AlignLeft)

                row_layout.addWidget(program_layout_widget)

                category_size_layout = QVBoxLayout()
                category_size_layout.setContentsMargins(0, 0, 0, 0)
                category_size_layout.setSpacing(2)

                category_label = QLabel(category)
                category_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)

                size_label = QLabel(size)
                size_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)

                category_size_layout.addWidget(category_label)
                category_size_layout.addWidget(size_label)

                spacer = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
                row_layout.addItem(spacer)
                row_layout.addLayout(category_size_layout)

                self.program_layout.insertWidget(i, row_widget)

                self.check_boxes.append(check_box)

                check_box.stateChanged.connect(self.update_selected_size)
                check_box.stateChanged.connect(self.update_header_label)

        self.program_group.adjustSize()

    def update_selected_size(self):
        total_size = 0
        selected_count = 0
        for check_box, (_, _, _, size) in zip(self.check_boxes, self.programs):
            if check_box.isChecked():
                selected_count += 1
                size_value, size_unit = size.split()
                size_in_mb = float(size_value)
                if size_unit == 'KB':
                    size_in_mb /= 1024
                total_size += size_in_mb

        if selected_count > 0:
            self.selected_size_label.setText(f'Выбрано для установки: {selected_count} программ ({total_size:.2f} MB)')
        else:
            self.selected_size_label.setText('Выбрано для установки: 0 программ (0 MB)')

    def update_header_label(self):
        if any(cb.isChecked() for cb in self.check_boxes):
            self.header_label.setText('Чтобы продолжить, нажмите "Далее"')
        else:
            self.header_label.setText('Выберите желаемые программы и нажмите "Далее"')

    def select_installation_path(self):
        path = QFileDialog.getExistingDirectory(self, "Выберите папку для установки")
        if path:
            self.installation_path = path
            self.installation_path_label.setText(self.format_path(self.installation_path))
            self.update_disk_space_label()

    def update_disk_space_label(self):
        try:
            # Получаем информацию о диске по выбранному пути установки
            total, used, free = shutil.disk_usage(self.installation_path)
            free_gb = free // (2**30)
            self.disk_space_label.setText(f'Доступно на диске: {free_gb} ГБ')
        except Exception as e:
            self.disk_space_label.setText('Ошибка получения информации о диске')
            print(f"Ошибка: {e}")

    def clear_program_selection(self):
        if any(check_box.isChecked() for check_box in self.check_boxes):
            for check_box in self.check_boxes:
                check_box.setChecked(False)
            self.update_selected_size()
            self.update_header_label()
        else:
            self.selected_size_label.setText('Выбрано для установки: 0 программ (0 MB)')
            self.header_label.setText('Выберите желаемые программы и нажмите "Далее"')

    def install_programs(self):
        for check_box, (name, description, category, size) in zip(self.check_boxes, self.programs):
            if check_box.isChecked():
                installer_path = self.installers.get(name)
                if installer_path:
                    # Если устанавливаем Google Chrome через MSI пакет
                    if name == 'Google Chrome Setup':
                        args = ["/qn"]  # Тихая установка MSI пакета
                        self.install_thread = InstallThread(installer_path, args)
                        self.install_thread.completed.connect(self.on_installation_complete)
                        self.install_thread.start()
                    else:
                        # Другие программы можно установить по аналогии
                        subprocess.run([installer_path], check=True)

    def on_installation_complete(self, message):
        self.go_to_completion_tab()  # Переходим на вкладку завершения установки
        print(message)  # Отображаем сообщение об окончании установки


# Запуск приложения
if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = InstallerApp()
    window.show()
    sys.exit(app.exec_())

Ответы (1 шт):

Автор решения: S. Nick

Проблема в том, что лабелы step1_label, step2_label, step3_label должны быть новыми объектами для каждой вкладки, а у вас они одни и те же для всех вкладок.

import sys
import shutil
import os
import subprocess
from PyQt5.QtWidgets import (
    QApplication, QMainWindow, QLabel, QVBoxLayout, QPushButton,
    QWidget, QScrollArea, QHBoxLayout, QLineEdit, QGroupBox, 
    QFileDialog, QCheckBox, QSizePolicy, QSpacerItem, QStackedWidget)
from PyQt5.QtCore import Qt, QThread, pyqtSignal
from PyQt5.QtGui import QIcon


class InstallThread(QThread):
    completed = pyqtSignal(str)

    def __init__(self, installer_path, args):
        super().__init__()
        self.installer_path = installer_path
        self.args = args

    def run(self):
        try:
            # Выполняем установку в тихом режиме
            subprocess.run([self.installer_path] + self.args, check=True)
            self.completed.emit("Установка завершена")
        except subprocess.CalledProcessError as e:
            self.completed.emit(f"Ошибка установки: {e}")


class InstallerApp(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle('Установщик программ')
        self.setFixedSize(1100, 1100)           # Фиксированный размер окна
        self.setWindowIcon(QIcon('im.png'))     # Иконка программы
        self.installation_path = "C:\\Program Files"  # C:\\Program Files

# !!! +++
        self._tab_indicator = {}                         # !!! +++

        # Основная компоновка
        central_widget = QWidget()
        main_layout = QVBoxLayout()
        central_widget.setLayout(main_layout)
        self.setCentralWidget(central_widget)

        # Создание стека вкладок
        self.stacked_widget = QStackedWidget()
        main_layout.addWidget(self.stacked_widget)

        # Добавляем первую вкладку — выбор программ
        self.create_program_selection_tab()
        # Вторая вкладка — процесс установки
        self.create_installation_tab()
        # Третья вкладка — завершение установки
        self.create_completion_tab()

        # Добавляем вкладки в QStackedWidget
        self.stacked_widget.addWidget(self.program_selection_tab)
        self.stacked_widget.addWidget(self.installation_tab)
        self.stacked_widget.addWidget(self.completion_tab)

        # По умолчанию открыта первая вкладка
        self.stacked_widget.setCurrentIndex(0)
# +++
        self.update_tab_indicator(1)                          # +++

    def create_program_selection_tab(self):
        self.program_selection_tab = QWidget()               
        layout = QVBoxLayout(self.program_selection_tab)

        # Верхний текст и строка поиска
        header_layout = QHBoxLayout()
    
        self.header_label = QLabel('Чтобы продолжить, нажмите "Далее"')
        header_layout.addWidget(self.header_label)
        # Добавляем растяжку, чтобы переместить строку поиска в правый угол
        header_layout.addStretch()

        # Создаем строку поиска
        self.search_bar = QLineEdit()
        self.search_bar.setPlaceholderText("Поиск программ...")
        self.search_bar.setFixedWidth(300)  # Устанавливаем фиксированную ширину для поиска
        self.search_bar.textChanged.connect(self.filter_programs)
        header_layout.addWidget(self.search_bar)
        layout.addLayout(header_layout)

        # Индикатор вкладок (перемещен под текст)
# +++ ----------------------------------> v <---- первую вкладку      
        self.create_tab_indicator(layout, 1)

        # Программы
        self.program_group = QGroupBox()
        self.program_group.setStyleSheet("QGroupBox { padding: 10px; margin: 0px; }")  # Уменьшаем padding и margin
        self.program_layout = QVBoxLayout()
        self.program_layout.setContentsMargins(0, 0, 0, 0)  # Убираем рамки
        self.program_layout.setSpacing(5)  # Уменьшаем расстояние между элементами
        self.program_group.setLayout(self.program_layout)
        self.program_layout.insertStretch(99, 1)

        self.all_programs = [
            ('Opera Browser', 'Новая версия самого быстрого браузера со встроенным VPN', 'Интернет', '2.45 MB'),
            ('Google Chrome', 'Новый браузер от Google, быстрая и безопасная работа', 'Интернет', '3.25 MB'),
            ('Mozilla Firefox', 'Открытый и бесплатный браузер с множеством возможностей', 'Интернет', '2.75 MB'),
            ('Microsoft Edge', 'Браузер от Microsoft с интеграцией с Windows', 'Интернет', '2.90 MB'),
            ('VLC Media Player', 'Проигрыватель мультимедиа для всех форматов', 'Мультимедиа', '4.15 MB'),
            ('7-Zip', 'Архиватор для работы с файлами', 'Система', '3.36 MB'),
            ('Notepad++', 'Продвинутый текстовый редактор для разработчиков', 'Разработка', '1.65 MB'),
            ('Skype', 'Программа для видеозвонков и сообщений', 'Коммуникации', '1.85 MB'),
            ('Spotify', 'Платформа для прослушивания музыки', 'Мультимедиа', '3.50 MB'),
            ('Visual Studio Code', 'Редактор кода от Microsoft для разработчиков', 'Разработка', '2.95 MB'),
            ('Google Chrome Setup', 'Установщик Google Chrome', 'Интернет', '10.0 MB'),
        ]

        # Добавляем пути к установщикам на сервере
        self.installers = {
            'Opera Browser': r'\\10.10.10.190\it\36602\Program\OperaSetup.exe',
            'Google Chrome': r'\\10.10.10.190\it\36602\Program\ChromeSetup.exe',  # MSI файл для тихой установки
            'Mozilla Firefox': r'\\10.10.10.190\it\36602\Program\FirefoxSetup.exe',
            'Microsoft Edge': r'\\10.10.10.190\it\36602\Program\EdgeSetup.exe',
            'VLC Media Player': r'\\10.10.10.190\it\36602\Program\VLCSetup.exe',
            '7-Zip': r'\\10.10.10.190\it\36602\Program\7zipSetup.exe',
            'Notepad++': r'\\10.10.10.190\it\36602\Program\Notepad++Setup.exe',
            'Skype': r'\\10.10.10.190\it\36602\Program\SkypeSetup.exe',
            'Spotify': r'\\10.10.10.190\it\36602\Program\SpotifySetup.exe',
            'Visual Studio Code': r'\\10.10.10.190\it\36602\Program\VSCodeSetup.exe',
            'Google Chrome Setup': r'\\10.10.10.190\it\36602\Program\ChromeSetup.msi'
        }

        self.programs = self.all_programs.copy()
        self.check_boxes = []
        self.create_program_list()

        self.scroll_area = QScrollArea()
        self.scroll_area.setWidgetResizable(True)
        self.scroll_area.setWidget(self.program_group)
        layout.addWidget(self.scroll_area)

        bottom_layout = QHBoxLayout()
        left_side_layout = QVBoxLayout()
        self.selected_size_label = QLabel('Выбрано для установки: 0 программ (0 MB)')
        self.selected_size_label.setFixedWidth(500)
        self.disk_space_label = QLabel('Доступно на диске: 36 ГБ')
        left_side_layout.addWidget(self.selected_size_label)
        left_side_layout.addWidget(self.disk_space_label)

        center_side_layout = QVBoxLayout()
        path_title_label = QLabel('Путь установки:')
        center_side_layout.addWidget(path_title_label, alignment=Qt.AlignLeft)
        self.installation_path_label = QLabel(self.format_path(self.installation_path))
        center_side_layout.addWidget(self.installation_path_label, alignment=Qt.AlignLeft)

        right_side_layout = QVBoxLayout()
        self.clear_selection_button = QPushButton('Очистить выбор программ')
        self.clear_selection_button.clicked.connect(self.clear_program_selection)
        self.install_path_button = QPushButton('Выбрать путь установки')
        self.install_path_button.clicked.connect(self.select_installation_path)
        right_side_layout.addWidget(self.clear_selection_button)
        right_side_layout.addWidget(self.install_path_button)

        bottom_layout.addLayout(left_side_layout)
        bottom_layout.addStretch()
        bottom_layout.addLayout(center_side_layout)
        bottom_layout.addStretch()
        bottom_layout.addLayout(right_side_layout)
        layout.addLayout(bottom_layout)

        self.next_button = QPushButton('Далее')
        self.next_button.clicked.connect(self.go_to_installation_tab)
        layout.addWidget(self.next_button)

    def filter_programs(self, search_text):
        search_text = search_text.lower()
        if search_text:
            # Фильтруем программы только по названию и проверяем, начинается ли название с введенного текста
            self.programs = [
                (name, description, category, size) for name, description, category, size in self.all_programs
                if name.lower().startswith(search_text)
            ]
        else:
            # Если текст пустой, показываем все программы
            self.programs = self.all_programs.copy()

        # Пересоздаем список программ после фильтрации
        self.create_program_list()
        self.program_group.adjustSize()
        self.scroll_area.verticalScrollBar().setValue(self.scroll_area.verticalScrollBar().minimum())

# +++ -----------------------------------> vvv <---- номер вкладки
    def create_tab_indicator(self, layout, ind):
        # Индикатор шагов
        steps_layout = QHBoxLayout()

# !!!
# ----> vvvv <--- step1(2, 3)_label это новые объекты для каждой вкладки
#       self.step1_label = QLabel('1. Выбор программ')

        step1_label = QLabel('1. Выбор программ')
        step1_label.setObjectName('step1_label')
        step1_label.setAlignment(Qt.AlignCenter)
        step1_label.setStyleSheet("""
            background-color: red;
            color: white;
            padding: 10px;
            font-size: 14px;
            min-width: 150px;
        """)
        step2_label = QLabel('2. Процесс установки')
        step2_label.setObjectName('step2_label')
        step2_label.setAlignment(Qt.AlignCenter)
        step2_label.setStyleSheet("""
            background-color: blue;
            color: black;
            padding: 10px;
            font-size: 14px;
            min-width: 150px;
        """)
        step3_label = QLabel('3. Завершение')
        step3_label.setObjectName('step3_label')
        step3_label.setAlignment(Qt.AlignCenter)
        step3_label.setStyleSheet("""
            background-color: lightgray;
            color: black;
            padding: 10px;
            font-size: 14px;
            min-width: 150px;
        """)

        # Добавляем вкладки в горизонтальный компоновщик
        steps_layout.addWidget(step1_label)
        steps_layout.addWidget(step2_label)
        steps_layout.addWidget(step3_label)

# !!! +++ сохраняем в словарь для применения к ним setStyleSheet
        self._tab_indicator[ind] = [step1_label, step2_label, step3_label]

        layout.addLayout(steps_layout)

    def update_tab_indicator(self, step):
# !!! +++ достаем из словаря и применяем к ним setStyleSheet
        step1_label, step2_label, step3_label = self._tab_indicator[step]
        
        if step == 1:
            step1_label.setStyleSheet("background-color: red; color: white; padding: 10px;")
            step2_label.setStyleSheet("background-color: lightgray; color: black; padding: 10px;")
            step3_label.setStyleSheet("background-color: lightgray; color: black; padding: 10px;")
        elif step == 2:
            step1_label.setStyleSheet("background-color: lightgray; color: black; padding: 10px;")
            step2_label.setStyleSheet("background-color: red; color: white; padding: 10px;")
            step3_label.setStyleSheet("background-color: lightgray; color: black; padding: 10px;")
        elif step == 3:
            step1_label.setStyleSheet("background-color: lightgray; color: black; padding: 10px;")
            step2_label.setStyleSheet("background-color: lightgray; color: black; padding: 10px;")
            step3_label.setStyleSheet("background-color: red; color: white; padding: 10px;")

    def create_installation_tab(self):
        # Создаем вторую вкладку — процесс установки (такую же, как первая)
        self.installation_tab = QWidget()
        layout = QVBoxLayout(self.installation_tab)

        # Верхний текст и строка поиска (вместо строки поиска можно добавить описание процесса)
        header_layout = QHBoxLayout()
    
        self.header_label_install = QLabel('Идет процесс установки программ...')
        header_layout.addWidget(self.header_label_install)
        # Добавляем растяжку
        header_layout.addStretch()
        layout.addLayout(header_layout)

        # Индикатор вкладок
# +++ ----------------------------------> v <---- вторую вкладку
        self.create_tab_indicator(layout, 2)

        # Прогресс процесса установки
        self.installation_group = QGroupBox()
        self.installation_group.setStyleSheet("QGroupBox { padding: 10px; margin: 0px; }")
        self.installation_layout = QVBoxLayout()
        self.installation_layout.setContentsMargins(0, 0, 0, 0)
        self.installation_layout.setSpacing(5)
        self.installation_group.setLayout(self.installation_layout)

        self.scroll_area_install = QScrollArea()
        self.scroll_area_install.setWidgetResizable(True)
        self.scroll_area_install.setWidget(self.installation_group)
        layout.addWidget(self.scroll_area_install)

        # Нижняя часть с кнопкой "Назад"
        bottom_layout = QHBoxLayout()
        self.back_button = QPushButton('Назад')
        self.back_button.clicked.connect(self.go_to_program_selection_tab)
        bottom_layout.addStretch()
        bottom_layout.addWidget(self.back_button)
        layout.addLayout(bottom_layout)

    def create_completion_tab(self):
        # Создаем третью вкладку — завершение установки
        self.completion_tab = QWidget()
        layout = QVBoxLayout(self.completion_tab)

        # Индикатор шагов (добавляем его и на эту вкладку)
# +++ ----------------------------------> v <---- третию вкладку
        self.create_tab_indicator(layout, 3)

        self.completion_label = QLabel('Установка завершена!')
        layout.addWidget(self.completion_label)

        self.finish_button = QPushButton('Завершить')
        self.finish_button.clicked.connect(self.close)
        layout.addWidget(self.finish_button)

    def go_to_installation_tab(self):
        # Сначала обновляем индикатор на второй шаг
        self.update_tab_indicator(2)

        # Переключаемся на вкладку процесса установки
        self.stacked_widget.setCurrentIndex(1)
        # Начинаем процесс установки программ
        self.install_programs()
 
    def go_to_program_selection_tab(self):
        # Возврат на вкладку выбора программ
        self.update_tab_indicator(1)
        self.stacked_widget.setCurrentIndex(0)

    def go_to_completion_tab(self):
        # Переход на вкладку завершения установки
        self.update_tab_indicator(3)
        self.stacked_widget.setCurrentIndex(2)

    def format_path(self, path):
        if len(path) > 20:
            return f"...\\{os.path.basename(path)}"
        return path

    def create_program_list(self):
        self.check_boxes.clear()

        for i in reversed(range(self.program_layout.count())):
            widget = self.program_layout.itemAt(i).widget()
            if widget:
                widget.deleteLater()

        if not self.programs:
            empty_label = QLabel("Программы не найдены")
            self.program_layout.addWidget(empty_label, alignment=Qt.AlignCenter)
        else:
            for i, (name, description, category, size) in enumerate(self.programs):
                row_widget = QWidget()
                row_layout = QHBoxLayout(row_widget)
                row_layout.setContentsMargins(0, 5, 0, 5)
                row_layout.setSpacing(10)
                row_layout.setAlignment(Qt.AlignVCenter)

                program_layout_widget = QWidget()
                program_layout_vbox = QVBoxLayout(program_layout_widget)
                program_layout_vbox.setContentsMargins(0, 0, 0, 0)
                program_layout_vbox.setSpacing(2)

                check_box = QCheckBox(name)
                check_box.setFixedWidth(200)

                desc_label = QLabel(description)
                desc_label.setStyleSheet("color: gray;")
                desc_label.setWordWrap(True)
                desc_label.setFixedWidth(650)

                program_layout_vbox.addWidget(check_box, alignment=Qt.AlignLeft)
                program_layout_vbox.addWidget(desc_label, alignment=Qt.AlignLeft)

                row_layout.addWidget(program_layout_widget)

                category_size_layout = QVBoxLayout()
                category_size_layout.setContentsMargins(0, 0, 0, 0)
                category_size_layout.setSpacing(2)

                category_label = QLabel(category)
                category_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)

                size_label = QLabel(size)
                size_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)

                category_size_layout.addWidget(category_label)
                category_size_layout.addWidget(size_label)

                spacer = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
                row_layout.addItem(spacer)
                row_layout.addLayout(category_size_layout)

                self.program_layout.insertWidget(i, row_widget)

                self.check_boxes.append(check_box)

                check_box.stateChanged.connect(self.update_selected_size)
                check_box.stateChanged.connect(self.update_header_label)

        self.program_group.adjustSize()

    def update_selected_size(self):
        total_size = 0
        selected_count = 0
        for check_box, (_, _, _, size) in zip(self.check_boxes, self.programs):
            if check_box.isChecked():
                selected_count += 1
                size_value, size_unit = size.split()
                size_in_mb = float(size_value)
                if size_unit == 'KB':
                    size_in_mb /= 1024
                total_size += size_in_mb

        if selected_count > 0:
            self.selected_size_label.setText(f'Выбрано для установки: {selected_count} программ ({total_size:.2f} MB)')
        else:
            self.selected_size_label.setText('Выбрано для установки: 0 программ (0 MB)')

    def update_header_label(self):
        if any(cb.isChecked() for cb in self.check_boxes):
            self.header_label.setText('Чтобы продолжить, нажмите "Далее"')
        else:
            self.header_label.setText('Выберите желаемые программы и нажмите "Далее"')

    def select_installation_path(self):
        path = QFileDialog.getExistingDirectory(self, "Выберите папку для установки")
        if path:
            self.installation_path = path
            self.installation_path_label.setText(self.format_path(self.installation_path))
            self.update_disk_space_label()

    def update_disk_space_label(self):
        try:
            # Получаем информацию о диске по выбранному пути установки
            total, used, free = shutil.disk_usage(self.installation_path)
            free_gb = free // (2**30)
            self.disk_space_label.setText(f'Доступно на диске: {free_gb} ГБ')
        except Exception as e:
            self.disk_space_label.setText('Ошибка получения информации о диске')
            print(f"Ошибка: {e}")

    def clear_program_selection(self):
        if any(check_box.isChecked() for check_box in self.check_boxes):
            for check_box in self.check_boxes:
                check_box.setChecked(False)
            self.update_selected_size()
            self.update_header_label()
        else:
            self.selected_size_label.setText('Выбрано для установки: 0 программ (0 MB)')
            self.header_label.setText('Выберите желаемые программы и нажмите "Далее"')

    def install_programs(self):
        for check_box, (name, description, category, size) in zip(self.check_boxes, self.programs):
            if check_box.isChecked():
                installer_path = self.installers.get(name)
                if installer_path:
                    # Если устанавливаем Google Chrome через MSI пакет
                    if name == 'Google Chrome Setup':
                        args = ["/qn"]  # Тихая установка MSI пакета
                        self.install_thread = InstallThread(installer_path, args)
                        self.install_thread.completed.connect(self.on_installation_complete)
                        self.install_thread.start()
                    else:
                        # Другие программы можно установить по аналогии
                        subprocess.run([installer_path], check=True)

    def on_installation_complete(self, message):
        self.go_to_completion_tab()  # Переходим на вкладку завершения установки
        print(message)  # Отображаем сообщение об окончании установки


if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = InstallerApp()
    window.show()
    sys.exit(app.exec_())

введите сюда описание изображения

введите сюда описание изображения

введите сюда описание изображения

→ Ссылка