Kirjautuminen

Haku

Tehtävät

Koodit: Python + Qt: taskulaskin graafisella käyttöliittymällä

Kirjoittaja: The Alchemist

Kirjoitettu: 23.12.2020 – 23.12.2020

Tagit: kirjaston käyttö, käyttöliittymä, ohjelmointitavat, hyvää koodia, koodi näytille, sovellus, vinkki, työpöytä

Niin sanottu single file -esimerkki graafisen peruslaskimen toteuttamisesta PyQt 5 -kirjastolla.

Laskin osaa laskea (vain) kahdesta numerosta ja yhdesta operaattorista koostuvat laskut.

Kun ruudulle on syötetty kaava muodossa [numero] [operaattori] [numero], laskin suorittaa laskutoimenpiteen automaattisesti ja päivittää tuloksen ruudulle.

Erikoisominaisuutena ruudulla näkyvää tulosta voidaan käyttää automaattisesti seuraavan laskun ensimmäisenä numerona, kun syötetään uuden numeron sijaan operaattori.

import math
import sys
from PyQt5.QtCore import Qt
from PyQt5 import QtCore, QtGui, QtWidgets

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        self.setup()
        self.calculator = Calculator()
        self.equation = []

    def setup(self):
        container = QtWidgets.QWidget(self)

        display = QtWidgets.QLabel()
        display.setFont(QtGui.QFont("monospace", 20))
        display.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
        display.setText("0")

        numpad = Numpad(self)
        numpad.number.connect(self.onNumber)
        numpad.operator.connect(self.onOperator)

        layout = QtWidgets.QVBoxLayout()
        layout.addWidget(display)
        layout.addWidget(numpad)

        container.setLayout(layout)
        self.setCentralWidget(container)

        self.display = display
        self.numpad = numpad

    def onNumber(self, number):
        if len(self.equation) == 4:
            self.equation = []
            self.display.setText("")

        if len(self.equation) == 0:
            self.display.setText(str(number))
            self.equation.append(number)

        if len(self.equation) == 2:
            new_text = "{0} {1}".format(self.display.text(), number)
            self.display.setText(new_text)
            self.equation.append(number)
            self.compute()

    def onOperator(self, operator):
        if len(self.equation) == 4:
            result = self.equation.pop()
            self.equation = []
            self.onNumber(result)

        if len(self.equation) == 1:
            new_text = "{0} {1}".format(self.display.text(), operator)
            self.display.setText(new_text)
            self.equation.append(operator)

    def compute(self):
        result = self.calculator.compute(*self.equation)
        new_text = "{0} = {1}".format(self.display.text(), result)
        self.display.setText(new_text)
        self.equation.append(result)

class Numpad(QtWidgets.QWidget):
    number = QtCore.pyqtSignal([int])
    operator = QtCore.pyqtSignal([str])

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

    def setup(self):
        layout = QtWidgets.QGridLayout()
        self.setLayout(layout)
        numbers = range(0, 10)
        operators = ("+", "-", "*", "/")

        emit_number = lambda num: lambda: self.number.emit(num)
        emit_operator = lambda op: lambda: self.operator.emit(op)

        for num in numbers:
            button = QtWidgets.QPushButton(str(num), self)
            button.clicked.connect(emit_number(num))

            if num == 0:
                layout.addWidget(button, 3, 0, 1, 3)
            else:
                row = math.floor((num - 1) / 3)
                col = (num - 1) % 3
                layout.addWidget(button, row, col)

        for i, op in enumerate(operators):
            button = QtWidgets.QPushButton(op, self)
            button.clicked.connect(emit_operator(op))
            layout.addWidget(button, i, 3)

class Calculator:
    def compute(self, first_number, operator, second_number):
        ops = {
            "+": lambda: first_number + second_number,
            "-": lambda: first_number - second_number,
            "*": lambda: first_number * second_number,
            "/": lambda: first_number / second_number,
        }

        return ops[operator]()

if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    window = MainWindow()
    window.show()
    app.exec_()

Kommentit

The Alchemist [23.12.2020 20:53:04]

#

