0

Summary:

I am trying to make a pyqt5 UI that reads in a dictionary from a json file and dynamically creates an editable form. I would then like to be able to change the json file and have my form update.

What I've tried:

I think the simplest solution would be just to somehow destroy the ui and re-initialize the ui with a new dictionary, I have tried to understand this post, but I am not to sure how to modify this answer to reboot and then instantiate a new UI class with a new dictionary?

I have also read a few posts like this post which I think I can use to first delete all my widgets, then delete my layout and then re add new widgets and layouts from a new dictionary, but I wonder if this is just over complicating the problem?

Some example code:

import sys
from PyQt5.QtWidgets import QWidget
from PyQt5.QtWidgets import QVBoxLayout
from PyQt5.QtWidgets import QFormLayout
from PyQt5.QtWidgets import QMainWindow
from PyQt5.QtWidgets import QLineEdit
from PyQt5.QtWidgets import QPushButton
from PyQt5.QtWidgets import QApplication

class JsonEditor(QMainWindow):
    def __init__(self, dictionary):
        super().__init__()
        self.dictionary = dictionary

        self.setWindowTitle("Main Window")
        self.setGeometry(200, 200, 800, 100)

        self.main_widget = QWidget(self)
        self.setCentralWidget(self.main_widget)
        self.main_layout = QVBoxLayout(self.main_widget)

        self.createDynamicForm()
        self.createUpdateButton()

    def createDynamicForm(self):
        self.dynamiclayout = QFormLayout()
        self.dynamic_dictionary = {}
        for key, value in self.dictionary.items():
            self.dynamic_dictionary[key] = QLineEdit(value)
            self.dynamiclayout.addRow(key, self.dynamic_dictionary[key])
        self.main_layout.addLayout(self.dynamiclayout)

    def createUpdateButton(self):
        self.update_button = QPushButton('update')
        self.main_layout.addWidget(self.update_button)
        self.update_button.clicked.connect(self.updateDictionary)

    def updateDictionary(self):
        dictionary2 = {}
        dictionary2['foo2'] = 'foo_string2'
        dictionary2['bar2'] = 'bar_string2'
        dictionary2['foo_bar'] = 'foo_bar_string2'
        self.dictionary = dictionary2


dictionary1 = {}
dictionary1['foo'] = 'foo_string'
dictionary1['bar'] = 'bar_string'


if __name__ == "__main__":
    test_app = QApplication(sys.argv)
    MainWindow = JsonEditor(dictionary1)
    MainWindow.show()
    sys.exit(test_app.exec_())

I suppose I am at the stage of learning where I probably don't know exactly the right questions to ask or how to describe terminology correctly, so I hope this makes sense.

musicamante
  • 41,230
  • 6
  • 33
  • 58
jumball
  • 13
  • 3
  • Is [QTableWidget](https://doc.qt.io/qt-6/qtablewidget.html) what you want to "creates an editable form"? – hellohawaii Jul 10 '22 at 13:40
  • Are you sure you want to *reset* the current window and its layout? It can be done, but most of the cases you only need to close/destroy the current window and create a new one, which is quite simpler. – musicamante Jul 10 '22 at 17:11
  • @musicamante So I suppose if it is possible, I would like to close the window and then reinitialise the UI with a new dictionary argument, which would be a different dictionary and therefore when the UI dynamically builds again, a new form layout from the new dictionary keys and values? Something like: `MainWindow.close()MainWindow` `JsonEditor(dictionary2)` When I try this code linked to the 'updateDictionary' function, I can close the window, but the window UI re-opens empty? – jumball Jul 11 '22 at 18:22
  • I was trying to figure out how to format my code snippet correctly in a comment, but I ran out of time! `MainWindow.close()` `MainWindow = JsonEditor(dictionary2)` – jumball Jul 11 '22 at 18:31
  • @hellohawaii May be I do want to use a QTable widget instead of trying to rebuild my form layout by reinitialising my UI, I will read up on that! – jumball Jul 11 '22 at 18:34

3 Answers3

1

As long as you're using a QFormLayout, you can consider using removeRow(), and do that before adding the new widgets (even the first time). Note that the layout has to be created outside that function, so that you can always reuse it as needed.

import sys
from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout, QFormLayout, 
    QMainWindow, QLineEdit, QPushButton)

