Qt / QML RESTクライアント

今日、テープの記事を見て自分のプロジェクトについて、自分のプロジェクトについて2、3行書きたいと思いました。



一般的に、私はしばらくの間、iOSの技術顧問として働いていました。Androidプログラマーは、Django / Yii2 /独自のコードで多くのAPIを使用していました。 そして、REST APIを操作するためのツールの側面から見て、Qtに似たものを追加することにしました。 Qtモデルを使用してRESTを操作するための通常のツールはありませんでした。



すぐに言ってやった。 下の図で、結果のスキームとその下は、実際にはアイデア、アーキテクチャ、および使用のための簡単な指示の説明です。



画像



したがって、ここで説明します。



  1. アイデアと機能
  2. 建築
  3. 使用例
  4. ソースコードとサンプルアプリケーション


アイデアと機能



基本的に、通常設計されたREST APIはすべて、HTTPリクエストを受信し、リスト/単一データオブジェクトをJSON / XMLでクライアントに送信します。



一方、Qtには、データの読み取りと変更の両方の問題を解決できるモデルのメカニズムが長い間あり、モデルの基本クラスの対応する関数を再定義するだけで済みます。



これらのメッセージに基づいて、ライブラリは次の要件を満たす必要があると判断しました。





要件の開発では、 Yii2-RESTDjango RESTフレームワークなどのサーバー側API作成ツールに焦点を当てました。 私の意見では、これらは完全に異なる世界から来ているだけでなく、RESTサービスを作成するための最も機能的な無料のソリューションであり、ドキュメントを分析し、テストプロジェクトを書くときに、サーバーでRESTを整理するためのさまざまなアプローチに関するデータを受け取りました。



建築



したがって、上記のように、すべてがQAbstractListModelを中心に展開します。これは、Qt C ++ / QMLからデータにアクセスするネイティブな方法だからです。 詳細に移りましょう。



上記の図によると、2つの基本クラスがあります:APIBase(QObjectの子孫)とBaseRestListModel(QAbstractListModelの子孫)。





APIBaseクラスのプロパティ:





このプロパティのセットは、原則として、どのサービスでも機能するのに十分です。 各プロパティ自体は、派生クラスおよびQMLで使用できます(これらはすべてQ_PROPERTYであるため)。



APIを操作するための独自のクラスを作成するには、APIBaseを継承し、少なくともhandleRequestメソッドを実装する必要があります。また、上記のプロパティ、モデルのパラメーター(以下)、およびprotectedメソッドを使用してサーバーからデータを受信するために必要なすべてのメソッドを取得、投稿、配置する必要があります、deleteResource、head、options、patch(すべてHTTPプロトコルの同じメソッドに対応)。



それだけです。データを受信するメソッド内には、モデルから転送されたパラメーターを解析するためのコードが必要です(アプリケーションから読み取ります)。それは技術の問題であり、QUrl / QUrlQueryを使用してサーバーに正しいリクエストを送信するだけです。 結果は、QNetworkReplyへのポインターであり、QNetworkReplyは基本クラスに戻り、すでに完了信号へのサブスクリプションがあります。



クラスAPIとモデルを使用するためのさまざまなシナリオを考えてみましょう。



1. handleRequestと既製の読み取り専用モデル



このシナリオは、独自のデータモデルが不要で、準備が必要な場合に使用します。 ライブラリには、JsonRestListModelとXmlRestListModelという2つのモデルがあります。



これらのモデルは両方とも読み取り専用であり、C ++ / QMLからすぐに使用できます。



ReadOnlyモデルを使用するには、APIクラスでhandleRequestメソッドを実装する必要があります。そのインターフェイスは次のとおりです。



virtual QNetworkReply *handleRequest(QString path, QStringList sort, Pagination *pagination, QVariantMap filters = QVariantMap(), QStringList fields = QStringList(), QString id = 0)
      
      





