Writing Custom Applications

From KitwarePublic
Jump to navigationJump to search

Motivation

This document describes how to create Qt-based custom visualization applications using ParaView's Parallel Visualization framework.

Applications based on ParaView have grown since the release of ParaView 3.0. In spite of best of our intentions we soon realized that it was not very easy to create applications that use ParaView core without subscribing to an user interface identical to ParaView's. The infamous pqMainWindowCore ended up being copy-pasted for every application and then tweaked and modified breaking every possible rule for good programming practices. Also it was hard to create domain specific ParaView-clones with limited user interface components, since various components have cross dependencies among each other and it becomes hard to ensure that all signals/slots are connected correctly for them to work.

To address these issues, we re-structured the application layer in Paraview. The main goals we set out to address are:

  • Facilitate creation of applications radically different from ParaView, with totally different workflows.
  • Facilitate creation of ParaView-variants that use most of ParaView functionality with minor behavioral or user-interface changes.

All such custom applications created using this framework will be referred to as ParaView-brands in this document.

Do I need a Custom App, or merely a Plugin?

When adding functionality to ParaView eg. filters, panels, menus, views etc. without changing the application level behavior of ParaView, plugins are the easiest. Note that plugins are only "additive" i.e. you can add functionality to ParaView using plugins, never remove or qualify existing behavior i.e you cannot change what happens when user clicks "File-Open" in ParaView using plugins, however, you can add support for a new reader that becomes available for the user in the File-Open dialog.

If what you need is change the way ParaView as an application works, i.e. totally customize the menus, get rid of the pipeline browser, change the pipeline centric user interface etc., then you should look into creating a custom application. Of course, custom applications can use plugins to add new features required by them.

Where are the examples?

ParaView application itself serves as an example of this framework. Look under ParaViewSourceDir/Applications/ParaView. Additional examples are available under ParaViewSourceDir/CustomApplications

How to write radically different applications based on ParaView?

Let's consider the simplest case first, we want to write an appliaction that's totally different in work flow than ParaView i.e. we want maximum customization. Here's how such an applications main.cxx might look:

<source lang="cpp">

 #include <QApplication>
 #include "pqApplicationCore.h"
 #include "myCustomMainWindow.h"
 int main(int argc, char** argc)
 {
  QApplication app(argc, argv);
  pqApplicationCore appCore(argc, argv);
  myCustomMainWindow window;
  window.show();
  return app.exec();
 }

</source>