class JsonEditor(QMainWindow):
    def __init__(self, dictionary):
        super().__init__()
        self.dictionary = dictionary.copy()

        self.setWindowTitle("Main Window")

        central = QWidget(self)
        self.setCentralWidget(central)

        self.main_layout = QVBoxLayout(central)

        self.form_layout = QFormLayout()
        self.main_layout.addLayout(self.form_layout)

        self.createDynamicForm()
        self.createUpdateButton()

    def createDynamicForm(self):
        while self.form_layout.rowCount():
            self.form_layout.removeRow(0)
        self.dynamic_dictionary = {}
        for key, value in self.dictionary.items():
            self.dynamic_dictionary[key] = QLineEdit(value)
            self.form_layout.addRow(key, self.dynamic_dictionary[key])
        QApplication.processEvents()
        self.adjustSize()

    def createUpdateButton(self):
        self.update_button = QPushButton('update')
        self.main_layout.addWidget(self.update_button)
        self.update_button.clicked.connect(self.updateDictionary)

    def updateDictionary(self):
        dictionary2 = {}
        dictionary2['foo2'] = 'foo_string2'
        dictionary2['bar2'] = 'bar_string2'
        dictionary2['foo_bar'] = 'foo_bar_string2'
        self.dictionary = dictionary2
        self.createDynamicForm()
musicamante
  • 41,230
  • 6
  • 33
  • 58
  • This solution is great! Rebuilding `self.form_layout` is avoided by just modifying it. However, I wonder why you add `QApplication.processEvents()` in the function `createDynamicForm`. I delete it and it works fine. – hellohawaii Jul 12 '22 at 04:45
  • 1
    @hellohawaii if the new layout contents occupy less space than the previous, the new size hint obtained by `adjustSize()` might not be updated yet (due to delayed layout requests), so the cached size won't match the actual contents and the window will not reduce the size as expected. – musicamante Jul 12 '22 at 21:33
  • Out of curiosity, why is it beneficial to use `dictionary.copy()`? I'm guessing so the original dictionary remains unchanged but I'm not sure why it matters? – jumball Jul 13 '22 at 07:41
  • @jumball Yes, it's not really necessary, it's just a precaution. A better implementation would use the dict as argument of `createDynamicForm()` (so that it would be *explicit*) while not keeping a reference to avoid confusion in case the original dict is changed in the meantime. – musicamante Jul 13 '22 at 13:37
1

As suggested in the comments, I tried out the Qtable widget. In this script the table builder refreshes, removes all table rows and then builds the table from a dictionary.

It builds the first table as a show event, although I don't think that's actually needed in the end. I thought it might be a way to have the table rebuild upon closing and reopening the window as suggested in the comments too. In the end the refresh button is enough on it's own I think, without closing the window.

The research I did was from a training video by Chris Zurbrigg on the the Table widget in pyside2.

from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout, QMainWindow, QLineEdit, QPushButton,
                             QTableWidget, QTableWidgetItem, QHeaderView)

