Skip to content
  • Categories
  • Recent
  • Tags
  • Popular
  • Users
  • Groups
  • Search
  • Get Qt Extensions
  • Unsolved
Collapse
Brand Logo
  1. Home
  2. Qt Development
  3. Qt for Python
  4. Cannot set text on dynamically created QLabel-s when user clicks a button to display the chosen option from a QComboBox
Forum Updated to NodeBB v4.3 + New Features

Cannot set text on dynamically created QLabel-s when user clicks a button to display the chosen option from a QComboBox

Scheduled Pinned Locked Moved Solved Qt for Python
pythonqt for python
13 Posts 2 Posters 704 Views
  • Oldest to Newest
  • Newest to Oldest
  • Most Votes
Reply
  • Reply as topic
Log in to reply
This topic has been deleted. Only users with topic management privileges can see it.
  • D Offline
    D Offline
    dynaspinner
    wrote on last edited by dynaspinner
    #1

    I am new to GUI programming and I was creating a project for a course I am attending. I had to implement a feature where the user can pick an option from a drop-down and confirm it by clicking on a button which displays it on a QLabel below. However, when I choose an option and try to display it on the QLabel, it only displays the selected option from the last card's drop down on its QLabel. I have provided a reduced version of what it is supposed to be in my app below:

    from PySide6 import QtWidgets
    import sys
    
    app = QtWidgets.QApplication([])
    
    main_window = QtWidgets.QWidget()
    main_window_layout = QtWidgets.QVBoxLayout()
    main_window.setLayout(main_window_layout)
    
    cards_container_layout = QtWidgets.QHBoxLayout()
    main_window_layout.addLayout(cards_container_layout)
    
    for _ in range(3):
        card = QtWidgets.QWidget()
        card_layout = QtWidgets.QVBoxLayout()
        card.setLayout(card_layout)
    
        option_container_layout = QtWidgets.QHBoxLayout()
        card_layout.addLayout(option_container_layout)
    
        options = QtWidgets.QComboBox()
        options.setPlaceholderText("Choose an option")
        options.addItem("foo")
        options.addItem("bar")
        option_container_layout.addWidget(options)
    
        confirm_cur_option = QtWidgets.QPushButton("Add")
        option_container_layout.addWidget(confirm_cur_option)
    
        display_cur_option = QtWidgets.QLabel()
        card_layout.addWidget(display_cur_option)
    
        confirm_cur_option.clicked.connect(lambda : display_cur_option.setText(options.currentText()))
        confirm_cur_option.clicked.connect(lambda : print("clicked"))
    
        cards_container_layout.addWidget(card)
    
    main_window.show()
    sys.exit(app.exec())
    

    Result:
    68fa4ede-52de-4db3-9ff6-db42d0d460ea-image.png

    All buttons are emitting the clicked signal but it somehow only works on the last card's QLabel as you can see.

    PySide version is 6.9.0 installed via pip install pyside6==6.9.0 in a virtual environment.

    JonBJ 1 Reply Last reply
    0
    • D dynaspinner

      I am new to GUI programming and I was creating a project for a course I am attending. I had to implement a feature where the user can pick an option from a drop-down and confirm it by clicking on a button which displays it on a QLabel below. However, when I choose an option and try to display it on the QLabel, it only displays the selected option from the last card's drop down on its QLabel. I have provided a reduced version of what it is supposed to be in my app below:

      from PySide6 import QtWidgets
      import sys
      
      app = QtWidgets.QApplication([])
      
      main_window = QtWidgets.QWidget()
      main_window_layout = QtWidgets.QVBoxLayout()
      main_window.setLayout(main_window_layout)
      
      cards_container_layout = QtWidgets.QHBoxLayout()
      main_window_layout.addLayout(cards_container_layout)
      
      for _ in range(3):
          card = QtWidgets.QWidget()
          card_layout = QtWidgets.QVBoxLayout()
          card.setLayout(card_layout)
      
          option_container_layout = QtWidgets.QHBoxLayout()
          card_layout.addLayout(option_container_layout)
      
          options = QtWidgets.QComboBox()
          options.setPlaceholderText("Choose an option")
          options.addItem("foo")
          options.addItem("bar")
          option_container_layout.addWidget(options)
      
          confirm_cur_option = QtWidgets.QPushButton("Add")
          option_container_layout.addWidget(confirm_cur_option)
      
          display_cur_option = QtWidgets.QLabel()
          card_layout.addWidget(display_cur_option)
      
          confirm_cur_option.clicked.connect(lambda : display_cur_option.setText(options.currentText()))
          confirm_cur_option.clicked.connect(lambda : print("clicked"))
      
          cards_container_layout.addWidget(card)
      
      main_window.show()
      sys.exit(app.exec())
      

      Result:
      68fa4ede-52de-4db3-9ff6-db42d0d460ea-image.png

      All buttons are emitting the clicked signal but it somehow only works on the last card's QLabel as you can see.

      PySide version is 6.9.0 installed via pip install pyside6==6.9.0 in a virtual environment.

      JonBJ Online
      JonBJ Online
      JonB
      wrote on last edited by JonB
      #2

      @dynaspinner
      This is tricky/non-intuitive when you use Python and lambdas!

      There are several ways to skin a cat, and here we might do it a different way if we didn't want to use a lambda. But let's say we do. Then the problem is how/when the statement

      confirm_cur_option.clicked.connect(lambda : display_cur_option.setText(options.currentText()))
      

      gets interpreted. Here the confirm_cur_option is treated the way you would expect --- as the line is hit it takes the current value of confirm_cur_option which is the latest QPushButton just created in the loop, so each one in turn, as you would expect. So far so good.

      But being in a lambda the display_cur_option and options variables do not get executed as you would/might expect. It is not interpreted as the values at the time the connect() is executed. Rather its evaluation delayed. The values are taken only when the lambda is being executed at the instant the signal calls the lambda slot because of the connection. That means the values of both display_cur_option and options will always be the last value they had when your loop executed, and hence they are always just the final QLabel and QPushButton.

      You have to do some Python lambda tomfoolery to achieve what you intend as follows:

      confirm_cur_option.clicked.connect(lambda cur_opt=display_cur_option, opt=options : cur_opt.setText(opt.currentText()))
      

      You can use whatever you like for the "new" variable names to the left of the = symbol, they are like formal parameters, some people even give them identical names to the actual parameter being passed in. It doesn't matter, I have chosen different names to make it clearer.

      This (somehow! it makes it more like a function call with parameters passed in from caller instead of local variables in the outside world which happen to be sitting around for use) makes Python do what you intend, so the values of the variables inside the lambda body will be captured from what they were at the time the connect() was executed, not just local variables outside the lambda which have the value they happen to have been set to the last time they were changed.

      When I first did lambdas in Python (I'm more a C++-er) I was totally confused by this behaviour, and so have others been, so you are not alone facing this issue!

      D 2 Replies Last reply
      2
      • JonBJ JonB

        @dynaspinner
        This is tricky/non-intuitive when you use Python and lambdas!

        There are several ways to skin a cat, and here we might do it a different way if we didn't want to use a lambda. But let's say we do. Then the problem is how/when the statement

        confirm_cur_option.clicked.connect(lambda : display_cur_option.setText(options.currentText()))
        

        gets interpreted. Here the confirm_cur_option is treated the way you would expect --- as the line is hit it takes the current value of confirm_cur_option which is the latest QPushButton just created in the loop, so each one in turn, as you would expect. So far so good.

        But being in a lambda the display_cur_option and options variables do not get executed as you would/might expect. It is not interpreted as the values at the time the connect() is executed. Rather its evaluation delayed. The values are taken only when the lambda is being executed at the instant the signal calls the lambda slot because of the connection. That means the values of both display_cur_option and options will always be the last value they had when your loop executed, and hence they are always just the final QLabel and QPushButton.

        You have to do some Python lambda tomfoolery to achieve what you intend as follows:

        confirm_cur_option.clicked.connect(lambda cur_opt=display_cur_option, opt=options : cur_opt.setText(opt.currentText()))
        

        You can use whatever you like for the "new" variable names to the left of the = symbol, they are like formal parameters, some people even give them identical names to the actual parameter being passed in. It doesn't matter, I have chosen different names to make it clearer.

        This (somehow! it makes it more like a function call with parameters passed in from caller instead of local variables in the outside world which happen to be sitting around for use) makes Python do what you intend, so the values of the variables inside the lambda body will be captured from what they were at the time the connect() was executed, not just local variables outside the lambda which have the value they happen to have been set to the last time they were changed.

        When I first did lambdas in Python (I'm more a C++-er) I was totally confused by this behaviour, and so have others been, so you are not alone facing this issue!

        D Offline
        D Offline
        dynaspinner
        wrote on last edited by dynaspinner
        #3

        @JonB Thanks for the response! I tried out the code but unfortunately I have a new problem now. For whatever reason, it thinks the first parameter is a bool type

        Traceback (most recent call last):
          File "/home/dyna_spinner/PyProgramming/Projects/test.py", line 33, in <lambda>
            confirm_cur_option.clicked.connect(lambda cur_opt=display_cur_option, opt=options : cur_opt.setText(opt.currentText()))
                                                                                                ^^^^^^^^^^^^^^^
        AttributeError: 'bool' object has no attribute 'setText'
        

        Swapping the parameters' position around gives the same error saying the first parameter is a "bool" object.

        On a side note about lambdas, I faced another problem with it a while ago where I called self.parentWidget().layout().setCurrentIndex(0) to return to the first item in the stack layout which said "NoneType has no attribute layout()" but moving it to a function fixed the error. It only returned the error when instantiating the object. I think it was due to evaluating self.parentWidget().layout().setCurrentIndex(0) during instantiating but I am not sure if that's the reason.

        D 2 Replies Last reply
        0
        • D dynaspinner

          @JonB Thanks for the response! I tried out the code but unfortunately I have a new problem now. For whatever reason, it thinks the first parameter is a bool type

          Traceback (most recent call last):
            File "/home/dyna_spinner/PyProgramming/Projects/test.py", line 33, in <lambda>
              confirm_cur_option.clicked.connect(lambda cur_opt=display_cur_option, opt=options : cur_opt.setText(opt.currentText()))
                                                                                                  ^^^^^^^^^^^^^^^
          AttributeError: 'bool' object has no attribute 'setText'
          

          Swapping the parameters' position around gives the same error saying the first parameter is a "bool" object.

          On a side note about lambdas, I faced another problem with it a while ago where I called self.parentWidget().layout().setCurrentIndex(0) to return to the first item in the stack layout which said "NoneType has no attribute layout()" but moving it to a function fixed the error. It only returned the error when instantiating the object. I think it was due to evaluating self.parentWidget().layout().setCurrentIndex(0) during instantiating but I am not sure if that's the reason.

          D Offline
          D Offline
          dynaspinner
          wrote on last edited by
          #4

          @dynaspinner I printed the values and the first parameter always gives "False". It somehow converts it to a boolean but I don't know why.

          1 Reply Last reply
          0
          • JonBJ JonB

            @dynaspinner
            This is tricky/non-intuitive when you use Python and lambdas!

            There are several ways to skin a cat, and here we might do it a different way if we didn't want to use a lambda. But let's say we do. Then the problem is how/when the statement

            confirm_cur_option.clicked.connect(lambda : display_cur_option.setText(options.currentText()))
            

            gets interpreted. Here the confirm_cur_option is treated the way you would expect --- as the line is hit it takes the current value of confirm_cur_option which is the latest QPushButton just created in the loop, so each one in turn, as you would expect. So far so good.

            But being in a lambda the display_cur_option and options variables do not get executed as you would/might expect. It is not interpreted as the values at the time the connect() is executed. Rather its evaluation delayed. The values are taken only when the lambda is being executed at the instant the signal calls the lambda slot because of the connection. That means the values of both display_cur_option and options will always be the last value they had when your loop executed, and hence they are always just the final QLabel and QPushButton.

            You have to do some Python lambda tomfoolery to achieve what you intend as follows:

            confirm_cur_option.clicked.connect(lambda cur_opt=display_cur_option, opt=options : cur_opt.setText(opt.currentText()))
            

            You can use whatever you like for the "new" variable names to the left of the = symbol, they are like formal parameters, some people even give them identical names to the actual parameter being passed in. It doesn't matter, I have chosen different names to make it clearer.

            This (somehow! it makes it more like a function call with parameters passed in from caller instead of local variables in the outside world which happen to be sitting around for use) makes Python do what you intend, so the values of the variables inside the lambda body will be captured from what they were at the time the connect() was executed, not just local variables outside the lambda which have the value they happen to have been set to the last time they were changed.

            When I first did lambdas in Python (I'm more a C++-er) I was totally confused by this behaviour, and so have others been, so you are not alone facing this issue!

            D Offline
            D Offline
            dynaspinner
            wrote on last edited by
            #5

            @JonB While searching for any answers to the error I posted when using your code, I stumbled upon this article which explains the quirk of lambda in Python which confirms what you said:

            https://realpython.com/python-lambda/#evaluation-time

            Posting this here in case someone stumbles upon this thread since I found it helpful.

            D 1 Reply Last reply
            0
            • D dynaspinner

              @JonB Thanks for the response! I tried out the code but unfortunately I have a new problem now. For whatever reason, it thinks the first parameter is a bool type

              Traceback (most recent call last):
                File "/home/dyna_spinner/PyProgramming/Projects/test.py", line 33, in <lambda>
                  confirm_cur_option.clicked.connect(lambda cur_opt=display_cur_option, opt=options : cur_opt.setText(opt.currentText()))
                                                                                                      ^^^^^^^^^^^^^^^
              AttributeError: 'bool' object has no attribute 'setText'
              

              Swapping the parameters' position around gives the same error saying the first parameter is a "bool" object.

              On a side note about lambdas, I faced another problem with it a while ago where I called self.parentWidget().layout().setCurrentIndex(0) to return to the first item in the stack layout which said "NoneType has no attribute layout()" but moving it to a function fixed the error. It only returned the error when instantiating the object. I think it was due to evaluating self.parentWidget().layout().setCurrentIndex(0) during instantiating but I am not sure if that's the reason.

              D Offline
              D Offline
              dynaspinner
              wrote on last edited by
              #6

              @dynaspinner I figured out the reason. The first parameter gets overwritten by the checked variable from clicked and its default value is False. I made the following change and it works perfectly now:

              confirm_cur_option.clicked.connect(lambda _, cur_opt=display_cur_option, opt=options: cur_opt.setText(opt.currentText()))
              

              Thanks for the help @JonB !

              JonBJ 1 Reply Last reply
              0
              • D dynaspinner has marked this topic as solved on
              • D dynaspinner has marked this topic as solved on
              • D dynaspinner

                @dynaspinner I figured out the reason. The first parameter gets overwritten by the checked variable from clicked and its default value is False. I made the following change and it works perfectly now:

                confirm_cur_option.clicked.connect(lambda _, cur_opt=display_cur_option, opt=options: cur_opt.setText(opt.currentText()))
                

                Thanks for the help @JonB !

                JonBJ Online
                JonBJ Online
                JonB
                wrote on last edited by JonB
                #7

                @dynaspinner
                I am not a regular Pythoneer. This does ring a bell, I think, as a further implementation detail/annoyance. Normally you can connect a slot to a signal which sends actual parameters and not bother to declare formal parameters in the slot (function or lambda) to receive them if you are not going to use them. This is same from C++. But if you want pass in your own extra parameters to a Python lambda, as in this case, I guess you are required then to have an explicit parameter (which must come before the extra ones) to receive the signal argument(s). I think I knew/discovered this at some point but had forgotten about this detail and the "checked" argument sent by the clicked() signal.

                1 Reply Last reply
                1
                • JonBJ Online
                  JonBJ Online
                  JonB
                  wrote on last edited by
                  #8

                  Although the poster does not explain in "nice words", the stackoverflow answer at https://stackoverflow.com/a/35821092/489865 is exactly this/your situation, with the clicked() signal.

                  Ah, here in this forum back in 2021 it looks like I discovered this in the whole thread transmitting extra data with signals that already sends extra data :)

                  D 1 Reply Last reply
                  2
                  • JonBJ JonB

                    Although the poster does not explain in "nice words", the stackoverflow answer at https://stackoverflow.com/a/35821092/489865 is exactly this/your situation, with the clicked() signal.

                    Ah, here in this forum back in 2021 it looks like I discovered this in the whole thread transmitting extra data with signals that already sends extra data :)

                    D Offline
                    D Offline
                    dynaspinner
                    wrote on last edited by
                    #9

                    @JonB I actually found out the solution to the error from the first link you provided. I'll check the other thread out right away. Thanks for the help and additional resources!

                    JonBJ 1 Reply Last reply
                    0
                    • D dynaspinner

                      @JonB I actually found out the solution to the error from the first link you provided. I'll check the other thread out right away. Thanks for the help and additional resources!

                      JonBJ Online
                      JonBJ Online
                      JonB
                      wrote on last edited by
                      #10

                      @dynaspinner I have a lot of "vague memories" about discussions here :) Looks like I went through your pain to discover this complication at the time of the thread...!

                      1 Reply Last reply
                      1
                      • D dynaspinner

                        @JonB While searching for any answers to the error I posted when using your code, I stumbled upon this article which explains the quirk of lambda in Python which confirms what you said:

                        https://realpython.com/python-lambda/#evaluation-time

                        Posting this here in case someone stumbles upon this thread since I found it helpful.

                        D Offline
                        D Offline
                        dynaspinner
                        wrote on last edited by
                        #11

                        @dynaspinner I didn't realise the link I sent required you to sign in to check the article. I found a link from the official Python docs instead:

                        https://docs.python.org/3/faq/programming.html#why-do-lambdas-defined-in-a-loop-with-different-values-all-return-the-same-result

                        JonBJ 1 Reply Last reply
                        1
                        • D dynaspinner

                          @dynaspinner I didn't realise the link I sent required you to sign in to check the article. I found a link from the official Python docs instead:

                          https://docs.python.org/3/faq/programming.html#why-do-lambdas-defined-in-a-loop-with-different-values-all-return-the-same-result

                          JonBJ Online
                          JonBJ Online
                          JonB
                          wrote on last edited by
                          #12

                          @dynaspinner ...so all in all that's why it's easier and clearer to use C++... ;)

                          D 1 Reply Last reply
                          1
                          • JonBJ JonB

                            @dynaspinner ...so all in all that's why it's easier and clearer to use C++... ;)

                            D Offline
                            D Offline
                            dynaspinner
                            wrote on last edited by
                            #13

                            @JonB Haha yeah I might as well end up learning C++ for using Qt

                            1 Reply Last reply
                            0

                            • Login

                            • Login or register to search.
                            • First post
                              Last post
                            0
                            • Categories
                            • Recent
                            • Tags
                            • Popular
                            • Users
                            • Groups
                            • Search
                            • Get Qt Extensions
                            • Unsolved