You are not logged in.
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
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
Thanks for your reply, you have given me some ideas to look into.
... 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.
.... 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 .
... 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
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
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.
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
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
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
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".
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
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