class TableEditUI(QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle('table edit')
        self.setMinimumWidth(500)

        self.main_widget = QWidget(self)
        self.setCentralWidget(self.main_widget)

        self.create_widgets()
        self.create_layouts()

        self.toggle = True
        self.set_dictionary()
        self.refresh_table()

        self.create_connections()

    def create_widgets(self):
        self.table = QTableWidget()
        self.table.setColumnCount(2)
        self.table.setHorizontalHeaderLabels(["Attribute", "Data"])
        header_view = self.table.horizontalHeader()
        header_view.setSectionResizeMode(1, QHeaderView.Stretch)
        self.toggle_dictionary_btn = QPushButton('toggle dictionary')

    def refresh_table(self):
        self.table.setRowCount(0)
        self.dynamic_dictionary = {}
        for count, (key, value) in enumerate(self.dictionary.items()):
            print(count, key, value)
            self.dynamic_dictionary[key] = QLineEdit(value)
            self.table.insertRow(count)

            self.insert_item(count, 0, str(key))
            self.table.setCellWidget(count, 1, self.dynamic_dictionary[key])

    def insert_item(self, row, column, text):
        item = QTableWidgetItem(text)
        self.table.setItem(row, column, item)

    def showEvent(self, e):
        super(TableEditUI, self).showEvent(e)
        self.refresh_table

    def create_layouts(self):
        self.main_layout = QVBoxLayout(self.main_widget)
        self.main_layout.addWidget(self.table)
        self.main_layout.addWidget(self.toggle_dictionary_btn)

    def create_connections(self):
        self.toggle_dictionary_btn.clicked.connect(self.set_dictionary)

    def set_dictionary(self):
        self.toggle = not self.toggle
        dictionary1 = {}
        dictionary1['foo'] = 'foo_string'
        dictionary1['bar'] = 'bar_string'

        dictionary2 = {}
        dictionary2['foo2'] = 'foo_string2'
        dictionary2['bar2'] = 'bar_string2'
        dictionary2['foo_bar'] = 'foo_bar_string2'

        if self.toggle:
            self.dictionary = dictionary1
        else:
            self.dictionary = dictionary2

        self.refresh_table()


if __name__ == "__main__":

    app = QApplication(sys.argv)
    TableEditUIWindow = TableEditUI()
    TableEditUIWindow.show()
    sys.exit(app.exec())
jumball
  • 13
  • 3
  • It works! However, aftering reading the answer by @musicamante, I think perhaps my previous comment is misleading and the code refactoring using QTableWidget can be avoided:-) – hellohawaii Jul 13 '22 at 01:52
0

Solution 1

I modify your codes in a way that delete the self.dynamiclayout and rebuild it. Just by using the link you have posted.

You just need to add a call of deleteItemsOfLayout(self.dynamiclayout) and then self.createDynamicForm() in your updateDictionary() function. Another small modification is changing self.main_layout.addLayout(self.dynamiclayout) in your createDynamicForm() function to self.main_layout.insertLayout(0, self.dynamiclayout) in order to keep the order of elements in the self.main_layout.

import sys
from PyQt5.QtWidgets import QWidget
from PyQt5.QtWidgets import QVBoxLayout
from PyQt5.QtWidgets import QFormLayout
from PyQt5.QtWidgets import QMainWindow
from PyQt5.QtWidgets import QLineEdit
from PyQt5.QtWidgets import QPushButton
from PyQt5.QtWidgets import QApplication


# copy from https://stackoverflow.com/a/45790404/9758790
def deleteItemsOfLayout(layout):
    if layout is not None:
        while layout.count():
            item = layout.takeAt(0)
            widget = item.widget()
            if widget is not None:
                widget.setParent(None)
            else:
                deleteItemsOfLayout(item.layout())

class JsonEditor(QMainWindow):
    def __init__(self, dictionary):
        super().__init__()
        self.dictionary = dictionary

        self.setWindowTitle("Main Window")
        self.setGeometry(200, 200, 800, 100)

        self.main_widget = QWidget(self)
        self.setCentralWidget(self.main_widget)
        self.main_layout = QVBoxLayout(self.main_widget)

        self.createDynamicForm()
        self.createUpdateButton()

    def createDynamicForm(self):
        self.dynamiclayout = QFormLayout()
        self.dynamic_dictionary = {}
        for key, value in self.dictionary.items():
            self.dynamic_dictionary[key] = QLineEdit(value)
            self.dynamiclayout.addRow(key, self.dynamic_dictionary[key])
        self.main_layout.insertLayout(0, self.dynamiclayout)

    def createUpdateButton(self):
        self.update_button = QPushButton('update')
        self.main_layout.addWidget(self.update_button)
        self.update_button.clicked.connect(self.updateDictionary)

    def updateDictionary(self):
        dictionary2 = {}
        dictionary2['foo2'] = 'foo_string2'
        dictionary2['bar2'] = 'bar_string2'
        dictionary2['foo_bar'] = 'foo_bar_string2'
        self.dictionary = dictionary2

        deleteItemsOfLayout(self.dynamiclayout)
        self.createDynamicForm()

