I am using PyQt6 6.1.0 on Python 3.9.6 and I would like to get the parent QMainWindow of the parent QTreeWidget of parents QTreeWidgetItems of a QTreeWidgetItem, because I need to modify the window from within the QTreeWidgetItem.
The full code is very long, over 1800 lines and over 60 KiB in size, when I put it in one file it works without problem, but when in one file its readability is very low, and when split into files the global variables can be hard to access from within classes.
Basically my idea is to call .parent() repetitively until the object's class name is 'MainWindow'.
def getWindow(obj):
window = obj.parent()
while True:
if type(window).__name__ == 'MainWindow':
break
window = window.parent()
return window
I didn't use the class directly because the class is defined in the main file.
Here are two classes defined in a file named groupA.py:
class TreeNode(QTreeWidgetItem):
def __init__(self, texts):
super().__init__()
self.isSongNode = False
self.setFlags(
FLAG.ItemIsAutoTristate
| FLAG.ItemIsEnabled
| FLAG.ItemIsSelectable
| FLAG.ItemIsUserCheckable
)
self.setCheckState(0, Qt.CheckState.Unchecked)
self.data = dict(zip(fields, texts))
self.song = None
for i in ("title", "album", "artist"):
if i in self.data:
if i == "title":
self.setTextAlignment(0, Qt.AlignmentFlag.AlignLeft)
self.setText(0, self.data["title"])
self.setToolTip(0, self.data["title"])
self.setText(1, self.data["duration"])
self.setText(2, self.data["instrumental"])
self.setText(3, self.data["downloaded"])
self.setText(4, ",".join(self.data["language"]))
self.setText(5, self.data["artistgender"])
for j in range(1, 6):
self.setTextAlignment(j, Qt.AlignmentFlag.AlignCenter)
self.song = song(*[self.data[i] for i in cells])
self.isSongNode = True
else:
self.setText(0, self.data[i])
self.setToolTip(0, self.data[i])
break
def detail(self):
print(self)
print(self.parent())
print(self.parent().parent())
print(self.parent().parent().parent())
window = getWindow(self)
if self.childCount() != 0:
for i in range(self.childCount()):
self.child(i).detail()
else:
if self.song not in detailed_items and self.isSongNode:
widget = SongPage(self.data)
window.detailArea.scrollArea.Layout.addWidget(widget)
widget.autoResize()
detailed_items.append(self.song)
class Tree(QTreeWidget):
def __init__(self):
super().__init__()
self.init()
def init(self):
self.setColumnCount(len(HEADERS))
self.setHeaderLabels(HEADERS)
font = Font(9)
self.setFont(font)
self.header().setFont(font)
self.setColumnWidth(0, 900)
fontRuler = QFontMetrics(font)
for i in range(1, len(HEADERS)):
Width = fontRuler.size(0, HEADERS[i]).width() + 16
self.setColumnWidth(i, Width)
self.setAutoScroll(True)
self.setIndentation(32)
self.setAlternatingRowColors(True)
self.setUniformRowHeights(True)
self.itemClicked.connect(self.onItemClicked)
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
#pyqtSlot(QTreeWidgetItem)
def onItemClicked(self, item):
window = getWindow(self)
nodes = self.findItems(
"", Qt.MatchFlag.MatchContains | Qt.MatchFlag.MatchRecursive
)
for node in nodes:
if (
node.checkState(0) == Qt.CheckState.Unchecked
and node.song in detailed_items
):
i = detailed_items.index(node.song)
detailed_items.remove(node.song)
layoutitem = window.detailArea.scrollArea.Layout.itemAt(i)
widget = layoutitem.widget()
window.detailArea.scrollArea.Layout.removeItem(layoutitem)
window.detailArea.scrollArea.Layout.removeWidget(widget)
elif (
node.childCount() == 0
and node.checkState(0) == Qt.CheckState.Checked
and node.isSongNode
):
node.detail()
if item.childCount() == 0 and item.isSongNode:
if item.song not in detailed_items:
item.detail()
index = detailed_items.index(item.song)
dim_others(window, index)
widget = window.detailArea.scrollArea.Layout.itemAt(index).widget()
QTimer.singleShot(
25, lambda: window.detailArea.scrollArea.ensureWidgetVisible(
widget)
)
widget.highlight()
setButtonsStatus(window)
The tree is filled by this:
def add_nodes(tree, cls, data):
indexes.clear()
for artist, albums in data.items():
artist_node = cls([artist])
indexes.add([artist])
for album, songs in albums.items():
album_node = cls([artist, album])
indexes.add([artist, album])
for song in songs:
song_node = cls([artist, album, *song])
indexes.add([artist, album, song[0]])
album_node.addChild(song_node)
artist_node.addChild(album_node)
tree.addTopLevelItem(artist_node)
And this is the start of code execution:
if __name__ == '__main__':
mysql80 = psutil.win_service_get("MySQL80").as_dict()
if mysql80["status"] != "running":
os.system("net start MySQL80")
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(u'Asceteria')
songs = fetchdata()
entries = treefy(songs)
app = QApplication(sys.argv)
app.setStyle("Fusion")
tree = Tree()
add_nodes(tree, TreeNode, entries)
filterArea = FilterArea()
Window = MainWindow()
print(tree)
print(tree.parent())
print(tree.parent().parent())
Window.showMaximized()
app.setWindowIcon(QIcon(ICON))
app.exec()
When I check any QTreeWidgetItem, the program crashes, because the top level QTreeWidgetItems don't have parents:
<groupA.Tree object at 0x0000022ABCFE9430>
<PyQt6.QtWidgets.QWidget object at 0x0000022AC10DBAF0>
<__main__.MainWindow object at 0x0000022AC10DBA60>
<groupA.TreeNode object at 0x0000022ABE087EE0>
<groupA.TreeNode object at 0x0000022ABE087E50>
<groupA.TreeNode object at 0x0000022ABE087DC0>
None
Traceback (most recent call last):
File "D:\Asceteria\groupA.py", line 148, in onItemClicked
node.detail()
File "D:\Asceteria\groupA.py", line 90, in detail
window = getWindow(self)
File "D:\Asceteria\functions.py", line 112, in getWindow
window = window.parent()
AttributeError: 'NoneType' object has no attribute 'parent'
How can I fix this?
It is not necessary to implement any method to obtain the window associated with a widget since Qt provides the window() method for it:
QWidget *QWidget::window() const Returns the window for this widget,
i.e. the next ancestor widget that has (or could have) a window-system
frame.
If the widget is a window, the widget itself is returned.
Typical usage is changing the window title:
aWidget->window()->setWindowTitle("New Window Title");
In your case use:
window = self.treeWidget().window()
Note: If you want to verify that an object is an instance of a class or its ancestor then you must use isinstace:
isinstance(window, MainWindow)
Note: The relationship between QTreeWidgetItem is different from that of QWidgets.
I am doing a rigging tool and I want to assign a different color to the controllers for right and left using a slider. the idea is to select en item from the menu and get the color for that. but for now it is working just the first item and none the others.
My code is inside a class, at this point I have the window, the function to select a joint and a function to change the color. I am querying the selection for the option menu. This information is use by the three conditionals to set a color for each menu item. However, it works just the first option and none the other two. I tried to do the same code but without a class just defining the function and it works. What can I be doing wrong inside the class? I am kind of new in python and this is my first time doing a tool for ringing.
import maya.cmds as cmds
import maya.OpenMaya as om
import maya.mel as mel
import sys
import math
class rigCreator:
def __init__(self, *args):
#self.startFunction()
self.window = "uiWindow"
self.title = "Rigging Tool Bipeds"
self.winSize = (150, 200)
self.createUI()
def createUI(self, *args):
#check if window and prefs exist. If yes, delete
if cmds.window(self.window, ex=True):
cmds.deleteUI(self.window, wnd=True)
elif cmds.windowPref(self.window, ex=True):
cmds.windowPref(self.window, r=True)
self.window = cmds.window(self.window, t=self.title, wh = self.winSize, s=1, mnb=1, mxb=1)
self.mainForm = cmds.formLayout(nd=100)
self.tagLine= cmds.text(label = "Rig Tool")
cmds.frameLayout(label="1. Choose the root joint")
self.Layout = cmds.columnLayout(adj=1)
#Add a saparator to the window
cmds.separator()
# button to select the first joint
cmds.rowColumnLayout (nc = 2, cs=[(1,6), (2,6)])
rootBt = cmds.textField ('rootJnt', tx = 'First joint of your Arm chain', w = 250)
#cmds.textFieldButtonGrp('rootJnt', width=380, cal=(8, "center"), cw3=(100, 200, 75), h=40, pht="First joint of your Arm chain", l="First Joint", bl="Select", bc = lambda x: findJnt(), p=self.Layout)
cmds.button(l = 'Select', c = lambda x:self.findJnt())
cmds.setParent(self.Layout)
frame = cmds.frameLayout("2. Name options", lv=1, li=1, w=250)
#cmds.text(label="", align = "left")
cmds.rowColumnLayout(nc=4, cs=[(1,6), (2,6), (3,6), (4,6)])
#cmds.text('Side', l='Side:')
cmds.optionMenu('Part_Side', l='Side:', cc=lambda x:self.colorChange(), acc=1, ni=1)
cmds.menuItem(label='L_')
cmds.menuItem(label='R_')
cmds.menuItem(label='_')
cmds.text('Part', l='Part:')
cmds.optionMenu('part_Body')
cmds.menuItem(label='Arm')
cmds.menuItem(label='Leg')
cmds.menuItem(label='Spine')
cmds.setParent(self.Layout)
frame2 = cmds.frameLayout("3. Type of rig", lv=True, li=1, w=250)
cmds.rowColumnLayout(nc=3, cs=[(1,6), (2,6), (3,6)])
cmds.radioCollection("limb side")
cmds.radioButton(label='IK/FK', select=True)
cmds.radioButton(label='IK')
cmds.radioButton(label='FK')
cmds.setParent(self.Layout)
frame3 = cmds.frameLayout("4. Thick if you want to apply stretch", lv=True, li=1, w=250)
cmds.rowColumnLayout(nc=1, cs=[(1,6)])
cmds.checkBox( label='Stretchy limb', align='center' )
cmds.setParent(self.Layout)
cmds.setParent(self.Layout)
frame4 = cmds.frameLayout("5. Pick icon color", lv=True, li=1, w=250)
cmds.gridLayout(nr=1, nc=5, cwh=[62,20])
cmds.iconTextButton('darkBlue_Btn', bgc=[.000,.016,.373])
cmds.iconTextButton('lightBlue_Btn', bgc=[0,0,1])
cmds.iconTextButton('Brown_Btn', bgc=[.537,.278,.2])
cmds.iconTextButton('red_Btn', bgc=[1,0,0])
cmds.iconTextButton('Yellow_Btn', bgc=[1,1,0])
cmds.setParent(self.Layout)
colorBt = cmds.colorIndexSliderGrp('rigColor', w=250, h=50, cw2=(150,0), min=0, max=31, v=7)
#This button will creat the chain of joins with streatch and squach
cmds.button('b_create', label='Create', h=30, c='create()')
#show the window
cmds.showWindow(self.window)
def findJnt(self, *args):
self.root = cmds.ls(sl=True)
if len(self.root) == 1:
selRoot = self.root[0]
rootBt = cmds.textField ('rootJnt', e=1, tx=selRoot)
else:
cmds.warning ('Please select only the first joint!')
def colorChange(self, *args):
self.limbSide = cmds.optionMenu('Part_Side', q=1, sl=1)
if self.limbSide ==1:
self.sideColor = 7
if self.limbSide == 2:
self.sideColor = 14
if self.limbSide ==3:
self.sideColor = 18
colorBt = cmds.colorIndexSliderGrp('rigColor', e=1, v=self.sideColor)
def partBody(self, *args):
pass
def rigType(self, *args):
pass
rigCreator()
I did just clean the lambda and comma function and it was working as expected for me.
Because you don't seem to be familiar with maya ui/class, I've cleaned your whole script with some line comments beginning with DW :
So you can find how to manage maya ui more easily !
Don't hesitate to look at partial, I've lots of posts with some tips, hope it helps :
import maya.cmds as cmds
import maya.OpenMaya as om
import maya.mel as mel
import sys
import math
from functools import partial
def create(rigType, sideColor, isStretch, *args):
# DW : refreshed every time by property that is executing before being passed
if rigType == 0:
print('IK/FK mode')
elif rigType == 1:
print('IK mode')
elif rigType == 2:
print('FK mode')
# DW : in this state of the script, it will return always 7 because there is no refresh query
# so you can see to pass outside your class some arguments
# below an example for an alternative of property
print('color picked is {}'.format(sideColor))
# DW : in this example the function for getting the status is passed
# it is different from property because we still use to execute the command
if isStretch():
print('rig is stretchy')
else:
print('no stretch')
# Dw : note that if you create an instance of your window inside a variable :
# ui = rigCreator()
# ui.ikfkPick is something you can print at any moment to debug
# any variable with 'self' would be replaced by 'ui' in this case
class rigCreator:
# DW : default colour, self don't need to be specify but it is like so
# personally I put the important parameters or parameters that set default values on top
# it is easier when parsing the code few month later
sideColor = 7 #: DW : don't hesitate to do inline comment to say this is RED color (im followng google doctstring https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html)
isStretch = 0
ikfkPick = 0 #: pick IK/FK option, 1 for IK, 2 for FK
def __init__(self, *args):
#self.startFunction()
self.window = "uiWindow"
self.title = "Rigging Tool Bipeds"
self.winSize = (150, 200)
self.createUI()
def createUI(self):
# DW : only put *args if you are using maya command flags, otherwise no need
#check if window and prefs exist. If yes, delete
if cmds.window(self.window, ex=True):
cmds.deleteUI(self.window, wnd=True)
elif cmds.windowPref(self.window, ex=True):
cmds.windowPref(self.window, r=True)
self.window = cmds.window(self.window, t=self.title, wh = self.winSize, s=1, mnb=1, mxb=1)
self.mainForm = cmds.formLayout(nd=100)
self.tagLine= cmds.text(label = "Rig Tool")
cmds.frameLayout(label="1. Choose the root joint")
self.Layout = cmds.columnLayout(adj=1)
#Add a saparator to the window
cmds.separator()
# button to select the first joint
cmds.rowColumnLayout (nc = 2, cs=[(1,6), (2,6)])
self.tf_rootBt = cmds.textField ('rootJnt', tx = 'First joint of your Arm chain', w = 250)
#cmds.textFieldButtonGrp('rootJnt', width=380, cal=(8, "center"), cw3=(100, 200, 75), h=40, pht="First joint of your Arm chain", l="First Joint", bl="Select", bc = lambda x: findJnt(), p=self.Layout)
# DW : use lambda only to parse arguments, the better way to not make confusion is to use partial module
cmds.button(l = 'Select', c = self.findJnt)
cmds.setParent(self.Layout)
frame = cmds.frameLayout("2. Name options", lv=1, li=1, w=250)
#cmds.text(label="", align = "left")
cmds.rowColumnLayout(nc=4, cs=[(1,6), (2,6), (3,6), (4,6)])
#cmds.text('Side', l='Side:')
# DW : string naming can be dangerous, if another script create this type of name, it will conflict
# put your ctrl name into a variable or a class variable if you are using it somewhere else
# Same don't use lambda if you are not parsing arguments
self.om_partside = cmds.optionMenu('Part_Side', l='Side:', cc=self.colorChange, acc=1, ni=1)
cmds.menuItem(label='L_')
cmds.menuItem(label='R_')
cmds.menuItem(label='_')
cmds.text('Part', l='Part:')
cmds.optionMenu('part_Body')
cmds.menuItem(label='Arm')
cmds.menuItem(label='Leg')
cmds.menuItem(label='Spine')
cmds.setParent(self.Layout)
frame2 = cmds.frameLayout("3. Type of rig", lv=True, li=1, w=250)
cmds.rowColumnLayout(nc=3, cs=[(1,6), (2,6), (3,6)])
# DW :conforming to the default user settings on top of the class
# demonstrate property in class
self.rc_ikfk = cmds.radioCollection("limb side")
for x, lb in enumerate(['IK/FK', 'IK', 'FK']):
if x == self.ikfkPick:
defvalue = True
else:
defvalue = False
cmds.radioButton(label=lb, select=defvalue)
cmds.setParent(self.Layout)
frame3 = cmds.frameLayout("4. Thick if you want to apply stretch", lv=True, li=1, w=250)
cmds.rowColumnLayout(nc=1, cs=[(1,6)])
# DW : adding default value, class variable naming
self.ckb_stretch = cmds.checkBox( label='Stretchy limb', align='center', value=self.isStretch, cc=self.getStretch)
cmds.setParent(self.Layout)
cmds.setParent(self.Layout)
frame4 = cmds.frameLayout("5. Pick icon color", lv=True, li=1, w=250)
cmds.gridLayout(nr=1, nc=5, cwh=[62,20])
cmds.iconTextButton('darkBlue_Btn', bgc=[.000,.016,.373])
cmds.iconTextButton('lightBlue_Btn', bgc=[0,0,1])
cmds.iconTextButton('Brown_Btn', bgc=[.537,.278,.2])
cmds.iconTextButton('red_Btn', bgc=[1,0,0])
cmds.iconTextButton('Yellow_Btn', bgc=[1,1,0])
cmds.setParent(self.Layout)
self.sl_colorBt = cmds.colorIndexSliderGrp('rigColor', w=250, h=50, cw2=(150,0), min=0, max=31, v= self.sideColor)
#This button will creat the chain of joins with streatch and squach
# DW : never use comma for executing some function, if you use this script as a module afterward, you will have problems dealing with python namespacing
# maya is always passing a default argument True, so put *args in any command that is used by your ui controls
cmds.button('b_create', label='Create', h=30, c=partial(create, self.rigType, self.sideColor, self.getStretch))
#show the window
cmds.showWindow(self.window)
def findJnt(self, *args):
self.root = cmds.ls(sl=True, type='joint', l=True)
# DW : just becaue I was toying around the concept of root, sorry... ignor below
rootAbove = [i for i in self.root[0].split('|')[1:] if cmds.nodeType(i) == 'joint']
if rootAbove[0] != self.root[0].split('|')[-1]:
cmds.warning('you didn\'t choose the top joint !')
if len(self.root) == 1:
selRoot = self.root[0]
# DW : you dont have to store your edit, use class variable instead of string name
cmds.textField (self.tf_rootBt, e=1, tx=selRoot.split('|')[-1])
else:
cmds.warning ('Please select only the first joint!')
def colorChange(self, *args):
# DW : you don't need to put self on limbSide has you are not using it anywhere else
limbSide = cmds.optionMenu(self.om_partside, q=1, sl=1)
# DW : putting some elif
if limbSide == 1:
self.sideColor = 7
elif limbSide == 2:
self.sideColor = 14
elif limbSide ==3:
self.sideColor = 18
# DW : conforming editing controls
cmds.colorIndexSliderGrp(self.sl_colorBt, e=1, v=self.sideColor)
def partBody(self, *args):
pass
#property
def rigType(self):
'''Returns:
int: option selected in ui'''
# DW : demonstrating property usage and partial
rb = cmds.radioCollection(self.rc_ikfk, q=True, select=True)
cia = [i.split('|')[-1] for i in cmds.radioCollection(self.rc_ikfk, q=True, cia=True)]
return cia.index(rb)
def getStretch(self, *args):
"""function to get if the user need stretchy things
Note:
*args is just here to bypass maya default added True argument
Returns:
bool: use stretch or not
"""
# dw making some example function with some docstring
self.isStretch = cmds.checkBox(self.ckb_stretch, q=True, value=True)
return self.isStretch
rigCreator()
I am writing an app that dynamically adds and removes widgets to a QScrollView. The code below, using Qt3 and python, will give me dynamic widgets, but when I add too many to be seen, no scroll bar appears. It is not yet scrollable. I've put the relevant pieces of code below.
Any answers must be in Qt3 because my company only uses Qt3. I'm new to programming and Qt in general.
PL = parser.Plist()
class EC_Conf_App(QDialog):
def __init__(self,parent = None,name = None,modal = 0,fl = 0):
QDialog.__init__(self,parent,name,modal,fl)
self.gridLayout = QGridLayout(self)
self.scrollArea = QScrollView(self)
self.scrollArea.setGeometry(0, 0, 369, 286)
self.Form1Layout = QGridLayout(self.scrollArea)
self.gridLayout.addWidget(self.scrollArea, 0, 0)
for item in PL.plist:
self.section_create(item.name, item.variables)
def section_create(self, name, variables):
# ADD ROW BUTTON
for key, value in sorted(variables.iteritems()):
if len(value) > 3: # if there is more than one option for the combobox
self.addButton = QPushButton(self.scrollArea, name + '_AddButton')
self.addButton.setText('Add Row')
self.Form1Layout.addWidget(self.addButton, self.Ay, self.Ax)
self.addButton.show()
self.connect(self.addButton,SIGNAL("clicked()"),self.add_rows)
def add_rows(self):
self.addButton = self.sender()
self.addButton.name()
copy_class = self.addButton.name()
clean_name = copy_class[:-10]
for item in PL.plist:
if item.name == clean_name:
PL.insert(item.name, item.heading, item.variables)
self.remove_widgets()
break
def remove_widgets(self):
for item in self.widgets:
item.deleteLater()
self.Form1Layout.remove(item)
self.construct()
def construct(self):
for item in PL.plist:
self.section_create(item.name, item.variables)
The only way to use a layout with a QScrollView is to set the layout on its viewport(), not the view itself. This is documented.
Replace self.Form1Layout = QGridLayout(self.scrollArea) with
self.Form1Layout = QGridLayout(self.scrollArea.viewport())
This question deals with the same problem for Qt4/5.
So I'm trying to add the "text" associated with a checked checkbox to a list as soon as they're checked, and I'm doing this:
class Interface(QtGui.QMainWindow):
def __init__(self):
super(Interface, self).__init__()
self.initUI()
self.shops=[]
def initUI(self):
widthPx = 500
heightPx = 500
self.setGeometry(100,100,widthPx,heightPx)
#menus
fileMenu = menuBar.addMenu("&File")
helpMenu = menuBar.addMenu("&Help")
#labels
shopList = _getShops()
for i, shop in enumerate(shopList):
cb = QtGui.QCheckBox(shop, self)
cb.move(20, 15*(i)+50)
cb.toggle()
cb.stateChanged.connect(self.addShop)
self.setWindowTitle("Absolute")
self.show()
def addShop(self, state):
if state == QtCore.Qt.Checked:
#I want to add the checkbox's text
self.shops.append('IT WORKS')
else:
self.shops.remove('IT WORKS')
But instead of adding "IT WORKS" I want to add the text associated with the checkbox that was just selected.
I usually pass additionnal parameters in my signals/slots using partial
Functools doc
You can use it to pass your checkbox text.
First, import partial:
from functools import partial
Then, change your connect() method and pass your checkbox text:
cb.stateChanged.connect( partial( self.addShop, shop) )
To finish, update your addShop() method:
def addShop(self, shop, state):
if state == Qt.Checked:
self.shops.append(shop)
else:
try:
self.shops.remove(shop)
except:
print ""
Notes:
I've added a try/except at the end because your checkboxes are checked by default. When you uncheck them, it tries to remove an unknow item from your self.shops list.
With this method, this is not the current checkbox text which is send to your method. It it the first text that was used to initialize your checkboxes. If, during the execution of your script, you modify the checkbox text, it will not be updated in your addShop method.
Update:
In fact, you can pass your checkbox in the partial:
cb.stateChanged.connect( partial( self.addShop, cb) )
and retrieve it this way:
def addShop(self, shop, state):
if state == Qt.Checked:
self.shops.append(shop.text())
else:
try:
self.shops.remove(shop.text())
except:
print ""