实现鼠标悬停高亮(Hover Highlight)功能

在 CAD 和可视化软件中,鼠标悬停高亮是一种常见的交互反馈方式。当鼠标移动到某个对象上时,该对象会以特殊效果显示,帮助用户快速识别当前指向的对象。本文将介绍如何在 ParaView/VTK 二次开发中实现这一功能。

功能需求

  • 鼠标移动时实时检测悬停的 Block
  • 悬停的 Block 显示高亮轮廓
  • 悬停高亮与选择高亮区分显示
  • 显示 Tooltip 提示 Block 名称
  • 性能优化:避免频繁的 pick 操作

整体架构

1
2
3
4
5
HoverHighlightManager
├── 鼠标事件监听 (eventFilter)
├── Pick 检测 (vtkSMRenderViewProxy::PickBlock)
├── 高亮渲染 (vtkActor + vtkFeatureEdges)
└── Tooltip 显示 (QToolTip)

核心实现

1. 事件过滤器

通过 QObject::eventFilter 拦截鼠标移动事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bool HoverHighlightManager::eventFilter(QObject *watched, QEvent *event) {
if (!m_enabled || watched != m_viewWidget)
return QObject::eventFilter(watched, event);

switch (event->type()) {
case QEvent::MouseMove:
onMouseMove(static_cast<QMouseEvent*>(event));
break;
case QEvent::Leave:
clearHoverHighlight();
break;
default:
break;
}

return QObject::eventFilter(watched, event);
}

2. 鼠标移动处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void HoverHighlightManager::onMouseMove(QMouseEvent *event) {
// 性能优化:限制 pick 频率
if (m_pickTimer.isValid() && m_pickTimer.elapsed() < m_pickInterval) {
return;
}
m_pickTimer.restart();

int x = event->pos().x();
int y = m_viewWidget->height() - event->pos().y() - 1; // Y 坐标翻转

vtkSMRenderViewProxy *renderViewProxy = m_renderView->getRenderViewProxy();
vtkSMProxy *reprProxy = nullptr;

vtkIdType flatIndex = renderViewProxy->PickBlock(x, y, &reprProxy);

if (flatIndex < 0) {
clearHoverHighlight();
return;
}

// 避免重复处理同一个 block
if (flatIndex == m_currentHoverIndex)
return;

m_currentHoverIndex = flatIndex;
updateHoverHighlight(flatIndex, reprProxy);
showTooltip(event->globalPos(), flatIndex);
}

3. Pick 频率优化

频繁的 pick 操作会影响性能,使用计时器限制调用频率:

1
2
3
4
5
6
class HoverHighlightManager {
private:
QElapsedTimer m_pickTimer;
int m_pickInterval = 50; // 毫秒,即每秒最多 20 次 pick
vtkIdType m_currentHoverIndex = -1; // 缓存当前悬停的 block
};

4. 高亮渲染

悬停高亮使用与选择高亮不同的颜色,便于区分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void HoverHighlightManager::setupHighlightActor() {
m_surfaceFilter = vtkSmartPointer<vtkDataSetSurfaceFilter>::New();

m_featureEdges = vtkSmartPointer<vtkFeatureEdges>::New();
m_featureEdges->BoundaryEdgesOn();
m_featureEdges->FeatureEdgesOn();
m_featureEdges->ManifoldEdgesOff();
m_featureEdges->NonManifoldEdgesOff();
m_featureEdges->SetFeatureAngle(30.0);

m_mapper = vtkSmartPointer<vtkPolyDataMapper>::New();
m_mapper->ScalarVisibilityOff();

m_hoverActor = vtkSmartPointer<vtkActor>::New();
m_hoverActor->SetMapper(m_mapper);
m_hoverActor->SetPickable(false);
m_hoverActor->VisibilityOff();

vtkProperty *prop = m_hoverActor->GetProperty();
prop->SetColor(0.0, 1.0, 1.0); // 青色,与选择高亮的黄色区分
prop->SetLineWidth(2.0f);
prop->SetLighting(false);
prop->SetAmbient(1.0);
prop->SetDiffuse(0.0);
}

5. 更新悬停高亮

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
void HoverHighlightManager::updateHoverHighlight(vtkIdType flatIndex, 
vtkSMProxy *reprProxy) {
if (!m_hoverActor)
return;

// 获取 block 数据
vtkDataObject *dataObj = getBlockData(flatIndex, reprProxy);
if (!dataObj) {
clearHoverHighlight();
return;
}

vtkDataSet *dataSet = vtkDataSet::SafeDownCast(dataObj);
if (!dataSet || dataSet->GetNumberOfCells() == 0) {
clearHoverHighlight();
return;
}

// 提取特征边
m_surfaceFilter->SetInputData(dataSet);
m_surfaceFilter->Update();

m_featureEdges->SetInputConnection(m_surfaceFilter->GetOutputPort());
m_featureEdges->Update();

m_mapper->SetInputConnection(m_featureEdges->GetOutputPort());
m_hoverActor->VisibilityOn();

// 刷新渲染
vtkRenderWindow *renderWindow = getActiveRenderWindow();
if (renderWindow) {
renderWindow->Render();
}
}

6. 清除悬停高亮

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void HoverHighlightManager::clearHoverHighlight() {
m_currentHoverIndex = -1;

if (m_hoverActor) {
m_hoverActor->VisibilityOff();
}

QToolTip::hideText();

vtkRenderWindow *renderWindow = getActiveRenderWindow();
if (renderWindow) {
renderWindow->Render();
}
}

