SALOME Tutorial
SALOME Tutorial
USERS GUIDE
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
Page 1 of 82
Contents
1. Introduction ..................................................................................................................................3
1.1 How to use this tutorial ...........................................................................................................3
1.2 Pre-requisites..........................................................................................................................3
1.3 Cohesion of sample components ...........................................................................................3
2. SALOME build procedure ............................................................................................................5
2.1 General description of the build procedure ............................................................................5
2.2 Typical sources package organization ...................................................................................6
2.3 Customize build procedure .....................................................................................................6
2.4 Build the module .....................................................................................................................7
2.5 Running SALOME with enabled ATOMIC module .................................................................8
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
7. Attachments ................................................................................................................................81
Page 2 of 82
1. INTRODUCTION
This document represents a tutorial on SALOME platform. The tutorial provides an introduction to
the developing of an application based on SALOME platform. It does not cover all the aspects of
software application development; however it describes basic workflow which should be applied by
the developers in order to implement new SALOME component(s).
This tutorial is intended for the programmers with extended experience in C++ and/or Python
languages, but unfamiliar with SALOME platform. In addition, some knowledge of CORBA
technology, numerical computation cycle (pre-processing, processing, post-processing), and Qt
library is required.
P l a t f o r m
P l a t f o r m
S A L O M E
S A L O M E
1.2 PRE-REQUISITES
This tutorial is applicable to the SALOME platform version 7.3.0 and newer. For other prerd
requisites (3 -party products) please refer to the SALOME requirements.
Note: SALOME platform distribution under Linux (SALOME Install Wizard) includes pre-compiled
binaries of SALOME Tutorial modules, described in this document. If you choose installation of all
SALOME modules into single directory (this option is available in SALOME Install Wizard), you will
not be able to complete this tutorial as modules installed with SALOME will have more priority and,
thus, they will be used in run-time instead of manually built ones.
1.3 COHESION OF SAMPLE COMPONENTS
Since the main goal of the tutorial is to provide the guide for creation of a new component, it is
separated into 3 main parts:
ATOMIC: light-weight component.
ATOMGEN: Python component with CORBA engine.
ATOMSOLV: C++ component with engine
It is proposed to study the tutorial in this order (Light-weight, then Python, then C++ with engine)
because some topics are common and they will be explained only once in previous chapters. Every
chapter consists of explanations and the code in C++/Python that builds up a component.
The components developed with SALOME platform are mostly used for pre- and post-processing
applications. Usually the data is prepared by one component, then it is processed by various
solvers of other components that represent application of different physical algorithms to the data,
and the results of calculations might be displayed by yet another component! Such interaction
between components on the basis of data processing is known as coupling of the components.
This tutorial will simulate a simple model of components coupling; the following interaction between
the components will be developed during the tutorial:
Copyright 2001-2013. All rights reserved.
Page 3 of 82
The first component to be developed is a light-weight component named ATOMIC. Its main
goal is to prepare the data for the calculations. It allows the user to input the data using
dialog boxes and export the data to a file. In the ATOMIC component it is impossible to
visualize this data in any way, neither it is intended to perform any algorithmic processing
of the data. These tasks will be carried out by the other components. The data is a list of
records, each containing 3 floating point values (Cartesian coordinates), and a string value
(name). It represents molecules and atoms. An XML format is used for the exporting of the
data.
The third component is a C++ component with CORBA engine named ATOMSOLV. It
allows further processing of the data and displaying of the results. This component
introduces concepts of the viewer, view management, selection management, etc. It is
possible to open a 3D view window in this component and display the "atoms" and
"molecules". The "atoms" are represented by spheres in 3D space. ATOMSOLV also
performs further processing of data, this time not spatial, but let's say "thermal". After the
processing, different properties are assigned to the "atoms", that is reflected in their 3D
representation the atoms are differently colored. It will be also possible to change the
color and display mode of the "atoms" manually. An important issue is how the data
("atoms") is retrieved from the ATOMGEN component. It is done by means of CORBA
interface directly from the Python component's engine.
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
Page 4 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
CMake build system is used to define build rules for the application that allows developing of the
cross-platform build procedures. They allow hiding a lot of details of Makefile's from the developer.
With CMake tool, the build procedure of each SALOME module consists of the following steps:
Step
Description
Input
Output
cmake
CMakeLists.txt
files
CMakeCache.txt
*.cmake files
Makefile
other files
*.in files
make install
Makefile
Libraries,
executables, etc.
in the build
directory
Libraries,
executables,
scripts,
resources,
documentation,
etc. in the build
directory
Libraries,
executables, etc.
in the target
directory
The CMake tool based procedure uses set of input files which define the build rules. The CMake
build system is implemented in such a way to simplify as much as possible the defining of the build
rules. It represents a top-level layer above the GNU make utility, providing simplified rules for
such objects like libraries (static and shared), executables, python scripts, resources,
documentation files, etc. It also provides integration with other build tools, like compilers, linkers,
documentation generation utilities; supports different programming languages (C++, Fortran,
Python, etc.) and other. Also it is possible to define own build rules; this is done by creating custom
scripts following CMake syntax. The input for CMake build system is usually a set of
CMakeLists.txt and *.cmake files.
CMake input files are used to describe the build procedure, in particular:
Test platform;
Test system configuration;
Copyright 2001-2013. All rights reserved.
Page 5 of 82
Detect pre-requisites;
Specify targets (libraries, executables);
Generate build rules (for example, standard UNIX makefiles on Linux, MSVC solutions,
etc).
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
Unpack attached archive light-00.tar.gz with ATOMIC module initial source tree to your working
directory. It represents typical sources directory tree:
ATOMIC_SRC
ATOMIC_SRC/adm_local
ATOMIC_SRC/adm_local/cmake_files
ATOMIC_SRC/bin
ATOMIC_SRC/bin/VERSION.in
ATOMIC_SRC/resources
ATOMIC_SRC/resources/SalomeApp.xml
ATOMIC_SRC/resources/LightApp.xml
ATOMIC_SRC/resources/ATOMIC.png
ATOMIC_SRC/src
ATOMIC_SRC/src/ATOMICGUI
ATOMIC_SRC/src/ATOMICGUI/ATOMICGUI.h
ATOMIC_SRC/src/ATOMICGUI/ATOMICGUI.cxx
ATOMIC_SRC/src/ATOMICGUI/resources
ATOMIC_SRC/ATOMIC_version.h.in
ATOMIC_SRC/CMakeLists.txt
- root directory
- administrative directory
- CMake-related administrative files
- directory for scripts and other tools
- version file
- directory for common resources
- configuration file (full SALOME)
- configuration file (light SALOME)
- main module icon
- directory for source code
- GUI library source code directory
- GUI library header file
- GUI library implementation file
- GUI library custom resources
- Module header file template
- root CMake system file
Some secondary files are not listed. Typically, each sub-directory also contains own
CMakeLists.txt file.
In addition, other modules can provide more complex sources package structure including
additional directories, for example doc for the documentation sources, idl for CORBA interfaces
files, etc.
2.3 CUSTOMIZE BUILD PROCEDURE
Project's root directory provides main CMake configuration that allows build all targets into one set
of binaries and libraries. Each sub-directory also includes CMake configuration file
(CMakeLists.txt) that specifies targets being build.
The file CMakeLists.txt in root directory of the SALOME module provides basic build rules to be
used in other CMakeLists.txt files. It sets main properties of project: name, version, prerequisites, installation paths, programming languages being used by the project, tree of subdirectories, etc.
A lot of files used by the build procedure of each SALOME module are located in SALOME
KERNEL module (that is referenced by the KERNEL_ROOT_DIR environment variable), namely in
its salome_adm sub-folder. Similarly, the GUI_ROOT_DIR environment variable is used for the
graphical user interface (GUI) module of SALOME; this module also provides a set of configuration
utilities (*.cmake files) in its adm_local folder.
Some modules can need some external packages in order to compile and run properly. The usual
approach is to write a special *.cmake file for the purpose of finding a certain piece of software
and to set it's libraries, include files and definitions into appropriate variables so that they can be
used in the build process of another project. It is possible to include the standard CMake detection
Copyright 2001-2013. All rights reserved.
Page 6 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
Take a look at the attached ready-to-use FindGd.cmake file. It provides a typical example of
such procedure implementation. The CMake tool includes a large set of helper macrocommands which can be used in the check procedures. In our case, we check presence and
availability of the gd.h header file and libgd.so library using pre-defined CMake macrocommands FIND_PATH and FIND_LIBRARY. If everything goes well, we set the global
variable GD_FOUND to TRUE, otherwise print the warning message. In addition, we specify
GD_LIBRARIES and GD_INCLUDE_DIRS variables that can be then used in the
CMakeLists.txt files when its necessary to link against the gd library.
Add check procedure into main CMakeLists.txt file and print GD_FOUND variable value
in the summary section:
...
INCLUDE_DIRECTORIES(${GUI_INCLUDE_DIR} ${GD_INCLUDE_DIRS})
...
TARGET_LINK_LIBRARIES(mylibrary ${GUI_LIBRARIES} ${GD_LIBRARIES})
...
2.4 BUILD THE MODULE
We start developing the component with a stub that contains only basic directory structure,
CMakeLists.txt files, several resources files and two source files with minimal code required
for GUI library building. The archive file light-00.tar.gz with ATOMIC module initial source tree has
to be unpacked to the working directory. To build the module its necessary to perform the following
actions:
Check prerequisites: Linux operating system, C/C++ compiler, CMake tool availability, etc.
and
Set ATOMIC module environment (this is optional for build procedure, but helps giving
further instructions) - set paths to the ATOMIC module source, build and installation
directories, either using file atomic_env_build.sh (note: you have to edit it before
usage to set correct paths):
Page 7 of 82
P l a t f o r m
P l a t f o r m
Here, we suppose that a source directory and a build directory are on the same level in the
directories hierarchy. Create build directory ${ATOMIC_BUILD_DIR}, and cd to this
directory:
S A L O M E
S A L O M E
[%] cd ${ATOMIC_SRC_DIR}
[%] cd ..
Invoke make and make install commands to build and install ATOMIC module:
[%] make
[%] make install
2.5 RUNNING SALOME WITH ENABLED ATOMIC MODULE
To launch SALOME session with the ATOMIC module, its necessary to perform the following
steps:
Launch SALOME.
Page 8 of 82
To launch Salome with some modules available, for example GEOM and ATOMIC, use
runSalome command with --modules option:
[%] ${KERNEL_ROOT_DIR}/bin/salome/runSalome --modules=GEOM,ATOMIC
This command starts SALOME GUI session. The Modules toolbar will contain two items:
Geometry and Atomic.
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
Now you know how to build SALOME modules and run SALOME. Lets use this knowledge to
study ATOMIC: light-weight component.
Page 9 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
Here we have to say a few words about different modes of building and running of SALOME
applications. 2 core modules of SALOME platform - KERNEL and GUI - can be compiled and run
in 2 configurations (can be understood as modes): full and light.
Light configuration means that all CORBA-based services are disabled. To build the
modules in light configuration -DSALOME_LIGHT_ONLY=ON option must be passed to the
cmake command (see paragraph 2 for details). To run SALOME in the light configuration,
the command runLightSalome.csh (or runLightSalome.sh) from GUI module is
used.
A light-weight component can work with SALOME KERNEL and GUI modules built in both light and
full configurations. In other words, a light-weight component can be a part of a multi-component
SALOME application, and the other components do not have to be necessarily light-weight. But if
all components are light-weight (in particular, if there is only 1 light-weight component in an
application), then it is preferable to use KERNEL and GUI modules in light configuration. This will
increase the application performance since a number of unused CORBA-based services will not be
started.
Studying this chapter step by step, we shall create a simple light-weight component ATOMIC.
Beginning with the next section Instantiating a GUI module - we start the development of our
component. First we create a GUI module class, then create its internal data structure (Component
with data section), implement persistence of the data (Implementing persistence section), learn
how to display the data in Object Browser (Working with Object Browser section), set and retrieve
selected objects and display a popup menu (Selection management section). At last, we shall
implement several new functions wrapped into Operation objects.
Having completed this chapter of the tutorial, the sample light-weight component should look like
shown at the Figure 1.
Page 10 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
Page 11 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
Resource file(s) are parsed. Resource file(s) contain a lot of customizable information
which is used during application start up and run. The information contains instruction
which components should be activated, which CORBA servers started (in full
configuration), and many other important things. A general rule for resource files location
is: first files listed in LightAppConfig (light-weight configuration) or SalomeAppConfig
variable value (file names must be separated by ';' or ':' symbol) are parsed, then a userdependent resource file ~/.SalomeApprc.<version> is parsed. Commands
runSalome and runLightSalome extend this variable automatically using according
<module_name>_ROOT_DIR values.
Components to be activated in the current SALOME session are listed in resource files
(section "launch", parameter "modules"). Names of GUI modules library files (.so or
.dll) are either dynamically constructed at run time (on Linux the algorithm is:
"lib"+<component_name>+".so") or they are explicitly indicated in the resource file.
Below we shall examine the functions that are called on GUI module object when it is activated and
deactivated:
initialize(): Called when a component is activated for the first time. Function
initialize() is called only once for each GUI module object created. It is the best
place for code that creates actions, menu items, tool buttons, operations, popup menu
items, etc.
Page 12 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
#include <LightApp_Module.h>
/*!
* Class
: ATOMICGUI
* Description : GUI module class for ATOMIC component
*/
class ATOMICGUI: public LightApp_Module
{
Q_OBJECT
enum { agCreateConf, agAddAtom };
public:
ATOMICGUI();
virtual void initialize ( CAM_Application* );
public slots:
virtual bool activateModule
( SUIT_Study* );
virtual bool deactivateModule ( SUIT_Study* );
private slots:
void
onOperation();
};
#endif // ATOMICGUI_H
using namespace std;
#include "ATOMICGUI.h"
#include <LightApp_Application.h>
#include <SUIT_ResourceMgr.h>
#include <SUIT_Session.h>
#include <SUIT_Desktop.h>
/*! Constructor */
ATOMICGUI::ATOMICGUI()
: LightApp_Module( "ATOMICGUI" )
{
}
/*! Initialization function.
Called only once on first activation of GUI module.
*/
void ATOMICGUI::initialize ( CAM_Application* app )
{
LightApp_Module::initialize( app ); // call parent implementation
Copyright 2001-2013. All rights reserved.
Page 13 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
Page 14 of 82
ATOMICGUI_EXPORT __declspec(dllexport)
// WNT
ATOMICGUI_EXPORT
// WNT
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
Please, build the application and run it. Now we can see that the Desktop has Atomic menu with
sub items and a new tool bar:
Textual
resources
[ATOMIC_msg_en.qm]
file
name:
<Resource_name>_msg_<language>.qm
For more information on using qm files and internationalization support by Qt toolkit, please, refer to
Qt API reference on Qt web site: QTranslator class.
At this point we have built a GUI module for a light-weight component with a set of commands
accessible through menus and tool buttons. The goal of our component is to prepare data for its
further processing. In the next section we shall develop an internal data model for our component
and implement its persistence.
Page 15 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
The data itself can be organized in any way a programmer desires. In our simple case we will use
a list of values, more complicated data structures can be implemented using complex tools for data
storage and retrieval (as CORBA-based SALOMEDS library or even SQL servers), but the common
gateway for accessing the data will be Data Model.
Data Model represents arbitrary internal data in a tree-like structure. It is done through root()
method of CAM_DataModel class. It returns object of the highest level of a component, usually
this object represents the component itself. Here we have to say a few words about what kind of
object is returned by Data Model. This object is a called a Data Object. Its primary mission is to
provide a common view of arbitrary data. It is a proxy-object - it hides the real implementation of
data and provides a generic interface to accessing it by other objects. For example, Object
Browser "knows" how to display Data Objects, Selection Manager "knows" how to select Data
Objects, and only Data Object itself "knows" which real piece of data was accessed (displayed,
selected, etc.) through it.
OK, let's return to our ATOMIC component and develop a data structure for it. First of all, we have
to develop a data itself. In our simple case, we shall use a simple list of values to represent a set
of molecules. Each value - is an object of a class that we are going to develop. Let's assume that
we have done it :), and here it is:
class ATOMICGUI_AtomicMolecule
{
private:
class Atom
{
public:
Atom();
Atom(const QString& name, const double x, const double y, const
double z);
QString name() const { return myName; }
double x()
const { return myX;
}
double y()
const { return myY;
}
double z()
const { return myZ;
}
int
id()
private:
QString
double
double
double
int
static int
myName;
myX;
myY;
myZ;
myId;
myMaxId;
Page 16 of 82
id
name
count
int
QString
double
double
double
atomId
atomName
atomX
atomY
atomZ
void
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
private:
QString
QList<Atom>
int
static int
};
const
const
const
const
const
int
int
int
int
int
index
index
index
index
index
)
)
)
)
)
const;
const;
const;
const;
const;
myName;
myAtoms;
myId;
myMaxId;
This class represents one molecule that consists of several atoms. The data structure of ATOMIC
component consists of a set of molecules. How do we do it and where do we store this set? As we
have described above, the best place for it would be a Data Model. We develop
ATOMICGUI_DataModel class, and its header file will look like this:
class ATOMICGUI_DataModel : public LightApp_DataModel
{
Q_OBJECT
public:
ATOMICGUI_DataModel ( CAM_Module* );
virtual ~ATOMICGUI_DataModel();
bool
bool
createMolecule ();
addAtom (const QString& moleculeID, const QString& atomName,
const double x, const double y, const double z);
private:
QList<ATOMICGUI_AtomicMolecule> myMolecules;
};
The private member field myMolecules is basically all data structure of our component.
createMolecule() and addAtom() methods allow to add new objects to the data structure.
Please, take the source file of the current state of ATOMIC component, study it carefully and build
it. We have implemented a virtual function of GUI module class which creates Data Model. This
function will be called automatically when new study is created. Also Data Model will receive a
number of callbacks for saving and restoring of it data - we shall implement them later. We also
added functionality to onOperation() slot of ATOMICGUI class, so that pressing "Create
molecule" tool button (or selecting a menu item) creates a real object in our data structure. In the
future we shall replace this code (we shall use Operation object). Also it is not currently possible to
add Atoms to molecules since we do not know how to identify a molecule to be used (wait until we
learn Selection management). "Everything is good in its season"!
Now we are going to develop a Data Object for our model. As it was mentioned above, Data
Object plays a role of proxy - it hides the real implementation of data and provides a generic
interface for accessing it by other objects. For ATOMIC component we shall develop one Data
Object class to represent both Molecule and Atom objects. Another successor of Data Object that
we will develop represents a root object - parent of all our Molecules and Atoms.
Page 17 of 82
name()
const;
icon(const int = NameId)
const;
toolTip(const int = NameId) const;
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
bool
bool
isMolecule() const;
isAtom() const;
private:
ATOMICGUI_AtomicMolecule*
int
};
myMolecule;
myIndex;
Page 18 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
How the real data (classes declared in ATOMICGUI_Data.h file) is "wrapped" with
Data Object interface.
How a certain molecule or atom is accessed (modified) using its Data Object.
As it is still not possible to create atoms of a certain molecule (we simply do not know yet how to
select a certain molecule), we have added a temporary code that creates 3 atoms for any new
molecule in createMolecule() method.
Calling root()->dump() in ATOMICGUI_DataModel::build() method displays in the
terminal the current structure of Data Objects. After we learn how to display the objects in Object
Browser, there will be no need in this dump().
In the next section we shall implement persistence of our data.
3.3 IMPLEMENTING PERSISTENCE
Data Model has a number of virtual functions that are called by Study when it is being saved to a
file or restored from a file. The functions are: save(), saveAs(), open(). The algorithm of
saving of a Study is the following: Study iterates active components, retrieves their Data Models
and calls save() or saveAs() functions. In save() and saveAs() a Data Model saves its
data to a temporary file(s) in arbitrary format and returns (in out-parameter) a list of file names
which contain the saved data. The first file name must be a name of a directory, the next file
names (any number of them) - names of files in this directory. If we go deeper into persistence
implementation details, we'll see that contents of these files will be serialized into a binary stream,
this stream will be saved into one single file (or multiple files, depends on settings), and afterwards
Copyright 2001-2013. All rights reserved.
Page 19 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
Page 20 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
}
bool ATOMICGUI_DataModel::saveAs ( const QString& URL,
CAM_Study* study, QStringList& listOfFiles )
{
myStudyURL = URL;
return save( listOfFiles );
}
The member field myStudyURL is used to store the file name of last used persistent file.
In save() we create an XML file, export our data into this file, and return in the out parameter the
directory and the file name.
In the open() function we construct the file name using the first (directory name) and the second
(file name) members of the input list, and call importFile() to restore the data structure from
this file.
All we have to do now - is implement importFile() and exportFile() functions. This is
already done in the sources of the current state of ATOMIC component. Please, save the source
files and build the component. Now it is possible to save the study and restore it. We can see in the
terminal that dump() after opening of a study outputs the same, previously saved data structure.
At this point we finish to develop the ATOMICGUI_DataModel class - now has all functionality that
is supposed to have.
In the next section we learn how to use Object Browser for displaying the data structure, so we will
not have to use dump() any more.
3.4 WORKING WITH OBJECT BROWSER
Object Browser is a view window based on QTreeView class, which can display the data structure
of components based on Data Objects. Object Browser is shared among all components; it is
created for every new (opened) Study. Components display their "portions" of data in Object
Browser under their "root objects". On the Figure 3 below we can see 2 root objects of 2
components: Geometry component and Mesh component.
Page 21 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
name()
const;
icon()
const;
toolTip() const;
OK, now we come to the first question: how does a component opens and accesses the Object
Browser?
To make the Object Browser available for a component, a component's GUI module class
must redefine virtual method windows() and add a "flag" of Object Browser to outparameter map. That's all!
Page 22 of 82
Now Object Browser window is opened every time a component is activated. If user closes
Object Browser window, then it can be opened again using menu View Windows
Object Browser.
A
component
accesses
Object
Browser
through
LightApp_Application::objectBrowser(). So from a GUI
getApp()->objectBrowser().
methods
of
module class:
After the Object Browser is active, it automatically scans Data Models of active components, takes
their root Data Objects (using method "root()"), and displays them in the list view.
Please, take the source files of ATOMIC component with available Object Browser. The data
structure is shown in Object Browser. In the next section we are going to learn how to retrieve
selected objects from Object Browser and how to install different popup menus on different objects.
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
When selection event is emitted in the list view of Object Browser, the global Selection
Manager receives this event and requests the selector of Object Browser to return the
selected objects. Selector of Object Browser scans its Data Objects and creates a Data
Owner for every selected Data Object. Entry of a new Data Owner is set equal to the entry
of the corresponding Data Object.
Page 23 of 82
OK, let's implement a method in ATOMICGUI class, which will return a list of strings - entries of the
selected objects.
void ATOMICGUI::selected( QStringList& entries, const bool multiple )
{
LightApp_SelectionMgr* mgr = getApp()->selectionMgr();
if( !mgr )
return;
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
SUIT_DataOwnerPtrList anOwnersList;
mgr->selected( anOwnersList );
for ( int i = 0; i < anOwnersList.size(); i++ )
{
const LightApp_DataOwner* owner =
dynamic_cast<const LightApp_DataOwner*>
( anOwnersList[ i ].get() );
QStringList es = owner->entry().split( "_" );
if ( es.count() > 1 && es[ 0 ] == "ATOMICGUI" &&
es[ 1 ] != "root" )
{
entries.append( owner->entry() );
if( !multiple )
break;
}
}
}
In this method we receive from the Selection Manager the list Data Owners, iterate them and then
select only "our" entries. We also exclude "ATOMICGUI_root" entry as it is the root entry of the
Data Model and does not correspond to any molecule or atom.
Now we shall implement creation of atoms of a certain molecule. We remove the code which
creates 3 atoms of any new molecule from Data Model class and add the following code to
onOperation() slot of ATOMICGUI class:
if ( id == agAddAtom ) {
QStringList entries;
selected( entries, false );
ATOMICGUI_AddAtomDlg dlg ( getApp()->desktop() );
int res = dlg.exec();
ATOMICGUI_DataModel* dm = dynamic_cast<ATOMICGUI_DataModel*>
( dataModel() );
if( dm && res == QDialog::Accepted && dlg.acceptData( entries ) ) {
QString name;
double x, y, z;
dlg.data( name, x, y, z );
dm->addAtom( entries.first(), name, x, y, z );
getApp()->updateObjectBrowser();
}
}
ATOMICGUI_AddAtomDlg is a very simple dialog that allows user to input a name of a new atom
and 3 coordinates: X, Y, and Z. Please, download the source files of the current version of ATOMIC
with selection management, and enjoy atoms creation under selected molecules!
Another aspect of selection management which we are going to study in this section is
management of popup menus. Popup menu is shown when user clicks a right mouse button in any
view window including Object Browser. This view window in terms of popup management is called
"a client window". If there are selected objects, a popup menu should contain commands that use
Page 24 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
Page 25 of 82
rule );
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
A construct "$type in {'Molecule' 'Atom'}" means "value of 'type' property must be equal
to one of the following: 'Molecule' or 'Atom'". Another rule could look like this: "'Molecule' in
$type", and that would mean "there should be at least one object, for which the value of a property
'type' would be equal to 'Molecule'". The rules can be combined with "and" and "or" operators.
The function setRule() of the QtxPopupMgr class has 2 significant parameters (as we see in
the code above).
Lets return to LightApp_Selection class. We must create a successor of this class and
redefine its virtual methods init(), count(), and param() in order to compute the value of
parameter "type" of the logical rules. The other parameters "client" and "selcount" are
computed by LightApp_Selection class, and we can simply use them in our logical rules.
The init() method is called when a "request popup" event comes (user clicks a right mouse
button). In this method the class must initialize its internal fields (lists, maps, etc. for calculation of
parameters in parameter() method. parameter() returns a value of a certain parameter of an
certain object (object index is given). And the last method count() returns the number of
objects. Let's take a look at implementation of these methods in ATOMICGUI_Selection class:
void ATOMICGUI_Selection::init( const QString& client,
LightApp_SelectionMgr* mgr )
{
if ( mgr ) {
SUIT_DataOwnerPtrList sel;
mgr->selected( sel);
SUIT_DataOwnerPtrList::const_iterator anIt = sel.begin(),
aLast = sel.end();
for ( ; anIt != aLast; anIt++ ) {
QString type = "Unknown";
SUIT_DataOwner* owner = (SUIT_DataOwner*)( (*anIt).get() );
LightApp_DataOwner* sowner =
dynamic_cast<LightApp_DataOwner*>( owner );
QStringList es = sowner->entry().split( "_" );
if ( es.count() > 0 && es[ 0 ] == "ATOMICGUI" ) {
if ( es.count() > 1 ) {
if( es[ 1 ] == "root" )
type = "Root";
else
type = "Molecule";
if ( es.count() > 2 )
type = "Atom";
}
}
myTypes.append( type );
}
}
Copyright 2001-2013. All rights reserved.
Page 26 of 82
P l a t f o r m
P l a t f o r m
The ATOMICGUI_Selection class has a member field myTypes of QStringList type. It stores
the types of selected objects. How the types are "calculated" using the entries of the objects - is
shown in init() method. Method param() is very simple - it returns type of an object stored in
myTypes.
Now we must compile all these changes together. Please, download the version of ATOMIC source
file with advanced popup management.
At this point of our tutorial we have studied almost all topics related to development of a lightweight component. In the next and last section of light-component chapter we improve the way in
which we implemented onOperation() slot of ATOMICGUI class. A new conceptual object of
SALOME platform will be introduced and studied - the Operation. We also implement several new
functionalities using Operations - import and export of data, renaming and removal of molecules
and atoms.
S A L O M E
S A L O M E
3.6 OPERATIONS
Operation is a manager of an action inside a component GUI. By "action" we understand any
functionality a GUI module of a component provides to a user. Examples of actions may be the
following: creation of a sphere in Geometry component, Atom creation in ATOMIC component,
mesh calculation in Mesh component.
Using an Operation for action management gives the following advantages:
An Operation instance can control which other Operations can be executed simultaneously
with this Operation. It is implemented using method
bool isValid(SUIT_Operation* theOtherOperation) const.
Before starting a new operation (operation_A), an application calls isValid() method of
the operation being executed (operation_B) passing it operation_A as a parameter. If
operation_B returns false, then operation_A is not started, it must wait untill operation_B
finishes its execution. This mechanism can be overridden, though, with yet another virtual
method of SUIT_Operation class:
bool SUIT_Operation::isGranted() const.
If this method returns true, then the operation is started any way, ignoring isValid()
return value.
Page 27 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
bool isReadyToStart() const returns true if all initialization steps are done (location of
resources, creation of dialog, etc.) and the operation is
ready to be started.
void stopOperation()
void startOperation()
void abortOperation()
void commitOperation()
void resumeOperation()
void suspendOperation()
bool hasTransaction() const returns true if the operation uses any transaction
mechanism (for keeping track of undo/redo or other
means).
bool abortTransaction()
aborts transaction
bool openTransaction()
bool commitTransaction(
const QString& =
QString::null )
LightApp_Operation class adds access to GUI module instance, Desktop, Selection manager
from within the Operation, and adds 2 virtual methods for working with a dialog window:
LightApp_Dialog* dlg() const;
void setDialogActive( const bool );
If an Operation works with a dialog window, then its method dlg() should return it. It will be
automatically shown in start(), and hidden in abort() or commit().
OK, now we are going to develop Operations for the ATOMIC component. Custom Operation
objects must be created in the virtual method of the GUI module class :
LightApp_Operation* createOperation( const int operationID ) const;
Page 28 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
Let's create Operations for Atom and Molecule creation. First of all we create 2 classes implementations
of
our
custom
Operations:
ATOMICGUI_CreateMolOp
and
ATOMICGUI_AddAtomOp. We shall also create a special base class for Operations of ATOMIC
component. Implementations of these classes will be the following (we already have the
corresponding functionality in onOperation() slot of ATOMICGUI class, here we moved it to the
new Operation classes):
ATOMICGUI_CreateMolOp:
ATOMICGUI_CreateMolOp::ATOMICGUI_CreateMolOp()
: ATOMICGUI_Operation()
{
}
ATOMICGUI_CreateMolOp::~ATOMICGUI_CreateMolOp()
{
}
void ATOMICGUI_CreateMolOp::startOperation()
{
if( dataModel() && dataModel()->createMolecule() )
commit();
else
abort();
}
ATOMICGUI_AddAtomOp:
ATOMICGUI_AddAtomOp::ATOMICGUI_AddAtomOp()
: ATOMICGUI_Operation(),
myDlg( 0 )
{
}
ATOMICGUI_AddAtomOp::~ATOMICGUI_AddAtomOp()
{
if ( myDlg )
delete myDlg;
}
LightApp_Dialog* ATOMICGUI_AddAtomOp::dlg() const
{
if ( !myDlg )
const_cast<ATOMICGUI_AddAtomOp*>( this )->myDlg =
new ATOMICGUI_AddAtomDlg( module()->getApp()->desktop() );
return myDlg;
}
void ATOMICGUI_AddAtomOp::onApply()
{
QStringList entries;
atomModule()->selected( entries, false );
ATOMICGUI_AddAtomDlg* d =
dynamic_cast<ATOMICGUI_AddAtomDlg*>( dlg() );
if( dataModel() && d && d->acceptData( entries ) )
{
QString name;
double x, y, z;
d->data( name, x, y, z );
dataModel()->addAtom( entries.first(), name, x, y, z );
module()->getApp()->updateObjectBrowser();
}
}
And the following modifications must be done to ATOMICGUI class:
void ATOMICGUI::onOperation()
{
Copyright 2001-2013. All rights reserved.
Page 29 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
}
The onOperation() slot of ATOMICGUI class simply calls startOperation() method passing
the Operation identifier. The Operation objects are stored on the base level of GUI module (in
LightApp_Module class) and starting the Operation multiple times does not create multiple
Operation objects (can be understood as caching). But if the Operation is started for the first time, it
is created by createOperation() method, which we have redefined in ATOMICGUI class in
order to create custom Operations of our component.
The modifications are reflected in this version of ATOMIC source files. It also became possible to
improve "Add atom" operation: ATOMICGUI_AddAtomDlg has 3 buttons now: Apply, Ok, and
Close, so it is possible to create several atoms during one single operation pressing Apply button.
Please, notice, that signals of the Operation dialog (ATOMICGUI_AddAtomDlg for example) are
connected to virtual slots of base Operation class ATOMICGUI_Operation. Child Operations
need to redefine these virtual functions to add customized processing.
ATOMIC component is almost finished. Finally, we are going to implement several other Operators
for the following actions: import of data, export of data, renaming of atoms and molecules, and
removal. The core functionality for these actions already exists in Data Model. All we have to do is
create 4 new Operator classes, and add their creation to createOperation() method.
Let's take a look at new Operation for import and export (we shall combine these 2 actions into 1
Operator for simplicity):
ATOMICGUI_ImportExportOp::ATOMICGUI_ImportExportOp( const bool import
)
: ATOMICGUI_Operation(),
myIsImport( import )
{
}
ATOMICGUI_ImportExportOp::~ATOMICGUI_ImportExportOp()
{
}
void ATOMICGUI_ImportExportOp::startOperation()
{
ATOMICGUI_DataModel* dm = dataModel();
if ( !dm )
{
abort();
return;
}
QStringList filtersList;
Copyright 2001-2013. All rights reserved.
Page 30 of 82
P l a t f o r m
P l a t f o r m
S A L O M E
S A L O M E
Page 31 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
To implement the dump python mechanism in the SALOME component with CORBA
engine it is necessary implement virtual function DumpPython() of the module engine, a
successor of the Engines::EngineComponent CORBA interface. The signature of this
method is:
Take into account that component engine should be also inherited from the
SALOMEDS::Driver interface and provide (at least empty) implementation of persistence
methods. This requirement is stipulated by the architectural features of the SALOME data
server.
3.7.1 Different approaches of the dump python mechanism implementation
Generally there are two main approaches for implementation of the dump python mechanism:
1. Each method of the custom SALOME component that publishes any data in the SALOME
Study, also records some additional information to the component. This information can be
later used for generation of the Python script, more precisely the part of the script that
concerns the component. In the simple case it can be a string representation of the Python
command with all required parameters and returning value(s) that reproduces the
components function being invoked. This information can be stored in arbitrary way, for
example directly in the components data model. Other approach is to use SALOMEDS
attributes to store Python commands directly in SALOME study. Take into account that
Dump python data should be persistent, i.e. it should be stored/retrieved during the study
saving/loading. The described approach is called historical dump.
Advantages:
The dump python functionality in most cases can be trivially implemented.
Disadvantages:
Main disadvantage of the historical dump is a problem of backward compatibility. Since
the Python command is generated and stored directly at the moment of the function
invocation, the maintenance of the studies (for example, in case of significant changing
component API) becomes complicated task.
2. Dump python method analyzes current content of the SALOME Study, namely the part
related to the component, and generates a Python script basing on the information
retrieved from the study and the corresponding data stored in the component data model.
This approach is called snapshot dump since it allows generation of minimal and
sufficient script that reproduces the current content of the study, avoiding generation any
intermediate commands (like data edition or removal commands). Usually this approach
Copyright 2001-2013. All rights reserved.
Page 32 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
Thus, taking into account advantages and disadvantages of both approaches it is recommended to
use the historical approach when creating a new component, since all required functionality can
be initially included to the data model being implemented. The second can be applied when adding
support of dump python mechanism to the existent component.
In fact, its possible to mix both approached when the most complicated objects in the custom
component store the additional information thus simplifying the implementation of the dump python
method and less complex objects are written in a Python script basing on analysis of their content,
that allows minimizing of the modifications in the existing code.
3.7.2 Adding snapshot dump in ATOMIC module
The simplicity of the data model of the ATOMIC component allows applying snapshot dump
approach to it.
First step of the dump python mechanism implementation in our component is creation of the
Python interface, since currently ATOMIC component has no way to create and publish objects
using Python interpreter. Firstly, lets change return type of the createMolecule() method of the
ATOMICGUI_DataModel class from bool to QString; now this method will return study entry
(unique identifier) of the created molecule. Also, we will remove temporary debug code that
automatically added three atoms to the just created molecule in createMolecule() function:
QString ATOMICGUI_DataModel::createMolecule()
{
// add new molecule
ATOMICGUI_AtomicMolecule mol;
myMolecules.append( mol );
// obtain its id (entry)
QString id = QString( "ATOMICGUI_%1" ).arg( mol.id() );
// update object browser
update();
// return entry of the created molecule
return id;
}
The entry returned by the createMolecule() function will be required later in Python module in
order to access the corresponding C++ data object.
Lets adds new AtomicPy python module in our component. For wrapping C++ classes into
Python we will use sip third-party open-source software (by Riverbank Computing Ltd). This seems
to be natural choice, since sip is one of the SALOME pre-requisites; it provides a simple way to
generate Python wrapping for C++ code, especially Qt-based one. AtomicPy library will contain
AtomicMolecule class:
class TCreateMoleculeEvent: public SALOME_Event
{
public:
QString myName;
typedef QString TResult;
TResult myResult;
Copyright 2001-2013. All rights reserved.
Page 33 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
LightApp_Module* module =
dynamic_cast<LightApp_Module*>( app->module("Atomic") );
if(!module)
return;
ATOMICGUI_DataModel* model =
dynamic_cast<ATOMICGUI_DataModel*>(module->dataModel());
if(!model)
return;
myResult = model->createMolecule();
model->renameObj(myResult,myName);
}
};
AtomicMolecule::AtomicMolecule( const QString& name )
{
myId = ProcessEvent( new TCreateMoleculeEvent( name ) );
}
Above code creates a molecule and publishes it in the study. The constructor
AtomicMolecule(const QString& name) performs in such a way the same action as Create
molecule function from GUI interface .
Below code adds new atom to the molecule, like the command Add atom from the GUI interface:
class TAddAtomEvent: public SALOME_Event
{
public:
QString myId;
QString myName;
double myX, myY, myZ;
TAddAtomEvent(const QString& id,
const QString& name,
const double x,
const double y,
const double z) : myId(id), myName(name), myX(x), myY(y), myZ(z)
{}
virtual void Execute()
{
if ( !SUIT_Session::session() )
return;
LightApp_Application* app =
dynamic_cast<LightApp_Application*>(
SUIT_Session::session()->activeApplication() );
if(!app)
return;
LightApp_Module* module =
Copyright 2001-2013. All rights reserved.
Page 34 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
};
void AtomicMolecule::addAtom( const QString& atom,
const double x,
const double y,
const double z ){
ProcessVoidEvent( new TAddAtomEvent( myId, atom, x, y, x ) );
}
Note, that all the functions modifying the contents of the study are wrapped by the events using
SALOME events mechanism. This is important since all the Python commands executed in the
SALOME embedded Python interpreter are serialized in order to avoid concurrent access to the
Python interpreter from different threads that might lead to the application crashes.
To wrap our AtomicMolecule class into Python module we should describe it in the SIP
specification file:
%Module AtomicPy
%Import QtGuimod.sip
%ExportedHeaderCode
#include <AtomicPy.h>
%End
class AtomicMolecule /NoDefaultCtors/
{
public:
AtomicMolecule( const QString& name ) /Transfer/;
void addAtom( const QString& atom, const double x,
const double y, const double z );
private:
AtomicMolecule(AtomicMolecule&);
};
Page 35 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
Please, use this link to download the latest source files of the fully functional version of ATOMIC
component with implemented dump python mechanism.
This is the end of the chapter dedicated to development of a light-weight component. If you are
ready to continue with our tutorial and learn about other types of components - please, proceed to
the next chapter - ATOMGEN: Python component.
Page 36 of 82
Throughout this chapter we shall develop a Python component named ATOMGEN, with CORBA
engine and graphic user interface. This component is able to perform pseudo-algorithmic
processing of the data prepared by ATOMIC (light-weight C++ component developed in the first
chapter of the tutorial). The data processing in ATOMGEN is in some sense a "spatial analysis" of
molecules and atoms: for every molecule it will create 5 new molecules, atoms of new molecules
will have different coordinates (translated by a constant distance along X, Y, and Z axes).
ATOMGEN is also able to read XML file with data prepared by ATOMIC component and export its
data to an XML file (just like ATOMIC).
Having completed this chapter, the ATOMGEN component should look like shown on the picture
below:
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
A very important issue of this chapter is a concept of CORBA Engine. Engine is a part of a
component, built upon CORBA technology, which usually performs algorithmic data processing.
Engine can be understood as a stand-alone piece of software within a component, whose services
may be used by a component it belongs to as well as by other external components.
Page 37 of 82
In our sample ATOMGEN Python component we want to perform analytical processing of data
prepared by previously developed ATOMIC component, and provide the next following component
- C++ component - with the results of processing. It means that we will use 2 advantages of using
an Engine in our application:
Interaction with another component is more convenient and transparent (passing results of
processing to the next component is done by means of intra-CORBA communication
communication between Engines of components).
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
void ping()
long getStudyId()
void destroy()
Container GetContainerRef()
FieldsDict getProperties()
The following methods are used by Supervisor component for managing a component remotely
(starting a certain method of an engine, killing, suspending, etc.)
boolean Kill_impl()
boolean Stop_impl()
boolean Suspend_impl()
Page 38 of 82
long CpuUsed_impl()
Another interface - Container (declared in the same file SALOME_Component.idl) is used for
instanciating of components (to be more precise - engines of components). Main methods of
Container interface are:
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
boolean load_component_Library(in
string componentName)
So, basically, an Engine is a CORBA object and a Container is its manager-object which creates it,
publishes in the naming service, and destroys it.
In the next section we are going to write an IDL file for ATOMGEN engine and a Python file with
implementation of the methods declared in the IDL file.
4.2 ENGINE: INTERFACE AND IMPLEMENTATION
Development of any CORBA object (and our Engine is a CORBA object) is accomplished in 2
steps:
Development of the implementation of methods declared in the IDL file using one of the
following programming languages: C, C++, Java, Smalltalk, COBOL, Ada, Lisp, PL/1,
Python. In SALOME applications we use Python and C++.
Page 39 of 82
Deep study of IDL language is beyond the scope of this tutorial. If you are unfamiliar with IDL,
please, read the corresponding literature. These links may help:
Mastering the Interface Definition Language (IDL) from Teach Yourself CORBA In 14 Days
tutorial
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
OK, lets start coding. In IDL file of ATOMGEN component we would like to declare the following
CORBA interfaces and methods:
interface Atom {
string getName();
double getX();
double getY();
double getZ();
};
interface Molecule {
string getName();
long getNbAtoms();
Atom getAtom(in long theIndex);
};
typedef sequence<Molecule>
MoleculeList;
interface ATOMGEN_Gen :
Engines::EngineComponent {
void setCurrentStudy (in
SALOMEDS::Study);
boolean importXmlFile (in string
theFileName);
boolean exportXmlFile (in string
theFileName);
boolean processData (in
MoleculeList theData);
MoleculeList getData (in long
studyID);
};
Let's assume we have written the methods above in IDL file ATOMGEN.idl. IDL files are placed in
the idl subdirectory, located under main source directory of the component. Please, download the
first version of ATOMGEN component with ATOMGEN.idl file in idl subdirectory.
Now we have to create a Python module with implementation of the declared IDL interfaces. We
shall create an src subdirectory (under main source directory) and add a package called
ATOMGEN. Then we are going to create 3 files with code in Python:
ATOMGEN.py
Implementation
ATOMGEN.idl
of
ATOMGEN_Gen
interface
declared
in
Page 40 of 82
ATOMGEN_XmlParser.xml
Let's take a look at the code of ATOMGEN Python module (from next version of ATOMGEN
component):
General initialization
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
Page 41 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
Page 42 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
SObject
GenericAttribute
StudyManager
StudyBuilder
Page 43 of 82
Driver
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
Page 44 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
Page 45 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
Method Load() of ATOMGEN class performs the opposite task: it iterates the SALOMEDS data
structure and creates ATOMGEN_ORB.Molecule and ATOMGEN_ORB.Atom objects for SObjects
of the corresponding level (children of ATOMGEN SComponent correspond to molecules,
grandchildren - to atoms). Please, pay attention at using special iterator classes (interfaces
declared in SALOMEDS.idl file) for traversing the SALOMEDS data structure.
def Load( self, component, stream, URL, isMultiFile ):
global __entry2IOR__
__entry2IOR__.clear()
import StringIO, pickle
study = component.GetStudy()
iter = study.NewChildIterator(component)
data = []
while iter.More():
sobject = iter.Value()
iter.Next()
found, attr = sobject.FindAttribute("AttributeName")
if not found: continue
from ATOMGEN_Data import Molecule, Atom
mol = Molecule(attr.Value())
__entry2IOR__[sobject.GetID()] = ObjectToString(mol._this())
iter1 = study.NewChildIterator(sobject)
while iter1.More():
sobject1 = iter1.Value()
iter1.Next()
found, attr = sobject1.FindAttribute("AttributeName")
if not found: continue
name = attr.Value()
x = y = z = None
iter2 = study.NewChildIterator(sobject1)
while iter2.More():
sobject2 = iter2.Value()
iter2.Next()
found, attr1 =
sobject2.FindAttribute("AttributeName")
if not found: continue
found, attr2 =
sobject2.FindAttribute("AttributeReal")
if not found: continue
if attr1.Value() == "x": x = attr2.Value()
if attr1.Value() == "y": y = attr2.Value()
if attr1.Value() == "z": z = attr2.Value()
if None not in [x, y, z]:
atom = Atom(name, x, y, z)
mol.addAtom(atom)
__entry2IOR__[sobject1.GetID()] =
ObjectToString(atom._this())
pass
data.append(mol)
self.studyData[ study._get_StudyId() ] = data
return 1
At this point we finish to study SALOMEDS and its use in ATOMGEN component. Please,
download the source files of the current version of ATOMGEN with advanced data structure.
In the next chapter we create a graphic user interface for ATOMGEN component to finalize its
development. Please, continue with GUI for Python component, if you are ready!
Page 46 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
In the previous chapter about light-weight component, in the section "Instantiating a GUI module",
we have pointed that a GUI library file name can be also specified in the resource file
(SalomeApp.xml or LightApp.xml file). For C++ components the usual practice is not to
indicate the name of GUI library file explicitly. The default algorithm of library file name
construction is "lib" + component_name + ".so", and usually GUI libraries of C++ components have
exactly the same file name, so there is no need to indicate them explicitly. But for a Python
component it is not the case, and we must add the following lines to the resource file
SalomeApp.xml (Python components are loaded only in full configuration of SALOME, it is a
current limitation, so we examine only SalomeApp.xml file and omit LightApp.xml):
<section name="ATOMGEN" >
<parameter name="name"
value="AtomGen" />
<parameter name="icon"
value="ATOMGEN.png" />
<parameter name="library" value="SalomePyQtGUI" />
</section>
After we have indicated a parameter "library" in resource section of ATOMGEN component,
the GUI library file to be loaded on the first activation of ATOMGEN component will be
libSalomePyQtGUI.so.
libSalomePyQtGUI.so
is
a
library
built
in
SALOME_PYQT/SALOME_PYQT_GUI package of GUI module of SALOME platform. It contains
SALOME_PYQT_Module class - a successor of SalomeApp_Module and a utility class
SALOME_PYQT_PyInterp. SALOME_PYQT_Module implements all main virtual methods of GUI
modules that allow for customization of GUI of a certain component:
void
bool
bool
void
void
void
initialize();
activateModule();
deactivateModule();
windows() const;
viewManagers() const;
contextMenuPopup(), etc.
Page 47 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
),
myObj
( _obj
) {}
protected:
virtual void execute()
{
myObj->deactivate( myStudy );
}
private:
SUIT_Study*
myStudy;
SALOME_PYQT_Module* myObj;
};
// Posting the request
PyInterp_Dispatcher::Get()->Exec( new DeactivateReq( myInterp,
theStudy, this ) );
return SalomeApp_Module::deactivateModule( theStudy );
}
First of all, the method does all usual deactivation things: disconnect the signals and hide menus
and tool buttons. Then it creates an object of PyInterp_LockRequest type and passes it to
PyInterp_Dispatcher. This mechanism is used in order to synchronize the calls to Python
interpreter. User actions are asynchronous by their nature (user can start an action, then - yet
another action before the first one is finished, and so on) and if we pass them to Python interpreter
in the same asynchronous order (or disorder), then actions easily lock each other and the Python
interpreter hangs. This problem is solved with the help of special class PyInterp_Dispatcher
which creates a queue of requests of the Python interpreter and calls the actions one after another.
Let's see what happens in deactivatedModule() up to the end. The code that will be
synchronously executed is in the protected virtual method execute() of DeactivateReq
(PyInterp_LockRequest subclass object):
virtual void execute()
{
myObj->deactivate( myStudy );
}
Here, myObj is an object of SALOME_PYQT_Module type, so we have
SALOME_PYQT_Module::deactivate() method now (as it is called in execute()):
to
see
Page 48 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
OK, we finally come to the code executed in ATOMGEN component. Let's see the deactivate
method of ATOMGENGUI.py:
def deactivate():
print "ATOMGENGUI::deactivate"
# connect selection
global myStudy
studyId = myStudy._get_StudyId()
selection = __study_data_map__[ studyId ][ "selection" ]
selection.ClearIObjects()
QObject.disconnect( selection,
SIGNAL( "currentSelectionChanged()" ), selectionChanged )
global myRunDlg
if myRunDlg:
myRunDlg.close()
myRunDlg = None
myStudy = None
pass
This method performs the local de-activation, in particular, it clears selection and closes the dialog
box if it was open.
Once again, let's retrace the call stack:
1. SALOME_PYQT_Module::deactivateModule() is called asynchronously (in general)
in response to an external event (component deactivation)
2. SALOME_PYQT_Module::deactivate() is called synchronously using an internal
event queue
3. deactivate from ATOMGENGUI is called
This procedure of calls from SALOME_PYQT_Module to Python script is followed by all methods of
GUI module for a Python component. The following methods of ATOMGENGUI.py are called from
SALOME_PYQT_Module in response to corresponding events:
initialize
windows
views
activate
deactivate
activeStudyChanged
createPopupMenu
OnGUIEvent
Page 49 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
import ui_rundlg
class RunDlg(QDialog, ui_rundlg.Ui_RunDlg):
......
The Python module ui_rundlg, mentioned in this code, is a module automatically generated from
rundlg.ui file. Class _rundlg.Ui_RunDlg is a class of dialog box described in rundlg.ui
file. In the source directory we have only this ui file, but after we build ATOMGEN component, the
build directory will contain ui_rundlg.py file (ATOMGEN_BUILD/bin/salome subdirectory)
generated by pyuic compiler. So we can derive a new class from rundlg.RunDlg:
class RunDlg(QDialog, ui_rundlg.Ui_RunDlg)
Deriving from classes generated by the pyuic compiler is very common in development of GUI for
Python components. QDialog is the parent class, that allows RunDlg to be Qt dialog window
widget that contains all components declared in ui file. In constructor on RunDlg it is called
setupUi method of Ui_RunDlg to initialize and insert all content widgets into the dialog box.
The last topic that we are going to learn in this section is creation of menu items and tool buttons in
GUI modules written in Python. GUI module of a C++ component can create menu items and tool
buttons using only createMenu(), and createTool() methods. Python components also can
use these methods of SalomePyQt Python module. As we can see in activate method of
ATOMGENGUI module a menu items are created using the following calls:
a = sgPyQt.createAction( __CMD_IMPORT_XML__,
tr( "MEN_IMPORT_XML" ),
tr( "TOP_IMPORT_XML" ),
tr( "STB_IMPORT_XML" ) )
fileMnu = sgPyQt.createMenu( QApplication.translate( "ATOMGENGUI",
"MEN_FILE" ), -1, -1 )
sgPyQt.createMenu( __CMD_IMPORT_XML__, fileMnu, 10 )
But we can also see that menu ATOMGEN with Run item is not present in the code of
ATOMGENGUI. Identifier of the "Run" command is declared there (__CMD_RUN_ALGO__ =
4002), moreover, the command is properly handled in ATOMGENGUI (onRunAlgo() method).
This menu item and the tool button are declared in a special XML file which is located in the
resource directory of ATOMGEN component: ATOMGEN_en.xml
<?xml version='1.0' encoding='us-ascii'?>
<!DOCTYPE application PUBLIC "" "desktop.dtd">
<application title="ATOMGEN component" date="15/11/2005"
author="SALOME team" appId="SALOME" >
<desktop>
<!-- ### MENUBAR ### -->
<menubar>
<menu-item label-id="ATOMGEN" item-id="90" pos-id="3">
<popup-item item-id="4002" label-id="Run" icon-id=""
tooltip-id="Runs calculations" accel-id="" toggle-id=""
Page 50 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
</desktop>
</application>
This XML file describes menu items and tool buttons for ATOMGEN component. In order to use
this way of menu items creation, the XML file must be named in the following way:
component_name + "_" + language_id + ".xml". language_id is 2 letters language
identifier used in the current session of SALOME ("en", "fr", "ru", etc. - same postfix is used
for naming the po-files in SALOME). This way of menu items and tool buttons creation (via XML
file) was adopted in previous versions of SALOME platform (series 1.x, 2.x), now it is left only for
Python components for compatibility reasons.
With this section we would like to finish the development of ATOMGEN Python component. Please,
use this link to download the latest source files of the fully functional version of ATOMGEN. Study
them carefully, and after that return to our tutorial to study the next chapter which will explain how
to develop a component in C++ with engine and advanced visualization for our molecular data!
4.5 DUMP PYTHON MECHANISM
As it has been discussed in chapter 3.7.1 there are basically two different approaches for
implementing of Dump python functionality in SALOME components to make historical or
snapshot dump. For the ATOMGEN component historical dump approach seems to suit better
than snapshot one, because engine of this component implements methods, which can create
and publish in the study more then one object at a time. As described in the paragraph 3.7.1, in
case of the historical dump approach each command that creates and publishes data in the study
should store additional information related to the Dump python functionality in the component. To
store this additional information we will use AttributeTableOfString attribute class that will be
created on the root SObject of the components data tree. This approach allows us to avoid
keeping the data related to the Dump python functionality somewhere in additional data structures
at the engine side. An additional advantage of SALOME attribute usage is that it is persistent (as all
other SALOMEDS attributes) it is saved/restored to the study file automatically by SALOME data
server.
Lets add required changes in the implementation of the components methods:
1. Changes in the importXmlFile() method:
def importXmlFile( self, fileName ):
"""
Imports atomic data from external XML file
and publishes the data in the active study
"""
if self.study:
# import file
from ATOMGEN_XmlParser import readXmlFile
new_data = readXmlFile( fileName )
entries = self.appendData( new_data )
if len(entries) > 0 :
cmd = "[" + ", ".join(entries) +
"] = " + __pyEngineName__
cmd += ".importXmlFile('" + fileName + "')"
attr = self._getTableAttribute()
Copyright 2001-2013. All rights reserved.
Page 51 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
Page 52 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
Finally, lets impement DumpPython method. This method iterates through the stored python
commands, for each command generates unique valid object name and replaces objects entry by
the generated name in the resulting Python command:
def DumpPython(self, theStudy, isPublished, isMultiFile):
script = []
prefix = ""
if isMultiFile :
script.append("import salome")
script.append("\n")
prefix = "\t"
script.append("import ATOMGEN\n")
script.append(__pyEngineName__ + " =
salome.lcc.FindOrLoadComponent(\"FactoryServerPy\",
\"ATOMGEN\")")
if isMultiFile :
script.append("def RebuildData(theStudy):\n")
script.append(prefix+__pyEngineName__ +
".setCurrentStudy(theStudy)\n")
else:
script.append(__pyEngineName__ +
".setCurrentStudy(theStudy)\n")
attr = self._getTableAttribute()
if attr is not None:
for idx in range(attr.GetNbRows()):
s = prefix + attr.GetValue(idx+1,1)
script.append(s)
if isMultiFile :
script.append(prefix+"pass")
else:
script.append("\n")
script.append("\0")
all = "\n".join(script)
self._getPyNames()
studyID = self.study._get_StudyId()
for k in self.entry2PyName[studyID].keys() :
all = all.replace(k,self.entry2PyName[studyID][k])
return (all,1)
Please, use this link to download the latest source files of the fully functional version of
ATOMGEN componet with implemented dump python mechanism.
Page 53 of 82
the data (molecules and atoms) are created in ATOMIC component (light-weight) and
saved in XML format;
ATOMGEN component reads the data from the XML file and performs their "spacial
analysis": number of molecules and atoms is increased.
ATOMSOLV component (to be developed in this chapter) retrieves the data from ATOMGEN
component and performs its further analysis. It assigns properties to molecules (we think of these
properties as "temperature" of molecules). And ATOMSOLV finally displays the results: atoms are
displayed as spheres in 3D space; the color of the spheres reflects the value of temperature of a
molecule the atoms belong to.
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
Studying this chapter step by step, we shall develop the next new component for processing of
molecules and atoms - ATOMSOLV component. Currently the cycle of our sample data processing
is the following:
Page 54 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
ATOMSOLV_Gen : Engines::EngineComponent
setData( in long studyID, in TMoleculeList theData );
getData( in long studyID, out TMoleculeList outData );
processData( in long studyID );
};
Let's take a look at the implementation of the ATOMSOLV engine interface:
class ATOMSOLV: public POA_ATOMSOLV_ORB::ATOMSOLV_Gen, public
Engines_Component_i
{
public:
ATOMSOLV(CORBA::ORB_ptr, PortableServer::POA_ptr,
PortableServer::ObjectId *, const char *, const char *);
virtual ~ATOMSOLV();
bool setData( long studyID,
const ATOMSOLV_ORB::TMoleculeList& theData );
bool getData( long studyID,
ATOMSOLV_ORB::TMoleculeList_out outData );
bool processData( long studyID );
private:
std::map<long, ATOMSOLV_ORB::TMoleculeList*> myData;
};
The implementation class ATOMSOLV inherits POA_ATOMSOLV_ORB::ATOMSOLV_Gen class which
is an automatically generated stub for ATOMSOLV_Gen CORBA interface. And it inherits
Engines_Component_i class as the base component (engine) implementation class.
The private member field myData (std::map) is used to store lists of molecules for multiple studies.
The key in the map is a studyID, and the value is a list of TMolecules (molecules with temperature).
The processing is done very simple - temperature assigned to molecules is a randomly generated
floating point value:
bool ATOMSOLV::processData( long studyID )
{
Copyright 2001-2013. All rights reserved.
Page 55 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
GUI module for a component with engine must be derived from SalomeApp_Module class, which
presumes use of an engine. Pure virtual method SalomeApp_Module::engineIOR() must
return IOR of CORBA engine of a component. The engine is usually loaded by GUI module on the
first invocation (in virtual method initialize()) and registered in LifeCycleCORBA. Let's take a
look at methods of ATOMSOLVGUI that work with engine:
virtual QString engineIOR() const;
static void InitATOMSOLVGen( SalomeApp_Application* );
static ATOMSOLV_ORB::ATOMSOLV_Gen_var GetATOMSOLVGen();
void ATOMSOLVGUI::InitATOMSOLVGen( SalomeApp_Application* app )
{
if ( !app )
myEngine = ATOMSOLV_ORB::ATOMSOLV_Gen::_nil();
else {
Engines::EngineComponent_var comp =
app->lcc()->FindOrLoad_Component( "FactoryServer",
"ATOMSOLV" );
ATOMSOLV_ORB::ATOMSOLV_Gen_ptr atomGen =
ATOMSOLV_ORB::ATOMSOLV_Gen::_narrow(comp);
ASSERT( !CORBA::is_nil( atomGen ) );
myEngine = atomGen;
}
}
ATOMSOLV_ORB::ATOMSOLV_Gen_var ATOMSOLVGUI::GetATOMSOLVGen()
{
if ( CORBA::is_nil( myEngine ) ) {
SUIT_Application* suitApp =
SUIT_Session::session()->activeApplication();
SalomeApp_Application* app =
dynamic_cast<SalomeApp_Application*>( suitApp );
InitATOMSOLVGen( app );
}
return myEngine;
}
QString ATOMSOLVGUI::engineIOR() const
{
CORBA::String_var anIOR =
getApp()->orb()->object_to_string( GetATOMSOLVGen() );
return QString( anIOR.in() );
}
Method GetATOMSOLVGen() is made static to allow access to ATOMSOLV engine from different
GUI classes. It is also possible to acquire a pointer to ATOMSOLV_Gen calling directly
FindOrLoad_Component() method of LifeCycleCORBA interface that can be aquired from
SalomeApp_Application instance.
Let's implement a method to retrieve data from ATOMGEN engine and store it in ATOMSOLV
engine. It will be a slot connected to "Retrive data" action of ATOMSOLVGUI class:
Copyright 2001-2013. All rights reserved.
Page 56 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
1. void ATOMSOLVGUI::OnRetrieveData()
2. {
3.
ATOMSOLV_ORB::ATOMSOLV_Gen_var engine = GetATOMSOLVGen();
4.
SalomeApp_Application* app = getApp();
5.
if ( !CORBA::is_nil( engine ) && app ) {
6.
// acquire ATOMGEN engine: use LifeCycleCORBA service for it
7.
Engines::EngineComponent_var comp =
8.
app->lcc()->FindOrLoad_Component("FactoryServerPy","ATOMGEN");
9.
ATOMGEN_ORB::ATOMGEN_Gen_var atomGen =
10.
ATOMGEN_ORB::ATOMGEN_Gen::_narrow( comp );
11.
SalomeApp_Study* appStudy =
12.
dynamic_cast<SalomeApp_Study*>( app->activeStudy() );
13.
if ( !CORBA::is_nil( atomGen ) && appStudy ) {
14.
const int studyID = appStudy->id();
15.
// load study data if it is not done yet by ATOMGEN component
16.
if ( _PTR( Study ) studyDS = appStudy->studyDS() ) {
17.
if ( _PTR( SComponent ) atomGenSComp =
18.
studyDS->FindComponent( "ATOMGEN" ) ) {
19.
_PTR( StudyBuilder ) builder = studyDS->NewBuilder();
20.
std::string atomGenIOR =
21
app->orb()->object_to_string( atomGen );
22.
builder->LoadWith( atomGenSComp, atomGenIOR );
23.
}
24.
}
25.
// retrieve data from ATOMGEN
26.
ATOMGEN_ORB::MoleculeList_var inData =
27.
atomGen->getData( studyID );
28.
// "convert" Molecules to TMolecules,
29.
// set default temperature '0'
30.
const int n = inData->length();
31.
ATOMSOLV_ORB::TMoleculeList_var outData =
32.
new ATOMSOLV_ORB::TMoleculeList();
33.
outData->length( n );
34.
for ( int i = 0; i < n; i++ ) {
35.
ATOMSOLV_ORB::TMolecule_var tmol =
36
new ATOMSOLV_ORB::TMolecule();
37.
tmol->molecule =
38.
ATOMGEN_ORB::Molecule::_duplicate( inData[i] );
39.
tmol->temperature = 0;
40.
outData[ i ] = tmol;
41.
}
42.
// store the data in ATOMSOLV engine
43.
engine->setData( studyID, outData );
44.
45.
// update object browser so new data objects appear in it
46.
app->updateObjectBrowser();
47.
}
48.
}
49. }
As we see, first of all we acquire reference to both engines: of ATOMSOLV and ATOMGEN
components (lines 3, 9). Then we must get the data from ATOMGEN that corresponds to the
currently opened study. We obtain the integer study ID (14), and then use SALOMEDS services for
loading of the study by the component ATOMGEN (16-22). It is necessary to do it, because internal
data structure of ATOMGEN may not be initialized in case if ATOMGEN was not previously loaded
and therefore it has not "connected" to the current study. We do this "connection" of ATOMGEN
engine to study manually, calling StudyBuilder::LoadWith() method (22). After that we
retrieve the data and convert it to format of ATOMSOLV (list TMolecules instead of Molecules),
setting default temperature of 0 (25-41). Finally, we store the data in ATOMSOLV engine (43) and
update the object browser in order to see the new data under ATOMSOLV root.
Page 57 of 82
P l a t f o r m
P l a t f o r m
S A L O M E
S A L O M E
}
}
QString ATOMSOLVGUI_DataObject::entry() const
{
QString id = "root";
if ( myMoleculeIndex > -1 ) {
id = QString::number( myMoleculeIndex );
if ( myAtomIndex >= 0 )
id += QString( "_%1" ).arg( myAtomIndex );
}
return QString( "ATOMSOLVGUI_%1" ).arg( id );
}
QString ATOMSOLVGUI_DataObject::name() const
{
ATOMSOLV_ORB::TMolecule tmolecule = getTMolecule();
ATOMGEN_ORB::Molecule_var mol = tmolecule.molecule;
if ( !CORBA::is_nil( mol ) ) {
if ( !isAtom() )
return QString( "%1 [%2]" ).arg( mol->getName() ).arg(
tmolecule.temperature );
else if ( myAtomIndex < mol->getNbAtoms() )
return mol->getAtom( myAtomIndex )->getName();
}
return QString("-Error-");
}
ATOMSOLV_ORB::TMolecule ATOMSOLVGUI_DataObject::getTMolecule() const
{
ATOMSOLV_ORB::ATOMSOLV_Gen_var engine =
Copyright 2001-2013. All rights reserved.
Page 58 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
Data Model builds a tree of Data objects in the following way: first of all it creates a root object in
case it was not created before. Then it gets the list of TMolecules from engine, iterates it, and
builds Data Objects for every TMolecule, and for every atom of TMolecule.
Data Object stores 2 integer indexes: index of TMolecule(myMoleculeIndex), and index of
atom within TMolecule(myAtomicIndex). If myAtomicIndex is equal to '-1' then the Data
Object corresponds to a molecule object, if myAtomicIndex is a valid index ( >= 0), then the Data
Object corresponds to an atom.
Please, download the source files of the current version of ATOMSOLV component, compile them,
and start the application. ATOMGEN component must be also made available. It means that
ATOMGEN_ROOT_DIR variable must be correctly set and ATOMGEN component must be added to
the list of active components (with --modules=ATOMSOLV,ATOMGEN command line parameter,
for example). After the application is started, switch to ATOMGEN and import an XML file with data
prepared by ATOMIC component (for example, sample.xml from ATOMIC component, it is located
in resources directory of ATOMIC). Now activate ATOMSOLV component and select AtomSolv
Retrieve data command. AtomSolv root object with molecules and atoms must appear in Object
Browser - they were retrieved from ATOMGEN engine using CORBA technology!
If we choose AtomSolv Process data command now, the molecules will be assigned new
temperature
properties
(see
ATOMSOLVGUI::OnProcessData()
and
ATOMSOLV::processData() methods). It is reflected in the Data Objects that correspond to the
molecules: the number in square brackets is changed.
Page 59 of 82
P l a t f o r m
P l a t f o r m
S A L O M E
S A L O M E
Page 60 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
We would like to add the following actions to the popup menu of atoms (displayable objects):
Display, Erase, Representation mode - Points, Representation mode - Wireframe, Representation
mode - Surface, Change color, Change Transparency. So called "representation mode" (or "display
mode") is the way the object looks in 3D viewer:
Page 61 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
OCCViewer, SOCC
Plot2d, SPlot2d
VTKViewer, SVTK
All visualization packages of GUI module presented above contain several obligatory classes
inherited from classes of SUIT package that allow for abstraction from a certain way the object
visualization is implemented in this package. The interface of all visualization packages is the
same, the implementation, of course, differs. In the table below we will try to describe the base
classes and their objectives:
SUIT_ViewWindow
Page 62 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
The name of the class shows its main purpose: it manages the views
(view windows). It contains a view model as a member field for creation
of a view and various methods for accessing the managed views
(getActiveView(), getViewsCount(), etc.).
SUIT_ViewManager is a "gateway" class for working with view
windows from application or another side. STD_Application class
(parent
class
for
LightApp_Application
and
SalomeApp_Application) stores view managers and it is possible to
retrieve a view manager of a certain type using methods of the
application.
The classes presented above from SUIT package play the role of an abstraction layer that
generalizes where the objects are displayed (view window). Now we have to observe the objects
themselves.
Different viewers naturally work with different internal graphic presentations. OCCViewer, for
example, uses AIS_InteractiveObject class for presentation of an object in the graphic
scene; VTKViewer uses vtkActor for the same purpose. The goal of SALOME platform is to
support a unified way of working with presentation objects of different types. This is done using an
abstraction layer declared in Prs package of GUI module. This package contains only 2 files
(SALOME_Prs.h/cxx), they contain declaration and implementation of several classes described
in the table below:
SALOME_Prs
SALOME_OCCPrs,
SALOME_VTKPrs,
SALOME_Prs2d
Page 63 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
SALOME_Displayer
Let's create our own Displayer class (in fact, we have already done it above) and redefine
buildPresentation() method:
SALOME_Prs* ATOMSOLVGUI_Displayer::buildPresentation( const QString&
entry, SALOME_View* view )
{
const int studyID = getStudyID();
if ( studyID == -1 )
return 0;
SVTK_Prs* prs = dynamic_cast<SVTK_Prs*>(
LightApp_Displayer::buildPresentation( entry, view ) );
if ( !prs ) return 0;
double temperature;
ATOMGEN_ORB::Atom_var atom = getAtom( entry, studyID,
temperature );
if ( !CORBA::is_nil( atom ) ) {
double center[ 3 ];
center[ 0 ] = atom->getX();
center[ 1 ] = atom->getY();
center[ 2 ] = atom->getZ();
vtkSphereSource* vtkObj = vtkSphereSource::New();
vtkObj->SetRadius( radius );
vtkObj->SetCenter( center );
vtkObj->SetThetaResolution( (int)( vtkObj->GetEndTheta() *
quality_coefficient ) );
vtkObj->SetPhiResolution( (int)( vtkObj->GetEndPhi() *
quality_coefficient ) );
vtkPolyDataMapper* vtkMapper = vtkPolyDataMapper::New();
vtkMapper->SetInput( vtkObj->GetOutput() );
Copyright 2001-2013. All rights reserved.
Page 64 of 82
vtkObj->Delete();
SALOME_Actor* actor = SALOME_Actor::New();
actor->SetMapper( vtkMapper );
actor->setIO( new SALOME_InteractiveObject( entry.toLatin1(),
"ATOMSOLV" ) );
setTemperature( actor, temperature );
actor->SetRepresentation( 2 ); // 2 == surface mode
vtkMapper->Delete();
prs->AddObject( actor );
}
return prs;
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
}
The main lines in the code above are highlighted in bold font: creation of vtkActor
(SALOME_Actor is its ancestor), and filling of SALOME_Prs object by this vtkActor (SVTK_Prs
to be more specific). Such SALOME_Prs object will be possible to display in VTK viewer.
SVTK_ViewModel, as an ancestor of SALOME_View will receive this SALOME_Prs, downcast it to
SVTK_Prs, get vtkActor from it - and display it in the active view window. That's how it works in
SALOME.
As we decided to work with VTK viewer (canBeDisplayed() method of ATOMSOLV_Displayer
returns "true" only if type of viewer is "VTKViewer"), we must create a VTK view window when our
component GUI starts. This is done using virtual method of LightApp_Module
viewManagers(). We must return in the out parameter of this method the list of viewer types that
we want to use. In our case, the implementation will be the following:
void ATOMSOLVGUI::viewManagers( QStringList& theViewMgrs ) const
{
theViewMgrs.append( SVTK_Viewer::Type() );
}
OK, now we have to return to the new commands that we were going to implement in ATOMSOLV
GUI: changing of 3D representation mode (points, wireframe, surface mode), changing of color and
transparency. We are going to create new actions in ATOMSOLVGUI and create the logical rules
for these actions, so they are correctly added to the popup menu:
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
Page 65 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32,
33.
34.
35.
36.
37.
38.
The last section of the code (lines 33-38) is rather interesting: we make the representation mode
actions to be toggle actions ("checkable" menu items) and set logical rules for the toggle status. (If
QtxPopupMrg::setRule() function is called with the last parameter equal to
QtxPopupMgr::ToggleAction, then it sets the rule for the toggle status, and not for command
visibility as it would be with default last parameter).
Let's take a closer look at the rules themselves. In natural language they would mean: "values of
displaymode parameter must be equal to the list that contains 1 element ('Points', 'Wireframe', or
'Surface')". Such rule differs from the rule "displaymode='Points'" (or 'Wireframe', or 'Surface'),
because in case when 2 elements with different representation modes will be selected, the first rule
would return "false" result for both actions (because value of displaymode parameter will be equal
to the list containing 2 different elements), although the second rule would return "true" for both
actions.
The new parameter "displaymode" that we are using in the logical rules is not computed anywhere
for us (as "client" or "selcount" parameters). We have to create a custom Selection class and
compute this parameter in it. We already got acquainted with this mechanism of parameters
computation in ATOMIC component (see Selection section of Light-weight component chapter), so
here we will just present the function of our custom Selection class (ATOMSOLVGUI_Selection),
which computes the new "displaymode" parameter:
QString ATOMSOLVGUI_Selection::displayMode( const int index ) const
{
SALOME_View* view = LightApp_Displayer::GetActiveView();
QString viewType = activeViewType();
if ( view && viewType == SVTK_Viewer::Type() ) {
if ( SALOME_Prs* prs = view->CreatePrs(
entry( index ).toLatin1() )){
SVTK_Prs* vtkPrs = dynamic_cast<SVTK_Prs*>( prs );
vtkActorCollection* lst = vtkPrs ? vtkPrs->GetObjects() : 0;
if ( lst ) {
lst->InitTraversal();
vtkActor* actor = lst->GetNextActor();
if ( actor ) {
SALOME_Actor* salActor =
dynamic_cast<SALOME_Actor*>( actor );
if ( salActor ) {
int dm = salActor->GetRepresentation();
if ( dm == 0 )
return "Points";
else if ( dm == 1 )
return "Wireframe";
else if ( dm == 2 )
Copyright 2001-2013. All rights reserved.
Page 66 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
The last thing that we must implement is the slot in ATOMSOLVGUI class to which all visualization
actions are connected (Display, Erase, Representation modes, Color, Transparency). We will also
implement
changing
of
representation
mode,
color,
and
transparency
in
ATOMSOLVGUI_Displayer class.
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
);
23.
24.
25.
26.
27.
28.
);
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
);
39.
40.
41.
42.
43.
44.
45.
46.
void ATOMSOLVGUI::OnDisplayerCommand()
{
const QObject* obj = sender();
if ( obj && obj->inherits( "QAction" ) ) {
const int id = actionId ( (QAction*)obj );
switch ( id ) {
case Display : {
QStringList entries;
selected ( entries, true );
ATOMSOLVGUI_Displayer d;
for ( QStringList::const_iterator it = entries.begin(),
last = entries.end(); it != last; it++ )
d.Display( it->toLatin1(), /*updateviewer=*/false, 0 );
d.UpdateViewer();
} break;
case Erase
: {
QStringList entries;
selected ( entries, true );
ATOMSOLVGUI_Displayer d;
for ( QStringList::const_iterator it = entries.begin(),
last = entries.end(); it != last; it++ )
d.Erase( *it, /*forced=*/true, /*updateViewer=*/false, 0
d.UpdateViewer();
} break;
case Shading
: {
QStringList entries;
selected ( entries, true );
ATOMSOLVGUI_Displayer().setDisplayMode(entries, "Surface"
} break;
case Wireframe
: {
QStringList entries;
selected ( entries, true );
ATOMSOLVGUI_Displayer().setDisplayMode(entries, "Wireframe");
} break;
case PointsMode
: {
QStringList entries;
selected ( entries, true );
ATOMSOLVGUI_Displayer().setDisplayMode( entries, "Points"
} break;
case Color
: {
QStringList entries;
selected ( entries, true );
QColor initialColor( "white" );
if ( entries.count() == 1 )
initialColor=ATOMSOLVGUI_Displayer().getColor(entries[0]);
Page 67 of 82
getApp()->desktop() );
if ( color.isValid() )
ATOMSOLVGUI_Displayer().setColor( entries, color );
} break;
case Transparency
: {
QStringList entries;
selected ( entries, true );
ATOMSOLVGUI_TransparencyDlg( getApp()->desktop(),
entries ).exec();
} break;
default: printf( "ERROR: Action with ID = %d was not found
in ATOMSOLVGUI\n", id ); break;
}
}
}
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
As we see in the code above, to display an object we call basic method of Displayer class:
Display() (lines 7-14). This method is not implemented in ATOMSOLVGUI_Displayer, but the
core implementation in the parent class (LightApp_Displayer) will call virtual function
buildPresentation() which is redefined in ATOMSOLVGUI_Displayer.
Please, download the current version of ATOMSOLV component source files, compile it and run.
Don't forget to start ATOMGEN component as well and import a valid XML file with data in it
(sample.xml from ATOMIC component resources). Then retrieve the data in ATOMSOLV, select
atoms in Object Browser and display them in the viewer. Pay attention to the methods of
ATOMSOLV_Displayer: how it assigns new color to presentations of atoms when "Process Data"
is called (updateActor(), setTemperature() methods), how it sets new representation
mode (setDisplayMode() method), and transparency (setTransparency()).
For setting transparency we use a separate modal dialog box ATOMSOLVGUI_TransparencyDlg,
which calls setTransparency() method of Displayer every time user moves the transparency
control (slider bar).
As an exercise, we would propose you to upgrade ATOMSOLV component and make it possible to
display atoms in OCCViewer (another type of 3D viewer). Don't forget to add modifications in the
following methods:
Get/set methods for representation mode, color, transparency must take into account the
type of viewer! Try to use methods of SALOME_View interface for simplification.
At this point we would like to finish the section about graphical capabilities of SALOME and switch
to the next section about user-defined preferences in a SALOME application.
5.4 PREFERENCES
In the previous section we implemented visualization of atoms as 3D spheres with hard-coded
values of radius and initial representation mode. Such approach is not flexible and does not meet
requirements of an industrial application. Even our ATOMSOLV component is not an industrial
application, we would like it to be as good as possible, and in the present section we will learn how
Copyright 2001-2013. All rights reserved.
Page 68 of 82
As we see in the dialog box above, the list-box on the left contains the names of all available
components. But only untill the GUI modules of these components are loaded. As soon as GUI
modules are loaded, they are requested for their preferences. If a component does not have any
preferences (in case of ATOMGEN and ATOMSOLV component - it is true), the name of the
component is removed from the list-box. So if we load ATOMGEN and ATOMSOLV components,
and open Preferences dialog box after that -- we will not see the names of our components in the
list-box.
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
In ATOMSOLV component we would like to add the following parameters to be edited by user:
floating point parameter radius, and representation mode parameter that can be equal to one of the
3 predefined values: "Points", "Wireframe", "Surface". How can this be done?
Page 69 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
void ATOMSOLVGUI::createPreferences()
{
int tabId = addPreference( tr( "ATOMSOLV_PREFERENCES" ) );
int groupId = addPreference( tr( "PRESENTATION_PREF_GROUP" ),
tabId );
setPreferenceProperty( groupId, "columns", 1 );
// Representation mode preference
int dispModeId = addPreference( tr( "DISPLAY_MODE_PREF" ),
groupId, LightApp_Preferences::Selector,
"ATOMSOLV", "Representation" );
QList<QVariant> intDispModes;
QStringList strDispModes;
intDispModes.append( 0 );
strDispModes.append( tr( "MEN_POINTSMODE" ) );
intDispModes.append( 1 );
strDispModes.append( tr( "MEN_WIREFRAME" ) );
intDispModes.append( 2 );
strDispModes.append( tr( "MEN_SHADING" ) );
setPreferenceProperty( dispModeId, "strings", strDispModes );
setPreferenceProperty( dispModeId, "indexes", intDispModes );
// Radius preference
int radisusId = addPreference( tr( "RADIUS_PREF" ), groupId,
LightApp_Preferences::DblSpin, "ATOMSOLV", "Radius" );
setPreferenceProperty( radisusId, "min", .001 );
setPreferenceProperty( radisusId, "max", 1000 );
setPreferenceProperty( radisusId, "precision", 3 );
}
Adding preferences is very simple. We use only 1 method to create a separate tab for preferences
of ATOMSOLV component (line 3), the same method to create a group (QGroupBox) for our
preferences (line 4), and the same method for creation of editable parameters as well (lines 8, 22).
The method is addPreference(), it is inherited from LightApp_Module class.
If addPreference() is called with 1 parameter it creates a tab, with 2 - it creates a group (a tab
ID must be passed as second parameter), with more then 2 - it creates a control for editing of a
certain value. The type of control is passed as a third parameter (different control types are
described in the table below), 4th and 5th parameters are descriptors of the value being edited.
Later retrieval of the value using Resource Manager must use these descriptors.
LightApp_Preferences::Space
LightApp_Preferences::Bool
LightApp_Preferences::Color
LightApp_Preferences::String
LightApp_Preferences::Selector
LightApp_Preferences::DblSpin
Page 70 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
LightApp_Preferences::Double
LightApp_Preferences::Integer
LightApp_Preferences::GroupBox
LightApp_Preferences::Font
LightApp_Preferences::DirList
LightApp_Preferences::File
LightApp_Preferences::User
After we have added the necessary parameters, they must be adjusted. For example, by default
the preferences are grouped in 2 columns on a tab. In our case 1 column would look better, and
we add the following line in createPreferences() method:
setPreferenceProperty( groupId, "columns", 1 );
It makes the controls in the previously created group with ID = groupId to be aligned in 1 column,
one under another.
For Selector control it is necessary to install the list of values. It is possible to set up 2 lists: one list
of displayable values ("strings"), and another list of values to be returned ("indexes"). In our case it
is very helpful, because representation mode "Surface" has ID of 2, for example. We set up 2
properties for representation mode preference (with dispModeId) -- see lines 11-18 in the code
above.
For the Radius parameter we will set up 3 properties: minimum value, maximum value, and
precision. How it is done - shown on lines 24-26 in the code above.
The next important task is to track changes of our preferences. User may open Preferences dialog
box at any time and modify values of our parameters. ATOMSOLV component must reflect to
these changes, and draw next atoms with new radius, for example, in case the radius was
changed. Tracking these changes is very easy: we have to redefine one more virtual method in
ATOMSOLVGUI: preferencesChanged():
void ATOMSOLVGUI::preferencesChanged( const QString& group, const
QString& param )
{
Copyright 2001-2013. All rights reserved.
Page 71 of 82
At this point we would like to finish our Getting started with SALOME platform tutorial. We believe
that now you are ready for development of new SALOME-based components on your own. If you
have questions or/and suggestions - please, we will be happy to hear them from you on SALOME
platform web forum: http://www.salome-platform.org/forum/!
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
Please, download the last final version of ATOMSOLV source files with editable preferences.
Page 72 of 82
6. SALOME CONCEPTS
6.1 KERNEL CONCEPTS
6.1.1 Build configurations
P l a t f o r m
P l a t f o r m
2 core modules of SALOME platform - KERNEL and GUI - can be compiled and run in 2
configurations (can be understood as versions): full and light.
Light configuration means that all CORBA-based services are disabled. To build the
modules in light configuration -DSALOME_LIGHT_ONLY=ON parameter must be passed to
the cmake command (see chapter SALOME build procedure for details). To run SALOME
in light configuration a command runLightSalome.csh (or runLightSalome.sh) from
GUI module is used.
Light-weight components can work with SALOME built in both light and full configurations. In other
words, a light-weight component can be a part of a multi-component SALOME application, and the
other components do not have to be necessarily light-weight. But if all components are light-weight
(in particular, if there is only 1 light-weight component in an application), then it is preferable to use
KERNEL and GUI modules in light configuration. This will increase the application performance
since a number of unused CORBA-based services will not be started.
Full-weight components can be compiled and run only if KERNEL and GUI are built in full
configuration.
S A L O M E
S A L O M E
6.1.2 Component
A component is the base concept of SALOME platform. It can be understood as a separate
software application - a piece of software which is dedicated to do a certain functionality.
Examples of components are:
GEOM component - allows user to create geometrical data using various algorithms of
creation and modification of geometrical primitives
YACS component - allows user to perform complicated data processing using other
components and embedded Python
etc.
Page 73 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
Page 74 of 82
P l a t f o r m
P l a t f o r m
Light-weight component consists of GUI module and functional packages which are accessible only
from its GUI module and not accessible from within other components.
Light-weight components can work with SALOME built in both light and full configurations. In other
words, a light-weight component can be a part of a multi-component SALOME application, and the
other components do not have to be necessarily light-weight. But if all components are light-weight
(in particular, if there is only 1 light-weight component in an application), then it is preferable to use
KERNEL and GUI modules in light configuration. This will increase the application performance
since a number of unused CORBA-based services will not be started.
Currently only C++ light-weight components are supported by SALOME platform. In future, it is
planned to add support for Python light-weight components.
6.1.6 Numerical computations cycle
Software data processing which is usually performed in 3 phases:
S A L O M E
S A L O M E
The data becomes available for other components (for processing, visualization, etc.), it
can accessed using SALOMEDS tools and services.
The data becomes automatically persistent (can be saved and restored), as persistence is
already implemented in SALOMEDS library.
SALOMEDS also provides the mechanism of data persistence for components that do not publish
their data in a common SALOMEDS data structure. This mechanism is described in Implementing
persistence section of the tutorial. Briefly, SALOMEDS provides the following: a component saves
its data in arbitiary format to an external file and returns the name of this file to SALOMEDS.
SALOMEDS serializes this file into a binary stream and includes it into the common Study file on
Copyright 2001-2013. All rights reserved.
Page 75 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
Data Model represents arbitrary internal data in a tree-like structure. It is done through root()
method of CAM_DataModel class. It returns object of the highest level of component's data,
usually this object represents the component itself. This objects (instance of Data Object class) has
child objects, they also have child objects, and so forth.
Data model class is usually redefined by a component (inherits LightApp_DataModel or
SalomeApp_DataModel classes).
6.2.2 Data object
It is a unitary piece of data within a component GUI. Its primary mission is to provide a common
view to an arbitrary data. It is a proxi-object - it hides the real implementation of data and provides
a generic interface to accessing it by other objects. For example, Object Browser "knows" how to
display Data Objects, Selection Manager "knows" how to select Data Objects, and only Data
Object itself "knows" which real piece of component's data was accessed (displayed, selected, etc.)
through it.
Data object supports tree-like structure: it has a parent Data object (or null, if it is a root-level
object), and arbitrary number of child objects.
Data object class is usually redefined by a component (inherits LightApp_DataObject or
SalomeApp_DataObject classes).
6.2.3 Data owner
Data owner is an abstract representation of a piece of data. It is mainly used for selection
management - Data owner represents a selected entity independently from the source of selection
(Object Browser, 3D viewer, 2D chart). Setting and retrieval of selection always operates with the
list of Data owners.
Data owner contains a unique string identifier called "entry" which is used for locating the real
object in the component data structure.
Usually it is not necessary to declare a custom Data owner class in a component, the base
implementation is in LightApp_DataOwner class.
6.2.4 Desktop
Desktop represents a main frame of a SALOME application. It contains a menu bar, tool bars, and
central area for GUI controls of components: Object Browser, Python console, 3D/2D viewers, etc.
Base desktop class is SUIT_Desktop, it defines methods to access active window inside the
desktop frame, managers of tool bars and main menu. STD package contains 3 successors of
base Desktop class: STD_SDIDesktop, STD_MDIDesktop, and STD_TabDesktop. SDI and
MDI desktop classes implement single and multiple document interfaces within the desktop frame.
TabDesktop allows for "tabbing" of windows within the desktop frame. By default, if it is not
overridden in a component GUI, SALOME applications use TabDesktop. Tabbed windows can be
Page 76 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
split vertically or horizontally, it is possible to store positions of windows, how they are split, etc.
and restore afterwards. Example of application with STD_TabDesktop:
Creation of main menu items and toolbar buttons, connection them to the component's
functions.
Definition of types of shared GUI objects that the component will use (viewers, object
browser, etc.).
In general, a GUI module coordinates the behavior of a component especially when it relates to
interaction with user.
GUI module class of C++ component must inherit LightApp_Module (for light architecture) or
SalomeApp_Module class.
GUI module of Python component is implemented in a more complex way. Please, refer to GUI for
Python component section of the tutorial for details.
6.2.6 Operation
Operation is a manager of an action inside a component GUI. By "action" we understand any
functionality a GUI module of a component provides to a user. Examples of actions may be the
following: creation of a sphere in Geometry component, Atom creation in ATOMIC component,
graph execution in Supervisor component.
Using an Operation for action management gives the following advantages:
Page 77 of 82
Operation instance can control which other Operations can be executed simultaneously
with this Operation. It is implemented using method
bool SUIT_Operation::isValid(SUIT_Operation* theOtherOperation)
const.
Before starting a new operation (operation_A), an application calls isValid() method of the
operation being executed (operation_B) passing it operation_A as a parameter. If
operation_B returns false, then operation_A is not started, it must wait untill operation_B
finishes its execution. This mechanism can be overridden, though, with yet another virtual
method of SUIT_Operation class:
bool SUIT_Operation::isGranted() const.
If this method returns true, then the operation is started any way, ignoring isValid() return
value.
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
Base class for Operation object is SUIT_Operation, then it is inherited in LightApp package LightApp_Operation.
Custom
operations
of
a
component
should
inherit
LightApp_Operation class.
6.2.7 Resource manager
It is a class that provides access to various resources at run-time. Values of integer, floating point,
boolean, string and even complex (QFont, QColor) types can be set and retrieved from the
Resource manager. Resource manager can be statically accessed from any place in the code
using the following call:
SUIT_Session::session()->resourceMgr();
Between the sessions Resource manager stores resources in external files in XML or INI formats
(XML by default). When SALOME application starts, Resouce manager locates these files and
loads resources from them. The resouces files are:
Page 78 of 82
"/work/ATOMGEN_BUILD/share/salome/resources:/work/ATOMSOLV_BUILD/sha
re/salome/resources:/work/GUI_BUILD/share/salome/resources"
Resource manager will try to load 3 SalomeApp.xml files from the 3 listed directories.
Certain resources can be modified by user using Preferences dialog box (see Preferences section
of the tutorial for details). Modified resources are written by the Resouce manager to the user
resource file (.LightApprc.<version> or .SalomeApprc.<version> in the user home directory).
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
When a selection event happens in a window, it emits a signal. This signal is caught by
the global Selection Manager.
Selection Manager requests for selected objects from the Selector of the signal emitter.
Selector creates a list of Data owners that correspond to the selected in its window entities.
Selection Manager receives the list of Data owners; then it iterates the other registered
Selectors and programmatically sets the selection in them passing the list of Data owners.
Having received the list of Data owners, Selectors try to select the corresponding objects in
their windows.
Page 79 of 82
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
As view window is a basic frame, it receives the basic window events: mouse moves, clicks,
keyboard presses, etc. One of the objectives of view window is to pass these events further - it is
done through various signals emitted by SUIT_ViewWindow class: mousePressed(),
mouseReleased(), weeling(), keyPressed(), etc.
Page 80 of 82
7. ATTACHMENTS
File name
Description
Paragraph #
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
light-00.tar.gz
2.2
FindGd.cmake
2.2
light-01.tar.gz
3.1
light-02.tar.gz
3.2
light-03.tar.gz
3.2
light-04.tar.gz
3.3
light-05.tar.gz
3.4
light-06.tar.gz
3.5
light-07.tar.gz
3.5.2
light-08.tar.gz
3.6
light-09.tar.gz
3.6
light-10.tar.gz
3.7
3.1
Page 81 of 82
py-01.tar.gz
4.2
py-02.tar.gz
4.2
py-03.tar.gz
4.3
py-04.tar.gz
4.4
py-05.tar.gz
4.5
S A L O M E
S A L O M E
P l a t f o r m
P l a t f o r m
5.1
c-02.tar.gz
5.2
c-03.tar.gz
5.3
c-04.tar.gz
5.4
Page 82 of 82