QGraphics族类的控件用来实现地图放缩的场景确实很方便,但是初次接触,对于某些结构设计还是有些难以理解。在此,将我成功实现的案例以及实现中途步入的误区记录下来。
1.思路
- 继承QGraphicsView实现窗口,并重写mousePressEvent、wheelEvent等函数,对场景的点击事件进行处理。
- 创建(QGraphicsPixmapItem*)m_map,承载地图。
- 创建vector容器,保存特定类型的Item指针,如std::vector<GuiCamera*> m_vecGuiCamera,std::vector<GuiLandMark*> m_vecGuiLandmark等。
- 在wheelEvent函数中,实现地图的放缩功能。
- 继承QGraphicsRectItem,QGraphicsEllipseItem,重写paint函数,去除选中时,难看的虚线边框。
- 实现单击空白处,取消选中当前item的功能。
2. 部分细节记录
这一节主要记录我在实现过程中遇到的难点。
1. 对于item.boundingRect() 和 item.pos() 的理解。boundingRect是指的他的item可接受外部事件的区域,它的坐标是相对于pos的,一般设置的大小应该是(0,0,width,height)。
2. 通过父子控件,可实现多Item组合成一个整体的效果,即同步拖动,同步放缩。
3. 接上一点,如果不设置父控件,只是通过 QGraphicsScene::addItem() 进行添加,是不能达到上述效果的。另外,放缩时,是需要在重写的wheelEvent 中,设置父控件的 setScale() 完成放缩。
- 继承 QGraphicsRectItem 重写paint函数,去除选中时,难看的虚线边框。
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) Q_DECL_OVERRIDE
{
QStyleOptionGraphicsItem op;
op.initFrom(widget);
if (option->state & QStyle::State_Selected)
op.state = QStyle::State_None;
QGraphicsRectItem::paint(painter, &op, widget);
}
- 继承QGraphicsEllipseItem 重写paint函数。同上。
- 对 item 的属性(可拖动,可选中等)的设置。
m_item->setToolTip(QStringLiteral("camera")); // 提示
m_item->setAcceptDrops(true); // 接受拖拽事件
m_item->setFlags(QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemIsFocusable
| QGraphicsItem::ItemIsSelectable); // 设置可移动,可聚焦,可选择
m_item->setRect(0, 0, 2 * m_radius, 2 * m_radius); // item是继承于QGraphicsEllipseItem,这里设置大小。(0,0)相对于自身的坐标。
m_item->setEnabled(true); // 注意:此属性默认为true。但是如果父控件设为false,子控件也会默认为false。
- 另外一些附属控件,可进行如下设置。(m_text 是 m_item 上面的文字标注,不接受事件)
m_text = new QGraphicsTextItem(m_item);
m_text->setEnabled(false);
m_text->setAcceptedMouseButtons(Qt::NoButton);
// m_lineCenter->setFlags(QGraphicsItem::ItemStacksBehindParent); // 此属性可设置,子控件在父控件下面绘制,避免遮挡父控件
// QGraphicsTextItem 类设置文本的居中比较麻烦,下面是我找到一个方法,不知道还有没有别的方法
m_text->setPlainText(QString::number(m_camera->getConfig().nCameraID));
m_text->setDefaultTextColor(QColor(0, 0, 0));
QTextBlockFormat format;
format.setAlignment(Qt::AlignCenter);
QTextCursor cursor = m_text->textCursor();
cursor.select(QTextCursor::Document);
cursor.mergeBlockFormat(format);
cursor.clearSelection();
m_text->setTextCursor(cursor);
- 放缩部分。参见第三点,想要在放缩地图时,保持 items 与地图的相对位置不变,需要构建与 m_map 同等大小同等位置的一个透明控件,作为缩放的母体。甚至可以直接以 m_map 作为这些 items 的父控件(我就是 ,☺)。不过,其实 QT 提供了更加方便的方式,就是使用 QGraphicsItemGroup(我开始尝试用过,但是出现问题的时候,怀疑是它引起的,就让我删了,后来没再用)。不过,无论谁作为父控件,想要子项处理自己的事件,都要保证下条语句。
m_map->setHandlesChildEvents(false); // 默认就是false,但是不设置的话,同样会受到他的父项的影响
void wheelEvent(QWheelEvent * event)
{
int angle = event->angleDelta().y();
if (angle > 0)
zoomIn();
else
zoomOut();
QGraphicsView::wheelEvent(event);
}
void zoomIn()
{
if (m_scene->items().isEmpty())
return;
if (nullptr != m_map)
{
qreal scale = m_map->scale() + c_fWheelScaleRate;
m_map->setScale(scale);
m_scene->setSceneRect(m_map->sceneBoundingRect());
}
}
void zoomOut()
{
if (m_scene->items().isEmpty())
return;
if (nullptr != m_map)
{
qreal scale = m_map->scale() - c_fWheelScaleRate;
if (scale < 1.0) // 保证不小于原图
scale += c_fWheelScaleRate;
m_map->setScale(scale);
m_scene->setSceneRect(m_map->sceneBoundingRect());
}
}
- 单击空白处,取消选择当前 item。
focusItemChanged 信号,我们可以在 MyView (QGraphicsView 的继承类)中设置一个表示当前 item 的指针,然后在重写的 mousePressEvent 函数中,判断点击的点,是否在当前 item 的区域内。
void mousePressEvent(QMouseEvent *event)
{
QGraphicsView::mousePressEvent(event); // 一定要在前面,
// use scene coordinate
QPointF p = mapToScene(event->pos());
if (m_map != nullptr)
p /= m_map->scale(); // 这里需要对放缩后的坐标,进行一个转换,QT并没有默认提供这个功能
emit signalIsContainPoint(p); // 这里不能直接判断,因为此时被选择的 item 还为完成更改,
// 应该在这里先将实践压入 Qt 的槽处理栈内,
// 在QGraphicsView::mousePressEvent 响应部分之后处理。
}
void slotIsContainPoint(QPointF p)
{
// 我用到了两种不同类型的Item,可以直接代表不同item,从而使用不用的区域计算方法
auto ellipse = dynamic_cast<QGraphicsEllipseItem*>(m_currentGuiWidget->getItem());
auto rect = dynamic_cast<QGraphicsRectItem*>(m_currentGuiWidget->getItem());
if (ellipse != nullptr)
{
QRectF rr(ellipse->pos(), QSizeF(2*c_nSizeCamera, 2*c_nSizeCamera));
if (!rr.contains(point))
setCurrentGui(nullptr);
}
else if (rect != nullptr)
{
QRectF rr(rect->pos(), QSizeF(rect->boundingRect().width() , rect->boundingRect().height()));
if (!rr.contains(point))
setCurrentGui(nullptr);
}
}