Динамическое создание QPushButton с последующим сохранением в ini файл

проблема с динамическим созданием кнопок. Пишу приложение, нужно:

  1. Динамически создавать кнопки, которые по сути будут ярлыками на внешние программы.
  2. При необходимости, удалять нужную кнопку, чтобы остальные смещались согласно макету.
  3. Сохранять конфигурацию в ini файл, чтобы после перезапуска все созданные кнопки сохранялись.

Что есть на сейчас:

  1. Кнопки динамически создаются согласно Grid макету и могут запускать прикрепленный к ним exe файл, но только тот, что был прикреплен последним.
  2. Удаление работает только на последнюю созданную кнопку по клику по ней правой кнопкой мыши.
  3. Получается сохранить конфиг кнопки в файл settings.ini - но не могу извлечь данные при запуске программы.

Если у кого будут мысли, буду безмерно благодарен. Прикладываю минимально работающий пример.

Файл main.py:

from PyQt5.QtWidgets import QWidget, QApplication, QGridLayout, QGroupBox, QPushButton, QMessageBox, QMenu
from PyQt5.QtCore import QEvent, QSettings, Qt
from PyQt5.QtGui import QIcon

class Main(QWidget):

    settings = QSettings("settings.ini", QSettings.IniFormat)

    def __init__(self):
        super(Main, self).__init__()
        self.setWindowTitle('Образец')
        self.resize(500, 300)

        grid = QGridLayout()
        grid.addWidget(self.prog_group_box(), 0, 0, 1, 1)
        grid.addWidget(self.b_new_prog(), 1, 0, Qt.AlignRight)

        self.setLayout(grid)

    def prog_group_box(self):
        groupBox1 = QGroupBox(self)
        self.grid_prog = QGridLayout()
        groupBox1.setLayout(self.grid_prog)

        return groupBox1

    def saved_conf(self):
        # Сохранение созданной кнопки в ini файл
        self.settings.setValue('button', self.new_but)
        self.settings.setValue('label', self.click_newprog._leProgLabel.text())
        self.settings.setValue('source', self.click_newprog._leReview.text())
        self.settings.sync()

    # def restore_settings(self):
    # Функция восстановления сохраненного конфига из ini файла
        # test = self.settings.setValue('button')

        # print(f'RESTORE: {test}')

    def delete_setting(self):
        # Функция удаления данных из ini файла с сохраненной конфигурацией
        self.settings = QSettings("settings.ini", QSettings.IniFormat)
        self.settings.remove('button')

    def b_new_prog(self):
        self.b_new_prog = QPushButton(self)
        self.b_new_prog.setText("Добавить")
        self.b_new_prog.setMinimumSize(129, 43)
        self.b_new_prog.clicked.connect(self.click_newprog)

        return self.b_new_prog

    def click_newprog(self):
        from second import Second
        self.click_newprog = Second(self)
        self.click_newprog.show()

    def create_new_button(self):
        self.new_but = QPushButton()
        self.new_but.setFixedSize(70, 70)
        self.new_but.clicked.connect(self.btnClicked)
        self.new_but.setText(self.click_newprog._leProgLabel.text())
        self.grid_prog.addWidget(self.new_but, 0, 0)
        i = self.grid_prog.count() - 1
        self.grid_prog.addWidget(self.new_but, 1 + i // 8, i % 8)
        self.new_but.installEventFilter(self)

    def btnClicked(self):
        import os
        os.startfile(self.click_newprog._leReview.text())

    # Контекстное меню
    def eventFilter(self, source, event):
        if event.type() == QEvent.ContextMenu and source is self.new_but:
            menu = QMenu()
            menu.addAction('Удалить')

            if menu.exec_(event.globalPos()):
                reply = QMessageBox.question(
                    self,
                    'Message', "Вы действительно хотите удалить?",
                    QMessageBox.Yes | QMessageBox.No,
                    QMessageBox.No
                    )
                if reply == QMessageBox.Yes:
                    self.new_but.hide()
                    self.delete_setting()
                else:
                    pass

        return super().eventFilter(source, event)

if __name__ == ('__main__'):
    import sys
    app = QApplication(sys.argv)
    w = Main()
    w.show()
    sys.exit(app.exec_())

файл second.py:

from PyQt5.QtWidgets import (QLabel, QVBoxLayout, QLineEdit, QPushButton,
                             QHBoxLayout, QFileDialog, QMessageBox, QDialog)
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont


class Second(QDialog):
    def __init__(self, Main):
        super().__init__()
        self.main = Main
        self.setMinimumSize(300, 160)
        self.setWindowFlags(Qt.Dialog)

        vbox = QVBoxLayout()
        hbox = QHBoxLayout()
        hbox.addWidget(self._lProgLabel())
        hbox.addWidget(self._leProgLabel())
        vbox.addLayout(hbox)
        vbox.addWidget(self._lReview())
        hbox2 = QHBoxLayout()
        hbox2.addWidget(self._leReview())
        hbox2.addWidget(self._bReview())
        vbox.addLayout(hbox2)
        hbox3 = QHBoxLayout()
        hbox3.addWidget(self.b_create())
        hbox3.addWidget(self.b_close())
        vbox.addLayout(hbox3)
        self.setLayout(vbox)

    def _lProgLabel(self):
        _lProgLabel = QLabel(self)
        _lProgLabel.setText('<center style=font-size:10pt><FONT FACE="Century Gothic">Название кнопки:</center>')

        return _lProgLabel

    def _leProgLabel(self):
        self._leProgLabel = QLineEdit(self)
        self._leProgLabel.setFont(QFont('Century Gothic', 10))

        return self._leProgLabel

    def _lReview(self):
        _lreview = QLabel(self)
        _lreview.setText(
            '<center style=font-size:10pt><FONT FACE="Century Gothic">Укажите путь к программе или нажмите "Обзор":</center>')

        return _lreview

    def _leReview(self):
        self._leReview = QLineEdit(self)
        self._leReview.setFixedSize(250, 30)
        self._leReview.setFont(QFont('Century Gothic', 10))

        return self._leReview

    def _bReview(self):
        _breview = QPushButton()
        _breview.setFixedSize(90, 30)
        _breview.setText("Обзор")
        _breview.clicked.connect(self.browseFiles)

        return _breview

    def browseFiles(self):
        fname = QFileDialog.getOpenFileName(self, 'Open file', 'C:\Program Files', 'exe files (*.exe)')
        self._leReview.setText(fname[0])

    def b_create(self):

        b_create = QPushButton(self)
        b_create.setText("Добавить")
        b_create.setMinimumSize(10, 40)

        b_create.clicked.connect(self.create_button)

        return b_create

    def b_close(self):
        b_close = QPushButton(self)
        b_close.setText("Отмена")
        b_close.setMinimumSize(10, 40)
        b_close.clicked.connect(self._cancel)

        return b_close

    def _cancel(self):
        self.close()

    def showMessageBox(self, title, message):
        msgBox = QMessageBox()
        msgBox.setWindowTitle(title)
        msgBox.setText(message)
        msgBox.setStyleSheet("font: 12px;"
                             "font-family: Century Gothic;")
        msgBox.setStandardButtons(QMessageBox.Ok)
        msgBox.exec_()

    # Проверка ввода данных
    def create_button(self):
        if len(self._leReview.text()) == 0:
            self.showMessageBox('Внимание!',
                                '<center><br/><u style=font-size:10pt><FONT FACE="Arial"><b>Вы не заполнили поля</b></u></center></FONT>')
        else:
            self.main.create_new_button()
            self.main.saved_conf()
            self.close()


if __name__ == ('__main__'):
    w = Second()
    w.show()

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

Автор решения: Sergey Tatarincev

Ваша проблема в том что вы каждый раз переназначаете self.new_but поэтому у вас и работает только последний добавленый.

В вашей задаче достаточно в слотах ипспользовать не self.new_but а источник сигнала (кто его вызвал) - self.sender() Вот вам пример на минималках:

from PyQt5.QtWidgets import QWidget, QApplication, QGridLayout, QGroupBox, QPushButton, QMessageBox, QMenu, QFileDialog
from PyQt5.QtCore import QEvent, Qt, QFileInfo

class Main(QWidget):

    def __init__(self):
        super(Main, self).__init__()
        self.setWindowTitle('Образец')
        self.resize(500, 300)
        grid = QGridLayout()
        grid.addWidget(self.prog_group_box(), 0, 0, 1, 1)
        grid.addWidget(self.b_new_prog(), 1, 0, Qt.AlignRight)
        self.setLayout(grid)

    def prog_group_box(self):
        groupBox1 = QGroupBox(self)
        self.grid_prog = QGridLayout()
        groupBox1.setLayout(self.grid_prog)
        return groupBox1

    def b_new_prog(self):
        self.b_new_prog = QPushButton(self)
        self.b_new_prog.setText("Добавить")
        self.b_new_prog.setMinimumSize(129, 43)
        self.b_new_prog.clicked.connect(self.click_newprog)

        return self.b_new_prog

    def click_newprog(self):
        fname = QFileDialog.getOpenFileName(self, 'Open file', '', '')
        new_but = QPushButton()
        new_but.setFixedSize(70, 70)
        new_but.clicked.connect(self.btnClicked)
        fi = QFileInfo(fname[0])
        new_but.setText(fi.baseName())
        new_but.setProperty("filename",fname[0])
        self.grid_prog.addWidget(new_but, 0, 0)
        i = self.grid_prog.count() - 1
        self.grid_prog.addWidget(new_but, 1 + i // 8, i % 8)
        new_but.installEventFilter(self)

    def btnClicked(self):
        QMessageBox.information(self.sender(), "", self.sender().property("filename"))


    # Контекстное меню
    def eventFilter(self, source, event):

        if event.type() == QEvent.ContextMenu and source.metaObject().className()=='QPushButton':
            menu = QMenu()
            menu.addAction('Удалить')

            if menu.exec_(event.globalPos()):
                reply = QMessageBox.question(
                    self,
                    'Message', "Вы действительно хотите удалить?",
                    QMessageBox.Yes | QMessageBox.No,
                    QMessageBox.No
                    )
                if reply == QMessageBox.Yes:
                    source.hide()

                else:
                    pass

        return super().eventFilter(source, event)

if __name__ == ('__main__'):
    import sys
    app = QApplication(sys.argv)
    w = Main()
    w.show()
    sys.exit(app.exec_())
→ Ссылка
Автор решения: Djony Cooper

Вот решение, если у кого будет такой же вопрос:

Изменения коснутся только файла: main.py:

from PyQt5.QtWidgets import QWidget, QApplication, QGridLayout, QGroupBox, QPushButton, QMessageBox, QMenu
from PyQt5.QtCore import QEvent, QSettings, Qt
from PyQt5.QtGui import QIcon
 
class Main(QWidget):
 
    
 
    def __init__(self):
        super(Main, self).__init__()
        self.setWindowTitle('Образец')
        self.resize(500, 300)
 
        grid = QGridLayout()
        grid.addWidget(self.prog_group_box(), 0, 0, 1, 1)
        grid.addWidget(self.b_new_prog(), 1, 0, Qt.AlignRight)
 
        self.setLayout(grid)
        
        self.settings = QSettings("settings.ini", QSettings.IniFormat)
        self.dictButtons = {}
    
    def prog_group_box(self):
        groupBox1 = QGroupBox(self)
        self.grid_prog = QGridLayout()
        groupBox1.setLayout(self.grid_prog)
 
        return groupBox1
 
    def saved_conf(self):
        # Сохранение созданной кнопки в ini файл
        self.settings.beginGroup(self.new_but.text())
        self.settings.setValue('exe', self.dictButtons[self.new_but.text()])
        self.settings.endGroup()
        self.settings.sync()
    
    
    def restore_settings(self):
    # Функция восстановления сохраненного конфига из ini файла
        for i in self.settings.allKeys():
            self.restore_buttons(i.split('/')[0], self.settings.value(i))
            
    def restore_buttons(self, name, exe):
        self.new_but = QPushButton()
        self.new_but.setFixedSize(70, 70)
        self.new_but.clicked.connect(self.btnClicked)
        self.new_but.setText(name)
        self.grid_prog.addWidget(self.new_but, 0, 0)
        i = self.grid_prog.count() - 1
        self.grid_prog.addWidget(self.new_but, 1 + i // 8, i % 8)
        self.new_but.installEventFilter(self)
 
        self.dictButtons[name] = exe  
        
 
    def delete_setting(self, button):
        # Функция удаления данных из ini файла с сохраненной конфигурацией
        del self.dictButtons[button.text()]
        self.settings.remove(button.text())
 
    def b_new_prog(self):
        self.b_new_prog = QPushButton(self)
        self.b_new_prog.setText("Добавить")
        self.b_new_prog.setMinimumSize(129, 43)
        self.b_new_prog.clicked.connect(self.click_newprog)
 
        return self.b_new_prog
 
    def click_newprog(self):
        from second import Second
        self.click_newprog = Second(self)
        self.click_newprog.show()
 
    def create_new_button(self):
        self.new_but = QPushButton()
        self.new_but.setFixedSize(70, 70)
        self.new_but.clicked.connect(self.btnClicked)
        self.new_but.setText(self.click_newprog._leProgLabel.text())
        self.grid_prog.addWidget(self.new_but, 0, 0)
        i = self.grid_prog.count() - 1
        self.grid_prog.addWidget(self.new_but, 1 + i // 8, i % 8)
        self.new_but.installEventFilter(self)
 
        self.dictButtons[self.new_but.text()] = self.click_newprog._leReview.text()
 
    def btnClicked(self):
        import os
        button = self.sender()
        os.startfile(self.dictButtons[button.text()])
    
    # Контекстное меню
    def eventFilter(self, source, event):
        button = source
        if event.type() == QEvent.ContextMenu:
            menu = QMenu()
            menu.addAction('Удалить')
 
            if menu.exec_(event.globalPos()):
                reply = QMessageBox.question(
                    self,
                    'Message', "Вы действительно хотите удалить?",
                    QMessageBox.Yes | QMessageBox.No,
                    QMessageBox.No
                    )
                if reply == QMessageBox.Yes:
                    
                    button.hide()
                    self.delete_setting(button)
                else:
                    pass
 
        return super().eventFilter(source, event)
 
if __name__ == ('__main__'):
    import sys
    app = QApplication(sys.argv)
    w = Main()
    w.show()
    w.restore_settings()
    sys.exit(app.exec_())
→ Ссылка