You are not logged in.

#1 2021-12-31 04:47:39

IrvineHimself
Member
From: Scotland
Registered: 2016-08-21
Posts: 275

[Solved] PyQt5: click handler being called multiple times

For my New Years' resolution, I have decide to make a determined effort to master Python. Towards this end, I have been working on a text editor with bells and whistles.

For the last three or four days, I have been struggling with adding and/or closing moveable tabs. Unsurprisingly, once you go beyond the basics, there is a lot of really crappy, incomplete and/or unworkable examples and tutorials out there in Google land. However, with a great deal of hair pulling, I have finally got something that, (more or less,) works. The problem is that the close tab handler is being called multiple times.

Main Class

""" Editor main window """
import sys
from PyQt5.QtWidgets import (QMainWindow, QApplication, QWidget, QTabWidget,
                             QVBoxLayout, QLabel)
from PyQt5.QtGui import (QIcon)
import ToolBar
import MenuBar


class Main(QMainWindow):
    """ init main app """

    def __init__(self):
        super().__init__()

        # Define initial window geometry
        self.setWindowTitle('My Editor')
        self.setWindowIcon(QIcon('../icons/Quill.png'))
        self.left = 0
        self.top = 0
        self.width = 1024
        self.height = 768
        self.setGeometry(self.left, self.top, self.width, self.height)

        # Define the Layout
        widget = QWidget(self)
        self.setCentralWidget(widget)
        self.layout = QVBoxLayout(widget)
        self.tabs = QTabWidget()

        # For debugging purposes
        self.tabCount = 0

        self.initUi()

    def addTab(self):
        """ Create Tabs """
        # inc tab count
        self.tabCount += 1
        print("Add tab --- Tab count = " + str(self.tabCount))

        tabWidget = self.tabs
        tabLabel = QLabel('Editor instance #' +
                          str(self.tabCount) + ' goes here')
        tabWidget.addTab(tabLabel, "Untitled")

        tabBar = tabWidget.tabBar()
        tabBar.tabCloseRequested.connect(self.CloseTab)
        tabBar.setTabsClosable(True)
        tabBar.setMovable(True)

        # Refresh the layout
        self.layout.addWidget(tabWidget)

    def initUi(self):
        """ Set up user interface """
        # Create a menu bar
        MenuBar.createMenuBar(self)

        # Create a tool bar
        ToolBar.createToolBar(self)

        # Create first tab
        self.addTab()

    def CloseTab(self, index):
        """ Close Tab Handler with some debugging output """
        print('Del tab --- Tab count = ' + str(self.tabCount))
        print('Tab index = ' + str(index))
        self.tabs.removeTab(index)

    # def CloseTab(self, index):
    #     """ Close Tab Handler with a useless 'fiix' """
    #     test = index + 1
    #     if test == self.tabCount:
    #         self.tabCount -= 1
    #         print('Del tab --- Tab count = ' + str(self.tabCount))
    #         print('Tab index = ' + str(index))
    #         self.tabs.removeTab(index)


m = QApplication(sys.argv)
M = Main()
M.show()
sys.exit(m.exec_())

createToolBar

"""
**Module to create a tool bar:**

Basicaly, as more options are added, the 'createToolBar' function is likely to
become very long. This would make the  'Main' class  unreadable. :)

"""
from PyQt5.QtWidgets import (QAction, QToolBar)


def createToolBar(self):
    """ Creating a tool bar"""
    ToolBar = QToolBar(movable=False)

    # Add tab action
    addTabAct = QAction('+', self)
    addTabAct.setShortcut('Ctrl+T')
    addTabAct.setStatusTip('Add Tab')
    addTabAct.triggered.connect(self.addTab)

    self.layout.addWidget(ToolBar)
    ToolBar.addAction(addTabAct)

createMenuBar

"""
**Module to create a menu bar:**

Basicaly, as more options are added, the 'createMenuBar' function is likely to
become very long. This would make the 'Main' class unreadable. :)

"""
from PyQt5.QtWidgets import (QAction, qApp)


def createMenuBar(self):
    """ Creating a menu bar"""
    # Quit action
    exitAct = QAction('&Exit', self)
    exitAct.setShortcut('Ctrl+Q')
    exitAct.setStatusTip('Exit application')
    exitAct.triggered.connect(qApp.quit)

    menubar = self.menuBar()
    fileMenu = menubar.addMenu('&File')
    fileMenu.addAction(exitAct)

Could someone please point me in the correct direction as to what I am doing wrong.

Irvine

Last edited by IrvineHimself (2022-01-01 08:09:16)


