该实例,主要是在已有的QWidget工程中,加入qml工程,方便qml项目的开发与维护,让qml开发者更好的上手qml。
(1)展示了c++和qml常见的交互方式。
(2)qwidget工程如何加载qml工程,如何加载自己实现的qml tool库。
(3)创建无边框qml界面,支持拖拽,窗口的缩小与展开,界面的分段实现。
(4)展示一个简单的堆栈窗口(SwipeView),相当于QStackedWidget,管理多个子窗口页面。
(5)实现一个简单的列表框(ListView),相当于QListWidget,定时请求网络数据,展示学生信息,将view和model进行分离,降低界面与数据的耦合。
(6)点击学号、年龄,实现了列表数据的排序。
交互有很多种方式,有的比较繁杂,不易理解,此处只使用了最方便使用的方法,满足基本的交互。
需要先将C++类(MyWindow)注册到QQuickView中。和qml相关的C++类,最好都进行注册。
class MainQuickView : public QQuickView
{
Q_OBJECT
public:
MainQuickView(QQuickView *parent = nullptr);
~MainQuickView() override;
void initialzeUI();
protected:
};
void MainQuickView ::initialzeUI()
{
// 注册,为了qml中可以直接访问C++对象
MyWindow *w = new MyWindow();
this->rootContext()->setContextProperty("myWindow", w);
...
}
这样C++的信号
,public槽函数
,Q_INVOKABLE 修饰的类成员函数
class MyWindow : public QWidget
{
Q_OBJECT
public:
MyWindow();
// Q_INVOKABLE可以将C++函数暴漏给qml引擎,注册到元对象系统
Q_INVOKABLE void invokableMethod();
signals:
void dataChanged();
public slots:
void refresh();
};
就可以在qml中调用
// main.qml
Rectangle {
id: root
width: 1200
height: 800
onClicked: {
myWindow.showNormal(); // showNormal是QWidget父类的槽函数
}
}
MainWindow::MainWindow(QWidget *parent)
: QWidget(parent)
, ui(new Ui::MainWindow)
, m_pQuickVew(nullptr)
{
ui->setupUi(this);
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::on_pushButton_clicked()
{
if (!m_pQuickVew)
{
// 加载qml包含两种方式,一种是QQuickView,一种是QQmlApplicationEngine
// 当前使用第一种方式,MainQuickView继承自QQuickView
m_pQuickVew = new MainQuickView();
m_pQuickVew->setObjectName("quickView");
}
m_pQuickVew->show();
}
static const char* s_mainPath = "qrc:/qml/main.qml";
MainQuickView::MainQuickView(QQuickView *parent)
: QQuickView(parent)
, m_bMin(false)
{
this->setFlags(Qt::Window | Qt::FramelessWindowHint);
this->setTitle(QString::fromLocal8Bit("图书馆"));
initialize();
setMoveable();
setTitleData();
}
MainQuickView::~MainQuickView()
{
}
void MainQuickView::initialize()
{
// 初始化view 和 model,降低耦合,提高可维护性
if (!m_pStudentInfoView)
m_pStudentInfoView = new StudentInfoView();
if (!m_pStudentInfoModel)
m_pStudentInfoModel = m_pStudentInfoView->getStudentInfoMode();
// ...其他功能
initialzeUI();
}
void MainQuickView::initialzeUI()
{
// 注册,qml中可以直接访问C++对象
this->rootContext()->setContextProperty("mainQuickView", this);
// qml可以通过"studentInfoView",去使用其实现的函数
this->rootContext()->setContextProperty("studentInfoView", m_pStudentInfoView);
this->rootContext()->setContextProperty("studentInfoModel", m_pStudentInfoModel);
// qml根对象大小随窗口大小改变而改变
this->setResizeMode(QQuickView::SizeRootObjectToView);
// 导入自定义模块工具
this->engine()->addImportPath(":/qmlTools");
// 设置准备加载得qml文件,相当于c++的main函数入口
this->setSource(QUrl(s_mainPath));
}
void MainQuickView::minMaxQmlWindow()
{
m_bMin = !m_bMin;
emit minQmlWindow(m_bMin);
}
void MainQuickView::onSendTopRectPos(QVariant pX, QVariant pY)
{
this->setX(this->x() + pX.toInt());
this->setY(this->y() + pY.toInt());
}
void MainQuickView::onCloseQuickView()
{
close();
}
void MainQuickView::onShowMinimized()
{
showMinimized();
}
void MainQuickView::setMoveable()
{
// 当移动qml窗口时,将移动坐标告诉C++
// 找到对象名叫topRect的qml,C++绑定qml的信号
QQuickItem* topRect = this->rootObject()->findChild<QQuickItem*>("topRect");
if (topRect) {
connect(topRect, SIGNAL(sendTopRectPos(QVariant, QVariant)),
this, SLOT(onSendTopRectPos(QVariant, QVariant)));
}
}
void MainQuickView::setTitleData()
{
// 找到对象名叫topRect的qml,C++向qml发送消息,触发qml中实现的setTitleText函数
QQuickItem* topRect = this->rootObject()->findChild<QQuickItem*>("topRect");
QMetaObject::invokeMethod(topRect, "setTitleText", Q_ARG(QVariant, QString::fromLocal8Bit("Qml窗口标题")));
}
StudentInfoView::StudentInfoView(QObject *parent)
: QObject(parent)
, m_sortType(StudentInfoSort::sortNone)
{
if (!m_pStudentInfoModel)
m_pStudentInfoModel = new StudentInfoModel(this);
// 如果数据来自网络,就需要定时请求接口,并更新界面
m_timer = new QTimer(this);
m_timer->setInterval(10 * 1000);
QObject::connect(m_timer, &QTimer::timeout, this, &StudentInfoView::onUpdateInfoData);
onUpdateInfoData();
m_timer->start();
}
StudentInfoModel *StudentInfoView::getStudentInfoMode()
{
return m_pStudentInfoModel;
}
// qSort()比较函数不能定义为类成员函数(参数包含隐藏的this指针),会导致参数不符
// 可以定义为static类成员函数、static函数、普通函数
bool StudentInfoView::idAscend(const StudentInfoItem &stu1, const StudentInfoItem &stu2)
{
return stu1.stuId < stu2.stuId; //学号升序
}
// 普通函数
bool idDescend(const StudentInfoItem &stu1, const StudentInfoItem &stu2)
{
return stu1.stuId > stu2.stuId; //学号降序
}
bool ageAscend(const StudentInfoItem &stu1, const StudentInfoItem &stu2)
{
return stu1.age < stu2.age; //年龄升序
}
void StudentInfoView::setSort(StudentInfoSort sortType)
{
m_sortType = sortType;
if (m_sortType == StudentInfoSort::sortIdAscend)
{
qSort(m_allDatas.begin(), m_allDatas.end(), idAscend);
}
else if (m_sortType == StudentInfoSort::sortIdDescend)
{
qSort(m_allDatas.begin(), m_allDatas.end(), idDescend);
}
else if (m_sortType == StudentInfoSort::sortAgeAscend)
{
// 比较函数也可以写为lambda表达式
qSort(m_allDatas.begin(), m_allDatas.end(),
[](const StudentInfoItem& stu1, const StudentInfoItem& stu2)
{
return stu1.age < stu2.age;
});
}
}
void StudentInfoView::updateSort(int sortType)
{
// qml调用,执行哪一种排序方式,并刷新数据
setSort((StudentInfoSort)sortType);
if (m_pStudentInfoModel)
m_pStudentInfoModel->clear();
for (auto studentInfo : m_allDatas) {
if (m_pStudentInfoModel) {
m_pStudentInfoModel->AddModel(studentInfo);
}
}
}
void StudentInfoView::onUpdateInfoData()
{
// 每隔十秒请求网络接口,根据返回的数据刷新model,该实例模拟网络数据
m_allDatas.clear();
if (m_pStudentInfoModel)
m_pStudentInfoModel->clear();
QVector<StudentInfoItem> studentInfos;
StudentInfoItem item1;
item1.stuId = 9704;
item1.stuName = QString::fromLocal8Bit("百里");
item1.sex = 1;
item1.age = 14;
StudentInfoItem item2;
item2.stuId = 9207;
item2.stuName = QString::fromLocal8Bit("黄忠");
item2.sex = 1;
item2.age = 26;
StudentInfoItem item3;
item3.stuId = 9206;
item3.stuName = QString::fromLocal8Bit("鲁班");
item3.sex = 1;
item3.age = 17;
StudentInfoItem item4;
item4.stuId = 9787;
item4.stuName = QString::fromLocal8Bit("女娲");
item4.sex = 0;
item4.age = 33;
studentInfos << item1 << item2 << item3 << item4;
m_allDatas = studentInfos;
setSort(m_sortType); //每一次网络数据刷新后,执行保存的排序方式
for (auto studentInfo : m_allDatas) {
if (m_pStudentInfoModel) {
m_pStudentInfoModel->AddModel(studentInfo);
}
}
}
#include "StudentInfoModel.h"
StudentInfoModel::StudentInfoModel(QObject *parent)
: QAbstractListModel(parent)
{
roleNames();
}
StudentInfoModel::~StudentInfoModel()
{
}
void StudentInfoModel::clear()
{
if (m_allDatas.size() <= 0)
return;
beginRemoveRows(QModelIndex(), 0, m_allDatas.size() - 1);
m_allDatas.clear();
endRemoveRows();
}
void StudentInfoModel::mremove(int index)
{
beginRemoveRows(QModelIndex(), index, index);
m_allDatas.erase(m_allDatas.begin() + index);
endRemoveRows();
}
void StudentInfoModel::update(int index, const StudentInfoItem &infoModel)
{
if (index < 0 || m_allDatas.size() < index)
return;
StudentInfoItem &srcModel = m_allDatas[index];
srcModel = infoModel;
}
void StudentInfoModel::AddModel(const StudentInfoItem &md)
{
beginInsertRows(QModelIndex(), rowCount(), rowCount());
m_allDatas.push_back(md);
endInsertRows();
}
QVariant StudentInfoModel::data(const QModelIndex &index, int role) const
{
if (index.row() < 0 || index.row() >= m_allDatas.size()) {
return QVariant();
}
const StudentInfoItem &itemInfo = m_allDatas.at(index.row());
StudentInfoRoles infoRole = static_cast<StudentInfoRoles>(role);
switch (infoRole) {
case StudentInfoRoles::stuIdRole :
return itemInfo.stuId;
break;
case StudentInfoRoles::stuNameRole :
return itemInfo.stuName;
break;
case StudentInfoRoles::stuSexRole :
return itemInfo.sex;
break;
case StudentInfoRoles::stuAgeRole :
return itemInfo.age;
break;
default:
return QVariant();
break;
}
}
int StudentInfoModel::rowCount(const QModelIndex &parent) const
{
return m_allDatas.size();
}
QHash<int, QByteArray> StudentInfoModel::roleNames() const
{
// 映射C++端的枚举与QML端的字符串
QHash<int, QByteArray> data;
data[int(StudentInfoRoles::stuIdRole)] = "stuId";
data[int(StudentInfoRoles::stuNameRole)] = "stuName";
data[int(StudentInfoRoles::stuSexRole)] = "sex";
data[int(StudentInfoRoles::stuAgeRole)] = "age";
return data;
}
import QtQuick 2.0
import QtQuick.Layouts 1.0
Item {
id: root
width: 1200
height: 800
Rectangle {
anchors.fill: parent
color: "red"
ColumnLayout {
spacing: 0
MainQuickTopRect {
id: topRect
objectName: "topRect"
width: root.width
height: 50
}
MainQuickMiddleRect {
id: middleRect
objectName: "middleRect"
width: root.width
height: root.height - topRect.height
}
}
}
}
import QtQuick 2.12
import QtQuick.Layouts 1.0
import QtQuick.Controls 2.12
import "../qmlTools/ButtonTools"
Rectangle {
id: root
color: "#181D33"
signal sendTopRectPos(var x, var y)
function setTitleText(text) {
titleText.text = text
}
function onMinQmlWindow(bMin){
if (bMin)
narrowBtn.clicked() // narrowBtn是最小化按钮,当然也可以直接mainQuickView.onShowMinimized()
else
mainQuickView.showNormal()
}
Component.onCompleted: {
mainQuickView.minQmlWindow.connect(onMinQmlWindow)
}
RowLayout {
id: rowLayout
anchors.fill: root
anchors.leftMargin: 16
anchors.rightMargin: 16
Text {
id: titleText
anchors.fill: root
anchors.left: rowLayout.left
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
color: "white"
font.pixelSize: 18
font.family: "Microsoft YaHei UI"
font.bold: true
text: ""
// 设置文本框背景颜色
Rectangle {
id: titleTextBack
anchors.fill: titleText
color: "#000000"
z: -1
}
}
// 最小化
ImageBtn {
id: narrowBtn
width: 24
height: 24
anchors.right: closeBtn.left
normalUrl: "qrc:/resource/minNormal.png"
hoveredUrl: "qrc:/resource/minHover.png"
pressedUrl: "qrc:/resource/minHover.png"
onClicked: {
mainQuickView.onShowMinimized();
console.log("min");
}
}
// 关闭
ImageBtn {
id: closeBtn
width: 24
height: 24
anchors.right: rowLayout.right
normalUrl: "qrc:/resource/closeNormal.png"
hoveredUrl: "qrc:/resource/closeHover.png"
pressedUrl: "qrc:/resource/closeHover.png"
onClicked: {
mainQuickView.onCloseQuickView();
}
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton
propagateComposedEvents: true //传递鼠标事件
property point clickPos: "0, 0"
onPressed: {
clickPos = Qt.point(mouse.x, mouse.y)
console.log("onPressed " + clickPos);
}
onPositionChanged: {
var delta = Qt.point(mouse.x - clickPos.x, mouse.y - clickPos.y)
sendTopRectPos(delta.x, delta.y)
}
}
}
import QtQuick 2.0
import QtQuick 2.12
import QtQuick.Controls 1.2
import QtQuick.Controls 2.5
import QtQuick.Layouts 1.1
import QtQuick.Controls.Styles 1.2
import "../qmlTools/ButtonTools"
Rectangle {
id: root
color: "#2E3449"
property int oneCount: 50
property int twoCount: 50
property int threeCount: 50
readonly property int pageWidth : 400
Rectangle {
id: titleSwitchBtn
width: parent.width / 2
anchors.top: parent.top
anchors.topMargin: 30
anchors.left: parent.left
anchors.leftMargin: width - width / 2
height: 46
color: "#2C5CA5"
RowLayout {
spacing: 20
anchors.bottom: parent.bottom
anchors.bottomMargin: 0
anchors.left: parent.left
anchors.leftMargin: 40
DownLineBtn { // qmlTool中自定义实现的下划线按钮
id: cover_one_btn
btn_width: 100
btn_string: qsTr("一班") + oneCount + qsTr("人")
onMyClicked: {
coverBtnStat(cover_one_btn, true)
coverBtnStat(cover_two_btn, false)
coverBtnStat(cover_three_btn, false)
swipeView.currentIndex = 0
}
}
DownLineBtn {
id: cover_two_btn
btn_width: 100
btn_string: qsTr("二班") + twoCount + qsTr("人")
onMyClicked: {
coverBtnStat(cover_one_btn, false)
coverBtnStat(cover_two_btn, true)
coverBtnStat(cover_three_btn, false)
swipeView.currentIndex = 1
}
}
DownLineBtn {
id: cover_three_btn
btn_width: 100
btn_string: qsTr("三班") + threeCount + qsTr("人")
onMyClicked: {
coverBtnStat(cover_one_btn, false)
coverBtnStat(cover_two_btn, false)
coverBtnStat(cover_three_btn, true)
swipeView.currentIndex = 2
}
}
}
}
Rectangle {
id: view
anchors.top: titleSwitchBtn.bottom
anchors.topMargin: 4
anchors.left: titleSwitchBtn.left
width: titleSwitchBtn.width
height: pageWidth
color: "white"
radius: 4
SwipeView {
id: swipeView
objectName: "outSideSwipView"
anchors.fill: parent
currentIndex: 0
clip: true //隐藏未选中的界面
interactive: false //鼠标能否滑动
Item { //St: 0,第一页
id: onePage
Rectangle {
id: oneView
anchors.fill: parent
color: "#D7B9A1"
}
}
Item { //St: 1,第二页
id: twoPage
MainQuickMiddleTableRect {
id: tableView
color: "green"
width: titleSwitchBtn.width
height: pageWidth
}
}
Item { //St: 2,第三页
id: defaultPage
Rectangle {
anchors.fill: parent
color: "#E6EC12"
radius: 4
Text {
text: qsTr("没有数据")
font.pixelSize:16
color: "red"
font.family: "Microsoft YaHei"
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
}
Component.onCompleted: {
// 初始化按钮状态
coverBtnStat(cover_one_btn, true)
coverBtnStat(cover_two_btn, false)
coverBtnStat(cover_three_btn, false)
}
/* btn: object
* st : modify state
*/
function coverBtnStat(btn, st) {
btn.bold_st = st
btn.show_line = st
}
}
import QtQuick 2.0
import "../qmlTools/ButtonTools"
import QtQuick.Controls 1.4
import QtQuick.Controls 2.5
Rectangle {
id: win
anchors.fill: parent
color: "green"
property var current_numbers : 0
property var stu_id_sort_state : 1
property var stu_id_sort_open : false
// 绘制表头
Rectangle {
id: table_Head
height: 48
anchors.top: titleSwitchBtn.bottom
Rectangle {
id: table_Text
height: 47 // 比整个表头高度小1,为了容纳下划线
TextShow {
id: name_text
text: qsTr("姓名")
anchors.left: table_Text.left
anchors.leftMargin: 68
anchors.verticalCenter: table_Text.verticalCenter
}
TextShow {
id: id_text
// 学号未点击时,显示"↕",表示不排序,点击后再判断时升序还是降序
text: qsTr("学号") + (!stu_id_sort_open ?
qsTr("↕") :
(stu_id_sort_state ? qsTr("↓") : qsTr("↑")))
anchors.left: table_Text.left
anchors.leftMargin: 150
anchors.verticalCenter: table_Text.verticalCenter
MouseArea {
anchors.fill: parent
propagateComposedEvents: true
onPressed: {
console.log("student id clicked")
studentInfoView.updateSort(stu_id_sort_state ? 1 : 2) // 通知C++修改排序方式
stu_id_sort_state = !stu_id_sort_state
stu_id_sort_open = true
}
}
}
TextShow {
id: sex_text
text: qsTr("性别")
anchors.left: table_Text.left
anchors.leftMargin: 220
anchors.verticalCenter: table_Text.verticalCenter
}
TextShow {
id: age_text
text: qsTr("年龄")
anchors.left: table_Text.left
anchors.leftMargin: 290
anchors.verticalCenter: table_Text.verticalCenter
MouseArea {
anchors.fill: parent
propagateComposedEvents: true
onPressed: {
console.log("student age clicked")
studentInfoView.updateSort(3)
stu_id_sort_open = false
}
}
}
LongLine {
anchors.top: table_Text.bottom
}
}
}
//listView
Rectangle {
width: parent.width
height: parent.height - table_Head.height
color: "green"
anchors.top: table_Head.bottom
ListView {
id: list_view
anchors.rightMargin: 10
anchors.bottomMargin: 50
anchors.leftMargin: 0
anchors.topMargin: 0
anchors.fill: parent
maximumFlickVelocity: 800
clip: true
delegate: studentInfoDelegate
model: studentInfoModel
boundsBehavior: Flickable.StopAtBounds
highlightMoveDuration: 0
ScrollBar.vertical: ScrollBar {
id: scrollbar
//visible: (current_numbers > 8)
visible: true
anchors.right: list_view.right
width: 8
active: true
background: Item {
Rectangle {
anchors.right: parent.right
height: parent.height
color: "yellow"
radius: 4
}
}
contentItem: Rectangle {
radius: 4
color: "red"
}
}
}
//studentInfoDelegate 渲染每一个item
Component {
id: studentInfoDelegate
Rectangle {
id: list_item
width: parent.width
height: 46
color: "green"
Rectangle {
width: parent.width
height: parent.height
color: "green"
anchors.verticalCenter: parent.verticalCenter
Rectangle {
id: rect_item
color: "#333333"
height: 45
TextShow {
id: stu_name
text: model.stuName
anchors.verticalCenter: rect_item.verticalCenter
anchors.left: rect_item.left
anchors.leftMargin: 68
}
TextShow {
id: id_type
text: model.stuId
anchors.verticalCenter: rect_item.verticalCenter
anchors.left: rect_item.left
anchors.leftMargin: 150
}
TextShow {
id: sex_type
text: (model.sex == 1) ? qsTr("男") : qsTr("女")
anchors.verticalCenter: rect_item.verticalCenter
anchors.left: rect_item.left
anchors.leftMargin: 220
}
TextShow {
id: age_type
text: model.age + qsTr(" 岁")
anchors.verticalCenter: rect_item.verticalCenter
anchors.left: rect_item.left
anchors.leftMargin: 290
}
}
LongLine {
anchors.top: rect_item.bottom
}
}
}
}
}
Component.onCompleted: {
// qml加载完成后,可以获取C++中数据的个数,用来标识是否需要展示滚动条
current_numbers = 4
}
}