Làm cách nào để tách giao diện người dùng khỏi logic trên ứng dụng Pyqt / Qt đúng cách?


20

Tôi đã đọc khá nhiều về chủ đề này trong quá khứ và xem một số cuộc nói chuyện thú vị như thế này từ Bác Bob . Tuy nhiên, tôi luôn cảm thấy khá khó khăn để kiến ​​trúc sư các ứng dụng máy tính để bàn của mình một cách chính xác và phân biệt đâu là trách nhiệm ở phía UI và cái nào thuộc về phía logic .

Tóm tắt rất ngắn gọn về thực hành tốt là một cái gì đó như thế này. Bạn nên thiết kế logic tách rời khỏi UI, theo cách đó bạn có thể sử dụng (về mặt lý thuyết) thư viện của mình bất kể loại khung phụ trợ / giao diện người dùng nào. Điều này có nghĩa là về cơ bản, UI nên càng giả càng tốt và việc xử lý nặng nên được thực hiện ở phía logic. Nói cách khác, tôi có thể sử dụng thư viện đẹp của mình với ứng dụng bảng điều khiển, ứng dụng web hoặc máy tính để bàn.

Ngoài ra, chú Bob gợi ý các cuộc thảo luận khác nhau về việc sử dụng công nghệ nào sẽ mang lại cho bạn rất nhiều lợi ích (giao diện tốt), khái niệm trì hoãn này cho phép bạn có các thực thể được thử nghiệm kỹ lưỡng, nghe có vẻ hay nhưng vẫn khó hiểu.

Vì vậy, tôi biết câu hỏi này là một câu hỏi khá rộng đã được thảo luận nhiều lần trên toàn bộ internet và cả tấn sách hay. Vì vậy, để có được thứ gì đó tốt từ nó, tôi sẽ đăng một ví dụ rất nhỏ đang cố gắng sử dụng MCV trên 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_())

Đoạn mã trên chứa rất nhiều sai sót, rõ ràng hơn là mô hình được ghép nối với khung UI (tín hiệu QObject, pyqt). Tôi biết ví dụ này thực sự giả và bạn có thể viết mã trên một vài dòng bằng cách sử dụng một QMainWindow duy nhất nhưng mục đích của tôi là hiểu cách kiến ​​trúc sư một ứng dụng pyqt lớn hơn.

CÂU HỎI

Làm thế nào bạn có thể kiến ​​trúc sư đúng một ứng dụng PyQt lớn bằng cách sử dụng MVC theo các thực tiễn chung tốt?

TÀI LIỆU THAM KHẢO

Tôi đã đặt một câu hỏi tương tự như thế này ở đây

Câu trả lời:


1

Tôi đến từ nền tảng WPF / ASP.NET (chủ yếu) và đang cố gắng tạo một ứng dụng PyQT MVC-ish ngay bây giờ và chính câu hỏi này đang ám ảnh tôi. Tôi sẽ chia sẻ những gì tôi đang làm và tôi tò mò muốn nhận được bất kỳ nhận xét hoặc phê bình mang tính xây dựng nào.

Đây là một sơ đồ ASCII nhỏ:

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

Ứng dụng của tôi có rất nhiều (rất nhiều) các yếu tố và widget UI cần được sửa đổi dễ dàng bởi một số lập trình viên. Mã "xem" bao gồm một QMainWindow với một QTreeWidget chứa các mục được hiển thị bởi QStackedWidget ở bên phải (nghĩ chế độ xem Chi tiết tổng thể).

Vì các mục có thể được thêm và xóa một cách linh hoạt khỏi QTreeWidget và tôi muốn hỗ trợ chức năng hoàn tác lại, tôi đã chọn tạo một mô hình theo dõi các trạng thái hiện tại / trước đó. Các lệnh UI chuyển thông tin đến mô hình (thêm hoặc xóa một widget, cập nhật thông tin trong một widget) bởi bộ điều khiển. Lần duy nhất bộ điều khiển chuyển thông tin đến UI là khi xác thực, xử lý sự kiện và tải tệp / hoàn tác & làm lại.