Tässä vielä äärimmilleen viety esimerkki sovelluslogiikan ja käyttöliittymäkomponenttien eristämisestä toisistaan. Lienee mielipidekysymys, kummallako tavalla toteutettu laskin on paremman laatuista koodia.

Tämän sovelluksen toiminta on identtinen tuon ylläolevan kanssa. Koodia on kuitenkin liki 40 riviä / 33 % enemmän. Hmm...

import math
import sys
from PyQt5.QtCore import Qt
from PyQt5 import QtCore, QtGui, QtWidgets

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        self.setup()
        self.calculator = Calculator()

    def setup(self):
        container = QtWidgets.QWidget(self)

        display = QtWidgets.QLabel()
        display.setFont(QtGui.QFont("monospace", 20))
        display.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
        display.setText("0")

        numpad = Numpad(self)
        numpad.number.connect(self.onNumber)
        numpad.operator.connect(self.onOperator)

        layout = QtWidgets.QVBoxLayout()
        layout.addWidget(display)
        layout.addWidget(numpad)

        container.setLayout(layout)
        self.setCentralWidget(container)

        self.display = display
        self.numpad = numpad

    def onNumber(self, number):
        if self.calculator.has_result():
            self.display.setText("")

        self.calculator.add_number(number)

        if self.calculator.can_compute():
            self.compute()
        else:
            self.update()

    def onOperator(self, operator):
        if self.calculator.has_result():
            result = self.calculator.result
            self.calculator.clear()
            self.onNumber(result)

        if self.calculator.set_operator(operator):
            self.update()

    def compute(self):
        self.update()

        result = self.calculator.compute()
        new_text = "{0} = {1}".format(self.display.text(), result)
        self.display.setText(new_text)

    def update(self):
        new_text = " ".join(map(str, self.calculator.equation))
        self.display.setText(new_text)

class Numpad(QtWidgets.QWidget):
    number = QtCore.pyqtSignal([int])
    operator = QtCore.pyqtSignal([str])

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

    def setup(self):
        layout = QtWidgets.QGridLayout()
        self.setLayout(layout)
        numbers = range(0, 10)
        operators = ("+", "-", "*", "/")

        emit_number = lambda num: lambda: self.number.emit(num)
        emit_operator = lambda op: lambda: self.operator.emit(op)

        for num in numbers:
            button = QtWidgets.QPushButton(str(num), self)
            button.clicked.connect(emit_number(num))

            if num == 0:
                layout.addWidget(button, 3, 0, 1, 3)
            else:
                row = math.floor((num - 1) / 3)
                col = (num - 1) % 3
                layout.addWidget(button, row, col)

        for i, op in enumerate(operators):
            button = QtWidgets.QPushButton(op, self)
            button.clicked.connect(emit_operator(op))
            layout.addWidget(button, i, 3)

class Calculator:
    def __init__(self):
        self.equation = []
        self.result = None

    def add_number(self, number):
        if len(self.equation) == 3:
            self.equation = []

        if len(self.equation) == 0 or len(self.equation) == 2:
            self.equation.append(number)
            self.result = None
            return min(len(self.equation), 2)

        return None

    def set_operator(self, operator):
        if self.has_result():
            self.equation = []
            self.add_number(self.result)

        if len(self.equation) == 1:
            self.equation.append(operator)
            return True

        return False

    def can_compute(self):
        return len(self.equation) == 3

    def has_result(self):
        return self.result is not None

    def clear(self):
        self.equation = []
        self.result = None

    def compute(self):
        ops = {
            "+": lambda: first_number + second_number,
            "-": lambda: first_number - second_number,
            "*": lambda: first_number * second_number,
            "/": lambda: first_number / second_number,
        }

        first_number, operator, second_number = self.equation
        self.result = ops[operator]()
        return self.result

if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    window = MainWindow()
    window.show()
    app.exec_()

jalski [23.12.2020 21:32:30]

#

Saatko kuvaa ohjelmasta näytille mihinkään? Kiinnostaa minkänäköisen ohjelman tuolla esimerkillä toteuttaa! Miksi muuten valisit QT:n? Kaupallinen lisenssi indie-kehittäjälle on mielestäni kuitenkin kohtuullisen kallis...

