0

I am attempting to generate output information in a tabbed layout using PyQT5. In the case that I have multiple devices, I would expect multiple tabs to show up in the output with information corresponding to their respective device.

Image of my current output

For some reason, I am getting a single tab, and a single button. Basically like the information is getting written over. The issue is that I do not know how to dynamically add new tabs and hold on to all of the information inside them properly upon each iteration. I will not know the number of devices or device parameters ahead of time, so I can not set this information in code. It must be dynamic.

Here is my current code:

# $description: Testing
# $show-in-menu

import pya
import json
import re, sys
from pe import generateDeviceParameters
from datetime import datetime
import os
import subprocess, os, platform
import functools

class CommandWindow( pya.QPlainTextEdit ):
    def __init__( self, parent ):
        super( CommandWindow, self ).__init__( parent )
        self.setPlaceholderText( "Command Window" )
        self.setReadOnly( True )
    
    def log( self, text ):
        now = datetime.now()
        current_time = now.strftime("%H:%M:%S")
        self.insertPlainText(f"{current_time}: {text}\n")
        self.repaint()
        
class CommandWindowButton( pya.QPushButton ):
    def __init__( self, parent ):
       super( CommandWindowButton, self ).__init__( parent )
       self.app = parent
       self.text = "Clear Command Window"
       
    def clearText( self ):
        self.app.commandWindow.setPlainText( "" )

