Qt:QML ListViewの汎用モデルの作成

一部の人にとっては、この記事の内容はあまりにも単純に見えるかもしれませんが、役に立たないかもしれませんが、初めてListView



モデルを作成する必要に直面するQtおよびQMLの初心者は、少なくとも代替*、高速かつ非常に効果的なソリューションとして役立つでしょう価格/品質の面で。







*少なくとも、私はこのようなものをGoogleで管理することはできませんでした。 知っていて追加できる場合-ようこそ。







ノイズとは何ですか?



QtでのQMLの登場により、ユーザーインターフェイスの作成は、C ++コードとの密接な対話が必要になるまで、より簡単かつ迅速になりました。 エクスポートされたC ++クラスの作成については、 ドキュメントで詳しく説明されており、単純な構造で作業している限り、すべては非常に簡単です(まあ、ほとんど)。 主な迷惑は、QMLでコンテナの要素を「表示」する必要があるときに、簡単な方法で-コレクション、特にこれらの要素がネストされたオブジェクトや他のコレクションの複雑な構造を持つ場合に発生します。







面白い?







Qtの用語に精通していると想定されており、 デリゲートロール 、リストおよびリストコンポーネントに適用されるコンテナなどの言葉は、私がかつて行ったように驚くことではありません...







QMLでリストデータを表示するために最も使用されるコンポーネントはListView



です。 Qtのドキュメントに続いて、データを渡す方法はいくつかありますが、C ++実装に適したオプションは1つしかありませんQAbstractItemModel



からの継承による独自のモデルの作成。 ハブラーでそれを作る方法はすでに記事がありました、例えばこれ 。 そして、すべてがうまくいきますが、最初にいくつかの事実を概説しましょう:









いくつかのそのようなモデルを実装すると、それらの定型文の量がスケール外であることをすぐに認識します。 ロール名を転送するだけの価値があります。 とにかく、なぜ、これらのすべてが既にQ_OBJECT



Q_GADGET



か? これをすべて一般化できるテンプレートコンテナを持ちたいとすぐに思い浮かびます:シートインターフェイスを持ち、同時にListView



モデルとして何らかの方法で機能できるようになります、たとえばListModel<ModelItem> itemsCollection



...







モデルの目的は何ですか?



シートは、デリゲート(個々の要素のレンダラー)を一度に作成するのではなく、現在表示されるはずのデリゲートとオプションのキャッシュのみを作成します。 シートをスクロールすると、可視性の境界を超えたデリゲートは破棄され、新しいデリゲートが作成されます。 リストに新しいアイテムを追加しましょう。 この場合、どのインデックスが追加されたかをListView



に通知する必要があります。このインデックスが現在表示されているインデックスの間にある場合は、新しいデリゲートを作成し、データで初期化して、既存のデリゲートの間に配置する必要があります。 状況を削除するときは逆です。 要素のプロパティを変更すると、ここに「ロール」を変更する信号が追加されます-デリゲートで直接表示されるデータ(正直なところ、誰がこの名前を思いついたのかわかりません)。







「純粋な」C ++構造を使用する場合、選択肢はありません。そのようなデータを何らかの方法でエクスポートする唯一の方法は、QAbstractItemModelから独自の子孫モデルを使用することです。 また、Q_OBJECTまたはQ_GADGET要素がある場合、QMLでプロパティを「表示」する方法とロールの追加複製を既に知っているだけでなく、そのようなオブジェクトを変更するときにモデルを「ジャグリング」することは非常に不便で不適切になります。 ロールを介して構造も転送する必要がある場合、タスクはさらに複雑になります。 この場合、構造はQVariant



でホストされて転送されます。







QMLで構造要素を渡す



まず、見てみましょうが、複雑な構造を持つコンテナ要素をデリゲートに転送するにはどうすればよいですか?







オブジェクトのこの構造を持つ要素のリストがあるとします:







 class Person + string name + string address + string phoneNumber class Employee + Person* person + string position + double salary
      
      





もちろん、この場合、そのような構造を表示するには、痛みを伴わずにフラットにすることができますが、データが複雑であり、それができないと想像してみましょう。







したがって、 QAbstractListModel



から相続人を作成します(これはQAbstractItemModel



からの相続人QAbstractItemModel



)。 ストレージには、人気のあるQList



ます。 ただし、役割を割り当てないでください! 代わりに、次のように進めます。









 qmlRegisterUncreatableType<Person>( "Personal", 1, 0, "Person", "interface" ); qmlRegisterUncreatableType<Employee>( "Personal", 1, 0, "Employee", "interface" );
      
      





まだ忘れないで







 Q_DECLARE_METATYPE( Person* ) Q_DECLARE_METATYPE( Employee* )
      
      





