البيانات من الواجهة الخلفية C++ إلى الواجهة الأمامية QML
كما هو موضح في الدرس السابق، يمكنك وصل كود C++ بـ QML بإنشاء صف سيُعامل كمكون آخر في QML. لكن قد ترغب في تمثيل بيانات أكثر تعقيدًا، مثل بيانات تحتاج إلى العمل كنموذج قائمة ListModel مخصص أو تحتاج بطريقة ما إلى التفويض من Repeater.
يمكننا إنشاء نماذج خاصة بنا من جانب C++، وتصريح كيف ينبغي تمثيل البيانات من ذلك النموذج على الواجهة الأمامية لـ QML.
يُوصى بشدة بقراءة البرنامج التعليمي طرق عرض القوائم قبل هذا البرنامج.
globalDrawer:Kirigami.GlobalDrawer{isMenu:trueactions:[Kirigami.Action{text:i18n("Exposing to QML")icon.name:"kde"onTriggered:pageStack.push(Qt.createComponent("org.kde.tutorial.components","ExposePage"))},Kirigami.Action{text:i18n("C++ models in QML")icon.name:"kde"onTriggered:pageStack.push(Qt.createComponent("org.kde.tutorial.components","ModelsPage"))},Kirigami.Action{text:i18n("Quit")icon.name:"application-exit-symbolic"shortcut:StandardKey.QuitonTriggered:Qt.quit()}]}
سيكون هذا بمثابة اللوحة لصفحة هذا البرنامج التعليمي.
استخدام السلاسل النصية الخام
لتسهيل فهم كيفية تعبئة النموذج في هذا البرنامج التعليمي، سنعطل ميزة تستخدمها تطبيقات كيدي التي تستخدم وحدات cmake الإضافية (ECM) افتراضيًا والتي تحسّن كود السلاسل النصية. يتيح لنا هذا تجنب كتابة QStringLiteral() في كل مرة تُقدَّم فيها سلسلة نصية في كود C++ الخاص بنا، وهو ما سيكون مفيدًا للكود في ملف الرأس القادم.
تعطيل علامة CMake هذه يتم لأغراض تعليمية فقط. يجب أن يستخدم كود الإنتاج QStringLiteral() أو مساحة أسماء حرفيات السلاسل النصية في Qt بدلاً من ذلك حيثما أمكن.
تحضير الصنف
سننشئ صنفًا يحتوي على QMap، حيث يُستخدم QString كمفتاح وكائنات QStringList كقيم. ستتمكن الواجهة الأمامية من قراءة وعرض المفاتيح والقيم وستكون بسيطة الاستخدام تمامًا مثل مصفوفة أحادية البعد. يجب أن تبدو مشابهة لـ QML ListModel.
لفعل هذا، نحتاج إلى إنشاء صنف يرث من QAbstractListModel. لنُضِف أيضًا بعض البيانات إلى QMap. ستكون هذه التصريحات موجودة في model.h.
أنشِئ ملفين جديدين، src/components/model.h و src/components/model.cpp.
أضِف هذين الملفين الجديدين إلى src/components/CMakeLists.txt:
أضِف ما يلي كمحتويات أولية إلى src/components/model.h:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#pragma once
#include<QAbstractListModel>#include<qqmlintegration.h>classModel:publicQAbstractListModel{Q_OBJECTQML_ELEMENTpublic:private:QMap<QString,QStringList>m_list={{"Feline",{"Tigress","Waai Fuu"}},{"Fox",{"Carmelita","Diane","Krystal"}},{"Goat",{"Sybil","Toriel"}}};};
بالطبع، لا يمكننا عرض هذا الصنف كما هو. نحتاج أيضًا إلى إخبار QML بكيفية تمثيل هذه البيانات في الصنف. يمكننا فعل ذلك بتجاوز ثلاث دوال افتراضية أساسية:
rowCount() - اعتبر هذه الدالة وسيلة لإخبار QML بعدد العناصر التي يجب أن يعرضها النموذج.
roleNames() - يمكنك اعتبار أسماء الأدوار كأسماء خصائص مرفقة بالبيانات في QML. تسمح لك هذه الدالة بإنشاء تلك الأدوار.
data() - تُستدعى هذه الدالة عندما تريد استرداد البيانات التي تتوافق مع أسماء الأدوار من النموذج.
ملاحظة
أسماء الأدوار المخصصة التي أنشأتها roleNames() قابلة للاستخدام فقط عندما يكون النموذج مفوضًا، وغير قابلة للاستخدام خارجه. انظر النماذج وطرق العرض.
ملاحظة
من الناحية التقنية، يتم تمثيل النماذج في Qt كجداول، بصفوف وأعمدة. لذا، ما يفعله تجاوز rowCount() هو إخبار Qt بعدد الصفوف في النموذج. وبما أننا نتعامل فقط مع مصفوفة أحادية البعد في هذا البرنامج التعليمي، يمكنك فقط اعتبار "الصفوف" على أنها "عدد العناصر."
تجاوز وتنفيذ rowCount()
لنتجاوز الدالة في ملف الرأس src/components/model.h. تأتي دالة rowCount() مع معاملها الخاص، لكنه لن يُستخدم في هذا المثال ولذا لا يحتاج إلى تسمية.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#pragma once
#include<QAbstractListModel>#include<qqmlintegration.h>classModel:publicQAbstractListModel{Q_OBJECTQML_ELEMENTpublic:introwCount(constQModelIndex&)constoverride;private:QMap<QString,QStringList>m_list={{"Feline",{"Tigress","Waai Fuu"}},{"Fox",{"Carmelita","Diane","Krystal"}},{"Goat",{"Sybil","Toriel"}}};};
ثم، لنُصرِّح بعدد الصفوف في هذا النموذج في src/components/model.cpp:
قبل أن نتجاوز roleNames()، نحتاج إلى تصريح ما هي الأدوار في جانب C++ باستخدام تعداد عام. السبب في ذلك هو أن قيم التعداد هذه تُمرر إلى data() في كل مرة يصل فيها QML إلى دور مقابل، وبالتالي يمكننا جعل data() تُرجع ما نريد.
لنبدأ بإنشاء التعداد للأدوار في src/components/model.h، حيث كل قيمة هي دور لجانب C++.
#pragma once
#include<QAbstractListModel>#include<qqmlintegration.h>classModel:publicQAbstractListModel{Q_OBJECTQML_ELEMENTpublic:enumRoles{SpeciesRole=Qt::UserRole,CharactersRole};QHash<int,QByteArray>roleNames()constoverride;introwCount(constQModelIndex&)constoverride;private:QMap<QString,QStringList>m_list={{"Feline",{"Tigress","Waai Fuu"}},{"Fox",{"Carmelita","Diane","Krystal"}},{"Goat",{"Sybil","Toriel"}}};};
بمجرد أن نستقر على ذلك، يمكننا أخيرًا إنشاء ما هي هذه الأدوار في جانب QML باستخدام QHash حيث المفاتيح هي القيم المعدودة مقترنة بـ QByteArrays. يجب أن يذهب هذا إلى src/components/model.cpp. النص في QByteArray هو ما يُستخدم في كود QML الفعلي.
في نموذجنا المثال، يمكن استخدام الدور "species" لاسترداد مفتاح QString "Feline"، "Fox"، "Goat"، كل في مفوض منفصل. يمكن فعل الشيء نفسه مع قيم QStringList لقائمة أسماء الشخصيات.
تجاوز وتنفيذ data()
هناك معلمتان تُمرران إلى data(): index و role. index هو موضع البيانات في النموذج. كما ذُكر سابقًا، يُستخدم role بواسطة QML للحصول على بيانات محددة تُعاد عند الوصول إلى دور.
في data()، يمكننا استخدام جملة switch لإرجاع البيانات ونوع البيانات المناسبين اعتمادًا على الدور، وهذا ممكن لأن data() تُرجع QVariant. ما زلنا بحاجة للتأكد من الحصول على الموقع المناسب للبيانات. في هذا المثال أدناه، يمكنك رؤية أن متغير مكرر جديد يُصرح عنه، ويُضبط من بداية القائمة زائد صف الفهرس، والبيانات التي يشير إليها المكرر هي ما يُعاد.
لا يمكننا فقط إرجاع أي بيانات نريدها. قد نحاول ربط بيانات بخاصية بنوع بيانات غير متوافق، مثل QStringList إلى QString. قد تضطر لإجراء تحويل بيانات لكي تُعرض البيانات بشكل صحيح. لهذا، ننشئ دالة خاصة ثابتة جديدة باسم formatList().
ينتج عن هذا الكود التالي في src/components/model.cpp:
#pragma once
#include<QAbstractListModel>#include<qqmlintegration.h>classModel:publicQAbstractListModel{Q_OBJECTQML_ELEMENTpublic:enumRoles{SpeciesRole=Qt::UserRole,CharactersRole};introwCount(constQModelIndex&)constoverride;QHash<int,QByteArray>roleNames()constoverride;QVariantdata(constQModelIndex&index,introle)constoverride;private:QMap<QString,QStringList>m_list={{"Feline",{"Tigress","Waai Fuu"}},{"Fox",{"Carmelita","Diane","Krystal"}},{"Goat",{"Sybil","Toriel"}}};staticQStringformatList(constQStringList&list);};
استخدام الصنف في QML
ملف QML المستخدم سيحتوي فقط على ثلاثة مكونات Kirigami.AbstractCard، حيث المفتاح هو الرأس والقيمة هي المحتوى. تُنشأ هذه البطاقات بتفويض AbstractCard باستخدام Repeater، حيث يعمل النموذج المخصص الذي أنشأناه كنموذج. تُوصل البيانات باستخدام كلمة model، متبوعة بالأدوار التي أعلناها في roleNames().
importQtQuickimportQtQuick.LayoutsimportQtQuick.ControlsasControlsimportorg.kde.kirigamiasKirigamiKirigami.ScrollablePage{title:"C++ models in QML"Model{id: customModel}ColumnLayout{anchors.left:parent.leftanchors.right:parent.rightRepeater{model:customModeldelegate:Kirigami.AbstractCard{header:Kirigami.Heading{text:model.specieslevel:2}contentItem:Controls.Label{text:model.characters}}}}}
تعديل البيانات
تحرير البيانات باستخدام dataChanged() و setData()
قد تواجه موقفًا تريد فيه تعديل البيانات في النموذج، وأن تنعكس التغييرات على جانب الواجهة الأمامية. كل مرة نغير فيها البيانات في النموذج، يجب أن نُصدر إشارة dataChanged() التي ستطبق تلك التغييرات على جانب الواجهة الأمامية في الخلايا المحددة في وسائطها. في هذا الدليل، يمكننا فقط استخدام وسيطة index لـ setData().
setData() هي دالة افتراضية يمكنك تجاوزها بحيث أن تعديل البيانات من جانب الواجهة الأمامية يعكس تلقائيًا تلك التغييرات على جانب الخلفية. تتطلب ثلاث معاملات:
index - موقع البيانات.
value - محتويات البيانات الجديدة.
role - في هذا السياق، يُستخدم الدور هنا لإخبار العروض كيف يجب أن تتعامل مع البيانات. يجب أن يكون الدور هنا Qt::EditRole.
معامل role في هذه الحالة يُستخدم لضمان أن setData() يمكن تحريرها عبر إدخال المستخدم (Qt::EditRole). باستخدام index، يمكننا استخدام ذلك لتحديد موقع حيث يجب تحرير البيانات بمحتويات value.
#pragma once
#include<QAbstractListModel>#include<qqmlintegration.h>classModel:publicQAbstractListModel{Q_OBJECTQML_ELEMENTpublic:enumRoles{SpeciesRole=Qt::UserRole,CharactersRole};introwCount(constQModelIndex&)constoverride;QHash<int,QByteArray>roleNames()constoverride;QVariantdata(constQModelIndex&index,introle)constoverride;boolsetData(constQModelIndex&index,constQVariant&value,introle)override;private:QMap<QString,QStringList>m_list={{"Feline",{"Tigress","Waai Fuu"}},{"Fox",{"Carmelita","Diane","Krystal"}},{"Goat",{"Sybil","Toriel"}}};staticQStringformatList(constQStringList&list);};
لنحدث كود QML بحيث يمكننا فتح موجه يسمح لنا بتحرير النموذج باستخدام Controls.Button ملحق بالبطاقات.
أضف التالي Kirigami.PromptDialog إلى src/components/ModelsPage.qml، مع زر تحرير جديد:
importQtQuickimportQtQuick.LayoutsimportQtQuick.ControlsasControlsimportorg.kde.kirigamiasKirigamiimportorg.kde.tutorial.componentsKirigami.ScrollablePage{title:"C++ models in QML"Model{id: customModel}ColumnLayout{anchors.left:parent.leftanchors.right:parent.rightRepeater{model:customModeldelegate:Kirigami.AbstractCard{Layout.fillHeight:trueheader:Kirigami.Heading{text:model.specieslevel:2}contentItem:Item{implicitWidth:delegateLayout.implicitWidthimplicitHeight:delegateLayout.implicitHeightColumnLayout{id: delegateLayoutControls.Label{text:model.characters}Controls.Button{text:"Edit"onClicked:{editPrompt.text=model.characters;editPrompt.model=model;editPrompt.open();}}}}}}}Kirigami.PromptDialog{id: editPromptpropertyvarmodelpropertyaliastext:editPromptText.texttitle:"Edit Characters"standardButtons:Kirigami.Dialog.Ok|Kirigami.Dialog.CancelonAccepted:{constmodel=editPrompt.model;model.characters=editPromptText.text;editPrompt.close();}Controls.TextField{id: editPromptTextonAccepted:editPrompt.accept()}}}
الآن، كلما تغيرت قيم النموذج في الواجهة الأمامية، يجب أن تُحدث التغييرات تلقائيًا في الخلفية.
إضافة صفوف
أضفنا طريقة لتعديل البيانات في المفاتيح الموجودة لـ QMap، وفي الواجهة الأمامية، ينعكس هذا كتعديل المحتويات داخل AbstractCards. لكن ماذا لو احتجنا لإضافة إدخال مفتاح جديد في QMap وجعل ذلك ينعكس على جانب QML؟ لنفعل هذا بإنشاء طريقة جديدة قابلة للاستدعاء على جانب QML لأداء هذه المهمة.
لجعل الطريقة مرئية في QML، يجب أن نبدأ تعريف الطريقة بماكرو Q_INVOKABLE. ستتضمن هذه الطريقة أيضًا معامل سلسلة نصية، والذي يُقصد به أن يكون المفتاح الجديد في QMap.
#pragma once
#include<QAbstractListModel>#include<qqmlintegration.h>classModel:publicQAbstractListModel{Q_OBJECTQML_ELEMENTpublic:enumRoles{SpeciesRole=Qt::UserRole,CharactersRole};introwCount(constQModelIndex&)constoverride;QHash<int,QByteArray>roleNames()constoverride;QVariantdata(constQModelIndex&index,introle)constoverride;Q_INVOKABLEvoidaddSpecies(constQString&species);private:QMap<QString,QStringList>m_list={{"Feline",{"Tigress","Waai Fuu"}},{"Fox",{"Carmelita","Diane","Krystal"}},{"Goat",{"Sybil","Toriel"}}};staticQStringformatList(constQStringList&list);};
داخل هذه الطريقة، نحتاج إلى إخبار كيوت بأننا نريد إنشاء المزيد من الصفوف في النموذج. يُفعل ذلك باستدعاء beginInsertRows() لبدء عملية إضافة الصفوف، يليه إدراج ما نحتاجه، ثم استخدام endInsertRows() لإنهاء العملية. ومع ذلك، لا يزال يتعين علينا إصدار dataChanged() في النهاية. هذه المرة، سنقوم بتحديث جميع الصفوف، من الصف الأول إلى الأخير، لأن QMap قد يعيد تنظيم نفسه أبجديًا، ونحتاج إلى التقاط ذلك عبر جميع الصفوف.
عند استدعاء beginInsertRows()، نحتاج أولاً إلى تمرير فئة QModelIndex لتحديد موقع إضافة الصفوف الجديدة، يليه ما سيكون عليه رقما الصف الأول والأخير الجديدان. في هذا الدليل، ستكون الوسيطة الأولى مجرد QModelIndex() حيث لا حاجة لاستخدام المعامل هنا. يمكننا فقط استخدام حجم الصف الحالي لرقمي الصف الأول والأخير، لأننا سنضيف صفًا واحدًا فقط في نهاية النموذج.
تستخدم دالة dataChanged() QModelIndex كنوع بيانات لمعاملاتها. ومع ذلك، يمكننا تحويل الأعداد الصحيحة إلى أنواع بيانات QModelIndex باستخدام دالة index().
لنقم بتحديث كود QML لنمنح القدرة على إضافة مفتاح جديد إلى QMap.
importQtQuickimportQtQuick.LayoutsimportQtQuick.ControlsasControlsimportorg.kde.kirigamiasKirigamiimportorg.kde.tutorial.componentsKirigami.ScrollablePage{title:"C++ models in QML"actions:[Kirigami.Action{icon.name:"list-add-symbolic"text:"Add New Species"onTriggered:{addPrompt.open();}}]Model{id: customModel}ColumnLayout{// ...
}Kirigami.PromptDialog{id: addPrompttitle:"Add New Species"standardButtons:Kirigami.Dialog.OkonAccepted:{customModel.addSpecies(addPromptText.text);addPromptText.text="";// Clear TextField every time it's done
addPrompt.close();}Controls.TextField{id: addPromptTextLayout.fillWidth:trueonAccepted:addPrompt.accept()}}Kirigami.PromptDialog{id: editPrompt// ...
}}
الآن، يجب أن يُمنحنا إجراء جديد في أعلى التطبيق يعرض موجهًا يسمح بإضافة عنصر جديد إلى النموذج، مع بياناتنا المخصصة.
إزالة الصفوف
طريقة إزالة الصفوف مشابهة لإضافة الصفوف. لننشئ طريقة أخرى سنستدعيها في QML. هذه المرة، سنستخدم معاملًا إضافيًا، وهو عدد صحيح يمثل رقم الصف. يُستخدم اسم النوع لحذف المفتاح من QMap، بينما يُستخدم رقم الصف لحذف الصف في الواجهة الأمامية.
أضف دالة Q_INVOKABLE جديدة باسم deleteSpecies() في src/components/model.h:
#pragma once
#include<QAbstractListModel>#include<qqmlintegration.h>classModel:publicQAbstractListModel{Q_OBJECTQML_ELEMENTpublic:enumRoles{SpeciesRole=Qt::UserRole,CharactersRole};introwCount(constQModelIndex&)constoverride;QHash<int,QByteArray>roleNames()constoverride;QVariantdata(constQModelIndex&index,introle)constoverride;Q_INVOKABLEvoidaddSpecies(constQString&species);Q_INVOKABLEvoiddeleteSpecies(constQString&speciesName,constint&rowIndex);private:QMap<QString,QStringList>m_list={{"Feline",{"Tigress","Waai Fuu"}},{"Fox",{"Carmelita","Diane","Krystal"}},{"Goat",{"Sybil","Toriel"}}};staticQStringformatList(constQStringList&list);};
importorg.kde.kirigamiasKirigamiimportorg.kde.tutorial.componentsKirigami.Page{title:"Exposing to QML Tutorial"Kirigami.Heading{anchors.centerIn:parenttext:Backend.introductionText}}
src/components/backend.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#pragma once
#include<QObject>#include<qqmlintegration.h>classBackend:publicQObject{Q_OBJECTQML_ELEMENTQML_SINGLETONQ_PROPERTY(QStringintroductionTextREADintroductionTextWRITEsetIntroductionTextNOTIFYintroductionTextChanged)public:explicitBackend(QObject*parent=nullptr);QStringintroductionText()const;voidsetIntroductionText(constQString&introductionText);Q_SIGNALvoidintroductionTextChanged();private:QStringm_introductionText=QStringLiteral("Hello World!");};
importQtQuickimportQtQuick.LayoutsimportQtQuick.ControlsasControlsimportorg.kde.kirigamiasKirigamiimportorg.kde.tutorial.componentsKirigami.ScrollablePage{title:"C++ models in QML"actions:[Kirigami.Action{icon.name:"list-add-symbolic"text:"Add New Species"onTriggered:{addPrompt.open();}}]Model{id: customModel}ColumnLayout{anchors.left:parent.leftanchors.right:parent.rightRepeater{model:customModeldelegate:Kirigami.AbstractCard{Layout.fillHeight:trueheader:Kirigami.Heading{text:model.specieslevel:2}contentItem:Item{implicitWidth:delegateLayout.implicitWidthimplicitHeight:delegateLayout.implicitHeightColumnLayout{id: delegateLayoutControls.Label{text:model.characters}RowLayout{Layout.fillWidth:trueControls.Button{text:"Edit"onClicked:{editPrompt.text=model.characters;editPrompt.model=model;editPrompt.open();}}Controls.Button{text:"Delete"onClicked:{customModel.deleteSpecies(model.species,index);}}}}}}}}Kirigami.PromptDialog{id: addPrompttitle:"Add New Species"standardButtons:Kirigami.Dialog.OkonAccepted:{customModel.addSpecies(addPromptText.text);addPromptText.text="";// Clear TextField every time it's done
addPrompt.close();}Controls.TextField{id: addPromptTextLayout.fillWidth:trueonAccepted:addPrompt.accept()}}Kirigami.PromptDialog{id: editPromptpropertyvarmodelpropertyaliastext:editPromptText.texttitle:"Edit Characters"standardButtons:Kirigami.Dialog.Ok|Kirigami.Dialog.CancelonAccepted:{constmodel=editPrompt.model;model.characters=editPromptText.text;editPrompt.close();}Controls.TextField{id: editPromptTextonAccepted:editPrompt.accept()}}}
#pragma once
#include<QAbstractListModel>#include<qqmlintegration.h>classModel:publicQAbstractListModel{Q_OBJECTQML_ELEMENTpublic:enumRoles{SpeciesRole=Qt::UserRole,CharactersRole};introwCount(constQModelIndex&)constoverride;QHash<int,QByteArray>roleNames()constoverride;QVariantdata(constQModelIndex&index,introle)constoverride;boolsetData(constQModelIndex&index,constQVariant&value,introle)override;Q_INVOKABLEvoidaddSpecies(constQString&species);Q_INVOKABLEvoiddeleteSpecies(constQString&speciesName,constint&rowIndex);private:QMap<QString,QStringList>m_list={{"Feline",{"Tigress","Waai Fuu"}},{"Fox",{"Carmelita","Diane","Krystal"}},{"Goat",{"Sybil","Toriel"}}};staticQStringformatList(constQStringList&list);};