This is similar to what one would do to create a Qt based application except that in addition to creating the QApplication, we are creating pqApplicationCore (or it's subclass). pqApplicationCore ensures that ParaView server-manager is initialized correctly. The CMakeLists.txt file for such an application would be pretty much standard with an ADD_EXECUTABLE for creating an executable with proper target dependencies.

ParaView-Based Applications

Writing custom applications using above style isn't too complicated. However, more often than not, custom applications tend to be more domain specific variants of ParaView that tweak the GUI to expose only those components that are relevant to the experts in the field eg. custom menus, toolbars etc. Such applications can benefit from the components we have developed to make customization easier.

The new architecture pivots around two new concepts:

  • Reactions - these are action handlers. They implement logic to handle the triggering of an action. They also implement logic to keep the enable state for the action update-to-date based on current application state. eg. pqLoadStateReaction -- reaction of load state action, which encapsulates ParaView's response for load state action. Reactions require the QAction to which they are reacting to. Any custom user interface that wants a QAction to be behave exactly like Paraview's, simply instantiates the reaction for it and that's it! The rest it managed by the reaction.
  • Behaviors - these are abstract application behaviors eg. ParaView always remains connected to a server (builtin by default). This gets encapsulated into pqAlwaysConnectedBehavior. If any custom application wants to a particular behavior, simply instantiates the corresponding behavior!

There's also a CMake macro provided to make it easier to build the client executables.

<source lang="python"> build_paraview_client(

 # The name for this client. This is the name used for the executable created.
 paraview
 # Optional name for the application (If none is specified then the
 # client-name is used). Application is name that shows up on the menu on a Mac, 
 # or the name that's used when saving the settings and may differ from the executable
 # name.
 APPLICATION_NAME "ParaView"
 # This is the title bar text. If none is provided the name will be used.
 TITLE "Kitware ParaView"
 
 # This is the organization name.
 ORGANIZATION "Kitware Inc."
 # PNG Image to be used for the Splash screen. If none is provided, default
 # ParaView splash screen will be shown. 
 SPLASH_IMAGE "${CMAKE_CURRENT_SOURCE_DIR}/Splash.png"
 # Provide version information for the client.
 VERSION_MAJOR ${PARAVIEW_VERSION_MAJOR}
 VERSION_MINOR ${PARAVIEW_VERSION_MINOR}
 VERSION_PATCH ${PARAVIEW_VERSION_PATCH}
 # Icon to be used for the Mac bundle.
 BUNDLE_ICON   "${CMAKE_CURRENT_SOURCE_DIR}/Icon.icns"
 # Icon to be used for the Windows application.
 APPLICATION_ICON "${CMAKE_CURRENT_SOURCE_DIR}/Icon.ico"
                                                                                
 # Name of the class to use for the main window. If none is specified,
 # default QMainWindow will be used.
 PVMAIN_WINDOW QMainWindow-subclass
 PVMAIN_WINDOW_INCLUDE QMainWindow-subclass-header
 # Next specify the plugins that are needed to be built and loaded on startup
 # for this client to work. These must be specified in the order that they
 # should be loaded. The name is the name of the plugin specified in the
 # add_paraview_plugin call.
 # Currently, only client-based plugins are supported. i.e. no effort is made
 # to load the plugins on the server side when a new server connection is made.
 # That may be added in future, if deemed necessary.
 # NOTE: This option may change in future.
 REQUIRED_PLUGINS PointSpritePlugin
 # Next specify the plugin that are not required, but if available, should be
 # loaded on startup. These must be specified in the order that they
 # should be loaded. The name is the name of the plugin specified in the
 # add_paraview_plugin call.
 # Currently, only client-based plugins are supported. i.e. no effort is made
 # to load the plugins on the server side when a new server connection is made.
 # That may be added in future, if deemed necessary.
 # NOTE: This option may change in future.
 OPTIONAL_PLUGINS ClientGraphView ClientTreeView
                                                                                
 # Extra targets that this executable depends on. Useful only if you are
 # building extra libraries for your application.
 EXTRA_DEPENDENCIES blah1 blah2
                                                                                
 # GUI Configuration XMLs that are used to configure the client eg. readers,
 # writers, sources menu, filters menu etc.
 GUI_CONFIGURATION_XMLS <list of xml files>
                                                                                
 # The Qt compressed help file (*.qch) which provides the documentation for the
 # application. *.qch files are typically generated from *.qhp files using
 # the qhelpgenerator executable.
 COMPRESSED_HELP_FILE MyApp.qch
                                                                                
 # Additional source files.
 SOURCES <list of source files>
 # If this option is present, then this macro will create a library named
 # pq{Name}Initializer with all the source components generated by this macro
 # that the executable links against. Otherwise, for sake of simplicity no
 # extra library is created.
 MAKE_INITIALIZER_LIBRARY
 
 # Optional to specify the installation prefix for all the binaries.
 # "bin" is used if none is specified.
 INSTALL_BIN_DIR "bin"
 
 # Optional to specify the installation prefix for all the libraries.
 # "lib/appname-major.minor" is used if none is specified (on windows "bin" is used").
 INSTALL_LIB_DIR "lib"
 )

</source>


The CMakeLists.txt file for ParaView application itself looks like:

<source lang="python">

  1. ------------------------------------------------------------------------------
  2. Build the client

build_paraview_client(paraview

   TITLE "ParaView ${PARAVIEW_VERSION_FULL}"
   ORGANIZATION  "Kitware"
   VERSION_MAJOR ${PARAVIEW_VERSION_MAJOR} 
   VERSION_MINOR ${PARAVIEW_VERSION_MINOR}
   VERSION_PATCH ${PARAVIEW_VERSION_PATCH}
   SPLASH_IMAGE "${CMAKE_CURRENT_SOURCE_DIR}/PVSplashScreen.png"
   PVMAIN_WINDOW ParaViewMainWindow
   PVMAIN_WINDOW_INCLUDE ParaViewMainWindow.h
   BUNDLE_ICON   "${CMAKE_CURRENT_SOURCE_DIR}/MacIcon.icns"
   APPLICATION_ICON  "${CMAKE_CURRENT_SOURCE_DIR}/WinIcon.ico"
   GUI_CONFIGURATION_XMLS
     ${CMAKE_CURRENT_SOURCE_DIR}/ParaViewSources.xml
     ${CMAKE_CURRENT_SOURCE_DIR}/ParaViewFilters.xml
     ${CMAKE_CURRENT_SOURCE_DIR}/ParaViewReaders.xml
     ${CMAKE_CURRENT_SOURCE_DIR}/ParaViewWriters.xml
   COMPRESSED_HELP_FILE "${ParaView_BINARY_DIR}/Documentation/paraview.qch"
   SOURCES ${ParaView_SOURCE_FILES}

) </source>

This macro generates a file with main() in it initializing different components of the application the proper sequence.

The QMainWindow subclass is typically the class where you have code that initializes the applications MainWindow, possibly from a ui file. ParaView has several QWidget subclasses that can be directly used in your ui file to bring in different components eg.

  • pqPipelineBrowserWidget -- pipeline browser
  • pqProxyTabWidget -- object inspector with apply button and properties pages
  • pqViewManager -- manages multiple views. Use this as the central-widget in your QMainWindow to provide ParaView-like workspace for the views.
  • pqPVAnimationWidget -- widget for the animation track editor
  • pqStatusBar -- QStatusBar subclass that can be used to promote the default status bar. It adds the progress widget to the status bar.

Additionally, your ui file will have code to setup the menubar with menus such as File menu, View menu etc. You can either add actions to these menus as per your choice, or if you want that menu to look exactly identical to ParaView's then use appropriate static method from pqParaViewMenuBuilder to build the menu for it. It will also set up the action-handlers for the actions in those menus to perform the same response as given by ParaView when that action is triggered. If instead, you choose to populate your own actions, you can still easily incorporate the same response as ParaView by using reactions. Looking at pqParaViewMenuBuilders.cxx, one can easily figure out what reaction class is required to handle the response for a particular menu action in ParaView.

Similarly there are ParaView-specific toolbars that can be created by using pqParaViewMenuBuilders::buildToolbars(). If you want to create only a subset of the toolbars, look at the implementation and pick the toolbars that you need in your application.

Most of these QWidget specializations and toolbars are designed to work off components provided by pqApplicationCore (or pqPVApplicationCore) directly, without dependencies among each other. Which makes it possible to simply pick and choose these components in an application.

The constructor for QMainWindow subclass for ParaView looks as follows:

<source lang="cpp"> //----------------------------------------------------------------------------- ParaViewMainWindow::ParaViewMainWindow() {

 this->Internals = new pqInternals();
 this->Internals->setupUi(this);
 ...
 // Populate application menus with actions.
 pqParaViewMenuBuilders::buildFileMenu(*this->Internals->menu_File);
 pqParaViewMenuBuilders::buildEditMenu(*this->Internals->menu_Edit);
 // Populate sources menu.
 pqParaViewMenuBuilders::buildSourcesMenu(*this->Internals->menuSources, this);
 // Populate filters menu.
 pqParaViewMenuBuilders::buildFiltersMenu(*this->Internals->menuFilters, this);
 // Populate Tools menu.
 pqParaViewMenuBuilders::buildToolsMenu(*this->Internals->menuTools);
 // setup the context menu for the pipeline browser.
 pqParaViewMenuBuilders::buildPipelineBrowserContextMenu(
   *this->Internals->pipelineBrowser);
 pqParaViewMenuBuilders::buildToolbars(*this);
 // Setup the View menu. This must be setup after all toolbars and dockwidgets
 // have been created.
 pqParaViewMenuBuilders::buildViewMenu(*this->Internals->menu_View, *this);
 // Setup the menu to show macros.
 pqParaViewMenuBuilders::buildMacrosMenu(*this->Internals->menu_Macros);
 // Setup the help menu.
 pqParaViewMenuBuilders::buildHelpMenu(*this->Internals->menu_Help);
 // Final step, define application behaviors. Since we want all ParaView
 // behaviors, we use this convenience method.
 new pqParaViewBehaviors(this, this);

}