Bản thân mô hình bao gồm một từ điển của ID phần tử UI với giá trị được giữ lần cuối (và một vài thông tin bổ sung). Tôi giữ một danh sách các từ điển trước đó và có thể trở lại từ điển trước nếu ai đó hoàn tác. Cuối cùng, mô hình được kết xuất vào đĩa dưới dạng định dạng tệp nhất định.

Tôi sẽ thành thật - Tôi thấy điều này khá khó để thiết kế. PyQT không cảm thấy như mình cho vay tốt khi ly dị với người mẫu và tôi thực sự không thể tìm thấy bất kỳ chương trình nguồn mở nào đang cố gắng làm điều gì đó tương tự như thế này. Tò mò làm thế nào những người khác đã tiếp cận điều này.

PS: Tôi nhận ra QML là một tùy chọn để thực hiện MVC và nó có vẻ hấp dẫn cho đến khi tôi nhận ra có bao nhiêu Javascript - và thực tế nó vẫn còn khá non nớt khi được chuyển sang PyQT (hoặc chỉ là giai đoạn). Các yếu tố phức tạp của việc không có các công cụ sửa lỗi tuyệt vời (đủ cứng chỉ với PyQT) và sự cần thiết của các lập trình viên khác để sửa đổi mã này một cách dễ dàng, những người không biết JS đã trộn nó.


0

Tôi muốn xây dựng một ứng dụng. Tôi bắt đầu viết các hàm riêng lẻ thực hiện các tác vụ nhỏ (tìm kiếm một cái gì đó trong db, tính toán một cái gì đó, tìm kiếm một người dùng với tự động hoàn thành). Hiển thị trên thiết bị đầu cuối. Sau đó đặt các phương thức này vào một tệp, main.py..

Sau đó, tôi muốn thêm một giao diện người dùng. Tôi đã xem xét các công cụ khác nhau và giải quyết cho Qt. Tôi đã sử dụng Creator để xây dựng giao diện người dùng, sau đó pyuic4để tạo UI.py.

Trong main.py, tôi nhập UI. Sau đó, thêm các phương thức được kích hoạt bởi các sự kiện UI ở trên cùng của chức năng cốt lõi (nghĩa đen là trên cùng: mã "lõi" nằm ở cuối tệp và không liên quan gì đến UI, bạn có thể sử dụng nó từ trình bao nếu bạn muốn đến).

Dưới đây là ví dụ về phương pháp display_suppliershiển thị danh sách các nhà cung cấp (các trường: tên, tài khoản) trên Bảng. (Tôi cắt phần này từ phần còn lại của mã chỉ để minh họa cấu trúc).

Khi người dùng nhập vào trường văn bản HSGsupplierNameEdit, văn bản sẽ thay đổi và mỗi lần thực hiện, phương thức này được gọi để Bảng thay đổi khi người dùng nhập.

Nó nhận được các nhà cung cấp từ một phương thức được gọi get_suppliers(opchoice)là độc lập với UI và cũng hoạt động từ bảng điều khiển.

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

Tôi không biết nhiều về các thực tiễn tốt nhất và những thứ tương tự, nhưng đây là điều có ý nghĩa với tôi và tình cờ giúp tôi dễ dàng quay lại ứng dụng sau khi gián đoạn và muốn tạo một ứng dụng web từ ứng dụng web2py hoặc webapp2. Thực tế mã thực sự làm công cụ này là độc lập và ở phía dưới giúp bạn dễ dàng lấy nó, và sau đó chỉ cần thay đổi cách hiển thị kết quả (phần tử html so với phần tử máy tính để bàn).


0

... rất nhiều sai sót, rõ ràng hơn là mô hình được ghép nối với khung UI (tín hiệu QObject, pyqt).

Vì vậy, đừng làm điều này!

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)

Đó là một thay đổi nhỏ, đã tách hoàn toàn mô hình của bạn khỏi Qt. Bạn thậm chí có thể di chuyển nó vào một mô-đun khác bây giờ.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.