Search code examples
pythonpython-3.xpyqtpyqt5qtreewidgetitem

How to fetch comboBox.currentText() and SpinBox.text() from within a treeWidgetItem


Context:

I have a QTreeWidgetItem that I'd like to save as a JSON file. The QTreeWidgetItem is composed of three columns ["Name", "Type", "Value"]. name is a line edit, type is a comboBox, value is a spinBox. I'd like to retrieve value of each field and on each line to save them in the JSON

Tries:

There's no problem to retrieve the name, but I can't find how to retrieve the values from the comboBox and spinBox.

I've first tried to retrieve data in the simpliest form (node being a QTreeWidgetItem):

type = node.text(1)
value= node.text(2)

I've tried QTreeWidgetItem.data(int column, int role) but not quite understood the parameter "role" needed for this method.

Some code:

python file:

from qgis.PyQt import uic, QtCore, QtGui
import os, sys
import qgis.core
import logging

try:
    from qgis.PyQt.QtGui import (QWidget,QDialog,QMainWindow,QApplication,QTreeWidgetItem,
                                 QMenu,QToolButton,QSpinBox,QDoubleSpinBox,QHeaderView,QLineEdit,
                                 QTextBrowser,QTreeWidgetItemIterator, QComboBox, QLabel, UserRole)
except ImportError:
    from qgis.PyQt.QtWidgets import (QWidget,QDialog,QMainWindow,QApplication,QTreeWidgetItem,
                                     QMenu,QToolButton,QSpinBox,QDoubleSpinBox,QHeaderView,QLineEdit,
                                     QTextBrowser,QTreeWidgetItemIterator, QComboBox, QLabel)

import pprint
import json

class AMCWindow(QDialog):

    def __init__(self, parent=None, dbase=None):
        super(AMCWindow, self).__init__(parent=parent)
        uipath = os.path.join(os.path.dirname(__file__), 'amcwindow1.ui')
        uic.loadUi(uipath, self)
        self.dict = {}

        # treeWidget
        self.treeWidget.setColumnCount(3)
        self.treeWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        self.treeWidget.customContextMenuRequested.connect(self.openMenu)

        self.maintreewdgitem = QTreeWidgetItem(self.treeWidget.invisibleRootItem(), ['note'])

        headerlist = ["Name", "Type", "Value"]
        self.treeWidget.setHeaderItem(QTreeWidgetItem(headerlist))

        # Right click on criteria
        self.menu = QMenu()
        self.menu.addAction(self.tr("Add criteria"))
        self.menu.addAction(self.tr("Remove criteria"))
        self.menu.triggered.connect(self.menuAction)

        # Click on save
        self.buttonSave.clicked.connect(self.saveClicked)

        self.menuitem = None
        self.itemnameediting = None

    def openMenu(self, position):
        self.menuitem = self.treeWidget.currentItem()
        self.menu.exec_(self.treeWidget.viewport().mapToGlobal(position))

    def menuAction(self, actionname):
        if actionname.text() == self.tr("Add criteria"):
            qtreewidgetitm = QTreeWidgetItem(self.menuitem, ["New criteria"])

            # Type
            qComboBox = QComboBox()
            qComboBox.addItems(["Pondération", "Classe"])
            self.treeWidget.setItemWidget(qtreewidgetitm, 1, qComboBox)
            # qComboBox.setProperty("widgetitem", qtreewidgetitm)

            # Value
            spinbox = QDoubleSpinBox()
            self.treeWidget.setItemWidget(qtreewidgetitm, 2, spinbox)
            self.menuitem.setExpanded(True)

            return qtreewidgetitm

        elif actionname.text() == self.tr("Remove criteria"):
            self.menuitem.parent().removeChild(self.menuitem)

    def saveClicked(self):
        print("> Save clicked")
        self.dict.clear()
        self.visitTree(self.maintreewdgitem, self.dict)

        # Define directory
        directory = "C://Users//jean.robertou//Desktop//file.json"
        # Creates file and writes queryDict as json
        with open(directory, "w") as file:
            json.dump(self.dict, file)

    def visitTree(self, node, dct):
        """
        Iterate over the tree and build dct
        :param node: QTreeWidgetItem, current node
        :param dct: dict, contain elements from current node
        :return:
        """

        print("Enter visitTree")
        print("> visitTree:", node.text(0))

        # Get number of child
        childNr = node.childCount()

        # If node is leaf
        if childNr == 0:
            print(">", node.text(0), "has no child")
            type = node.text(1) # <----- Need help here
            value= node.text(2) # <----- Need help here

            # Set default dict values
            dct.setdefault("Type", type)
            dct.setdefault("Value", value)

        # If node has children
        else:
            print(">", node.text(0), "has children")
            # Add children to dct
            for childItem in range(childNr):
                child = node.child(childItem)
                dct.setdefault(child.text(0), {})
                # Loop through children recursively
                self.visitTree(child, dct[child.text(0)])


        print("Exit visitTree")