pathはAPIメソッド、sortは並べ替えパラメーター、paginationはページングパラメーターオブジェクト、filtersはフィルターパラメーター、fieldsは返されるフィールドのリスト、idはレコードの一意の識別子です。



各ReadOnlyモデルは、次の形式でAPIへのアクセスを実装します。



 QNetworkReply *JsonRestListModel::fetchMoreImpl(const QModelIndex &parent) { Q_UNUSED(parent) return apiInstance()->handleRequest(requests()->get(), sort(), pagination(), filters(), fields()); }
      
      





少し先を見て、このようなモデルをQMLで使用する方法を紹介します。



 ... MyApi { id: myApi } JsonRestListModel { id: jsonSampleModel api: myApi //  API  idField: 'id' // -    //    API  handleRequest // ReadOnly    readOnly  get ( )  getDetails (   ) //  -  .  Requests requests { get: "/v1/coupon" getDetails: "/v1/coupon/{id}" } //    filters: {'isArchive': '0'} //    fields: ['id','title'] //  sort: ['-id'] //   pagination { policy: Pagination.PageNumber // -    perPage: 20 //-     } //         Component.onCompleted: { reload(); } } ...
      
      





既製のモデルを使用すると、APIクラスのみを実装でき、モデルの継承について心配する必要はありません。 もちろん、次のシナリオのように、ここではAPIクラスに完全な機能が必要です。



2.独自のモデルを書く



ReadOnlyモデルが私たちに合わない場合、AbstractJsonRestListModelとAbstractXmlRestListModelを継承し、データを操作するために必要なすべてのメソッドを使用して独自のモデルを作成できます。 このタイプのモデルについては、使用例で詳しく説明します。



データ操作のために、2つのオプションを使用できます。



ここでは、制限を導入しないことにしましたが、2番目のオプションはより正確です。 理由を説明します。 各モデル要素はRestItemクラスのインスタンスであり、モデル自体には次のようなヘルパーメソッドがあります。



したがって、モデルには、APIクラスには存在しないモデル要素を使用した直接作業のメカニズムがあり、サーバーでの作業に最低限必要な機能はAPIクラスに残り、すべてのデータの準備/処理はモデルで行われます。



RestItemクラス自体は、QMLで利用可能なキー/値を表すために、標準のQtメソッドであるdata()およびroleNames()モデルメソッドに必要です。



3. APIクラスを直接使用する



最後に、APiクラスで作業のすべてのロジックを記述したため、モデルなしで実行できます。このため、replyFinishedメソッドを再定義し、APIを介して直接リクエストを行うだけで十分です。



うーん...私はもちろんシナリオの説明に夢中になりました...私たちはさらにモデルに行きます。 先ほど言ったように、すべてのモデルの基本クラスはBaseRestListModelです。 実際、この基本クラスはほとんどすべての作業を行います。



したがって、クラスプロパティのリスト:





ここでも、すべてのプロパティはQ_PROPERTYです。



プロパティに加えて、このクラスから継承された各モデルには次のメソッドがあります。





AbstractJsonRestListModelまたはAbstractXmlRestListModelから継承した作業モデルを作成するには、いくつかのメソッドも実装する必要があります。





もちろん、このモデルにはあらゆるものが含まれていますが、ソースでこれらすべてを確認できます。個々のニーズに合わせてモデルを理解し、書き直すのに十分なコメントがあります。 すべての内線 保護されたセクションのメソッド。



あなたが言ったように、2つの特定のクラスがモデルに関連付けられています-PaginationとDetailsModel。

DetailsModelを使用すると、原則としてすべてがシンプルになります。 アプリケーションでリスト項目をクリックすると、データが要求され、このモデルでデータが書き込まれ、アプリケーションへのポインターが与えられます。 アプリケーションでは、真実を少し歪め、1つの要素で非対話型のListViewを作成する必要があります。これには、デリゲートと詳細モデルへのポインターが必要です。これにより、「詳細情報のページ」が取得されます。



ページネーションも問題になりません。 このクラスは、ページネーションパラメーターのみを定義し、モデルの現在の状態を保存します。 すべてはプロパティのセットによっても設定されます:





それだけです。モデル自体は、ListViewとともに新しいデータのロードを台無しにし、最大値に達するとロードを停止します。 カウント。



ああ、ReadOnlyモデルとQMLで使用されるRequestsクラスがまだあります。 ソースを見てください、すべてがそこで簡単です)