vesikuusi [24.12.2020 00:18:36]

#

jalski kirjoitti:

Miksi muuten valisit QT:n? Kaupallinen lisenssi indie-kehittäjälle on mielestäni kuitenkin kohtuullisen kallis...

Harrastelija voi tehdä Qt:llä niin paljon kuin huvittaa ihan ilmaiseksi. Jos taas käytät Qt:tä liiketoimintaan niin ei tuo hinnoittelu mitenkään paha silloinkaan ole.

Lisäksi tarjolla on ilmainen avoimen lähdekoodin lisenssi, mikäli tuotteesi lisensointi sitä tukee.

jalski [24.12.2020 00:49:22]

#

vesikuusi kirjoitti:

Harrastelija voi tehdä Qt:llä niin paljon kuin huvittaa ihan ilmaiseksi. Jos taas käytät Qt:tä liiketoimintaan niin ei tuo hinnoittelu mitenkään paha silloinkaan ole.

Lisäksi tarjolla on ilmainen avoimen lähdekoodin lisenssi, mikäli tuotteesi lisensointi sitä tukee.

Jos taas haluaa Pythonilla käyttäen PyQT:ta kaupallista softaa kirjoitella, niin tuohon voi heti lisätä 550 USD noin ensi alkuun... Lisäksi pitää huomioida, että tuo pienelle yritykselle tarjottava lisensi ei tarjoa kunnollista teknistä tukea asennusta lukuunottamatta.

vesikuusi [24.12.2020 01:03:48]

#

Niin, mikäli lisenssisi ei ole GPL-yhteensopiva.

Ei siinä, erikoinen kysymys vain jonkun taskulaskinkoodiesimerkin yhteydessä.

The Alchemist [24.12.2020 08:22:17]

#

jalski kirjoitti:

Saatko kuvaa ohjelmasta näytille mihinkään? Kiinnostaa minkänäköisen ohjelman tuolla esimerkillä toteuttaa!

Luulisin jokaisen karvalakkilaskimen näyttävän 1:1 samalta...

https://ibb.co/FnTZqSx

(Kuva poistuu automaattisesti kuukauden kuluttua.)

jalski kirjoitti:

Miksi muuten valisit QT:n? Kaupallinen lisenssi indie-kehittäjälle on mielestäni kuitenkin kohtuullisen kallis...

En ole miettinyt tällaisia asioita. Mutta Qt:llahan voi kehittää kaupallistakin softaa ilmaiseksi; GPL ei sitä kiellä laisinkaan.

Aikoinaan kiinnostuin Qt:sta koska se tarjosi erinomaisen crossplatform-tuen useille eri käyttiksille. Eräs vuosia sitten tekemäni ohjelmisto on portattu jonkun minulle tuntemattoman harrastajan toimesta jopa OS/2:lle...

jalski [24.12.2020 12:50:30]

#

The Alchemist kirjoitti:

Aikoinaan kiinnostuin Qt:sta koska se tarjosi erinomaisen crossplatform-tuen useille eri käyttiksille. Eräs vuosia sitten tekemäni ohjelmisto on portattu jonkun minulle tuntemattoman harrastajan toimesta jopa OS/2:lle...

Käytin OS/2:sta useita vuosia, vieläkin pidän työpöydästä enemmän kuin Windowsin. Harmi, että IBM ei vienyt kehitystyötä eteenpäin. PowerPC alustalle oli kuitenkin jo valmiina toimiva prototyyppi käytännössä AIX:n päällä toimivasta Workplace shell työpöydästä.

8th sisältää nykyään integroidun Nuklear GUI tuen. Omaan käyttööni tuo soveltuu erittäin hyvin ja toimii kaikilla alustoilla, hieman epästandardin näköisen käyttöliittymän kustannuksella tosin. Tietysti jos tarvitaan jotain hienoja widgettejä ja erikoistoiminnallisuutta, niin ne joutuu toteuttamaan itse. QT:n etuna varmasti on, että lähestulkoon kaikenmoiset widgetit ja erikoistoiminnalisuus löytyvät valmiina ja kehittäjä voi alkaa suoraan keskittymään itse ohjelman toteutukseen.

