Pyqt / QtアプリのロジックからUIを適切に分離する方法は?


20

私は過去にこの主題について多くのことを読み、ボブおじさんのこのような興味深い講演をいくつか見てきました。それでも、デスクトップアプリケーションを適切に設計し、UI側の責任とロジック側の責任を区別することは常に非常に難しいと感じています。

優れた実践の非常に短い要約は、このようなものです。UIから切り離したロジックを設計する必要があります。これにより、どの種類のバックエンド/ UIフレームワークに関係なく(理論的に)ライブラリを使用できるようになります。これが意味することは、基本的にUIは可能な限りダミーであるべきであり、重い処理はロジック側で行われるべきだということです。別の言い方をすれば、文字通り、コンソールアプリケーション、Webアプリケーション、またはデスクトップアプリケーションで素敵なライブラリを使用できます。

また、ボブおじさんは、どのテクノロジーを使用するとさまざまなメリットが得られるか(良いインターフェース)を議論することを提案します。

ですから、この質問は非常に広範な質問であり、インターネット全体で何度も議論されてきました。そこで、何か良いものを得るために、pyqtでMCVを使用しようとする非常に小さなダミーの例を投稿します。

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アプリケーションを適切に設計する方法を理解することです。

質問

適切な一般的なプラクティスに従って、MVCを使用して大きなPyQtアプリケーションをどのように適切に設計しますか?

参考文献

ここでこれと同様の質問をしました

回答:


1

私は(主に)WPF / ASP.NETのバックグラウンドから来ており、MVC風の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
      ...

私のアプリケーションには多くの(LOT)UI要素とウィジェットがあり、多くのプログラマーが簡単に変更する必要があります。「ビュー」コードは、右側のQStackedWidgetによって表示されるアイテムを含むQTreeWidgetを持つQMainWindowで構成されます(マスター詳細ビューを考えてください)。

項目はQTreeWidgetから動的に追加および削除でき、元に戻すとやり直しの機能をサポートしたいので、現在/以前の状態を追跡するモデルを作成することを選択しました。UIコマンドは、コントローラーによってモデルに情報を渡します(ウィジェットの追加または削除、ウィジェット内の情報の更新)。コントローラーがUIに情報を渡すのは、検証、イベント処理、およびファイルの読み込み/取り消しとやり直しのみです。

モデル自体は、UI要素IDの辞書と、最後に保持した値(およびいくつかの追加情報)で構成されます。以前の辞書のリストを保持し、誰かが元に戻す操作を行った場合、以前の辞書に戻すことができます。最終的に、モデルは特定のファイル形式としてディスクにダンプされます。

正直に言うと、これは設計が非常に難しいことがわかりました。PyQTは、モデルから離婚するのに向いているとは感じず、これと非常に似たようなことをしようとするオープンソースプログラムを見つけることができませんでした。他の人がこれにどのように近づいてきたのか興味があります。

PS:QMLはMVCを実行するためのオプションであり、Javascriptがどの程度関与しているかを理解するまで魅力的でした。PyQT(またはピリオド)に移植されるという点ではまだかなり未熟であるという事実に気付きました。優れたデバッグツールがないという複雑な要因(PyQTだけでは十分に難しい)と、JSを知らない他のプログラマーがこのコードを簡単に変更する必要があるためです。


0

アプリケーションを構築したかった。私は小さなタスクを実行する個々の関数の作成を開始しました(dbで何かを探し、何かを計算し、オートコンプリートを持つユーザーを探します)。端末に表示されます。次に、これらのメソッドをファイルに配置しmain.pyます。

次に、UIを追加したかった。さまざまなツールを調べて、Qtに落ち着きました。Creatorを使用してUIを構築し、次にpyuic4を生成しましたUI.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

私はベストプラクティスやそのようなことについてあまり知りませんが、これは私にとって理にかなったことであり、偶然にweb2pyを使用してWebアプリケーションを作りたいと思いますまたは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.