それは私が得たアーキテクチャについてです。 もちろん、ライブラリを作成し、すぐに実行に移そうとしたので、コンパイルして表示できるデモアプリケーションを作成しました。 以下にその例を示します。ライブラリを使用する手順を分析します。



使用例



Yii2には1つの小さなプロジェクトがあります。それは...にありますが、どのプロジェクトかはわかりません。



そのため、このプロジェクトでは、実際にデモを開発するときに使用したいくつかのAPIメソッドを実装しました。



以下は、使用されるAPIメソッドとそれらが返すデータです。



/ v1 /カテゴリ
 [{ "id": 1, "sourceServiceId": 2, "categoryName": "", "categoryCode": "aktsii", "categoryIdentifier": "0", "parentCategoryIdentifier": "0", "categoryAdditionalInfo": "0", "isActive": 1 }, { "id": 2, "sourceServiceId": 2, "categoryName": "", "categoryCode": "kupony", "categoryIdentifier": "28", "parentCategoryIdentifier": "28", "categoryAdditionalInfo": "https://blizzard.kz/kuponator/categ/28", "isActive": 1 } , ...]
      
      







/ v1 /クーポン
 [{ "id": 1, "sourceServiceId": 1, "cityId": 1, "createTimestamp": "2015-03-12 14:01:57", "lastUpdateDateTime": "2016-10-20 03:54:47", "recordHash": "e7b01c1a69bc66e1f1a62d8fcb0825de", "title": "Home Club", "shortDescription": "    , , ,    ", "longDescription": "   –       .   ,    –   -  Home Club.           50%!      , ,    .   ! ", "conditions": " <p class="e-condition__text">:</p> <ul class="b-conditions-list"> <li class="e-condition">      -  Home Club.</li> <li class="e-condition"> <strong></strong>:  50%   - 1 500 .  3 000 .</li> <li class="e-condition">         ( ).</li> <li class="e-condition">      .</li> <li class="e-condition">    10 .</li> <li class="e-condition"> <strong>VIP-     .   VIP-     .</strong> </li> <li class="e-condition">        .</li> <li class="e-condition"> <strong>    :</strong><br> +7 (727) 308-23-63,<br> +7 (747) 841-42-51,<br> +7 (701) 985-90-72.</li> <li class="e-condition"> <strong>   .</strong> </li> <li class="e-condition">      2 ,     ( ).</li> <li class="e-condition">        ,       «».</li> <li class="e-condition">      Home Club  :  ,    ,  , Home Club.</li> <li class="e-condition">         ,   ,    .</li> <li class="e-condition"> <strong>     .</strong> </li> <li class="e-condition">    12  2015 . ().</li> <li class="e-condition"> <span hashstring="deal_refunds_policy" hashtype="content">&nbsp</span> </li> <li class="e-condition"> <span hashstring="deal_standard_conditions" hashtype="content">&nbsp</span> </li> </ul> <p class="e-offer__features"></p> <ul class="b-offer__features-list"> <li class="e-offer__feature ">  ,    ,  , Home Club </li> <li class="e-offer__feature "> :<br> +7 (727) 308-23-63<br>+7 (747) 841-42-51<br>+7 (701) 985-90-72<br> </li> <li class="e-offer__feature "> :<br> : </li> </ul>", "features": " <p class="e-offer__features">:</p> <ul class="b-offer__features-list"> <li class="e-offer__feature">Home Club            ,    ,     .</li> <li class="e-offer__feature"> ​   10  (5    5 Vip-).  Vip-      , , -, ,  .</li> <li class="e-offer__feature"> ​ Home Club    -  3- : <ul> <li> ;</li> <li>;</li> <li> .</li> </ul> </li> <li class="e-offer__feature"> ​  : <ul> <li> 4-  7-  ;</li> <li>1 :  , -, , ;</li> <li>2 : 2-  ,  ,    , ,   ;</li> <li>3 : 2  ,   .</li> </ul> </li> <li class="e-offer__feature"> ​VIP-: <ul> <li> 4-  11-  ;</li> <li>1 : ,    10 ,  , ,   , ,  ;</li> <li>2 : -12 ,  2-  4-   (    ), 2 ..</li> </ul> </li> <li class="e-offer__feature"> : <a data-seohide-href="/deal/away/20056/" class="e-offer__feature--link seohide-link" target="_blank" rel="nofollow" title="http://www.home-club.kz/">www.home-club.kz/</a> </li> </ul>", "imagesLinks": [ "https://static.chocolife.me/static/upload/images/deal/for_deal_page/21000/20056/660x305/1_20150312023051426147565.7364.jpg", "https://static.chocolife.me/static/upload/images/deal/for_deal_page/21000/20056/660x305/2_20150312023051426147565.9348.jpg", "https://static.chocolife.me/static/upload/images/deal/for_deal_page/21000/20056/660x305/4_20150312093171426174997.7985.jpg", "https://static.chocolife.me/static/upload/images/deal/for_deal_page/21000/20056/660x305/5_20150312093171426174997.944.jpg", "https://www.chocolife.me/" ], "timeToCompletion": null, "mainImageLink": "https://www.chocolife.me/", "originalCouponPrice": "30 000", "originalPrice": "30 000", "discountPercent": "-51%", "discountPrice": "18 000", "discountType": "full", "boughtCount": "1", "sourceServiceCategories": "1 , 82 , 8 , 2", "pageLink": "https://www.chocolife.me//20056-arenda-kottedzha-s-dvumya-spalnyami-gorki-sauna-darts-i-mnogoe-drugoe-v-prirodno-razvlekatelnom-parke-home-club-skidka-do-50", "isArchive": 1, "tryToUpdateCount": 0, "viewCount": "0", "serviceName": "Chocolife.me", "cityName": "" }, { "id": 2, "sourceServiceId": 1, "cityId": 1, "createTimestamp": "2015-03-12 14:01:57", "lastUpdateDateTime": "2016-11-01 12:39:53", "recordHash": "dce10232f1acb53b1ee7a8bf3902e0c0", "title": "    AquaBike Centre", "shortDescription": "       ", "longDescription": null, "conditions": null, "features": " <p class="e-offer__features">:</p> <ul class="b-offer__features-list"> <li class="e-offer__feature">        .     .</li> <li class="e-offer__feature"> Aquabike – : <ul> <li> ;</li> <li> ;</li> <li>  ;</li> <li>  ;</li> <li>  ;</li> <li>    ;</li> <li> ;</li> <li> .</li> </ul> </li> <li class="e-offer__feature"> <strong> :</strong> <ul> <li>    ;</li> <li>         ;</li> <li> ,  ;</li> <li>  ;</li> <li>   ,    ;</li> <li>  ,    ;</li> <li>   ;</li> <li>   , ,  .</li> </ul> </li> <li class="e-offer__feature"> <strong>     1    .</strong> </li> <li class="e-offer__feature">  AquaBike Centre  : <ul> <li>  ,   2 ;</li> <li>   ;</li> <li>,  45 .</li> </ul> </li> </ul>", "imagesLinks": [ "https://static.chocolife.me/static/upload/images/deal/for_deal_page/21000/20016/660x305/1_20150314013241426318344.7033.jpg", "https://static.chocolife.me/static/upload/images/deal/for_deal_page/21000/20016/660x305/2_20150314013241426318344.8157.JPG", "https://static.chocolife.me/static/upload/images/deal/for_deal_page/21000/20016/660x305/4_20150311053411426073981.6524.JPG", "https://www.chocolife.me/" ], "timeToCompletion": null, "mainImageLink": "https://www.chocolife.me/", "originalCouponPrice": "3 000", "originalPrice": "3 000", "discountPercent": "-50%", "discountPrice": "1 500", "discountType": "full", "boughtCount": "58", "sourceServiceCategories": "1 , 68 , 36 , 2", "pageLink": "https://www.chocolife.me//20016-novinka-iz-francii-vse-dlya-vashey-krasoty-zdorovya-i-relaksacii-trenirovki-po-akvabaykingu-a-takzhe-pressoterapiya-so-skidkoy-50-v-aquabike-centre", "isArchive": 1, "tryToUpdateCount": 0, "viewCount": "0", "serviceName": "Chocolife.me", "cityName": "" }, ...]
      
      







/ v1 /クーポン/ {id}
 { "id": 1, "sourceServiceId": 1, "cityId": 1, "createTimestamp": "2015-03-12 14:01:57", "lastUpdateDateTime": "2016-10-20 03:54:47", "recordHash": "e7b01c1a69bc66e1f1a62d8fcb0825de", "title": "Home Club", "shortDescription": "    , , ,    ", "longDescription": "   –       .   ,    –   -  Home Club.           50%!      , ,    .   ! ", "conditions": " <p class="e-condition__text">:</p> <ul class="b-conditions-list"> <li class="e-condition">      -  Home Club.</li> <li class="e-condition"> <strong></strong>:  50%   - 1 500 .  3 000 .</li> <li class="e-condition">         ( ).</li> <li class="e-condition">      .</li> <li class="e-condition">    10 .</li> <li class="e-condition"> <strong>VIP-     .   VIP-     .</strong> </li> <li class="e-condition">        .</li> <li class="e-condition"> <strong>    :</strong><br> +7 (727) 308-23-63,<br> +7 (747) 841-42-51,<br> +7 (701) 985-90-72.</li> <li class="e-condition"> <strong>   .</strong> </li> <li class="e-condition">      2 ,     ( ).</li> <li class="e-condition">        ,       «».</li> <li class="e-condition">      Home Club  :  ,    ,  , Home Club.</li> <li class="e-condition">         ,   ,    .</li> <li class="e-condition"> <strong>     .</strong> </li> <li class="e-condition">    12  2015 . ().</li> <li class="e-condition"> <span hashstring="deal_refunds_policy" hashtype="content">&nbsp</span> </li> <li class="e-condition"> <span hashstring="deal_standard_conditions" hashtype="content">&nbsp</span> </li> </ul> <p class="e-offer__features"></p> <ul class="b-offer__features-list"> <li class="e-offer__feature ">  ,    ,  , Home Club </li> <li class="e-offer__feature "> :<br> +7 (727) 308-23-63<br>+7 (747) 841-42-51<br>+7 (701) 985-90-72<br> </li> <li class="e-offer__feature "> :<br> : </li> </ul>", "features": " <p class="e-offer__features">:</p> <ul class="b-offer__features-list"> <li class="e-offer__feature">Home Club            ,    ,     .</li> <li class="e-offer__feature"> ​   10  (5    5 Vip-).  Vip-      , , -, ,  .</li> <li class="e-offer__feature"> ​ Home Club    -  3- : <ul> <li> ;</li> <li>;</li> <li> .</li> </ul> </li> <li class="e-offer__feature"> ​  : <ul> <li> 4-  7-  ;</li> <li>1 :  , -, , ;</li> <li>2 : 2-  ,  ,    , ,   ;</li> <li>3 : 2  ,   .</li> </ul> </li> <li class="e-offer__feature"> ​VIP-: <ul> <li> 4-  11-  ;</li> <li>1 : ,    10 ,  , ,   , ,  ;</li> <li>2 : -12 ,  2-  4-   (    ), 2 ..</li> </ul> </li> <li class="e-offer__feature"> : <a data-seohide-href="/deal/away/20056/" class="e-offer__feature--link seohide-link" target="_blank" rel="nofollow" title="http://www.home-club.kz/">www.home-club.kz/</a> </li> </ul>", "imagesLinks": [ "https://static.chocolife.me/static/upload/images/deal/for_deal_page/21000/20056/660x305/1_20150312023051426147565.7364.jpg", "https://static.chocolife.me/static/upload/images/deal/for_deal_page/21000/20056/660x305/2_20150312023051426147565.9348.jpg", "https://static.chocolife.me/static/upload/images/deal/for_deal_page/21000/20056/660x305/4_20150312093171426174997.7985.jpg", "https://static.chocolife.me/static/upload/images/deal/for_deal_page/21000/20056/660x305/5_20150312093171426174997.944.jpg", "https://www.chocolife.me/" ], "timeToCompletion": null, "mainImageLink": "https://www.chocolife.me/", "originalCouponPrice": "30 000", "originalPrice": "30 000", "discountPercent": "-51%", "discountPrice": "18 000", "discountType": "full", "boughtCount": "1", "sourceServiceCategories": "1 , 82 , 8 , 2", "pageLink": "https://www.chocolife.me//20056-arenda-kottedzha-s-dvumya-spalnyami-gorki-sauna-darts-i-mnogoe-drugoe-v-prirodno-razvlekatelnom-parke-home-club-skidka-do-50", "isArchive": 1, "tryToUpdateCount": 0, "viewCount": "0", "serviceName": "Chocolife.me", "cityName": "" }
      
      







まあ、このような...私はそれを作って、写真付きの部分配列を含む多くの異なる種類のデータが存在するようにしました。



私たちはこのすべての善で何をすべきでしょうか?もちろんハンドル!このセクションでは、コメント付きのドライコードがありますが、正しい順序になっています。だから、すべてが明確でなければなりません。



そのため、まず、SkidKZApi APIクラスを作成し、サーバーデータを操作するためのメソッドを実装します。



skidkzapi.h
 #ifndef SKIDKZAPI_H #define SKIDKZAPI_H #include "apibase.h" #include <QtQml> class SkidKZApi : public APIBase { Q_OBJECT public: Q_INVOKABLE explicit SkidKZApi(); // .      QML static void declareQML() { qmlRegisterType<SkidKZApi>("com.github.qtrestexample.skidkzapi", 1, 0, "SkidKZApi"); } //      ReadOnly  QNetworkReply *handleRequest(QString path, QStringList sort, Pagination *pagination, QVariantMap filters = QVariantMap(), QStringList fields = QStringList(), QString id = 0); //      /v1/coupon QNetworkReply *getCoupons(QStringList sort, Pagination *pagination, QVariantMap filters = QVariantMap(), QStringList fields = QStringList()); //      /v1/coupon/{id} QNetworkReply *getCouponDetail(QString id); //      /v1/categories QNetworkReply *getCategories(QStringList sort, Pagination *pagination); }; #endif // SKIDKZAPI_H
      
      







skidkzapi.cpp
 #include "skidkzapi.h" #include <QFile> #include <QTextStream> #include <QUrlQuery> SkidKZApi::SkidKZApi() : APIBase(0) { } QNetworkReply *SkidKZApi::handleRequest(QString path, QStringList sort, Pagination *pagination, QVariantMap filters, QStringList fields, QString id) { // ,           if (path == "/v1/coupon") { return getCoupons(sort, pagination, filters, fields); } else if (path == "/v1/coupon/{id}") { return getCouponDetail(id); } else if (path == "/v1/categories") { return getCategories(sort, pagination); } } //  ,   ,           QNetworkReply *SkidKZApi::getCoupons(QStringList sort, Pagination *pagination, QVariantMap filters, QStringList fields) { //   QUrl url = QUrl(baseUrl()+"/v1/coupon"); QUrlQuery query; // if (!sort.isEmpty()) { query.addQueryItem("sort", sort.join(",")); } //      switch(pagination->policy()) { case Pagination::PageNumber: query.addQueryItem("per-page", QString::number(pagination->perPage())); query.addQueryItem("page", QString::number(pagination->currentPage())); break; case Pagination::None: case Pagination::Infinity: case Pagination::LimitOffset: case Pagination::Cursor: default: break; } // .  ,     -    if (!filters.isEmpty()) { QMapIterator<QString, QVariant> i(filters); while (i.hasNext()) { i.next(); query.addQueryItem(i.key(), i.value().toString()); } } //       if (!fields.isEmpty()) { query.addQueryItem("fields", fields.join(",")); } //  url.setQuery(query.query()); //     GET QNetworkReply *reply = get(url); return reply; } //      QNetworkReply *SkidKZApi::getCouponDetail(QString id) { if (id.isEmpty()) { qDebug() << "ID is empty!"; return 0; } //         GET QUrl url = QUrl(baseUrl()+"/v1/coupon/"+id); QNetworkReply *reply = get(url); return reply; } //    ,   QNetworkReply *SkidKZApi::getCategories(QStringList sort, Pagination *pagination) { // QUrl url = QUrl(baseUrl()+"/v1/categories"); QUrlQuery query; // if (!sort.isEmpty()) { query.addQueryItem("sort", sort.join(",")); } // switch(pagination->policy()) { case Pagination::PageNumber: query.addQueryItem("per-page", QString::number(pagination->perPage())); query.addQueryItem("page", QString::number(pagination->currentPage())); break; case Pagination::None: case Pagination::Infinity: case Pagination::LimitOffset: case Pagination::Cursor: default: break; } url.setQuery(query.query()); QNetworkReply *reply = get(url); return reply; }
      
      







APIクラスの準備が完了しました。いくつかの簡単なメソッドで、現時点で必要なすべてのサーバー作業を実装しました。次に、モデルを使用するための2つのオプションを検討します。カテゴリには、ライブラリに組み込まれたJsonRestListModelモデルを使用し、クーポンには、AbstractJsonListModelから継承したモデルを使用します。



ticketmodel.h
 #ifndef COUPONMODEL_H #define COUPONMODEL_H #include "abstractjsonrestlistmodel.h" #include "api/skidkzapi.h" class CouponModel : public AbstractJsonRestListModel { Q_OBJECT public: explicit CouponModel(QObject *parent = 0); //   QML (    main.cpp   QML) static void declareQML() { AbstractJsonRestListModel::declareQML(); qmlRegisterType<CouponModel>("com.github.qtrestexample.coupons", 1, 0, "CouponModel"); } protected: //    API QNetworkReply *fetchMoreImpl(const QModelIndex &parent); QNetworkReply *fetchDetailImpl(QString id); //    QVariantMap preProcessItem(QVariantMap item); }; #endif // COUPONMODEL_H
      
      







bonusmodel.cpp
 #include "couponmodel.h" CouponModel::CouponModel(QObject *parent) : AbstractJsonRestListModel(parent) { } QNetworkReply *CouponModel::fetchMoreImpl(const QModelIndex &parent) { Q_UNUSED(parent) //   API  return static_cast<SkidKZApi *>(apiInstance())->getCoupons(sort(), pagination(), filters(), fields()); } QNetworkReply *CouponModel::fetchDetailImpl(QString id) { //   API  return static_cast<SkidKZApi *>(apiInstance())->getCouponDetail(id); } QVariantMap CouponModel::preProcessItem(QVariantMap item) { //      createTimestamp QDate date = QDateTime::fromString(item.value("createTimestamp").toString(), "yyyy-MM-dd hh:mm:ss").date(); item.insert("createDate", date.toString("dd.MM.yyyy")); //  -  originalCouponPrice QString originalCouponPrice = item.value("originalCouponPrice").toString().trimmed(); if (originalCouponPrice.isEmpty()) { originalCouponPrice = "?"; } QString discountPercent = item.value("discountPercent").toString().trimmed().remove("—").remove("-").remove("%"); if (discountPercent.isEmpty()) { discountPercent = "?"; } QString originalPrice = item.value("originalPrice").toString().trimmed(); if (originalPrice.isEmpty()) { originalPrice = "?"; } QString discountPrice = item.value("discountPrice").toString().remove(".").trimmed(); if (discountPrice.isEmpty()) { discountPrice = "?"; } //    discountString,     API QString discountType = item.value("discountType").toString(); QString discountString = tr("Undefined Type"); if (discountType == "freeCoupon" || discountType == "coupon") { discountString = tr("Coupon: %1. Discount: %2%").arg(originalCouponPrice).arg(discountPercent); } else if (discountType == "full") { discountString = tr("Cost: %1. Certificate: %2. Discount: %3%").arg(originalPrice).arg(discountPrice).arg(discountPercent); } item.insert("discountString", discountString); return item; }
      
      







できた!データを受信し、GUIパーツに接続するために必要なものはすべて揃っています。



まず、main.cppでdeclareQMLメソッドを呼び出すことを忘れないでください。例はソースにあります。



それでは-通常どおり、QMLアプリケーションを作成し、モデルをデータソースとして使用します。



somewhere.qml
 ... import com.github.qtrestexample.skidkzapi 1.0 import com.github.qtrest.jsonrestlistmodel 1.0 import com.github.qtrest.pagination 1.0 import com.github.qtrest.requests 1.0 ... //API ,    ,   -  =) SkidKZApi { id: skidKZApi baseUrl: "http://api.skid.kz" authTokenHeader: "Authorization" authToken: "Bearer 8aef452ee3b32466209535b96d456b06" Component.onCompleted: console.log("completed!"); } // ,  ReadOnly  // ,            -     ,      JsonRestListModel { id: categoriesRestModel api: skidKZApi idField: 'id' requests { get: "/v1/categories" } sort: ['categoryName'] pagination { policy: Pagination.PageNumber perPage: 20 currentPageHeader: "X-Pagination-Current-Page" totalCountHeader: "X-Pagination-Total-Count" pageCountHeader: "X-Pagination-Page-Count" } Component.onCompleted: { console.log(pagination.perPage); reload(); } } //  CouponModel ,     requests, ..    fetchMoreImpl. CouponModel { id: coupons; api: skidKZApi filters: {'isArchive': '0'} idField: 'id' fields: ['id','title','sourceServiceId','imagesLinks', 'mainImageLink','pageLink','cityId','boughtCount', 'shortDescription','createTimestamp', 'serviceName', 'discountType', 'originalCouponPrice', 'originalPrice', 'discountPercent', 'discountPrice'] sort: ['-id'] pagination { policy: Pagination.PageNumber perPage: 20 currentPageHeader: "X-Pagination-Current-Page" totalCountHeader: "X-Pagination-Total-Count" pageCountHeader: "X-Pagination-Page-Count" } Component.onCompleted: { console.log(pagination.perPage); reload(); } }
      
      







それだけです。ListViewでモデルを使用する例を紹介するつもりはありません。



ソースコードとサンプルアプリケーション



まあ、実際に私たちは最も興味深いものに渡します。プロジェクト全体は、GitHubの次のアドレスにあります。





上記のすべてが誰かにとって有用であり、自分のニーズに合わせてAPIクライアントを開発する時間を無駄にしないことを願っています。



PS:残念なことに、技術記事に関する議論は3年前からありませんでした。トピックに興味がある場合は、コメントを書くようにしてください。 =)



All Articles