class MyApp( pya.QDialog ):
  def __init__( self, parent ):
    super( MyApp, self ).__init__( parent )
    
    # Change title
    self.windowTitle = 'Automated Parameter Extraction'
    self.resize( 1250, 500 )
    
    # Set application font
    self.font = pya.QFont("Arial", 10)
    self.setFont(self.font)
    
    # Create a vertical layout for the main widget
    self.layout = pya.QGridLayout()
    self.setLayout(self.layout)

    # Create the first panel
    self.first_panel = pya.QWidget()
    self.first_layout = pya.QVBoxLayout()
    self.first_panel.setLayout(self.first_layout)

    # Create and connect the combo box to switch between pages
    self.deviceCombo = pya.QComboBox()
    self.deviceCombo.addItems( ["Multiple Devices (Config File)"] )
    self.deviceCombo.activated = self.switchPage
    self.first_layout.addWidget( self.deviceCombo )

    # Create the inputs related to a single device
    self.createPages()
    self.first_layout.addWidget( self.deviceGroupBox )
    
    # Create command window
    self.commandWindow = CommandWindow( self )
    self.first_layout.addWidget( self.commandWindow )
    self.cwClearButton = CommandWindowButton( self )
    self.cwClearButton.clicked = self.cwClearButton.clearText
    self.first_layout.addWidget( self.cwClearButton )
    
    # Add first panel to the main layout
    self.layout.addWidget(self.first_panel, 0, 0, 1, 1)
    
    # Create the second panel (scroll layout)
    self.second_panel = pya.QWidget()
    self.second_layout = pya.QVBoxLayout()
    self.second_panel.setLayout( self.second_layout )

    # Create a scroll area
    self.scroll_area = pya.QScrollArea()
    self.scroll_widget = pya.QWidget()
    self.scroll_layout = pya.QVBoxLayout()
    self.scroll_widget.setLayout( self.scroll_layout )
    self.scroll_area.setWidgetResizable(True)
    self.scroll_area.setWidget( self.scroll_widget )

    # Add scroll area to the second layout
    self.second_layout.addWidget( self.scroll_area )

    # Analysis button
    self.ok = pya.QPushButton( "Run Extraction" )
    self.ok.clicked = self.runPE
    self.second_layout.addWidget( self.ok )
    
    # Add second panel to the main layout
    self.layout.addWidget(self.second_panel, 0, 1, 1, 1)

  def createPages( self ):
  
    # Initialize
    self.deviceGroupBox = pya.QGroupBox( 'Extraction Information' )
    self.stackedLayout = pya.QStackedLayout()
    self.deviceGroupBox.setLayout( self.stackedLayout )
    
    self.page1 = pya.QWidget()
    self.page1_layout = pya.QGridLayout()
    
    self.edit1_pane = pya.QWidget()
    self.edit1_layout = pya.QVBoxLayout()
    self.edit1_pane.setLayout( self.edit1_layout )
    
    self.button1_pane = pya.QWidget()
    self.button1_layout = pya.QVBoxLayout()
    self.button1_pane.setLayout( self.button1_layout )
    
    # Create output location edit field
    self.outputLocation = pya.QLineEdit( self )
    self.button1 = pya.QPushButton()
    self.edit1_layout.addWidget( self.outputLocation )
    self.button1_layout.addWidget( self.button1 )
    
    # Create top cell choice edit field
    self.topCell = pya.QLineEdit( self )
    self.button2 = pya.QPushButton()
    self.edit1_layout.addWidget( self.topCell )
    self.button1_layout.addWidget( self.button2 )
    
    # Create unit cell choice edit field
    self.unitCell = pya.QLineEdit( self )
    self.button3 = pya.QPushButton()
    self.edit1_layout.addWidget( self.unitCell )
    self.button1_layout.addWidget( self.button3 )
    
    # Create edge cell choice edit field
    self.edgeCell = pya.QLineEdit( self )
    self.button4 = pya.QPushButton()
    self.edit1_layout.addWidget( self.edgeCell )
    self.button1_layout.addWidget( self.button4 )
    
    # Reformat
    self.page1_layout.addWidget( self.edit1_pane, 0, 0, 1, 1 )
    self.page1_layout.addWidget( self.button1_pane, 0, 1, 1, 1 )
    
    self.page1.setLayout( self.page1_layout )
    self.stackedLayout.addWidget( self.page1 )
    
    # Initialize
    self.page2 = pya.QWidget()
    self.page2_layout = pya.QGridLayout()
    
    self.edit2_pane = pya.QWidget()
    self.edit2_layout = pya.QVBoxLayout()
    self.edit2_pane.setLayout( self.edit2_layout )
    
    self.button2_pane = pya.QWidget()
    self.button2_layout = pya.QVBoxLayout()
    self.button2_pane.setLayout( self.button2_layout )
    
    # Create input files location edit field
    self.inFiles = pya.QLineEdit( self )
    self.button5 = pya.QPushButton()
    self.edit2_layout.addWidget( self.inFiles )
    self.button2_layout.addWidget( self.button5 )
    
    # Create config file location edit field
    self.configFile = pya.QLineEdit( self )
    self.button6 = pya.QPushButton()
    self.edit2_layout.addWidget( self.configFile )
    self.button2_layout.addWidget( self.button6 )
    
    # Reformat
    self.page2_layout.addWidget( self.edit2_pane, 0, 0, 1, 1 )
    self.page2_layout.addWidget( self.button2_pane, 0, 1, 1, 1 )
    
    self.page2.setLayout( self.page2_layout )
    self.stackedLayout.addWidget( self.page2 )
    
  def switchPage( self ):
    self.stackedLayout.setCurrentIndex(self.deviceCombo.currentIndex)
    
  def runPE( self ):
    self.multipleDevice()
    
  def multipleDevice( self ):
    
    # Create tab widget within croll layout
    self.tab_widget = pya.QTabWidget()
    self.scroll_layout.addWidget( self.tab_widget )
    tab = []
    tab_layout = []
    
    data_for_tabs = dict()
    data_for_tabs["Device1"] = dict()
    data_for_tabs["Device1"]["Parameter1"] = dict()
    data_for_tabs["Device1"]["Parameter1"]["x"] = 1.2
    data_for_tabs["Device1"]["Parameter1"]["y"] = 1.5
    data_for_tabs["Device1"]["Parameter2"] = dict()
    data_for_tabs["Device1"]["Parameter2"]["x"] = 1.4
    data_for_tabs["Device1"]["Parameter2"]["y"] = 1.3
    
    data_for_tabs["Device2"] = dict()
    data_for_tabs["Device2"]["Parameter1"] = dict()
    data_for_tabs["Device2"]["Parameter1"]["x"] = 2.2
    data_for_tabs["Device2"]["Parameter1"]["y"] = 2.5
    data_for_tabs["Device2"]["Parameter2"] = dict()
    data_for_tabs["Device2"]["Parameter2"]["x"] = 2.4
    data_for_tabs["Device2"]["Parameter2"]["y"] = 2.3

    # First generate tabs
    tabNumber = 0
    for key in data_for_tabs.keys():
        
        # Create tab
        tab.append( pya.QWidget() )
        tab_layout.append( pya.QVBoxLayout() )
        tab[tabNumber].setLayout( tab_layout[tabNumber] )
        self.tab_widget.addTab( tab[tabNumber], key )
        self.tab = tab[tabNumber]
        self.tab_layout = tab_layout[tabNumber]
        
        self.runExtraction( data_for_tabs[key], self.tab, self.tab_layout )
        tabNumber += 1
        
  def runExtraction( self, data, tab, tab_layout ):
    row = 0
    for key, value in data.items():
        outGroupBox = pya.QGroupBox( key )
        self.out_layout = pya.QHBoxLayout()
        for pkey, pval in value.items():
            txtlbl = pya.QLabel( tab )
            txtlbl.setText(f"{pkey} = ")
            txt = pya.QLineEdit( tab )
            txt.setText(f"{pval}")
            txt.setReadOnly( True )
            
            self.out_layout.addWidget( txtlbl )
            self.out_layout.addWidget( txt )
            
        self.button = pya.QPushButton()
        self.button.text = f"{key}"
        self.out_layout.addWidget( self.button )
        
        # Reformat
        outGroupBox.setLayout( self.out_layout )
        outGroupBox.update()
        setattr( self, "outGroupBox" + str(row), outGroupBox )
        tab_layout.addWidget( getattr( self, "outGroupBox" + str(row) ) )
        row += 1
        
    # Add stretch so everything sits at the top
    tab_layout.addStretch()
        
  
