0

I have a QMainWindow, inside there is a QMenu, QLineEdit, and one QPushButton.

Every time I click the button, it plays a sound and then adds a text to the QLineEdit. In my QMenu the user must be able to choose which sound plays by checking it.

Like this

I tried to achieve this by changing a variable self.s inside the MainWindow class every time a QAction is checked, meanwhile, the other QAction's are unchecked. So in my playsound() I just put the self.view.s as the argument.

But it seems that it's only reading the original self.view.s, which is the first sound. My signals to change self.view.s does not work. Also, the other QActions aren't unchecked as I wanted them to.

Below is my code:

import sys
from functools import partial
from playsound import playsound
from threading import Thread
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.buttons = {}
        self.setWindowTitle("Try")

        central_widget = QWidget()
        self.setCentralWidget(central_widget)

        self.lay = QVBoxLayout(central_widget)
        self.lineedit()

        button = {"HEY! ": (0, 0, 0, 0)}

        page = QWidget()
        layout = QGridLayout(page)
        for btnText, pos in button.items():
            self.buttons[btnText] = QPushButton(btnText)
            layout.addWidget(self.buttons[btnText], *pos)

        self.lay.addWidget(page)
        self.music()

    def music(self):
        self.s = 'sound1.mp3'
        
        self.x = 'sound1.mp3'
        self.y = 'sound2.mp3'
        self.z = 'disable.mp3'

    def lineedit(self):
        self.le = QLineEdit()
        self.le.setFixedHeight(35)
        self.lay.addWidget(self.le)

    def set_lineedit(self, text):
        self.le.setText(text)
        self.le.setFocus()

    def line(self):
        return self.le.text()


class Menu:

    def __init__(self, MainWindow):

        super().__init__()

        self.view = MainWindow
        self.menuBar()
        #self.actionSignals()

    def menuBar(self):
        self.menuBar = QMenuBar()
        self.view.setMenuBar(self.menuBar)

        self.menu = QMenu(self.menuBar)
        self.menu.setTitle('Menu')

        self.sounds = QMenu(self.menu)
        self.sounds.setTitle('Select Sound')

        self.sound1 = QAction(self.menuBar)
        self.sound2 = QAction(self.menuBar)
        self.disable = QAction(self.menuBar)

        self.mute = QAction(self.menuBar)
        self.mute.setText('Mute Background')
        self.mute.setCheckable(True)
        self.mute.setChecked(False)

        self.sound1.setText('Sound 1')
        self.sound1.setCheckable(True)
        self.sound1.setChecked(True)
        self.sound2.setText('Sound 2')
        self.sound2.setCheckable(True)
        self.sound2.setChecked(False)
        self.disable.setText('Disable Sound')
        self.disable.setCheckable(True)
        self.disable.setChecked(False)

        self.sounds.addAction(self.sound1)
        self.sounds.addAction(self.sound2)
        self.sounds.addAction(self.disable)


        self.menuBar.addAction(self.menu.menuAction())
        self.menu.addAction(self.mute)
        self.menu.addAction(self.sounds.menuAction())

    def menu_signals(self):
        
        self.sound1.triggered.connect(self.sound_1)
        self.sound2.triggered.connect(self.sound_2)
        self.disable.triggered.connect(self.disabled)

    def sound_1(self, checked):
        
        if checked:
            self.sound2.setChecked(False)
            self.disable.setChecked(False)
            self.view.s = self.view.x

        else:
            self.sound1.setChecked(True)


    def sound_2(self, checked):

        if checked:
            self.sound1.setChecked(False)
            self.disable.setChecked(False)
            self.view.s = self.view.y

        else:
            self.sound2.setChecked(True)

    def disabled(self, checked):
        
        if checked:
            self.sound2.setChecked(False)
            self.sound1.setChecked(False)
            self.view.s = self.view.z

        else:
            self.sound1.setChecked(True)

class Controller:

    def __init__(self, MainWindow):

        self.view = MainWindow
        self.connectSignals()

    def background(self):
        while True:
            playsound('background.mp3')

    def playsound(self):
        playsound(self.view.s, False)

    def buildExpression(self, sub_exp):

        expression = self.view.line() + sub_exp
        self.view.set_lineedit(expression)

    def connectSignals(self):

        for btnText, btn in self.view.buttons.items():
            self.view.buttons[btnText].clicked.connect(self.playsound)
            self.view.buttons[btnText].clicked.connect(partial(self.buildExpression, btnText))

app = QApplication(sys.argv)
w = MainWindow()
x = Controller(w)
Thread(target = x.background, daemon = True).start()
m = Menu(w)
w.show()
app.exec()

I want to be able to change the value within playsound() depending on which QAction is checked in the Menu Bar. While one QAction is checked, the other QAction's should be unchecked.

musicamante
  • 41,230
  • 6
  • 33
  • 58
JA23Z
  • 25
  • 5
  • 2
    Typo: you're not calling `menu_signals`. Since it's unlikely that you'll connect the signals again, there's little use in a specialized function. Just merge the connections with the previous functions. Also, consider using a [QActionGroup](https://doc.qt.io/qt-5/qactiongroup.html) with `setExclusive(True)`, then just connect to the action group `triggered` signal. – musicamante Jul 02 '21 at 14:05

1 Answers1

1

This is where an action group comes into play. QActionGroup allows for mutually exclusive actions. It also provides convenient access to the selected action through the checkedAction method.

  1. Create a QActionGroup object (e.g. self.soundGroup = QActionGroup(self))
  2. Create your actions with the group as parent (e.g. self.sound1 = QAction(self.soundGroup))
  3. For each of your actions, set their corresponding sound as their data, e.g. self.sound1.setData('sound1.mp3')
  4. Ensure the action group is exclusive (I believe it's the default, but you may use self.soundGroup.setExclusive(True))
  5. Use self.soundGroup.checkedAction() to get the checked action (selected sound) instead of self.view.s: playsound(self.soundGroup.checkedAction().data(), False)

You do not need any of your wiring between the actions and updates to self.view.s anymore. Just remove all of that.

ypnos
  • 50,202
  • 14
  • 95
  • 141
  • I'm getting `AttributeError: 'Controller' object has no attribute 'soundGroup'` since the `soundGroup` is in another class `Menu`. Is there a way I can access it from there instead? My `playsound()` should still be in the class `Controller`. Thank you! – JA23Z Jul 02 '21 at 15:43
  • Got it, I used the steps found here [link](https://stackoverflow.com/questions/19048657/how-to-find-an-object-by-name-in-pyqt) to find a QActionGroup with the objectName. – JA23Z Jul 02 '21 at 16:38
  • 1
    @JA23Z just create the action group as an instance member (eg. `self.soundGroup`). `findChild()` should only be used in very specific situations on which there's no direct programmatical control on the object creation. Also note that the MVC pattern suggested in the tutorial you're following should always be used only when actually needed. For simple situations like yours, it only makes things unnecessarily more complex than they should be. – musicamante Jul 02 '21 at 17:27