GUI file (amcwindow1.ui):

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>Dialog</class>
 <widget class="QDialog" name="Dialog">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>941</width>
    <height>518</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>Dialog</string>
  </property>
  <layout class="QVBoxLayout" name="verticalLayout" stretch="1,10,1,1">
   <item>
    <widget class="QFrame" name="frame">
     <property name="frameShape">
      <enum>QFrame::StyledPanel</enum>
     </property>
     <property name="frameShadow">
      <enum>QFrame::Raised</enum>
     </property>
     <layout class="QHBoxLayout" name="horizontalLayout">
      <item>
       <widget class="QLabel" name="label">
        <property name="text">
         <string>Nom</string>
        </property>
       </widget>
      </item>
      <item>
       <widget class="QLineEdit" name="lineEdit"/>
      </item>
      <item>
       <widget class="QToolButton" name="buttonSave">
        <property name="text">
         <string>Save</string>
        </property>
       </widget>
      </item>
     </layout>
    </widget>
   </item>
   <item>
    <widget class="QTreeWidget" name="treeWidget">
     <property name="dragEnabled">
      <bool>true</bool>
     </property>
     <property name="dragDropMode">
      <enum>QAbstractItemView::InternalMove</enum>
     </property>
     <property name="alternatingRowColors">
      <bool>true</bool>
     </property>
     <property name="animated">
      <bool>true</bool>
     </property>
     <property name="wordWrap">
      <bool>true</bool>
     </property>
     <property name="columnCount">
      <number>3</number>
     </property>
     <attribute name="headerDefaultSectionSize">
      <number>50</number>
     </attribute>
     <attribute name="headerStretchLastSection">
      <bool>false</bool>
     </attribute>
     <column>
      <property name="text">
       <string notr="true">1</string>
      </property>
     </column>
     <column>
      <property name="text">
       <string notr="true">2</string>
      </property>
     </column>
     <column>
      <property name="text">
       <string notr="true">3</string>
      </property>
     </column>
    </widget>
   </item>
   <item>
    <widget class="QFrame" name="frame_2">
     <property name="frameShape">
      <enum>QFrame::StyledPanel</enum>
     </property>
     <property name="frameShadow">
      <enum>QFrame::Raised</enum>
     </property>
     <layout class="QGridLayout" name="gridLayout">
      <item row="0" column="1">
       <widget class="QLineEdit" name="lineEdit_sqlfinal"/>
      </item>
      <item row="0" column="0">
       <widget class="QLabel" name="label_2">
        <property name="text">
         <string>Final SQL</string>
        </property>
       </widget>
      </item>
      <item row="0" column="2">
       <widget class="QToolButton" name="toolButton_testsql">
        <property name="text">
         <string>Test SQL</string>
        </property>
       </widget>
      </item>
      <item row="1" column="0" colspan="3">
       <widget class="QTextBrowser" name="textBrowser_res">
        <property name="maximumSize">
         <size>
          <width>16777215</width>
          <height>90</height>
         </size>
        </property>
       </widget>
      </item>
     </layout>
    </widget>
   </item>
   <item>
    <widget class="QDialogButtonBox" name="buttonBox">
     <property name="orientation">
      <enum>Qt::Horizontal</enum>
     </property>
     <property name="standardButtons">
      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
     </property>
    </widget>
   </item>
  </layout>
 </widget>
 <resources/>
 <connections>
  <connection>
   <sender>buttonBox</sender>
   <signal>accepted()</signal>
   <receiver>Dialog</receiver>
   <slot>accept()</slot>
   <hints>
    <hint type="sourcelabel">
     <x>248</x>
     <y>254</y>
    </hint>
    <hint type="destinationlabel">
     <x>157</x>
     <y>274</y>
    </hint>
   </hints>
  </connection>
  <connection>
   <sender>buttonBox</sender>
   <signal>rejected()</signal>
   <receiver>Dialog</receiver>
   <slot>reject()</slot>
   <hints>
    <hint type="sourcelabel">
     <x>316</x>
     <y>260</y>
    </hint>
    <hint type="destinationlabel">
     <x>286</x>
     <y>274</y>
    </hint>
   </hints>
  </connection>
 </connections>
