6

I'm creating my first application in Python. Currently I'm implementing the first view with PyQt5 and the MVC pattern. Therefore I created a "main" class that is starting the controller by creating a new controller object (The first programming language I learned was Java, so maybe this is not necassary?). In this controller objects init method I'm creating the first view. The view class itself creates a new QWidget. After the QWidget is created the controller is calling a method of the view (QWidget). This method should show the login screen. To show the login screen a new class (Login) is created. That class is of the type QVBoxLayout and is added to the main view (QWidget). This combination leads to an application that is showing a window with the login. This means main class -> controller -> main window (QWidget) -> login (QVBoxLayout).

Here I have the following question: Is it the correct way to create a main window (QWidget) and use methods to add the inner layout to the window (using other files/classes)? Or should everything be written in one class?

Now I reached the point that my layout and window are displayed correctly. What is missing is the model and how the model is invoked. I searched how I can check if a button was pressed. I found button.clicked.connect(callfunction). This seems to be the right way but how can I call this function from the controller? So that the controller creates the application window and displays the login inside this window. Then the controller listens and waits until the button is pressed. Then the inputs will be forwarded to the model and in the model the credentials will be checked. Here is my sourcecode for the controller with my try on listening to the button:

class Controller(object):

    def __init__(self):
        # Applikation starten und Login anzeigen
        app = QApplication(sys.argv)
        widget = View()
        widget.showLogin(0, "")
        widget.loginButton.clicked.connect(self.loginPressed())
        sys.exit(app.exec_())

    def loginPressed(self):
        widget.showLogin(1, "err1")

The code for my Login class:

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

    # Zum vertikalen zentrieren des Inhalts
    self.addStretch()

    # Label (Bild) erstellen und zum Layout hinzufügen
    label = QLabel()
    pixmap = QPixmap(pathLogo)
    label.setPixmap(pixmap.scaledToWidth(logoWidth, Qt.SmoothTransformation))
    label.setAlignment(Qt.AlignCenter)
    self.addWidget(label)

    # Label für den Nutzername
    usernameLabel = QLabel("Username")
    usernameLabel.setAlignment(Qt.AlignCenter)
    usernameLabel.setStyleSheet("QLabel {color: #ffffff; font-size: 14px; font-weight:bold; "
                                "margin:50px, 0, 5px, 0 ;}")
    self.addWidget(usernameLabel)

    # Eingabefeld für den Nutzername
    uihbox = QHBoxLayout()
    uihbox.addStretch()
    usernameInput = QLineEdit()
    usernameInput.setFixedWidth(150)
    usernameInput.setStyleSheet(
        "QLineEdit {border-radius: 5px; padding: 4px; line-height:12px; padding-left: 5px;}")
    uihbox.addWidget(usernameInput)
    uihbox.addStretch()
    self.addLayout(uihbox)

    # Label für das Passwort
    passwortLabel = QLabel("Passwort")
    passwortLabel.setAlignment(Qt.AlignCenter)
    passwortLabel.setStyleSheet("QLabel {color: #ffffff; font-size: 14px; font-weight:bold; "
                                "margin:15px, 0, 5px, 0 ;}")
    self.addWidget(passwortLabel)

    # Eingabefeld für den Nutzername
    pihbox = QHBoxLayout()
    pihbox.addStretch()
    passwordInput = QLineEdit()
    passwordInput.setFixedWidth(150)
    passwordInput.setEchoMode(QLineEdit.Password)
    passwordInput.setStyleSheet(
        "QLineEdit {border-radius: 5px; padding: 4px; line-height:12px; padding-left: 5px;}")
    pihbox.addWidget(passwordInput)
    pihbox.addStretch()
    self.addLayout(pihbox)

    # Button erstellen
    bihbox = QHBoxLayout()
    bihbox.addStretch()
    loginButton = QPushButton("Login")
    loginButton.setStyleSheet("QPushButton {margin: 25px, 0, 0, 0; border-radius:5px; border: 1px solid white; "
                              "padding-right: 15px; padding-left: 15px; padding-top: 5px; padding-bottom:5px;"
                              "color:white; font-weight:bold; font-size: 14px;}")
    loginButton.setCursor(QCursor(Qt.PointingHandCursor))

    bihbox.addWidget(loginButton)
    bihbox.addStretch()
    self.addLayout(bihbox)

    # Zum vertikalen zentrieren des Inhalts
    self.addStretch()

