user15427282
user15427282

Reputation:

Change QTabWidget Active Tab From Other Class

I'm trying to mimic the tab button behaviour with buttons from another class.

So that when I click the Orders or History button the corresponding tab is selected.

ATM the code I have seems to work in so far as updating the currentIndex attribute as it prints correctly (0 for Orders and 1 for History).

However this change does not seem to reflect in the window, am I right in thinking this is an 'update widget' problem? If so how do I 'refresh' it? If not am I missing some fundamental concept?

I've slimmed the code as much as I could...

import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *

Application = QApplication(sys.argv)

class Window(QWidget):
    def __init__(self):
        super().__init__()

        # Layout #
        Layout = QVBoxLayout()
        Layout.addLayout(LayoutTabButtons())
        Layout.addWidget(Tabs())
        self.setLayout(Layout)
        self.show()
        
class Tabs(QTabWidget):
    def __init__(self):
        super().__init__()

        # Children #
        self.addTab(QLabel('This Is The Orders Page'),'Orders')
        self.addTab(QLabel('This Is The History Page'),'History')

        # Connections #
        InstanceButtonOrders = ButtonOrders(self)
        InstanceButtonHistory = ButtonHistory(self)

    def ChangeIndex(self, Input):
        self.setCurrentIndex(Input)
        print(self.currentIndex()) # <--- Prints Correctly

class LayoutTabButtons(QHBoxLayout):
    def __init__(self): 
        super().__init__() 

        # Children #
        self.addWidget(ButtonOrders(Tabs()))
        self.addWidget(ButtonHistory(Tabs()))

class ButtonOrders(QPushButton):
    def __init__(self, Tabs): 
        super().__init__()  

        # Content #
        self.setText('Orders') 

        # Connections #
        self.clicked.connect(lambda: self.SetIndex(Tabs)) 

    def SetIndex(self,Tabs):
        Tabs.ChangeIndex(0)

class ButtonHistory(QPushButton):
    def __init__(self, Tabs): 
        super().__init__() 

        # Content #
        self.setText('History')

        # Connections #
        self.clicked.connect(lambda: self.SetIndex(Tabs)) 

    def SetIndex(self,Tabs):
        Tabs.ChangeIndex(1)

InstanceWindow = Window()

sys.exit(Application.exec_())

As an aside my 1st instinct was to go through the PyQt5 module files in order to get a clue as to how the default behaviour is done, but as Qt is written in C++ that wasn't an option here.

Upvotes: 2

Views: 1320

Answers (2)

Jack Lilhammers
Jack Lilhammers

Reputation: 1247

You're creating 3 instances of Tabs, therefore each clicked() signal is connected to its "own" instance of Tabs.

The Window constructor creates one of them directly and the other two are created by LayoutTabButtons

class Window(QWidget):
    def __init__(self):
        super().__init__()

        # Layout #
        Layout = QVBoxLayout()
        Layout.addLayout(LayoutTabButtons()) # <-- 2 here
        Layout.addWidget(Tabs()) # <-- 1 here
        self.setLayout(Layout)
        self.show()

The other two, which are never shown, because they're just connected to one button and nothing else.

class LayoutTabButtons(QHBoxLayout):
    def __init__(self): 
        super().__init__() 

        # Children #
        self.addWidget(ButtonOrders(Tabs()))  # <-- Here
        self.addWidget(ButtonHistory(Tabs())) # <-- Here

To make your code work you have to keep an instance of Tabs inside the parent Window and pass it to the children.

Application = QApplication(sys.argv)

class Window(QWidget):
    def __init__(self):
        super().__init__()

        self.tabs = Tabs() # <-- The local instance!
        # Layout #
        Layout = QVBoxLayout()
        Layout.addLayout(LayoutTabButtons(self.tabs)) # <-- Passed to the children
        Layout.addWidget(self.tabs) # <-- Added to the layout
        self.setLayout(Layout)
        self.show()

class Tabs(QTabWidget):
    def __init__(self):
        super().__init__()

        # Children #
        self.addTab(QLabel('This Is The Orders Page'),'Orders')
        self.addTab(QLabel('This Is The History Page'),'History')

        # Connections #
        # Now the class has local instances of the buttons
        self.InstanceButtonOrders = ButtonOrders(self)   # <-- local instance
        self.InstanceButtonHistory = ButtonHistory(self) # <-- local instance

    def ChangeIndex(self, Input):
        self.setCurrentIndex(Input)
        print(self.currentIndex()) # <-- Prints Correctly

class LayoutTabButtons(QHBoxLayout):
    def __init__(self, tabs):
        super().__init__()

        # Children #
        # Now the layout gets the buttons in Tabs
        self.addWidget(tabs.InstanceButtonOrders)
        self.addWidget(tabs.InstanceButtonHistory)

class ButtonOrders(QPushButton):
    def __init__(self, tabs):
        super().__init__()

        self.tabs = tabs # <-- These are the tabs in Window
        # Content #
        self.setText('Orders')

        # Connections #
        self.clicked.connect(self.SetIndex) # <-- The lambda was superfluos

    def SetIndex(self):
        self.tabs.ChangeIndex(0)