# Create the app instance and run the event loop
peDialog = MyApp( pya.Application.instance().main_window() )
peDialog.show()

Please feel free to leave comments or provide any help. My guess is that I will need to create some sort of class for the tabbed layout, but I am not sure.

I have tried using a variation of setattr and getattr in order to dynamically create tab widgets and layouts for each tab and device parameter. For some reason this causes the app to not see anything however.

I am hoping to see multiple tabs with their respective device. And then each tab should have the information given from that device.

musicamante
  • 41,230
  • 6
  • 33
  • 58
  • First of all, KLayout is *not* PyQt. It provides a Python API against Qt, but it has important differences: it took me a while to understand that they connect signals using the equal sign (I understand the concept, but I find it quite confusing, since it's not an actual assignment). That said, after converting it to *real* standard PyQt, your code *seems* to be fine (I can see two tabs, and two buttons in each of them), so you're probably doing something wrong somewhere else, or you didn't provide a valid [mre] or KLayout does something it shouldn't with python references to Qt objects. – musicamante Jul 19 '23 at 00:53
  • The latter point seems unlikely, but I wouldn't be completely surprised, given the strange implementation of KLayout. Unfortunately, your code style is quite confusing and doesn't help to understand if the problem actually exists: in standard Qt python bindings (PyQt and PySide), when an object is added to a parent and that parent takes its ownership (for instance, adding a widget to a layout of a window) and it doesn't need a persistent reference on the python side; I sincerely doubt that KLayout would take into account object references in Python, but there's no documentation about this. – musicamante Jul 19 '23 at 00:58
  • The only related reference in the documentation is the Qt binding documentation for Ruby, which, actually, shows a very similar concept: to me, "It is safe however to keep a reference to that object" means that it is also safe to *not* keep a reference as long as the Qt object has an owner. That's why creating those lists and instance attributes would make absolutely no sense, at least in PyQt/PySide. It's also unclear the need for `setattr()` and `getattr()`, especially considering that that reference will be overwritten in the next for loop iteration. – musicamante Jul 19 '23 at 01:02

0 Answers0