def showError(self, errCode):
    errMsg = QLabel(err1)
    errMsg.setAlignment(Qt.AlignCenter)
    errMsg.setStyleSheet("QLabel {color:red;}")
    self.addWidget(errMsg)
    self.addStretch()

And my View class:

class View(QWidget):

    # Methode um das Fenster der Applikation zu initialisieren
    def __init__(self):
        super().__init__()

        # Breite und Höhe setzen
        self.resize(initWidth, initHeight)

        # Titel und Icon setzen
        self.setWindowTitle(appTitle)
        self.setWindowIcon(QIcon(pathFavicon))

        # Hintergrund mit der bgColor füllen
        self.setAutoFillBackground(True)
        p = self.palette()
        p.setColor(self.backgroundRole(), QColor(bgColor))
        self.setPalette(p)

        # Anzeigen des Fensters
        self.show()

    # Methode, um Login zu zeigen
    def showLogin(self, err, errcode):
        # Laden des Inhalts mittels Login
        if (err != 0):
            vbox = Login()
            vbox.showError(errcode)
        if (err == 0):
            vbox = Login()

        self.setLayout(vbox)

Further questions:

  • Is my understanding of the MVC pattern correct or do I use to many classes?
  • Should I listen on the button in the controller or is it correct that the view is invoking a method in the controller?
  • Further I implemented that I can call the login QVBoxLayout with an error code to display an error at the bottom of the view. I have not found a way to dynamically change the view from the controller class. The only solution I could imagine is that I "repaint" the content of the QWidget with the added error message. Is this the correct solution?

Thanks in advance!

5
  • 1
    change widget.loginButton.clicked.connect(self.loginPressed()) to widget.loginButton.clicked.connect(self.loginPressed) without () Commented Sep 16, 2018 at 23:00
  • change widget to self.widget Commented Sep 16, 2018 at 23:03
  • show your Login class complete. Commented Sep 16, 2018 at 23:05
  • 2
    Typically the MVC pattern would have a main class instantiating both view and controller, and the view would receive a reference to the controller. Don't have the controller instantiating the view. Commented Sep 16, 2018 at 23:21
  • 1
    The view class should define the button and setup its event response. The button click event should call the controller that the view has a reference to. Commented Sep 16, 2018 at 23:23

2 Answers 2

10

The idea is not the number of classes, but what is the task of each class.

  • A model is the entity that keeps the information.
  • The view is the entity that shows the information.
  • And the controller is the one that controls the flow of sight data according to a certain logic.

So do you think your classes implement the above?

In the following example I show how those tasks are redistributed, my example does not meet all the classic MCV rules like the one pointed out by @101 because the GUIs have an internal event handler.

import sys
from functools import partial
from PyQt5 import QtCore, QtGui, QtWidgets


class Model:
    def __init__(self):
        self.username = ""
        self.password = ""

    def verify_password(self):
        return self.username == "USER" and self.password == "PASS"


class View(QtWidgets.QWidget):
    verifySignal = QtCore.pyqtSignal()

    def __init__(self):
        super(View, self).__init__()
        self.username = ""
        self.password = ""
        self.initUi()

    def initUi(self):
        lay = QtWidgets.QVBoxLayout(self)
        title = QtWidgets.QLabel("<b>LOGIN</b>")
        lay.addWidget(title, alignment=QtCore.Qt.AlignHCenter)

        fwidget = QtWidgets.QWidget()
        flay = QtWidgets.QFormLayout(fwidget)
        self.usernameInput = QtWidgets.QLineEdit()
        self.usernameInput.textChanged.connect(partial(setattr, self, "username"))
        self.passwordInput = QtWidgets.QLineEdit(echoMode=QtWidgets.QLineEdit.Password)
        self.passwordInput.textChanged.connect(partial(setattr, self, "password"))
        self.loginButton = QtWidgets.QPushButton("Login")
        self.loginButton.clicked.connect(self.verifySignal)

        flay.addRow("Username: ", self.usernameInput)
        flay.addRow("Password: ", self.passwordInput)
        flay.addRow(self.loginButton)

        lay.addWidget(fwidget, alignment=QtCore.Qt.AlignHCenter)
        lay.addStretch()

    def clear(self):
        self.usernameInput.clear()
        self.passwordInput.clear()

    def showMessage(self):
        messageBox = QtWidgets.QMessageBox(self)
        messageBox.setText("your credentials are valid\n Welcome")
        messageBox.exec_()
        self.close()

    def showError(self):
        messageBox = QtWidgets.QMessageBox(self)
        messageBox.setText("your credentials are not valid\nTry again...")
        messageBox.setIcon(QtWidgets.QMessageBox.Critical)
        messageBox.exec_()