class ButtonHistory(QPushButton):
    def __init__(self, tabs):
        super().__init__()

        self.tabs = tabs # These are the tabs in Window
        # Content #
        self.setText('History')

        # Connections #
        self.clicked.connect(self.SetIndex) # <-- The lambda was superfluos

    def SetIndex(self):
        self.tabs.ChangeIndex(1)

InstanceWindow = Window()

sys.exit(Application.exec_())

However, while with a simple fix your code works, it has serious design issues.
The buttons should not depend on the tabs, they shouldn't even know about them.
In fact you should use plain QPushButtons and the only object aware of them should be the instance of Tabs, that would handle the interaction between tabs and buttons.


Here's a little refactoring that tries to stay close as possible to the OP idea while avoiding unnecessary classes

Application = QApplication(sys.argv)

class TabWindow(QWidget):
    def __init__(self):
        super().__init__()

        self.tabs = Tabs()
        # Layout #
        Layout = QVBoxLayout()
        Layout.addWidget(self.tabs)
        self.setLayout(Layout)
        self.show()

    def AddTab(self, Widget, Name, Button=None):
        self.tabs.AddTab(Widget, Name, Button)

class Tabs(QWidget):
    def __init__(self):
        super().__init__()

        self.buttons = []
        self.tabs = QTabWidget()

        # Buttons bar layout #
        self.barLayout = QHBoxLayout()

        # Window layout #
        Layout = QVBoxLayout()
        Layout.addLayout(self.barLayout)
        Layout.addWidget(self.tabs)
        self.setLayout(Layout)

    # Sets up a new tab with its button
    # and connects the click to ChangeIndex()
    def AddTab(self, Widget, Name, Button=None):
        self.tabs.addTab(Widget, Name)

        # Uses QPushButton by default, but can be replaced
        # by providing any instance of QAbstractButton
        if Button is None:
            Button = QPushButton(Name)
        index = len(self.buttons)
        Button.clicked.connect(lambda: self.ChangeIndex(index))

        self.buttons.append(Button)
        self.barLayout.addWidget(Button)

    def ChangeIndex(self, Input):
        self.tabs.setCurrentIndex(Input)
        print(self.tabs.currentIndex()) # <-- Prints Correctly

InstanceWindow = TabWindow()
# Tabs #
InstanceWindow.AddTab(QLabel('This Is The Orders Page'), 'Orders')
InstanceWindow.AddTab(QLabel('This Is The History Page'), 'History')

sys.exit(Application.exec_())

Upvotes: 1

eyllanesc
eyllanesc

Reputation: 243897

The main problem is that you are thinking that doing "X()" will always create the same class object and that is false (unless you have implemented a singleton). In addition, the idea is to create objects that have a limited scope and that share information through signals as if it were a black box where there are only inputs and outputs and what is inside does not matter. Considering the above, a possible solution is:

from enum import IntEnum
import sys

from PyQt5.QtCore import pyqtSignal
from PyQt5.QtWidgets import (
    QApplication,
    QHBoxLayout,
    QLabel,
    QPushButton,
    QTabWidget,
    QVBoxLayout,
    QWidget,
)


class Page(IntEnum):
    OrderPage = 0
    HistoryPage = 1


class Button(QPushButton):
    pageChanged = pyqtSignal(Page)

    def __init__(self, text, page, parent=None):
        super().__init__(parent)

        self._page = page

        self.setText(text)
        self.clicked.connect(self.handle_clicked)

    @property
    def page(self):
        return self._page

    def handle_clicked(self):
        self.pageChanged.emit(self.page)


class ButtonsContainer(QWidget):
    pageChanged = pyqtSignal(Page)

    def __init__(self, parent=None):
        super().__init__(parent)
        lay = QHBoxLayout(self)

    def add_button(self, text, page):
        button = Button(text, page)
        button.pageChanged.connect(self.pageChanged)
        self.layout().addWidget(button)


class Tabs(QTabWidget):
    def __init__(self, parent=None):
        super().__init__(parent)

        self.addTab(QLabel("This Is The Orders Page"), "Orders")
        self.addTab(QLabel("This Is The History Page"), "History")

    def change_page(self, page):
        self.setCurrentIndex(page)


class Window(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)

        button_container = ButtonsContainer()
        button_container.add_button("Orders", Page.OrderPage)
        button_container.add_button("History", Page.HistoryPage)

        tabs = Tabs()

        button_container.pageChanged.connect(tabs.change_page)

        layout = QVBoxLayout(self)
        layout.addWidget(button_container)
        layout.addWidget(tabs)


def main():

    app = QApplication(sys.argv)
    view = Window()
    view.show()
    sys.exit(app.exec_())


if __name__ == "__main__":
    main()

For example, in my previous code, each button is associated with each page, and when it is clicked, it sends the page that is received by the ButtonsContainer class, and the latter transmits it to the QTabWidget. So you could add more buttons for more pages without changing anything.

Upvotes: 1

Related Questions