Bagaimana cara decouple UI dari logika pada aplikasi Pyqt / Qt dengan benar?


20

Saya sudah membaca cukup banyak tentang subjek ini di masa lalu dan menonton beberapa pembicaraan menarik seperti ini dari Paman Bob . Namun, saya selalu menemukan kesulitan untuk merancang aplikasi desktop saya dengan benar dan membedakan mana yang harus menjadi tanggung jawab di sisi UI dan mana yang di sisi logika .

Ringkasan singkat dari praktik yang baik adalah sesuatu seperti ini. Anda harus mendesain logika Anda dipisahkan dari UI, sehingga Anda bisa menggunakan (secara teoritis) perpustakaan Anda apa pun jenis kerangka backend / UI. Apa artinya ini pada dasarnya UI harus sedapat mungkin dummy dan pemrosesan yang berat harus dilakukan pada sisi logika. Berkata sebaliknya, saya benar-benar dapat menggunakan perpustakaan saya yang bagus dengan aplikasi konsol, aplikasi web atau desktop.

Juga, paman Bob menyarankan diskusi berbeda tentang teknologi mana yang akan digunakan akan memberi Anda banyak manfaat (antarmuka yang baik), konsep penangguhan ini memungkinkan Anda untuk memiliki entitas yang teruji dengan sangat baik, kedengarannya hebat tetapi masih rumit.

Jadi, saya tahu pertanyaan ini adalah pertanyaan yang cukup luas yang telah dibahas berkali-kali di seluruh internet dan juga dalam banyak buku bagus. Jadi untuk mendapatkan sesuatu yang baik dari itu saya akan memposting contoh dummy yang sangat sedikit mencoba menggunakan MCV di pyqt:

import sys
import os
import random

from PyQt5 import QtWidgets
from PyQt5 import QtGui
from PyQt5 import QtCore

random.seed(1)


class Model(QtCore.QObject):

    item_added = QtCore.pyqtSignal(int)
    item_removed = QtCore.pyqtSignal(int)

    def __init__(self):
        super().__init__()
        self.items = {}

    def add_item(self):
        guid = random.randint(0, 10000)
        new_item = {
            "pos": [random.randint(50, 100), random.randint(50, 100)]
        }
        self.items[guid] = new_item
        self.item_added.emit(guid)

    def remove_item(self):
        list_keys = list(self.items.keys())

        if len(list_keys) == 0:
            self.item_removed.emit(-1)
            return

        guid = random.choice(list_keys)
        self.item_removed.emit(guid)
        del self.items[guid]


class View1():

    def __init__(self, main_window):
        self.main_window = main_window

        view = QtWidgets.QGraphicsView()
        self.scene = QtWidgets.QGraphicsScene(None)
        self.scene.addText("Hello, world!")

        view.setScene(self.scene)
        view.setStyleSheet("background-color: red;")

        main_window.setCentralWidget(view)


class View2():

    add_item = QtCore.pyqtSignal(int)
    remove_item = QtCore.pyqtSignal(int)

    def __init__(self, main_window):
        self.main_window = main_window

        button_add = QtWidgets.QPushButton("Add")
        button_remove = QtWidgets.QPushButton("Remove")
        vbl = QtWidgets.QVBoxLayout()
        vbl.addWidget(button_add)
        vbl.addWidget(button_remove)
        view = QtWidgets.QWidget()
        view.setLayout(vbl)

        view_dock = QtWidgets.QDockWidget('View2', main_window)
        view_dock.setWidget(view)

        main_window.addDockWidget(QtCore.Qt.RightDockWidgetArea, view_dock)

        model = main_window.model
        button_add.clicked.connect(model.add_item)
        button_remove.clicked.connect(model.remove_item)


class Controller():

    def __init__(self, main_window):
        self.main_window = main_window

    def on_item_added(self, guid):
        view1 = self.main_window.view1
        model = self.main_window.model

        print("item guid={0} added".format(guid))
        item = model.items[guid]
        x, y = item["pos"]
        graphics_item = QtWidgets.QGraphicsEllipseItem(x, y, 60, 40)
        item["graphics_item"] = graphics_item
        view1.scene.addItem(graphics_item)

    def on_item_removed(self, guid):
        if guid < 0:
            print("global cache of items is empty")
        else:
            view1 = self.main_window.view1
            model = self.main_window.model

            item = model.items[guid]
            x, y = item["pos"]
            graphics_item = item["graphics_item"]
            view1.scene.removeItem(graphics_item)
            print("item guid={0} removed".format(guid))


