วิธีการแยก UI จากตรรกะบนแอป Pyqt / Qt อย่างถูกต้อง?


20

ผมเคยอ่านค่อนข้างมากเกี่ยวกับเรื่องนี้ในอดีตและเฝ้าดูการเจรจาที่น่าสนใจบางเช่นเดียวจากนี้ลุงบ๊อบ ถึงกระนั้นฉันมักจะพบว่ามันยากที่จะออกแบบสก์ท็อปของฉันอย่างถูกต้องและแยกแยะความแตกต่างซึ่งควรเป็นความรับผิดชอบในด้านUIและสิ่งที่อยู่ในด้านตรรกะ

สรุปโดยย่อเกี่ยวกับแนวทางปฏิบัติที่ดีคือสิ่งนี้ คุณควรออกแบบตรรกะของคุณแยกจาก UI ดังนั้นวิธีที่คุณสามารถใช้ (ในทางทฤษฎี) ไลบรารีของคุณไม่ว่าจะเป็นแบ็กเอนด์ / UI ใด ๆ สิ่งนี้หมายความว่าโดยพื้นฐานแล้ว UI ควรเป็นคนโง่ที่สุดและควรทำการประมวลผลอย่างหนักที่ด้านตรรกะ ถ้าอย่างนั้นฉันก็สามารถใช้ไลบรารี่ที่สวยงามของฉันกับแอพพลิเคชั่นคอนโซลเว็บแอพพลิเคชั่น

นอกจากนี้ลุงบ๊อบแนะนำการอภิปรายที่แตกต่างกันของเทคโนโลยีที่ใช้จะให้ประโยชน์มากมาย (อินเทอร์เฟซที่ดี) แนวคิดการชะลอตัวนี้ช่วยให้คุณมีเอนทิตี้ที่ผ่านการทดสอบอย่างดีซึ่งฟังดูดี แต่ก็ยังยุ่งยาก

ดังนั้นฉันรู้ว่าคำถามนี้เป็นคำถามที่ค่อนข้างกว้างขวางซึ่งมีการพูดคุยกันหลายครั้งผ่านอินเทอร์เน็ตทั้งหมดและในหนังสือที่ดีมากมาย ดังนั้นเพื่อให้ได้สิ่งที่ดีออกมาฉันจะโพสต์ตัวอย่างตัวอย่างเล็ก ๆ น้อย ๆ ที่พยายามใช้ MCV บน 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_())

ตัวอย่างด้านบนมีข้อบกพร่องมากมายรูปแบบที่ชัดเจนยิ่งขึ้นประกอบกับกรอบของ UI (สัญญาณ QObject, pyqt) ฉันรู้ว่าตัวอย่างนั้นหลอกตาจริง ๆ และคุณสามารถเขียนมันในไม่กี่บรรทัดโดยใช้ QMainWindow เพียงอันเดียว แต่จุดประสงค์ของฉันคือการเข้าใจวิธีการออกแบบแอปพลิเคชัน pyqt ที่ใหญ่ขึ้นอย่างถูกต้อง

คำถาม

คุณจะออกแบบแอปพลิเคชัน PyQt ขนาดใหญ่ให้เหมาะสมโดยใช้ MVC ตามแนวทางปฏิบัติทั่วไปอย่างไร

ข้อมูลอ้างอิง

ฉันได้ทำคำถามที่คล้ายกันนี้ที่นี่

คำตอบ:


1

ฉันมาจากพื้นหลัง WPF / ASP.NET (เป็นหลัก) และพยายามที่จะสร้างแอป PyQT MVC-ish PyQT ในตอนนี้และคำถามนี้ก็ทำให้ฉันรำคาญ ฉันจะแบ่งปันสิ่งที่ฉันทำและฉันอยากรู้อยากเห็นเพื่อแสดงความคิดเห็นหรือคำวิจารณ์ที่สร้างสรรค์

นี่คือแผนภาพ ASCII เล็กน้อย:

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

แอปพลิเคชันของฉันมีองค์ประกอบ UI และวิดเจ็ตจำนวนมาก (LOT) ที่ต้องได้รับการแก้ไขอย่างง่ายดายโดยโปรแกรมเมอร์จำนวนหนึ่ง รหัส "มุมมอง" ประกอบด้วย QMainWindow พร้อม QTreeWidget ที่มีรายการที่แสดงโดย QStackedWidget ทางด้านขวา (คิดว่ามุมมองรายละเอียดหลัก)

เนื่องจากสามารถเพิ่มและลบรายการได้แบบไดนามิกจาก QTreeWidget และฉันต้องการสนับสนุนฟังก์ชั่นการเลิกทำซ้ำฉันเลือกที่จะสร้างแบบจำลองที่ติดตามสถานะปัจจุบัน / ก่อนหน้า คำสั่ง UI ส่งผ่านข้อมูลไปยังโมเดล (การเพิ่มหรือลบวิดเจ็ตการอัพเดตข้อมูลในวิดเจ็ต) โดยคอนโทรลเลอร์ ครั้งเดียวที่คอนโทรลเลอร์ส่งข้อมูลถึง UI คือการตรวจสอบความถูกต้องการจัดการเหตุการณ์และการโหลดไฟล์ / เลิกทำ & ทำซ้ำ

