工程存放的路径在:Examples\Qt-XX.XX.XX\widgets\painting\affine
其中XX.XX.XX为Qt的版本号,如:5.14.1。
该工程有如下个人认为的难点、亮点:
对XFormView类的drawPixmapType函数解释:
void XFormView::drawPixmapType(QPainter *painter)
{
QPointF center(m_pixmap.width() / qreal(2), m_pixmap.height() / qreal(2));
/* 将painter移动(ctrlPoints.at(0) - center)距离,也即将HoverPoints类对象pts
的第一个控制点移动到位图的中心位置,以便第一个控制点绘制在位图的中心*/
painter->translate(ctrlPoints.at(0) - center);
// 进行变换,以便位图绕着自己的中心轴旋转
painter->translate(center);
painter->rotate(m_rotation);
painter->scale(m_scale, m_scale);
painter->shear(0, m_shear);
painter->translate(-center);
.............................// 其它代码
}
如果被绘制对象(本例是指m_pixmap对象表示的图片)的中心点和绘图对象painter的中心点不重合,这时候旋转被绘制对象,则这个对象会围绕painter设置的中心点进行旋转,会转一个大圈。那么怎么做才能让它在任何位置的时候,都围绕自己的轴心进行旋转?解决思路如下:
1. 先保存物体在世界坐标系下的坐标,即物体在世界坐标系下的中心点坐标
2. 再将物体移动到世界坐标系的原点。
3. 在世界坐标系的原点旋转好后,再移动回原来的位置,即步骤1中的提到的坐标。
上段代码的思路就是按这样来的。
- drawTextType函数同drawPixmapType函数,不再赘述。
- HoverPoints分析
构造函数分析:
HoverPoints::HoverPoints(QWidget *widget, PointShape shape)
: QObject(widget)
{
m_widget = widget;
widget->installEventFilter(this);
widget->setAttribute(Qt::WA_AcceptTouchEvents);
m_connectionType = CurveConnection;
m_sortType = NoSort;
m_shape = shape;
m_pointPen = QPen(QColor(255, 255, 255, 191), 1);
m_connectionPen = QPen(QColor(255, 255, 255, 127), 2);
m_pointBrush = QBrush(QColor(191, 191, 191, 127));
m_pointSize = QSize(11, 11);
m_currentIndex = -1;
m_editable = true;
m_enabled = true;
connect(this, &HoverPoints::pointsChanged,
m_widget, QOverload<>::of(&QWidget::update));
}
构造函数初始化了一些变量,对各个变量作用解释如下:
- m_widget:外层传入的窗体部件对象,也即HoverPoints类附属在哪个窗体上,通过通读代码可以发现其是指外层的XFormView窗体对象。
- 函数的第5行为外层传入的窗体部件对象安装了事件过滤器。installEventFilter函数的用法请参见《installEventFilter、eventFilter函数理解》
- 第6行设置触屏事件,从后续代码可以看到本例支持触屏操作。
- m_connectionType:连接类型。取值为:直线、曲线(贝塞尔曲线)和无
- m_sortType: 点的排序类型。取值为:不排序、按点的x坐标排序、按点的y坐标排序。
- m_shape:悬浮点的现状。取值为:圆形、矩形。
- m_pointPen:用于画悬浮点的画笔。
- m_connectionPen:用于画连接线的画笔。
- m_pointBrush:用于点的画刷。
- m_pointSize:点所在形状的外围矩形宽高。
- m_currentIndex:当前在控制点容器m_points中的点的索引。
- m_editable:是否可编辑。
- m_enabled:是否可用。
eventFilter 中的鼠标左键按下流程分析:
如下鼠标左键按下时的流程:(图片下载到本地放大看):
说明:
- 本程序不仅支持鼠标还支持触摸显示屏操作。上述流程是支持鼠标且鼠标左键按下流程。
- 程序根据m_sortType来决定坐标排序类型,在构造函数中默认不排序。当排序类型为XSort 时,则从控制点的容器m_points中找到第1个横坐标大于鼠标按下时鼠标单击点的横坐标的点。当排序类型为YSort 时,则从控制点的容器m_points中找到第1个纵坐标大于鼠标按下时鼠标单击点的纵坐标的点,并记录该点在m_points中的索引号。这样做的效果是为了实现如下效果:
起始m_points中只有A、B两个控制点,则画出上图类似的A到B的线,当鼠标左键单击的点为C时,且C在A、B之间时,则画出如下直线:
当C在B 的右侧时,则画出如下的:
3 .鼠标左键按下时,当步骤2完成后,将符合条件的鼠标左键单击时所在的点插入m_points、m_locks,插入的索引号为步骤2记录的索引号然后调用 firePointChange函数。
4. firePointChange函数中根据m_sortType表示的排序规则将m_points排序。排序后再次从m_points中找到步骤2鼠标左键按下时的点在m_points中的索引m_currentIndex,然后发送pointsChanged信号。
eventFilter 中的触摸事件流程分析:
即QEvent::TouchBegin、QEvent::TouchUpdate、Qt::TouchPointReleased、QEvent::TouchEnd事件。其中最需要搞懂的是QEvent::TouchBegin、QEvent::TouchUpdate中的Qt::TouchPointPressed,实现代码如下:
// find the point, move it
const auto mappedPoints = m_fingerPointMapping.values();
QSet<int> activePoints = QSet<int>(mappedPoints.begin(), mappedPoints.end());
int activePoint = -1;
qreal distance = -1;
const int pointsCount = m_points.size();
const int activePointCount = activePoints.size();
if (pointsCount == 2 && activePointCount == 1)
{ // only two points
activePoint = activePoints.contains(0) ? 1 : 0;
}
else
{
for (int i = 0; i < pointsCount; ++i)
{
if (activePoints.contains(i))
continue;
qreal d = QLineF(touchPoint.pos(), m_points.at(i)).length();
if ((distance < 0 && d < 12 * pointSize) || d < distance)
{
distance = d;
activePoint = i;
}
}
}
if (activePoint != -1)
{
m_fingerPointMapping.insert(touchPoint.id(), activePoint);
movePoint(activePoint, touchPoint.pos());
}
其基本思路是:将触摸点的id和触摸点索引值作为QHash的键值对插入到类型为QHash的成员变量m_fingerPointMapping,以后移动、释放都从m_fingerPointMapping取出相应的点进行操作。
eventFilter 中的QEvent::Paint流程分析:
代码如下:
QWidget *that_widget = m_widget;
m_widget = nullptr;
QCoreApplication::sendEvent(object, event);
m_widget = that_widget;
paintPoints();
return true;
这里需要着重说明的是:在发送QEvent::Paint事件之前必须m_widget将先保存起来(第1句代码),然后将m_widget设置为nullptr(第2句),发送完后再将m_widget恢复为原来的。设置为nullptr是避免eventFilter 函数无限递归调用导致栈耗尽程序崩溃,恢复回来为了下次再次发送QEvent::Paint事件。
updateCtrlPoints函数分析:
void XFormView::updateCtrlPoints(const QPolygonF &points)
{
QPointF trans = points.at(0) - ctrlPoints.at(0);
if (qAbs(points.at(0).x() - points.at(1).x()) < 10
&& qAbs(points.at(0).y() - points.at(1).y()) < 10)
pts->setPoints(ctrlPoints);
if (!trans.isNull()) {
ctrlPoints[0] = points.at(0);
ctrlPoints[1] += trans;
pts->setPoints(ctrlPoints);
}
ctrlPoints = points;
QLineF line(ctrlPoints.at(0), ctrlPoints.at(1));
m_rotation = 360 - QLineF(0, 0, 1, 0).angleTo(line);
if (trans.isNull())
emit rotationChanged(int(m_rotation * 10));
}
第3行代码:就是计算通过信号pointsChanged发送过来的m_points第一个控制点和当前的第一个控制点的在x、y坐标上的偏移量。
第5-7行:检测m_points第1个控制点、第2个控制点x、y坐标上的偏移量都小于10,如果是,则控制点还是设置为当前控制点。
第8-11行:如果第3句代码算出的偏移量不为空,则当前第1个控制点更新为m_points中的第1个点,注意:在m_sortType为NoSort时,且鼠标左键按下时,m_points中的第1个点为鼠标左键按下时的点,而在XFormView类的paint函数绘制函数如:drawPixmapType、drawTextType经过如下代码将绘制中心移动到了被绘制对象(如:图片、文本)的中心了(参见前面对drawPixmapType的分析):
painter->translate(ctrlPoints.at(0) - center);
所以这样造成的现象是鼠标左键在哪单击,被绘制物体的中心就移动到鼠标左键单击的点。
当前第2个控制点累加第3行产生的偏移量,并重新设置HoverPoints类的控制点容器m_points
第15-16行根据更新后的控制点ctrlPoints算出其和水平线的夹角,并设置旋转成员变量m_rotation。根据Qt的元系统属性技术( 属性通过Q_PROPERTY关键字标识),一旦m_rotation被更新,则XFormView类的setRotation函数会自动调用,从而导致被绘制的物体呈现旋转。
程序bug说明:
HoverPoints类的m_sortType 为 XSort或YSort时,会崩溃,更改如下:
movePoint函数加入如下代码:
void HoverPoints::movePoint(int index, const QPointF &point, bool emitUpdate)
{
// 索引越界了,要判断下
if ((index < 0) || (m_points.size() <= index) )
{
return;
}
if ( m_locks.size() <= index )
{
return;
}
// 其它代码
}
firePointChange()函数加入如下代码:
void HoverPoints::firePointChange()
{
// printf("HoverPoints::firePointChange(), current=%d\n", m_currentIndex);
if (m_sortType != NoSort) {
QPointF oldCurrent;
/*if (m_currentIndex != -1) {*/
// 这里有崩溃,索引越界了,要判断下
if ((0 <= m_currentIndex) && (m_currentIndex < m_points.size()))
{
oldCurrent = m_points[m_currentIndex];
}
// 其它代码
}
XFormView类的updateCtrlPoints函数加入如下判断:
void XFormView::updateCtrlPoints(const QPolygonF &points)
{
// 右键在控制点单击时,这里有崩溃,索引越界了,要判断下
if (points.size() < 2)
{
return;
}
..................
}
未完,待续!