PyIt / Qt uygulamalarındaki mantıktan UI'yi nasıl düzgün şekilde ayırırım?


20

Geçmişte bu konu hakkında çok şey okudum ve Bob Amca'nın bu gibi ilginç konuşmalarını izledim . Yine de, masaüstü uygulamalarımı düzgün bir şekilde tasarlamak ve hangisinin UI tarafında ve hangilerinin mantık tarafında olması gerektiğini ayırt etmek oldukça zor buluyorum .

İyi uygulamaların çok kısa bir özeti böyle bir şeydir. Mantığınızı kullanıcı arayüzünden ayrıştırılmış olarak tasarlamalısınız, böylece hangi tür arka uç / kullanıcı arayüzü çerçevesi olursa olsun kütüphanenizi (teorik olarak) kullanabilirsiniz. Bunun anlamı, temel olarak UI'nin mümkün olduğunca kukla olması ve ağır işlemin mantık tarafında yapılmasıdır. Aksi takdirde, güzel kütüphanemi bir konsol uygulaması, bir web uygulaması veya bir masaüstü uygulamasıyla tam anlamıyla kullanabilirim.

Ayrıca, Bob Amca, hangi teknolojinin kullanılacağına dair size çok fazla fayda (iyi arayüzler) sunacak farklı tartışmalar önerir, bu erteleme kavramı, kulağa harika gelen ama yine de zor olan, son derece ayrıştırılmış iyi test edilmiş varlıklara sahip olmanızı sağlar.

Bu sorunun, tüm internette ve ayrıca tonlarca iyi kitapta birçok kez tartışılan oldukça geniş bir soru olduğunu biliyorum. İyi bir şey elde etmek için pyqt üzerinde MCV kullanmaya çalışan çok az kukla bir örnek yayınlayacağım:

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_())

Yukarıdaki snippet çok sayıda kusur içeriyor, daha açık olan model UI çerçevesine (QObject, pyqt sinyalleri) bağlanıyor. Örnek gerçekten kukla ve tek bir QMainWindow kullanarak birkaç satır kod olabilir biliyorum ama amacım daha büyük bir pyqt uygulaması düzgün bir şekilde nasıl mimar anlamak için.

SORU

İyi genel uygulamaları takip ederek MVC kullanarak büyük bir PyQt uygulamasını nasıl düzgün şekilde tasarlarsınız?

REFERANSLAR

Burada buna benzer bir soru yaptım

Yanıtlar:


1

(Öncelikle) WPF / ASP.NET arka planından geliyorum ve şu anda bir MVC-ish PyQT uygulaması yapmaya çalışıyorum ve bu çok soru beni rahatsız ediyor. Yaptığım şeyi paylaşacağım ve yapıcı yorumlar veya eleştiriler almak isterdim.

İşte küçük bir ASCII diyagramı:

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
      ...

Uygulamam, bir çok programcı tarafından kolayca değiştirilmesi gereken çok sayıda UI öğesi ve widget'a sahip. "Görünüm" kodu, sağda bir QStackedWidget tarafından görüntülenen öğeleri içeren bir QTreeWidget içeren bir QMainWindow'dan oluşur (Ana Detay Ayrıntısını düşünün).

Öğeler QTreeWidget'ten dinamik olarak eklenip kaldırılabildiğinden ve geri alma işlevini geri almak istediğim için, geçerli / önceki durumları izleyen bir model oluşturmayı seçtim. Kullanıcı arabirimi komutları, denetleyici tarafından bilgileri modele iletir (pencere öğesi ekleme veya kaldırma, pencere aracındaki bilgileri güncelleme). Denetleyicinin kullanıcı arayüzüne kadar bilgi aktardığı tek zaman doğrulama, olay işleme ve bir dosya yükleme / geri alma ve yineleme üzerinedir.

Modelin kendisi, UI eleman kimliğinin son tuttuğu değerle (ve birkaç ek bilgi parçasından) oluşan bir sözlükten oluşur. Önceki sözlüklerin bir listesini saklıyorum ve birisi geri alırsa bir öncekine dönebilirim. Sonunda model belirli bir dosya formatı olarak diske dökülür.

Dürüst olacağım - bunu tasarlamak oldukça zor buldum. PyQT, modelden boşanmaya kendini iyi borçlu gibi hissetmiyor ve buna oldukça benzer bir şey yapmaya çalışan herhangi bir açık kaynak programı bulamadım. Diğer insanların buna nasıl yaklaştığını merak ediyorum.

Not: QML'nin MVC yapmak için bir seçenek olduğunu fark ettim ve ne kadar Javascript'in dahil olduğunu anlayana kadar çekici görünüyordu - ve PyQT'ye (veya sadece döneme) taşınması açısından hala oldukça olgunlaşmamıştı. Büyük hata ayıklama araçlarının karmaşık faktörleri (sadece PyQT ile yeterince zor) ve JS'yi bilmeyen diğer programcıların bu kodu kolayca değiştirmesi gereği.


0

Bir uygulama oluşturmak istedim. Küçük görevler (db'de bir şey arayın, bir şey hesaplayın, otomatik tamamlama ile bir kullanıcı arayın) bireysel işlevler yazmaya başladım. Terminalde görüntülenir. Sonra bu yöntemleri bir dosyaya koyun,main.py ..

Sonra bir kullanıcı arayüzü eklemek istedim. Farklı araçlara baktım ve Qt. Kullanıcı arayüzünü oluşturmak için İçerik Oluşturucu'yu kullandım.pyuic4 üretmek içinUI.py .

İçinde main.py , içe aktardımUI . Daha sonra çekirdek işlevselliğin üzerine UI olayları tarafından tetiklenen yöntemleri ekledi (kelimenin tam anlamıyla üstte: "çekirdek" kodu dosyanın altındadır ve UI ile ilgisi yoktur, isterseniz kabuğundan kullanabilirsiniz kadar).

İşte bir yöntem örneği display_suppliersTabloda tedarikçi listesini (alanlar: ad, hesap) görüntüleyen bir . (Bunu sadece yapıyı göstermek için kodun geri kalanından kestim).

Kullanıcı metin alanına yazarken HSGsupplierNameEdit , metin değişir ve her seferinde bu yöntem çağrılır, böylece tablo kullanıcı türleri olarak değişir.

Tedarikçileri get_suppliers(opchoice), kullanıcı arayüzünden bağımsız olan ve konsoldan da çalışan bir yöntemden alır .

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

En iyi uygulamalar ve bunun gibi şeyler hakkında fazla bir şey bilmiyorum, ama bu benim için mantıklı olan ve tesadüfen bir aradan sonra uygulamaya geri dönmeyi ve web2py kullanarak bir web uygulaması yapmak istememi kolaylaştıran şey bu veya webapp2. Aslında şeyleri yapan kodun bağımsız ve altta olması, onu yakalamanızı ve ardından sonuçların görüntülenme şeklini değiştirmenizi kolaylaştırır (html öğeleri ve masaüstü öğeleri).


0

... bir çok kusur, modelin UI çerçevesine (QObject, pyqt sinyalleri) bağlanması daha açıktır.

Yani bunu yapma!

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)

Bu, modelinizi Qt'den tamamen ayıran önemsiz bir değişiklikti. Şimdi farklı bir modüle bile taşıyabilirsiniz.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.