class Controller:
    def __init__(self):
        self._app = QtWidgets.QApplication(sys.argv)
        self._model = Model()
        self._view = View()
        self.init()

    def init(self):
        self._view.verifySignal.connect(self.verify_credentials)

    def verify_credentials(self):
        self._model.username = self._view.username
        self._model.password = self._view.password
        self._view.clear()
        if self._model.verify_password():
            self._view.showMessage()
        else:
            self._view.showError()

    def run(self):
        self._view.show()
        return self._app.exec_()


if __name__ == '__main__':
    c = Controller()
    sys.exit(c.run())
Sign up to request clarification or add additional context in comments.

2 Comments

I'm curious, in this example there's no data being passed into the view from the model. If the view needed some data at startup, how would you normally handle that?
Is there a specific reason you define the classes without parens at the end of the class names, and only adding the parens when you create the instances?
1

To augment what @eyllanesc said above and to abstract it a bit more I have used the MVC model to handle just about everything that involves presenting information to a user. As such my MVC sort of also incorporates some of the other models that were subsequently derived from MVC and just look at it all as various MVC flavors aka if its ice cream is ice cream regardless of what flavorants you put into it.

First it was mainly designed to handle database connectivity with the Model being the database, the View being the GUI, and the Controller the Middle-Tier that handled the flow from the database to GUI and from the GUI to the database. However, in my version the Middle-Tier has become the work horse of the process as it handles all the data-validation as well as applies any business logic that needs to applied to the data regardless of what direction it is flowing in.

That all being said we can then take one step back and look at a Python/PyQt application like this:

Model = Back-End that which handles Data Management, Storage and/or Production (Data Associated Language - SQL, C, etc...)

Controller = Middle-Tier handles Data Validation and the Application of most of the Business Rules Note: as I stated above I slightly depart from the traditional version in that I shift the validation and business rules to this section and I do this because this means data then just becomes the data and I can apply that to things that would not be capable of implementing validation or rules such as low level serial ports and the like. Granted if the back-end is a database there may be some data rules that are implemented within it that may be similar to business rules but in this rendition those should be kept to minimum and deal more with data propagation and such. (Python only)

View = GUI Front-End handles presenting the data to the user. Note sometimes some of the more basic data validation might occur here such as if its only numeric make sure its only numeric that kind of thing. (Python/PyQt mostly the latter)

Each of these entities should then be produced such that they are pretty much independent from the other and with the Front-End completely divorced from the Back-End. This is to say if I wanted to replace the Middle-Tier I should be able to do so nearly seamlessly and without needing to make changes to the Front-End or the Back-End and the same goes for both the Front-End and Back-End if I replaced them with something else and made the same connections to the Middle-Tier as previously then nothing else should be affected. Of course this does not always happen because sometimes there are changes that affect all 3 Tiers but then they can still be done independently all that has to be agreed upon is what is being sent/received from the GUI and the Back-End and/or the Middle-Tier.

On the Front-End this can be implemented while coding by not making a connection when doing the front-end you just call a place-holder function (which would eventually be replaced by the middle-tier) and all that function does is supply the data that would (or should) come form the middle-tier. From the GUI side you do not care how that data came to be in the format that it is in all you need to know is that will be how it is supplied. Same goes for sending data that returns data just create a dummy function for the front-end that simulates the situation. This way you are focused on just presenting the data to the user and receiving the input from the user and that is all and remember above all K.I.S.S. it -- (Keep It Simple and Smart)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.