Prechádzať zdrojové kódy

Merge branch 'crosshairAndMagnificationWidgets'

* crosshairAndMagnificationWidgets:
  ENH: Can update ctkVTKMagnifyView on vtkRenderWindow renders, and can specify fastest update interval.
  ENH: Add sizeHints to ctkAxesWidget
  ENH: Add implementation of crosshair and magnification widgets.
Danielle Pace 14 rokov pred
rodič
commit
57eeba35fe
25 zmenil súbory, kde vykonal 2676 pridanie a 3 odobranie
  1. 5 0
      Libs/Visualization/VTK/Widgets/CMakeLists.txt
  2. 3 0
      Libs/Visualization/VTK/Widgets/Plugins/CMakeLists.txt
  3. 69 0
      Libs/Visualization/VTK/Widgets/Plugins/ctkVTKMagnifyViewPlugin.cpp
  4. 44 0
      Libs/Visualization/VTK/Widgets/Plugins/ctkVTKMagnifyViewPlugin.h
  5. 2 0
      Libs/Visualization/VTK/Widgets/Plugins/ctkVTKWidgetsPlugins.h
  6. 33 1
      Libs/Visualization/VTK/Widgets/Testing/Cpp/CMakeLists.txt
  7. 206 0
      Libs/Visualization/VTK/Widgets/Testing/Cpp/ctkVTKMagnifyViewTest1.cpp
  8. 437 0
      Libs/Visualization/VTK/Widgets/Testing/Cpp/ctkVTKMagnifyViewTest2.cpp
  9. 7 0
      Libs/Visualization/VTK/Widgets/ctkVTKAbstractView.cpp
  10. 6 0
      Libs/Visualization/VTK/Widgets/ctkVTKAbstractView.h
  11. 621 0
      Libs/Visualization/VTK/Widgets/ctkVTKMagnifyView.cpp
  12. 116 0
      Libs/Visualization/VTK/Widgets/ctkVTKMagnifyView.h
  13. 87 0
      Libs/Visualization/VTK/Widgets/ctkVTKMagnifyView_p.h
  14. 3 0
      Libs/Widgets/CMakeLists.txt
  15. 3 0
      Libs/Widgets/Plugins/CMakeLists.txt
  16. 77 0
      Libs/Widgets/Plugins/ctkCrosshairLabelPlugin.cpp
  17. 45 0
      Libs/Widgets/Plugins/ctkCrosshairLabelPlugin.h
  18. 2 0
      Libs/Widgets/Plugins/ctkWidgetsPlugins.h
  19. 27 2
      Libs/Widgets/Testing/Cpp/CMakeLists.txt
  20. 131 0
      Libs/Widgets/Testing/Cpp/ctkCrosshairLabelTest1.cpp
  21. 248 0
      Libs/Widgets/Testing/Cpp/ctkCrosshairLabelTest2.cpp
  22. 28 0
      Libs/Widgets/ctkAxesWidget.cpp
  23. 6 0
      Libs/Widgets/ctkAxesWidget.h
  24. 340 0
      Libs/Widgets/ctkCrosshairLabel.cpp
  25. 130 0
      Libs/Widgets/ctkCrosshairLabel.h

+ 5 - 0
Libs/Visualization/VTK/Widgets/CMakeLists.txt