</source>


Interesting Side-effects

  • Often we have two disconnected sections of the gui wanting to the same thing eg. the pipeline browser's context menu has a "Change Input" action as well as the "Edit" menu. In such cases we either end up duplicating the code or hacking to have a cross reference (in current ParaView, the Edit menu calls the pqPipelineBrowser::changeInput(). Hence now edit menu requires the pqPipelineBrowser to work! A nice thing with implementing reactions is that they provide a logical place to put such logic that can be reused by whoever is interested. So now I have a pqChangePipelineInputReaction which handles change of input, including when change input action is enabled etc. and I instantiate the reaction for both the Edit menu as well as the PIpeline browser's context menu. As a result both behave exactly the same with no code duplication and less bug prone since there's only one place to fix how an input changes! And because reactions even manages the enable/disable state it's just works nicely together -- I am eulogizing I know.
  • Reactions make it easier to avoid undo-redo related issues. As a rule of thumb, every reaction does work within an undo-block. So just use the helper functions BEGIN_UNDO_SET("name for undo-set") and END_UNDO_SET() and the start and end of your reaction crux and you are golden!

Resource Space

The Resource space has some reserved directories/files which are used to load brand specific configurations.

Location Role
:/<app-name>/SplashImage.img Splash Image used for splash screen and About dialog
:/<app-name>/Configuration/*.xml GUI configuration XML files which includes readers, writers, filters menu, sources menu etc. If muliple xml files are present, then all are loaded.
:/<app-name>/Documentation/*.qch Application documentation. If multiple Qt compressed help files are detected, then all are loaded.

Configuration XML Formats

Some components of the application can be configured using configuration xmls. These xmls must either have the root element as the tag required for configuring the component or that tag must a first-level child element under the root element i.e. for configuring the reader-factory, your configuration xml can be: <source lang="xml"> <ParaViewReaders> </ParaViewReaders> </source>

OR

<source lang="xml"> <SomeRoot>

 ...
 <ParaViewReaders>
   ...
 </ParaViewReaders>
 ..

</SomeRoot>. </source>

ParaViewReaders: Reader Factory Configuration

Reader Factory is used by FileOpen dialog/recent files and the like. Configure the reader factory to specify the support readers and file-formats. The identification tag for this configuration is <ParaViewReaders>. The format of this xml is as follows:

<source lang="xml">

 <ParaViewReaders>
    <Proxy group="[sm-proxy-group]" name="[sm-proxy-name]" />
    ....
 </ParaViewReaders>

</source>

ParaViewWriters:Writer Factory Configuration

Writer factory is used when writing datasets. Configure the writer factory to specify the supported writers. The identification tag for this configuration is <ParaViewWriters> and the format is same as that for the reader-factory.

<source lang="xml">

 <ParaViewWriters>
    <Proxy group="[sm-proxy-group]" name="[sm-proxy-name]" />
    ....
 </ParaViewWriters>

</source>

ParaViewFilters : Filters Menu Configuration

This is useful only if you are using the standard filter's menu provided by ParaView. The identification tag for this configuration is <ParaViewFilters />.

<source lang="xml">

 <ParaViewFilters>
   <Category name="[category name]" menu_label="[label for category sub-menu" 
     preserve_order="[optional, when 1, the filters are not sorted alphabetically in this sub-menu]"
     show_in_toolbal="[optional, when 1, a toolbar is created for this category]">
     ...
     <Proxy group="sm-group" name="sm-name" icon="optional, icon resource name" />
     ...
   </Category>
   ....
  
   <Proxy group="sm-group" name="sm-name" icon="optional, icon resource name" />
   
   ....
 </ParaViewFilters>

</source>

ParaViewSources : Sources Menu Configuration

This is useful only when you are using the standard sources menu provided by ParaView. The identification tag is <ParaViewSources /> and the format is same as that for the ParaViewFilters.


Application Initialization Sequence

When an ParaView-based application is created using the build_paraview_client() mechanism described here, following are sequence in which different main operations are performed.

  • The applicationName, applicationVersion and organizationNAme as specified in the macro, are set using the static QCoreApplication API. This happens before any objects are instantiated.
  • QApplication instance is created. This is required for any Qt-based application.
  • pqPVApplicationCore instance in instantiated.
    • This first initializes the server-manager application i.e. the process-module is set up, the proxy-manager is set up.
    • This results in creation of the various managers such as the pqPluginManager, pqPQLookupTableManager, pqAnimationManager, pqSelectionManager etc.
  • The QMainWindow subclass specified in the macro or QMainWindow if none is specified, is instantiated. Once the core components are initialized, the main window is created. So if you write your own QMainWindow subclass, you are free to use any of the server-manager or pqPVApplicationCore components as needed in your initialization code.
  • Next, we try to load the required and optional plugins are listed in the macro. If a required plugin could not be located or loaded, then the application quits with an error. If an optional plugin could not be located or loaded, then they are quietly skipped. Note this is happening after the mainWindow has been created. So do not use any components that will be brought in by the plugins in your mainWindow initialization code. The locations where these plugins are searched are as follows in the given order:
    • executable-dir (for Mac *.app, it's the app dir)
    • executable-dir/plugins/pluginname
    • *.app/Contents/Plugins/ (for Mac)
This is bound to change. Please refer to the documentation of Qt/Core/pqBrandPluginsLoader.h for a complete and updated list.
  • Once the plugins are loaded, the next step is to load the configuration xmls specified in the macro. All these xmls get compiled into a qt-resource that is then processed one after the other by calling pqApplicationCore::loadConfiguration(). Any GUI components that processes such configuration files listen to the pqApplicationCore::loadXML() signal and process the configuration xml as and when it is loaded. Since the configuration xmls are loaded after the plugins are loaded, your plugins can rely on configuration xmls.
  • Finally, the mainWindow's window-title is updated to match that specified in the macro and then the mainWindow is shown and the Qt event loop is begun.

Packaging

This section is under construction

Now that we have seen how to build custom applications, the next step is to build packages so that these custom applications can be distributed. ParaView uses CPack for packaging. Using CPack it's possible to create installer for different platforms.

Before reading this section, the developer is encouraged to familiarize himself/herself with the INSTALL() command from CMake and how to write install rules in CMake.

ParaView provides cmake-macros that can be used to make is easier to create packages for custom apps. These are defined in CMake/ParaViewBrandingCPack.cmake. This file automatically gets included when one does the following in their CMakeLists.txt <source lang="bash">

 FIND_PACKAGE(ParaView)
 INCLUDE(${PARAVIEW_USE_FILE})

</source>

There are two macros that constitute the packaging support provided by ParaView.

  • build_paraview_client_cpack_config_init
Sets up the cpack environment for packaging using the options passed in to this function.
  • build_paraview_client_cpack_config
Generate the cpack configuration file.

This is split into two macros, so that the developer has an opportunity to change some of the cpack options before generating the configuration e.g. picking the install-components that should be packaged.

<source lang="python"> build_paraview_client_cpack_config_init(

 # Name for the package  (or application). Corresponds to CPACK_PACKAGE_NAME
 PACKAGE_NAME "ParaView"
 # Name for the organization. Corresponds to CPACK_PACKAGE_VENDOR
 ORGANIZATION "Kitware Inc."
 # Application Version.
 VERSION_MAJOR 1
 VERSION_MINOR 0
 VERSION_PATCH 0
 # Short description of the project (only a few words). 
 # Corresponds to CPACK_PACKAGE_DESCRIPTION_SUMMARY.
 DESCRIPTION "Short Description"
 # License file  License file for the project, which  will typically be 
 # displayed to the user (often with an explicit "Accept" button,
 # for graphical installers) prior to installation. (Optional).
 # Corresponds to CPACK_RESOURCE_FILE_LICENSE.
 LICENSE_FILE "License.txt"
 # A text file used to describe the
 # project. Used, for example, the introduction screen of a
 # CPack-generated Windows installer to describe the project.
 # Corresponds to CPACK_PACKAGE_DESCRIPTION_FILE.
 DESCRIPTION_FILE "Description.txt"
 # Lists each of the executables along
 # with a text label, to be used to create Start Menu shortcuts on
 # Windows. For example, setting this to the list ccmake;CMake will
 # create a shortcut named "CMake" that will execute the installed
 # executable ccmake.
 # Corresponds to CPACK_PACKAGE_EXECUTABLES.
 PACKAGE_EXECUTABLES "paraview;ParaView"

) </source>

For other CPACK packaging variables, refer to CPack documentation. These can be set/changed in your application CMakeLists.txt file after calling build_paraview_client_cpack_config_init().

Once all any additional CPack variables are set, if any, you call build_paraview_client_cpack_config() which will generate actual CPack configuration file in the binary directory with the name CPack${PACKAGE_NAME}Config.cmake

This is the configuration file that must be specified as a command line argument (using --config) to cpack packaging executable to generate the package eg.

 cpack -G TGZ --config CPackParaViewConfig.cmake

What's in the package

A typical application structure is as follows:

 {prefix}
         \bin                                           :-- all binaries
         \lib\paraview-{versionMajor}.{versionMinor}    :-- paraview libraries + Qt libraries
         \lib\customapp-{versionMajor}.{versionMinor]   :-- custom app libraries

On Windows, there's no separate lib directory and all dlls are put in the same place as the bin, by default. A custom application can change where it's executables and libraries are installed by passing the INSTALL_LIB_DIR, INSTALL_BIN_DIR variables to the build_paraview_client macro. A custom application cannot change, however, where the paraview binaries or libraries get placed in the package.

A custom application however, can choose whether what components from ParaView it wants to install, if any at all. i.e. it is very easily possible to generate a package that does not include any ParaView components expecting the user to download the ParaView binaries separately. For that, change the CPACK_INSTALL_CMAKE_PROJECTS after calling the build_paraview_client_cpack_config_init() macro. The default value is:

 SET (CPACK_INSTALL_CMAKE_PROJECTS
   "${ParaView_BINARY_DIR}" "ParaView Runtime Libs" "Runtime" "/"
   "${ParaView_BINARY_DIR}" "VTK Runtime Libs" "RuntimeLibraries" "/"
   "${CMAKE_CURRENT_BINARY_DIR}" "${BCC_PACKAGE_NAME} Components" "BrandedRuntime" "/"
 )

If you don't want VTK or ParaView components to be installed, simply remove the corresponding items from the list (the top twp 4-tuples). You can add to this list of 4-tuples if you have additional components in your application that need to be packaged. Again, the user is strongly encouraged to look at CPack documentation for details on CPACK_INSTALL_CMAKE_PROJECTS and other cpack variables.


Special Case: Linux Packages and RPATH

On linuxes, when BUILD_SHARED_LIBS is ON, cmake by default turns on rpaths which make it easier to run the executables from the build directory. However when generating packages, users are encouraged to build the binaries with the advanced CMake variable CMAKE_SKIP_RPATH set to ON. You also may want to set BUILD_SHARED_LIBS to match ParaView's using:

 SET (BUILD_SHARED_LIBS ${PARAVIEW_BUILD_SHARED_LIBS}) # enable shared builds, if ParaView was built shared.

This will result in creation of two executables:

  • appname
  • appname-real

appname is a stub executable that sets up the environment so that it can locate the shared libraries correctly in the build directory as well on installation without the need for setting LD_LIBRARY_PATH.

As a rule of thumb, when generating package:

  • Build ParaView with BUILD_SHARED_LIBS: ON, VTK_USE_RPATH: OFF
  • Build you application with BUILD_SHARED_LIBS: ON, CMAKE_SKIP_RPATH: ON

Special Case: Mac DragNDrop App bundles

Refer to Applications/ParaView/CMakeLists.txt.

On Macs, ParaView enables creation of an app bundle with all required libraries packaged in a nice ParaView.app. For this, the CMakeList adds a new install component "Bundle" which runs a couple of scripts for generating the package. Then, it tells cpack to install only the Bundle component when generating DragNDrop bundle as follows:

 # This ensure that the only component that is used by cpack when building the
 # drag-n-drop on Mac is the "Bundle" component.
 SET (CPACK_INSTALL_CMAKE_PROJECTS_DRAGNDROP
   "${CMAKE_CURRENT_BINARY_DIR}" "ParaView Mac Bundle" "Bundle" "/")

Currently, we don't have a cleaner solution for custom applications to create app bundles. They can start with the scripts ParaView uses and then modify them to suit their application.

Acknowledgements

This effort has been funded by EDF and Sandia National Labs.



ParaView: [Welcome | Site Map]