รูปแบบนั้นประกอบด้วยพจนานุกรมขององค์ประกอบองค์ประกอบ UI ที่มีค่าที่เก็บล่าสุด (และข้อมูลเพิ่มเติมอีกสองสามชิ้น) ฉันเก็บรายการพจนานุกรมก่อนหน้าและสามารถเปลี่ยนกลับไปเป็นพจนานุกรมก่อนหน้าได้หากมีคนกดปุ่มเลิกทำ ในที่สุดโมเดลจะถูกดัมพ์ไปยังดิสก์เป็นรูปแบบไฟล์ที่แน่นอน

ฉันจะซื่อสัตย์ - ฉันพบว่ามันค่อนข้างยากในการออกแบบ PyQT ไม่รู้สึกว่ามันยืมตัวเองได้ดีจากการหย่าร้างจากแบบจำลองและฉันก็ไม่สามารถหาโปรแกรมโอเพนซอร์ซที่พยายามทำบางสิ่งที่คล้ายกับสิ่งนี้ สงสัยว่าคนอื่น ๆ เข้าหาสิ่งนี้อย่างไร

PS: ฉันรู้ว่า QML เป็นตัวเลือกสำหรับการทำ MVC และมันก็ดูน่าสนใจจนกระทั่งฉันรู้ว่ามีจาวาสคริปต์มากน้อยแค่ไหน - และความจริงแล้วมันยังไม่สมบูรณ์พอสมควรในแง่ของการส่งต่อไปยัง PyQT (หรือแค่ช่วงเวลา) ปัจจัยที่ซับซ้อนของเครื่องมือที่ใช้ในการดีบั๊ก (ยากพอเพียงแค่ PyQT) และความต้องการโปรแกรมเมอร์อื่น ๆ ในการแก้ไขโค้ดนี้ได้อย่างง่ายดายโดยที่ไม่รู้ว่า JS nixed มัน


0

ฉันต้องการสร้างแอปพลิเคชัน ฉันเริ่มเขียนฟังก์ชั่นส่วนบุคคลที่ทำงานเล็ก ๆ (มองหาบางอย่างใน db, คำนวณบางสิ่ง, มองหาผู้ใช้ที่เติมข้อความอัตโนมัติ) แสดงบนเครื่อง จากนั้นใส่วิธีการเหล่านี้ในไฟล์main.py..

จากนั้นฉันต้องการเพิ่ม UI ฉันมองไปรอบ ๆ เครื่องมือต่าง ๆ และตัดสินให้ Qt ผมใช้ผู้สร้างจะสร้าง UI แล้วในการสร้างpyuic4UI.py

ในผมนำเข้าmain.py UIจากนั้นเพิ่มวิธีการที่ถูกทริกเกอร์โดยเหตุการณ์ UI ที่ด้านบนของฟังก์ชั่นหลัก (ตัวอักษรด้านบน: รหัส "คอร์" อยู่ที่ด้านล่างของไฟล์และไม่มีอะไรเกี่ยวข้องกับ UI คุณสามารถใช้มันจากเชลล์ได้ถ้าคุณต้องการ ถึง).

นี่คือตัวอย่างวิธีการdisplay_suppliersที่แสดงรายการผู้จำหน่าย (ฟิลด์: ชื่อบัญชี) บนตาราง (ฉันตัดมันจากส่วนที่เหลือของรหัสเพียงเพื่อแสดงโครงสร้าง)

ในขณะที่ผู้ใช้พิมพ์ในฟิลด์HSGsupplierNameEditข้อความข้อความจะเปลี่ยนและทุกครั้งที่มันทำวิธีนี้เรียกว่าดังนั้นตารางจะเปลี่ยนเป็นประเภทผู้ใช้

ได้รับซัพพลายเออร์จากวิธีการที่เรียกว่าget_suppliers(opchoice)เป็นอิสระจาก UI และใช้งานได้จากคอนโซลเช่นกัน

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

ฉันไม่รู้มากเกี่ยวกับแนวปฏิบัติที่ดีที่สุดและสิ่งต่าง ๆ เช่นนั้น แต่นี่เป็นสิ่งที่สมเหตุสมผลสำหรับฉันและทำให้ฉันกลับไปที่แอพได้ง่ายขึ้นหลังจากที่หายไป หรือ webapp2 ความจริงรหัสที่ทำสิ่งนั้นเป็นอิสระและที่ด้านล่างทำให้ง่ายต่อการคว้ามันแล้วเปลี่ยนวิธีการแสดงผล (องค์ประกอบ HTML กับองค์ประกอบเดสก์ทอป)


0

... ข้อบกพร่องมากมายรูปแบบที่ชัดเจนยิ่งขึ้นคือคู่กับเฟรมเวิร์ก UI (สัญญาณ QObject, pyqt)

ดังนั้นอย่าทำเช่นนี้!

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)

นั่นเป็นการเปลี่ยนแปลงเล็กน้อยที่แยกโมเดลของคุณออกจาก Qt อย่างสมบูรณ์ คุณสามารถย้ายไปไว้ในโมดูลอื่นได้ทันที

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.