web-dev-qa-db-ja.com

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

私は過去にこの主題についてかなりたくさん読んだことがあり、 ncle Bob's からのこのような興味深い話をいくつか見ました。それでも、デスクトップアプリケーションを適切に構築し、どちらが[〜#〜] ui [〜#〜]側とlogic側のどちらか。

良い実践の非常に短い要約はこのようなものです。 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シグナル)に結合されているモデルがより明白です。この例は本当にダミーであり、1つのQMainWindowを使用して数行でコーディングできますが、私の目的は、より大きなpyqtアプリケーションを適切に構築する方法を理解することです。

[〜#〜]質問[〜#〜]

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

[〜#〜]参照[〜#〜]

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

21
BPL

私は(主に)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
      ...

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

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

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

正直に言うと、これを設計するのはかなり難しいと思いました。 PyQTは、モデルから離婚するのに適しているとは感じていません。また、これとよく似たものを実行しようとするオープンソースプログラムを見つけることができませんでした。他の人々がこれにどのように取り組んだか興味があります。

PS:私はQMLがMVCを実行するためのオプションであることを理解し、JavaScriptがどれほど含まれているかを理解するまでは魅力的であるように見えました-そしてPyQT(または単に期間)への移植に関してはまだ未熟です。優れたデバッグツールがないことの複雑な要素(PyQTだけでは十分難しい)と、JSが知らない他のプログラマがこのコードを簡単に変更する必要性。

1
phyllis diller

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

次に、UIを追加したいと思いました。さまざまなツールを見回して、Qtに落ち着きました。 Creatorを使用してUIを作成し、次に_pyuic4_を使用して_UI.py_を生成しました。

_main.py_では、UIをインポートしました。次に、コア機能の上にUIイベントによってトリガーされるメソッドを追加しました(文字通り上:「コア」コードはファイルの下部にあり、UIとは関係ありません。必要に応じてシェルから使用できます。に)。

以下は、テーブルにサプライヤー(フィールド:名前、アカウント)のリストを表示するメソッド_display_suppliers_の例です。 (構造を説明するために、残りのコードからこれを切り取りました)。

ユーザーがテキストフィールドHSGsupplierNameEditに入力すると、テキストが変更され、そのたびにこのメソッドが呼び出されるため、ユーザーの入力に応じてテーブルが変更されます。

UIから独立し、コンソールからも機能するget_suppliers(opchoice)というメソッドからサプライヤーを取得します。

_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
Jugurtha Hadjar

...多くの欠陥、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から完全に切り離しました。これを別のモジュールに移動することもできます。

0
Useless