</ui>

Expected ouput:

A single line QTreeWidgetItem, for instance: "nameExample" | Ponderation | 2

The JSON should look like:

{"nameExample": {"Type": "Ponderation", "Value": 2}


Solution

  • I have the following comments about your code:

    • I think you've had an error in the lines: self.treeWidget.setItemWidget(qtreewidgetitm, 2, qComboBox) and self.treeWidget.setItemWidget(qtreewidgetitm, 3, spinbox) since the columns are 2 and 3 but the indices in Qt start from 0 so they must be 1 and 2, respectively.

    • When you add a child, use a text different from the previous one because when you build the dictionary you will have problems because the duplicate elements will not be shown. It may not be necessary for the implementation of your project but when testing your current code, look at this problem.

    • If you want to obtain the data shown in the widget, you must first obtain the widget using the itemWidget() method.

    Considering the above, the solution is:

    class AMCWindow(QDialog):
        def __init__(self, parent=None, dbase=None):
            super(AMCWindow, self).__init__(parent=parent)
            uipath = os.path.join(os.path.dirname(__file__), "amcwindow1.ui")
            uic.loadUi(uipath, self)
            self.dict = {}
    
            # treeWidget
            self.treeWidget.setColumnCount(3)
            self.treeWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
            self.treeWidget.customContextMenuRequested.connect(self.openMenu)
    
            self.maintreewdgitem = QTreeWidgetItem(
                self.treeWidget.invisibleRootItem(), ["note"]
            )
    
            headerlist = ["Name", "Type", "Value"]
            self.treeWidget.setHeaderItem(QTreeWidgetItem(headerlist))
    
            # Right click on criteria
            self.menu = QMenu()
            self.menu.addAction(self.tr("Add criteria"))
            self.menu.addAction(self.tr("Remove criteria"))
            self.menu.triggered.connect(self.menuAction)
    
            # Click on save
            self.buttonSave.clicked.connect(self.saveClicked)
    
            self.menuitem = None
            self.itemnameediting = None
    
            self.counter = 0
    
        def openMenu(self, position):
            self.menuitem = self.treeWidget.currentItem()
            self.menu.exec_(self.treeWidget.viewport().mapToGlobal(position))
    
        def menuAction(self, actionname):
            if actionname.text() == self.tr("Add criteria"):
                qtreewidgetitm = QTreeWidgetItem(
                    self.menuitem, ["New criteria(%s)" % (self.counter)]
                )
                self.counter += 1
    
                # Type
                qComboBox = QComboBox()
                qComboBox.addItems(["Pondération", "Classe"])
                self.treeWidget.setItemWidget(qtreewidgetitm, 1, qComboBox)
                # qComboBox.setProperty("widgetitem", qtreewidgetitm)
    
                # Value
                spinbox = QDoubleSpinBox()
                self.treeWidget.setItemWidget(qtreewidgetitm, 2, spinbox)
                self.menuitem.setExpanded(True)
    
                return qtreewidgetitm
    
            elif actionname.text() == self.tr("Remove criteria"):
                self.menuitem.parent().removeChild(self.menuitem)
    
        def saveClicked(self):
            print("> Save clicked")
            self.dict.clear()
            self.visitTree(self.maintreewdgitem, self.dict)
    
            # Define directory
            directory = "C://Users//jean.robertou//Desktop//file.json"
            # Creates file and writes queryDict as json
            with open(directory, "w") as file:
                json.dump(self.dict, file)
    
        def visitTree(self, node, dct):
            """
            Iterate over the tree and build dct
            :param node: QTreeWidgetItem, current node
            :param dct: dict, contain elements from current node
            :return:
            """
    
            print("Enter visitTree")
            print("> visitTree:", node.text(0))
    
            # Get number of child
            childNr = node.childCount()
    
            # If node is leaf
            if childNr == 0:
                print(">", node.text(0), "has no child")
                qComboBox = self.treeWidget.itemWidget(node, 1)
                type = qComboBox.currentText()
                spinbox = self.treeWidget.itemWidget(node, 2)
                value = spinbox.value()
                dct.setdefault("Type", type)
                dct.setdefault("Value", value)
            # If node has children
            else:
                print(">", node.text(0), "has children")
                # Add children to dct
                for childItem in range(childNr):
                    child = node.child(childItem)
                    dct.setdefault(child.text(0), {})
                    # Loop through children recursively
                    self.visitTree(child, dct[child.text(0)])
    
            print("Exit visitTree")