Et voilà, elle arrive. La pièce, le sous, peut-être qu'il arrive avec vous!

Offline

#2 2021-12-31 09:00:17

Raynman
Member
Registered: 2011-10-22
Posts: 1,539

Re: [Solved] PyQt5: click handler being called multiple times

IrvineHimself wrote:

The problem is that the close tab handler is being called multiple times.

But only after adding a second tab, right? addTab does more than the name indicates: it also sets up the tabBar and part of that is adding a connection to CloseTab. It would probably work to make that a UniqueConnection, but you really only want to do this setup once (probably in initUi).

Offline

#3 2021-12-31 13:47:34

IrvineHimself
Member
From: Scotland
Registered: 2016-08-21
Posts: 275

Re: [Solved] PyQt5: click handler being called multiple times

Thanks for your reply, you have given me some ideas to look into.

Raynman wrote:

... But only after adding a second tab, right? addTab does more than the name indicates: it also sets up the tabBar and part of that is adding a connection to CloseTab.....

I understand what you are saying, and completely agree that the CloseTab handler is iterating through the tabBar array.

Raynman wrote:

.... but you really only want to do this setup once (probably in initUi).

I have about 30 or 40 backup copies, (with roughly 10 minor edits for each version,) all trying to do what you are suggesting. The above is the only thing that comes even close to working.

In fact, in desperation, to see how  QtDesigner handled the problem, I tried creating a test window with closable tabs .... It created the appropriate code in the initU, but the code itself didn't work. ie, the tabs were not  closable .

Raynman wrote:

... It would probably work to make that a UniqueConnection ....

This sounds really interesting. I have done a very quick google search, and fully intend to look into this further. Could you provide a link to a good quality tutorial, (or example.)

My first attempts were variations on the following:

        tabBar = tabWidget.tabBar()

        tabBar.tabCloseRequested.connect(self.CloseTab, Qt.UniqueConnection)
        # I also tried:
        # tabBar.tabCloseRequested.connect(self.CloseTab, type=Qt.UniqueConnection)

        tabBar.setTabsClosable(True)
        tabBar.setMovable(True)

The results were of the following form:

Add tab --- Tab count = 1
Add tab --- Tab count = 2

# After trying to close a tab
Traceback (most recent call last):
  File "/home/stupidme/Documents/Python/Mq/Mq-Code/bin/Mq_7.py", line 53, in addTab
    tabBar.tabCloseRequested.connect(self.CloseTab, type=Qt.UniqueConnection)
TypeError: connection is not unique
Aborted (core dumped)

Thanks again for your interest. I will look further into UniqueConnection
Irvine


Et voilà, elle arrive. La pièce, le sous, peut-être qu'il arrive avec vous!

Offline

#4 2021-12-31 15:02:30

Trilby
Inspector Parrot
Registered: 2011-11-29
Posts: 29,520
Website

Re: [Solved] PyQt5: click handler being called multiple times

The problem is the line "tabBar.tabCloseRequested.connect(self.CloseTab)", that should only be called once.  You can call setTabsClosable as often as you want (though it should only be needed once), but the connect is for the bar, not for each individual tab on the bar.

The following patch avoids this issue - but there are other problems: you are doing a lot of work in addTab that should not be there.  The specific symptom mentioned in this thread is not only from calling the connect every time through the addTab function, but also from recreating the tabbar ever time a tab is added.  You have ONE tab bar in the window and it should be saved as an instance variable when the UI is created:

@@ -44,10 +44,8 @@
                           str(self.tabCount) + ' goes here')
         tabWidget.addTab(tabLabel, "Untitled")

-        tabBar = tabWidget.tabBar()
-        tabBar.tabCloseRequested.connect(self.CloseTab)
-        tabBar.setTabsClosable(True)
-        tabBar.setMovable(True)
+        self.tabBar.setTabsClosable(True)
+        self.tabBar.setMovable(True)

         # Refresh the layout
         self.layout.addWidget(tabWidget)
@@ -60,6 +58,8 @@
         # Create a tool bar
         ToolBar.createToolBar(self)

+        self.tabBar = self.tabs.tabBar()
+        self.tabBar.tabCloseRequested.connect(self.CloseTab)
         # Create first tab
         self.addTab()

Last edited by Trilby (2021-12-31 15:11:05)


"UNIX is simple and coherent..." - Dennis Ritchie, "GNU's Not UNIX" -  Richard Stallman

Offline

#5 2021-12-31 15:06:34

Raynman
Member
Registered: 2011-10-22
Posts: 1,539

Re: [Solved] PyQt5: click handler being called multiple times

