37——Vector Deformation
最后更新于:2022-04-01 07:22:29
本系列所有文章可以在这里查看[http://blog.csdn.net/cloud_castle/article/category/2123873](http://blog.csdn.net/cloud_castle/article/category/2123873)
接上文[](http://blog.csdn.net/cloud_castle/article/details/43672509)[Qt5官方demo解析集36——Wiggly Example](http://blog.csdn.net/cloud_castle/article/details/44018679)
在 Qt 中设计GUI界面,经常需要考虑不同尺寸,不同分辨率下面的情况,因此我们经常需要准备几套图片素材来应付不同的场景。不过好在,我们还可以使用矢量绘图和矢量图形。
今天这个例子基于 QPainterPath 绘制了文本字符的矢量路径,并可以在一个"滤镜"范围内进行变形处理,效果萌萌哒~下面就来看看吧:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd0aee1db.jpg)
这个例子看起来代码量较多,我们关心的核心处理也就是几行,经过抽丝剥茧,去掉布局按钮定时器之类的逻辑,其实这也就是个小demo啦。
还是先从main.cpp来看:
~~~
#include "pathdeform.h"
#include <QApplication>
int main(int argc, char **argv)
{
Q_INIT_RESOURCE(deform);
QApplication app(argc, argv);
bool smallScreen = QApplication::arguments().contains("-small-screen");
PathDeformWidget deformWidget(0, smallScreen);
QStyle *arthurStyle = new ArthurStyle();
deformWidget.setStyle(arthurStyle);
QList<QWidget *> widgets = deformWidget.findChildren<QWidget *>();
foreach (QWidget *w, widgets)
w->setStyle(arthurStyle);
if (smallScreen)
deformWidget.showFullScreen();
else
deformWidget.show();
#ifdef QT_KEYPAD_NAVIGATION
QApplication::setNavigationMode(Qt::NavigationModeCursorAuto);
#endif
return app.exec();
}
~~~
在上一个例子中我们提到了这个 smallScreen,这里就不多提啦,注意将这个参数传递给我们的窗口,以做不同的处理。
接着它将窗口风格设置为 “ArthurStyle”,这个类是在 shared 子工程中定义的,继承自 QCustomStyle,之所以放在shared 中是因为有好几个 demo 是使用它的。如果大家一直在做 Qt 开发,不妨留心建立下自己的“Qt库”,把可扩展性和通用性考虑进去之后,就可以在下一个项目中复用一些东西啦。
好,不扯远了,如果屏蔽掉这几行 setStyle 语句后,可以看到这个 control 面板几乎不忍直视。。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd0b1bd35.jpg)
不看 shared 子工程,我们就剩 pathdeform.h 和 pathdeform.cpp 俩文件了,不过里面定义了三个类:
PathDeformWidget、PathDeformControls、PathDeformRenderer,分别是主窗口,控件窗体和渲染窗体
可以看到,控件窗体 PathDeformControls 针对 smallScreen 参数设计了两套布局
layoutForDesktop()
layoutForSmallScreen()
以应对不同的场景。
接下来,绘制“滤镜”并保存在 QImage 或 QPixmap 中,代码就不帖啦,
然后使用 QPainterPath 的 addText 将用户输入的字符添加到绘制路径中:
~~~
bool do_quick = true;
for (int i=0; i<text.size(); ++i) {
if (text.at(i).unicode() >= 0x4ff && text.at(i).unicode() <= 0x1e00) {
do_quick = false;
break;
}
}
if (do_quick) {
for (int i=0; i<text.size(); ++i) {
QPainterPath path;
path.addText(advance, f, text.mid(i, 1));
m_pathBounds |= path.boundingRect();
m_paths << path;
advance += QPointF(fm.width(text.mid(i, 1)), 0);
}
} else {
QPainterPath path;
path.addText(advance, f, text);
m_pathBounds |= path.boundingRect();
m_paths << path;
}
~~~
有意思的是,针对字符的不同编码,这里使用了两种不同的添加方式:
Unicode编码大于 0X4FF 且 小于 0X1E00 的字符,直接将整个文本添加到路径中去;
否则,使用循环语句添加该字符串中每个字符。
根据 do_quick 命名似乎是第二种方式要更快速一些,不过暂时没有找到相关的证明-.-
函数lensDeform()用来返回一个经过变形的QPainterPath,并由 painter 绘制出来:
~~~
QPainterPath PathDeformRenderer::lensDeform(const QPainterPath &source, const QPointF &offset)
{
QPainterPath path;
path.addPath(source);
qreal flip = m_intensity / qreal(100);
for (int i=0; i<path.elementCount(); ++i) {
const QPainterPath::Element &e = path.elementAt(i);
qreal x = e.x + offset.x();
qreal y = e.y + offset.y();
qreal dx = x - m_pos.x();
qreal dy = y - m_pos.y();
qreal len = m_radius - qSqrt(dx * dx + dy * dy);
if (len > 0) {
path.setElementPositionAt(i,
x + flip * dx * len / m_radius,
y + flip * dy * len / m_radius);
} else {
path.setElementPositionAt(i, x, y);
}
}
return path;
}
~~~
这里通过 len 判断 QPainterPath 的元素是否在 “滤镜”内,len值越大,离“滤镜”中心点越近。
一个在圆内的点各参数可以表示为下面这张图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd0b2f317.jpg)
由
~~~
x + flip * dx * len / m_radius,
y + flip * dy * len / m_radius
~~~
可知当 flip 为正数时,越靠近滤镜中心的部分 x,y增量越大,表现为中心部分向外扩张得更厉害,并且保证坐标增量不至于超出滤镜范围,从而形成凸透镜的效果。
反之,flip为负数时,越靠近中心部分的x,y减去的值更大,从而使图形向中心收缩,形成凹透镜的效果。
主要就是这些啦~
36——Wiggly Example
最后更新于:2022-04-01 07:22:26
本系列所有文章可以在这里查看[http://blog.csdn.net/cloud_castle/article/category/2123873](http://blog.csdn.net/cloud_castle/article/category/2123873)
接上文[Qt5官方demo解析集35——Music Player](http://blog.csdn.net/cloud_castle/article/details/43672509)
今天同样带来一个有趣的小例子,如下图所示,我们输入的文字将在中央窗口显示并以正弦波形浮动。
这个例子中涉及到 Qt 定时器族中的 QBasicTimer 类以及十分实用的 QFontMetrics 类。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd0acd1e6.jpg)
当我们将应用部署在移动设备上时,Qt 将为该应用添加 "-small-screen" 的命令行参数。因此,通过判断该参数是否存在,我们就可以为这个应用选择不同的显示模式,如同该 demo 中 main() 函数所示:
~~~
#include <QApplication>
#include "dialog.h"
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
bool smallScreen = QApplication::arguments().contains("-small-screen");
Dialog dialog(0, smallScreen);
if (!smallScreen) // 如果不是smallScreen直接显示,否则全屏显示
dialog.show();
else
dialog.showFullScreen();
return app.exec();
}
~~~
Dialog 类继承自 QDialog,使用垂直布局添加了中心窗口以及输入框,并将输入框的 textChanged(QString) 信号连接到中心窗口的 setText(QString) 槽函数上。
接着看看我们的wigglywidget.h:
~~~
#include <QBasicTimer>
#include <QWidget>
//! [0]
class WigglyWidget : public QWidget
{
Q_OBJECT
public:
WigglyWidget(QWidget *parent = 0);
public slots:
void setText(const QString &newText) { text = newText; } // 当输入框中文字变换该槽函数被执行,由于窗口每60毫秒刷新一次,因此无需调用update()
protected:
void paintEvent(QPaintEvent *event);
void timerEvent(QTimerEvent *event);
private:
QBasicTimer timer;
QString text;
int step;
};
~~~
QBasicTimer 是一个快速、轻量级的定时器类,适合在嵌入式系统等对性能要求较高的环境下使用。
它不像 QTimer 一样继承自 QObject,因此不具有使用信号槽的能力,
但我们可以使用它为 QObject 及其派生类对象重复发送 QTimerEvent 事件,通过重载 timerEvent(QTimerEvent*) 以捕获该事件。
wigglywidget.cpp:
~~~
#include <QtWidgets>
#include "wigglywidget.h"
//! [0]
WigglyWidget::WigglyWidget(QWidget *parent)
: QWidget(parent)
{
setBackgroundRole(QPalette::Midlight); // 设置背景色为亮白
setAutoFillBackground(true); // 记得设置该属性为true
QFont newFont = font(); // 然后获取该窗口的字体对象
newFont.setPointSize(newFont.pointSize() + 20); // 增大20号
setFont(newFont);
step = 0;
timer.start(60, this); // 开启定时器,间隔60毫秒
}
//! [0]
//! [1]
void WigglyWidget::paintEvent(QPaintEvent * /* event */)
//! [1] //! [2]
{
static const int sineTable[16] = { // 正弦波形的 y 值抽样数组
0, 38, 71, 92, 100, 92, 71, 38, 0, -38, -71, -92, -100, -92, -71, -38
};
QFontMetrics metrics(font()); // 使用当前窗体的字体初始化QFontMetrics
int x = (width() - metrics.width(text)) / 2; // 通过metrics得到显示文本的信息,使文本居中显示
int y = (height() + metrics.ascent() - metrics.descent()) / 2; // 关于ascent 与 descent,我们在下面解释
QColor color;
//! [2]
//! [3]
QPainter painter(this);
//! [3] //! [4]
for (int i = 0; i < text.size(); ++i) { // 获取text中字符个数并循环
int index = (step + i) % 16;
color.setHsv((15 - index) * 16, 255, 191); // 使用HSV设置每个字符的颜色值,三个参数分别代表色调,饱和度,亮度
painter.setPen(color);
painter.drawText(x, y - ((sineTable[index] * metrics.height()) / 400), // 400用来控制正弦波形的幅度,数值越大波动越小
QString(text[i]));
x += metrics.width(text[i]); // 以当前字符宽度增加 x 的值
}
}
//! [4]
//! [5]
void WigglyWidget::timerEvent(QTimerEvent *event)
//! [5] //! [6]
{
if (event->timerId() == timer.timerId()) { // 我们通过timerId来判断这个timerEvent是不是由我们定义的定时器触发
++step;
update(); // 刷新显示
} else {
QWidget::timerEvent(event); // 如果不是则将该事件继续下发
}
//! [6]
}
~~~
大家一定记得当我们使用Qt绘制图形时,类似drawRect()什么的,我们输入的位置坐标即是该图像左上角所在的位置,简直是指哪打哪~
但使用drawText()时呢,又好像不是这样了。似乎文本的原点变成了左下角,但是还是感觉差点意思?如果你对这点有些疑惑的话,可以看看下面这张图,没错,图中的Origin才是我们真正的文本原点。它是Baseline与文本左侧的交汇处。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd0add471.jpg)
有了这张图,Ascent与Descent也就很明显了,它们分别是文本最上方和最下方与Baseline之间的距离。
~~~
int y = (height() + metrics.ascent() - metrics.descent()) / 2;
~~~
那么这行代码也就很明显了,它得到的正是我们**纵向居中显示文本时Baseline在窗口上的y轴坐标。**
如果将这个算式转换成这样,可能就更一目了然了:
int y = metrics.ascent() + ( height() - metrics.ascent() - metrics.descent() ) / 2;
可能我们使用QFontMetrics时会更多的使用它的一些便利函数例如height()之类,
而实际上height()正是等于ascent()+descent()+1,最后的1即是Baseline的宽度。
好了,伴随着跳动的字符,就先到这里吧O(∩_∩)O~
35——Music Player
最后更新于:2022-04-01 07:22:24
本系列所有文章可以在这里查看[http://blog.csdn.net/cloud_castle/article/category/2123873](http://blog.csdn.net/cloud_castle/article/category/2123873)
接上文[](http://blog.csdn.net/cloud_castle/article/details/39251931)[](http://blog.csdn.net/cloud_castle/article/details/39291069)[Qt5官方demo解析集34——Concentric Circles Example](http://blog.csdn.net/cloud_castle/article/details/43490767)
光看标题大家可能觉得我们今天会聊一聊 Qt 中 multimedia 模块的相关内容,确实,该 demo 基于这个模块实现了一个音乐播放器,不过呢,我们今天更侧重于该 demo 中 winextras 模块的使用。
从名字可以猜到,该模块可以用来为我们提供一些Windows平台上额外的扩展功能,例如DWM(Desktop Window Manager) 特效,Aero Peek,Taskbar,Jump Lists,Thumbnail Toolbar等等,Qt为我们封装了相关 API 使得它们变得更加简单易用。如果大家对这些名词感到陌生,可以前往Qt 官网查看更详细的介绍:http://doc.qt.io/qt-5/qtwinextras-overview.html 。或者,我给大家举几个身边的栗子:
不知道大家有没有留意过,当我们在使用 Qt Creator 进行构建时,其任务栏图标上的进度状态?
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd0a7049b.jpg)
或者当我们将鼠标左键放在QQ音乐任务栏图标上时,出现的上一曲、暂停、下一曲这些预览窗口按钮:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd0a821e3.jpg)
亦或是,右键点击QQ音乐出现的最近收听列表:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd0a95b0c.jpg)
等等之类,我就不一一列举了,站在GUI的角度来说,这些东西绝不是可有可无的
细节决定成败,用户总是能够在一些小的细节上收获惊喜和感动。
那么,看看我们如何在 Qt 中使用这些贴心的小玩意儿。
记得在pro文件中添加
~~~
QT += winextras
~~~
然后看看main.cpp,这里面有个实用的关联文件格式的helper函数:
**[cpp]** [view plain](http://blog.csdn.net/cloud_castle/article/details/43672509# "view plain")[copy](http://blog.csdn.net/cloud_castle/article/details/43672509# "copy")[![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-10-13_561c56d3593bb.png)](https://code.csdn.net/snippets/605094 "在CODE上查看代码片")[![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-10-13_561c56d363c1b.svg)](https://code.csdn.net/snippets/605094/fork "派生到我的代码片")
~~~
#include "musicplayer.h"
#include <QApplication>
#include <QFileInfo>
#include <QSettings>
#include <QIcon>
#include <QDir>
//! [0]
static void associateFileTypes(const QStringList &fileTypes) // 这是一个helper函数,用来将某文件格式与本程序关联
{
QString displayName = QGuiApplication::applicationDisplayName();
QString filePath = QCoreApplication::applicationFilePath();
QString fileName = QFileInfo(filePath).fileName();
QSettings settings("HKEY_CURRENT_USER\\Software\\Classes\\Applications\\" + fileName, QSettings::NativeFormat);
settings.setValue("FriendlyAppName", displayName);
settings.beginGroup("SupportedTypes");
foreach (const QString& fileType, fileTypes)
settings.setValue(fileType, QString());
settings.endGroup();
settings.beginGroup("shell");
settings.beginGroup("open");
settings.setValue("FriendlyAppName", displayName);
settings.beginGroup("Command");
settings.setValue(".", QChar('"') + QDir::toNativeSeparators(filePath) + QString("\" \"%1\""));
}
//! [0]
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
app.setApplicationName("MusicPlayer");
app.setOrganizationName("QtWinExtras");
app.setOrganizationDomain("qt-project.org");
app.setApplicationDisplayName("QtWinExtras Music Player");
app.setWindowIcon(QIcon(":/logo.png"));
associateFileTypes(QStringList(".mp3")); // helper函数的使用方式
MusicPlayer player;
const QStringList arguments = QCoreApplication::arguments();
if (arguments.size() > 1) // 如果打开参数包含文件名
player.playFile(arguments.at(1)); // 则开始播放第一首
player.resize(300, 60);
player.show();
return app.exec();
}
~~~
VolumeButton 类继承自QToolButton,使用 Qt 中的标准音量图像设置为自身图标,
~~~
setIcon(style()->standardIcon(QStyle::SP_MediaVolume));
~~~
并提供了一个可以调节音量的弹出菜单,并根据DWM的混合状态决定自身的显示状态,代码都很容易理解,就不贴出来了。
在MusicPlayer类中,首先记得
~~~
#include <QtWinExtras>
~~~
然后我们通过下面的函数来创建Jump Lists:
~~~
void MusicPlayer::createJumpList()
{
QWinJumpList jumplist;
jumplist.recent()->setVisible(true);
}
~~~
Taskbar的创建与Progress类似,我们将其与窗口上的进度条关联起来:
~~~
void MusicPlayer::createTaskbar()
{
taskbarButton = new QWinTaskbarButton(this);
taskbarButton->setWindow(windowHandle()); // 使用窗口句柄作为参数
taskbarProgress = taskbarButton->progress();
connect(positionSlider, SIGNAL(valueChanged(int)), taskbarProgress, SLOT(setValue(int)));
connect(positionSlider, SIGNAL(rangeChanged(int,int)), taskbarProgress, SLOT(setRange(int,int)));
connect(&mediaPlayer, SIGNAL(stateChanged(QMediaPlayer::State)), this, SLOT(updateTaskbar()));
}
~~~
要创建预览窗口按钮,首先需要创建一个QWinThumbnailToolBar,然后在上面添加按钮:
~~~
void MusicPlayer::createThumbnailToolBar()
{
thumbnailToolBar = new QWinThumbnailToolBar(this);
thumbnailToolBar->setWindow(windowHandle());
playToolButton = new QWinThumbnailToolButton(thumbnailToolBar);
playToolButton->setEnabled(false);
playToolButton->setToolTip(tr("Play"));
playToolButton->setIcon(style()->standardIcon(QStyle::SP_MediaPlay));
connect(playToolButton, SIGNAL(clicked()), this, SLOT(togglePlayback()));
forwardToolButton = new QWinThumbnailToolButton(thumbnailToolBar);
forwardToolButton->setEnabled(false);
forwardToolButton->setToolTip(tr("Fast forward"));
forwardToolButton->setIcon(style()->standardIcon(QStyle::SP_MediaSeekForward));
connect(forwardToolButton, SIGNAL(clicked()), this, SLOT(seekForward()));
backwardToolButton = new QWinThumbnailToolButton(thumbnailToolBar);
backwardToolButton->setEnabled(false);
backwardToolButton->setToolTip(tr("Rewind"));
backwardToolButton->setIcon(style()->standardIcon(QStyle::SP_MediaSeekBackward));
connect(backwardToolButton, SIGNAL(clicked()), this, SLOT(seekBackward()));
thumbnailToolBar->addButton(backwardToolButton);
thumbnailToolBar->addButton(playToolButton);
thumbnailToolBar->addButton(forwardToolButton);
connect(&mediaPlayer, SIGNAL(positionChanged(qint64)), this, SLOT(updateThumbnailToolBar()));
connect(&mediaPlayer, SIGNAL(durationChanged(qint64)), this, SLOT(updateThumbnailToolBar()));
connect(&mediaPlayer, SIGNAL(stateChanged(QMediaPlayer::State)), this, SLOT(updateThumbnailToolBar()));
}
~~~
最后我们看看程序运行效果:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd0aa9e81.jpg)
在Win7下当我们暂停播放时,任务栏会出现一个暂停样式的图标,而在Win8中则表现为绿色的进度条变成了黄色:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd0abd45f.jpg)
34——Concentric Circles Example
最后更新于:2022-04-01 07:22:22
本系列所有文章可以在这里查看[http://blog.csdn.net/cloud_castle/article/category/2123873](http://blog.csdn.net/cloud_castle/article/category/2123873)
接上文[](http://blog.csdn.net/cloud_castle/article/details/39251931)[Qt5官方demo解析集33——Qt Quick Examples - Window and Screen](http://blog.csdn.net/cloud_castle/article/details/39291069)
好像有一段时间没有更新这个系列了,一方面是很多的事掺杂着一起来了,稍微比原来忙了一些;但时间哪有挤不出来的呢,所以更重要的一个原因其实是很难找到一个特别合适的Demo来做这个主题。有的Demo内容很偏很少项目在做,有的Demo又过于基础,我总希望能挑出一些大家经常能接触得到又需要了解的知识点~
因为Qt的每一个官方Demo都有一个关注的点,每次把所有的代码都贴出来可能这个系列的重复部分会越来越多。。。所以从本文开始,我们只把关键代码挑出来浓墨重彩地介绍一下(*^__^*)
好了,废话少说,进入今天的正题吧:Concentric Circles Example
大家都知道在Qt里面绘制一个椭圆可以在paintEvent()中使用
~~~
painter.drawEllipse(QRect(-diameter / 2, -diameter / 2, diameter, diameter));
~~~
那么我们这样绘制多个同心圆的效果大概像下面这样:
(在放大镜效果下对Demo运行效果的截图)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd0a0b877.jpg)
首先让我们将这及其不圆润的边缘放在一边,细心的你可能会发现,这些同心圆总是两两之间挨得特别近,而我们明明是绘制的等距的同心圆?其实答案很简单,它就是对整型坐标值进行计算后四舍五入得到的结果。知道问题的来源后,解决起来就容易了。
Qt很贴心地为我们提供了另一个接受QRectF()参数的drawEllipse():
~~~
painter.drawEllipse(QRectF(-diameter / 2.0, -diameter / 2.0, diameter, diameter));
~~~
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd0a1adc7.jpg)
看看有没有好一点?一方面我们确实得到了等距的同心圆,另一方面也显得圆润了些,上图中那么显眼的直线也没那么长了。
如果想要进一步得到更加圆润的圆,我们可以设置painter的抗锯齿效果QPainter::Antialiasing,简单点说它是通过采样算法将在图形边缘会造成锯齿的像素与周围的像素作一个平均的运算,并增加像素的数目,这样来形成区域像素点的平滑过渡的效果。不过,这样做的副作用是图像会显得有些模糊。
drawEllipse(QRect()) + 抗锯齿:
~~~
painter.setRenderHint(QPainter::Antialiasing);
painter.drawEllipse(QRect(-diameter / 2, -diameter / 2, diameter, diameter));
~~~
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd0a272b8.jpg)
drawEllipse(QRectF()) + 抗锯齿:
~~~
painter.setRenderHint(QPainter::Antialiasing);
painter.drawEllipse(QRectF(-diameter / 2.0, -diameter / 2.0, diameter, diameter));
~~~
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd0a395e9.jpg)
最后我们还是看下整个程序的界面吧:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd0a4a920.jpg)
33——Qt Quick Examples – Window and Screen
最后更新于:2022-04-01 07:22:19
本系列所有文章可以在这里查看[http://blog.csdn.net/cloud_castle/article/category/2123873](http://blog.csdn.net/cloud_castle/article/category/2123873)
接上文[Qt5官方demo解析集32——Qt Quick Examples - Threading](http://blog.csdn.net/cloud_castle/article/details/39251931)
来到我们Qt Quick Examples的第二个例子了,之所以挑这个demo,主要是我们使用Qt开发界面(尤其是跨平台界面)时,本地屏幕信息与窗口调用是不可避免的课题。
这个例子便向我们展示了在QML中获取本地屏幕信息的方法。
项目树如图,其中shared.qrc是很多QML示例共享的一些资源,包括按钮,滑块等。
此外,为了良好的跨平台,demo中添加了两种格式的图标文件(MAC支持的icns, 以及其他平台通用的png)。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd09c8bba.jpg)
程序运行如图,我们可以调用一个子窗口,以窗口化或最大化形式显示。下方是我的笔记本屏幕的一些信息,包括分辨率,像素密度,方向等等。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd09daedc.jpg)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd09e7781.jpg)
由于使用到图标,我们有必要将pro文件拿出来看看:
window.pro:
~~~
TEMPLATE = app
QT += quick qml
SOURCES += main.cpp
RESOURCES += \
window.qrc \
../shared/shared.qrc
EXAMPLE_FILES = \
window.qml
target.path = $$[QT_INSTALL_EXAMPLES]/quick/window
INSTALLS += target
ICON = resources/icon64.png // 设置ICON 图标
macx: ICON = resources/icon.icns // MAC平台额外定义图标
win32: RC_FILE = resources/window.rc
~~~
main.cpp:
~~~
#include <QtGui/QGuiApplication> // 个人比较推荐的头文件包含方式
#include <QtQml/QQmlEngine>
#include <QtQml/QQmlComponent>
#include <QtQuick/QQuickWindow>
#include <QtCore/QUrl>
#include <QDebug>
int main(int argc, char* argv[])
{
QGuiApplication app(argc, argv);
QQmlEngine engine; // 注意到在5.3以后我们大多使用QQmlApplicationEngine来加载qml文件 // 它实际也就是封装了这里QQmlEngine + QQmlComponent,并提供了一些其他的便利函数
QQmlComponent component(&engine);
QQuickWindow::setDefaultAlphaBuffer(true); // 如果我们需要应用透明窗体,需要在第一个QQuickWindow出现前设置该函数为true
component.loadUrl(QUrl("qrc:///window/window.qml"));
if ( component.isReady() )
component.create();
else
qWarning() << component.errorString();
return app.exec();
}
~~~
我们随着加载的顺序来看第一个qml文件
window.qml:
~~~
import QtQuick 2.0
import QtQuick.Window 2.1
import "../shared" as Shared
QtObject { // 非可视化的轻量顶层对象
property real defaultSpacing: 10 // 各对象间隔
property SystemPalette palette: SystemPalette { } // SystemPalette可以很方便地提供本地化的控件样式
property var controlWindow: Window { // 主窗口
width: visibilityLabel.implicitWidth * 1.2
height: col.implicitHeight + defaultSpacing * 2
color: palette.window
title: "Control Window"
Column {
id: col
anchors.fill: parent
anchors.margins: defaultSpacing
spacing: defaultSpacing
property real cellWidth: col.width / 3 - spacing // Grid 单元格宽度
Text { text: "Control the second window:" }
Grid {
id: grid
columns: 3
spacing: defaultSpacing
width: parent.width
Shared.Button { // 来自Shared 控件包的Button
id: showButton
width: col.cellWidth
text: testWindow.visible ? "Hide" : "Show"
onClicked: testWindow.visible = !testWindow.visible
}
//! [windowedCheckbox]
Shared.CheckBox { // CheckBox
text: "Windowed"
height: showButton.height
width: col.cellWidth
Binding on checked { value: testWindow.visibility === Window.Windowed } // 注意Binding是一个QML类型,而不是关键字。这个语句等于Binding{target: checked; value: testWindow.visibility === Window.Windowed}。相对于checked: XXX 的属性绑定方式,这种方式更适用于var 类型的属性绑定。因此对于bool 型的checked 而言,checked: testWindow.visibility === Window.Windowed 也可行
onClicked: testWindow.visibility = Window.Windowed
}
//! [windowedCheckbox]
Shared.CheckBox {
height: showButton.height
width: col.cellWidth
text: "Full Screen"
Binding on checked { value: testWindow.visibility === Window.FullScreen }
onClicked: testWindow.visibility = Window.FullScreen
}
Shared.Button { // Window.AutomaticVisibility 根据所在平台调整显示方式,比如在Windows 上为窗口,Android 则为全屏
id: autoButton
width: col.cellWidth
text: "Automatic"
onClicked: testWindow.visibility = Window.AutomaticVisibility
}
Shared.CheckBox {
height: autoButton.height
text: "Minimized"
Binding on checked { value: testWindow.visibility === Window.Minimized }
onClicked: testWindow.visibility = Window.Minimized
}
Shared.CheckBox {
height: autoButton.height
text: "Maximized"
Binding on checked { value: testWindow.visibility === Window.Maximized }
onClicked: testWindow.visibility = Window.Maximized
}
}
function visibilityToString(v) { // 状态转换String 函数
switch (v) {
case Window.Windowed:
return "windowed";
case Window.Minimized:
return "minimized";
case Window.Maximized:
return "maximized";
case Window.FullScreen:
return "fullscreen";
case Window.AutomaticVisibility:
return "automatic";
case Window.Hidden:
return "hidden";
}
return "unknown";
}
Text {
id: visibilityLabel
text: "second window is " + (testWindow.visible ? "visible" : "invisible") +
" and has visibility " + parent.visibilityToString(testWindow.visibility)
}
Rectangle {
id: horizontalRule
color: "black"
width: parent.width
height: 1
}
ScreenInfo { } // 屏幕信息获取,实现代码在下方
}
} // !controlWindow
property var testWindow: Window { // 子窗口
width: 320
height: 240
color: "#215400" // 窗口背景色,但我们看到的颜色只在周围一圈,是因为中间为Rectangle,那一圈为间隔defaultSpacing
title: "Test Window with color " + color
flags: Qt.Window | Qt.WindowFullscreenButtonHint
Rectangle {
anchors.fill: parent
anchors.margins: defaultSpacing // 矩形与窗口间隔
Text {
anchors.centerIn: parent
text: "Second Window"
}
MouseArea { // 点击切换颜色
anchors.fill: parent
onClicked: testWindow.color = "#e0c31e"
}
Shared.Button {
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: defaultSpacing
text: testWindow.visibility === Window.FullScreen ? "exit fullscreen" : "go fullscreen"
width: 150
onClicked: {
if (testWindow.visibility === Window.FullScreen)
testWindow.visibility = Window.AutomaticVisibility
else
testWindow.visibility = Window.FullScreen
}
}
Shared.Button {
anchors.left: parent.left
anchors.top: parent.top
anchors.margins: defaultSpacing
text: "X"
width: 30
onClicked: testWindow.visible = false // 窗口隐藏
}
}
} // !testWindow
property var splashWindow: Splash { // 启动窗口
onTimeout: controlWindow.visible = true // 自定义信号处理函数,显示主窗口
}
}
~~~
为了在QML获取屏幕信息,Qt为我们提供了Screen类型,它可以作为其他可视的QML类型的附加对象,指向该对象所显示的设备:
ScreenInfo.qml:
~~~
import QtQuick 2.1
import QtQuick.Window 2.1
Item {
id: root
width: 400
height: propertyGrid.implicitHeight + 16
function orientationToString(o) { // 状态转string 函数
switch (o) {
case Qt.PrimaryOrientation:
return "primary";
case Qt.PortraitOrientation:
return "portrait";
case Qt.LandscapeOrientation:
return "landscape";
case Qt.InvertedPortraitOrientation:
return "inverted portrait";
case Qt.InvertedLandscapeOrientation:
return "inverted landscape";
}
return "unknown";
}
Grid {
id: propertyGrid
columns: 2
spacing: 8
x: spacing
y: spacing
//! [screen]
Text {
text: "Screen \"" + Screen.name + "\":" // 显示设备名称
font.bold: true
}
Item { width: 1; height: 1 } // 设备名称右方的占位器
Text { text: "dimensions" } // 分辨率
Text { text: Screen.width + "x" + Screen.height }
Text { text: "pixel density" } // 像素密度
Text { text: Screen.pixelDensity.toFixed(2) + " dots/mm (" + (Screen.pixelDensity * 25.4).toFixed(2) + " dots/inch)" }
Text { text: "logical pixel density" } // 逻辑像素密度
Text { text: Screen.logicalPixelDensity.toFixed(2) + " dots/mm (" + (Screen.logicalPixelDensity * 25.4).toFixed(2) + " dots/inch)" }
Text { text: "available virtual desktop" } // 可用虚拟桌面,比分辨率的高度少了30 是因为Windows底下的状态栏
Text { text: Screen.desktopAvailableWidth + "x" + Screen.desktopAvailableHeight }
Text { text: "orientation" } // 屏幕方向
Text { text: orientationToString(Screen.orientation) + " (" + Screen.orientation + ")" }
Text { text: "primary orientation" } // 优先方向
Text { text: orientationToString(Screen.primaryOrientation) + " (" + Screen.primaryOrientation + ")" }
//! [screen]
}
}
~~~
最后是启动画面的实现
Splash.qml:
~~~
import QtQuick 2.0
import QtQuick.Window 2.1
//! [splash-properties]
Window {
id: splash
color: "transparent"
title: "Splash Window"
modality: Qt.ApplicationModal // 应用窗口模式
flags: Qt.SplashScreen // 启动画面
property int timeoutInterval: 2000
signal timeout // 自定义溢出信号
//! [splash-properties]
//! [screen-properties]
x: (Screen.width - splashImage.width) / 2 // 居中
y: (Screen.height - splashImage.height) / 2
//! [screen-properties]
width: splashImage.width // 窗口大小与图片大小一致
height: splashImage.height
Image {
id: splashImage
source: "../shared/images/qt-logo.png"
MouseArea {
anchors.fill: parent
onClicked: Qt.quit() // 点击退出
}
}
//! [timer]
Timer {
interval: timeoutInterval; running: true; repeat: false
onTriggered: {
visible = false // 2秒后隐藏,并发出timeout信号,以显示主窗口
splash.timeout()
}
}
//! [timer]
Component.onCompleted: visible = true
}
~~~
32——Qt Quick Examples – Threading
最后更新于:2022-04-01 07:22:17
本系列所有文章可以在这里查看[http://blog.csdn.net/cloud_castle/article/category/2123873](http://blog.csdn.net/cloud_castle/article/category/2123873)
接上文[Qt5官方demo解析集31——StocQt](http://blog.csdn.net/cloud_castle/article/details/38258159)
因为涉及到QML线程看到这个例子,发现它是属于Qt Quick Example这个系列的。这个系列共有19个demo,涵盖了Qt Quick中多种元素,有空我们把这个系列一篇篇做下来,相信是一趟不错的旅途~
好了,在我们编写应用程序,尤其是用户界面程序,多线程往往是避不开的一个话题。界面卡死超过3秒,估计许多用户就打算Ctrl+Alt+Delete了...
那么Qt当然也为我们编写多线程提供了便利,在Widgets中呢我们通常使用QThread来完成多线程,那对于更强调用户体验的QML而言呢,就不得不提到WorkerScript了。
WorkScript 包含了一个属性source来声明一个js文件,用来开辟一个新的线程以处理那些耗时的计算。更详细的介绍,大家可以查看Manual。
程序运行起来是我们熟悉的选择界面:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd097f3c8.jpg)
1)Threaded ListModel
这个例子比较简单,介绍了如何使用WorkScript 在一个新的线程中更新ListModel。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd098b686.jpg)
程序由两个文件构成:
timedisplay.qml:
~~~
import QtQuick 2.0
Rectangle {
color: "white"
width: 200
height: 300
ListView {
anchors.fill: parent
model: listModel
delegate: Component {
Text { text: time }
}
ListModel { id: listModel }
WorkerScript {
id: worker
source: "dataloader.js" // 声明js处理函数
}
// ![0]
Timer { // 2秒间隔定时器
id: timer
interval: 2000; repeat: true
running: true
triggeredOnStart: true
onTriggered: {
var msg = {'action': 'appendCurrentTime', 'model': listModel}; // 这里将ListModel对象作为sendMessage()的参数,除此以外,其他QObject对象类型都不被允许
worker.sendMessage(msg); // 调用sendMessage()运行WorkerScript的计算
}
}
// ![0]
}
}
~~~
dataloader.js:
~~~
// ![0]
WorkerScript.onMessage = function(msg) { // 处理函数的格式如下WorkerScript.onMessage = function()....
if (msg.action == 'appendCurrentTime') {
var data = {'time': new Date().toTimeString()};
msg.model.append(data); // 注意到这里的model由msg参数声明
msg.model.sync(); // 该函数专为WorkScript而生,用来保存WorkScript对ListModel的修改
}
}
// ![0]
~~~
以上就是一个基本的WorkScript的实现,下面来看一个稍微复杂一点的例子:
2)WorderScript
这个例子使用了多线程在后台计算帕斯卡三角形(Pascal's Triangle),并且有意采用了一种并非很优化的算法(通过二项式好像可以直接求,我不太记得了),使得我们可以体会到后台计算而界面依然保持响应的效果。
至于什么是Pascal's Triangle,我放张图想必大家不会陌生,其实它就是我们说的杨辉三角:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd099a1e6.jpg)
也就是说,每个小方格的值等于其上面两个方格数值之和。
而在我们这个程序中,方格的层数为64层,如果是使用这种累加的方法到得到任意一个方格的值,自然是一个比较费时的操作。
先看下运行效果:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd09b6071.jpg)
可以看到,当我选择了第54行,41列的方格时,下方的结果变成了"Loading...",但界面并没有卡死,我们依然可以后退,或是选择新的方格。
workerscript.qml:
~~~
import QtQuick 2.0
Rectangle {
width: 320; height: 480
//! [1]
WorkerScript {
id: myWorker
source: "workerscript.js"
onMessage: { // 注意到这个的onMessage与js中的onMessage不同,这里用来更新界面显示,而非执行计算,类似信号槽,响应js中的sendMessage
if (messageObject.row == rowSpinner.value && messageObject.column == columnSpinner.value){ //Not an old result
if (messageObject.result == -1)
resultText.text = "Column must be <= Row";
else
resultText.text = messageObject.result;
}
}
}
//! [1]
Row {
y: 24
spacing: 24
anchors.horizontalCenter: parent.horizontalCenter
//! [0]
Spinner { // 自定义的Spinner控件
id: rowSpinner
label: "Row"
onValueChanged: { // 值改变调用线程函数
resultText.text = "Loading...";
myWorker.sendMessage( { row: rowSpinner.value, column: columnSpinner.value } );
}
}
//! [0]
Spinner {
id: columnSpinner
label: "Column"
onValueChanged: {
resultText.text = "Loading...";
myWorker.sendMessage( { row: rowSpinner.value, column: columnSpinner.value } );
}
}
}
Text {
id: resultText
y: 180
width: parent.width
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
font.pixelSize: 32
}
Text {
text: "Pascal's Triangle Calculator"
anchors { horizontalCenter: parent.horizontalCenter; bottom: parent.bottom; bottomMargin: 50 }
}
}
~~~
workerscript.js:
~~~
//Will be initialized when WorkerScript{} is instantiated
var cache = new Array(64); // 64x64 的二维数组
for (var i = 0; i < 64; i++)
cache[i] = new Array(64);
function triangle(row, column) { // 定义函数triangle行列的合法性
if (cache[row][column]) // 如果缓存中计算过该方格的值,直接返回
return cache[row][column]
if (column < 0 || column > row) // 行数必须大于等于列数
return -1;
if (column == 0 || column == row) // 左右两侧值均为 1
return 1;
return triangle(row-1, column-1) + triangle(row-1, column); // 否则递归调用该函数计算方格数值
}
//! [0]
WorkerScript.onMessage = function(message) {
//Calculate result (may take a while, using a naive algorithm) // 原注:可能会花点时间噢,因为使用了一个天真的算法~
var calculatedResult = triangle(message.row, message.column);
//Send result back to main thread
WorkerScript.sendMessage( { row: message.row, // 类似信号槽,返回数据在WorkerScript中的onMessage中响应
column: message.column,
result: calculatedResult} );
}
//! [0]
~~~
最后贴出Spinner的实现,因为Qt Quick Controls似乎没有提供类似控件,如果要设计类似的控件可以参考~:
Spinner.qml:
~~~
import QtQuick 2.0
Rectangle {
width: 64
height: 64
property alias value: list.currentIndex
property alias label: caption.text
Text {
id: caption
text: "Spinner"
anchors.horizontalCenter: parent.horizontalCenter
}
Rectangle {
anchors.top: caption.bottom
anchors.topMargin: 4
anchors.horizontalCenter: parent.horizontalCenter
height: 48
width: 32
color: "black"
ListView {
id: list
anchors.fill: parent
highlightRangeMode: ListView.StrictlyEnforceRange
preferredHighlightBegin: height/3
preferredHighlightEnd: height/3
clip: true
model: 64
delegate: Text {
font.pixelSize: 18;
color: "white";
text: index;
anchors.horizontalCenter: parent.horizontalCenter
}
}
Rectangle {
anchors.fill: parent
gradient: Gradient {
GradientStop { position: 0.0; color: "#FF000000" }
GradientStop { position: 0.2; color: "#00000000" }
GradientStop { position: 0.8; color: "#00000000" }
GradientStop { position: 1.0; color: "#FF000000" }
}
}
}
}
~~~
31——StocQt
最后更新于:2022-04-01 07:22:15
本系列所有文章可以在这里查看[http://blog.csdn.net/cloud_castle/article/category/2123873](http://blog.csdn.net/cloud_castle/article/category/2123873)
接上文[Qt5官方demo解析集30——Extending QML - Binding Example](http://blog.csdn.net/cloud_castle/article/details/37534507)
最近在做QML制表,因此想找一些相关曲线绘制的demo看看,结果发现了这个例子,觉得挺不错,它相比于我们之前的Extend和Particle系列显得更大一些,涉及到的面也更广一些。因此想拿过来给大家分享~
这个例子是基于QML的股票走势图绘制,数据来源于yahoo的纳达克斯-100指数,向左滑动可以选择股票。
曲线页面:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd092e045.jpg)
列表页面:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd094a46e.jpg)
工程目录如下:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd09612bf.jpg)
这个例子的结构会稍微复杂一些,我建议大家在QtCreator中将这个例子打开,这样有什么想法或是疑虑时我们可以将代码改改,或是添加一些输出信息,举一反三的学习总是比较有效的。
ok,我们自顶而下的跟着调用的顺序来:
stocqt.qml:
~~~
import QtQuick 2.0
import QtQml.Models 2.1
import "./content" // 添加其他qml文件路径
Rectangle {
id: mainRect
width: 1000
height: 700
property int listViewActive: 0 // 该页面实际是一个ListView的一部分,这个属性用来表明前面的列表页面是否被激活
Rectangle { // 导航栏
id: banner
height: 80
anchors.top: parent.top
width: parent.width
color: "#000000"
Image {
id: arrow
source: "./content/images/icon-left-arrow.png"
anchors.left: banner.left
anchors.leftMargin: 20
anchors.verticalCenter: banner.verticalCenter
visible: root.currentIndex == 1 ? true : false // 曲线页面是ListView的第二个页面,currentIndex的值也就是1
MouseArea {
anchors.fill: parent
onClicked: listViewActive = 1;
}
}
Item { // 将相关组件放在一个Item容器中
id: textItem
width: stocText.width + qtText.width // 容器的尺寸由组件决定
height: stocText.height + qtText.height
anchors.horizontalCenter: banner.horizontalCenter
anchors.verticalCenter: banner.verticalCenter
Text { // Stoc
id: stocText
anchors.verticalCenter: textItem.verticalCenter
color: "#ffffff"
font.family: "Abel"
font.pointSize: 40
text: "Stoc"
}
Text { // Qt
id: qtText
anchors.verticalCenter: textItem.verticalCenter
anchors.left: stocText.right
color: "#5caa15"
font.family: "Abel"
font.pointSize: 40
text: "Qt"
}
}
}
ListView { // 标题栏下方则是ListView的内容,它为列表页面与曲线页面提供了滑动切换的能力
id: root
width: parent.width
anchors.top: banner.bottom
anchors.bottom: parent.bottom
snapMode: ListView.SnapOneItem // 设置该属性使View停止在一个完整的页面上
highlightRangeMode: ListView.StrictlyEnforceRange
highlightMoveDuration: 250 // 这里设置了一个无形的高亮,它不显示,但提供了0.25秒的切换动画
focus: false
orientation: ListView.Horizontal
boundsBehavior: Flickable.StopAtBounds // 滑动时停在边界,默认可以拉拽更远并在弹回时带有超调效果
currentIndex: listViewActive == 0 ? 1 : 0 // 使用属性绑定,当listViewActive被激活时,使currentIndex置0,从而跳转到列表页面
onCurrentIndexChanged: {
if (currentIndex == 1)
listViewActive = 0;
}
StockModel { // 初始化数据模型
id: stock
stockId: listView.currentStockId // 注意这个listView不是其父ListView,而是下面那个,即列表页面
stockName: listView.currentStockName // 列表页面将当前所选的股票ID及名字赋给StockModel,使其取得相应的数据
onStockIdChanged: stock.updateStock(); // 当ID改变时数据更新
onDataReady: {
console.log(listView.currentStockId, listView.currentStockName)
root.positionViewAtIndex(1, ListView.SnapPosition) // 上面的高亮即为这里的ListView.SnapPosition做准备,数据变更后切换到曲线页面,带有0.25秒的过渡动画
stockView.update() // 页面更新
}
}
model: ObjectModel { // ObjectModel源自上面import的Qt.QML.Model 2.1,它使得ListView可以使用一组对象作为模型
StockListView { // 列表页面
id: listView
width: root.width
height: root.height
}
StockView { // 曲线页面
id: stockView
width: root.width
height: root.height
stocklist: listView
stock: stock
}
}
}
}
~~~
有人说数据是一个程序骨架,那就看看这个架子吧~
StockModel.qml:
~~~
import QtQuick 2.0
ListModel { // ListModel作为根项目,自定义属性作为接口,并定义了多个函数。但他本身在初始化时并不进行运算
id: model
property string stockId: "" // 股票ID
property string stockName: "" // 股票名
property string stockDataCycle: "d" // 数据周期
property bool ready: false // 标志位
property real stockPrice: 0.0 // 股票价格
property real stockPriceChanged: 0.0 // 价格变化
signal dataReady // 耗时的数据类通常需要定义这个信号
function indexOf(date) { // 返回从特定date的数据在数据集中的位置
var newest = new Date(model.get(0).date); // 获取第一个数据对象的日期
var oldest = new Date(model.get(model.count - 1).date); // 最后一个数据对象的日期
if (newest <= date)
return -1; // 在最新日期之后直接返回
if (oldest >= date)
return model.count - 1; // 在最先日期之前全部返回
var currDiff = 0;
var bestDiff = Math.abs(date.getTime() - newest.getTime());
var retval = 0; // 返回变量
for (var i = 0; i < model.count; i++) {
var d = new Date(model.get(i).date);
currDiff = Math.abs(d.getTime() - date.getTime()); // 计算时间差值
if (currDiff < bestDiff) { // 从最新时间向目标时间推进
bestDiff = currDiff;
retval = i; // retval记录数据位置
}
if (currDiff > bestDiff) // 当达到目标时间后
return retval; // 将数据位置返回
}
return -1;
}
function requestUrl() { // 创建请求数据的url字符串函数
if (stockId === "")
return;
var startDate = new Date(2011, 4, 25); // 指定一个开始时间
var endDate = new Date(); // 结束时间为当前时间
if (stockDataCycle !== "d" && stockDataCycle !== "w" && stockDataCycle !== "m")
stockDataCycle = "d"; // 如果数据周期不是'天'、'周'、'月',则定义为'天'
/* // 注释给出了向yahoo请求数据的格式
Fetch stock data from yahoo finance:
url: http://ichart.finance.yahoo.com/table.csv?s=NOK&a=5&b=11&c=2010&d=7&e=23&f=2010&g=d&ignore=.csv
s:stock name/id, a:start day, b:start month, c:start year default: 25 April 1995, oldest c= 1962
d:end day, e:end month, f:end year, default:today (data only available 3 days before today)
g:data cycle(d daily, w weekly, m monthly, v Dividend)
*/
var request = "http://ichart.finance.yahoo.com/table.csv?";
request += "s=" + stockId;
request += "&a=" + startDate.getMonth();
request += "&b=" + startDate.getDate();
request += "&c=" + startDate.getFullYear();
request += "&d=" + endDate.getMonth();
request += "&e=" + endDate.getDate();
request += "&f=" + endDate.getFullYear();
request += "&g=" + stockDataCycle;
request += "&ignore=.csv";
return request; // 返回这一长串url
}
function createStockPrice(r) { // 存储数据对象函数
return { // 用来接收下面分离的7位数据,以类似结构体的形式存储下来
"date": r[0], // 这也是该model真正存储的数据类型格式
"open":r[1],
"high":r[2],
"low":r[3],
"close":r[4],
"volume":r[5],
"adjusted":r[6]
};
}
function updateStock() { // 数据更新
var req = requestUrl(); // 得到请求数据的url字符串
if (!req)
return;
var xhr = new XMLHttpRequest; // 创建一个XMLHttp的请求对象
xhr.open("GET", req, true); // 初始化请求参数,还未发送请求
model.ready = false; // 标志位置false
model.clear(); // 数据清空
var i = 1; // 输出一下调试信息可知,返回的数据第一行为描述符,因此将其跳过
xhr.onreadystatechange = function() { // readyState是XMLHttpRequest的一个属性,其值从0变化到4
if (xhr.readyState === XMLHttpRequest.LOADING || xhr.readyState === XMLHttpRequest.DONE) {
var records = xhr.responseText.split('\n'); // LOADING为3,DONE为4,分别表示数据正在载入和载入完成
// 以换行符分割数据
for (;i < records.length; i++ ) {
var r = records[i].split(','); // 以逗号将数据分割
if (r.length === 7) // 数据校验
model.append(createStockPrice(r)); // 函数调用,向model中添加数据
}
if (xhr.readyState === XMLHttpRequest.DONE) {
if (model.count > 0) {
model.ready = true;
model.stockPrice = model.get(0).adjusted; // 将最新的的调整收盘价赋予stockPrice
model.stockPriceChanged = model.count > 1 ? (Math.round((model.stockPrice - model.get(1).close) * 100) / 100) : 0; // 相比前一天的收盘价变化率
model.dataReady(); //emit signal
}
}
}
}
xhr.send() // 实际发出数据请求
}
}
~~~
我们将records的数据部分贴出来:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd096d1a2.jpg)
实际的数据被'\n'分开,也就是说,类似下面这个样子:
Data,Open,High,Low,Close,Volume,Adj Close,
2014-7-28,97.82,99.24,97.55,99.02,55239000,99.02,
2014-7-25 ......(7-26,7-27?周末休市啦......)
为了得到各股的变化率等,StockListView也采取了类似的实现方式:
StockListView.qml:
~~~
import QtQuick 2.0
Rectangle {
id: root
width: 320
height: 410
anchors.top: parent.top
anchors.bottom: parent.bottom
color: "white"
property string currentStockId: ""
property string currentStockName: ""
ListView {
id: view
anchors.fill: parent
width: parent.width
clip: true // clip以延时加载数据
keyNavigationWraps: true
highlightMoveDuration: 0
focus: true
snapMode: ListView.SnapToItem
model: StockListModel{} // 定义model
function requestUrl(stockId) { // 最近5天的url创建函数,与StockModel不同的是,由于未定义stockId属性,它带有这样一个参数
var endDate = new Date(""); // today
var startDate = new Date()
startDate.setDate(startDate.getDate() - 5);
var request = "http://ichart.finance.yahoo.com/table.csv?";
request += "s=" + stockId;
request += "&g=d";
request += "&a=" + startDate.getMonth();
request += "&b=" + startDate.getDate();
request += "&c=" + startDate.getFullYear();
request += "&d=" + endDate.getMonth();
request += "&e=" + endDate.getDate();
request += "&f=" + endDate.getFullYear();
request += "&g=d";
request += "&ignore=.csv";
return request;
}
function getCloseValue(index) {
var req = requestUrl(model.get(index).stockId); // 得到对应的股票Id
if (!req)
return;
var xhr = new XMLHttpRequest;
xhr.open("GET", req, true);
xhr.onreadystatechange = function() {
if (xhr.readyState === XMLHttpRequest.LOADING || xhr.readyState === XMLHttpRequest.DONE) {
var records = xhr.responseText.split('\n');
if (records.length > 0) {
var r = records[1].split(','); // 第一条数据,即最新一天的数据
model.setProperty(index, "value", r[4]); // 与StockModel类似,第五个数据为'Close',即收盘价
// 这里将model中index位置数据的"value"值设置为该收盘价 // 注意这个model是StockListModel而不是StockModel
var today = parseFloat(r[4]); // parseFloat()将字符串转化成浮点数,变量声明为var而不是real
r = records[2].split(','); // 再取前一天数据
var yesterday = parseFloat(r[4]);
var change = today - yesterday; // 计算变化值
if (change >= 0.0)
model.setProperty(index, "change", "+" + change.toFixed(2)); // 同样对model赋值大于零则带+号,保留两位小数
else
model.setProperty(index, "change", change.toFixed(2));
var changePercentage = (change / yesterday) * 100.0; // 变化率百分比
if (changePercentage >= 0.0)
model.setProperty(index, "changePercentage", "+" + changePercentage.toFixed(2) + "%");
else
model.setProperty(index, "changePercentage", changePercentage.toFixed(2) + "%");
}
}
}
xhr.send() // 发送请求
}
onCurrentIndexChanged: { // 当该ListView中的某个项目被选中
mainRect.listViewActive = 0; // 切换主ListView的页面
root.currentStockId = model.get(currentIndex).stockId; // 获取 Id 与 name
root.currentStockName = model.get(currentIndex).name;
}
delegate: Rectangle { // 委托组件,基本都是布局,不多说了
height: 102
width: parent.width
color: "transparent"
MouseArea {
anchors.fill: parent;
onClicked: {
view.currentIndex = index;
}
}
Text {
id: stockIdText
anchors.top: parent.top
anchors.topMargin: 15
anchors.left: parent.left
anchors.leftMargin: 15
width: 125
height: 40
color: "#000000"
font.family: "Open Sans" // 我的机器貌似不支持这种字体
font.pointSize: 20
font.weight: Font.Bold
verticalAlignment: Text.AlignVCenter
text: stockId
}
Text {
id: stockValueText
anchors.top: parent.top
anchors.topMargin: 15
anchors.right: parent.right
anchors.rightMargin: 0.31 * parent.width
width: 190
height: 40
color: "#000000"
font.family: "Open Sans"
font.pointSize: 20
font.bold: true
horizontalAlignment: Text.AlignRight
verticalAlignment: Text.AlignVCenter
text: value
Component.onCompleted: view.getCloseValue(index);
}
Text {
id: stockValueChangeText
anchors.top: parent.top
anchors.topMargin: 15
anchors.right: parent.right
anchors.rightMargin: 20
width: 135
height: 40
color: "#328930"
font.family: "Open Sans"
font.pointSize: 20
font.bold: true
horizontalAlignment: Text.AlignRight
verticalAlignment: Text.AlignVCenter
text: change
onTextChanged: {
if (parseFloat(text) >= 0.0) // 正为绿色,负为红色
color = "#328930";
else
color = "#d40000";
}
}
Text {
id: stockNameText
anchors.top: stockIdText.bottom
anchors.left: parent.left
anchors.leftMargin: 15
width: 330
height: 30
color: "#000000"
font.family: "Open Sans"
font.pointSize: 16
font.bold: false
elide: Text.ElideRight
maximumLineCount: 1
verticalAlignment: Text.AlignVCenter
text: name
}
Text {
id: stockValueChangePercentageText
anchors.top: stockIdText.bottom
anchors.right: parent.right
anchors.rightMargin: 20
width: 120
height: 30
color: "#328930"
font.family: "Open Sans"
font.pointSize: 18
font.bold: false
horizontalAlignment: Text.AlignRight
verticalAlignment: Text.AlignVCenter
text: changePercentage
onTextChanged: {
if (parseFloat(text) >= 0.0)
color = "#328930";
else
color = "#d40000";
}
}
Rectangle {
id: endingLine
anchors.bottom: parent.bottom
anchors.left: parent.left
height: 1
width: parent.width
color: "#d7d7d7"
}
}
highlight: Rectangle {
width: parent.width
color: "#eeeeee"
}
}
}
~~~
为了支撑这个StockListView,我们还需要一个StockListModel.qml:
它同样是一个ListModel,由许多个ListElement构成,代码也很明了。除了name和stockId 被赋值外,value,change,changePercentage都是在view中被动态赋值的,因此均初始化为0.0。
~~~
import QtQuick 2.0
ListModel {
id: stocks
// Data from : http://en.wikipedia.org/wiki/NASDAQ-100 // 这里告诉了我们数据来源
ListElement {name: "Apple Inc."; stockId: "AAPL"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Adobe Systems Inc."; stockId: "ADBE"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Analog Devices, Inc."; stockId: "ADI"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Automatic Data Processing, Inc."; stockId: "ADP"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Autodesk, Inc."; stockId: "ADSK"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Akamai Technologies, Inc."; stockId: "AKAM"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Altera Corp."; stockId: "ALTR"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Alexion Pharmaceuticals, Inc."; stockId: "ALXN"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Applied Materials, Inc."; stockId: "AMAT"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Amgen Inc."; stockId: "AMGN"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Amazon.com Inc."; stockId: "AMZN"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Activision Blizzard, Inc."; stockId: "ATVI"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Avago Technologies Limited"; stockId: "AVGO"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Bed Bath & Beyond Inc."; stockId: "BBBY"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Baidu, Inc."; stockId: "BIDU"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Biogen Idec Inc."; stockId: "BIIB"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Broadcom Corp."; stockId: "BRCM"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "CA Technologies"; stockId: "CA"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Celgene Corporation"; stockId: "CELG"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Cerner Corporation"; stockId: "CERN"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Check Point Software Technologies Ltd."; stockId: "CHKP"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "CH Robinson Worldwide Inc."; stockId: "CHRW"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Charter Communications, Inc."; stockId: "CHTR"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Comcast Corporation"; stockId: "CMCSA"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Costco Wholesale Corporation"; stockId: "COST"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Cisco Systems, Inc."; stockId: "CSCO"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Catamaran Corporation"; stockId: "CTRX"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Cognizant Technology Solutions Corporation"; stockId: "CTSH"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Citrix Systems, Inc."; stockId: "CTXS"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Discovery Communications, Inc."; stockId: "DISCA"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Dish Network Corp."; stockId: "DISH"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Dollar Tree, Inc."; stockId: "DLTR"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "DIRECTV"; stockId: "DTV"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "eBay Inc."; stockId: "EBAY"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Equinix, Inc."; stockId: "EQIX"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Express Scripts Holding Company"; stockId: "ESRX"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Expeditors International of Washington Inc."; stockId: "EXPD"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Expedia Inc."; stockId: "EXPE"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Fastenal Company"; stockId: "FAST"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Facebook, Inc."; stockId: "FB"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "F5 Networks, Inc."; stockId: "FFIV"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Fiserv, Inc."; stockId: "FISV"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Twenty-First Century Fox, Inc."; stockId: "FOXA"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Gilead Sciences Inc."; stockId: "GILD"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Keurig Green Mountain, Inc."; stockId: "GMCR"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Google Inc."; stockId: "GOOG"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Google Inc."; stockId: "GOOGL"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Garmin Ltd."; stockId: "GRMN"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Henry Schein, Inc."; stockId: "HSIC"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Illumina Inc."; stockId: "ILMN"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Intel Corporation"; stockId: "INTC"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Intuit Inc."; stockId: "INTU"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Intuitive Surgical, Inc."; stockId: "ISRG"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "KLA-Tencor Corporation"; stockId: "KLAC"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Kraft Foods Group, Inc."; stockId: "KRFT"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Liberty Global plc"; stockId: "LBTYA"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Liberty Interactive Corporation"; stockId: "LINTA"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Linear Technology Corporation"; stockId: "LLTC"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Liberty Media Corporation"; stockId: "LMCA"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Marriott International, Inc."; stockId: "MAR"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Mattel, Inc"; stockId: "MAT"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Mondelez International, Inc."; stockId: "MDLZ"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Monster Beverage Corporation"; stockId: "MNST"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Microsoft Corporation"; stockId: "MSFT"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Micron Technology Inc."; stockId: "MU"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Maxim Integrated Products, Inc."; stockId: "MXIM"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Mylan, Inc."; stockId: "MYL"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Netflix, Inc."; stockId: "NFLX"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "NetApp, Inc."; stockId: "NTAP"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "NVIDIA Corporation"; stockId: "NVDA"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "NXP Semiconductors NV"; stockId: "NXPI"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "O'Reilly Automotive Inc."; stockId: "ORLY"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Paychex, Inc."; stockId: "PAYX"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "PACCAR Inc."; stockId: "PCAR"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "The Priceline Group Inc."; stockId: "PCLN"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "QUALCOMM Incorporated"; stockId: "QCOM"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Regeneron Pharmaceuticals, Inc."; stockId: "REGN"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Ross Stores Inc."; stockId: "ROST"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "SBA Communications Corp."; stockId: "SBAC"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Starbucks Corporation"; stockId: "SBUX"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Sigma-Aldrich Corporation"; stockId: "SIAL"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Sirius XM Holdings Inc."; stockId: "SIRI"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "SanDisk Corp."; stockId: "SNDK"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Staples, Inc."; stockId: "SPLS"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Stericycle, Inc."; stockId: "SRCL"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Seagate Technology Public Limited Company"; stockId: "STX"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Symantec Corporation"; stockId: "SYMC"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "TripAdvisor Inc."; stockId: "TRIP"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Tractor Supply Company"; stockId: "TSCO"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Tesla Motors, Inc."; stockId: "TSLA"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Texas Instruments Inc."; stockId: "TXN"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Viacom, Inc."; stockId: "VIAB"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "VimpelCom Ltd."; stockId: "VIP"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Vodafone Group Public Limited Company"; stockId: "VOD"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Verisk Analytics, Inc."; stockId: "VRSK"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Vertex Pharmaceuticals Incorporated"; stockId: "VRTX"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Western Digital Corporation"; stockId: "WDC"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Whole Foods Market, Inc."; stockId: "WFM"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Wynn Resorts Ltd."; stockId: "WYNN"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
ListElement {name: "Xilinx Inc."; stockId: "XLNX"; value: "0.0"; change: "0.0"; changePercentage: "0.0"}
}
~~~
骨架已成,我们接下来就要为它添加血肉了。我在网上看到有人认为底层先于界面开发更快,也有人认为先搭界面框架再做底层更好。嗯...我倒觉得谈不上好坏之分,关键在于动手之前得有一个清晰的架构。不过底层在设计之初能够拥有丰富的API 减少日后改动,界面最初设计不要要求太高(因为总是会改的。。。),这样应该会让人比较舒服一些~
好了,扯远了,下面的StockView即是我们一开始看到的曲线界面,看看它包含哪些东西:
~~~
import QtQuick 2.0
import QtQuick.Window 2.1 // 下面的代码使用了Screen因此引入这个模块
Rectangle {
id: root
width: 320
height: 480
color: "transparent"
property var stock: null
property var stocklist: null
signal settingsClicked
function update() { // 用来更新图表显示
chart.update()
}
Rectangle {
id: mainRect
color: "transparent"
anchors.fill: parent
StockInfo { // 提供左上方的股票信息
id: stockInfo
anchors.left: parent.left
anchors.leftMargin: 10
anchors.top: parent.top
anchors.topMargin: 15
height: 160
anchors.right: Screen.primaryOrientation === Qt.PortraitOrientation ? parent.right : chart.left // 基于属性绑定的屏幕转向后布局方式的变化
anchors.rightMargin: 20
stock: root.stock
}
StockChart { // 右方的曲线绘制部分
id: chart
anchors.bottom: Screen.primaryOrientation === Qt.PortraitOrientation ? settingsPanel.top : parent.bottom
anchors.bottomMargin: 20
anchors.top : Screen.primaryOrientation === Qt.PortraitOrientation ? stockInfo.bottom : parent.top
anchors.topMargin: 20
anchors.right: parent.right
anchors.rightMargin: 20
width: Screen.primaryOrientation === Qt.PortraitOrientation ? parent.width - 40 : 0.6 * parent.width
stockModel: root.stock
settings: settingsPanel
}
StockSettingsPanel { // 左下方的显示设置面板
id: settingsPanel
anchors.left: parent.left
anchors.leftMargin: 20
anchors.right: Screen.primaryOrientation === Qt.PortraitOrientation ? parent.right : chart.left
anchors.rightMargin: 20
anchors.bottom: parent.bottom
onDrawOpenPriceChanged: root.update() // 更新
onDrawClosePriceChanged: root.update();
onDrawHighPriceChanged: root.update();
onDrawLowPriceChanged: root.update();
}
}
}
~~~
可以看到StockView由3个主要部分构成,分别是显示当前股票信息的StockInfo、设置显示曲线的StockSettingPanel、以及最后的绘图部分StockChart。
我们按顺序来看,StockInfo.qml:
~~~
import QtQuick 2.0
Rectangle { // 根项目是一个透明的Rectangle。为什么不用Item,我想可能是因为当时布局的时候把color设置出来可能更方便一些
id: root
width: 440
height: 160
color: "transparent"
property var stock: null // var类型的stock属性,它接受的是stocqt.qml中定义的StockModel
Text { // id
id: stockIdText
anchors.left: parent.left
anchors.leftMargin: 5
anchors.top: parent.top
anchors.topMargin: 15
color: "#000000"
font.family: "Open Sans"
font.pointSize: 38
font.weight: Font.DemiBold
text: root.stock.stockId // 类似的,对显示文本赋值
}
Text { // name
id: stockNameText
anchors.left: parent.left
anchors.leftMargin: 5
anchors.bottom: priceChangePercentage.bottom
anchors.right: priceChangePercentage.left
anchors.rightMargin: 15
color: "#000000"
font.family: "Open Sans"
font.pointSize: 16
elide: Text.ElideRight
text: root.stock.stockName
}
Text { // 价格
id: price
anchors.right: parent.right
anchors.rightMargin: 5
anchors.top: parent.top
anchors.topMargin: 15
horizontalAlignment: Text.AlignRight
color: "#000000"
font.family: "Open Sans"
font.pointSize: 30
font.weight: Font.DemiBold
text: root.stock.stockPrice
}
Text { // 价格变化
id: priceChange
anchors.right: parent.right
anchors.rightMargin: 20
anchors.top: price.bottom
anchors.topMargin: 5
horizontalAlignment: Text.AlignRight
color: root.stock.stockPriceChanged < 0 ? "#d40000" : "#328930"
font.family: "Open Sans"
font.pointSize: 20
font.weight: Font.Bold
text: root.stock.stockPriceChanged
}
Text { // 价格变化百分比
id: priceChangePercentage
anchors.right: parent.right
anchors.rightMargin: 20
anchors.top: priceChange.bottom
anchors.topMargin: 5
horizontalAlignment: Text.AlignRight
color: root.stock.stockPriceChanged < 0 ? "#d40000" : "#328930"
font.family: "Open Sans"
font.pointSize: 18
font.weight: Font.Bold
text: Math.abs(Math.round(root.stock.stockPriceChanged/(root.stock.stockPrice - root.stock.stockPriceChanged) * 100))/100 +"%"
}
}
~~~
stockSettingPanel.qml:
~~~
import QtQuick 2.0
Rectangle {
id: root
width: 440
height: 160
color: "transparent"
property bool drawOpenPrice: openButton.buttonEnabled // 对外的标志位
property bool drawClosePrice: closeButton.buttonEnabled
property bool drawHighPrice: highButton.buttonEnabled
property bool drawLowPrice: lowButton.buttonEnabled
property string openColor: "#face20" // 各曲线的颜色设置
property string closeColor: "#14aaff"
property string highColor: "#80c342"
property string lowColor: "#f30000"
property string volumeColor: "#14aaff" // 成交量绘制颜色
Text {
id: openText
anchors.left: root.left
anchors.top: root.top
color: "#000000"
font.family: "Open Sans"
font.pointSize: 19
text: "Open"
}
Text {
id: closeText
anchors.left: root.left
anchors.top: openText.bottom
anchors.topMargin: 10
color: "#000000"
font.family: "Open Sans"
font.pointSize: 19
text: "Close"
}
Text {
id: highText
anchors.left: root.left
anchors.top: closeText.bottom
anchors.topMargin: 10
color: "#000000"
font.family: "Open Sans"
font.pointSize: 19
text: "High"
}
Text {
id: lowText
anchors.left: root.left
anchors.top: highText.bottom
anchors.topMargin: 10
color: "#000000"
font.family: "Open Sans"
font.pointSize: 19
text: "Low"
}
Rectangle {
height: 4
anchors.left: root.left
anchors.leftMargin: 114
anchors.right: openButton.left
anchors.rightMargin: 65
anchors.verticalCenter: openText.verticalCenter
color: openColor
}
Rectangle {
height: 4
anchors.left: root.left
anchors.leftMargin: 114
anchors.right: closeButton.left
anchors.rightMargin: 65
anchors.verticalCenter: closeText.verticalCenter
color: closeColor
}
Rectangle {
height: 4
anchors.left: root.left
anchors.leftMargin: 114
anchors.right: highButton.left
anchors.rightMargin: 65
anchors.verticalCenter: highText.verticalCenter
color: highColor
}
Rectangle {
height: 4
anchors.left: root.left
anchors.leftMargin: 114
anchors.right: lowButton.left
anchors.rightMargin: 65
anchors.verticalCenter: lowText.verticalCenter
color: lowColor
}
CheckBox { // 自定义的CheckBox,提供了属性buttonEnabled表明是否被checked
id: openButton
buttonEnabled: false
anchors.verticalCenter: openText.verticalCenter
anchors.right: root.right
anchors.rightMargin: 40
}
CheckBox {
id: closeButton
buttonEnabled: false
anchors.verticalCenter: closeText.verticalCenter
anchors.right: root.right
anchors.rightMargin: 40
}
CheckBox {
id: highButton
buttonEnabled: true
anchors.verticalCenter: highText.verticalCenter
anchors.right: root.right
anchors.rightMargin: 40
}
CheckBox {
id: lowButton
buttonEnabled: true
anchors.verticalCenter: lowText.verticalCenter
anchors.right: root.right
anchors.rightMargin: 40
}
}
~~~
自定义的CheckBox.qml:
~~~
import QtQuick 2.0
Item {
id: button
property bool buttonEnabled: true
width: 30
height: 30
x: 5
MouseArea {
id: mouse
anchors.fill: parent
onClicked: {
if (buttonEnabled)
buttonEnabled = false;
else
buttonEnabled = true;
}
}
Rectangle {
id: checkbox
width: 30
height: 30
anchors.left: parent.left
border.color: "#999999"
border.width: 1
antialiasing: true
radius: 2
color: "transparent"
Rectangle {
anchors.fill: parent
anchors.margins: 5
antialiasing: true
radius: 1
color: mouse.pressed || buttonEnabled ? "#999999" : "transparent"
}
}
}
~~~
接着还有一个自定义的控件Button.qml,它在下面的StockChart将会用到:
~~~
import QtQuick 2.0
Rectangle {
id: button
signal clicked
property alias text: txt.text // 设置txt.text的属性别名为text,这样Rectangle就不用再声明一个属性以访问txt的text
property bool buttonEnabled: false
width: Math.max(64, txt.width + 16)
height: 32
color: "transparent"
MouseArea {
anchors.fill: parent
onClicked: button.clicked()
}
Text {
anchors.centerIn: parent
font.family: "Open Sans"
font.pointSize: 19
font.weight: Font.DemiBold
color: button.buttonEnabled ? "#000000" : "#14aaff"
id: txt
}
}
~~~
最终的绘制部分终于来了,StockChart.qml:
~~~
import QtQuick 2.0
Rectangle {
id: chart
width: 320
height: 200
property var stockModel: null
property var startDate: new Date() // new Date()初始化得到的是当前时间
property var endDate: new Date()
property string activeChart: "year" // 设置表格显示的时间跨度为一年
property var settings
property int gridSize: 4 // 每个网格宽度为4px
property real gridStep: gridSize ? (width - canvas.tickMargin) / gridSize : canvas.xGridStep // 网格数为(宽度 - 最右方那一栏的宽度)/ 网格宽度,如果gridSize为0,采用画布中的计算方式
function update() { // 更新函数
endDate = new Date();
if (chart.activeChart === "year") { // 显示一年数据
chart.startDate = new Date(chart.endDate.getFullYear() - 1, // 在当前时间的基础上减一年
chart.endDate.getMonth(),
chart.endDate.getDate());
chart.gridSize = 12; // 设置网格宽度为12,以固定网格数
}
else if (chart.activeChart === "month") { // 显示一个月数据
chart.startDate = new Date(chart.endDate.getFullYear(), // 在当前时间基础上减一个月
chart.endDate.getMonth() - 1,
chart.endDate.getDate());
gridSize = 0; // gridSize为0时,采用canvas中定义的网格宽度计算。使每个数据都绘制在坐标线上
}
else if (chart.activeChart === "week") { // 显示一周数据
chart.startDate = new Date(chart.endDate.getFullYear(), // 在当前时间基础上减七天
chart.endDate.getMonth(),
chart.endDate.getDate() - 7);
gridSize = 0;
}
else {
chart.startDate = new Date(2005, 3, 25); // 否则以2005年为初始年,并定义网格宽度为4
gridSize = 4;
}
canvas.requestPaint(); // 当更新时需要调用画布的这个函数
}
Row { // Row布局了4个自定义的按钮
id: activeChartRow
anchors.left: chart.left
anchors.right: chart.right
anchors.top: chart.top
anchors.topMargin: 4
spacing: 52
onWidthChanged: { // 该函数保证宽度变化时优先压缩spacing,且不会造成按钮重叠
var buttonsLen = maxButton.width + yearButton.width + monthButton.width + weekButton.width;
var space = (width - buttonsLen) / 3;
spacing = Math.max(space, 10);
}
Button {
id: maxButton
text: "Max"
buttonEnabled: chart.activeChart === "max"
onClicked: {
chart.activeChart = "max"; // 改变当前图表显示模式,这里的max 实际对应update中的'else',即2005年作为起始年
chart.update(); // 更新
}
}
Button {
id: yearButton
text: "Year"
buttonEnabled: chart.activeChart === "year"
onClicked: {
chart.activeChart = "year";
chart.update();
}
}
Button {
id: monthButton
text: "Month"
buttonEnabled: chart.activeChart === "month"
onClicked: {
chart.activeChart = "month";
chart.update();
}
}
Button {
id: weekButton
text: "Week"
buttonEnabled: chart.activeChart === "week"
onClicked: {
chart.activeChart = "week";
chart.update();
}
}
}
Text { // 下方的起始日期显示
id: fromDate
color: "#000000"
font.family: "Open Sans"
font.pointSize: 8
anchors.left: parent.left
anchors.bottom: parent.bottom
text: "| " + startDate.toDateString()
}
Text { // 结束日期显示
id: toDate
color: "#000000"
font.family: "Open Sans"
font.pointSize: 8
anchors.right: parent.right
anchors.rightMargin: canvas.tickMargin
anchors.bottom: parent.bottom
text: endDate.toDateString() + " |"
}
Canvas { // 画布,基本上与HTML的Canvas相同
id: canvas
// 注释介绍将下面两行语句取消注释以获得OpenGL的硬件加速渲染,为什么没有开?有些平台不支持嘛...
// Uncomment below lines to use OpenGL hardware accelerated rendering.
// See Canvas documentation for available options.
//renderTarget: Canvas.FramebufferObject // 渲染到OpenGL的帧缓冲
//renderStrategy: Canvas.Threaded // 渲染工作在一个私有渲染线程中进行
anchors.top: activeChartRow.bottom // 作为Item的派生类型,我们同样可以设置它的布局与属性
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: fromDate.top
property int pixelSkip: 1
property int numPoints: 1 // 存储数据点的个数
property int tickMargin: 32 // 右边格的宽度
property real xGridStep: (width - tickMargin) / numPoints // 网格宽度
property real yGridOffset: height / 26 // y方向向下的偏移度,用在水平线的绘制
property real yGridStep: height / 12 // 网格高度
function drawBackground(ctx) { // 界面的绘制由onPaint开始,这是绘制背景的一个函数。ctx作为传参,类似C++中的painter
ctx.save(); // 保存之前绘制内容
ctx.fillStyle = "#ffffff"; // 填充颜色,之所以叫Style是因为它还可以使用渐变等等...
ctx.fillRect(0, 0, canvas.width, canvas.height); // fillRect是一个便利函数,用来填充一个矩形区域
ctx.strokeStyle = "#d7d7d7"; // 描边颜色
ctx.beginPath();
// 水平网格线
for (var i = 0; i < 12; i++) {
ctx.moveTo(0, canvas.yGridOffset + i * canvas.yGridStep);
ctx.lineTo(canvas.width, canvas.yGridOffset + i * canvas.yGridStep);
}
// 垂直网格线
var height = 35 * canvas.height / 36; // 垂直线的高度为画布高度的 35/36
var yOffset = canvas.height - height; // 垂直线离顶部距离为高度的 1/36
var xOffset = 0;
for (i = 0; i < chart.gridSize; i++) {
ctx.moveTo(xOffset + i * chart.gridStep, yOffset);
ctx.lineTo(xOffset + i * chart.gridStep, height);
}
ctx.stroke(); // 描线
// 右方以及下方颜色较深的那几根线
ctx.strokeStyle = "#666666";
ctx.beginPath();
var xStart = canvas.width - tickMargin; // x = 画布宽度 - 价格部分宽度
ctx.moveTo(xStart, 0);
ctx.lineTo(xStart, canvas.height); // 向下画直线
for (i = 0; i < 12; i++) {
ctx.moveTo(xStart, canvas.yGridOffset + i * canvas.yGridStep); // 12根短横线
ctx.lineTo(canvas.width, canvas.yGridOffset + i * canvas.yGridStep);
}
ctx.moveTo(0, canvas.yGridOffset + 9 * canvas.yGridStep); // 移动绘制点到第九根横线左端
ctx.lineTo(canvas.width, canvas.yGridOffset + 9 * canvas.yGridStep); // 向右绘制横线
ctx.closePath(); // 完成路径
ctx.stroke(); // 描边
ctx.restore(); // 载入保存的内容
}
function drawScales(ctx, high, low, vol) // 绘制右方股票价格标尺函数
{
ctx.save();
ctx.strokeStyle = "#888888";
ctx.font = "10px Open Sans"
ctx.beginPath();
// prices on y-axis
var x = canvas.width - tickMargin + 3; // 离右边实线3px
var priceStep = (high - low) / 9.0; // 相隔最高价与最低价的差值除以9
for (var i = 0; i < 10; i += 2) { // 隔一级显示
var price = parseFloat(high - i * priceStep).toFixed(1);
ctx.text(price, x, canvas.yGridOffset + i * yGridStep - 2); // 绘制text的坐标在文字的左下角
}
// highest volume
ctx.text(vol, 0, canvas.yGridOffset + 9 * yGridStep + 12); // 绘制最高成交量
ctx.closePath();
ctx.stroke();
ctx.restore();
}
function drawPrice(ctx, from, to, color, price, points, highest, lowest) // 数据曲线绘制
{
ctx.save();
ctx.globalAlpha = 0.7; // 透明度
ctx.strokeStyle = color; // color由StockSettingPanel指定
ctx.lineWidth = 3;
ctx.beginPath();
var end = points.length; // 数据长度
var range = highest - lowest; // 取值范围
if (range == 0) {
range = 1; // range作为被除数不能为0
}
for (var i = 0; i < end; i += pixelSkip) {
var x = points[i].x;
var y = points[i][price]; // 取出对应设置的价格数据
var h = 9 * yGridStep; // 设置绘制高度为九倍的网格高度
y = h * (lowest - y)/range + h + yGridOffset; // lowest - y为非正数,h + yGridOffset为曲线绘制的底部
if (i == 0) {
ctx.moveTo(x, y); // 移动到初始点
} else {
ctx.lineTo(x, y); // 向后绘制
}
}
ctx.stroke();
ctx.restore();
}
function drawVolume(ctx, from, to, color, price, points, highest) // 成交量绘制函数
{
ctx.save();
ctx.fillStyle = color;
ctx.globalAlpha = 0.8;
ctx.lineWidth = 0; // 由于线宽影响绘制边界(参考HTML),这里将线宽设置为0
ctx.beginPath();
var end = points.length;
var margin = 0;
if (chart.activeChart === "month" || chart.activeChart === "week") {
margin = 8;
ctx.shadowOffsetX = 4; // x方向的阴影
ctx.shadowBlur = 3.5; // 模糊效果
ctx.shadowColor = Qt.darker(color);
}
// 由于柱状图的宽度限制,柱状图比实际的数据少一个
// To match the volume graph with price grid, skip drawing the initial
// volume of the first day on chart.
for (var i = 1; i < end; i += pixelSkip) {
var x = points[i - 1].x;
var y = points[i][price];
y = canvas.height * (y / highest);
y = 3 * y / 12; // 柱状图高度占画布的1/4
ctx.fillRect(x, canvas.height - y + yGridOffset,
canvas.xGridStep - margin, y); // "周"与"月"时有间隔,其他则没有
}
ctx.stroke();
ctx.restore();
}
onPaint: { // 绘制入口
if (!stockModel.ready) { // 等待数据完成
return;
}
numPoints = stockModel.indexOf(chart.startDate); // 由StockModel取得startDate到现在的数据数
if (chart.gridSize == 0)
chart.gridSize = numPoints // 使gridStep绑定到(width - canvas.tickMargin) / numPoints上
var ctx = canvas.getContext("2d"); // 创建ctx
ctx.globalCompositeOperation = "source-over"; // 混合模式
ctx.lineWidth = 1;
drawBackground(ctx); // 背景绘制
var highestPrice = 0;
var highestVolume = 0;
var lowestPrice = -1;
var points = []; // 创建一个数组
for (var i = numPoints, j = 0; i >= 0 ; i -= pixelSkip, j += pixelSkip) { // pixelSkip被定义为 1
var price = stockModel.get(i);
if (parseFloat(highestPrice) < parseFloat(price.high)) // 得到最高价
highestPrice = price.high;
if (parseInt(highestVolume, 10) < parseInt(price.volume, 10)) // 得到最低价
highestVolume = price.volume;
if (lowestPrice < 0 || parseFloat(lowestPrice) > parseFloat(price.low)) // 注意这里如果设置lowestPrice = 0或是别的数
lowestPrice = price.low; // 就有可能一直无法满足条件,因此添加这个负数使第一个price.low被赋予lowestPrice
points.push({ // 插入数据,它类似于Model,但多了一个 x 的坐标值
x: j * xGridStep,
open: price.open,
close: price.close,
high: price.high,
low: price.low,
volume: price.volume
});
}
if (settings.drawHighPrice) // 判断StockSettingPanel中相应的选项是否被勾选,然后绘制数据线段
drawPrice(ctx, 0, numPoints, settings.highColor, "high", points, highestPrice, lowestPrice);
if (settings.drawLowPrice)
drawPrice(ctx, 0, numPoints, settings.lowColor, "low", points, highestPrice, lowestPrice);
if (settings.drawOpenPrice)
drawPrice(ctx, 0, numPoints,settings.openColor, "open", points, highestPrice, lowestPrice);
if (settings.drawClosePrice)
drawPrice(ctx, 0, numPoints, settings.closeColor, "close", points, highestPrice, lowestPrice);
drawVolume(ctx, 0, numPoints, settings.volumeColor, "volume", points, highestVolume); // 成交量绘制
drawScales(ctx, highestPrice, lowestPrice, highestVolume); // 价格标尺绘制
}
}
}
~~~
30——Extending QML – Binding Example
最后更新于:2022-04-01 07:22:12
本系列所有文章可以在这里查看[http://blog.csdn.net/cloud_castle/article/category/2123873](http://blog.csdn.net/cloud_castle/article/category/2123873)
接上文[Qt5官方demo解析集29——Extending QML - Property Value Source Example](http://blog.csdn.net/cloud_castle/article/details/37526169)
还记得我们曾经在[Qt5官方demo解析集17——Chapter 3: Adding Property Bindings](http://blog.csdn.net/cloud_castle/article/details/36886779)一文中接触过QML自定义类型的属性绑定吗?如果不记得了,可以移步进行了解。因为项目尺寸的原因,那个例子可能更好理解。
这个例子也是我们Extending QML(扩展QML)系列的最后一个例子了,虽然相较前一个例子也只有小小的改动,不过我们还是把整个工程都完整的看一遍吧~
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd09034fd.jpg)
binding.qrc中是我们的qml文件,它实例化了BirthdayParty类以及其所有的子对象。
Person类建立了一个自定义的QML类型,由于它并不是一个可视化组件,且QML任何组件均基于Qt 的元对象系统,因此继承自QObject。
接着定义了ShoeDescription用来对Person类的shoe属性进行描述,使用特定的方法,我们在对shoe赋值时不需要实例化这个ShoeDescription组件。
再定义两个Person的派生类Boy、Girl,可以用来对Person对象分类。
person.h:
~~~
#ifndef PERSON_H
#define PERSON_H
#include <QObject>
#include <QColor>
class ShoeDescription : public QObject
{
Q_OBJECT
Q_PROPERTY(int size READ size WRITE setSize NOTIFY shoeChanged) // NOTIFY用在属性绑定,当该属性值发生改变时发出信号shoeChanged
Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY shoeChanged) // 通过该信号,我们就能使得被绑定的属性值随之发生改变
Q_PROPERTY(QString brand READ brand WRITE setBrand NOTIFY shoeChanged)
Q_PROPERTY(qreal price READ price WRITE setPrice NOTIFY shoeChanged)
public:
ShoeDescription(QObject *parent = 0);
int size() const;
void setSize(int);
QColor color() const;
void setColor(const QColor &);
QString brand() const;
void setBrand(const QString &);
qreal price() const;
void setPrice(qreal);
signals:
void shoeChanged(); // 定义该shoeChanged()信号
private:
int m_size;
QColor m_color;
QString m_brand;
qreal m_price;
};
class Person : public QObject
{
Q_OBJECT
Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged)
// ![0]
Q_PROPERTY(ShoeDescription *shoe READ shoe CONSTANT)
// ![0]
public:
Person(QObject *parent = 0);
QString name() const;
void setName(const QString &);
ShoeDescription *shoe();
signals:
void nameChanged();
private:
QString m_name;
ShoeDescription m_shoe;
};
class Boy : public Person
{
Q_OBJECT
public:
Boy(QObject * parent = 0);
};
class Girl : public Person
{
Q_OBJECT
public:
Girl(QObject * parent = 0);
};
#endif // PERSON_H
~~~
person.cpp:
~~~
#include "person.h"
ShoeDescription::ShoeDescription(QObject *parent)
: QObject(parent), m_size(0), m_price(0)
{
}
int ShoeDescription::size() const
{
return m_size;
}
void ShoeDescription::setSize(int s)
{
if (m_size == s)
return;
m_size = s;
emit shoeChanged(); // 该信号应该在该属性被正确写入后发出
}
QColor ShoeDescription::color() const
{
return m_color;
}
void ShoeDescription::setColor(const QColor &c)
{
if (m_color == c)
return;
m_color = c;
emit shoeChanged();
}
QString ShoeDescription::brand() const
{
return m_brand;
}
void ShoeDescription::setBrand(const QString &b)
{
if (m_brand == b)
return;
m_brand = b;
emit shoeChanged();
}
qreal ShoeDescription::price() const
{
return m_price;
}
void ShoeDescription::setPrice(qreal p)
{
if (m_price == p)
return;
m_price = p;
emit shoeChanged();
}
Person::Person(QObject *parent)
: QObject(parent)
{
}
QString Person::name() const
{
return m_name;
}
void Person::setName(const QString &n)
{
if (m_name == n)
return;
m_name = n;
emit nameChanged();
}
ShoeDescription *Person::shoe()
{
return &m_shoe;
}
Boy::Boy(QObject * parent)
: Person(parent)
{
}
Girl::Girl(QObject * parent)
: Person(parent)
{
}
~~~
接下来是我们的主类BirthdayParty,它也是example.qml中的根项目。它有一个以Person指针为参数的host属性,用来指明寿星;有一个以Person列表指针为参数guests属性,用来指明客人,并且该属性被设置为默认属性,这样在QML中没有指明属性的值将被划归它的名下;一个announcement属性,用来被动态改变以播放歌词。另外,该类还定义了一个partyStarted()信号,我们可以在QML中使用onPartyStarted 来响应该信号。
此外,再定义一个BirthdayPartyAttached类,它用来为BirthdayParty提供一个附加属性。
birthdayparty.h:
~~~
#ifndef BIRTHDAYPARTY_H
#define BIRTHDAYPARTY_H
#include <QObject>
#include <QDate>
#include <QDebug>
#include <qqml.h>
#include "person.h"
class BirthdayPartyAttached : public QObject
{
Q_OBJECT
Q_PROPERTY(QDate rsvp READ rsvp WRITE setRsvp NOTIFY rsvpChanged) // 该例中大多数属性均定义了属性绑定
public:
BirthdayPartyAttached(QObject *object);
QDate rsvp() const;
void setRsvp(const QDate &);
signals:
void rsvpChanged();
private:
QDate m_rsvp;
};
class BirthdayParty : public QObject
{
Q_OBJECT
// ![0]
Q_PROPERTY(Person *host READ host WRITE setHost NOTIFY hostChanged)
// ![0]
Q_PROPERTY(QQmlListProperty<Person> guests READ guests)
Q_PROPERTY(QString announcement READ announcement WRITE setAnnouncement)
Q_CLASSINFO("DefaultProperty", "guests")
public:
BirthdayParty(QObject *parent = 0);
Person *host() const;
void setHost(Person *);
QQmlListProperty<Person> guests();
int guestCount() const;
Person *guest(int) const;
QString announcement() const;
void setAnnouncement(const QString &);
static BirthdayPartyAttached *qmlAttachedProperties(QObject *);
void startParty();
signals:
void partyStarted(const QTime &time);
void hostChanged();
private:
Person *m_host;
QList<Person *> m_guests;
};
QML_DECLARE_TYPEINFO(BirthdayParty, QML_HAS_ATTACHED_PROPERTIES)
#endif // BIRTHDAYPARTY_H
~~~
birthdayparty.cpp:
~~~
#include "birthdayparty.h"
BirthdayPartyAttached::BirthdayPartyAttached(QObject *object)
: QObject(object)
{
}
QDate BirthdayPartyAttached::rsvp() const
{
return m_rsvp;
}
void BirthdayPartyAttached::setRsvp(const QDate &d)
{
if (d != m_rsvp) {
m_rsvp = d;
emit rsvpChanged();
}
}
BirthdayParty::BirthdayParty(QObject *parent)
: QObject(parent), m_host(0)
{
}
Person *BirthdayParty::host() const
{
return m_host;
}
void BirthdayParty::setHost(Person *c)
{
if (c == m_host) return;
m_host = c;
emit hostChanged();
}
QQmlListProperty<Person> BirthdayParty::guests()
{
return QQmlListProperty<Person>(this, m_guests);
}
int BirthdayParty::guestCount() const
{
return m_guests.count();
}
Person *BirthdayParty::guest(int index) const
{
return m_guests.at(index);
}
void BirthdayParty::startParty()
{
QTime time = QTime::currentTime();
emit partyStarted(time);
}
QString BirthdayParty::announcement() const
{
return QString();
}
void BirthdayParty::setAnnouncement(const QString &speak)
{
qWarning() << qPrintable(speak);
}
BirthdayPartyAttached *BirthdayParty::qmlAttachedProperties(QObject *object)
{
return new BirthdayPartyAttached(object);
}
~~~
在该系列第9个例子中,我们接触到了HappyBirthdaySong类,它是一个自定义的Property Value Source,用来为QML属性提供随时间变化的能力,类似于Animation。在该例子中,它被用于announcement属性。
happybirthdaysong.h:
~~~
#ifndef HAPPYBIRTHDAYSONG_H
#define HAPPYBIRTHDAYSONG_H
#include <QQmlPropertyValueSource>
#include <QQmlProperty>
#include <QStringList>
class HappyBirthdaySong : public QObject, public QQmlPropertyValueSource
{
Q_OBJECT
Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged)
Q_INTERFACES(QQmlPropertyValueSource)
public:
HappyBirthdaySong(QObject *parent = 0);
virtual void setTarget(const QQmlProperty &);
QString name() const;
void setName(const QString &);
private slots:
void advance();
signals:
void nameChanged();
private:
int m_line;
QStringList m_lyrics;
QQmlProperty m_target;
QString m_name;
};
#endif // HAPPYBIRTHDAYSONG_H
~~~
happybirthdaysong.cpp:
~~~
#include "happybirthdaysong.h"
#include <QTimer>
HappyBirthdaySong::HappyBirthdaySong(QObject *parent)
: QObject(parent), m_line(-1)
{
setName(QString());
QTimer *timer = new QTimer(this);
QObject::connect(timer, SIGNAL(timeout()), this, SLOT(advance()));
timer->start(1000);
}
void HappyBirthdaySong::setTarget(const QQmlProperty &p)
{
m_target = p;
}
QString HappyBirthdaySong::name() const
{
return m_name;
}
void HappyBirthdaySong::setName(const QString &name)
{
if (m_name == name)
return;
m_name = name;
m_lyrics.clear();
m_lyrics << "Happy birthday to you,";
m_lyrics << "Happy birthday to you,";
m_lyrics << "Happy birthday dear " + m_name + ",";
m_lyrics << "Happy birthday to you!";
m_lyrics << "";
emit nameChanged();
}
void HappyBirthdaySong::advance()
{
m_line = (m_line + 1) % m_lyrics.count();
m_target.write(m_lyrics.at(m_line));
}
~~~
在main.cpp中将这些C++类注册成QML类型后,我们就可以在QML中创建一个实例化的BirthdayParty,并对其属性赋值:
example.qml:
~~~
import People 1.0
import QtQuick 2.0 // For QColor
// ![0]
BirthdayParty {
id: theParty
HappyBirthdaySong on announcement { name: theParty.host.name } // 属性绑定
host: Boy {
name: "Bob Jones"
shoe { size: 12; color: "white"; brand: "Nike"; price: 90.0 }
}
// ![0]
onPartyStarted: console.log("This party started rockin' at " + time);
Boy {
name: "Leo Hodges"
BirthdayParty.rsvp: "2009-07-06"
shoe { size: 10; color: "black"; brand: "Reebok"; price: 59.95 }
}
Boy {
name: "Jack Smith"
shoe { size: 8; color: "blue"; brand: "Puma"; price: 19.95 }
}
Girl {
name: "Anne Brown"
BirthdayParty.rsvp: "2009-07-01"
shoe.size: 7
shoe.color: "red"
shoe.brand: "Marc Jacobs"
shoe.price: 699.99
}
// ![1]
}
// ![1]
~~~
最后,在main.cpp调用这个属性的信息,并基于一定的规则输出这些信息:
~~~
#include <QCoreApplication>
#include <QQmlEngine>
#include <QQmlComponent>
#include <QDebug>
#include "birthdayparty.h"
#include "happybirthdaysong.h"
#include "person.h"
int main(int argc, char ** argv)
{
QCoreApplication app(argc, argv);
qmlRegisterType<BirthdayPartyAttached>();
qmlRegisterType<BirthdayParty>("People", 1,0, "BirthdayParty");
qmlRegisterType<HappyBirthdaySong>("People", 1,0, "HappyBirthdaySong");
qmlRegisterType<ShoeDescription>();
qmlRegisterType<Person>();
qmlRegisterType<Boy>("People", 1,0, "Boy");
qmlRegisterType<Girl>("People", 1,0, "Girl");
QQmlEngine engine;
QQmlComponent component(&engine, QUrl("qrc:example.qml"));
BirthdayParty *party = qobject_cast<BirthdayParty *>(component.create());
if (party && party->host()) {
qWarning() << party->host()->name() << "is having a birthday!";
if (qobject_cast<Boy *>(party->host()))
qWarning() << "He is inviting:";
else
qWarning() << "She is inviting:";
for (int ii = 0; ii < party->guestCount(); ++ii) {
Person *guest = party->guest(ii);
QDate rsvpDate;
QObject *attached =
qmlAttachedPropertiesObject<BirthdayParty>(guest, false);
if (attached)
rsvpDate = attached->property("rsvp").toDate();
if (rsvpDate.isNull())
qWarning() << " " << guest->name() << "RSVP date: Hasn't RSVP'd";
else
qWarning() << " " << guest->name() << "RSVP date:" << qPrintable(rsvpDate.toString());
}
party->startParty();
} else {
qWarning() << component.errors();
}
return app.exec();
}
~~~
输出如下:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd0917427.jpg)
29——Extending QML – Property Value Source Example
最后更新于:2022-04-01 07:22:10
本系列所有文章可以在这里查看[http://blog.csdn.net/cloud_castle/article/category/2123873](http://blog.csdn.net/cloud_castle/article/category/2123873)
接上文[Qt5官方demo解析集28——Extending QML - Signal Support Example](http://blog.csdn.net/cloud_castle/article/details/37512407)
我们经常会在QML代码中使用Animation和bindings,以使得我们的程序具有更好的动态性能。那么,类似NumberAnimation这种QML类似实际上是提供了一个算法来为属性提供动态变化的数值,或者说是提供了一个值的集合。这里Qt将其称作“属性值来源”(Property Value Source),并为这些QML类型提供了一个通用的接口,即QQmlPropertyValueSource。通过继承这个类,我们可以实现自定义的Property Value Source。
在前面的项目中添加一个类happybirthdaysong,用来自定义地控制BirthdayParty中announcement属性的变化:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd08ccdd9.jpg)
这个demo向我们展示了这个自定义的过程,happybirthdaysong.h:
~~~
#ifndef HAPPYBIRTHDAYSONG_H
#define HAPPYBIRTHDAYSONG_H
#include <QQmlPropertyValueSource>
#include <QQmlProperty>
#include <qqml.h>
#include <QStringList>
// ![0]
class HappyBirthdaySong : public QObject, public QQmlPropertyValueSource // 由于QQmlPropertyValueSource是一个接口类
{ // 我们还需要继承QObject
Q_OBJECT
Q_INTERFACES(QQmlPropertyValueSource) // 声明接口
// ![0]
Q_PROPERTY(QString name READ name WRITE setName) // name属性用来设置生日歌的对象
// ![1]
public:
HappyBirthdaySong(QObject *parent = 0);
virtual void setTarget(const QQmlProperty &); // 用来指明作用的属性对象
// ![1] // <PropertyValueSource> on <property>时被调用
QString name() const; // 自定义属性的读写函数
void setName(const QString &);
private slots:
void advance(); // 更新函数,每秒输出一句歌词
private:
int m_line;
QStringList m_lyrics;
QQmlProperty m_target;
QString m_name;
// ![2]
};
// ![2]
#endif // HAPPYBIRTHDAYSONG_H
~~~
happybirthdaysong.cpp:
~~~
#include "happybirthdaysong.h"
#include <QTimer>
HappyBirthdaySong::HappyBirthdaySong(QObject *parent)
: QObject(parent), m_line(-1) // 初始化m_line为-1
{ // 使advance()第一次被调用时输出第一句歌词
setName(QString());
QTimer *timer = new QTimer(this);
QObject::connect(timer, SIGNAL(timeout()), this, SLOT(advance()));
timer->start(1000);
}
void HappyBirthdaySong::setTarget(const QQmlProperty &p)
{
m_target = p; // 该类型作用于某个属性时,Qt会使用这里的函数
}
QString HappyBirthdaySong::name() const
{
return m_name;
}
void HappyBirthdaySong::setName(const QString &name) // 初始化歌词,并带上“姓名”参数
{
m_name = name;
m_lyrics.clear();
m_lyrics << "Happy birthday to you,";
m_lyrics << "Happy birthday to you,";
m_lyrics << "Happy birthday dear " + m_name + ",";
m_lyrics << "Happy birthday to you!";
m_lyrics << "";
}
void HappyBirthdaySong::advance() // 循环显示的好方式
{
m_line = (m_line + 1) % m_lyrics.count();
m_target.write(m_lyrics.at(m_line));
}
~~~
Person类没有变化,而BirthdayParty类则单纯地添加了一个属性announcement来使上面的Source能作用其上,它与其他属性没有不同,类型为QString,用来赋予不同的歌词。
~~~
Q_PROPERTY(QString announcement READ announcement WRITE setAnnouncement)
~~~
main.cpp也没有改动:
~~~
#include <QCoreApplication>
#include <QQmlEngine>
#include <QQmlComponent>
#include <QDebug>
#include "birthdayparty.h"
#include "happybirthdaysong.h"
#include "person.h"
int main(int argc, char ** argv)
{
QCoreApplication app(argc, argv);
qmlRegisterType<BirthdayPartyAttached>();
qmlRegisterType<BirthdayParty>("People", 1,0, "BirthdayParty");
qmlRegisterType<HappyBirthdaySong>("People", 1,0, "HappyBirthdaySong");
qmlRegisterType<ShoeDescription>();
qmlRegisterType<Person>();
qmlRegisterType<Boy>("People", 1,0, "Boy");
qmlRegisterType<Girl>("People", 1,0, "Girl");
QQmlEngine engine;
QQmlComponent component(&engine, QUrl("qrc:example.qml"));
BirthdayParty *party = qobject_cast<BirthdayParty *>(component.create());
if (party && party->host()) {
qWarning() << party->host()->name() << "is having a birthday!";
if (qobject_cast<Boy *>(party->host()))
qWarning() << "He is inviting:";
else
qWarning() << "She is inviting:";
for (int ii = 0; ii < party->guestCount(); ++ii) {
Person *guest = party->guest(ii);
QDate rsvpDate;
QObject *attached =
qmlAttachedPropertiesObject<BirthdayParty>(guest, false);
if (attached)
rsvpDate = attached->property("rsvp").toDate();
if (rsvpDate.isNull())
qWarning() << " " << guest->name() << "RSVP date: Hasn't RSVP'd";
else
qWarning() << " " << guest->name() << "RSVP date:" << qPrintable(rsvpDate.toString());
}
party->startParty();
} else {
qWarning() << component.errors();
}
return app.exec();
}
~~~
最后,这个Source需要通过 on 这样的语句来调用,
因此在QML文件中添加了HappyBirthdaySongonannouncement{name:"BobJones"},使得这个程序得以循环地为Bob Jones唱生日快乐歌:
example.qml:
~~~
import People 1.0
import QtQuick 2.0 // For QColor
// ![0]
BirthdayParty {
HappyBirthdaySong on announcement { name: "Bob Jones" }
// ![0]
onPartyStarted: console.log("This party started rockin' at " + time);
host: Boy {
name: "Bob Jones"
shoe { size: 12; color: "white"; brand: "Nike"; price: 90.0 }
}
Boy {
name: "Leo Hodges"
BirthdayParty.rsvp: "2009-07-06"
shoe { size: 10; color: "black"; brand: "Reebok"; price: 59.95 }
}
Boy {
name: "Jack Smith"
shoe { size: 8; color: "blue"; brand: "Puma"; price: 19.95 }
}
Girl {
name: "Anne Brown"
BirthdayParty.rsvp: "2009-07-01"
shoe.size: 7
shoe.color: "red"
shoe.brand: "Marc Jacobs"
shoe.price: 699.99
}
// ![1]
}
// ![1]
~~~
效果如下:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd08e3c11.jpg)
28——Extending QML – Signal Support Example
最后更新于:2022-04-01 07:22:08
本系列所有文章可以在这里查看[http://blog.csdn.net/cloud_castle/article/category/2123873](http://blog.csdn.net/cloud_castle/article/category/2123873)
接上文[Qt5官方demo解析集27——Extending QML - Attached Properties Example](http://blog.csdn.net/cloud_castle/article/details/37510123)
这个demo演示了为QML自定义类型添加信号的方法,这与[Qt5官方demo解析集16——Chapter 2: Connecting to C++ Methods and Signals](http://blog.csdn.net/cloud_castle/article/details/36882625)所介绍的差不多,鉴于例子的尺寸,可能那一篇要更清晰一些。
无论如何,我们还是要继续扩展这个BirthdayParty例程。我们为这个生日派对添加了一个“派对开始”的信号,并定义了一个函数用来发射这个信号。
由于person类依然没有改变,我们看看小小改动了的birthdayparty.h:
~~~
#ifndef BIRTHDAYPARTY_H
#define BIRTHDAYPARTY_H
#include <QObject>
#include <QDate>
#include <qqml.h>
#include "person.h"
class BirthdayPartyAttached : public QObject
{
Q_OBJECT
Q_PROPERTY(QDate rsvp READ rsvp WRITE setRsvp)
public:
BirthdayPartyAttached(QObject *object);
QDate rsvp() const;
void setRsvp(const QDate &);
private:
QDate m_rsvp;
};
class BirthdayParty : public QObject
{
Q_OBJECT
Q_PROPERTY(Person *host READ host WRITE setHost)
Q_PROPERTY(QQmlListProperty<Person> guests READ guests)
Q_CLASSINFO("DefaultProperty", "guests")
public:
BirthdayParty(QObject *parent = 0);
Person *host() const;
void setHost(Person *);
QQmlListProperty<Person> guests();
int guestCount() const;
Person *guest(int) const;
static BirthdayPartyAttached *qmlAttachedProperties(QObject *);
void startParty(); // 自定义函数,之所以不使用Q_INVOKABLE是因为该函数是在main.cpp而不是QML中被调用的
// ![0]
signals:
void partyStarted(const QTime &time); // 自定义信号
// ![0]
private:
Person *m_host;
QList<Person *> m_guests;
};
QML_DECLARE_TYPEINFO(BirthdayParty, QML_HAS_ATTACHED_PROPERTIES)
#endif // BIRTHDAYPARTY_H
~~~
birthdayParty.cpp:
~~~
#include "birthdayparty.h"
BirthdayPartyAttached::BirthdayPartyAttached(QObject *object)
: QObject(object)
{
}
QDate BirthdayPartyAttached::rsvp() const
{
return m_rsvp;
}
void BirthdayPartyAttached::setRsvp(const QDate &d)
{
m_rsvp = d;
}
BirthdayParty::BirthdayParty(QObject *parent)
: QObject(parent), m_host(0)
{
}
Person *BirthdayParty::host() const
{
return m_host;
}
void BirthdayParty::setHost(Person *c)
{
m_host = c;
}
QQmlListProperty<Person> BirthdayParty::guests()
{
return QQmlListProperty<Person>(this, m_guests);
}
int BirthdayParty::guestCount() const
{
return m_guests.count();
}
Person *BirthdayParty::guest(int index) const
{
return m_guests.at(index);
}
void BirthdayParty::startParty() // 该函数用来将当前时间作为信号参数发射出去
{
QTime time = QTime::currentTime();
emit partyStarted(time);
}
BirthdayPartyAttached *BirthdayParty::qmlAttachedProperties(QObject *object)
{
return new BirthdayPartyAttached(object);
}
~~~
example.qml:
~~~
import People 1.0
import QtQuick 2.0 // For QColor
BirthdayParty {
// ![0]
onPartyStarted: console.log("This party started rockin' at " + time); // 在QML中我们不需要额外对信号进行处理
// ![0] // 只需要关注与该信号对应的onXXX函数
// 当信号发出时该函数即被执行
host: Boy {
name: "Bob Jones"
shoe { size: 12; color: "white"; brand: "Nike"; price: 90.0 }
}
Boy {
name: "Leo Hodges"
BirthdayParty.rsvp: "2009-07-06"
shoe { size: 10; color: "black"; brand: "Reebok"; price: 59.95 }
}
Boy {
name: "Jack Smith"
shoe { size: 8; color: "blue"; brand: "Puma"; price: 19.95 }
}
Girl {
name: "Anne Brown"
BirthdayParty.rsvp: "2009-07-01"
shoe.size: 7
shoe.color: "red"
shoe.brand: "Marc Jacobs"
shoe.price: 699.99
}
// ![1]
}
// ![1]
~~~
main.cpp:
~~~
#include <QCoreApplication>
#include <QQmlEngine>
#include <QQmlComponent>
#include <QDebug>
#include "birthdayparty.h"
#include "person.h"
int main(int argc, char ** argv)
{
QCoreApplication app(argc, argv);
qmlRegisterType<BirthdayPartyAttached>();
qmlRegisterType<BirthdayParty>("People", 1,0, "BirthdayParty");
qmlRegisterType<ShoeDescription>();
qmlRegisterType<Person>();
qmlRegisterType<Boy>("People", 1,0, "Boy");
qmlRegisterType<Girl>("People", 1,0, "Girl");
QQmlEngine engine;
QQmlComponent component(&engine, QUrl("qrc:example.qml"));
BirthdayParty *party = qobject_cast<BirthdayParty *>(component.create());
if (party && party->host()) {
qWarning() << party->host()->name() << "is having a birthday!";
if (qobject_cast<Boy *>(party->host()))
qWarning() << "He is inviting:";
else
qWarning() << "She is inviting:";
for (int ii = 0; ii < party->guestCount(); ++ii) {
Person *guest = party->guest(ii);
QDate rsvpDate;
QObject *attached =
qmlAttachedPropertiesObject<BirthdayParty>(guest, false);
if (attached)
rsvpDate = attached->property("rsvp").toDate();
if (rsvpDate.isNull())
qWarning() << " " << guest->name() << "RSVP date: Hasn't RSVP'd";
else
qWarning() << " " << guest->name() << "RSVP date:" << qPrintable(rsvpDate.toString()); // 上文讨论了qPrintable的问题
}
party->startParty(); // 这里调用了发射信号函数,因此party开始时间将被打印在最后一行
} else {
qWarning() << component.errors();
}
return 0;
}
~~~
结果如图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd08baf81.jpg)
27——Extending QML – Attached Properties Example
最后更新于:2022-04-01 07:22:06
本系列所有文章可以在这里查看[http://blog.csdn.net/cloud_castle/article/category/2123873](http://blog.csdn.net/cloud_castle/article/category/2123873)
接上文[Qt5官方demo解析集26——Extending QML - Grouped Properties Example](http://blog.csdn.net/cloud_castle/article/details/37501567)
该例子继续在上一个例子的基础上进行扩展,并向我们展示了向一个类型中添加附加属性的方法。
原工程不变,我们再添加一个BirthdayPartyAttached类来为BirthdayParth类提供一个额外的属性。
birthdayparth.h:
~~~
#ifndef BIRTHDAYPARTY_H
#define BIRTHDAYPARTY_H
#include <QObject>
#include <QDate>
#include <qqml.h>
#include "person.h"
class BirthdayPartyAttached : public QObject // 附加属性类
{
Q_OBJECT
Q_PROPERTY(QDate rsvp READ rsvp WRITE setRsvp) // 声明一个属性rsvp(答复),类型为QDate
public:
BirthdayPartyAttached(QObject *object);
QDate rsvp() const;
void setRsvp(const QDate &);
private:
QDate m_rsvp;
};
class BirthdayParty : public QObject
{
Q_OBJECT
Q_PROPERTY(Person *host READ host WRITE setHost)
Q_PROPERTY(QQmlListProperty<Person> guests READ guests)
Q_CLASSINFO("DefaultProperty", "guests")
public:
BirthdayParty(QObject *parent = 0);
Person *host() const;
void setHost(Person *);
QQmlListProperty<Person> guests();
int guestCount() const;
Person *guest(int) const;
//! [static attached]
static BirthdayPartyAttached *qmlAttachedProperties(QObject *); // 需要定义一个静态函数来返回这个静态属性的对象
//! [static attached]
private:
Person *m_host;
QList<Person *> m_guests;
};
//! [declare attached]
QML_DECLARE_TYPEINFO(BirthdayParty, QML_HAS_ATTACHED_PROPERTIES) // 利用QML_DECLARE_TYPEINFO声明使BirthdayParty类得以支持附加属性
//! [declare attached]
#endif // BIRTHDAYPARTY_H
~~~
birthdayparty.cpp:
~~~
#include "birthdayparty.h"
BirthdayPartyAttached::BirthdayPartyAttached(QObject *object)
: QObject(object)
{
}
QDate BirthdayPartyAttached::rsvp() const
{
return m_rsvp;
}
void BirthdayPartyAttached::setRsvp(const QDate &d)
{
m_rsvp = d;
}
BirthdayParty::BirthdayParty(QObject *parent)
: QObject(parent), m_host(0)
{
}
Person *BirthdayParty::host() const
{
return m_host;
}
void BirthdayParty::setHost(Person *c)
{
m_host = c;
}
QQmlListProperty<Person> BirthdayParty::guests()
{
return QQmlListProperty<Person>(this, m_guests);
}
int BirthdayParty::guestCount() const
{
return m_guests.count();
}
Person *BirthdayParty::guest(int index) const
{
return m_guests.at(index);
}
BirthdayPartyAttached *BirthdayParty::qmlAttachedProperties(QObject *object) // 当QML声明该附加属性时调用这个函数,返回一个该附加属性类的对象
{
return new BirthdayPartyAttached(object);
}
~~~
example.qml:
~~~
import People 1.0
import QtQuick 2.0 // For QColor
//! [begin]
BirthdayParty {
//! [begin]
//! [rsvp]
Boy {
name: "Robert Campbell"
BirthdayParty.rsvp: "2009-07-01" // 我们使用被附加的类型名 + "." + 附加属性来使用这个附加属性
}
//! [rsvp]
// ![1]
Boy {
name: "Leo Hodges"
shoe { size: 10; color: "black"; brand: "Reebok"; price: 59.95 }
BirthdayParty.rsvp: "2009-07-06"
}
// ![1]
host: Boy {
name: "Jack Smith"
shoe { size: 8; color: "blue"; brand: "Puma"; price: 19.95 }
}
//! [end]
}
//! [end]
~~~
main.cpp:
~~~
#include <QCoreApplication>
#include <QQmlEngine>
#include <QQmlComponent>
#include <QDebug>
#include "birthdayparty.h"
#include "person.h"
int main(int argc, char ** argv)
{
QCoreApplication app(argc, argv);
qmlRegisterType<BirthdayPartyAttached>(); // 同样的非实例化注册
qmlRegisterType<BirthdayParty>("People", 1,0, "BirthdayParty");
qmlRegisterType<ShoeDescription>();
qmlRegisterType<Person>();
qmlRegisterType<Boy>("People", 1,0, "Boy");
qmlRegisterType<Girl>("People", 1,0, "Girl");
QQmlEngine engine;
QQmlComponent component(&engine, QUrl("qrc:example.qml"));
BirthdayParty *party = qobject_cast<BirthdayParty *>(component.create());
if (party && party->host()) {
qWarning() << party->host()->name() << "is having a birthday!";
if (qobject_cast<Boy *>(party->host()))
qWarning() << "He is inviting:";
else
qWarning() << "She is inviting:";
for (int ii = 0; ii < party->guestCount(); ++ii) {
Person *guest = party->guest(ii);
//! [query rsvp]
QDate rsvpDate;
QObject *attached = qmlAttachedPropertiesObject<BirthdayParty>(guest, false); // 返回BirthdayParty的附加属性对象
if (attached)
rsvpDate = attached->property("rsvp").toDate(); // 由于property提取到的数据时QVariant类型的,因此需要转换成QDate
//! [query rsvp]
if (rsvpDate.isNull())
qWarning() << " " << guest->name() << "RSVP date: Hasn't RSVP'd";
else
qWarning() << " " << guest->name() << "RSVP date:" << qPrintable(rsvpDate.toString());
}
} else {
qWarning() << component.errors();
}
return 0;
}
~~~
显示结果如下:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd0898a3a.jpg)
可以看到日期的显示乱码。这本身不能说是代码的问题,因为国外的日期用单纯的ASCII码足够表示了,但是由于QDate使用本地习惯显示时间,那么在我们这它是会带中文的,qPrintable将QString转成了const char*,自然也就显示不出来了。
那解决办法是将qPrintable(rsvpDate.toString())直接改成rsvpDate.toString()就可以了。
最后显示如下:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd08a95d1.jpg)
26——Extending QML – Grouped Properties Example
最后更新于:2022-04-01 07:22:03
本系列所有文章可以在这里查看[http://blog.csdn.net/cloud_castle/article/category/2123873](http://blog.csdn.net/cloud_castle/article/category/2123873)
接上文[Qt5官方demo解析集25——Extending QML - Methods Example](http://blog.csdn.net/cloud_castle/article/details/37391595)
如果之前看过了我前面介绍粒子系统的朋友,应该对
velocity: AngleDirection {angleVariation: 360; magnitude: 80; magnitudeVariation: 40}
这样的属性设置格式屡见不鲜了,它实际是将一个AngleDirection类型作为velocity的属性值,这样我们就可以通过设置AngleDirection的属性值来达到对velocity更复杂的控制。
在这个例子中,Qt 向我们展示了一种更巧妙的做法 —— 直接使用AngleDirection的属性。
可能你之前经常会使用到font.family: "Ubuntu" 或是 font.pixelSize: 24 ,实际上这里的font 也是一个集合属性。
还是先把我们的项目文件介绍一下:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd08750fc.jpg)
两个自定义的C++类:Person与BirthdayParty,资源文件中是我们的QML文件example.qml。
为了有一个直观的印象,我们先来看看qml的实现:
example.qml:
~~~
import People 1.0
import QtQuick 2.0 // For QColor
// ![0]
BirthdayParty {
host: Boy {
name: "Bob Jones"
shoe { size: 12; color: "white"; brand: "Bikey"; price: 90.0 } // QML语法基础保证冒号为一个赋值运算
} // 而这个语句保证了属性赋值在大括号内部进行
Boy {
name: "Leo Hodges"
//![grouped]
shoe { size: 10; color: "black"; brand: "Thebok"; price: 59.95 }
//![grouped]
}
// ![1]
Boy {
name: "Jack Smith"
shoe {
size: 8
color: "blue"
brand: "Luma"
price: 19.95
}
}
// ![1]
Girl {
name: "Anne Brown"
//![ungrouped]
shoe.size: 7 // 大括号拆开来实际就变成这几个单独的语句
shoe.color: "red"
shoe.brand: "Job Macobs"
shoe.price: 699.99
//![ungrouped]
}
}
// ![0]
~~~
这种赋值方法是如何实现的呢,来看C++的定义:
BirthdayParty提供了生日派对的框架,这个类没有变化。birthdayparty.h:
~~~
#ifndef BIRTHDAYPARTY_H
#define BIRTHDAYPARTY_H
#include <QObject>
#include <QQmlListProperty>
#include "person.h"
class BirthdayParty : public QObject
{
Q_OBJECT
Q_PROPERTY(Person *host READ host WRITE setHost)
Q_PROPERTY(QQmlListProperty<Person> guests READ guests)
Q_CLASSINFO("DefaultProperty", "guests")
public:
BirthdayParty(QObject *parent = 0);
Person *host() const;
void setHost(Person *);
QQmlListProperty<Person> guests();
int guestCount() const;
Person *guest(int) const;
private:
Person *m_host;
QList<Person *> m_guests;
};
~~~
birthdayparty.cpp:
~~~
#include "birthdayparty.h"
BirthdayParty::BirthdayParty(QObject *parent)
: QObject(parent), m_host(0)
{
}
Person *BirthdayParty::host() const
{
return m_host;
}
void BirthdayParty::setHost(Person *c)
{
m_host = c;
}
QQmlListProperty<Person> BirthdayParty::guests()
{
return QQmlListProperty<Person>(this, m_guests);
}
int BirthdayParty::guestCount() const
{
return m_guests.count();
}
Person *BirthdayParty::guest(int index) const
{
return m_guests.at(index);
}
~~~
接下来,Person文件中定义了一个额外的类ShoeDescription用来为shoe属性提供描述。
person.h:
~~~
#ifndef PERSON_H
#define PERSON_H
#include <QObject>
#include <QColor>
class ShoeDescription : public QObject // 这个类的作用类似我们前面所说的AngleDirection,用这个自定义类型定义shoe属性
{
Q_OBJECT
Q_PROPERTY(int size READ size WRITE setSize) // 内部定义了四种属性:尺寸、颜色、品牌、价格
Q_PROPERTY(QColor color READ color WRITE setColor)
Q_PROPERTY(QString brand READ brand WRITE setBrand)
Q_PROPERTY(qreal price READ price WRITE setPrice)
public:
ShoeDescription(QObject *parent = 0);
int size() const;
void setSize(int);
QColor color() const;
void setColor(const QColor &);
QString brand() const;
void setBrand(const QString &);
qreal price() const;
void setPrice(qreal);
private:
int m_size;
QColor m_color;
QString m_brand;
qreal m_price;
};
class Person : public QObject // Person类定义
{
Q_OBJECT
Q_PROPERTY(QString name READ name WRITE setName)
// ![1]
Q_PROPERTY(ShoeDescription *shoe READ shoe) // 将shoe属性设置为只读,否则它需要被冒号赋值
// ![1]
public:
Person(QObject *parent = 0);
QString name() const;
void setName(const QString &);
ShoeDescription *shoe();
private:
QString m_name;
ShoeDescription m_shoe;
};
class Boy : public Person
{
Q_OBJECT
public:
Boy(QObject * parent = 0);
};
class Girl : public Person
{
Q_OBJECT
public:
Girl(QObject * parent = 0);
};
#endif // PERSON_H
~~~
person.cpp:
~~~
#include "person.h"
ShoeDescription::ShoeDescription(QObject *parent)
: QObject(parent), m_size(0), m_price(0)
{
}
int ShoeDescription::size() const
{
return m_size;
}
void ShoeDescription::setSize(int s)
{
m_size = s;
}
QColor ShoeDescription::color() const
{
return m_color;
}
void ShoeDescription::setColor(const QColor &c)
{
m_color = c;
}
QString ShoeDescription::brand() const
{
return m_brand;
}
void ShoeDescription::setBrand(const QString &b)
{
m_brand = b;
}
qreal ShoeDescription::price() const
{
return m_price;
}
void ShoeDescription::setPrice(qreal p)
{
m_price = p;
}
Person::Person(QObject *parent)
: QObject(parent)
{
}
QString Person::name() const
{
return m_name;
}
void Person::setName(const QString &n)
{
m_name = n;
}
ShoeDescription *Person::shoe()
{
return &m_shoe;
}
Boy::Boy(QObject * parent)
: Person(parent)
{
}
Girl::Girl(QObject * parent)
: Person(parent)
{
}
~~~
最后来看main.cpp:
~~~
#include <QCoreApplication>
#include <QQmlEngine>
#include <QQmlComponent>
#include <QDebug>
#include "birthdayparty.h"
#include "person.h"
int main(int argc, char ** argv)
{
QCoreApplication app(argc, argv);
qmlRegisterType<BirthdayParty>("People", 1,0, "BirthdayParty");
qmlRegisterType<ShoeDescription>(); // 注册该类型,但不进行实例化,这与Person类相同
qmlRegisterType<Person>();
qmlRegisterType<Boy>("People", 1,0, "Boy");
qmlRegisterType<Girl>("People", 1,0, "Girl");
QQmlEngine engine;
QQmlComponent component(&engine, QUrl("qrc:example.qml"));
BirthdayParty *party = qobject_cast<BirthdayParty *>(component.create());
if (party && party->host()) {
qWarning() << party->host()->name() << "is having a birthday!";
if (qobject_cast<Boy *>(party->host()))
qWarning() << "He is inviting:";
else
qWarning() << "She is inviting:";
Person *bestShoe = 0;
for (int ii = 0; ii < party->guestCount(); ++ii) { // 比较每个客人鞋子价格,得出谁穿着最好的鞋
Person *guest = party->guest(ii);
qWarning() << " " << guest->name();
if (!bestShoe || bestShoe->shoe()->price() < guest->shoe()->price())
bestShoe = guest;
}
if (bestShoe)
qWarning() << bestShoe->name() << "is wearing the best shoes!";
} else {
qWarning() << component.errors();
}
return 0;
}
~~~
运行效果:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd088659f.jpg)
可以看到,要想实现类似
~~~
shoe { size: 12; color: "white"; brand: "Bikey"; price: 90.0 }
~~~
这种“群属性”的设置方法,我们只需要设置shoe为只读就可以了,这样QML编译器就不会寻找shoe后面的冒号,进入大括号后的赋值语句,实际上是对ShoeDescription属性的赋值。
那么如果我们想要实现这种写法
~~~
shoe: ShoeDescription { size: 12; color: "white"; brand: "Bikey"; price: 90.0 }
~~~
该如何改动呢?
第一点是shoe属性的声明,它必须是可读写的:
Q_PROPERTY(ShoeDescription *shoe READ shoe WRITE setShoe)
当然还有对应的setShoe()函数的实现。
第二点是在main.cpp中QML类型的注册:
qmlRegisterType("People", 1,0, "ShoeDescription");
这里的ShoeDescription就需要被实例化了。
这样就可以再次运行了。
25——Extending QML – Methods Example
最后更新于:2022-04-01 07:22:01
本系列所有文章可以在这里查看[http://blog.csdn.net/cloud_castle/article/category/2123873](http://blog.csdn.net/cloud_castle/article/category/2123873)
接上文[Qt5官方demo解析集24——Extending QML - Default Property Example](http://blog.csdn.net/cloud_castle/article/details/37384881)
这个例子主要向我们介绍了在QML类型中定义函数的方法。
person.h:
~~~
#ifndef PERSON_H
#define PERSON_H
#include <QObject>
class Person : public QObject // 基本的person类没有改变,删掉了这个demo用不上的Boy和Girl
{
Q_OBJECT
Q_PROPERTY(QString name READ name WRITE setName)
Q_PROPERTY(int shoeSize READ shoeSize WRITE setShoeSize)
public:
Person(QObject *parent = 0);
QString name() const;
void setName(const QString &);
int shoeSize() const;
void setShoeSize(int);
private:
QString m_name;
int m_shoeSize;
};
#endif // PERSON_H
~~~
person.cpp:
~~~
#include "person.h"
Person::Person(QObject *parent)
: QObject(parent), m_shoeSize(0)
{
}
QString Person::name() const
{
return m_name;
}
void Person::setName(const QString &n)
{
m_name = n;
}
int Person::shoeSize() const
{
return m_shoeSize;
}
void Person::setShoeSize(int s)
{
m_shoeSize = s;
}
~~~
birthdayparty.h:
~~~
#ifndef BIRTHDAYPARTY_H
#define BIRTHDAYPARTY_H
#include <QObject>
#include <QQmlListProperty>
#include "person.h"
class BirthdayParty : public QObject
{
Q_OBJECT
Q_PROPERTY(Person *host READ host WRITE setHost)
Q_PROPERTY(QQmlListProperty<Person> guests READ guests)
public:
BirthdayParty(QObject *parent = 0);
Person *host() const;
void setHost(Person *);
QQmlListProperty<Person> guests();
int guestCount() const;
Person *guest(int) const;
// ![0] // 这里定义了一个函数
Q_INVOKABLE void invite(const QString &name); // 为了在QML中能够调用该函数,我们需要使用宏Q_INVOKABLE
// ![0] // 该宏使得这个函数可以被通过元对象系统调用
// 否则在QML中对其调用时编译器将抱怨invite未定义
private:
Person *m_host;
QList<Person *> m_guests;
};
#endif // BIRTHDAYPARTY_H
~~~
birthdayparty.cpp:
~~~
#include "birthdayparty.h"
BirthdayParty::BirthdayParty(QObject *parent)
: QObject(parent), m_host(0)
{
}
// ![0]
Person *BirthdayParty::host() const
{
return m_host;
}
void BirthdayParty::setHost(Person *c)
{
m_host = c;
}
QQmlListProperty<Person> BirthdayParty::guests()
{
return QQmlListProperty<Person>(this, m_guests);
}
int BirthdayParty::guestCount() const
{
return m_guests.count();
}
Person *BirthdayParty::guest(int index) const
{
return m_guests.at(index);
}
void BirthdayParty::invite(const QString &name) // 函数实现
{
Person *person = new Person(this);
person->setName(name);
m_guests.append(person);
}
// ![0]
~~~
example.qml:
~~~
import QtQuick 2.0
import People 1.0
BirthdayParty {
host: Person {
name: "Bob Jones"
shoeSize: 12
}
guests: [
Person { name: "Leo Hodges" },
Person { name: "Jack Smith" },
Person { name: "Anne Brown" }
]
// ![0]
Component.onCompleted: invite("William Green") // 我们需要在一个handler中调用这个函数
// ![0] // 这里是在组件完成时调用
}
~~~
main.cpp:
~~~
#include <QCoreApplication>
#include <QQmlEngine>
#include <QQmlComponent>
#include <QDebug>
#include "birthdayparty.h"
#include "person.h"
int main(int argc, char ** argv)
{
QCoreApplication app(argc, argv);
qmlRegisterType<BirthdayParty>("People", 1,0, "BirthdayParty");
qmlRegisterType<Person>("People", 1,0, "Person");
QQmlEngine engine;
QQmlComponent component(&engine, QUrl("qrc:example.qml"));
BirthdayParty *party = qobject_cast<BirthdayParty *>(component.create());
if (party && party->host()) {
qWarning() << party->host()->name() << "is having a birthday!";
qWarning() << "They are inviting:";
for (int ii = 0; ii < party->guestCount(); ++ii)
qWarning() << " " << party->guest(ii)->name();
} else {
qWarning() << component.errors();
}
return 0;
}
~~~
运行结果:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd086574c.jpg)
可以看到前三位客人是作为guests属性值初始化就存在的,而William Green则是在组件加载完成后才"被邀请"的。
24——Extending QML – Default Property Example
最后更新于:2022-04-01 07:21:59
本系列所有文章可以在这里查看[http://blog.csdn.net/cloud_castle/article/category/2123873](http://blog.csdn.net/cloud_castle/article/category/2123873)
接上文[](http://blog.csdn.net/cloud_castle/article/details/37355839)[Qt5官方demo解析集23——Extending QML - Inheritance and Coercion Example](http://blog.csdn.net/cloud_castle/article/details/37363547)
有时我们看到某个QML类型的声明中,某些数据并没有放在属性 + :后面,它们实际上属于这个类型的默认属性。而这个demo则向我们介绍了这个技术。
这个例子与上一篇几乎没有什么改动,除了两个小地方。
第一处在QML描述文件中example.qml:
~~~
import People 1.0
// ![0]
BirthdayParty {
host: Boy {
name: "Bob Jones"
shoeSize: 12
}
Boy { name: "Leo Hodges" } // 与前面不同的是,这三个Person并没有被放置在guests中
Boy { name: "Jack Smith" } // 但是它们的值被赋予了该类型的默认属性"guests"
Girl { name: "Anne Brown" }
}
// ![0]
~~~
另一个地方则是我们BirthdayParty类的声明,BirthdayParty.h:
~~~
#ifndef BIRTHDAYPARTY_H
#define BIRTHDAYPARTY_H
#include <QObject>
#include <QQmlListProperty>
#include "person.h"
// ![0]
class BirthdayParty : public QObject
{
Q_OBJECT
Q_PROPERTY(Person *host READ host WRITE setHost)
Q_PROPERTY(QQmlListProperty<Person> guests READ guests)
Q_CLASSINFO("DefaultProperty", "guests") // Q_CLASSINFO宏用来为类添加额外的信息
public: // 而这些信息可以被添加到该类的元对象信息中
BirthdayParty(QObject *parent = 0); // 这里为BirthdayParty类添加了一个默认属性"guests"
Person *host() const;
void setHost(Person *);
QQmlListProperty<Person> guests();
int guestCount() const;
Person *guest(int) const;
private:
Person *m_host;
QList<Person *> m_guests;
};
// ![0]
#endif // BIRTHDAYPARTY_H
~~~
23——Extending QML – Inheritance and Coercion Example
最后更新于:2022-04-01 07:21:57
本系列所有文章可以在这里查看[http://blog.csdn.net/cloud_castle/article/category/2123873](http://blog.csdn.net/cloud_castle/article/category/2123873)
接上文[Qt5官方demo解析集22——Extending QML - Object and List Property Types Example](http://blog.csdn.net/cloud_castle/article/details/37355839)
在上一个例子中,我们为BirthdayParty类创建了带有一个列表参数的属性guests,而这个列表参数的类型都是一致的,即Person。然而,使用QML的类型转换机制,我们可以使这个列表参数的类型变得不同。既然说到了这里,我们先来看看example.qml:
~~~
import People 1.0
// ![0]
BirthdayParty {
host: Boy {
name: "Bob Jones"
shoeSize: 12
}
guests: [ // 要知道注册属性使我们需要给其一个固定的参数类型,如int,bool,甚至自定义的类型
Boy { name: "Leo Hodges" }, // 因此想要得到该代码的效果我们需要使用继承
Boy { name: "Jack Smith" }, // Boy与Girl均继承自Person,而我们仅仅将guests注册为Person就够了
Girl { name: "Anne Brown" }
]
}
// ![0]
~~~
由于BirthdayParty类的代码没有改变,我们就看看Person.h:
~~~
#ifndef PERSON_H
#define PERSON_H
#include <QObject>
class Person : public QObject // Person也没有改动
{
Q_OBJECT
Q_PROPERTY(QString name READ name WRITE setName)
Q_PROPERTY(int shoeSize READ shoeSize WRITE setShoeSize)
public:
Person(QObject *parent = 0);
QString name() const;
void setName(const QString &);
int shoeSize() const;
void setShoeSize(int);
private:
QString m_name;
int m_shoeSize;
};
// ![0]
class Boy : public Person // 创建一个Boy继承自Person
{
Q_OBJECT
public:
Boy(QObject * parent = 0); // 需要一个最基本的构造函数使得QML可以实例化这个对象
};
//! [girl class]
class Girl : public Person // Girl同Boy
{
Q_OBJECT
public:
Girl(QObject * parent = 0);
};
//! [girl class]
// ![0]
#endif // PERSON_H
~~~
Person.cpp:
~~~
#include "person.h"
Person::Person(QObject *parent)
: QObject(parent), m_shoeSize(0)
{
}
QString Person::name() const
{
return m_name;
}
void Person::setName(const QString &n)
{
m_name = n;
}
int Person::shoeSize() const
{
return m_shoeSize;
}
void Person::setShoeSize(int s)
{
m_shoeSize = s;
}
// ![1]
Boy::Boy(QObject * parent) // 由于该例子只是简单演示继承,因此也并未为派生类添加额外的功能
: Person(parent)
{
}
Girl::Girl(QObject * parent) // 构造函数是必须的
: Person(parent)
{
}
// ![1]
~~~
main.cpp:
~~~
#include <QCoreApplication>
#include <QQmlEngine>
#include <QQmlComponent>
#include <QDebug>
#include "birthdayparty.h"
#include "person.h"
int main(int argc, char ** argv)
{
QCoreApplication app(argc, argv);
qmlRegisterType<BirthdayParty>("People", 1,0, "BirthdayParty");
//![0]
qmlRegisterType<Person>(); // 我们依然需要注册Person,否则Boy与Girl无法被强制转换为Person
//![0] // 该函数参数为空,因为我们并不需要实例化一个Person的对象
//![register boy girl]
qmlRegisterType<Boy>("People", 1,0, "Boy"); // 注册Boy和Girl
qmlRegisterType<Girl>("People", 1,0, "Girl");
//![register boy girl]
QQmlEngine engine;
QQmlComponent component(&engine, QUrl("qrc:example.qml"));
BirthdayParty *party = qobject_cast<BirthdayParty *>(component.create());
if (party && party->host()) {
qWarning() << party->host()->name() << "is having a birthday!";
if (qobject_cast<Boy *>(party->host())) // 判断主人为男孩还是女孩
qWarning() << "He is inviting:";
else
qWarning() << "She is inviting:";
for (int ii = 0; ii < party->guestCount(); ++ii)
qWarning() << " " << party->guest(ii)->name();
} else {
qWarning() << component.errors();
}
return 0;
}
~~~
22——Extending QML – Object and List Property Types Example
最后更新于:2022-04-01 07:21:54
本系列所有文章可以在这里查看[http://blog.csdn.net/cloud_castle/article/category/2123873](http://blog.csdn.net/cloud_castle/article/category/2123873)
接上文[Qt5官方demo解析集21——Extending QML - Adding Types Example](http://blog.csdn.net/cloud_castle/article/details/37055429)
在上一个例子中我们基于C++创建了一个自定义的QML类型,接下来,我们将该类作为另一个类的属性类型,定义了另一个birthdayparty类。这个例子与[Qt5官方demo解析集19——Chapter 5: Using List Property Types](http://blog.csdn.net/cloud_castle/article/details/36898495)是十分相似的,只不过在这个例子中只提供了对列表属性的访问能力。[](http://blog.csdn.net/cloud_castle/article/details/36898495)
项目文件如下:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd083f9a1.jpg)
Person的定义和声明与上一篇博文中所贴出的一致,因此我们先看看birthdayparty.h:
~~~
#ifndef BIRTHDAYPARTY_H
#define BIRTHDAYPARTY_H
#include <QObject>
#include <QQmlListProperty> // 这个类型我们在解析集19中有讨论过,它用来提供带有列表成员的属性
#include "person.h"
// ![0]
class BirthdayParty : public QObject
{
Q_OBJECT
// ![0]
// ![1]
Q_PROPERTY(Person *host READ host WRITE setHost) // host属性,指向一个Person对象
// ![1]
// ![2]
Q_PROPERTY(QQmlListProperty<Person> guests READ guests) // guests属性,指向一个Person列表
// ![2]
// ![3]
public:
BirthdayParty(QObject *parent = 0);
Person *host() const;
void setHost(Person *);
QQmlListProperty<Person> guests(); // 上面是属性的读写函数
int guestCount() const; // 下面是自定义的用来返回客人数与读取某个客人的函数
Person *guest(int) const;
private:
Person *m_host;
QList<Person *> m_guests; // 对应的需要将m_guests定义成QList<Person *>类型的成员列表
};
// ![3]
#endif // BIRTHDAYPARTY_H
~~~
birthdayparty.cpp:
~~~
#include "birthdayparty.h"
BirthdayParty::BirthdayParty(QObject *parent)
: QObject(parent), m_host(0) // 初始化参数赋值,这个0相当于NULL
{
}
// ![0]
Person *BirthdayParty::host() const
{
return m_host;
}
void BirthdayParty::setHost(Person *c)
{
m_host = c;
}
QQmlListProperty<Person> BirthdayParty::guests()
{
return QQmlListProperty<Person>(this, m_guests); // 由一个QList得到QQmlListProperty值,函数原型如下
}
int BirthdayParty::guestCount() const // 数据成员一般都为私有,要调用它的函数,我们需要提供一个公共的接口
{
return m_guests.count();
}
Person *BirthdayParty::guest(int index) const
{
return m_guests.at(index);
}
~~~
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd084e8e6.jpg)
example.qml:
~~~
import People 1.0 // 由于在main函数中将Person与Birthdayparty的名称空间均注册为People 1.0,导入它我们可以同时使用这两个类型
// ![0]
BirthdayParty {
host: Person { // host参数为单Person
name: "Bob Jones"
shoeSize: 12
}
guests: [ // guests参数为多Person
Person { name: "Leo Hodges" },
Person { name: "Jack Smith" },
Person { name: "Anne Brown" }
]
}
// ![0]
~~~
main.cpp:
~~~
#include <QCoreApplication>
#include <QQmlEngine>
#include <QQmlComponent>
#include <QDebug>
#include "birthdayparty.h"
#include "person.h"
int main(int argc, char ** argv)
{
QCoreApplication app(argc, argv);
//![register list]
qmlRegisterType<BirthdayParty>("People", 1,0, "BirthdayParty"); // QML类型注册
qmlRegisterType<Person>("People", 1,0, "Person");
//![register list]
QQmlEngine engine;
QQmlComponent component(&engine, QUrl("qrc:example.qml"));
BirthdayParty *party = qobject_cast<BirthdayParty *>(component.create());
if (party && party->host()) { // 由于host为"Bob Jones",该if得以进入
qWarning() << party->host()->name() << "is having a birthday!";
qWarning() << "They are inviting:";
for (int ii = 0; ii < party->guestCount(); ++ii) // 调用公共接口得到客人数
qWarning() << " " << party->guest(ii)->name(); // 逐一访问其姓名
} else {
qWarning() << component.errors();
}
return 0;
}
~~~
21——Extending QML – Adding Types Example
最后更新于:2022-04-01 07:21:52
本系列所有文章可以在这里查看[http://blog.csdn.net/cloud_castle/article/category/2123873](http://blog.csdn.net/cloud_castle/article/category/2123873)
又是一个新的系列了,不过这个系列和我们之前的Chapter系列是及其相似的,但是不过呢,Chapter主要演示了如何使用C++创建具有可视性的类型以扩展我们的QML,而这个系列则关注于如何使用C++扩展QML非可视化的内容。
这里第一个小例子与Chapter的第一个小例子及其类似:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd0815180.jpg)
person是我们自定义的C++类,然后我们将其注册为QML类型供资源文件中的example.qml使用。
person.h,这个类与之前的piechart没有太大区别:
~~~
#ifndef PERSON_H
#define PERSON_H
#include <QObject>
//![0]
class Person : public QObject // 要注意的是由于这个对象并不需要可视化,我们继承最基础的QObject就可以了
{
Q_OBJECT // 因为QML组件基于元对象系统,所以QObject和Q_OBJECT都不能少
Q_PROPERTY(QString name READ name WRITE setName) // 两个自定义属性
Q_PROPERTY(int shoeSize READ shoeSize WRITE setShoeSize)
public:
Person(QObject *parent = 0);
QString name() const;
void setName(const QString &);
int shoeSize() const;
void setShoeSize(int);
private:
QString m_name;
int m_shoeSize;
};
//![0]
~~~
person.cpp:
~~~
#include "person.h"
// ![0]
Person::Person(QObject *parent)
: QObject(parent), m_shoeSize(0)
{
}
QString Person::name() const
{
return m_name;
}
void Person::setName(const QString &n)
{
m_name = n;
}
int Person::shoeSize() const
{
return m_shoeSize;
}
void Person::setShoeSize(int s)
{
m_shoeSize = s;
}
~~~
example.qml:
~~~
import People 1.0
Person { // 非可视化组件,我们也不再需要以Item作为父对象
name: "Bob Jones"
shoeSize: 12
}
~~~
main.cpp:
~~~
#include <QCoreApplication> // 注意到Chapter中为QGuiApplication
#include <QQmlEngine> // 提供QML组件的运行环境
#include <QQmlComponent> // 提供对QML组件的封装与访问
#include <QDebug>
#include "person.h"
int main(int argc, char ** argv)
{
QCoreApplication app(argc, argv);
//![0]
qmlRegisterType<Person>("People", 1,0, "Person"); // 注册QML类型
//![0]
QQmlEngine engine;
QQmlComponent component(&engine, QUrl("qrc:example.qml")); // 获取QML文件中的组件
Person *person = qobject_cast<Person *>(component.create()); // 创建该组件的实例化对象
if (person) {
qWarning() << "The person's name is" << person->name(); // 依然是通过->访问其成员函数
qWarning() << "They wear a" << person->shoeSize() << "sized shoe";
} else {
qWarning() << component.errors(); // 类型转换失败则输出错误信息
}
return 0;
}
~~~
由于没有界面,实际运行就是一行控制台的输出:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd08271ca.jpg)
20——Chapter 6: Writing an Extension Plugin
最后更新于:2022-04-01 07:21:50
本系列所有文章可以在这里查看[http://blog.csdn.net/cloud_castle/article/category/2123873](http://blog.csdn.net/cloud_castle/article/category/2123873)
接上文[Qt5官方demo解析集19——Chapter 5: Using List Property Types](http://blog.csdn.net/cloud_castle/article/details/36898495)
在前文中我们定义的PieChart和PieSlice这两个自定义QML类型只用来供app.qml文件使用,如果希望我们所定义的类型可以被多个qml使用,那么可以将其创建为可扩展的插件。由于是将C++定义的类创建为供QML使用的插件,所以这与我们原先创建C++的插件略有不同。
1、首先我们需要继承QQmlExtensionPlugin类,这是一个抽象基类,提供了可供QML装载的插件。
2、接着我们需要在其子类中使用Q_PLUGIN_METADATA宏将其这个插件注册到Qt的元对象系统中。
3、重写纯虚函数registerTypes(),并在其中使用qmlRegisterType()注册插件中的QML类型,这与我们之前在main函数中做的一样。
4、然后我们需要编写一个插件的工程文件,包括TEMPLATE、CONFIG、DESTDIR、TARGET等等。
5、最后,我们还需要创建一个qmldir文件来描述这个插件。
先看下工程目录,其中piechart与pieslice并没有任何变化:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd07e93e5.jpg)
这实际上是两个工程,我们完全可以单独编译import来生成dll插件,然后放在合适的地方供app工程使用。我们先来看import工程中的文件
chartsplugin.h:
~~~
#ifndef CHARTSPLUGIN_H
#define CHARTSPLUGIN_H
//![0]
#include <QQmlExtensionPlugin>
class ChartsPlugin : public QQmlExtensionPlugin // 继承QQmlExtensionPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface") // 为这个插件定义了一个唯一的接口,并注册至元对象系统
public:
void registerTypes(const char *uri); // 注册类型函数重载
};
//![0]
#endif
~~~
chartsplugin.cpp:
~~~
#include "chartsplugin.h"
//![0]
#include "piechart.h"
#include "pieslice.h"
#include <qqml.h>
void ChartsPlugin::registerTypes(const char *uri) // 实现插件中类型的注册
{
qmlRegisterType<PieChart>(uri, 1, 0, "PieChart");
qmlRegisterType<PieSlice>(uri, 1, 0, "PieSlice");
}
//![0]
~~~
可以看到QML插件注册的代码也很间断,但真正的实现还在于下面两个文件:
qmldir这个文件只有两句话:
~~~
module Charts // 定义了组件名称空间为Charts,这个名称也是我们最后import时使用的
plugin chartsplugin // 定义插件,与上面的chartsplugin一致
~~~
然后是我们的import.pro文件:
~~~
TEMPLATE = lib // 生成库文件
CONFIG += plugin // 该库是一个插件
QT += qml quick
DESTDIR = ../Charts // 从Debug目录跳出到Build目录,并建立Charts目录,以存放dll与qmldir文件
TARGET = $$qtLibraryTarget(chartsplugin)
HEADERS += piechart.h \
pieslice.h \
chartsplugin.h
SOURCES += piechart.cpp \
pieslice.cpp \
chartsplugin.cpp
DESTPATH=$$[QT_INSTALL_EXAMPLES]/qml/tutorials/extending/chapter6-plugins/Charts // 设置了一个变量指向这个Charts目录
target.path=$$DESTPATH
qmldir.files=$$PWD/qmldir
qmldir.path=$$DESTPATH
INSTALLS += target qmldir
OTHER_FILES += qmldir
# Copy the qmldir file to the same folder as the plugin binary
QMAKE_POST_LINK += $$QMAKE_COPY $$replace($$list($$quote($$PWD/qmldir) $$DESTDIR), /, $$QMAKE_DIR_SEP)
~~~
这样,在这个工程编译后,我们将在build目录中的Charts文件夹中找到我们的chartsplugind.dll与qmldir文件。d表示由debug编译产生。
接下来的app项目调用了这个插件并绘制了这个饼状图:
app.qml:
~~~
import QtQuick 2.0
import Charts 1.0 // 这里会有一条红色波浪线,QtCreator会抱怨可能找不到这个模块
// 你可以不管它,也可以设置qmlplugindump让QtQuick获知到这个模块的信息
Item {
width: 300; height: 200
PieChart { // 我们可以像前面的例子一样使用我们自定义的类型
anchors.centerIn: parent
width: 100; height: 100
slices: [
PieSlice {
anchors.fill: parent
color: "red"
fromAngle: 0; angleSpan: 110
},
PieSlice {
anchors.fill: parent
color: "black"
fromAngle: 110; angleSpan: 50
},
PieSlice {
anchors.fill: parent
color: "blue"
fromAngle: 160; angleSpan: 100
}
]
}
}
~~~
main.cpp中不需要做额外的工作:
~~~
#include <QtQuick/QQuickView>
#include <QGuiApplication>
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
QQuickView view;
view.setResizeMode(QQuickView::SizeRootObjectToView);
view.setSource(QUrl("qrc:///app.qml"));
view.show();
return app.exec();
}
~~~
app.pro:
~~~
TARGET = chapter6-plugins // 设置项目名
QT += qml quick
# Avoid going to debug/release subdirectory
# so that our application will see the
# import path for the Charts module.
win32: DESTDIR = ./ // 这一句将程序从debug移到上级build目录,也就是Charts的同级目录,这样使得程序可以到上面生成的插件
SOURCES += main.cpp
RESOURCES += app.qrc
~~~
最后还有一个根工程文件chapter6-plugins.pro:
~~~
TEMPLATE = subdirs // 编译下面SUBDIRS定义的子目录
CONFIG += ordered // 顺序编译,可能是基于效率不太被推荐,不过在这个小例子中就无所谓了
SUBDIRS = \
import \ // 先编译import目录里的工程
app.pro // 然后编译app工程
~~~
最后还是看下效果呗~
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd0802ab6.jpg)
19——Chapter 5: Using List Property Types
最后更新于:2022-04-01 07:21:47
本系列所有文章可以在这里查看[http://blog.csdn.net/cloud_castle/article/category/2123873](http://blog.csdn.net/cloud_castle/article/category/2123873)
接上文[Qt5官方demo解析集18——Chapter 4: Using Custom Property Types](http://blog.csdn.net/cloud_castle/article/details/36896525)
上个例子向我们展示了如何为QML调用的C++类型添加自定义类型的属性,在这个例子中我们更进一步,将这个类型更换为一个PieSlice的列表,以此得到更丰富的处理能力。
项目文件与上个例子是一样的。
还是先看看piechart.h:
~~~
#ifndef PIECHART_H
#define PIECHART_H
#include <QtQuick/QQuickItem>
class PieSlice; // 由于这里只使用了PieSlice的指针,我们可以使用前向声明
//![0]
class PieChart : public QQuickItem
{
Q_OBJECT
Q_PROPERTY(QQmlListProperty<PieSlice> slices READ slices) // 我们使用QQmlListProperty<PieSlice>来定义slices属性的类型
//![0] // 该属性是一个PieSlice的列表,而QQmlListProperty则为我们提供了在QML中扩充该列表成员的能力
Q_PROPERTY(QString name READ name WRITE setName) // 由于QQmlListProperty的机制,即使我们没有定义该属性的WRITE功能,它也是可改变的
//![1]
public:
//![1]
PieChart(QQuickItem *parent = 0);
QString name() const;
void setName(const QString &name);
//![2]
QQmlListProperty<PieSlice> slices(); // 我们在属性中仅仅定义了可读性,因此这里并没有声明setSlices()函数
private:
static void append_slice(QQmlListProperty<PieSlice> *list, PieSlice *slice); // 一个向向列表中添加属性成员的函数
QString m_name;
QList<PieSlice *> m_slices; // 定义一个对应的数据成员
};
//![2]
#endif
~~~
piechart.cpp:
~~~
#include "piechart.h"
#include "pieslice.h"
PieChart::PieChart(QQuickItem *parent)
: QQuickItem(parent)
{
}
QString PieChart::name() const
{
return m_name;
}
void PieChart::setName(const QString &name)
{
m_name = name;
}
//![0]
QQmlListProperty<PieSlice> PieChart::slices() // 用来返回列表中的属性
{
return QQmlListProperty<PieSlice>(this, 0, &PieChart::append_slice, 0, 0, 0); // 该函数原型如下图
}
void PieChart::append_slice(QQmlListProperty<PieSlice> *list, PieSlice *slice) // 每当我们向slices中添加一个PieSlice都会调用这个静态的内部函数
{
PieChart *chart = qobject_cast<PieChart *>(list->object); // 取得列表的父对象
if (chart) { // 并设置为新属性成员的父对象
slice->setParentItem(chart);
chart->m_slices.append(slice);
}
}
//![0]
~~~
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd07c7730.jpg)
pieslice.h:
~~~
#ifndef PIESLICE_H
#define PIESLICE_H
#include <QtQuick/QQuickPaintedItem>
#include <QColor>
//![0]
class PieSlice : public QQuickPaintedItem
{
Q_OBJECT
Q_PROPERTY(QColor color READ color WRITE setColor)
Q_PROPERTY(int fromAngle READ fromAngle WRITE setFromAngle) // 添加了扇形的起始角与角度范围属性
Q_PROPERTY(int angleSpan READ angleSpan WRITE setAngleSpan) // 用来绘制饼状图的多个扇形的不同颜色
//![0]
public:
PieSlice(QQuickItem *parent = 0);
QColor color() const;
void setColor(const QColor &color);
int fromAngle() const;
void setFromAngle(int angle);
int angleSpan() const;
void setAngleSpan(int span);
void paint(QPainter *painter);
private:
QColor m_color;
int m_fromAngle; // 注意与定义的属性类型对应
int m_angleSpan;
};
#endif
~~~
pieslice.cpp:
~~~
#include "pieslice.h"
#include <QPainter>
PieSlice::PieSlice(QQuickItem *parent)
: QQuickPaintedItem(parent)
{
}
QColor PieSlice::color() const
{
return m_color;
}
void PieSlice::setColor(const QColor &color)
{
m_color = color;
}
int PieSlice::fromAngle() const
{
return m_fromAngle;
}
void PieSlice::setFromAngle(int angle)
{
m_fromAngle = angle;
}
int PieSlice::angleSpan() const
{
return m_angleSpan;
}
void PieSlice::setAngleSpan(int angle)
{
m_angleSpan = angle;
}
void PieSlice::paint(QPainter *painter)
{
QPen pen(m_color, 2);
painter->setPen(pen);
painter->setRenderHints(QPainter::Antialiasing, true);
painter->drawPie(boundingRect().adjusted(1, 1, -1, -1), m_fromAngle * 16, m_angleSpan * 16);
}
~~~
app.qml:
~~~
import Charts 1.0
import QtQuick 2.0
Item {
width: 300; height: 200
PieChart {
anchors.centerIn: parent
width: 100; height: 100
slices: [ // 使用中括号括起列表中的成员
PieSlice {
anchors.fill: parent
color: "red"
fromAngle: 0; angleSpan: 110
},
PieSlice {
anchors.fill: parent
color: "black"
fromAngle: 110; angleSpan: 50
},
PieSlice {
anchors.fill: parent
color: "blue"
fromAngle: 160; angleSpan: 100
}
]
}
}
~~~
运行效果如下:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd07dac1c.jpg)
18——Chapter 4: Using Custom Property Types
最后更新于:2022-04-01 07:21:45
本系列所有文章可以在这里查看[http://blog.csdn.net/cloud_castle/article/category/2123873](http://blog.csdn.net/cloud_castle/article/category/2123873)
接上文[Qt5官方demo解析集17——Chapter 3: Adding Property Bindings](http://blog.csdn.net/cloud_castle/article/details/36886779)
在前面的“饼状图”Demo中,我们为这个自定义类型定义了"name"和"color"属性,他们都是基于Qt内置类型"QString"和"QColor",这个例子则向我们演示了如何为这个PieChart添加自定义类型的属性。使用自定义类型的属性,更方便我们模块化编程。
这个项目中多了一个pieslice类,它用于饼状图的实际绘制,而PieChart仅用来提供框架:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd07a35e1.jpg)
piechart.h:
~~~
#ifndef PIECHART_H
#define PIECHART_H
#include <QtQuick/QQuickItem>
class PieSlice;
//![0]
class PieChart : public QQuickItem // 由于这个类不再需要进行实际绘制,所以继承自QQuickItem就可以了
{
Q_OBJECT
Q_PROPERTY(PieSlice* pieSlice READ pieSlice WRITE setPieSlice) // 我们定义了pieSlice属性,它是一个PieSlice类的指针
//![0]
Q_PROPERTY(QString name READ name WRITE setName) // color属性被移至PieSlice中
//![1]
public:
//![1]
PieChart(QQuickItem *parent = 0);
QString name() const;
void setName(const QString &name);
//![2]
PieSlice *pieSlice() const;
void setPieSlice(PieSlice *pieSlice);
//![2]
private:
QString m_name;
PieSlice *m_pieSlice;
//![3]
};
//![3]
#endif
~~~
piechart.cpp:
~~~
#include "piechart.h"
#include "pieslice.h"
PieChart::PieChart(QQuickItem *parent)
: QQuickItem(parent)
{
}
QString PieChart::name() const
{
return m_name;
}
void PieChart::setName(const QString &name)
{
m_name = name;
}
PieSlice *PieChart::pieSlice() const
{
return m_pieSlice;
}
//![0]
void PieChart::setPieSlice(PieSlice *pieSlice)
{
m_pieSlice = pieSlice;
pieSlice->setParentItem(this); // QML的可视化组件必须拥有一个父对象,否则无法显示
}
//![0]
~~~
将具体绘制放在一个单独的类中,pieslice.h:
~~~
#ifndef PIESLICE_H
#define PIESLICE_H
#include <QtQuick/QQuickPaintedItem>
#include <QColor>
//![0]
class PieSlice : public QQuickPaintedItem // 继承这个类以重载Paint
{
Q_OBJECT
Q_PROPERTY(QColor color READ color WRITE setColor)
public:
PieSlice(QQuickItem *parent = 0);
QColor color() const;
void setColor(const QColor &color);
void paint(QPainter *painter);
private:
QColor m_color;
};
//![0]
#endif
~~~
pieslice.cpp:
~~~
#include "pieslice.h"
#include <QPainter>
PieSlice::PieSlice(QQuickItem *parent)
: QQuickPaintedItem(parent)
{
}
QColor PieSlice::color() const
{
return m_color;
}
void PieSlice::setColor(const QColor &color)
{
m_color = color;
}
void PieSlice::paint(QPainter *painter)
{
QPen pen(m_color, 2);
painter->setPen(pen);
painter->setRenderHints(QPainter::Antialiasing, true);
painter->drawPie(boundingRect().adjusted(1, 1, -1, -1), 90 * 16, 290 * 16);
}
~~~
main.cpp:
~~~
#include "piechart.h"
#include "pieslice.h"
#include <QtQuick/QQuickView>
#include <QGuiApplication>
//![0]
int main(int argc, char *argv[])
{
//![0]
QGuiApplication app(argc, argv);
qmlRegisterType<PieChart>("Charts", 1, 0, "PieChart"); // 注册这两个C++类,定义在一个命名空间中
//![1]
qmlRegisterType<PieSlice>("Charts", 1, 0, "PieSlice");
//![1]
QQuickView view;
view.setResizeMode(QQuickView::SizeRootObjectToView);
view.setSource(QUrl("qrc:///app.qml"));
view.show();
return app.exec();
//![2]
}
//![2]
~~~
app.qml:
~~~
import Charts 1.0
import QtQuick 2.0
Item {
width: 300; height: 200
PieChart {
id: chart
anchors.centerIn: parent
width: 100; height: 100
pieSlice: PieSlice { // PieSlice属性的设置
anchors.fill: parent
color: "red"
}
}
Component.onCompleted: console.log("The pie is colored " + chart.pieSlice.color) // 最后在组件完成时显示这个饼状图的RGB值
}
//![0]
~~~
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-18_569cbd07b8e03.jpg)