8th Nuklear GUI, Laskin

Alla vertailun vuoksi koodi, mikä toteuttaa kyseisen laskimen:

\ Simple calculator

needs nk/gui

libbin font/Roboto-Regular.ttf

: new-win
  {
    name: "main",
    wide: 0.25,
    high: 0.25,
    fonts: {
      f1: {
        font: @font/Roboto-Regular.ttf
      }
    },
    fontheight: ` nk:screen-size a:open nip 20 n:/ 14 n:max `,
    font: "f1",
    title: "Simple Calculator"
  }
  "fontheight" m:@ 1.2 n:*
  "rowheight" swap m:!
  nk:win ;

: +n \ nk n --
  >r
  "num" nk:m@ 10 n:*
  r> n:+ "num" swap nk:m!  ;

: mathop \ nk w -- nk
  "op" swap nk:m!
  "num" nk:m@ "a" swap nk:m!
  "num" 0 nk:m! ;

: main-render
  {
    title: "calc",
    bg: "white",
    flags: [ @nk:WINDOW_NO_SCROLLBAR ]
  }

  nk:begin
    0 1 nk:layout-row-dynamic
    "num" nk:m@ null? if
      drop
      0 "num" over nk:m!
    then
    >s 32 nk:EDIT_SIMPLE nk:EDIT_READ_ONLY n:bor nk:PLUGIN_FILTER_FLOAT edit-string drop

    nk:win-high nk:widget-bounds 3 a:_@  n:- 5 n:/ 4 nk:layout-row-dynamic
    \ the button labels:
    [
      "1", "2", "3", "+" ,
      "4", "5", "6", "-" ,
      "7", "8", "9", "*" ,
      "C", "0", "=", "/"
    ]
    (
      \ nk n s
      swap >r \ save the button index
      (
        \ act based on what the index is
        r@
        [
          ( 1 +n ) , ( 2 +n ) , ( 3 +n ) , ( ' n:+ mathop ) ,
          ( 4 +n ) , ( 5 +n ) , ( 6 +n ) , ( ' n:- mathop ) ,
          ( 7 +n ) , ( 8 +n ) , ( 9 +n ) , ( ' n:* mathop ) ,
          ( "a" 0 nk:m! "num" 0 nk:m! ) , ( 0 +n ) , ( "eq" true nk:m! ) , ( ' n:/ mathop )
        ] case
        "eq" nk:m@ if
          "eq" false nk:m!
          "op" nk:m@ >r
          "num" nk:m@ >r
          "a" nk:m@ r> r> w:exec
          "num" swap nk:m!
          "a" 0 nk:m!
        then
      )
      nk:button-label
      rdrop
    ) a:each drop

  nk:end ;

: app:main
  new-win ' main-render -1 nk:render-loop ;

The Alchemist [24.12.2020 15:48:47]

#

jalski kirjoitti:

QT:n etuna varmasti on, että lähestulkoon kaikenmoiset widgetit ja erikoistoiminnalisuus löytyvät valmiina ja kehittäjä voi alkaa suoraan keskittymään itse ohjelman toteutukseen.

Itse asiassa tämä ns. natiivien widgettien vähyys minua alkoi ärsyttää. Qt:n vakio työkalupakki kun ei sisällä kuin Windows 98:n tasoiset vitkuttimet, ja täysin uudenlaisten omien teko on hemmetin vaikeaa.

Nykyään kehittelen harrastuspohjalta enää Linuxille, joten yritän siirtyä GTK:hon, mutta sen kanssa taas hiertää C-lähtöinen rajapinta, joka on erittäin työläs käyttää. Lisäksi GTK:ssa on joitakin suorituskykyä rampauttavia suunnittelumokia, jotka ovat nykytiedon valossa korjaantumassa vasta GTK 5:een – ja GTK 4.0 kun julkaistiin juuri männä viikolla...

Kirjoita kommentti

Muista lukea kirjoitusohjeet.
Tietoa sivustosta