So PyQt5 throws an exception instead of returning some invalid value when the connection is not unique (as in C++, where Qt doesn't use exceptions). You can simply catch and ignore the exception. I just tried your code and that seems to give normal tab behavior (add/close). But as I said, that is really just a workaround for putting the tabBar setup in the wrong place.

IrvineHimself wrote:

I understand what you are saying, and completely agree that the CloseTab handler is iterating through the tabBar array.

I'm not sure if you really understand. Especially when programming, it helps to be precise. For every tab added, a new connection is added. Qt then handles the tabCloseRequested signal by iterating over all these connections and invoking CloseTab for each one.

If you open four extra tabs and try to close the first one, you get five calls to CloseTab with index=0. The first one removes the first tab; then the indices shift and the second call removes the second tab (now at index 0) and so on -- one click ends up removing all tabs (but all five signal-slot connections remain). If you close one somewhere in the middle (index=n), all tabs to right (index >= n) are removed (the extra calls seem to do nothing).

I also tested my suggestion as follows and I don't see any issues (only showing the modified methods):

    def addTab(self):
        """ Create Tabs """
        # inc tab count
        self.tabCount += 1
        print("Add tab --- Tab count = " + str(self.tabCount))

        tabLabel = QLabel('Editor instance #' +
                          str(self.tabCount) + ' goes here')
        self.tabs.addTab(tabLabel, "Untitled")

    def initUi(self):
        """ Set up user interface """
        # Create a menu bar
        MenuBar.createMenuBar(self)

        # Create a tool bar
        ToolBar.createToolBar(self)

        tabWidget = self.tabs
        tabBar = tabWidget.tabBar()
        tabBar.tabCloseRequested.connect(self.CloseTab)
        tabBar.setTabsClosable(True)
        tabBar.setMovable(True)

        # Refresh the layout
        self.layout.addWidget(tabWidget)
        # Create first tab
        self.addTab()

Last edited by Raynman (2021-12-31 15:08:04)

Offline

#6 2022-01-01 08:08:30

IrvineHimself
Member
From: Scotland
Registered: 2016-08-21
Posts: 275

Re: [Solved] PyQt5: click handler being called multiple times

Thanks guys, that solved my problem.

Deleted: see @Raynman's comment below
I think what was causing me trouble with declaring the tabBar in initUi was that I was seeing it as an all or nothing proposition. What I mean is that since I had to declare tabWidget in addTab, while also having to refresh the layout with self.layout.addWidget(tabWidget), I took that to mean all the tabBar stuff had to be in the addTab function.

After four days of hair pulling, when the original posted code, (along with its misplaced tabBar declarations,) actually worked as intended, I saw this as conformation that this was indeed the correct place for that piece of code.

Given the problems I had with this, in the hope it becomes Googles top search result for adding, closing and moving tabs with the QtTabBar and QtTabWidget class, I am posting a small note along with my final source code.

Notes:

QtTabBar is a subclass of QtTabWidget and, despite multiple attempts, I could not get it to work by calling it directly. For example:

See @Trilby's post below about using QtTabBar

tabBar = QtTabBar()

For it to work, I used:

tabBar = QtTabWidget.tabBar()

I may have made stupid mistakes, but, if the reader is having problems with QtTabBar, I would suggest declaring it as a subclass of QtTabWidget

The final working version, edited to reflect comments below, with no debug print statements Is as follows:

""" Editor main window """
import sys
from PyQt5.QtWidgets import (QMainWindow, QApplication, QWidget, QTabWidget,
                             QVBoxLayout, QLabel)
from PyQt5.QtGui import (QIcon)
import ToolBar
import MenuBar


class Main(QMainWindow):
    """ init main app """

    def __init__(self):
        super().__init__()

        # Define initial window geometry
        self.setWindowTitle('My Editor')
        self.setWindowIcon(QIcon('../icons/Quill.png'))
        self.left = 0
        self.top = 0
        self.width = 1024
        self.height = 768
        self.setGeometry(self.left, self.top, self.width, self.height)

        # Define the Layout
        widget = QWidget(self)
        self.setCentralWidget(widget)
        self.layout = QVBoxLayout(widget)

        self.initUi()

    def initUi(self):
        """ Set up user interface """
        # Create a menu bar
        MenuBar.createMenuBar(self)

        # Create a tool bar
        ToolBar.createToolBar(self)

        # Setup a TabBar with movable closable tabs
        self.tabs = QTabWidget()
        tabWidget = self.tabs
        tabBar = tabWidget.tabBar()
        tabBar.tabCloseRequested.connect(self.closeTab)
        tabBar.setTabsClosable(True)
        tabBar.setMovable(True)

        # Add the tab to the layout
        self.layout.addWidget(tabWidget)

        # Create first tab
        self.addTab()

    def addTab(self):
        """ Create Tabs """

        # Define tab widget and create a test label
        tabWidget = self.tabs
        tabLabel = QLabel('Editor instance goes here')

        # Add tab
        tabWidget.addTab(tabLabel, "Untitled")

    def closeTab(self, index):
        """ Close Tab Handler """
        self.tabs.removeTab(index)


m = QApplication(sys.argv)
M = Main()
M.show()
sys.exit(m.exec_())

I didn't declare the tabBar stuff as instance attributes, ( @Trilby's' patch,) because the extremely opinionated pylint was complaining about there being too many of them.

I also changed the name of the function CloseTab, to closeTab for a more consistent naming style. (I turned that particular check off in pylint since it's output doesn't play well with PyQt.)

Thanks again for your valuable help, and I wish you all a happy New Year
Irvine

Edited to reflect comments below

Last edited by IrvineHimself (2022-01-01 17:20:13)


Et voilà, elle arrive. La pièce, le sous, peut-être qu'il arrive avec vous!

Offline

#7 2022-01-01 13:08:05

Trilby
Inspector Parrot
Registered: 2011-11-29
Posts: 29,520
Website

Re: [Solved] PyQt5: click handler being called multiple times

According to the docs, a QTabBar is really just the bar itself and cannot include the actual widgets (e.g., the pages/text areas).  When you create a QTabWidget it creates it's own QTabBar and QStackedWidget area.


"UNIX is simple and coherent..." - Dennis Ritchie, "GNU's Not UNIX" -  Richard Stallman

Offline

#8 2022-01-01 14:04:17

Raynman
Member
Registered: 2011-10-22
Posts: 1,539

Re: [Solved] PyQt5: click handler being called multiple times

IrvineHimself wrote:

I think what was causing me trouble with declaring the tabBar in initUi was that I was seeing it as an all or nothing proposition. What I mean is that since I had to declare tabWidget in addTab, while also having to refresh the layout with self.layout.addWidget(tabWidget), I took that to mean all the tabBar stuff had to be in the addTab function.

I didn't want to make my previous reply even longer and I simply moved some lines from one method to the other without further edits/comments, but now I really want to point out that addWidget does not "refresh the layout"; Qt usually takes care of redrawing widgets when something changes (like adding a tab in this case). This addWidget call is (logically/conceptually) part of defining the layout, which you do (according to some pretty arbitrary-seeming split) in __init__ and initUi. Repeatedly adding the same widget doesn't actually add extra copies, so as with setTabsClosable, having it in addTab doesn't cause any real issues, but I wouldn't call it a good "final, clean version".

IrvineHimself wrote:

QtTabBar is a subclass of QtTabWidget

It's not:

>>> issubclass(QTabBar, QTabWidget)
False

See Trilby's last reply. The tab widget consists of a tab bar and a "page area". This is composition, not inheritance. QTabWidget.tabBar() is a simple getter/accessor method that returns a pointer to the existing child tab bar object, so nothing was being recreated and adding an instance variable for that is not needed. So the comment "create a tab bar" is not entirely accurate, and while createToolBar does actually create a new object, createMenuBar also only adds to the existing menuBar (maybe continue with "init" naming or populateMenu or smth.).

Actually, for most things (in fact, everything in your simple program), you don't need to interact with the tab bar at all. The relevant properties and signals are all exposed on the tab widget.

Last edited by Raynman (2022-01-01 14:10:24)

Offline

#9 2022-01-01 17:45:30

IrvineHimself
Member
From: Scotland
Registered: 2016-08-21
Posts: 275

Re: [Solved] PyQt5: click handler being called multiple times

Thanks for your valuable criticism. As I pointed out at the beginning: I am very much learning all this as I go, and, (much like with Arch itself,) the quality of some of the results google returns when researching python topics is not always the best.

I have edited the above post to reflect both your comments. Also, I edited the final version to reflect @Raynman's comments about refreshing the layout and arbitrary splits

However, since it's a reference to an external module. I left the createMenuBar in the posted version but will change the name in my own copy.

Once again, I want to thank you for your help. Especially when it's people I respect, (Arch Forumites,) I really do listen to what they say and tend to take their comments as suggestions for further reading. This makes them doubly valuable.

Irvine


Et voilà, elle arrive. La pièce, le sous, peut-être qu'il arrive avec vous!

Offline

Board footer

Powered by FluxBB