I have a plugin for an application which provides the ability to use PyQt4 to create widgets which they can use to make their own tools for the application. The QApplication is maintained by a C++ plugin and an 'anchor' widget is parented to the host application's window handle. Then the user can create their own widgets and use this 'anchor' to be the parent widget. This works very well for PyQt4. Recently, I have been attempting to provide support for PySide and this too is working well right up until I need to create widgets from .ui files. It seems that when I use QUiLoader to load my .ui file the my resulting widget doesn't look the same as in PyQt4. It looks as if it is missing or skipping layouts and other properties described in the .ui file, such as the title. Thing is when I log the return value of that load function I get what seems right...
class ExampleUiFile(QDialog):
def __init__(self, parent, uifilepath):
QDialog.__init__(self, parent)
# load ui file
loader = QUiLoader(self)
file = QtCore.QFile(uifilepath)
file.open(QtCore.QFile.ReadOnly)
self.ui = loader.load(file, self)
file.close()
for k,v in vars(self.ui).items():
print("%s : %s" % (k,v))
# connect to the createCube function
self.ui.uiCreateCube.clicked.connect(self.createCube)
def createCube(self):
print(self.ui.uiCubeName.text())
Output..
horizontalLayout : <PySide.QtGui.QHBoxLayout object at 0x0000000023997308>
uiCubeName : <PySide.QtGui.QLineEdit object at 0x0000000023338508>
verticalLayout :<PySide.QtGui.QVBoxLayout object at 0x0000000023997548>
gridLayout : <PySide.QtGui.QGridLayout object at 0x0000000023997E08>
uiCubeLength : <PySide.QtGui.QDoubleSpinBox object at 0x0000000023338808>
uiCreateCube : <PySide.QtGui.QPushButton object at 0x0000000023338988>
So in an attempt to fix this I went digging, here and elsewhere, and found examples of sub-classing QUiLoader. I was able to copy a simple class which inherits QUiLoader which does some extra work to return the initial base class. This guy seemed to have worked and my dialog looks correct, the correct title is shown, layout, and resizing works. Unfortunately, the garbage collector removes all the widgets that my loader class created and I get message about my object being deleted...
class UiLoader(QUiLoader):
def __init__(self, baseinstance):
super(UiLoader, self).__init__(baseinstance)
self._baseinstance = baseinstance
def createWidget(self, classname, parent=None, name=""):
widget = super(UiLoader, self).createWidget(
classname, parent, name)
if parent is None:
return self._baseinstance
else:
setattr(self._baseinstance, name, widget)
return widget
Output using new sub-classed loader...
Internal C++ object (PySide.QtGui.QLineEdit) already deleted.
I have done a lot of digging on this issue and some bug in PySide in the past seemed to be the culprit, but I am using PySide 1.1.2 which has it fixed. I even built PySide from source with 1.1.3dev and still the same. I should add that I wasn't able to reproduce this sub-classing issue outside of the host application. I was able to make a simple python/PySide example that worked as expected.
Where do I go from here? I get a good looking ui without the functionality or I get an ugly ui with the functionality. I would prefer not to sub-class the QUiLoader since I am not doing anything fancy with custom widgets or anything.
Related
I have recently opted to learn programming using PyQt(6). I am trying to build a multi-tab main window where each tab is going to have their own functionality.
What I'm doing is that I designed my QMainWindow in QtDesigner and then used pyqt6.uic to import the .ui file, consequently accessing the widgets and views with findchild.
It is all going well If code like this, but the problem is that the great number of signals and slots in the code make the code very hard to debug and read after a short time.
I thought about defining each of these functionalities with their own class, but I could not make the connection between these sub classes and the main class.
I am not sure if that the correct approach to it, and if it is, then I probably miss a note or two with using multiple classes in PyQt.
Many thanks!
P.S.: I have updated the topic with a simple code to make my intention clearer Say I have main window with a tab widget consisting N tabs.
class UI(QMainWindow):
def __init__(self):
super(UI, self).__init__()
uic.loadUi('SO.ui', self)
#Instead of writing all the codes from tab1 to tabN. Use them in a clearer
#such as having their functionality in a different .py file.
#Maybe a way to inherit tab1 and tab2?
#Window runs via this method then.
class tab1(QMainWindow):
def __init__(self):
super(tab1, self).__init__()
uic.loadUi('SO.ui', self)
#signals and slots in Tab1 placed in a seperate .py file
class tab2(QMainWindow):
def __init__(self):
super(tab2, self).__init__()
uic.loadUi('SO.ui', self)
#signals and slots in Tab2 placed in a seperate .py file
I am searching for a way to have a QTreeView that contains hierarchical items which themselfs have a layout that is propperly drawn.
I tried to inherit from both QStandartItem and QWidget (to have a layout) but the second i set the layout on the widget part of this class the programm is shutting down when it tries to render.
class modPackItem(qtg.QStandardItem,qtw.QWidget):
def __init__(self,txt:str='',image_path:str='./assets/defaultModPack.jpg'):
super().__init__()
fnt = qtg.QFont('Calibri',12)
fnt.setBold(True)
self.setEditable(False)
self.setForeground(qtg.QColor(0,0,0))
self.setFont(fnt)
self.setText(txt)
self.horLayout = qtw.QHBoxLayout()
self.horLayout.addWidget(qtw.QLabel("test"))
#self.setLayout(self.horLayout) #this breaks the rendering
modPack_image = qtg.QImage(image_path)
self.setData(modPack_image.scaled(64,64,qtc.Qt.AspectRatioMode.KeepAspectRatioByExpanding),qtc.Qt.ItemDataRole.DecorationRole)
Is there a possible way to have all items in the QTreeView contain layouts (For example with multiple texts[description,tag-words,etc]).
Note: I also considered switching to a simple List of widgets which have children containing the hierarchical items. But that would increase complexity of my app-structure a lot and therefore i would like to avoid that.
Edit: To clearify what i want to do:
I want to build a mod(pack) manager in the style of the technic-launcher for minecraft mods but instead for any kind of game in any kind of infrastructure(steam, local instal,etc). By clicking different buttons i add new "modpacks" or "mods" (optimally custom QStandartItem with Layout for all the data) in an hierarchical fashion (therefore treeview). Adding the items and the steam-subsrciption or filecopy logic is no problem but i would like to see all infos (Name,descritpion, custom tags) on the overview (like in the example pic). I know i could bind the QStandartItem selection method to a new popup showing all infos but that would be inconvinient.
Edit2: On terms of implementation i just add the QStandartItem-object as an additional row to the root-node before setting the model. I allready tested adding new objects to the rootnode by clicking on a button and that worked fine. Just setting the layout in the object crashes the application at start.
class SteamModManager_Dialog(qtw.QDialog):
window: Ui_SteamModManagerFrame
treeModel: qtg.QStandardItemModel
rootNode: qtg.QStandardItem
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.window = Ui_SteamModManagerFrame()
self.window.setupUi(self)
self.window.label_footer.setText("")
self.treeModel = qtg.QStandardItemModel()
self.rootNode = self.treeModel.invisibleRootItem()
modPack = modPackItem('Dont Starve Together')
testMod = modItem("TestMod")
modPack.appendRow(testMod)
self.rootNode.appendRow(modPack)
self.window.tView_modPacks.setModel(self.treeModel)
self.window.tView_modPacks.expandAll()
On the behalf of #musicamente here the solution that worked out for me:
I created a widget in the designer (as usual, not posting the full ui code here).
Then i implemented the following code into the Dialog:
self.treeModel = qtg.QStandardItemModel()
self.rootNode = self.treeModel.invisibleRootItem()
modPack = modPackItem('Dont Starve Together')
testMod = modItem("TestMod")
modPack.appendRow(testMod)
self.rootNode.appendRow(modPack)
self.window.tView_modPacks.setModel(self.treeModel)
self.window.tView_modPacks.expandAll()
modPackWidget = qtw.QWidget()
ui = Ui_modPackWidget()
ui.setupUi(modPackWidget)
self.window.tView_modPacks.setIndexWidget(self.treeModel.index(0,0),modPackWidget)
This code resulted setting the custom widget to the treeview item. Here the final look:
I'm using PyQt, but I guess the same questions also applies to Qt C++.
Assume that I have a main window with a button that opens a wizard that collects data and that data needs to be used in the main window after the wizard has closed. standard procedure.
So there are multiple ways to do this. either I can pass a reference to the main window to the Wizard and it does all the work using the main window reference, but I'd say that breaks modularity. I can also wire up a callback to the wizard accepted rejected or finished signal, but in that callback, I don't have a reference to the wizard itself, so I cannot get to the data in the wizards fields. Unless I store a reference to the wizard as instance variable in order to access it again from the callback.
Another option is (even though I haven't fully understood it yet) to get a reference to the emitter of the signal (i.e. the wizard) in the callback using https://doc.qt.io/qt-5/qobject.html#sender. But that seems not recommended.
So whats the canonical way?
Premise: this is a bit of an opinion based question, as there is not one and only "good" way to do that. I just wanted to comment (opinion based answer/questions are discouraged here in SO), but the limited formatting isn't very helpful.
"Passing a reference" doesn't necessarily break modularity.
Instead, that's exactly what QDialog usually are initialized: the parent is the "calling" window, which is also how a QDialog can be "modal" to the parent or the whole application (meaning that no interaction outside the dialog is allowed as long as it is active).
AFAIK, I don't know if this is actually considered canonical, but the following is the most commonly suggested approach.
The idea is that you have a child object (a QDialog, usually) which might or might not be initialized everytime you need it, that's up to you; the important part is that you need a reference to it at least for the time required to update its result, which can even happen within the scope of a single method/slot.
from PyQt5 import QtWidgets
class MyWizard(QtWidgets.QDialog):
def __init__(self, parent=None):
super().__init__(parent)
layout = QtWidgets.QVBoxLayout()
self.setLayout(layout)
self.checkBox = QtWidgets.QCheckBox('check')
layout.addWidget(self.checkBox)
self.input = QtWidgets.QLineEdit()
layout.addWidget(self.input)
buttonBox = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok|QtWidgets.QDialogButtonBox.Cancel)
layout.addWidget(buttonBox)
buttonBox.accepted.connect(self.accept)
buttonBox.rejected.connect(self.reject)
def setData(self, **data):
self.checkBox.setChecked(data.get('check', False))
self.input.setText(data.get('text', ''))
def getData(self):
return {'check': self.checkBox.isChecked(), 'text': self.input.text()}
def exec_(self, **data):
self.setData(**data)
return super().exec_()
class MyWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
centralWidget = QtWidgets.QWidget()
self.setCentralWidget(centralWidget)
layout = QtWidgets.QHBoxLayout()
centralWidget.setLayout(layout)
self.showWizBtn = QtWidgets.QPushButton('Show wizard')
layout.addWidget(self.showWizBtn)
self.showWizBtn.clicked.connect(self.getDataFromWizard)
self.data = {}
def getDataFromWizard(self):
wiz = MyWizard(self)
if wiz.exec_(**self.data):
self.data.update(wiz.getData())
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
window = MyWindow()
window.show()
sys.exit(app.exec_())
Another possibility is to create a persistent child dialog (but keep in mind that if the data can be changed by the parent, you'll have to find a way to update it, at least when executed); the concept here is that you can exec the dialog whenever you need, and you have the accepted signal connected to a slot that can get the data from the dialog. This is not a common use (nor very suggested IMHO) and should be used only for very specific scenarios.
As you already found out, using sender is not suggested: signals are asynchronous, and while in normal conditions the sender is reliable it's better to avoid using it unless absolutely necessary.
I have created a GUI with Qt Designer and accessed it via
def loadUiWidget(uifilename, parent=None):
loader = QtUiTools.QUiLoader()
uifile = QtCore.QFile(uifilename)
uifile.open(QtCore.QFile.ReadOnly)
ui = loader.load(uifile, parent)
uifile.close()
return ui
MainWindow = loadUiWidget("form.ui")
MainWindow.show()
children = MainWindow.children()
button1 = MainWindow.QPushButton1
"children" does already contain the widgets "QPushButton1", "QTextBrowser1" created in the UI but shouldn't the be accessed by the recoursive findChildren() method?
What is an elegant way to access the widgets of the .ui File?
References:
Find correct instance,
Load .ui file
Since widget names in Qt Designer must be unique, the hierarchy (at least for getting references to the widgets) is flattened (with no risk of conflict), and so the best way is just to access them via:
loader = QtUiTools.QUiLoader()
ui = loader.load('filename.ui', parent)
my_widget = ui.my_widget_name
This would place a reference to the widget called 'my_widget_name' in Qt Designer in the variable my_widget.
I would say the above is the most pythonic way of accessing the widgets created when you load the .ui file.
There are two disadvantages of loading UI at run time:
overhead each time the program is run (actually, each time the loader is used)
lack of support of code completion and checking, since IDE doesn't know the code behind ui until the uifile has been loaded.
An alternative, assuming you are using the modern version of PySide called "Qt for Python", is to "compile" the .ui file to a Python class (see docs). For this, after saving filename.ui, execute
pyside2-uic filename.ui -o ui_mainwindow.py
while within your virtual environment, if any. The new class will be called Ui_MainWindow. Assuming you have a text_box widget in your UI, you can now access its properties and methods. Here is a full working example:
import sys
from PySide2.QtWidgets import QApplication, QMainWindow
from ui_mainwindow import Ui_MainWindow
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.ui.text_box.setPlainText('test') # here we are addressing widget
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
Notes:
pyside2-uic should be called after every change of the .ui file. This is a disadvantage of this approach in comparison to OP. It also means that you should either version-control both .ui and .py files for your UI, or somehow call uic during deployment.
The big advantage is that IDE like PyCharm has access to all widget methods and properties for autocompletion and code checking.
As of today, pyside2-uic creates non-PEP8 compliant code. However, as long as you give your widgets PEP8-compliant names, your own code will be OK.
I'll start with the question and then try to explain:
Is there a way for an imported module to call a function in the module that imports it?
I am just learning to use Qt and am starting with Qt Designer to get some fundamentals worked out.
I have figured out how to create more than one ".ui" file in order to get the code for multiple windows and have managed to work out how to call the multiple windows from a main application by importing the code for the two windows.
For example, starting with win1.ui and win2.ui I create win1.py and win2.py - from my main application I import win1 and win2...
Note - I got this far by following this simple tutorial : http://www.youtube.com/watch?v=bHsC6WJsK-U&list=PLF4575388795F2531&index=10&feature=plpp_video
OK - now the question. If I have a button in win2, I know how to link that button to a function in the win2.py code. What I don't know how to do is link the button in win2 to a function in my main application.
My only thought would be to add a function as an argument to the class that sets up the second window but if I do that then any changes to win2.ui will wreck the code that I have changed.
Thus, Is there a way for an imported module to call a function in the module that imports it?
I hope this is clear without adding a bunch of code that isn't really relevant...
Qt is based on event-driven programming. Generally when you start building up your widgets, what you are going to be wanting to do is providing information to receiver widgets via signals that are then processed. You don't want to explicitly have a child widget know or require to call methods on a parent widget (this is not always the case, but it is good to avoid when possible).
I'm gonna post some examples that don't have UI files for ease here, but just assume you can build the same widget with designer and have it work the same way...
testwidget.py
from PyQt4 import QtGui, QtCore
class TestWidget(QtGui.QWidget):
textSaved = QtCore.pyqtSignal(str)
def __init__( self, parent = None ):
super(TestWidget, self).__init__(parent)
# create the ui (or load it)
self.__edit = QtGui.QTextEdit(self)
self.__button = QtGui.QPushButton(self)
self.__button.setText('Save')
layout = QtGui.QVBoxLayout()
layout.addWidget(self.__edit)
layout.addWidget(self.__button)
self.setLayout(layout)
# create connections
self.__button.clicked.connect(self.emitTextSaved)
def emitTextSaved( self ):
# allow Qt's blocking of signals paradigm to control flow
if ( not self.signalsBlocked() ):
self.textSaved.emit(self.__edit.toPlainText())
testwindow.py
from PyQt4 import QtGui, QtCore
import testwidget
class TestWindow(QtGui.QMainWindow):
def __init__( self, parent == None ):
super(TestWindow, self).__init__(parent)
# create the ui (or load it)
self.__editor = testwidget.TestWidget(self)
self.setCentralWidget(self.__editor)
# create connections
self.__editor.textSaved.connect(self.showMessage)
def showMessage( self, message ):
QtGui.QMessageBox.information(self, 'Message', message)
So, here you can see that instead of thinking about it like - "when I click the button in TestWidget, I want to show a message in TestWindow" and explicitly link the two methods, you expose a signal that the TestWidget will emit out when the user performs an action, then connect that signal to the showMessage slot of the TestWindow. This way, your smaller widgets become more independent, and its more a matter of how you connect to each event that drives your application.
I could have done something like self.parent().showMessage(self.__edit.toPlainText()) within the TestWidget's emitTextSaved method to call the method directly - but this is not a good design.