7. Tooltip 显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void HoverHighlightManager::showTooltip(const QPoint &globalPos, 
vtkIdType flatIndex) {
QString blockName = getBlockName(flatIndex);
if (blockName.isEmpty()) {
blockName = QString("Block %1").arg(flatIndex);
}

QToolTip::showText(globalPos, blockName, m_viewWidget);
}

QString HoverHighlightManager::getBlockName(vtkIdType flatIndex) {
if (!m_inspector)
return QString();

// 在 MultiBlockInspector 中查找对应的 item
int currentIndex = 0;
for (int i = 0; i < m_inspector->topLevelItemCount(); ++i) {
QTreeWidgetItem *item = findItemByFlatIndex(
m_inspector->topLevelItem(i), flatIndex, currentIndex);
if (item) {
return item->text(0);
}
}

return QString();
}

与选择高亮的协调

悬停高亮和选择高亮需要协调工作:

1
2
3
4
5
6
7
8
9
10
void HoverHighlightManager::updateHoverHighlight(vtkIdType flatIndex, 
vtkSMProxy *reprProxy) {
// 如果当前 block 已被选中,不显示悬停高亮
if (m_selectionManager && m_selectionManager->isBlockSelected(flatIndex)) {
clearHoverHighlight();
return;
}

// 显示悬停高亮...
}

性能优化技巧

1. 限制 Pick 频率

1
2
3
4
// 使用 QElapsedTimer 限制 pick 频率
if (m_pickTimer.elapsed() < 50) // 50ms 间隔
return;
m_pickTimer.restart();

2. 缓存当前悬停状态

1
2
3
// 如果悬停的 block 没变,不重复处理
if (flatIndex == m_currentHoverIndex)
return;

3. 复用 VTK Filter

1
2
3
// 不要每次都 new,复用成员变量
m_surfaceFilter->SetInputData(dataSet);
m_surfaceFilter->Update();

4. 延迟 Tooltip 显示

1
2
3
4
5
6
7
8
// 使用 QTimer 延迟显示 tooltip,避免闪烁
m_tooltipTimer->start(500); // 500ms 后显示

void HoverHighlightManager::onTooltipTimeout() {
if (m_currentHoverIndex >= 0) {
QToolTip::showText(QCursor::pos(), getBlockName(m_currentHoverIndex));
}
}

完整类定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class HoverHighlightManager : public QObject {
Q_OBJECT
public:
explicit HoverHighlightManager(QObject *parent = nullptr);
~HoverHighlightManager();

void setRenderView(pqRenderView *renderView);
void setMultiBlockInspector(MultiBlockInspector *inspector);
void setSelectionManager(SelectionHighlightManager *selectionManager);

void setEnabled(bool enabled);
bool isEnabled() const;

void setHoverColor(double r, double g, double b);
void setHoverLineWidth(float width);
void setPickInterval(int milliseconds);
void setTooltipEnabled(bool enabled);

signals:
void blockHovered(int blockIndex);
void hoverCleared();

protected:
bool eventFilter(QObject *watched, QEvent *event) override;

private:
void onMouseMove(QMouseEvent *event);
void updateHoverHighlight(vtkIdType flatIndex, vtkSMProxy *reprProxy);
void clearHoverHighlight();
void showTooltip(const QPoint &globalPos, vtkIdType flatIndex);
QString getBlockName(vtkIdType flatIndex);
vtkDataObject* getBlockData(vtkIdType flatIndex, vtkSMProxy *reprProxy);
vtkRenderWindow* getActiveRenderWindow();
vtkRenderer* getActiveRenderer();

private:
pqRenderView *m_renderView = nullptr;
QWidget *m_viewWidget = nullptr;
MultiBlockInspector *m_inspector = nullptr;
SelectionHighlightManager *m_selectionManager = nullptr;

bool m_enabled = true;
bool m_tooltipEnabled = true;

vtkSmartPointer<vtkActor> m_hoverActor;
vtkSmartPointer<vtkDataSetSurfaceFilter> m_surfaceFilter;
vtkSmartPointer<vtkFeatureEdges> m_featureEdges;
vtkSmartPointer<vtkPolyDataMapper> m_mapper;

QElapsedTimer m_pickTimer;
int m_pickInterval = 50;
vtkIdType m_currentHoverIndex = -1;

double m_hoverColor[3] = {0.0, 1.0, 1.0};
float m_hoverLineWidth = 2.0f;
};

总结

实现鼠标悬停高亮功能的关键点:

  1. 事件监听:通过 eventFilter 拦截 MouseMoveLeave 事件
  2. Pick 检测:使用 vtkSMRenderViewProxy::PickBlock 检测悬停对象
  3. 性能优化:限制 pick 频率、缓存悬停状态、复用 VTK Filter
  4. 视觉反馈:使用与选择高亮不同的颜色,配合 Tooltip 显示名称
  5. 协调工作:与选择高亮配合,已选中的对象不显示悬停高亮

这套实现可以为用户提供流畅的交互体验,让用户在操作前就能预览将要选择的对象。

参考资料


实现鼠标悬停高亮(Hover Highlight)功能
http://aojian-blog.oss-cn-wuhan-lr.aliyuncs.com/2025/12/10/hover/
作者
遨见
发布于
2025年12月10日
许可协议