この場合、クラスはQObject



であると想定しています。 このアプローチの有効性については長い間議論することができますが、実際のタスクでは、QObjectを節約することはしばしばマッチを節約することになり、人件費と比較することはできません。 そして、Electronでアプリケーションを作成する一般的な傾向を見ると...

なぜ創造できないのか-それは簡単だからです。 これらのオブジェクトをQMLで作成することはありません。つまり、たとえばデフォルトのコンストラクターは必要ありません。 私たちにとって、それは単なる「インターフェース」です。









合計で、次のようになります。







 class Personal : public QAbstractListModel { public: //       Q_INVOKABLE Employee* getEmployee( int index ); //   QAbstractListModel: int rowCount( const QModelIndex& parent ) const override { return personal.count(); } //    , ..    . QVariant data( const QModelIndex& index, int role ) const override { return QVariant(); } //  -          //     QList,      // beginInsertRows(), endInsertRows()   . //   ,   ,  . private: QList<Employee*> personal; }
      
      





これで、このようなモデルを使用して、ビュー内で型付きオブジェクトを置換し、インスタンス化時にデリゲートを使用できます! さらに、Qt Creatorは、この構造体のフィールドにプロンプ​​トを表示するのに非常に優れており、これは喜ばせざるを得ません。







 // PS        QMLEngine Personal { id: personalModel } ListView { model: personalModel delegate: Item { // index -   .   ,      property Employee employee: personalModel.getEmployee(index) Text { text: employee.person.name } } }
      
      





ステップ1:インデックスモデル



では、得られたものを分析しましょう。 そして、 QAbstractListModel



はインデックスのみQAbstractListModel



使用し、残りの作業はQ_OBJECT



とそのメタプロパティによって行われることが判明しました。 つまり 一般に、インデックスでのみ動作するモデルを実装できます。これは、 ListView



何が起こっているかを知らせるのに十分です! 次のインターフェースを取得します。







 class IndicesListModelImpl : public QAbstractListModel { Q_OBJECT Q_PROPERTY( int count READ count NOTIFY countChanged ) public: int count() const; // --- QAbstractListModel --- int rowCount( const QModelIndex& parent ) const override; QVariant data( const QModelIndex& index, int role ) const override; protected: // Create "count" indices and push them to end void push( int count = 1 ); // Remove "count" indices from the end. void pop( int count = 1 ); // Remove indices at particular place. void removeAt( int index, int count = 1 ); // Insert indices at particular place. void insertAt( int index, int count = 1 ); // Reset model with new indices count void reset( int length = 0 ); Q_SIGNALS: void countChanged( const int& count ); private: int m_count = 0; };
      
      





実装では、次のように、特定のインデックスが変更されたように見えることをビューに通知します。







 void IndicesListModelImpl::insertAt( int index, int count ) { if ( index < 0 || index > m_length + 1 || count < 1 ) return; int start = index; int end = index + count - 1; beginInsertRows( QModelIndex(), start, end ); m_count += count; endInsertRows(); emit countChanged( m_count ); }
      
      





さて、悪くはありませんが、今ではQAbstractListModel



から直接ではQAbstractListModel



、必要なロジックの半分が既にある即興クラスから継承できます。 しかし、どうしたら...コンテナを一般化できますか?







ステップ2:コンテナーを追加する



コンテナのテンプレートクラスを作成するのは恥ずかしいことではありません。 混乱して、テンプレートの2つのパラメーターを作成できます:コンテナーと格納された型、したがって、何でも使用を許可しますが、私は最も頻繁に使用されるもので停止しません、私の場合はQList<QSharedPointer<ItemType>>



です。 QList



はQtで最もよく使用されるコンテナであり、 QSharedPointer



は所有権について心配する必要がありません。 (PSあなたがまだ心配する必要がある何か、しかしそれについては後で)







さあ、行きましょう。 理想的には、モデルにQList



と同じインターフェイスをQList



、可能な限りそれを模倣するようにしたいと思いますが、実際にはそれほど必要ないため、すべてを転送するには効率が悪くなります。変更に使用されるメソッドのみが追加、挿入です、removeAt。 残りについては、内部シートへのパブリックアクセサーを「そのまま」作成するだけです。







 template <class ItemType> class ListModelImplTemplate : public IndicesListModelImpl { public: void append( const QSharedPointer<ItemType>& item ) { storage.append( item ); IndicesListModelImpl::push(); } void append( const QList<QSharedPointer<ItemType>>& list ) { storage.append( list ); IndicesListModelImpl::push( list.count() ); } void removeAt( int i ) { if ( i > length() ) return; storage.removeAt( i ); IndicesListModelImpl::removeAt( i ); } void insert( int i, const QSharedPointer<ItemType>& item ) { storage.insert( i, item ); IndicesListModelImpl::insertAt( i ); } // --- QList-style comfort ;) --- ListModelImplTemplate& operator+=( const QSharedPointer<ItemType>& t ) { append( t ); return *this; } ListModelImplTemplate& operator<<( const QSharedPointer<ItemType>& t ) { append( t ); return *this; } ListModelImplTemplate& operator+=( const QList<QSharedPointer<ItemType>>& list ) { append( list ); return *this; } ListModelImplTemplate& operator<<( const QList<QSharedPointer<ItemType>>& list ) { append( list ); return *this; } // Internal QList storage accessor. It is restricted to change it directly, // since we need to proxy all this calls, but it is possible to use it's // iterators and other useful public interfaces. const QList<QSharedPointer<ItemType>>& list() const { return storage; } int count() const { return storage.count(); } protected: QList<QSharedPointer<ItemType>> storage; };
      
      





ステップ3:getItem()メソッドとモデルの一般化



このクラスからもう1つのテンプレートを作成し、それを任意のコレクションの型として使用し、次のように終了を処理することは残っているように思われます。







 class Personal : public QObject { public: ListModel<Employee>* personal; }
      
      





しかし、問題があり、3番目のステップはここで無駄ではありません:Q_OBJECTマクロを使用するQObjectクラスはテンプレートクラスにすることはできません。そのようなMOCクラスを初めてコンパイルしようとすると、それについて喜んでお話しします。 来た?







決してエレガントではありませんが、決してこの問題に対する解決策はありません。古き良きマクロ#define! 必要に応じて、クラスボイラーを動的に生成します(毎回定型文を書くよりもあらゆる点で優れています)。 幸いなことに、1つのメソッドを実装するだけで済みます。







 #define DECLARE_LIST_MODEL( NAME, ITEM_TYPE ) class NAME : ListModelImplTemplate<ITEM_TYPE> { Q_OBJECT protected: Q_INVOKABLE ITEM_TYPE* item( int i, bool keepOwnership = true ) const { if ( i >= 0 && i < storage.length() && storage.length() > 0 ) { auto obj = storage[i].data(); if ( keepOwnership ) QQmlEngine::setObjectOwnership( obj, QQmlEngine::CppOwnership ); return obj; } else { return Q_NULLPTR; } } }; Q_DECLARE_METATYPE( NAME* )
      
      





QQmlEngine::setObjectOwnership( obj, QQmlEngine::CppOwnership );



についても話し合う必要がありQQmlEngine::setObjectOwnership( obj, QQmlEngine::CppOwnership );



-このことは、QMLEngineが施設の管理を引き受けることを確実にしないために必要です。 何らかのJS関数でオブジェクトを使用し、ローカルスコープの変数に配置する場合、JSエンジンはこの関数を終了する際にためらうことなくクラッシュします。 QObject



は親がありQObject



ん。 一方、親を意図的に使用することはありません。 QSharedPointer



を使用してオブジェクトのライフタイムを既に制御しているため、別のメカニズムは必要ありません。







合計すると、次の図が表示されます。







  1. QAbstractListModel



    の基本実装QAbstractListModel



    インデックスを操作してListView



    応答させるため
  2. 標準コンテナ上の正直なテンプレートラッパークラス。タスクは、このコンテナが編集され、親IndicesListModelImpl



    ことを確認することです。
  3. 生成されたクラスは、このすべての「良さ」の子孫であり、QMLから要素にアクセスするための単一のメソッドを提供します。




おわりに



結果のソリューションの使用は非常に簡単です。オブジェクトのコレクションをQMLにエクスポートする必要がある場合、目的のモデルをすぐに作成し、そこで使用します。 たとえば、プロバイダークラス(およびQt用語ではバックエンド)があり、そのプロパティの1つがDataItem



リストを提供する必要があります。







 //    DECLARE_LIST_MODEL( ListModel_DataItem, DataItem ) class Provider : public QObject { Q_OBJECT Q_PROPERTY( ListModel_DataItem* itemsModel READ itemsModel NOTIFY changed ) public: explicit Provider( QObject* parent = Q_NULLPTR ); ListModel_DataItem* itemsModel() { return &m_itemsModel; }; Q_INVOKABLE void addItem() { m_itemsModel << QSharedPointer<DataItem>( new DataItem ); } Q_SIGNALS: void changed(); private: ListModel_DataItem m_itemsModel; };
      
      





そしてもちろん、これらすべてを一緒に使用すると、テンプレートと使用例の完全なコードを使用して、githubを理解することができます。







追加、コメント、プルリクエストは大歓迎です。








All Articles