@@ -19,6 +19,9 @@ SET(KIT_SRCS
   ctkVTKDataSetModel.h
   ctkVTKDataSetArrayComboBox.cpp
   ctkVTKDataSetArrayComboBox.h
+  ctkVTKMagnifyView.cpp
+  ctkVTKMagnifyView.h
+  ctkVTKMagnifyView_p.h
   ctkVTKMatrixWidget.cpp
   ctkVTKMatrixWidget.h
   ctkVTKRenderView.cpp
@@ -44,6 +47,8 @@ SET(KIT_MOC_SRCS
   ctkVTKAbstractView_p.h
   ctkVTKDataSetArrayComboBox.h
   ctkVTKDataSetModel.h
+  ctkVTKMagnifyView.h
+  ctkVTKMagnifyView_p.h
   ctkVTKMatrixWidget.h
   ctkVTKRenderView.h
   ctkVTKRenderView_p.h

+ 3 - 0
Libs/Visualization/VTK/Widgets/Plugins/CMakeLists.txt

@@ -15,6 +15,8 @@ SET(PLUGIN_SRCS
 
   ctkVTKDataSetArrayComboBoxPlugin.cpp
   ctkVTKDataSetArrayComboBoxPlugin.h
+  ctkVTKMagnifyViewPlugin.cpp
+  ctkVTKMagnifyViewPlugin.h
   ctkVTKRenderViewPlugin.cpp
   ctkVTKRenderViewPlugin.h
   ctkVTKScalarBarWidgetPlugin.cpp
@@ -32,6 +34,7 @@ SET(PLUGIN_MOC_SRCS
   ctkVTKWidgetsPlugins.h
 
   ctkVTKDataSetArrayComboBoxPlugin.h
+  ctkVTKMagnifyViewPlugin.h
   ctkVTKRenderViewPlugin.h
   ctkVTKScalarBarWidgetPlugin.h
   ctkVTKSliceViewPlugin.h

+ 69 - 0
Libs/Visualization/VTK/Widgets/Plugins/ctkVTKMagnifyViewPlugin.cpp

@@ -0,0 +1,69 @@
+/*=========================================================================
+
+  Library:   CTK
+
+  Copyright (c) Kitware Inc.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.commontk.org/LICENSE
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+
+=========================================================================*/
+
+// CTK includes
+#include "ctkVTKMagnifyViewPlugin.h"
+#include "ctkVTKMagnifyView.h"
+
+//-----------------------------------------------------------------------------
+ctkVTKMagnifyViewPlugin::ctkVTKMagnifyViewPlugin(QObject *_parent):QObject(_parent)
+{
+}
+
+//-----------------------------------------------------------------------------
+QWidget *ctkVTKMagnifyViewPlugin::createWidget(QWidget *_parent)
+{
+  ctkVTKMagnifyView* _widget = new ctkVTKMagnifyView(_parent);
+  return _widget;
+}
+
+//-----------------------------------------------------------------------------
+QString ctkVTKMagnifyViewPlugin::domXml() const
+{
+  return "<widget class=\"ctkVTKMagnifyView\" \
+          name=\"VTKMagnify\">\n"
+          " <property name=\"geometry\">\n"
+          "  <rect>\n"
+          "   <x>0</x>\n"
+          "   <y>0</y>\n"
+          "   <width>150</width>\n"
+          "   <height>150</height>\n"
+          "  </rect>\n"
+          " </property>\n"
+          "</widget>\n";
+}
+
+//-----------------------------------------------------------------------------
+QString ctkVTKMagnifyViewPlugin::includeFile() const
+{
+  return "ctkVTKMagnifyView.h";
+}
+
+//-----------------------------------------------------------------------------
+bool ctkVTKMagnifyViewPlugin::isContainer() const
+{
+  return false;
+}
+
+//-----------------------------------------------------------------------------
+QString ctkVTKMagnifyViewPlugin::name() const
+{
+  return "ctkVTKMagnifyView";
+}

+ 44 - 0
Libs/Visualization/VTK/Widgets/Plugins/ctkVTKMagnifyViewPlugin.h

@@ -0,0 +1,44 @@
+/*=========================================================================
+
+  Library:   CTK
+
+  Copyright (c) Kitware Inc.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.commontk.org/LICENSE
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+
+=========================================================================*/
+
+#ifndef __ctkVTKMagnifyViewPlugin_h
+#define __ctkVTKMagnifyViewPlugin_h
+
+// CTK includes
+#include "ctkVTKWidgetsAbstractPlugin.h"
+
+class CTK_VISUALIZATION_VTK_WIDGETS_PLUGINS_EXPORT ctkVTKMagnifyViewPlugin :
+  public QObject,
+  public ctkVTKWidgetsAbstractPlugin
+{
+  Q_OBJECT
+
+public:
+  ctkVTKMagnifyViewPlugin(QObject *_parent = 0);
+  
+  QWidget *createWidget(QWidget *_parent);
+  QString domXml() const;
+  QString includeFile() const;
+  bool isContainer() const;
+  QString name() const;
+  
+};
+
+#endif

+ 2 - 0
Libs/Visualization/VTK/Widgets/Plugins/ctkVTKWidgetsPlugins.h

@@ -27,6 +27,7 @@
 // CTK includes
 #include "ctkVisualizationVTKWidgetsPluginsExport.h"
 #include "ctkVTKDataSetArrayComboBoxPlugin.h"
+#include "ctkVTKMagnifyViewPlugin.h"
 #include "ctkVTKRenderViewPlugin.h"
 #include "ctkVTKScalarBarWidgetPlugin.h"
 #ifdef CTK_USE_CHARTS
@@ -49,6 +50,7 @@ public:
     {
     QList<QDesignerCustomWidgetInterface *> plugins;
     plugins << new ctkVTKDataSetArrayComboBoxPlugin;
+    plugins << new ctkVTKMagnifyViewPlugin;
     plugins << new ctkVTKRenderViewPlugin;
     plugins << new ctkVTKScalarBarWidgetPlugin;
 #ifdef CTK_USE_CHARTS

+ 33 - 1
Libs/Visualization/VTK/Widgets/Testing/Cpp/CMakeLists.txt

@@ -7,6 +7,7 @@ SET(TEST_SOURCES
   ctkVTKDataSetArrayComboBoxTest1.cpp
   ctkVTKDataSetModelTest1.cpp
   ctkVTKMatrixWidgetTest1.cpp
+  ctkVTKMagnifyViewTest1.cpp
   ctkVTKScalarBarWidgetTest1.cpp
   ctkTransferFunctionBarsItemTest1.cpp
   ctkTransferFunctionViewTest1.cpp
@@ -37,6 +38,7 @@ ENDIF(CTK_USE_CHARTS)
 #
 IF(EXISTS "${CTKData_DIR}")
   LIST(APPEND TEST_SOURCES
+    ctkVTKMagnifyViewTest2.cpp
     ctkVTKSliceViewTest2.cpp
     ctkVTKRenderViewTest2.cpp
     )
@@ -58,16 +60,31 @@ TARGET_LINK_LIBRARIES(${KIT}CppTests ${LIBRARY_NAME} vtkCharts ${CTK_BASE_LIBRAR
 SET( KIT_TESTS ${CPP_TEST_PATH}/${KIT}CppTests)
 
 MACRO( SIMPLE_TEST  TESTNAME )
-  ADD_TEST( ${TESTNAME} ${KIT_TESTS} ${TESTNAME} -D "${CTKData_DIR}/Data")
+  ADD_TEST( ${TESTNAME} ${KIT_TESTS} ${TESTNAME}
+            -D "${CTKData_DIR}/Data"
+            -V "${CTKData_DIR}/Baseline/Libs/Visualization/VTK/Widgets"
+            )
   SET_PROPERTY(TEST ${TESTNAME} PROPERTY LABELS ${PROJECT_NAME})
 ENDMACRO( SIMPLE_TEST  )
 
+MACRO( MAGNIFY_WIDGET_TEST  TESTNAME  TESTTYPE  SIZE  MAGNIFICATION)
+  ADD_TEST( ${TESTNAME}${TESTTYPE} ${KIT_TESTS} ${TESTNAME}
+            -D "${CTKData_DIR}/Data"
+            -V "${CTKData_DIR}/Baseline/Libs/Visualization/VTK/Widgets"
+            -T "${TESTTYPE}"
+            -S "${SIZE}"
+            -M "${MAGNIFICATION}"
+            )
+  SET_PROPERTY(TEST ${TESTNAME}${TESTTYPE} PROPERTY LABELS ${PROJECT_NAME})
+ENDMACRO( MAGNIFY_WIDGET_TEST  )
+
 #
 # Add Tests
 #
 
 SIMPLE_TEST( ctkVTKDataSetArrayComboBoxTest1 )
 SIMPLE_TEST( ctkVTKDataSetModelTest1 )
+SIMPLE_TEST( ctkVTKMagnifyViewTest1 )
 SIMPLE_TEST( ctkVTKMatrixWidgetTest1 )
 SIMPLE_TEST( ctkVTKScalarBarWidgetTest1 )
 SIMPLE_TEST( ctkTransferFunctionBarsItemTest1 )
@@ -95,6 +112,21 @@ SIMPLE_TEST( ctkVTKThumbnailViewTest1 )
 #
 IF(EXISTS "${CTKData_DIR}")
   #
+  # Dependencies required so that these tests won't run in parallel
+  #
+  MAGNIFY_WIDGET_TEST( ctkVTKMagnifyViewTest2 OddOdd 341 17 )
+  MAGNIFY_WIDGET_TEST( ctkVTKMagnifyViewTest2 EvenEven 340 18 )
+  SET_TESTS_PROPERTIES( ctkVTKMagnifyViewTest2EvenEven
+                        PROPERTIES DEPENDS
+                        ctkVTKMagnifyViewTest2OddOdd )
+  MAGNIFY_WIDGET_TEST( ctkVTKMagnifyViewTest2 OddEven 341 18 )
+  SET_TESTS_PROPERTIES( ctkVTKMagnifyViewTest2OddEven
+                        PROPERTIES DEPENDS
+                        ctkVTKMagnifyViewTest2EvenEven )
+  MAGNIFY_WIDGET_TEST( ctkVTKMagnifyViewTest2 EvenOdd 340 17 )
+  SET_TESTS_PROPERTIES( ctkVTKMagnifyViewTest2EvenOdd
+                        PROPERTIES DEPENDS
+                        ctkVTKMagnifyViewTest2OddEven )
   SIMPLE_TEST( ctkVTKSliceViewTest2 )
   SIMPLE_TEST( ctkVTKRenderViewTest2 )
 ENDIF()

+ 206 - 0
Libs/Visualization/VTK/Widgets/Testing/Cpp/ctkVTKMagnifyViewTest1.cpp

@@ -0,0 +1,206 @@
+/*=========================================================================
+
+  Library:   CTK
+
+  Copyright (c) Kitware Inc.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.commontk.org/LICENSE
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+
+=========================================================================*/
+
+// Qt includes
+#include <QApplication>
+#include <QTimer>
+
+// CTK includes
+#include "ctkVTKMagnifyView.h"
+
+// VTK includes
+#include <QVTKWidget.h>
+
+// STD includes
+#include <cstdlib>
+#include <iostream>
+
+//-----------------------------------------------------------------------------
+int ctkVTKMagnifyViewTest1(int argc, char * argv [] )
+{
+  QApplication app(argc, argv);
+
+  ctkVTKMagnifyView magnify;
+
+  // check default values
+  if (!magnify.showCrosshair() ||
+      magnify.crosshairPen().color() != magnify.palette().color(QPalette::Highlight) ||
+      magnify.crosshairPen().width() != 0 ||
+      magnify.crosshairPen().joinStyle() != Qt::MiterJoin ||
+      magnify.crosshairType() != ctkCrosshairLabel::SimpleCrosshair ||
+      magnify.marginColor() != magnify.palette().color(QPalette::Window) ||
+      magnify.bullsEyeWidth() != 15 ||
+      magnify.magnification() != 1.0 ||
+      magnify.observeRenderWindowEvents() != true ||
+      magnify.updateInterval() != 20 ||
+      magnify.numberObserved() != 0)
+    {
+    std::cerr << "ctkVTKMagnifyView: Wrong default values. " << std::endl
+              << " " << magnify.showCrosshair()
+              << " " << qPrintable(magnify.crosshairPen().color().name())
+              << " " << magnify.crosshairPen().width()
+              << " " << static_cast<int>(magnify.crosshairPen().joinStyle())
+              << " " << magnify.crosshairType()
+              << " " << qPrintable(magnify.marginColor().name())
+              << " " << magnify.bullsEyeWidth()
+              << " " << magnify.magnification()
+              << " " << magnify.observeRenderWindowEvents()
+              << " " << magnify.updateInterval()
+              << " " << magnify.numberObserved() << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  // Magnification
+  magnify.setMagnification(10.5);
+  if (magnify.magnification() != 10.5)
+    {
+    std::cerr << "ctkVTKMagnifyView:setMagnification failed. "
+              << magnify.magnification() << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  // Observe render window events
+  magnify.setObserveRenderWindowEvents(false);
+  if (magnify.observeRenderWindowEvents() != false)
+    {
+    std::cerr << "ctkVTKMagnifyView:setObserveRenderWindowEvents failed. "
+              << magnify.observeRenderWindowEvents() << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  // Update interval
+  magnify.setUpdateInterval(0);
+  if (magnify.updateInterval() != 0)
+    {
+    std::cerr << "ctkVTKMagnifyView:setUpdateInterval failed. "
+              << magnify.updateInterval() << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  // Adding / removing observed QVTKWidgets
+  QList<QVTKWidget *> allVTKWidgets;
+  int numVTKWidgets = 3;
+  for (int i = 0; i < numVTKWidgets; i++)
+    {
+    allVTKWidgets.append(new QVTKWidget());
+    }
+
+  // Observe one widget
+  magnify.observe(allVTKWidgets[0]);
+  if (!magnify.isObserved(allVTKWidgets[0]) || magnify.isObserved(allVTKWidgets[1]) ||
+      magnify.isObserved(allVTKWidgets[2]) || magnify.numberObserved() != 1)
+    {
+    std::cerr << "ctkVTKMagnifyView:observe(QVTKWidget*) failed. "
+              << "Number observed = " << magnify.numberObserved() << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  // Observe two more widgets
+  magnify.observe(allVTKWidgets.mid(1,2));
+  if (!magnify.isObserved(allVTKWidgets[0]) || !magnify.isObserved(allVTKWidgets[1]) ||
+      !magnify.isObserved(allVTKWidgets[2]) || magnify.numberObserved() != 3)
+    {
+    std::cerr << "ctkVTKMagnifyView:observe(QList<QVTKWidget*>) failed. "
+              << "Number observed = " << magnify.numberObserved() << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  // Re-observe a widget
+  magnify.observe(allVTKWidgets[2]);
+  if (!magnify.isObserved(allVTKWidgets[0]) || !magnify.isObserved(allVTKWidgets[1]) ||
+      !magnify.isObserved(allVTKWidgets[2]) || magnify.numberObserved() != 3)
+    {
+    std::cerr << "ctkVTKMagnifyView:observe(QVTKWidget*) failed on re-observe. "
+              << "Number observed = " << magnify.numberObserved() << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  // Re-observe a list of widgets
+  magnify.observe(allVTKWidgets.mid(0,2));
+  if (!magnify.isObserved(allVTKWidgets[0]) || !magnify.isObserved(allVTKWidgets[1]) ||
+      !magnify.isObserved(allVTKWidgets[2]) || magnify.numberObserved() != 3)
+    {
+    std::cerr << "ctkVTKMagnifyView:observe(QList<QVTKWidget*>) failed on re-"
+              << "observe. Number observed = " << magnify.numberObserved()
+              << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  // Remove a widget
+  magnify.remove(allVTKWidgets[2]);
+  if (!magnify.isObserved(allVTKWidgets[0]) || !magnify.isObserved(allVTKWidgets[1]) ||
+      magnify.isObserved(allVTKWidgets[2]) || magnify.numberObserved() != 2)
+    {
+    std::cerr << "ctkVTKMagnifyView:remove(QVTKWidget*) failed. "
+              << "Number observed = " << magnify.numberObserved() << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  // Re-remove a widget
+  magnify.remove(allVTKWidgets[2]);
+  if (!magnify.isObserved(allVTKWidgets[0]) || !magnify.isObserved(allVTKWidgets[1]) ||
+      magnify.isObserved(allVTKWidgets[2]) || magnify.numberObserved() != 2)
+    {
+    std::cerr << "ctkVTKMagnifyView:remove(QVTKWidget*) failed on re-remove. "
+              << "Number observed = " << magnify.numberObserved() << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  // Remove a list of widgets
+  magnify.remove(allVTKWidgets.mid(0,2));
+  if (magnify.isObserved(allVTKWidgets[0]) || magnify.isObserved(allVTKWidgets[1]) ||
+      magnify.isObserved(allVTKWidgets[2]) || magnify.numberObserved() != 0)
+    {
+    std::cerr << "ctkVTKMagnifyView:remove(QList<QVTKWidget*>) failed. "
+              << "Number observed = " << magnify.numberObserved() << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  // Re-remove a list of widgets
+  magnify.remove(allVTKWidgets.mid(1,2));
+  if (magnify.isObserved(allVTKWidgets[0]) || magnify.isObserved(allVTKWidgets[1]) ||
+      magnify.isObserved(allVTKWidgets[2]) || magnify.numberObserved() != 0)
+    {
+    std::cerr << "ctkVTKMagnifyView:remove(QList<QVTKWidget*>) failed on re-remove."
+              << " Number observed = " << magnify.numberObserved() << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  // Observe a list of widgets of length one
+  magnify.observe(allVTKWidgets.mid(1,1));
+  if (magnify.isObserved(allVTKWidgets[0]) || !magnify.isObserved(allVTKWidgets[1]) ||
+      magnify.isObserved(allVTKWidgets[2]) || magnify.numberObserved() != 1)
+    {
+    std::cerr << "ctkVTKMagnifyView:observe(QList<QVTKWidget*>) failed on lists of "
+              << "length 1. Number observed = " << magnify.numberObserved()
+              << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  magnify.show();
+  if (argc < 2 || QString(argv[1]) != "-I" )
+    {
+    QTimer::singleShot(200, &app, SLOT(quit()));
+    }
+
+  return app.exec();
+
+}
+

+ 437 - 0
Libs/Visualization/VTK/Widgets/Testing/Cpp/ctkVTKMagnifyViewTest2.cpp

@@ -0,0 +1,437 @@
+/*=========================================================================
+
+  Library:   CTK
+
+  Copyright (c) Kitware Inc.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.commontk.org/LICENSE
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+
+=========================================================================*/
+
+// Qt includes
+#include <QApplication>
+#include <QCursor>
+#include <QHBoxLayout>
+#include <QIcon>
+#include <QSignalSpy>
+#include <QStyle>
+#include <QTimer>
+
+// CTK includes
+#include "ctkVTKMagnifyView.h"
+#include "ctkCommandLineParser.h"
+#include "ctkVTKSliceView.h"
+
+// VTK includes
+#include <vtkImageReader2Factory.h>
+#include <vtkImageReader2.h>
+#include <vtkImageData.h>
+#include <vtkImageGaussianSmooth.h>
+#include <vtkSmartPointer.h>
+
+// STD includes
+#include <cstdlib>
+#include <iostream>
+
+//-----------------------------------------------------------------------------
+bool imageCompare(ctkVTKMagnifyView * magnify, QString baselineDirectory,
+                  QString baselineFilename)
+  {
+  QImage output = QPixmap::grabWidget(magnify).toImage();
+  QImage baseline(baselineDirectory + "/" + baselineFilename);
+  return output == baseline;
+  }
+
+//-----------------------------------------------------------------------------
+// (Used to create baselines, not during testing).
+void imageSave(ctkVTKMagnifyView * magnify, QString baselineDirectory,
+               QString baselineFilename)
+  {
+  QImage output = QPixmap::grabWidget(magnify).toImage();
+  output.save(baselineDirectory + "/" + baselineFilename);
+  }
+
+//-----------------------------------------------------------------------------
+bool runBaselineTest(int time, QApplication& app, ctkVTKMagnifyView * magnify,
+                     QWidget * underWidget, bool shouldBeUnder,
+                     QString baselineDirectory, QString testName,
+                     QString testNumber, QString errorMessage)
+{
+  QTimer::singleShot(time, &app, SLOT(quit()));
+  if (app.exec() == EXIT_FAILURE)
+    {
+    std::cerr << "ctkVTKMagnifyView exec failed when "
+        << qPrintable(errorMessage) << std::endl;
+    return false;
+    }
+  if (underWidget->underMouse() != shouldBeUnder)
+    {
+    std::cerr << "ctkMagnifyView mouse position failed when "
+        << qPrintable(errorMessage) << std::endl;
+    return false;
+    }
+  QString baselineFilename
+      = "ctkVTKMagnifyViewTest2" + testNumber + testName + ".png";
+  if (!imageCompare(magnify, baselineDirectory, baselineFilename))
+    {
+    std::cerr << "ctkVTKMagnifyView baseline comparison failed when "
+              << qPrintable(errorMessage) << "." << std::endl;
+    return false;
+    }
+  return true;
+}
+
+//-----------------------------------------------------------------------------
+int ctkVTKMagnifyViewTest2(int argc, char * argv [] )
+{
+  QApplication app(argc, argv);
+
+  // Command line parser
+  ctkCommandLineParser parser;
+  parser.addArgument("", "-D", QVariant::String);
+  parser.addArgument("", "-V", QVariant::String);
+  parser.addArgument("", "-I", QVariant::String);
+  parser.addArgument("", "-T", QVariant::String);
+  parser.addArgument("", "-S", QVariant::String);
+  parser.addArgument("", "-M", QVariant::String);
+  bool ok = false;
+  QHash<QString, QVariant> parsedArgs = parser.parseArguments(app.arguments(), &ok);
+  if (!ok)
+    {
+    std::cerr << qPrintable(parser.errorString()) << std::endl;
+    return EXIT_FAILURE;
+    }
+  QString dataDirectory = parsedArgs["-D"].toString();
+  QString baselineDirectory = parsedArgs["-V"].toString();
+  QString testType = parsedArgs["-T"].toString();
+  bool interactive = parsedArgs["-I"].toBool();
+  int size = parsedArgs["-S"].toInt();
+  double magnification = parsedArgs["-M"].toDouble();
+
+  // Create the parent widget
+  QWidget parentWidget;
+  QHBoxLayout layout(&parentWidget);
+
+  // Magnify widget parameters (we want an odd widget size and odd bullsEye)
+  bool showCrosshair = true;
+  QPen crosshairPen(QPen(Qt::yellow));
+  crosshairPen.setJoinStyle(Qt::MiterJoin);
+  ctkCrosshairLabel::CrosshairType crosshairType
+      = ctkCrosshairLabel::BullsEyeCrosshair;
+  double bullsEyeWidth = magnification + 2;
+  QColor marginColor = Qt::magenta;
+  bool observeRenderWindowEvents = false;
+  int updateInterval = 0;
+
+  // Create the magnify widget
+  ctkVTKMagnifyView * magnify = new ctkVTKMagnifyView(&parentWidget);
+  magnify->setMinimumSize(size,size);
+  magnify->setMaximumSize(size,size);
+  magnify->setShowCrosshair(showCrosshair);
+  magnify->setCrosshairPen(crosshairPen);
+  magnify->setCrosshairType(crosshairType);
+  magnify->setBullsEyeWidth(bullsEyeWidth);
+  magnify->setMarginColor(marginColor);
+  magnify->setMagnification(magnification);
+  magnify->setObserveRenderWindowEvents(observeRenderWindowEvents);
+  magnify->setUpdateInterval(updateInterval);
+  layout.addWidget(magnify);
+
+  // Test magnify widget parameters
+  if (magnify->showCrosshair() != showCrosshair)
+    {
+    std::cerr << "ctkVTKMagnifyView:setShowCrosshair failed. "
+              << magnify->showCrosshair() << std::endl;
+    return EXIT_FAILURE;
+    }
+  if (magnify->crosshairPen() != crosshairPen)
+    {
+    std::cerr << "ctkVTKMagnifyView:setCrosshairPen failed. "
+              << qPrintable(magnify->crosshairPen().color().name()) << std::endl;
+    return EXIT_FAILURE;
+    }
+  if (magnify->crosshairType() != crosshairType)
+    {
+    std::cerr << "ctkVTKMagnifyView:setCrosshairType failed. "
+              << magnify->crosshairType() << std::endl;
+    return EXIT_FAILURE;
+    }
+  if (magnify->bullsEyeWidth() != bullsEyeWidth)
+    {
+    std::cerr << "ctkVTKMagnifyView:setBullsEyeWidth failed. "
+              << magnify->bullsEyeWidth() << std::endl;
+    return EXIT_FAILURE;
+    }
+  if (magnify->marginColor() != marginColor)
+    {
+    std::cerr << "ctkVTKMagnifyView:setMarginColor failed. "
+              << qPrintable(magnify->marginColor().name()) << std::endl;
+    return EXIT_FAILURE;
+    }
+  if (magnify->magnification() != magnification)
+    {
+    std::cerr << "ctkVTKMagnifyView:setMagnification failed. "
+              << magnify->magnification() << std::endl;
+    return EXIT_FAILURE;
+    }
+  if (magnify->observeRenderWindowEvents() != observeRenderWindowEvents)
+    {
+    std::cerr << "ctkVTKMagnifyView:setObserveRenderWindowEvents failed. "
+              << magnify->observeRenderWindowEvents() << std::endl;
+    }
+  if (magnify->updateInterval() != updateInterval)
+    {
+    std::cerr << "ctkVTKMagnifyView:setUpdateInterval failed. "
+              << magnify->updateInterval() << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  // The remainder is interactive, so abort now if command line args specify otherwise
+  if (interactive)
+    {
+    return EXIT_SUCCESS;
+    }
+
+  // Add observed ctkVTKSliceViews (there are three, and the first two are observed)
+  QList<ctkVTKSliceView *> allSliceViews;
+  int numSliceViews = 3;
+  for (int i = 0; i < numSliceViews; i++)
+    {
+    allSliceViews.append(new ctkVTKSliceView(&parentWidget));
+    layout.addWidget(allSliceViews[i]);
+    }
+
+  // Observe the first two widgets
+  magnify->observe(allSliceViews[0]->VTKWidget());
+  magnify->observe(allSliceViews[1]->VTKWidget());
+  if (!magnify->isObserved(allSliceViews[0]->VTKWidget()) ||
+      !magnify->isObserved(allSliceViews[1]->VTKWidget()) ||
+      magnify->isObserved(allSliceViews[2]->VTKWidget()) ||
+      magnify->numberObserved() != 2)
+    {
+    std::cerr << "ctkVTKMagnifyView:observe(QVTKWidget*) failed. "
+              << "Number observed = " << magnify->numberObserved() << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  QString imageFilename = dataDirectory + "/" + "computerIcon.png";
+
+  // Instanciate the reader factory
+  vtkSmartPointer<vtkImageReader2Factory> imageFactory =
+      vtkSmartPointer<vtkImageReader2Factory>::New();
+
+  // Instanciate an image reader
+  vtkSmartPointer<vtkImageReader2> imageReader;
+  imageReader.TakeReference(imageFactory->CreateImageReader2(imageFilename.toLatin1()));
+  if (!imageReader)
+    {
+    std::cerr << "Failed to instanciate image reader using: "
+              << qPrintable(imageFilename) << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  // Read image
+  imageReader->SetFileName(imageFilename.toLatin1());
+  imageReader->Update();
+  vtkSmartPointer<vtkImageData> image = imageReader->GetOutput();
+
+  // Setup the slice views
+  for (int i = 0; i < numSliceViews; i++)
+    {
+    allSliceViews[i]->setRenderEnabled(true);
+    allSliceViews[i]->setMinimumSize(350,350);
+    allSliceViews[i]->setImageData(image);
+    allSliceViews[i]->setHighlightedBoxColor(Qt::yellow);
+    allSliceViews[i]->scheduleRender();
+    }
+
+  int time = 200;
+
+  // Get crosshair points of interest, used in the following tests
+  parentWidget.move(0,0);
+  parentWidget.show();
+  QTimer::singleShot(time, &app, SLOT(quit()));
+  if (app.exec() == EXIT_FAILURE)
+    {
+    std::cerr << "ctkVTKMagnifyView:show failed the first time." << std::endl;
+    return EXIT_FAILURE;
+    }
+  QPoint insideSlice0 = allSliceViews[0]->mapToGlobal(
+        QPoint(allSliceViews[0]->width()/2 + 100, allSliceViews[0]->height()/2 + 100));
+  QPoint outside = parentWidget.mapToGlobal(
+        QPoint(parentWidget.width(), parentWidget.height()));
+  QPoint insideSlice1 = allSliceViews[1]->mapToGlobal(
+        QPoint(allSliceViews[1]->width()/2 - 50, allSliceViews[1]->height() - 50));
+  QPoint insideSlice1edge = allSliceViews[1]->mapToGlobal(
+        QPoint(allSliceViews[1]->width() - 5, allSliceViews[1]->height() - 100));
+  QPoint insideSlice2 = allSliceViews[2]->mapToGlobal(
+        QPoint(allSliceViews[2]->width()/2, allSliceViews[2]->height()/2));
+  QPoint insideSlice0bottomRightCorner = allSliceViews[0]->mapToGlobal(
+        QPoint(allSliceViews[0]->width()-1, allSliceViews[0]->height()-1));
+  QPoint insideSlice0topLeftCorner = allSliceViews[0]->mapToGlobal(
+        QPoint(0,0));
+  parentWidget.hide();
+
+  // Make sure the magnify widget magnifies right away when shown with the crosshair inside
+  // an observed QVTKWidget
+  QCursor::setPos(insideSlice0);
+  parentWidget.show();
+  if (!runBaselineTest(time, app, magnify, allSliceViews[0], true,
+                       baselineDirectory, testType, "a",
+                       "magnify widget first shown with crosshair inside observed widget"))
+    {
+    return EXIT_FAILURE;
+    }
+  parentWidget.hide();
+
+  // Make sure the magnify widget shows blank right away when shown with the crosshair
+  // outside the observed QVTKWidgets
+  QCursor::setPos(outside);
+  parentWidget.show();
+  if (!runBaselineTest(time, app, magnify, &parentWidget, false,
+                       baselineDirectory, testType, "b",
+                       "magnify widget first shown with crosshair outside observed widget"))
+    {
+    return EXIT_FAILURE;
+    }
+
+  // Test magnification after move to allSliceViews[1]
+  QCursor::setPos(insideSlice1);
+  if (!runBaselineTest(time, app, magnify, allSliceViews[1], true,
+                       baselineDirectory, testType, "c",
+                       "crosshair moved inside 2nd observed widget the first time"))
+    {
+    return EXIT_FAILURE;
+    }
+
+  // Test magnification after move within allSliceViews[1] close to border
+  QCursor::setPos(insideSlice1edge);
+  if (!runBaselineTest(time, app, magnify, allSliceViews[1], true,
+                       baselineDirectory, testType, "d",
+                       "crosshair moved inside 2nd observed widget the second time"))
+    {
+    return EXIT_FAILURE;
+    }
+
+  // Test magnification after move outside an observed widget (should be blank)
+  QCursor::setPos(insideSlice2);
+  if (!runBaselineTest(time, app, magnify, allSliceViews[2], true,
+                       baselineDirectory, testType, "e",
+                       "crosshair moved inside unobserved widget"))
+    {
+    return EXIT_FAILURE;
+    }
+
+  // Test magnification after move back inside an observed widget (at extreme bottom
+  // right corner)
+  QCursor::setPos(insideSlice0bottomRightCorner);
+  if (!runBaselineTest(time, app, magnify, allSliceViews[0], true,
+                       baselineDirectory, testType, "f",
+                       "crosshair moved to bottom-right corner of observed widget"))
+    {
+    return EXIT_FAILURE;
+    }
+
+  // Test magnification after move back inside an observed widget (at extreme top left
+  // corner)
+  QCursor::setPos(insideSlice0topLeftCorner);
+  if (!runBaselineTest(time, app, magnify, allSliceViews[0], true,
+                       baselineDirectory, testType, "g",
+                       "crosshair moved to top-left corner of observed widget"))
+    {
+    return EXIT_FAILURE;
+    }
+
+  // Go back inside the widget
+  QCursor::setPos(insideSlice0);
+  if (!runBaselineTest(time, app, magnify, allSliceViews[0], true,
+                       baselineDirectory, testType, "a",
+                       "crosshair moved again inside observed widget"))
+    {
+    return EXIT_FAILURE;
+    }
+
+  // Test enabling observing render window events (trigger a render window event by
+  // changing the image data)
+  observeRenderWindowEvents = true;
+  magnify->setObserveRenderWindowEvents(observeRenderWindowEvents);
+  if (magnify->observeRenderWindowEvents() != observeRenderWindowEvents)
+    {
+    std::cerr << "ctkVTKMagnifyView:setObserveRenderWindowEvents failed. "
+              << magnify->observeRenderWindowEvents() << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  vtkImageGaussianSmooth * gaussian = vtkImageGaussianSmooth::New();
+  gaussian->SetInput(image);
+  gaussian->SetRadiusFactors(5,5);
+  gaussian->Update();
+  allSliceViews[0]->setImageData(gaussian->GetOutput());
+  allSliceViews[0]->scheduleRender();
+  if (!runBaselineTest(time, app, magnify, allSliceViews[0], true,
+                       baselineDirectory, testType, "h",
+                       "after Gaussian blur when observing render window events"))
+    {
+    return EXIT_FAILURE;
+    }
+
+  // Test disabling observing render window events (trigger a render window event by
+  // changing the image data)
+  observeRenderWindowEvents = false;
+  magnify->setObserveRenderWindowEvents(observeRenderWindowEvents);
+  if (magnify->observeRenderWindowEvents() != observeRenderWindowEvents)
+    {
+    std::cerr << "ctkVTKMagnifyView:setObserveRenderWindowEvents failed. "
+              << magnify->observeRenderWindowEvents() << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  gaussian->SetInput(image);
+  gaussian->SetRadiusFactors(0,0);
+  gaussian->Update();
+  allSliceViews[0]->setImageData(gaussian->GetOutput());
+  allSliceViews[0]->scheduleRender();
+  if (!runBaselineTest(time, app, magnify, allSliceViews[0], true,
+                       baselineDirectory, testType, "h",
+                       "after Gaussian blur when not observing render window events"))
+    {
+    return EXIT_FAILURE;
+    }
+
+  // Test changing the update interval
+  magnify->setUpdateInterval(time * 2);
+  magnify->setObserveRenderWindowEvents(true);
+  allSliceViews[0]->setImageData(image);
+  allSliceViews[0]->scheduleRender();
+  QCursor::setPos(insideSlice0bottomRightCorner);
+  // It should be waiting to update here
+  if (!runBaselineTest(time, app, magnify, allSliceViews[0], true,
+                       baselineDirectory, testType, "h",
+                       "after changing update interval: updated too quickly"))
+    {
+    return EXIT_FAILURE;
+    }
+  // It should have updated by now
+  if (!runBaselineTest(time + 50, app, magnify, allSliceViews[0], true,
+                       baselineDirectory, testType, "f",
+                       "after changing update interval: didn't update after waiting"))
+    {
+    return EXIT_FAILURE;
+    }
+
+  gaussian->Delete();
+
+  return EXIT_SUCCESS;
+
+}
+

+ 7 - 0
Libs/Visualization/VTK/Widgets/ctkVTKAbstractView.cpp

@@ -201,6 +201,13 @@ vtkCornerAnnotation* ctkVTKAbstractView::cornerAnnotation() const
 }
 
 //----------------------------------------------------------------------------
+QVTKWidget * ctkVTKAbstractView::VTKWidget() const
+{
+  Q_D(const ctkVTKAbstractView);
+  return d->VTKWidget;
+}
+
+//----------------------------------------------------------------------------
 CTK_SET_CPP(ctkVTKAbstractView, bool, setRenderEnabled, RenderEnabled);
 CTK_GET_CPP(ctkVTKAbstractView, bool, renderEnabled, RenderEnabled);
 

+ 6 - 0
Libs/Visualization/VTK/Widgets/ctkVTKAbstractView.h

@@ -24,6 +24,9 @@
 // Qt includes
 #include <QWidget>
 
+// VTK includes
+#include <QVTKWidget.h>
+
 // CTK includes
 #include "ctkVisualizationVTKWidgetsExport.h"
 class ctkVTKAbstractViewPrivate;
@@ -77,6 +80,9 @@ public:
   QString cornerAnnotationText() const;
   vtkCornerAnnotation* cornerAnnotation()const;
 
+  /// Get the underlying QVTKWidget
+  QVTKWidget * VTKWidget() const;
+
   /// Get background color
   virtual QColor backgroundColor() const = 0;
 

+ 621 - 0
Libs/Visualization/VTK/Widgets/ctkVTKMagnifyView.cpp

@@ -0,0 +1,621 @@
+/*=========================================================================
+
+  Library:   CTK
+
+  Copyright (c) Kitware Inc.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.commontk.org/LICENSE
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+
+=========================================================================*/
+
+// Qt includes
+#include <QEvent>
+#include <QMouseEvent>
+#include <QPointF>
+#include <QTimerEvent>
+
+// CTK includes
+#include "ctkVTKMagnifyView.h"
+#include "ctkVTKMagnifyView_p.h"
+#include "ctkLogger.h"
+
+// VTK includes
+#include <QVTKWidget.h>
+#include <vtkRenderWindow.h>
+#include <vtkUnsignedCharArray.h>
+
+// STD includes
+#include <cmath>
+
+// Convenient macro
+#define VTK_CREATE(type, name) \
+  vtkSmartPointer<type> name = vtkSmartPointer<type>::New()
+
+//--------------------------------------------------------------------------
+static ctkLogger logger("org.commontk.visualization.vtk.widgets.ctkVTKMagnifyView");
+//--------------------------------------------------------------------------
+
+// --------------------------------------------------------------------------
+// ctkVTKMagnifyViewPrivate methods
+
+// --------------------------------------------------------------------------
+ctkVTKMagnifyViewPrivate::ctkVTKMagnifyViewPrivate(ctkVTKMagnifyView& object)
+  : QObject(&object), q_ptr(&object)
+{
+  this->ObservedQVTKWidgets = QList<QVTKWidget *>();
+  this->Magnification = 1.0;
+  this->ObserveRenderWindowEvents = true;
+
+  this->EventHandler.EventType = NoEvent;
+  this->EventHandler.Widget = 0;
+  this->EventHandler.Position = QPointF(0,0);
+
+  this->EventHandler.UpdateInterval = 20;
+  this->EventHandler.TimerId = 0;
+
+}
+
+// --------------------------------------------------------------------------
+ctkVTKMagnifyViewPrivate::~ctkVTKMagnifyViewPrivate()
+{
+  if (this->EventHandler.TimerId != 0)
+    {
+    this->killTimer(this->EventHandler.TimerId);
+    }
+}
+
+// --------------------------------------------------------------------------
+void ctkVTKMagnifyViewPrivate::init()
+{
+  // Start by removing the pixmap
+  this->EventHandler.EventType = RemovePixmapEvent;
+  this->removePixmap();
+
+  // Start the timer
+  this->restartTimer();
+}
+
+// --------------------------------------------------------------------------
+void ctkVTKMagnifyViewPrivate::restartTimer()
+{
+  // Kill any old timers
+  if (this->EventHandler.TimerId != 0)
+    {
+    this->killTimer(this->EventHandler.TimerId);
+    this->EventHandler.TimerId = 0;
+    }
+
+  // Start timer if appropriate
+  if (this->EventHandler.UpdateInterval != 0)
+    {
+    this->EventHandler.TimerId = this->startTimer(this->EventHandler.UpdateInterval);
+    Q_ASSERT(this->EventHandler.TimerId);
+    }
+  // Not using any timers, process events as they come
+  else
+    {
+    this->EventHandler.TimerId = 0;
+    }
+}
+
+// --------------------------------------------------------------------------
+void ctkVTKMagnifyViewPrivate::resetEventHandler()
+{
+  this->EventHandler.EventType = NoEvent;
+}
+
+// --------------------------------------------------------------------------
+void ctkVTKMagnifyViewPrivate::timerEvent(QTimerEvent * event)
+{
+  Q_ASSERT(event->timerId() == this->EventHandler.TimerId);
+
+  if (this->EventHandler.EventType == UpdatePixmapEvent)
+    {
+    this->updatePixmap();
+    }
+  else if (this->EventHandler.EventType == RemovePixmapEvent)
+    {
+    this->removePixmap();
+    }
+}
+
+// --------------------------------------------------------------------------
+void ctkVTKMagnifyViewPrivate::pushUpdatePixmapEvent()
+{
+  Q_ASSERT(this->EventHandler.Widget);
+  this->pushUpdatePixmapEvent(
+        this->EventHandler.Widget->mapFromGlobal(QCursor::pos()));
+}
+
+// --------------------------------------------------------------------------
+void ctkVTKMagnifyViewPrivate::pushUpdatePixmapEvent(QPointF pos)
+{
+  Q_ASSERT(this->EventHandler.Widget);
+
+  // Add this event to the queue
+  this->EventHandler.EventType = UpdatePixmapEvent;
+  this->EventHandler.Position = pos;
+
+  // Process the event if we handle all events
+  if (this->EventHandler.UpdateInterval == 0)
+    {
+    this->updatePixmap();
+    }
+}
+
+// --------------------------------------------------------------------------
+void ctkVTKMagnifyViewPrivate::pushRemovePixmapEvent()
+{
+  // Add this event to the queue
+  this->EventHandler.EventType = RemovePixmapEvent;
+
+  // Process the event if we handle all events
+  if (this->EventHandler.UpdateInterval == 0)
+    {
+    this->removePixmap();
+    }
+}
+
+// --------------------------------------------------------------------------
+void ctkVTKMagnifyViewPrivate::connectRenderWindow(QVTKWidget * widget)
+{
+  Q_ASSERT(widget);
+  Q_ASSERT(this->ObserveRenderWindowEvents);
+
+  vtkRenderWindow * renderWindow = widget->GetRenderWindow();
+  if (renderWindow)
+    {
+    this->qvtkConnect(renderWindow, vtkCommand::EndEvent,
+                      this, SLOT(pushUpdatePixmapEvent()));
+    }
+}
+
+// --------------------------------------------------------------------------
+void ctkVTKMagnifyViewPrivate::disconnectRenderWindow(QVTKWidget * widget)
+{
+  Q_ASSERT(widget);
+
+  vtkRenderWindow * renderWindow = widget->GetRenderWindow();
+  if (renderWindow)
+    {
+    this->qvtkDisconnect(renderWindow, vtkCommand::EndEvent,
+                         this, SLOT(pushUpdatePixmapEvent()));
+    }
+}
+
+// --------------------------------------------------------------------------
+void ctkVTKMagnifyViewPrivate::observe(QVTKWidget * widget)
+{
+  Q_ASSERT(widget);
+
+  // If we are not already observing the widget, add it to the list and install
+  // the public implementation as the event filter to handle mousing
+  if (!this->ObservedQVTKWidgets.contains(widget))
+    {
+    this->ObservedQVTKWidgets.append(widget);
+    Q_Q(ctkVTKMagnifyView);
+    widget->installEventFilter(q);
+    if (this->ObserveRenderWindowEvents)
+      {
+      this->connectRenderWindow(widget);
+      }
+    }
+}
+
+// --------------------------------------------------------------------------
+void ctkVTKMagnifyViewPrivate::remove(QVTKWidget * widget)
+{
+  Q_ASSERT(widget);
+
+  // If we are observing the widget, remove it from the list and remove the
+  // public implementations event filtering
+  if (this->ObservedQVTKWidgets.contains(widget))
+    {
+    Q_ASSERT(this->ObservedQVTKWidgets.count(widget) == 1);
+    this->ObservedQVTKWidgets.removeOne(widget);
+    Q_Q(ctkVTKMagnifyView);
+    widget->removeEventFilter(q);
+    if (this->ObserveRenderWindowEvents)
+      {
+      this->disconnectRenderWindow(widget);
+      }
+    }
+}
+
+// --------------------------------------------------------------------------
+void ctkVTKMagnifyViewPrivate::removePixmap()
+{
+  Q_ASSERT(this->EventHandler.EventType == RemovePixmapEvent);
+  Q_Q(ctkVTKMagnifyView);
+  QPixmap nullPixmap;
+  q->setPixmap(nullPixmap);
+  q->update();
+  this->resetEventHandler();
+}
+
+// -------------------------------------------------------------------------
+void ctkVTKMagnifyViewPrivate::updatePixmap()
+{
+  Q_ASSERT(this->EventHandler.EventType == UpdatePixmapEvent);
+  Q_ASSERT(this->EventHandler.Widget);
+  Q_Q(ctkVTKMagnifyView);
+
+  // Retrieve buffer of given QVTKWidget from its render window
+  vtkRenderWindow * renderWindow = this->EventHandler.Widget->GetRenderWindow();
+  if (!renderWindow)
+    {
+    return;
+    }
+
+  // Get the window size and mouse position, and do error checking
+  QPointF pos = this->EventHandler.Position;
+  int * windowSize = renderWindow->GetSize();
+  QPointF mouseWindowPos(pos.x(), static_cast<double>(windowSize[1]-1)-pos.y());
+  if (mouseWindowPos.x() < 0 || mouseWindowPos.x() >= windowSize[0] ||
+      mouseWindowPos.y() < 0 || mouseWindowPos.y() >= windowSize[1])
+    {
+    return;
+    }
+
+  // Compute indices into the render window's data array
+  // Given a magnification and the label's widget size, compute the number of
+  // pixels we can show.  We should round to get a larger integer extent, since
+  // we will later adjust the pixmap's location in paintEvent().
+  // Left-right and up-down are in the render window coordinate frame.
+  // (which is different in the y-direction compared to Qt coordinates).
+  QSizeF sizeToMagnify = QSizeF(q->size()) / this->Magnification;
+  double posLeft = (mouseWindowPos.x() - ((sizeToMagnify.width()-1.0) / 2.0));
+  double posRight = (mouseWindowPos.x() + ((sizeToMagnify.width()-1.0) / 2.0));
+  double posBottom = (mouseWindowPos.y() - ((sizeToMagnify.height()-1.0) / 2.0));
+  double posTop = (mouseWindowPos.y() + ((sizeToMagnify.height()-1.0) / 2.0));
+
+  // Round to ints, for indexing into the pixel array
+  int indexLeft = std::floor(posLeft);
+  int indexRight = std::ceil(posRight);
+  int indexBottom = std::floor(posBottom);
+  int indexTop = std::ceil(posTop);
+
+  // Handle when mouse is near the border
+  int minLeft = 0;
+  int maxRight = windowSize[0]-1;
+  int minBottom = 0;
+  int maxTop = windowSize[1]-1;
+
+  bool overLeft = indexLeft < minLeft;
+  bool overRight = indexRight > maxRight;
+  bool overBottom = indexBottom < minBottom;
+  bool overTop = indexTop > maxTop;
+
+  // Ensure we don't access nonexistant indices
+  if (overLeft)
+    {
+    indexLeft = minLeft;
+    posLeft = minLeft;
+    }
+  if (overRight)
+    {
+    indexRight = maxRight;
+    posRight = maxRight;
+    }
+  if (overBottom)
+    {
+    indexBottom = minBottom;
+    posBottom = minBottom;
+    }
+  if (overTop)
+    {
+    indexTop = maxTop;
+    posTop = maxTop;
+    }
+
+  // Error case
+  if (indexLeft > indexRight || indexBottom > indexTop)
+    {
+    return;
+    }
+
+  // Setup the pixelmap's position in the label
+  Qt::Alignment alignment;
+  if (overLeft && !overRight)
+    {
+    alignment = Qt::AlignRight;
+    }
+  else if (overRight && !overLeft)
+    {
+    alignment = Qt::AlignLeft;
+    }
+  else
+    {
+    alignment = Qt::AlignLeft;
+    }
+  if (overBottom && !overTop)
+    {
+    alignment = alignment | Qt::AlignTop;
+    }
+  else if (overTop && !overBottom)
+    {
+    alignment = alignment | Qt::AlignBottom;
+    }
+  else
+    {
+    alignment = alignment | Qt::AlignTop;
+    }
+  q->setAlignment(alignment);
+
+  // Retrieve the pixel data into a QImage (flip vertically to move from render
+  // window coordinates to Qt coordinates)
+  QSize actualSize(indexRight-indexLeft+1, indexTop-indexBottom+1);
+  QImage image(actualSize.width(), actualSize.height(), QImage::Format_RGB32);
+  vtkUnsignedCharArray * pixelData = vtkUnsignedCharArray::New();
+  pixelData->SetArray(image.bits(), actualSize.width() * actualSize.height() * 4, 1);
+  int front = renderWindow->GetDoubleBuffer();
+  int success = renderWindow->GetRGBACharPixelData(
+      indexLeft, indexBottom, indexRight, indexTop, front, pixelData);
+  if (!success)
+    {
+    return;
+    }
+  pixelData->Delete();
+  image = image.rgbSwapped();
+  image = image.mirrored();
+
+  // Scale the image to zoom, using FastTransformation to prevent smoothing
+  QSize imageSize = actualSize * this->Magnification;
+  image = image.scaled(imageSize, Qt::KeepAspectRatioByExpanding,
+                       Qt::FastTransformation);
+
+  // Crop the magnified image to solve the problem of magnified partial pixels
+  double errorLeft
+      = (posLeft - static_cast<double>(indexLeft)) * this->Magnification;
+  double errorRight
+      = (static_cast<double>(indexRight) - posRight) * this->Magnification;
+  double errorBottom
+      = (posBottom - static_cast<double>(indexBottom)) * this->Magnification;
+  double errorTop
+      = (static_cast<double>(indexTop) - posTop) * this->Magnification;
+
+  // When cropping the Qt image, the 'adjust' variables are in Qt coordinates,
+  // not render window coordinates (bottom and top switch).
+  int cropIndexLeft = round(errorLeft);
+  int cropIndexRight = imageSize.width() - round(errorRight) - 1;
+  int cropIndexTop = round(errorTop);
+  int cropIndexBottom = imageSize.height() - round(errorBottom) - 1;
+
+  // Handle case when label size and magnification are not both even or odd
+  // (errorLeft/errorRight/errorBottom/errorTop will have fractional component,
+  // so cropped image wouldn't be the correct size unless we adjust further).
+  int requiredWidth = round((posRight - posLeft + 1) * this->Magnification);
+  int requiredHeight = round((posTop - posBottom + 1) * this->Magnification);
+  int actualWidth = cropIndexRight - cropIndexLeft + 1;
+  int actualHeight = cropIndexBottom - cropIndexTop + 1;
+  int diffWidth = requiredWidth - actualWidth;
+  int diffHeight = requiredHeight - actualHeight;
+  // Too wide
+  if (diffWidth < 0 && cropIndexRight != imageSize.width()-1)
+    {
+    Q_ASSERT(actualWidth - requiredWidth <= 1);
+    cropIndexRight += diffWidth;
+    }
+  // Too narrow
+  else if (diffWidth > 0 && cropIndexLeft != 0)
+    {
+    Q_ASSERT(requiredWidth - actualWidth <= 1);
+    cropIndexLeft -= diffWidth;
+    }
+  // Too tall
+  if (diffHeight < 0 && cropIndexBottom != imageSize.height()-1)
+    {
+    Q_ASSERT(actualHeight - requiredHeight <= 1);
+    cropIndexBottom += diffHeight;
+    }
+  // Too short
+  else if (diffHeight > 0 && cropIndexTop != 0)
+    {
+    Q_ASSERT(requiredHeight - actualHeight <= 1);
+    cropIndexTop -= diffHeight;
+    }
+
+  // Finally crop the QImage for display
+  QRect cropRect(QPoint(cropIndexLeft, cropIndexTop),
+                 QPoint(cropIndexRight, cropIndexBottom));
+  image = image.copy(cropRect);
+
+
+  // Finally, set the pixelmap to the new one we have created and update
+  q->setPixmap(QPixmap::fromImage(image));
+  q->update();
+  this->resetEventHandler();
+}
+
+//---------------------------------------------------------------------------
+// ctkVTKMagnifyView methods
+
+// --------------------------------------------------------------------------
+ctkVTKMagnifyView::ctkVTKMagnifyView(QWidget* parentWidget)
+  : Superclass(parentWidget)
+  , d_ptr(new ctkVTKMagnifyViewPrivate(*this))
+{
+  Q_D(ctkVTKMagnifyView);
+  d->init();
+}
+
+// --------------------------------------------------------------------------
+ctkVTKMagnifyView::~ctkVTKMagnifyView()
+{
+}
+
+// --------------------------------------------------------------------------
+CTK_GET_CPP(ctkVTKMagnifyView, double, magnification, Magnification);
+
+// --------------------------------------------------------------------------
+void ctkVTKMagnifyView::setMagnification(double newMagnification)
+{
+  Q_D(ctkVTKMagnifyView);
+  if (newMagnification == d->Magnification || newMagnification <= 0)
+    {
+    return;
+    }
+
+  d->Magnification = newMagnification;
+  this->update();
+}
+
+// --------------------------------------------------------------------------
+CTK_GET_CPP(ctkVTKMagnifyView, bool,
+            observeRenderWindowEvents, ObserveRenderWindowEvents);
+
+// --------------------------------------------------------------------------
+void ctkVTKMagnifyView::setObserveRenderWindowEvents(bool newObserve)
+{
+  Q_D(ctkVTKMagnifyView);
+  if (newObserve == d->ObserveRenderWindowEvents)
+    {
+    return;
+    }
+
+  d->ObserveRenderWindowEvents = newObserve;
+
+  // Connect/disconnect observations on vtkRenderWindow EndEvents, depending
+  // on whether we are switching from not-observing to observing or from
+  // observing to not-observing
+  QList<QVTKWidget *>::iterator it = d->ObservedQVTKWidgets.begin();
+  while (it != d->ObservedQVTKWidgets.end())
+    {
+    if (newObserve)
+      {
+      d->connectRenderWindow(*it);
+      }
+    else
+      {
+      d->disconnectRenderWindow(*it);
+      }
+    ++it;
+    }
+}
+
+// --------------------------------------------------------------------------
+int ctkVTKMagnifyView::updateInterval() const
+  {
+  Q_D(const ctkVTKMagnifyView);
+  return d->EventHandler.UpdateInterval;
+  }
+
+// --------------------------------------------------------------------------
+void ctkVTKMagnifyView::setUpdateInterval(int newInterval)
+{
+  Q_D(ctkVTKMagnifyView);
+  if (newInterval == d->EventHandler.UpdateInterval || newInterval < 0)
+    {
+    return;
+    }
+
+  d->EventHandler.UpdateInterval = newInterval;
+  d->restartTimer();
+}
+
+// --------------------------------------------------------------------------
+void ctkVTKMagnifyView::observe(QVTKWidget * widget)
+{
+  Q_D(ctkVTKMagnifyView);
+  if (widget)
+    {
+    d->observe(widget);
+    }
+}
+
+// --------------------------------------------------------------------------
+void ctkVTKMagnifyView::observe(QList<QVTKWidget *> widgets)
+{
+  foreach(QVTKWidget * widget, widgets)
+    {
+    this->observe(widget);
+    }
+}
+
+// --------------------------------------------------------------------------
+bool ctkVTKMagnifyView::isObserved(QVTKWidget * widget) const
+{
+  if (!widget)
+    {
+    return false;
+    }
+  Q_D(const ctkVTKMagnifyView);
+  return d->ObservedQVTKWidgets.contains(widget);
+}
+
+// --------------------------------------------------------------------------
+int ctkVTKMagnifyView::numberObserved() const
+  {
+  Q_D(const ctkVTKMagnifyView);
+  return d->ObservedQVTKWidgets.length();
+  }
+
+// --------------------------------------------------------------------------
+void ctkVTKMagnifyView::remove(QVTKWidget * widget)
+{
+  Q_D(ctkVTKMagnifyView);
+  if (widget)
+    {
+    d->remove(widget);
+    }
+}
+
+// --------------------------------------------------------------------------
+void ctkVTKMagnifyView::remove(QList<QVTKWidget *> widgets)
+{
+  foreach(QVTKWidget * widget, widgets)
+    {
+    this->remove(widget);
+    }
+}
+
+// --------------------------------------------------------------------------
+bool ctkVTKMagnifyView::eventFilter(QObject * obj, QEvent * event)
+{
+  // The given object should be a QVTKWidget in our list
+  QVTKWidget * widget = static_cast<QVTKWidget *>(obj);
+  Q_ASSERT(widget);
+  Q_D(ctkVTKMagnifyView);
+  Q_ASSERT(d->ObservedQVTKWidgets.contains(widget));
+  d->EventHandler.Widget = widget;
+
+  QEvent::Type eventType = event->type();
+
+  // On mouse move, update the pixmap with the zoomed image
+  if (eventType == QEvent::MouseMove)
+    {
+    QMouseEvent * mouseEvent = static_cast<QMouseEvent *>(event);
+    Q_ASSERT(mouseEvent);
+    d->pushUpdatePixmapEvent(mouseEvent->posF());
+    }
+  // On enter, update the pixmap with the zoomed image (required for zooming when
+  // widget is created with mouse already within it), and emit signal of enter event
+  else if (eventType == QEvent::Enter)
+    {
+    d->pushUpdatePixmapEvent();
+    emit enteredObservedWidget(widget);
+    }
+  // On leave, fill the pixmap with a solid color and emit signal of leave event
+  else if (eventType == QEvent::Leave)
+    {
+    d->pushRemovePixmapEvent();
+    emit leftObservedWidget(widget);
+    }
+  // For other event types, use standard event processing
+  else
+    {
+    return QObject::eventFilter(obj, event);
+    }
+
+  return false;
+}

+ 116 - 0
Libs/Visualization/VTK/Widgets/ctkVTKMagnifyView.h

@@ -0,0 +1,116 @@
+/*=========================================================================
+
+  Library:   CTK
+
+  Copyright (c) Kitware Inc.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.commontk.org/LICENSE
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+
+=========================================================================*/
+
+#ifndef __ctkVTKMagnifyView_h
+#define __ctkVTKMagnifyView_h
+
+// QT includes
+#include <QList>
+
+// CTK includes
+#include "ctkCrosshairLabel.h"
+#include "ctkVisualizationVTKWidgetsExport.h"
+
+// VTK includes
+class QVTKWidget;
+
+class ctkVTKMagnifyViewPrivate;
+
+/// Gives a magnified view of a QVTKWidget around the mouse position, with
+/// overlaid crosshair (ex. cross-hair).  You must specify the QVTKWidget(s) to be
+/// observed.
+/// \sa ctkCrosshairLabel
+
+class CTK_VISUALIZATION_VTK_WIDGETS_EXPORT ctkVTKMagnifyView
+  : public ctkCrosshairLabel
+{
+  Q_OBJECT
+  Q_PROPERTY(double magnification READ magnification WRITE setMagnification);
+  Q_PROPERTY(bool observeRenderWindowEvents
+             READ observeRenderWindowEvents WRITE setObserveRenderWindowEvents);
+  Q_PROPERTY(int updateInterval READ updateInterval WRITE setUpdateInterval)
+
+public:
+  /// Constructors
+  typedef ctkCrosshairLabel Superclass;
+  explicit ctkVTKMagnifyView(QWidget* parent = 0);
+  virtual ~ctkVTKMagnifyView();
+
+  /// Set/get the magnification (zoom).  Looks best when the magnification and
+  /// the widget size are both either even or odd.  Default 1.0.
+  double magnification() const;
+  void setMagnification(double newMagnification);
+
+  /// Set/get whether or not to observe EndEvents emitted by the observed
+  /// QVTKWidgets' vtkRenderWindows after they have rendered.  This triggers
+  /// updates to the magnify widget whenever the vtkRenderWindow does a render,
+  /// even if the mouse position does not move. Default true.
+  bool observeRenderWindowEvents() const;
+  void setObserveRenderWindowEvents(bool newObserve);
+
+  /// Set/get a fixed interval, in milliseconds, at which this widget will update
+  /// itself.  Default 20.  Specify an update interval of 0 to handle all events as
+  /// they occur.
+  int updateInterval() const;
+  void setUpdateInterval(int newInterval);
+
+  /// Add a QVTKWidget to observe mouse events on.  You can call this function
+  /// multiple times to observe multiple QVTKWidgets.
+  /// \sa observe
+  void observe(QVTKWidget * widget);
+
+  /// Add multiple QVTKWidgets at once to observe mouse events on.  You can
+  /// call this function multiple times to observe multiple QVTKWidgets.
+  /// \sa observe
+  void observe(QList<QVTKWidget *> widgets);
+
+  /// Remove a QVTKWidget to observe mouse events on.  You can call this
+  /// function multiple times to remove multiple QVTKWidgets.
+  /// \sa remove
+  void remove(QVTKWidget * widget);
+
+  /// Remove multiple QVTKWidgets at once to observe mouse events on.  You can
+  /// call this function multiple times to remove multiple QVTKWidgets.
+  /// \sa unobserve
+  void remove(QList<QVTKWidget *> widgets);
+
+  /// Returns whether a QVTKWidget is observed
+  bool isObserved(QVTKWidget * widget) const;
+
+  /// Returns the number of observed QVTKWidgets
+  int numberObserved()const;
+
+protected:
+  QScopedPointer<ctkVTKMagnifyViewPrivate> d_ptr;
+
+  /// Handles mouse events on the observed QVTKWidgets (specifically,
+  /// enterEvent, leaveEvent and mouseMoveEvent).
+  virtual bool eventFilter(QObject *obj, QEvent *event);
+
+signals:
+  void enteredObservedWidget(QVTKWidget * widget);
+  void leftObservedWidget(QVTKWidget * widget);
+
+private:
+  Q_DECLARE_PRIVATE(ctkVTKMagnifyView);
+  Q_DISABLE_COPY(ctkVTKMagnifyView);
+}; 
+
+#endif

+ 87 - 0
Libs/Visualization/VTK/Widgets/ctkVTKMagnifyView_p.h

@@ -0,0 +1,87 @@
+/*=========================================================================
+
+  Library:   CTK
+
+  Copyright (c) Kitware Inc.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.commontk.org/LICENSE
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+
+=========================================================================*/
+
+#ifndef __ctkVTKMagnifyView_p_h
+#define __ctkVTKMagnifyView_p_h
+
+// Qt includes
+#include <QObject>
+class QPointF;
+class QTimerEvent;
+
+// CTK includes
+#include "ctkVTKMagnifyView.h"
+#include <ctkVTKObject.h>
+
+// VTK includes
+class QVTKWidget;
+
+class ctkVTKMagnifyViewPrivate : public QObject
+{
+  Q_OBJECT
+  QVTK_OBJECT
+  Q_DECLARE_PUBLIC(ctkVTKMagnifyView);
+protected:
+  ctkVTKMagnifyView* const q_ptr;
+public:
+  ctkVTKMagnifyViewPrivate(ctkVTKMagnifyView& object);
+  virtual ~ctkVTKMagnifyViewPrivate();
+
+  void init();
+  void observe(QVTKWidget * widget);
+  void remove(QVTKWidget * widget);
+  void connectRenderWindow(QVTKWidget * widget);
+  void disconnectRenderWindow(QVTKWidget * widget);
+
+protected:
+  void updatePixmap();
+  void removePixmap();
+  void timerEvent(QTimerEvent * event);
+  void restartTimer();
+  void resetEventHandler();
+
+  enum PendingEventType {
+    NoEvent = 0,
+    UpdatePixmapEvent,
+    RemovePixmapEvent
+    };
+
+  struct EventHandlerStruct
+    {
+    PendingEventType EventType;
+    QVTKWidget * Widget;
+    QPointF Position;
+    int UpdateInterval;
+    int TimerId;
+    };
+
+public slots:
+  void pushUpdatePixmapEvent();
+  void pushUpdatePixmapEvent(QPointF pos);
+  void pushRemovePixmapEvent();
+
+public:
+  QList<QVTKWidget *> ObservedQVTKWidgets;
+  double Magnification;
+  bool ObserveRenderWindowEvents;
+  EventHandlerStruct EventHandler;
+};
+
+#endif

+ 3 - 0
Libs/Widgets/CMakeLists.txt

@@ -40,6 +40,8 @@ SET(KIT_SRCS
   ctkConfirmExitDialog.h
   ctkCoordinatesWidget.cpp
   ctkCoordinatesWidget.h
+  ctkCrosshairLabel.cpp
+  ctkCrosshairLabel.h
   ctkDirectoryButton.cpp
   ctkDirectoryButton.h
   ctkDoubleRangeSlider.cpp
@@ -164,6 +166,7 @@ SET(KIT_MOC_SRCS
   ctkConsole.h
   ctkConsole_p.h
   ctkCoordinatesWidget.h
+  ctkCrosshairLabel.h
   ctkDirectoryButton.h
   ctkDoubleRangeSlider.h
   ctkDoubleSlider.h

+ 3 - 0
Libs/Widgets/Plugins/CMakeLists.txt

@@ -27,6 +27,8 @@ SET(PLUGIN_SRCS
   ctkComboBoxPlugin.h
   ctkCoordinatesWidgetPlugin.cpp
   ctkCoordinatesWidgetPlugin.h
+  ctkCrosshairLabelPlugin.cpp
+  ctkCrosshairLabelPlugin.h
   ctkDirectoryButtonPlugin.cpp
   ctkDirectoryButtonPlugin.h
   ctkDoubleRangeSliderPlugin.cpp
@@ -84,6 +86,7 @@ SET(PLUGIN_MOC_SRCS
   ctkColorPickerButtonPlugin.h
   ctkComboBoxPlugin.h
   ctkCoordinatesWidgetPlugin.h
+  ctkCrosshairLabelPlugin.h
   ctkDirectoryButtonPlugin.h
   ctkDoubleRangeSliderPlugin.h
   ctkDoubleSliderPlugin.h

+ 77 - 0
Libs/Widgets/Plugins/ctkCrosshairLabelPlugin.cpp

@@ -0,0 +1,77 @@
+/*=========================================================================
+
+  Library:   CTK
+
+  Copyright (c) Kitware Inc.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.commontk.org/LICENSE
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+
+=========================================================================*/
+
+// CTK includes
+#include "ctkCrosshairLabelPlugin.h"
+#include "ctkCrosshairLabel.h"
+
+//-----------------------------------------------------------------------------
+ctkCrosshairLabelPlugin::ctkCrosshairLabelPlugin(QObject *_parent)
+        : QObject(_parent)
+{
+
+}
+
+//-----------------------------------------------------------------------------
+QWidget *ctkCrosshairLabelPlugin::createWidget(QWidget *_parent)
+{
+  ctkCrosshairLabel* _widget = new ctkCrosshairLabel(_parent);
+  return _widget;
+}
+
+//-----------------------------------------------------------------------------
+QString ctkCrosshairLabelPlugin::domXml() const
+{
+  return "<widget class=\"ctkCrosshairLabel\" \
+          name=\"CrosshairLabel\">\n"
+      " <property name=\"geometry\">\n"
+      "  <rect>\n"
+      "   <x>0</x>\n"
+      "   <y>0</y>\n"
+      "   <width>150</width>\n"
+      "   <height>150</height>\n"
+      "  </rect>\n"
+      " </property>\n"
+      "</widget>\n";
+}
+
+// --------------------------------------------------------------------------
+ QIcon ctkCrosshairLabelPlugin::icon() const
+ {
+   return QIcon();
+ }
+
+//-----------------------------------------------------------------------------
+QString ctkCrosshairLabelPlugin::includeFile() const
+{
+  return "ctkCrosshairLabel.h";
+}
+
+//-----------------------------------------------------------------------------
+bool ctkCrosshairLabelPlugin::isContainer() const
+{
+  return false;
+}
+
+//-----------------------------------------------------------------------------
+QString ctkCrosshairLabelPlugin::name() const
+{
+  return "ctkCrosshairLabel";
+}

+ 45 - 0
Libs/Widgets/Plugins/ctkCrosshairLabelPlugin.h

@@ -0,0 +1,45 @@
+/*=========================================================================
+
+  Library:   CTK
+
+  Copyright (c) Kitware Inc.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.commontk.org/LICENSE
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+
+=========================================================================*/
+
+#ifndef __ctkCrosshairLabelPlugin_h
+#define __ctkCrosshairLabelPlugin_h
+
+// CTK includes
+#include "ctkWidgetsAbstractPlugin.h"
+
+class CTK_WIDGETS_PLUGINS_EXPORT ctkCrosshairLabelPlugin :
+  public QObject,
+  public ctkWidgetsAbstractPlugin
+{
+  Q_OBJECT
+
+public:
+  ctkCrosshairLabelPlugin(QObject *_parent = 0);
+
+  QWidget *createWidget(QWidget *_parent);
+  QString  domXml() const;
+  QIcon    icon() const;
+  QString  includeFile() const;
+  bool     isContainer() const;
+  QString  name() const;
+
+};
+
+#endif

+ 2 - 0
Libs/Widgets/Plugins/ctkWidgetsPlugins.h

@@ -33,6 +33,7 @@
 #include "ctkColorPickerButtonPlugin.h"
 #include "ctkComboBoxPlugin.h"
 #include "ctkCoordinatesWidgetPlugin.h"
+#include "ctkCrosshairLabelPlugin.h"
 #include "ctkDirectoryButtonPlugin.h"
 #include "ctkDoubleRangeSliderPlugin.h"
 #include "ctkDoubleSliderPlugin.h"
@@ -73,6 +74,7 @@ public:
             << new ctkColorPickerButtonPlugin
             << new ctkComboBoxPlugin
             << new ctkCoordinatesWidgetPlugin
+            << new ctkCrosshairLabelPlugin
             << new ctkDirectoryButtonPlugin
             << new ctkDoubleRangeSliderPlugin
             << new ctkDoubleSliderPlugin

+ 27 - 2
Libs/Widgets/Testing/Cpp/CMakeLists.txt

@@ -1,6 +1,6 @@
 SET(KIT ${PROJECT_NAME})
 
-CREATE_TEST_SOURCELIST(Tests ${KIT}CppTests.cxx
+SET(TEST_SOURCES
   ctkActionsWidgetTest1.cpp
   ctkAddRemoveComboBoxTest1.cpp
   ctkAxesWidgetTest1.cpp
@@ -19,6 +19,7 @@ CREATE_TEST_SOURCELIST(Tests ${KIT}CppTests.cxx
   ctkConfirmExitDialogTest1.cpp
   ctkConsoleTest1.cpp
   ctkCoordinatesWidgetTest1.cpp
+  ctkCrosshairLabelTest1.cpp
   ctkDirectoryButtonTest1.cpp
   ctkDoubleRangeSliderTest1.cpp
   ctkDoubleRangeSliderTest2.cpp
@@ -55,6 +56,19 @@ CREATE_TEST_SOURCELIST(Tests ${KIT}CppTests.cxx
   ctkExampleUseOfWorkflowWidgetUsingSignalsAndSlots.cpp
   )
 
+#
+# Tests expecting CTKData to be set
+#
+IF(EXISTS "${CTKData_DIR}")
+  LIST(APPEND TEST_SOURCES
+    ctkCrosshairLabelTest2.cpp
+    )
+ENDIF()
+
+CREATE_TEST_SOURCELIST(Tests ${KIT}CppTests.cxx
+  ${TEST_SOURCES}
+  )
+
 SET (TestsToRun ${Tests})
 REMOVE (TestsToRun ${KIT}CppTests.cxx)
 
@@ -81,7 +95,10 @@ TARGET_LINK_LIBRARIES(${KIT}CppTests ${LIBRARY_NAME})
 SET( KIT_TESTS ${CPP_TEST_PATH}/${KIT}CppTests)
 
 MACRO( SIMPLE_TEST  TESTNAME )
-  ADD_TEST( ${TESTNAME} ${KIT_TESTS} ${TESTNAME} )
+  ADD_TEST( ${TESTNAME} ${KIT_TESTS} ${TESTNAME}
+            -D "${CTKData_DIR}/Data"
+            -V "${CTKData_DIR}/Baseline/Libs/Widgets"
+            )
   SET_PROPERTY(TEST ${TESTNAME} PROPERTY LABELS ${PROJECT_NAME})
 ENDMACRO( SIMPLE_TEST  )
 
@@ -107,6 +124,7 @@ SIMPLE_TEST( ctkComboBoxTest1 )
 SIMPLE_TEST( ctkConfirmExitDialogTest1 )
 SIMPLE_TEST( ctkConsoleTest1 )
 SIMPLE_TEST( ctkCoordinatesWidgetTest1 )
+SIMPLE_TEST( ctkCrosshairLabelTest1 )
 SIMPLE_TEST( ctkDateRangeWidgetTest1 )
 SIMPLE_TEST( ctkDirectoryButtonTest1 )
 SIMPLE_TEST( ctkDoubleRangeSliderTest1 )
@@ -141,3 +159,10 @@ SIMPLE_TEST( ctkToolTipTrapperTest1 )
 SIMPLE_TEST( ctkTreeComboBoxTest1 )
 SIMPLE_TEST( ctkWorkflowWidgetTest1 )
 SIMPLE_TEST( ctkWorkflowWidgetTest2 )
+
+#
+# Add Tests expecting CTKData to be set
+#
+IF(EXISTS "${CTKData_DIR}")
+  SIMPLE_TEST( ctkCrosshairLabelTest2 )
+ENDIF()

+ 131 - 0
Libs/Widgets/Testing/Cpp/ctkCrosshairLabelTest1.cpp

@@ -0,0 +1,131 @@
+/*=========================================================================
+
+  Library:   CTK
+
+  Copyright (c) Kitware Inc.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.commontk.org/LICENSE
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+
+=========================================================================*/
+
+// Qt includes
+#include <QApplication>
+#include <QTimer>
+
+// CTK includes
+#include "ctkCrosshairLabel.h"
+
+// STD includes
+#include <cstdlib>
+#include <iostream>
+
+//-----------------------------------------------------------------------------
+int ctkCrosshairLabelTest1(int argc, char * argv [] )
+{
+  QApplication app(argc, argv);
+
+  ctkCrosshairLabel crosshair;
+
+  // check default values
+  if (!crosshair.showCrosshair() ||
+      crosshair.crosshairPen().color()
+      != crosshair.palette().color(QPalette::Highlight) ||
+      crosshair.crosshairPen().width() != 0 ||
+      crosshair.crosshairPen().joinStyle() != Qt::MiterJoin ||
+      crosshair.crosshairType() != ctkCrosshairLabel::SimpleCrosshair ||
+      crosshair.marginColor() != crosshair.palette().color(QPalette::Window) ||
+      crosshair.bullsEyeWidth() != 15)
+    {
+    std::cerr << "ctkCrosshairLabel: Wrong default values. " << std::endl
+              << " " << crosshair.showCrosshair()
+              << " " << qPrintable(crosshair.crosshairPen().color().name())
+              << " " << crosshair.crosshairPen().width()
+              << " " << static_cast<int>(crosshair.crosshairPen().joinStyle())
+              << " " << crosshair.crosshairType()
+              << " " << qPrintable(crosshair.marginColor().name())
+              << " " << crosshair.bullsEyeWidth() << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  // Show crosshair
+  crosshair.setShowCrosshair(false);
+  if (crosshair.showCrosshair())
+    {
+    std::cerr << "ctkCrosshairLabel:setShowCrosshair failed. "
+              << crosshair.showCrosshair() << std::endl;
+    return EXIT_FAILURE;
+    }
+  crosshair.setShowCrosshair(true);
+
+  // Crosshair pen
+  QPen crosshairPen(Qt::yellow);
+  crosshairPen.setJoinStyle(Qt::MiterJoin);
+  crosshair.setCrosshairPen(crosshairPen);
+  if (crosshair.crosshairPen() != crosshairPen)
+    {
+    std::cerr << "ctkCrosshairLabel:setCrosshairPen failed. "
+              << qPrintable(crosshair.crosshairPen().color().name()) << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  // Crosshair type
+  crosshair.setCrosshairType(ctkCrosshairLabel::BullsEyeCrosshair);
+  if (crosshair.crosshairType() != ctkCrosshairLabel::BullsEyeCrosshair)
+    {
+    std::cerr << "ctkCrosshairLabel:setCrosshairType failed. "
+              << crosshair.crosshairType() << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  // Margin color - invalid input
+  QColor transparentBlue(Qt::blue);
+  transparentBlue.setAlphaF(0.25);
+  QColor origColor = crosshair.marginColor();
+  crosshair.setMarginColor(QColor());
+  if (crosshair.marginColor() != origColor)
+    {
+    std::cerr << "ctkCrosshairLabel:setMarginColor failed - invalid input. "
+              << qPrintable(crosshair.marginColor().name()) << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  // Margin color - should ignore alpha channel
+  crosshair.setMarginColor(transparentBlue);
+  if (crosshair.marginColor() != Qt::blue)
+    {
+      {
+      std::cerr << "ctkCrosshairLabel:setMarginColor failed - valid input. "
+                << qPrintable(crosshair.marginColor().name()) << std::endl;
+      return EXIT_FAILURE;
+      }
+    }
+
+  // Bulls eye width
+  crosshair.setBullsEyeWidth(0);
+  if (crosshair.bullsEyeWidth() != 0)
+    {
+    std::cerr << "ctkCrosshairLabel:setBullsEyeWidth failed. "
+              << crosshair.bullsEyeWidth() << std::endl;
+    return EXIT_FAILURE;
+    }
+
+  crosshair.show();
+  if (argc < 2 || QString(argv[1]) != "-I" )
+    {
+    QTimer::singleShot(200, &app, SLOT(quit()));
+    }
+
+  return app.exec();
+
+}
+

+ 248 - 0
Libs/Widgets/Testing/Cpp/ctkCrosshairLabelTest2.cpp

@@ -0,0 +1,248 @@
+/*=========================================================================
+
+  Library:   CTK
+
+  Copyright (c) Kitware Inc.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.commontk.org/LICENSE
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+
+=========================================================================*/
+
+// Qt includes
+#include <QApplication>
+#include <QIcon>
+#include <QSignalSpy>
+#include <QStyle>
+#include <QTimer>
+
+// CTK includes
+#include "ctkCrosshairLabel.h"
+#include "ctkCommandLineParser.h"
+
+// STD includes
+#include <cstdlib>
+#include <iostream>
+
+//-----------------------------------------------------------------------------
+bool imageCompare(ctkCrosshairLabel& crosshair, QString baselineDirectory,
+                  QString baselineFilename)
+{
+  QImage output = QPixmap::grabWidget(&crosshair).toImage();
+  QImage baseline(baselineDirectory + "/" + baselineFilename);
+  return output == baseline;
+}
+
+//-----------------------------------------------------------------------------
+// (Used to create baselines, not during testing).
+void imageSave(ctkCrosshairLabel& crosshair, QString baselineDirectory,
+               QString baselineFilename)
+{
+  QImage output = QPixmap::grabWidget(&crosshair).toImage();
+  output.save(baselineDirectory + "/" + baselineFilename);
+}
+
+//-----------------------------------------------------------------------------
+bool runBaselineTest(ctkCrosshairLabel& crosshair,
+                     QString baselineDirectory, QString baselineFilename,
+                     QString errorMessage)
+{
+  QApplication::processEvents();
+  if (!imageCompare(crosshair, baselineDirectory, baselineFilename))
+    {
+    std::cerr << "ctkCrosshairLabel baseline comparison failed when "
+              << qPrintable(errorMessage) << "." << std::endl;
+    return false;
+    }
+  return true;
+}
+
+//-----------------------------------------------------------------------------
+int ctkCrosshairLabelTest2(int argc, char * argv [] )
+{
+  QApplication app(argc, argv);
+
+  // Command line parser
+  ctkCommandLineParser parser;
+  parser.addArgument("", "-D", QVariant::String);
+  parser.addArgument("", "-V", QVariant::String);
+  parser.addArgument("", "-I", QVariant::String);
+  bool ok = false;
+  QHash<QString, QVariant> parsedArgs = parser.parseArguments(app.arguments(), &ok);
+  if (!ok)
+    {
+    std::cerr << qPrintable(parser.errorString()) << std::endl;
+    return EXIT_FAILURE;
+    }
+  QString dataDirectory = parsedArgs["-D"].toString();
+  QString baselineDirectory = parsedArgs["-V"].toString();
+  bool interactive = parsedArgs["-I"].toBool();
+
+  // The remainder is interactive, so abort now if command line args specify otherwise
+  if (interactive)
+    {
+    return EXIT_SUCCESS;
+    }
+
+  // Create the crosshair widget
+  ctkCrosshairLabel crosshair;
+  QPen crosshairPen(Qt::yellow);
+  crosshairPen.setJoinStyle(Qt::MiterJoin);
+  crosshair.setCrosshairPen(crosshairPen);
+  crosshair.setMarginColor(Qt::blue);
+
+  QPixmap pixmap(dataDirectory + "/" + "computerIcon.png");
+
+  // Basesize is always odd
+  QSize baseSize = pixmap.size();
+  if (pixmap.width() % 2 == 0)
+    {
+    baseSize.setWidth(baseSize.width()+1);
+    }
+  if (pixmap.height() % 2 == 0)
+    {
+    baseSize.setHeight(baseSize.height()+1);
+    }
+
+  // Odd widget size
+  crosshair.setMinimumSize(baseSize);
+  crosshair.setPixmap(pixmap.scaled(baseSize));
+  crosshair.show();
+
+  // Test bullsEyeWidth and line width with odd widget size
+  crosshair.setCrosshairType(ctkCrosshairLabel::BullsEyeCrosshair);
+
+  ///
+  crosshair.setBullsEyeWidth(15);
+  crosshairPen.setWidth(1);
+  crosshair.setCrosshairPen(crosshairPen);
+  if (!runBaselineTest(crosshair, baselineDirectory,
+                       "ctkCrosshairLabelTest2a.png",
+                       "using bulls-eye crosshair (odd size, bullsEye 15, width 1)"))
+    {
+    return EXIT_FAILURE;
+    }
+
+  ///
+  crosshairPen.setWidth(5);
+  crosshair.setCrosshairPen(crosshairPen);
+  if (!runBaselineTest(crosshair, baselineDirectory,
+                       "ctkCrosshairLabelTest2b.png",
+                       "using bulls-eye crosshair (odd size, bullsEye 15, width 5)"))
+    {
+    return EXIT_FAILURE;
+    }
+
+  ///
+  crosshair.setBullsEyeWidth(14);
+  crosshairPen.setWidth(0); // equivalent to 1
+  crosshair.setCrosshairPen(crosshairPen);
+  if (!runBaselineTest(crosshair, baselineDirectory,
+                       "ctkCrosshairLabelTest2c.png",
+                       "using bulls-eye crosshair (odd size, bullsEye 14, width 1)"))
+    {
+    return EXIT_FAILURE;
+    }
+
+  ///
+  crosshairPen.setWidth(4);
+  crosshair.setCrosshairPen(crosshairPen);
+  if (!runBaselineTest(crosshair, baselineDirectory,
+                       "ctkCrosshairLabelTest2d.png",
+                       "using bulls-eye crosshair (odd size, bullsEye 14, width 4)"))
+    {
+    return EXIT_FAILURE;
+    }
+
+  // Test bullsEyeWidth and line width with even widget size
+  crosshair.resize(baseSize.width()+1, baseSize.height()+1);
+
+  ///
+  crosshair.setBullsEyeWidth(14);
+  crosshairPen.setWidth(1);
+  crosshair.setCrosshairPen(crosshairPen);
+  if (!runBaselineTest(crosshair, baselineDirectory,
+                       "ctkCrosshairLabelTest2e.png",
+                       "using bulls-eye crosshair (even size, bullsEye 14, width 1)"))
+    {
+    return EXIT_FAILURE;
+    }
+
+  ///
+  crosshairPen.setWidth(4);
+  crosshair.setCrosshairPen(crosshairPen);
+  if (!runBaselineTest(crosshair, baselineDirectory,
+                       "ctkCrosshairLabelTest2f.png",
+                       "using bulls-eye crosshair (even size, bullsEye 14, width 4)"))
+    {
+    return EXIT_FAILURE;
+    }
+
+  ///
+  crosshair.setBullsEyeWidth(15);
+  crosshairPen.setWidth(0);
+  crosshair.setCrosshairPen(crosshairPen);
+  if (!runBaselineTest(crosshair, baselineDirectory,
+                       "ctkCrosshairLabelTest2g.png",
+                       "using bulls-eye crosshair (even size, bullsEye 15, width 1)"))
+    {
+    return EXIT_FAILURE;
+    }
+
+  ///
+  crosshairPen.setWidth(5);
+  crosshair.setCrosshairPen(crosshairPen);
+  if (!runBaselineTest(crosshair, baselineDirectory,
+                       "ctkCrosshairLabelTest2h.png",
+                       "using bulls-eye crosshair (even size, bullsEye 15, width 5)"))
+    {
+    return EXIT_FAILURE;
+    }
+
+  // Crosshair not shown
+  crosshair.resize(baseSize);
+  crosshair.setShowCrosshair(false);
+  crosshair.setCrosshairPen(crosshairPen);
+  if (!runBaselineTest(crosshair, baselineDirectory,
+                       "ctkCrosshairLabelTest2i.png",
+                       "show crosshair false"))
+    {
+    return EXIT_FAILURE;
+    }
+
+  // Crosshair crosshair
+  crosshair.setShowCrosshair(true);
+  crosshair.setCrosshairType(ctkCrosshairLabel::SimpleCrosshair);
+  crosshairPen.setWidth(0);
+  crosshair.setCrosshairPen(crosshairPen);
+  if (!runBaselineTest(crosshair, baselineDirectory,
+                       "ctkCrosshairLabelTest2j.png",
+                       "using cross-hair crosshair (odd size)"))
+    {
+    return EXIT_FAILURE;
+    }
+  crosshair.resize(baseSize.width()+1, baseSize.height()+1);
+  crosshairPen.setWidth(1);
+  crosshair.setCrosshairPen(crosshairPen);
+  if (!runBaselineTest(crosshair, baselineDirectory,
+                       "ctkCrosshairLabelTest2k.png",
+                       "using cross-hair crosshair (even size)"))
+    {
+    return EXIT_FAILURE;
+    }
+
+  // We already tested for interactive mode
+  QTimer::singleShot(200, &app, SLOT(quit()));
+  return app.exec();
+
+}
+

+ 28 - 0
Libs/Widgets/ctkAxesWidget.cpp

@@ -314,3 +314,31 @@ void ctkAxesWidget::mouseReleaseEvent(QMouseEvent *mouseEvent)
   Q_D(ctkAxesWidget);
   this->setCurrentAxis(d->axisAtPos(mouseEvent->pos()));
 }
+
+// --------------------------------------------------------------------------
+QSize ctkAxesWidget::minimumSizeHint()const
+{
+  // Pretty arbitrary size.
+  return QSize(100, 100);
+}
+
+// --------------------------------------------------------------------------
+QSize ctkAxesWidget::sizeHint()const
+{
+  // Pretty arbitrary size
+  return QSize(100, 100);
+}
+
+//----------------------------------------------------------------------------
+bool ctkAxesWidget::hasHeightForWidth()const
+{
+  return true;
+}
+
+//----------------------------------------------------------------------------
+int ctkAxesWidget::heightForWidth(int width)const
+{
+  // Tends to be square
+  return width;
+}
+

+ 6 - 0
Libs/Widgets/ctkAxesWidget.h

@@ -80,6 +80,12 @@ public slots :
   /// Set the autoReset property to None anytime the currentAxis is changed.
   void setAutoReset(bool reset);
 
+  /// Size hints
+  virtual QSize minimumSizeHint()const;
+  virtual QSize sizeHint()const;
+  virtual bool hasHeightForWidth()const;
+  virtual int heightForWidth(int width)const;
+
 protected: 
   void paintEvent(QPaintEvent *);
   void mousePressEvent(QMouseEvent *mouseEvent);

+ 340 - 0
Libs/Widgets/ctkCrosshairLabel.cpp

@@ -0,0 +1,340 @@
+/*=========================================================================
+
+  Library:   CTK
+
+  Copyright (c) Kitware Inc.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.commontk.org/LICENSE
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+
+=========================================================================*/
+
+// Qt includes
+#include <QColor>
+#include <QDebug>
+#include <QEvent>
+#include <QPainter>
+#include <QSize>
+
+// CTK includes
+#include "ctkCrosshairLabel.h"
+#include "ctkLogger.h"
+
+// STD includes
+#include <math.h>
+
+//--------------------------------------------------------------------------
+static ctkLogger logger("org.commontk.visualization.vtk.widgets.ctkCrosshairLabel");
+//--------------------------------------------------------------------------
+
+//-----------------------------------------------------------------------------
+class ctkCrosshairLabelPrivate
+{
+  Q_DECLARE_PUBLIC(ctkCrosshairLabel);
+protected:
+  ctkCrosshairLabel* const q_ptr;
+public:
+  ctkCrosshairLabelPrivate(ctkCrosshairLabel& object);
+
+  void init();
+  void drawCrosshair();
+  void drawSimpleCrosshair(QPainter& painter);
+  void drawBullsEyeCrosshair(QPainter& painter);
+
+  bool      ShowCrosshair;
+  QPen      CrosshairPen;
+  ctkCrosshairLabel::CrosshairTypes CrosshairType;
+  int       BullsEyeWidth;
+
+  static const double BULLS_EYE_BLANK_FRACTION = 0.1;
+};
+
+// --------------------------------------------------------------------------
+// ctkCrosshairLabelPrivate methods
+
+// --------------------------------------------------------------------------
+ctkCrosshairLabelPrivate::ctkCrosshairLabelPrivate(ctkCrosshairLabel& object)
+  :q_ptr(&object)
+{
+  this->ShowCrosshair = true;
+  this->CrosshairType = ctkCrosshairLabel::SimpleCrosshair;
+  this->BullsEyeWidth = 15;
+}
+
+//---------------------------------------------------------------------------
+void ctkCrosshairLabelPrivate::init()
+{
+  Q_Q(ctkCrosshairLabel);
+  q->setAutoFillBackground(true);
+  q->setAlignment(Qt::AlignCenter);
+  this->CrosshairPen.setColor(q->palette().color(QPalette::Highlight));
+  this->CrosshairPen.setWidth(0);
+  this->CrosshairPen.setJoinStyle(Qt::MiterJoin);
+}
+
+//---------------------------------------------------------------------------
+void ctkCrosshairLabelPrivate::drawCrosshair()
+{
+  // Abort if we are not to draw the crosshair
+  if (!this->ShowCrosshair)
+    {
+    return;
+    }
+
+  // Setup the painter object to paint on the label
+  Q_Q(ctkCrosshairLabel);
+  QPainter painter(q);
+  painter.setPen(this->CrosshairPen);
+
+  // Draw crosshair (based on current parameters) onto the label
+  switch (this->CrosshairType)
+    {
+    case ctkCrosshairLabel::SimpleCrosshair:
+      this->drawSimpleCrosshair(painter);
+      break;
+    case ctkCrosshairLabel::BullsEyeCrosshair:
+      this->drawBullsEyeCrosshair(painter);
+      break;
+    default:
+      qDebug() << "Unsupported crosshair type" << this->CrosshairType;
+      break;
+    }
+}
+
+//---------------------------------------------------------------------------
+void ctkCrosshairLabelPrivate::drawSimpleCrosshair(QPainter& painter)
+{
+  Q_Q(ctkCrosshairLabel);
+  QSize size = q->size();
+  double halfWidth = (size.width()-1.0) / 2.0;
+  double halfHeight = (size.height()-1.0) / 2.0;
+  painter.drawLine(QPointF(0, halfHeight), QPointF(size.width(), halfHeight));
+  painter.drawLine(QPointF(halfWidth, 0), QPointF(halfWidth, size.height()));
+}
+
+// --------------------------------------------------------------------------
+void ctkCrosshairLabelPrivate::drawBullsEyeCrosshair(QPainter& painter)
+{
+  Q_Q(ctkCrosshairLabel);
+  QSize size = q->size();
+
+  // Draw rectangle
+  double bullsEye = this->BullsEyeWidth;
+  double lineWidth = painter.pen().width();
+  lineWidth = std::max(lineWidth, 1.0);
+  double halfLineWidth = (lineWidth-1.0) / 2.0;
+  double x = (size.width()-bullsEye) / 2.0;
+  double y = (size.height()-bullsEye) / 2.0;
+  double rectWidth = bullsEye;
+  if (bullsEye != 1)
+    {
+    rectWidth = rectWidth - lineWidth;
+    }
+  rectWidth = std::max(rectWidth, 0.0);
+  painter.drawRect(
+      QRectF(x+halfLineWidth, y+halfLineWidth, rectWidth, rectWidth));
+
+  // Draw the lines
+  double halfWidth = (size.width()-1.0) / 2.0;
+  double halfHeight = (size.height()-1.0) / 2.0;
+  double blank = round(std::min(halfWidth, halfHeight)
+                       * this->BULLS_EYE_BLANK_FRACTION);
+
+  painter.drawLine(QPointF(0, halfHeight), QPointF(x-blank-1.0, halfHeight));
+  painter.drawLine(QPointF(x+bullsEye+blank, halfHeight),
+                   QPointF(size.width(), halfHeight));
+  painter.drawLine(QPointF(halfWidth, 0), QPointF(halfWidth, y-blank-1.0));
+  painter.drawLine(QPointF(halfWidth, y+bullsEye+blank),
+                   QPointF(halfWidth, size.height()));
+}
+
+//---------------------------------------------------------------------------
+// ctkCrosshairLabel methods
+
+// --------------------------------------------------------------------------
+ctkCrosshairLabel::ctkCrosshairLabel(QWidget* parent)
+  : Superclass(parent)
+  , d_ptr(new ctkCrosshairLabelPrivate(*this))
+{
+  Q_D(ctkCrosshairLabel);
+  d->init();
+}
+
+// --------------------------------------------------------------------------
+ctkCrosshairLabel::~ctkCrosshairLabel()
+{
+}
+
+// --------------------------------------------------------------------------
+CTK_GET_CPP(ctkCrosshairLabel, bool, showCrosshair, ShowCrosshair);
+
+// --------------------------------------------------------------------------
+void ctkCrosshairLabel::setShowCrosshair(bool newShow)
+{
+  Q_D(ctkCrosshairLabel);
+  if (newShow == d->ShowCrosshair)
+    {
+    return;
+    }
+
+  d->ShowCrosshair = newShow;
+  this->update();
+}
+
+// --------------------------------------------------------------------------
+CTK_GET_CPP(ctkCrosshairLabel, QPen, crosshairPen, CrosshairPen);
+
+// --------------------------------------------------------------------------
+void ctkCrosshairLabel::setCrosshairPen(const QPen& newPen)
+{
+  Q_D(ctkCrosshairLabel);
+  if (newPen == d->CrosshairPen)
+    {
+    return;
+    }
+
+  d->CrosshairPen = newPen;
+  this->update();
+}
+
+// --------------------------------------------------------------------------
+QColor ctkCrosshairLabel::crosshairColor() const
+{
+  Q_D(const ctkCrosshairLabel);
+  return d->CrosshairPen.color();
+}
+
+// --------------------------------------------------------------------------
+void ctkCrosshairLabel::setCrosshairColor(const QColor& newColor)
+{
+  Q_D(ctkCrosshairLabel);
+  if (d->CrosshairPen.color() == newColor)
+    {
+    return;
+    }
+
+  d->CrosshairPen.setColor(newColor);
+  this->update();
+}
+
+// --------------------------------------------------------------------------
+int ctkCrosshairLabel::lineWidth() const
+{
+  Q_D(const ctkCrosshairLabel);
+  return d->CrosshairPen.width();
+}
+
+// --------------------------------------------------------------------------
+void ctkCrosshairLabel::setLineWidth(int newWidth)
+{
+  Q_D(ctkCrosshairLabel);
+  if (d->CrosshairPen.width() == newWidth || newWidth < 0)
+    {
+    return;
+    }
+
+  d->CrosshairPen.setWidth(newWidth);
+  this->update();
+}
+
+// --------------------------------------------------------------------------
+CTK_GET_CPP(ctkCrosshairLabel, ctkCrosshairLabel::CrosshairTypes,
+            crosshairType, CrosshairType);
+
+// --------------------------------------------------------------------------
+void ctkCrosshairLabel::setCrosshairType(const CrosshairTypes& newType)
+{
+  Q_D(ctkCrosshairLabel);
+  if (newType == d->CrosshairType)
+    {
+    return;
+    }
+
+  d->CrosshairType = newType;
+  this->update();
+}
+
+// --------------------------------------------------------------------------
+QColor ctkCrosshairLabel::marginColor() const
+  {
+  return this->palette().color(QPalette::Window);
+  }
+
+// --------------------------------------------------------------------------
+void ctkCrosshairLabel::setMarginColor(const QColor& newColor)
+{
+  if (!newColor.isValid())
+    {
+    return;
+    }
+
+  QPalette palette(this->palette());
+  QColor solidColor(newColor.rgb());
+  if (solidColor != palette.color(QPalette::Window))
+    {
+    // Ignore alpha channel
+    palette.setColor(QPalette::Window, solidColor);
+    this->setPalette(palette);
+    this->update();
+    }
+}
+
+// --------------------------------------------------------------------------
+CTK_GET_CPP(ctkCrosshairLabel, int, bullsEyeWidth, BullsEyeWidth);
+
+// --------------------------------------------------------------------------
+void ctkCrosshairLabel::setBullsEyeWidth(int newWidth)
+{
+  Q_D(ctkCrosshairLabel);
+  if (newWidth == d->BullsEyeWidth || newWidth < 0)
+    {
+    return;
+    }
+
+  d->BullsEyeWidth = newWidth;
+  this->update();
+}
+
+// --------------------------------------------------------------------------
+void ctkCrosshairLabel::paintEvent(QPaintEvent * event)
+{
+  Superclass::paintEvent(event);
+  Q_D(ctkCrosshairLabel);
+  d->drawCrosshair();
+}
+
+// --------------------------------------------------------------------------
+QSize ctkCrosshairLabel::minimumSizeHint()const
+{
+  // Pretty arbitrary size (matches ctkVTKAbstractView)
+  return QSize(50, 50);
+}
+
+// --------------------------------------------------------------------------
+QSize ctkCrosshairLabel::sizeHint()const
+{
+  // Pretty arbitrary size (matches ctkVTKAbstractView)
+  return QSize(300, 300);
+}
+
+//----------------------------------------------------------------------------
+bool ctkCrosshairLabel::hasHeightForWidth()const
+{
+  return true;
+}
+
+//----------------------------------------------------------------------------
+int ctkCrosshairLabel::heightForWidth(int width)const
+{
+  // Tends to be square
+  return width;
+}

+ 130 - 0
Libs/Widgets/ctkCrosshairLabel.h

@@ -0,0 +1,130 @@
+/*=========================================================================
+
+  Library:   CTK
+
+  Copyright (c) Kitware Inc.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.commontk.org/LICENSE
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+
+=========================================================================*/
+
+#ifndef __ctkCrosshairLabel_h
+#define __ctkCrosshairLabel_h
+
+// QT includes
+#include <QLabel>
+#include <QPen>
+class QColor;
+class QSize;
+
+// CTK includes
+#include "ctkWidgetsExport.h"
+
+class ctkCrosshairLabelPrivate;
+
+/// Draws a crosshair onto a QLabel.  This widget is designed to be used to show
+/// a crosshair overlaid onto an image (the QLabel's pixmap).
+/// Since painting must be done in discrete pixels, this widget looks best
+/// when the widget size and the crosshair line width are both either even or odd.
+/// If using a bulls-eye crosshair, this widget looks best if the crosshair line
+/// width and the bullsEyeWidth are both either even or odd.
+
+class CTK_WIDGETS_EXPORT ctkCrosshairLabel : public QLabel
+{
+  Q_OBJECT
+  Q_FLAGS(CrosshairType CrosshairTypes)
+  Q_PROPERTY(bool showCrosshair READ showCrosshair WRITE setShowCrosshair)
+  // QT designer does not yet support QPen properties, so we provide the
+  // temporary properties crosshairColor and lineWidth.
+  Q_PROPERTY(QPen crosshairPen READ crosshairPen WRITE setCrosshairPen
+             DESIGNABLE false)
+  Q_PROPERTY(QColor crosshairColor READ crosshairColor WRITE setCrosshairColor)
+  Q_PROPERTY(int lineWidth READ lineWidth WRITE setLineWidth)
+  Q_PROPERTY(CrosshairTypes crosshairType READ crosshairType WRITE setCrosshairType)
+  Q_PROPERTY(QColor marginColor READ marginColor WRITE setMarginColor)
+  Q_PROPERTY(int bullsEyeWidth READ bullsEyeWidth WRITE setBullsEyeWidth)
+
+public:
+  /// Constructors
+  typedef QLabel Superclass;
+  explicit ctkCrosshairLabel(QWidget* parent = 0);
+  virtual ~ctkCrosshairLabel();
+
+  /// Enumeration over types of crosshairs
+  enum CrosshairType {
+    SimpleCrosshair = 0,
+    BullsEyeCrosshair
+  };
+  Q_DECLARE_FLAGS(CrosshairTypes, CrosshairType)
+
+  /// Set/get whether or not to draw the crosshair.  Default True.
+  bool showCrosshair() const;
+  void setShowCrosshair(bool newShow);
+
+  /// Set/get the pen used to draw the crosshair.  Default color is from the
+  /// widget's palette's Highlight role, default width is 0 (which draws
+  /// at 1 pixel wide regardless of painter transformation).  Since painting
+  /// must be done in discrete pixels, this widget looks best when the widget
+  /// size and the crosshair pen width are both either even or odd.
+  QPen crosshairPen() const;
+  void setCrosshairPen(const QPen& newPen);
+
+  /// Temporary: Set/get the crosshair color (via QPen).  This will be removed once
+  /// Qt Designer supports QPen properties.
+  QColor crosshairColor() const;
+  void setCrosshairColor(const QColor& newColor);
+
+  /// Temporary: Set/get the crosshair line width (via QPen).  This will be removed
+  /// once Qt Designer supports QPen properties.  Since painting
+  /// must be done in discrete pixels, this widget looks best when the widget
+  /// size and the crosshair pen width are both either even or odd.
+  int lineWidth() const;
+  void setLineWidth(int newWidth);
+
+  /// Set/get the crosshair type.  Default SimpleCrosshair.
+  CrosshairTypes crosshairType() const;
+  void setCrosshairType(const CrosshairTypes& newType);
+
+  /// Set/get color to set the widget to when not magnifying or when label
+  /// size is larger than pixmap size.  Default is the color from the widget's
+  /// palette Window role.  Note that the Window role is overridden if you
+  /// set this property.  Does not support transparent colors.
+  QColor marginColor() const;
+  void setMarginColor(const QColor& newColor);
+
+  /// Set/get the width of the crosshair when in BullsEye mode.
+  /// Looks best when the BullsEye width and the widget size are both either
+  /// even or odd, preferably odd.  Default 15.  Since painting must be done
+  /// in discrete pixels, this widget looks best if the crosshair pen width and
+  /// the bullsEyeWidth are both either even or odd.
+  int bullsEyeWidth() const;
+  void setBullsEyeWidth(int newWidth);
+
+  /// Size hints
+  virtual QSize minimumSizeHint()const;
+  virtual QSize sizeHint()const;
+  virtual bool hasHeightForWidth()const;
+  virtual int heightForWidth(int width)const;
+
+protected:
+  QScopedPointer<ctkCrosshairLabelPrivate> d_ptr;
+
+  // Draws the crosshair on top of the widget.
+  virtual void paintEvent(QPaintEvent * event);
+
+private:
+  Q_DECLARE_PRIVATE(ctkCrosshairLabel);
+  Q_DISABLE_COPY(ctkCrosshairLabel);
+}; 
+
+#endif