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
 # 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.
 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.
 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>
 )

</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.

Acknowledgements

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



ParaView: [Welcome | Site Map]