class MainWindow(QtWidgets.QMainWindow):

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

        # (M)odel ===> Model/Library containing should be UI agnostic, right now it's not
        self.model = Model()

        # (V)iew      ===> Coupled to UI
        self.view1 = View1(self)
        self.view2 = View2(self)

        # (C)ontroller ==> Coupled to UI
        self.controller = Controller(self)

        self.attach_views_to_model()

    def attach_views_to_model(self):
        self.model.item_added.connect(self.controller.on_item_added)
        self.model.item_removed.connect(self.controller.on_item_removed)


if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)

    form = MainWindow()
    form.setMinimumSize(800, 600)
    form.show()
    sys.exit(app.exec_())

Cuplikan di atas mengandung banyak kekurangan, yang lebih jelas adalah model yang digabungkan ke kerangka UI (QObject, sinyal pyqt). Saya tahu contohnya adalah benar-benar dummy dan Anda dapat mengkodekannya pada beberapa baris menggunakan QMainWindow tunggal tetapi tujuan saya adalah untuk memahami bagaimana merancang dengan benar aplikasi pyqt yang lebih besar.

PERTANYAAN

Bagaimana Anda merancang aplikasi PyQt besar menggunakan MVC dengan mengikuti praktik umum yang baik?

REFERENSI

Saya telah membuat pertanyaan serupa dengan ini di sini

Jawaban:


1

Saya berasal dari latar belakang (terutama) WPF / ASP.NET dan berusaha membuat aplikasi PyQT MVC-ish sekarang dan pertanyaan ini sangat menghantui saya. Saya akan membagikan apa yang saya lakukan dan saya ingin tahu untuk mendapatkan komentar atau kritik yang membangun.

Berikut adalah diagram ASCII kecil:

View                          Controller             Model
---------------
| QMainWindow |   ---------> controller.py <----   Dictionary containing:
---------------   Add, remove from View                |
       |                                               |
    QWidget       Restore elements from Model       UIElementId + data
       |                                               |
    QWidget                                         UIElementId + data
       |                                               |
    QWidget                                         UIElementId + data
      ...

Aplikasi saya memiliki banyak (BANYAK) elemen UI dan widget yang perlu dimodifikasi dengan mudah oleh sejumlah programmer. Kode "view" terdiri dari QMainWindow dengan QTreeWidget yang berisi item yang ditampilkan oleh QStackedWidget di sebelah kanan (pikirkan tampilan Detail Master).

Karena item dapat ditambahkan dan dihapus secara dinamis dari QTreeWidget, dan saya ingin mendukung fungsi undo-redo, saya memilih untuk membuat model yang melacak keadaan saat ini / sebelumnya. Perintah UI meneruskan informasi ke model (menambah atau menghapus widget, memperbarui informasi dalam widget) oleh pengontrol. Satu-satunya waktu pengontrol meneruskan informasi hingga UI adalah validasi, penanganan acara, dan memuat file / undo & redo.

Model itu sendiri terdiri dari kamus ID elemen UI dengan nilai yang terakhir dipegangnya (dan beberapa informasi tambahan). Saya menyimpan daftar kamus sebelumnya dan dapat kembali ke yang sebelumnya jika ada yang gagal. Akhirnya model akan dibuang ke disk sebagai format file tertentu.

Saya akan jujur ​​- saya menemukan ini cukup sulit untuk dirancang. PyQT merasa tidak cocok untuk bercerai dari model, dan saya tidak dapat menemukan program open source yang mencoba melakukan sesuatu yang mirip dengan ini. Penasaran bagaimana orang lain mendekati ini.

PS: Saya menyadari QML adalah pilihan untuk melakukan MVC, dan sepertinya menarik sampai saya menyadari berapa banyak Javascript yang terlibat - dan faktanya itu masih belum matang dalam hal porting ke PyQT (atau hanya periode). Faktor-faktor yang menyulitkan dari tidak ada alat debugging yang hebat (cukup keras hanya dengan PyQT) dan perlunya programmer lain untuk memodifikasi kode ini dengan mudah yang tidak tahu JS memperbaikinya.


0

Saya ingin membangun aplikasi. Saya mulai menulis fungsi individual yang melakukan tugas kecil (mencari sesuatu di db, menghitung sesuatu, mencari pengguna dengan pelengkapan otomatis). Ditampilkan di terminal. Kemudian masukkan metode ini dalam file, main.py..