dictionary1 = {}
dictionary1['foo'] = 'foo_string'
dictionary1['bar'] = 'bar_string'


if __name__ == "__main__":
    test_app = QApplication(sys.argv)
    MainWindow = JsonEditor(dictionary1)
    MainWindow.show()
    sys.exit(test_app.exec_())

Solution 2

You can also re-build the whole test_app. However,

  1. In this way I think you must make the dictionary a global varibal instead of a property of the class JsonEditor. To be honest, I think this is not elegant.
  2. In addition, the restart of the whole application may be unnecessary, perhaps you just need to close() the window and create a new one as suggested by @musicamante. I am not sure how to create it again. I wonder whether it's possible for a widget to close() itself and rebuild itself again, if not, you need to add the JsonEditor to parent widget and close/rebuild the JsonEditor there. I think it will be troublesome.
  3. Note that the window will "flash"(disappear and appear again) in the solution 2.
import sys
from PyQt5.QtWidgets import QWidget, qApp
from PyQt5.QtWidgets import QVBoxLayout
from PyQt5.QtWidgets import QFormLayout
from PyQt5.QtWidgets import QMainWindow
from PyQt5.QtWidgets import QLineEdit
from PyQt5.QtWidgets import QPushButton
from PyQt5.QtWidgets import QApplication
from PyQt5 import QtCore

class JsonEditor(QMainWindow):
    def __init__(self, dictionary):
        super().__init__()
        self.dictionary = dictionary

        self.setWindowTitle("Main Window")
        self.setGeometry(200, 200, 800, 100)

        self.main_widget = QWidget(self)
        self.setCentralWidget(self.main_widget)
        self.main_layout = QVBoxLayout(self.main_widget)

        self.createDynamicForm()
        self.createUpdateButton()

    def createDynamicForm(self):
        self.dynamiclayout = QFormLayout()
        self.dynamic_dictionary = {}
        for key, value in self.dictionary.items():
            self.dynamic_dictionary[key] = QLineEdit(value)
            self.dynamiclayout.addRow(key, self.dynamic_dictionary[key])
        self.main_layout.addLayout(self.dynamiclayout)

    def createUpdateButton(self):
        self.update_button = QPushButton('update')
        self.main_layout.addWidget(self.update_button)
        self.update_button.clicked.connect(self.updateDictionary)

    def updateDictionary(self):
        dictionary2 = {}
        dictionary2['foo2'] = 'foo_string2'
        dictionary2['bar2'] = 'bar_string2'
        dictionary2['foo_bar'] = 'foo_bar_string2'
        global dictionary
        dictionary = dictionary2

        qApp.exit(EXIT_CODE_REBOOT)


dictionary1 = {}
dictionary1['foo'] = 'foo_string'
dictionary1['bar'] = 'bar_string'
dictionary = dictionary1

EXIT_CODE_REBOOT = -11231351

if __name__ == "__main__":
    exitCode = 0
    while True:
        test_app = QApplication(sys.argv)
        MainWindow = JsonEditor(dictionary)
        MainWindow.show()
        exitCode = test_app.exec_()
        test_app = None
        if exitCode != EXIT_CODE_REBOOT:
            break

In fact, this is my first time to try to restart a Qt GUI. I referred to this answer, however, it didn't work directly as I stated in the comment below it. This answer works fine and is adopted in my answer. I also tried this answer, however, the application configuration stayed unchanged if using this implementation.

hellohawaii
  • 3,074
  • 6
  • 21
  • 1. Do not use globals, nor suggest to do so; 2. recreating the QApplication might have unexpected results (it very often results in a fatal crash); 3. managing the reopening/resetting of a window should ***not*** need to restart the whole application; – musicamante Jul 12 '22 at 04:31
  • @musicamante Thanks for your advice! However, I am still wondering how to close/rebuild a window("close/destroy the current window and create a new one") as you mentioned in your comments before, do I need to wrap the window with a parent window to receive some signal and then carry on the close/rebuild affair? – hellohawaii Jul 12 '22 at 04:38
  • Thanks for the 2 solutions @hellohawaii – jumball Jul 12 '22 at 21:18