济南友泉软件有限公司
SALOME是一套开源跨平台的CAE集成开发平台,包含全参数化建模、后处理可视化、求解器集成、分析优化等模块。而SALOME GUI模块则构筑了SALOME的整体框架。
Ref. from SALOME GUI Architecture
SALOME GUI module or SUIT (SALOME User Interface Toolkit) represents a user interface framework for SALOME platform. It reflects the last trends of the GUI developments and best practices of the software development. SUIT is developed as a set of packages implementing reusable software components, allowing building multi-scale CAD applications. With SUIT it is possible both to implement new CAD applications (completely independent from SALOME itself) and develop and integrate fully SALOME-compliant modules - “light-weight” (without any CORBA connection, also called “light”) and “full-scale” (distributed, CORBA-based), using C++ or Python programming languages.
Among other important features SUIT proposes flexible, powerful and safe mechanisms of interaction with other SALOME modules (both distributed and “light”), resources management, viewers and selection handling, exception/signals processing, bringing to the top the multi desktop dockable-windowed user interface which improves usability of SALOME GUI.
本模块涉及的主要技术要点包括:
- Single Responsibility Principle
- Layered Architecture
- Command Pattern
- Data Object Model
- Plug-in System
关于上述技术要点,可参见笔者博文:大型CAx(CAD/CAE/CAM)工业软件开发中的关键组件
鉴于SALOME GUI模块是整个SALOME的基础模块,宜作为研究的切入点。同时考虑到SALOME GUI模块功能特点与独立性,本文拟主窗口、视图、菜单、工具栏等CAx软件界面组件方面探讨SALOME GUI模块实现的原理。
注1:限于笔者研究水平,难免有表述不当,欢迎批评指正。
注2:博文会不定期更新,敬请关注。
一、整体流程
suitexe是SALOM提供的加载不同应用的一个启动器,默认会加载LightApp。对应代码为gui/src/SUITApp/SUITApp.cxx。
// gui/src/SUITApp/SUITApp.cxx
//............
int main( int argc, char* argv[] )
{
//............
if ( args.empty() )
args << "LightApp"; // fallback application library
//............
// Create session
Session session( iniFormat );
// Initialize and start application supplied by the library specified via the parameter
SUIT_Application* sessionApp = session.startApplication( appName );
if ( sessionApp )
{
#ifdef USE_SALOME_STYLE
Style_Salome::initialize( session->resourceMgr() );
if ( session->resourceMgr()->booleanValue( "Style", "use_salome_style", true ) )
Style_Salome::apply();
#endif // USE_SALOME_STYLE
if ( !debugExceptions )
app.setHandler( session.handler() );
if ( splash )
splash->finish( sessionApp->desktop() );
return app.exec();
}
return 1;
}
SUIT_Session用于根据用户输入参数appName创建不同的应用对象,实际上就是加载不同的动态链接库,并通过调用动态链接库内的createApplication()来生成应用程序对象。
从上图可以看出,SUIT_Application是所用应用对象的基类,应用对象定义文档类型(SUIT_Application::myStudy)、主窗口(SUIT_Application::myDesktop)、公用菜单与公用工具栏(SUIT_Application::myActionMap)、视图(STD_Application::myViewMgrs)等组件。
注:主窗口菜单、工具栏主要来自两部分,一部分是由SUIT_Application::myActionMap所定义菜单与工具栏;另一部分是来自CAM_Module::myActionMap。
Ref. from gui/src/SUIT/SUIT_Application.h
An Application is a class which defines application configuration and behaviour. For example Application object defines what Viewers are used in this application, what auxilliary windows are present, how user can dial with them. Also Application object defines an sertain type of data structure by holding of pointer on an instance of SUIT_Study class (which represents Document data structure). In other words Application defines type of sata structure, type of used Viewers, type of main GUI widget (Desktop), and other auxilliary tools.
二、主要组件
2.1 SUIT_Session
SUIT_Session实际上可以看作整个程序的启动器,根据指定的动态链接库名称,调用动态链接库内的createApplication()来创建不同的SUIT_Application对象,进而通过虚函数SUIT_Application::start()来完成主窗口创建、模块加载等启动工作。
Ref. from SUIT_Session.h
The class Sesssion manages launching of Applications. Application must be returned by static function "createApplication" in external library. The Library must be loaded with loadLibrary method and after that application can be started.
2.2 CAM_Application
SUIT_Application用于窗口SUIT_Application::myDesktop与文档对象SUIT_Application::myStudy等的管理。
在SALOME中,主窗口菜单、工具栏等界面组件主要来自两部分:一部分是共用菜单与工具栏,这部分实际上是存储在SUIT_Application::myActionMap;另一部分是与当前模块相关的菜单、工具栏,这部分存储在CAM_Module::myActionMap。
STD_Application(派生于SUIT_Application)定义了文件打开、文件保存等共用菜单栏、工具栏,同时提供了视图管理功能。
CAM_Application(派生于STD_Application)主要提供了功能模块CAM_Module加载与管理的功能。
2.3 数据对象模型
SUIT_Study实际上就是文档对象,用于组织树状结构的数据对象SUIT_DataObject。另外,SUIT_Study提供了基于事务的文档修改的接口。
CAM_Study(派生于SUIT_Study)提供了对CAM_DataObject(派生于SUIT_DataObject)的支持。
2.4 主窗口
SUIT_Desktop定义了菜单管理器SUIT_Desktop::myMenuMgr与工具栏管理器SUIT_Desktop::myToolMgr。根据QAction的编排顺序,菜单管理器myMenuMgr与myToolMgr用于将QAction添加到菜单与工具栏。
/*!
\class SUIT_Desktop
Provides standard desktop: main window with
main menu manager, toolbars manager and logo.
*/
class SUIT_EXPORT SUIT_Desktop : public QtxMainWindow
{
Q_OBJECT
class ReparentEvent;
enum { Reparent = QEvent::User };
public:
SUIT_Desktop();
virtual ~SUIT_Desktop();
QtxActionMenuMgr* menuMgr() const;
QtxActionToolMgr* toolMgr() const;
QtxLogoMgr* logoMgr() const;
virtual SUIT_ViewWindow* activeWindow() const = 0;
virtual void setActiveWindow(SUIT_ViewWindow*);
virtual QList<SUIT_ViewWindow*> windows() const = 0;
int logoCount() const;
void logoClear();
void logoRemove( const QString& );
void logoInsert( const QString&, QMovie*, const int = -1 );
void logoInsert( const QString&, const QPixmap&, const int = -1 );
void emitActivated();
void emitMessage( const QString& );
signals:
void activated();
void deactivated();
// void moved();
void windowActivated( SUIT_ViewWindow* );
void closing( SUIT_Desktop*, QCloseEvent* );
void message( const QString& );
protected:
virtual bool event( QEvent* );
virtual void customEvent( QEvent* );
virtual void closeEvent( QCloseEvent* );
virtual void childEvent( QChildEvent* );
virtual void addWindow( QWidget* ) = 0;
private:
QtxActionMenuMgr* myMenuMgr;
QtxActionToolMgr* myToolMgr;
QtxLogoMgr* myLogoMgr;
};
另外,在STD模块中,派生了STD_SDIDesktop、STD_MDIDesktop、STD_TabDesktop等三种主窗口,STD_Application与CAM_Application默认使用STD_MDIDesktop类型主窗口(参见STD_Application构造函数)。
2.5 视图
SUIT_ViewWindow是所有视图的基类,SUIT_ViewManager、SUIT_ViewModel用于管理某一类型的视图,而STD_Application::myViewMgrs记录了LightApp_Application所支持的所有视图类型;同时每当激活一个Module时,变创建/激活对应的视图。
Ref. from SALOME GUI ArchitectureView Manager:Handles all the 3D or 2D View windows of the given type (OCC, VTK, Plot2d, etc) (2.9).View ModelResponsible for appearance and behavior of the View window (provide methods for displaying/erasing of objects, process user actions, like mouse clicks and keystrokes, processes selection of objects in the View window , handles popup menus, etc)View WIndowView frame that embeds 3D or 2D viewer in GUI
2.6 CAM_Module
CAM_Module不仅定义了一组菜单、工具栏等界面元素,而且维护了模块相关的对象数据结构。
/*!
\class CAM_Module
\brief Base implementation of the module in the CAM application architecture.
Provides support of menu/toolbars management.
*/
class CAM_EXPORT CAM_Module : public QObject
{
Q_OBJECT
public:
CAM_Module();
CAM_Module( const QString& );
virtual ~CAM_Module();
virtual void initialize( CAM_Application* );
QString name() const;
QString moduleName() const;
virtual QPixmap moduleIcon() const;
virtual QString iconName() const;
CAM_DataModel* dataModel() const;
CAM_Application* application() const;
virtual void contextMenuPopup( const QString&, QMenu*, QString& ) {};
virtual void updateCommandsStatus() {};
virtual void putInfo( const QString&, const int = -1 );
int showNotification(const QString& message, const QString& title, int timeout = -1);
void hideNotification(const QString& message);
void hideNotification(int id);
bool isActiveModule() const;
virtual void setMenuShown( const bool );
void setMenuShown( QAction*, const bool );
void setMenuShown( const int, const bool );
virtual void setToolShown( const bool );
void setToolShown( QAction*, const bool );
void setToolShown( const int, const bool );
virtual void updateModuleVisibilityState();
virtual bool activateOperation( int actionId );
virtual bool activateOperation( const QString& actionId );
virtual bool activateOperation( const QString& actionId, const QString& pluginName );
// actions/menu/toolbars management
QtxActionMenuMgr* menuMgr() const;
QtxActionToolMgr* toolMgr() const;
virtual QAction* action( const int ) const;
virtual int actionId( const QAction* ) const;
virtual QAction* createAction( const int, const QString&, const QIcon&, const QString&,
const QString&, const int, QObject* = 0,
const bool = false, QObject* = 0, const char* = 0, const QString& = QString() );
virtual QAction* createAction( const int, const QString&, const QIcon&, const QString&,
const QString&, const QKeySequence&, QObject* = 0,
const bool = false, QObject* = 0, const char* = 0, const QString& = QString() );
QtxActionGroup* createActionGroup( const int, const bool = true );
int createTool( const QString&, const QString& = QString() );
int createTool( const int, const int, const int = -1 );
int createTool( const int, const QString&, const int = -1 );
int createTool( QAction*, const int, const int = -1, const int = -1 );
int createTool( QAction*, const QString&, const int = -1, const int = -1 );
void clearTool( const QString& title );
int createMenu( const QString&, const int, const int = -1, const int = -1, const int = -1,QMenu * = 0);
int createMenu( const QString&, const QString&, const int = -1, const int = -1, const int = -1 );
int createMenu( const int, const int, const int = -1, const int = -1 );
int createMenu( const int, const QString&, const int = -1, const int = -1 );
int createMenu( QAction*, const int, const int = -1, const int = -1, const int = -1 );
int createMenu( QAction*, const QString&, const int = -1, const int = -1, const int = -1 );
static QAction* separator();
public slots:
virtual bool activateModule( SUIT_Study* );
virtual bool deactivateModule( SUIT_Study* );
virtual void connectToStudy( CAM_Study* );
virtual void studyClosed( SUIT_Study* );
virtual void studyChanged( SUIT_Study*, SUIT_Study* );
virtual void onApplicationClosed( SUIT_Application* );
private slots:
void onInfoChanged( QString );
protected:
virtual bool isSelectionCompatible();
virtual CAM_DataModel* createDataModel();
void setName( const QString& );
virtual void setModuleName( const QString& );
int registerAction( const int, QAction* );
bool unregisterAction( const int );
bool unregisterAction( QAction* );
// IMN 05/03/2015: we copied myActionMap for reset/unset actions accelerator keys
// after activate/deactivate modules
QMap<QAction*, QKeySequence> myActionShortcutMap; //!< copy actions shortcut map
virtual bool abortAllOperations();
private:
CAM_Application* myApp; //!< parent application object
QString myName; //!< module title (user name)
QPixmap myIcon; //!< module icon
QString myInfo; //!< latest info message
CAM_DataModel* myDataModel; //!< data model
QMap<int, QAction*> myActionMap; //!< menu actions
bool myMenuShown; //!< menu shown flag
bool myToolShown; //!< tool shown flag
friend class CAM_Application;
};
通过重写CAM_Module::initialize()函数可以将创建的QAction对象添加到CAM_Module::myActionMap,同时将其注册到主窗口SUIT_Desktop的菜单管理器SUIT_Desktop::myMenuMgr与工具栏管理器SUIT_Desktop::myToolMgr,进而将这些QAction对象添加到主窗口。
CAM_DataModel实现了树状层次结构的数据对象模型,每个CAM_Module将创建的CAM_DataObject添加到CAM_DataModel::myDataModel。
三、总结与讨论
按照研究问题的顺序,笔者抛出以下问题供大家分析与讨论。
Q1. 试分析SALOME启动器的工作原理与优缺点。
Q2. 总结CAM_Application及其子类的创建过程?
Q3. Module是何时加载的?
A3. 在CAM_Application创建时读取module列表,SUIT_Session::startApplication()会调用CAM_Application::start(),进而调用CAM_Application::loadModules()加载各个模块。
Q4. 在GUI模块中,toolbar、menu等何时创建与更新?
整个界面的toolbar、menu等界面元素来自两部分:
Q5. toolbar与menu是如何出发命令响应的?
Q6. 命令Redo/Undo是如何实现的?
Q7. 每个module都有一个数据对象模型,其与study对象关系是怎样的?
Q8. ViewManager是何时创建的,怎样添加不同的Viewer?
Q9. 切换Module时, SALOME时如何更新菜单、工具栏、停靠窗口、视图等界面元素的?
A9. 切换Module,会调用LightApp_Application::activateModule完成界面元素的更新。
updateModuleActions()更新菜单与工具栏;updateWindows()更新停靠窗口;updateViewManagers()更新视图;
/*!Activate module by \a modName*/
bool LightApp_Application::activateModule( const QString& modName )
{
QString actName;
CAM_Module* prevMod = activeModule();
if ( prevMod )
actName = prevMod->moduleName();
QString name = modName;
if ( !name.isEmpty() && !moduleTitle( modName ).isEmpty() )
name = moduleTitle( modName );
if ( actName == name )
return true;
putInfo( tr( "ACTIVATING_MODULE" ).arg( name ) );
saveDockWindowsState();
if ( infoPanel() )
infoPanel()->clear();
bool status = CAM_Application::activateModule( name );
updateModuleActions();
putInfo( "" );
if ( !status )
return false;
updateWindows();
updateViewManagers();
if ( activeStudy() && activeStudy()->root() && objectBrowser() ) {
if ( objectBrowser()->root() != activeStudy()->root() )
objectBrowser()->setRoot( activeStudy()->root() );
updateObjectBrowser( true );
}
if ( activeModule() ) activeModule()->updateModuleVisibilityState();
updateActions();
return true;
}
参考文献
Erich Gamma,Richard Helm,Ralph Johnson,John Vlissides. Design Patterns:elements of reusable object-oriented software. Addison Wesley, 1994.
Martin Fowler. Patterns of Enterprise Application Architecture. Addison Wesley, 2002.
网络资料
SALOMEhttps://www.salome-platform.org/
SALOME GUI Architecturehttps://docs.salome-platform.org/latest/extra/SALOME_GUI_Architecture.pdf
GUI Architectures https://martinfowler.com/eaaDev/uiArchs.html