Lalu saya ingin menambahkan UI. Saya melihat-lihat alat yang berbeda dan memilih Qt. Saya menggunakan Creator untuk membangun UI, lalu pyuic4menghasilkan UI.py.

Di main.py, saya mengimpor UI. Kemudian menambahkan metode yang dipicu oleh peristiwa UI di atas fungsionalitas inti (secara harfiah di atas: "inti" kode di bagian bawah file dan tidak ada hubungannya dengan UI, Anda bisa menggunakannya dari shell jika Anda mau) untuk).

Berikut adalah contoh metode display_suppliersyang menampilkan daftar pemasok (bidang: nama, akun) pada Tabel. (Saya memotong ini dari sisa kode hanya untuk menggambarkan struktur).

Saat pengguna mengetik di bidang teks HSGsupplierNameEdit, teks berubah dan setiap kali melakukannya, metode ini disebut sehingga Tabel berubah saat pengguna mengetik.

Ia mendapat pemasok dari metode yang disebut get_suppliers(opchoice)independen dari UI dan bekerja dari konsol juga.

from PyQt4 import QtCore, QtGui
import UI

class Treasury(QtGui.QMainWindow):

    def __init__(self, parent=None):
        self.ui = UI.Ui_MainWindow()
        self.ui.setupUi(self)
        self.ui.HSGsuppliersTable.resizeColumnsToContents()
        self.ui.HSGsupplierNameEdit.textChanged.connect(self.display_suppliers)

    @QtCore.pyqtSlot()
    def display_suppliers(self):

        """
            Display list of HSG suppliers in a Table.
        """
        # TODO: Refactor this code and make it generic
        #       to display a list on chosen Table.


        self.suppliers_virement = self.get_suppliers(self.OP_VIREMENT)
        name = unicode(self.ui.HSGsupplierNameEdit.text(), 'utf_8')
        # Small hack for auto-modifying list.
        filtered = [sup for sup in self.suppliers_virement if name.upper() in sup[0]]

        row_count = len(filtered)
        self.ui.HSGsuppliersTable.setRowCount(row_count)

        # supplier[0] is the supplier's name.
        # supplier[1] is the supplier's account number.

        for index, supplier in enumerate(filtered):
            self.ui.HSGsuppliersTable.setItem(
                index,
                0,
                QtGui.QTableWidgetItem(supplier[0])
            )

            self.ui.HSGsuppliersTable.setItem(
                index,
                1,
                QtGui.QTableWidgetItem(self.get_supplier_bank(supplier[1]))
            )

            self.ui.HSGsuppliersTable.setItem(
                index,
                2,
                QtGui.QTableWidgetItem(supplier[1])
            )

            self.ui.HSGsuppliersTable.resizeColumnsToContents()
            self.ui.HSGsuppliersTable.horizontalHeader().setStretchLastSection(True)


    def get_suppliers(self, opchoice):
        '''
            Return a list of suppliers who are 
            relevant to the chosen operation. 

        '''
        db, cur = self.init_db(SUPPLIERS_DB)
        cur.execute('SELECT * FROM suppliers WHERE operation = ?', (opchoice,))
        data = cur.fetchall()
        db.close()
        return data

Saya tidak tahu banyak tentang praktik terbaik dan hal-hal seperti itu, tetapi inilah yang masuk akal bagi saya dan secara kebetulan membuatnya lebih mudah bagi saya untuk kembali ke aplikasi setelah jeda dan ingin membuat aplikasi web darinya menggunakan web2py atau webapp2. Faktanya kode yang benar-benar melakukan hal-hal itu independen dan di bagian bawah membuatnya mudah untuk hanya mengambilnya, dan kemudian hanya mengubah cara hasilnya ditampilkan (elemen html vs elemen desktop).


0

... banyak kekurangan, yang lebih jelas adalah model yang digabungkan ke kerangka UI (QObject, sinyal pyqt).

Jadi jangan lakukan ini!

class Model(object):
    def __init__(self):
        self.items = {}
        self.add_callbacks = []
        self.del_callbacks = []

    # just use regular callbacks, caller can provide a lambda or whatever
    # to make the desired Qt call
    def emit_add(self, guid):
        for cb in self.add_callbacks:
            cb(guid)

Itu adalah perubahan sepele, yang sepenuhnya memisahkan model Anda dari Qt. Anda bahkan dapat memindahkannya ke modul yang berbeda sekarang.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.