Difference between revisions of "Component and Dialog"

From Apache OpenOffice Wiki
Jump to: navigation, search
m (Direct calls of all counter methods)
m (Appel direct de toutes les méthodes du compteur)
Line 128: Line 128:
 
We plan in this section to modify the C++ counter code to make it working with direct call.
 
We plan in this section to modify the C++ counter code to make it working with direct call.
  
==Appel direct de toutes les méthodes du compteur==
 
 
Dans cette section nous allons chercher à modifier le compteur pour qu'il fonctionne complètement par appel direct de toutes ses méthodes. Comme indiqué en fin de section précédente, cela veut dire que nos deux méthodes "setCount" et "getCount" n'auront plus de paramètres. Elles devront être capables de lire/écrire dans les contrôles de champs de textes de la boîte de dialogue. Ceci va avoir de profondes répercutions sur le code du compteur car il va nous falloir obtenir un  Service Manager (<idl>com.sun.star.lang.XMultiComponentFactory</idl>) pour être capable de lancer la boîte de dialogue à partir du code C++ et surtout d'obtenir l'interface <idl>com.sun.star.awt.XControlContainer</idl> pour accéder aux contrôles qui nous intéressent. Le [[Documentation/DevGuide/WritingUNO/C%2B%2B/Storing_the_Service_Manager_for_Further_Use|Developer's Guide]] considère que l'obtention d'un Service Manager se fait par l'intermédiaire du contexte. Nous allons donc d'abord résoudre ce problème.
 
Dans cette section nous allons chercher à modifier le compteur pour qu'il fonctionne complètement par appel direct de toutes ses méthodes. Comme indiqué en fin de section précédente, cela veut dire que nos deux méthodes "setCount" et "getCount" n'auront plus de paramètres. Elles devront être capables de lire/écrire dans les contrôles de champs de textes de la boîte de dialogue. Ceci va avoir de profondes répercutions sur le code du compteur car il va nous falloir obtenir un  Service Manager (<idl>com.sun.star.lang.XMultiComponentFactory</idl>) pour être capable de lancer la boîte de dialogue à partir du code C++ et surtout d'obtenir l'interface <idl>com.sun.star.awt.XControlContainer</idl> pour accéder aux contrôles qui nous intéressent. Le [[Documentation/DevGuide/WritingUNO/C%2B%2B/Storing_the_Service_Manager_for_Further_Use|Developer's Guide]] considère que l'obtention d'un Service Manager se fait par l'intermédiaire du contexte. Nous allons donc d'abord résoudre ce problème.
 
===Obtention du contexte===
 
===Obtention du contexte===

Revision as of 11:27, 25 May 2009

The goal of this chapter is to use a dialog to make our counter working. Because it's very easy to construct a dialog with OOoBasic we will use the OOoBasic Daialog editor and not the Dialog at runtime. As we will see along this chapter, two methods are now designed to facilitate the call of a component method with a dialog event. But befor going further we will first examine a method which consists to encapsulate C++ with OOoBasic.

Adding a Dialog to the Counter

For your information, we recall that our counter has four methods :

  1. increment
  2. decrement
  3. setCount
  4. getCount

We then design a Dialog with one button by method (then four buttons) and two text fields : O

  1. a text field to set a value in the counter (then associated with "setCount" button)
  2. a text field to get the value of the counter (then associated with "getCount" button)

Here is the corresponding dialog

Notre boîte de dialogue

To make the complete example working, you can simply use the following OOoBasic code :

'Listing 1
REM  *****  BASIC  *****
	Dim oSimpleComponent
	Dim oDialog
Sub demonstrateSimpleComponent
	oSimpleComponent = CreateUnoService( "foo.Counter" )
	'oInspector = createUnoService("org.openoffice.InstanceInspector")
	'oInspector.inspect(oSimpleComponent, "MyCounter")
	'XRay oSimpleComponent
	oDialog=CreateUnoDialog(DialogLibraries.Standard.Dialog1)
	oDialog.Execute()
	oDialog.dispose()
End Sub
 
Sub increment
  oSimpleComponent.increment()
End Sub
 
Sub decrement
   oSimpleComponent.decrement()
End Sub
 
Sub getCount
   Dim oTextField
   oTextField = oDialog.getControl("TextField2")
   oTextField.setText( oSimpleComponent.getCount())  
End Sub
 
Sub setCount
	Dim oTextField
	oTextField = oDialog.getControl("TextField1")
	'implicit conversion String to Integer
	oSimpleComponent.setCount(oTextField.getText())
End Sub

Because these procedures are created in OpenOffice.org Basic, you can assign them to an event required using the property window of the dialog editor. In this program we have left comment on different introspection uses in our counter but this is not important. Template:Documentation/Note

Direct Calls of methods with Introspection Service

As mentioned in Developer's Guide, since 2.0.4 version it is possible to bound methods of a component with events of a control in a Dialog. To put it differently, wrappers, as in the OOoBasic code below :

' Listing 2
REM  *****  BASIC  *****
Sub increment
  oSimpleComponent.increment()
End Sub

could be avoided now. But, for that, you have a price to pay : we have eventually to change our C++ code of our component. To begin with the more easy task, we will modify our OOoBasic program, our Dialog but not our counter.

Direct Call of Counter Methods without changing its C++ code

We have to change the previous OOobasic program to use the new com.sun.star.awt.DialogProvider2 service which provide the very interesting "createDialogWithHandler" method. Here is the corresponding code :

' Listing 3
REM  *****  BASIC  *****
	Dim oSimpleComponent
	Dim oDialog
Sub demonstrateSimpleComponent
	oSimpleComponent = CreateUnoService( "foo.Counter" )
	oCreateDialog2=CreateUnoService("com.sun.star.awt.DialogProvider2")
	'Thank you ms777 for the line below (see http://www.oooforum.org/forum/viewtopic.phtml?t=84168)
	oCreateDialog2.initialize(Array(ThisComponent))
	oDialog=oCreateDialog2.createDialogWithHandler("vnd.sun.star.script:Standard.Dialog1?location=document", _
		oSimpleComponent )
	oDialog.Execute()
	oDialog.dispose()
End Sub
 
'Sub increment
'  oSimpleComponent.increment()
'End Sub
 
Sub decrement
   oSimpleComponent.decrement()
End Sub
 
Sub getCount
   Dim oTextField
   oTextField = oDialog.getControl("TextField2")
   oTextField.setText( oSimpleComponent.getCount())  
End Sub
 
Sub setCount
	Dim oTextField
	oTextField = oDialog.getControl("TextField1")
	'implicit conversion String to Integer
	oSimpleComponent.setCount(oTextField.getText())
End Sub

As you can see, the OOoBasic Sub "increment" is now in comment and then cannot work. We have also to modify the Dialog for the "increment" button which have to call a method instead of a macro. This operation is explained in Developer's Guide. Template:Documentation/Note We can go further with the "decrement" method of the counter but not with the two last "setCount" and "getCount". The DialogProvider uses the com.sun.star.beans.Introspection service to detect if the method is provided by one of the interfaces supported by the component, but this only works if the corresponding method has one of both prototype below :

void [MethodName](void);

or

  void [MethodName] 
  ( 
      [in] com::sun::star::awt::XDialog xDialog, 
      [in] any aEvent 
  );

This is the case for our "increment" and "decrement" methods but not for the setCount and getCount methods.

Direct calls of all counter methods

We plan in this section to modify the C++ counter code to make it working with direct call.

Dans cette section nous allons chercher à modifier le compteur pour qu'il fonctionne complètement par appel direct de toutes ses méthodes. Comme indiqué en fin de section précédente, cela veut dire que nos deux méthodes "setCount" et "getCount" n'auront plus de paramètres. Elles devront être capables de lire/écrire dans les contrôles de champs de textes de la boîte de dialogue. Ceci va avoir de profondes répercutions sur le code du compteur car il va nous falloir obtenir un Service Manager (com.sun.star.lang.XMultiComponentFactory) pour être capable de lancer la boîte de dialogue à partir du code C++ et surtout d'obtenir l'interface com.sun.star.awt.XControlContainer pour accéder aux contrôles qui nous intéressent. Le Developer's Guide considère que l'obtention d'un Service Manager se fait par l'intermédiaire du contexte. Nous allons donc d'abord résoudre ce problème.

Obtention du contexte

L'obtention du contexte peut se faire à l'aide de quatre modifications de notre fichier source counter.cxx.

Ajout d'un champ

Pour récupérer le contexte on ajoute le champ

Reference< XComponentContext > m_xContext;

dans la classe du compteur. On rappelle qu'un contexte est une paire chaîne de caractères/valeur.

Remplacement du sous-programme d'initialisation

L'ancien sous-programme d'initialisation (qui appelle le constructeur de notre compteur) doit être remplacé. Il s'agissait de

// ancien sous programme a supprimer
Reference< XInterface > SAL_CALL MyCounterImpl_create(
    Reference< XComponentContext > const & xContext )
    SAL_THROW( () )
{
    return static_cast< XTypeProvider * >( new MyCounterImpl() );
}

que l'on change maintenant par

// nouveau sous-programme de remplacement
Reference< XInterface > SAL_CALL MyCounterImpl_createInstance(
    Reference< XComponentContext > const & xContext )
    SAL_THROW( () )
{
    return static_cast< ::cppu::OWeakObject * >( new MyCounterImpl( xContext ) );
}

Le nom de ce sous-programme n'a aucune importance, car il va être mis dans une structure très particulière (ou un tableau de ces structures). C'est ce que l'on se propose d'examiner maintenant.

Changement du tableau des entrées

On doit procéder au changement dans le tableau des entrées indiqué dans le commentaire ci-dessous

static struct ::cppu::ImplementationEntry s_component_entries [] =
{
    { // MyCounterImpl_create replaced by MyCounterImpl_createInstance 19/05/09
        MyCounterImpl_createInstance, getImplementationName_MyCounterImpl,
        getSupportedServiceNames_MyCounterImpl, ::cppu::createSingleComponentFactory,
        0, 0
    },
    { 0, 0, 0, 0, 0, 0 }
};

Changement du constructeur de la classe

Le constructeur de la classe doit être modifié car il possède un paramètre com.sun.star.uno.XComponentContext maintenant.

inline MyCounterImpl( Reference< XComponentContext > const & xContext)
        : m_xContext( xContext ) {
	//m_xMCF=m_xContext->getServiceManager();
	}

Ces quatres opérations fonctionnent, on peut mémoriser le contexte dans notre classe mais on n'a toujours pas le service manager !

Documentation caution.png J'ai essayé de réaliser un constructeur de classe du type
// doesn't work
Reference< XInterface > SAL_CALL MyCounterImpl_createInstance( const Reference< XMultiServiceFactory > & rSMgr)
	throw( Exception )
{
	return (cppu::OWeakObject*) new MyCounterImpl( rSMgr );
}

qui obtient directement un service Manager (comme dans les AddOn) mais sans succès. A explorer plus tard.

Obtention du Service Manager

Il existe en fait plusieurs Service Manager mais celui qu'il nous est possible de récupérer à partir du contexte d'un composant est une interface com.sun.star.lang.XMultiComponentFactory. L'interface XMultiComponentFactory peut être mémorisée en ajoutant un champ :

Reference< XMultiComponentFactory > m_xMCF;

à notre classe compteur. Il nous faut alors changer notre constructeur de la classe pour qu'il mémorise vraiment le Service Manager :

inline MyCounterImpl( Reference< XComponentContext > const & xContext)
        : m_xContext( xContext ) {
	m_xMCF=m_xContext->getServiceManager();
	}

Vous devez ajouter dans le code source une directive d'inclusion

#ifndef _COM_SUN_STAR_LANG_XMULTICOMPONENTFACTORY_HPP_
#include <com/sun/star/lang/XMultiComponentFactory.hpp>
#endif

mais rien dans le Makefile car si vous prenez soin de regarder le fichier CppComponent.uno.xml vous verrez bien apparaître l'interface com.sun.star.lang.XMultiComponentFactory et donc le fichier com/sun/star/lang/XMultiComponentFactory.hpp a été généré pour vous.

Prêt pour le grand saut

Nous sommes prêt à réaliser notre compteur autonome avec sa boîte de dialogue. Pour éviter un agacement du lecteur, nous allons cesser de détailler les modifications une par une et donner dans cette section les codes complets associés.

Fichier Counter.idl

J'ai beaucoup hésité sur la façon de procéder. Il y a un exemple dans le Developer's Guide détaillé en Java et j'ai pensé le traduire en C++ mais je n'ai pas réussi à traduire la ligne

// gros gros problemes avec le this !!!
XDialog xDialog = xDialogProvider.createDialogWithHandler( DialogURL, this );

Ce qui caractérise l'exemple c'est que c'est un composant qui affiche tout seul son menu. Moi je sais seulement l'afficher en OOoBasic. Je vais garder cet affichage en OOoBasic mais puisque mes sous programmes C++ devront lire les contrôles de texte, il faut que je passe une interface com.sun.star.awt.XDialog à mon composant. Notre nouveau fichier IDL va prendre en compte cette contrainte et respecte vraiment la signature des méthodes maintenant. Il devient donc :

#include <com/sun/star/uno/XInterface.idl>
#include <com/sun/star/awt/XDialog.idl>
 
module foo
{
       interface XCountable : com::sun::star::uno::XInterface
	{
		void setDialog( [in] ::com::sun::star::awt::XDialog xDialog);
		void getCount();
		void setCount();
		void increment();
		void decrement();
	};	
	service Counter : XCountable ;
};

où nous avons ajouté une méthode "setDialog". L'objectif de cette méthode est de passer notre interface XDialog au composant et par la même occasion de voir un peu comment on gère les types de paramètres un peu évolués. Examinons maintenant le code correspondant du compteur.

Fichier counter.cxx

Documentation caution.png Je suis resté plusieurs jours bloqué par un message d'erreur pas très clair qui clamait son impossibilité d'invoquer le constructeur de ma classe compteur. Le problème venait tout simplement de la signature de mon sous programme "setDialog" en C++. Donc regardez cela de très près si vous ne voulez pas être confronté à des problèmes : les "const" et "&" sont obligatoires dans le(s) paramètre(s).

Le code source est un peu long et fait appel aux interfaces com.sun.star.awt.XDialog, com.sun.star.awt.XTextComponent, com.sun.star.awt.XControl, com.sun.star.awt.XControlContainer, com.sun.star.lang.XMultiComponentFactory, com.sun.star.lang.XServiceInfo et com.sun.star.registry.XRegistryKey.

/***************************************************************************************************
 ***************************************************************************************************
 *
 * service implementation:	 foo.Counter
 * exported interfaces:		 foo.XCounter
 *
 * simple example component implementing a counter
 *
 ***************************************************************************************************
 **************************************************************************************************/
 
#include <rtl/ustring.hxx>
#include <rtl/string.hxx>
#include <cppuhelper/queryinterface.hxx> // helper for queryInterface() impl
#include <cppuhelper/factory.hxx> // helper for component factory
// New
#include <cppuhelper/implbase2.hxx> // "2" implementing three interfaces
#include <cppuhelper/implementationentry.hxx>
#include <com/sun/star/awt/XDialog.hpp>
#include <com/sun/star/awt/XTextComponent.hpp>
#include <com/sun/star/awt/XControl.hpp>
#include <com/sun/star/awt/XControlContainer.hpp>
// generated c++ interfaces
#include <com/sun/star/lang/XSingleServiceFactory.hpp>
#include <com/sun/star/lang/XMultiServiceFactory.hpp>
#include <com/sun/star/lang/XMultiComponentFactory.hpp>
#include <com/sun/star/lang/XServiceInfo.hpp>
#include <com/sun/star/registry/XRegistryKey.hpp>
#include <foo/XCountable.hpp>
 
 
#define SERVICENAME "foo.Counter"
#define IMPLNAME "com.sun.star.comp.example.cpp.Counter"
 
using namespace ::rtl;
using namespace ::osl;
using namespace ::cppu;
using namespace ::com::sun::star::awt;
using namespace ::com::sun::star::uno;
using namespace ::com::sun::star::lang;
using namespace ::com::sun::star::registry;
//using namespace ::com::sun::star::frame;
using namespace ::foo;
//==================================================================================================
namespace my_sc_impl
{
static Sequence< OUString > getSupportedServiceNames_MyCounterImpl()
{
	static Sequence < OUString > *pNames = 0;
	if( ! pNames )
	{
//		MutexGuard guard( Mutex::getGlobalMutex() );
		if( !pNames )
		{
			static Sequence< OUString > seqNames(1);
			seqNames.getArray()[0] = OUString(RTL_CONSTASCII_USTRINGPARAM(SERVICENAME));
			pNames = &seqNames;
		}
	}
	return *pNames;
}
 
static OUString getImplementationName_MyCounterImpl()
{
	static OUString *pImplName = 0;
	if( ! pImplName )
	{
//		MutexGuard guard( Mutex::getGlobalMutex() );
		if( ! pImplName )
		{
			static OUString implName( RTL_CONSTASCII_USTRINGPARAM(IMPLNAME) );
			pImplName = &implName;
		}
	}
	return *pImplName;
}
 
// New
class MyCounterImpl : public ::cppu::WeakImplHelper2<
      XCountable, XServiceInfo >
{
	// to obtain other services if needed
	Reference< XMultiComponentFactory > m_xMCF;
	Reference< XComponentContext > m_xContext;
	Reference< XDialog > m_xDialog;
	sal_Int32 m_nCount;
 
public:
// added this constructor 19/05/09
    inline MyCounterImpl( Reference< XComponentContext > const & xContext) throw ()
        : m_xContext( xContext ) {
	m_xMCF=m_xContext->getServiceManager();
	}
    virtual ~MyCounterImpl() {}
 
    // XServiceInfo specification
    virtual OUString SAL_CALL getImplementationName(  ) throw(RuntimeException);
    virtual sal_Bool SAL_CALL supportsService( const OUString& ServiceName ) throw(RuntimeException);
    virtual Sequence< OUString > SAL_CALL getSupportedServiceNames(  ) throw(RuntimeException);
    static Sequence< OUString > SAL_CALL getSupportedServiceNames_Static(  );
 
    // XCountable specification
    virtual void SAL_CALL getCount() throw (RuntimeException);
    virtual void SAL_CALL setCount() throw (RuntimeException);
    virtual void SAL_CALL increment() throw (RuntimeException);
    virtual void SAL_CALL decrement() throw (RuntimeException);
//    virtual OUString createDialog( const OUString & DialogURL/*, Reference< XModel > const & xModel, 
//					Reference< XFrame > const & xFrame*/ )throw (RuntimeException);
    virtual void setDialog( Reference< XDialog > const & xDialog )throw (RuntimeException);
};
// XCountable implementation
//*************************************************************************
void SAL_CALL MyCounterImpl::setDialog(Reference< XDialog > const & xDialog) throw (RuntimeException)
{
	m_xDialog=xDialog;
}
void SAL_CALL MyCounterImpl::getCount() throw (RuntimeException){
	Reference< XControlContainer > xControlContainer(m_xDialog,UNO_QUERY);
	Reference< XControl > xTextControl=xControlContainer->getControl(OUString::createFromAscii("TextField2"));
	Reference< XTextComponent > xTextComponent(xTextControl,UNO_QUERY);
	xTextComponent->setText(OUString::valueOf((sal_Int32)m_nCount));  
}
void SAL_CALL MyCounterImpl::setCount() throw (RuntimeException){
	Reference< XControlContainer > xControlContainer(m_xDialog,UNO_QUERY);
	Reference< XControl > xTextControl=xControlContainer->getControl(OUString::createFromAscii("TextField1"));
	Reference< XTextComponent > xTextComponent(xTextControl,UNO_QUERY);
	m_nCount=(xTextComponent->getText()).toInt32();  
}
 
void SAL_CALL MyCounterImpl::increment() throw (RuntimeException)
		{ ++m_nCount; }
void SAL_CALL MyCounterImpl::decrement() throw (RuntimeException)
		{ --m_nCount; }
// XServiceInfo	implementation
//*************************************************************************
OUString SAL_CALL MyCounterImpl::getImplementationName(  )
	throw(RuntimeException)
{
	return OUString( RTL_CONSTASCII_USTRINGPARAM(IMPLNAME) );
}
 
//*************************************************************************
sal_Bool SAL_CALL MyCounterImpl::supportsService( const OUString& ServiceName )
	throw(RuntimeException)
{
	return ServiceName.equalsAsciiL( RTL_CONSTASCII_STRINGPARAM(SERVICENAME) );
}	
 
//*************************************************************************
Sequence<OUString> SAL_CALL MyCounterImpl::getSupportedServiceNames(  )
	throw(RuntimeException)
{
	return getSupportedServiceNames_Static();
}
 
//*************************************************************************
Sequence<OUString> SAL_CALL MyCounterImpl::getSupportedServiceNames_Static(  )
{
	Sequence<OUString> names(1);
    names[0] = OUString(RTL_CONSTASCII_USTRINGPARAM(SERVICENAME));
    return names;
}
 
//*****New vesion with context management
Reference< XInterface > SAL_CALL MyCounterImpl_createInstance(
    Reference< XComponentContext > const & xContext )
    SAL_THROW( () )
{
    return static_cast< ::cppu::OWeakObject * >( new MyCounterImpl( xContext ) );
//	return static_cast< XTypeProvider * >( new MyCounterImpl( xContext) );
//	return static_cast< XTypeProvider * >( new MyCounterImpl() );
}
}
 
//##################################################################################################
//#### EXPORTED ####################################################################################
//##################################################################################################
 
/* New ****/
/* shared lib exports implemented without helpers in service_impl1.cxx */
 
namespace my_sc_impl
{
static struct ::cppu::ImplementationEntry s_component_entries [] =
{
    { // MyCounterImpl_create replaced by MyCounterImpl_createInstance 19/05/09
        MyCounterImpl_createInstance, getImplementationName_MyCounterImpl,
        getSupportedServiceNames_MyCounterImpl, ::cppu::createSingleComponentFactory,
        0, 0
    },
    { 0, 0, 0, 0, 0, 0 }
};
}
 
extern "C"
{
void SAL_CALL component_getImplementationEnvironment(
    sal_Char const ** ppEnvTypeName, uno_Environment ** ppEnv )
{
    *ppEnvTypeName = CPPU_CURRENT_LANGUAGE_BINDING_NAME;
}
sal_Bool SAL_CALL component_writeInfo(
    XMultiServiceFactory * xMgr, XRegistryKey * xRegistry )
{
    return ::cppu::component_writeInfoHelper(
        xMgr, xRegistry, ::my_sc_impl::s_component_entries );
}
void * SAL_CALL component_getFactory(
    sal_Char const * implName, XMultiServiceFactory * xMgr,
    XRegistryKey * xRegistry )
{
    return ::cppu::component_getFactoryHelper(
        implName, xMgr, xRegistry, ::my_sc_impl::s_component_entries );
}
}

Template:Documentation/Note

La partie OOoBasic

Le programme OOoBasic doit changer. On retire toutes les anciens sous-programmes et surtout on apelle le setDialog au bon moment. Voici donc le nouveau programme :

REM  *****  BASIC  *****
 
Sub demonstrateSimpleComponent
    Dim oSimpleComponent
	Dim oDialog
	oSimpleComponent = CreateUnoService( "foo.Counter" )
	oCreateDialog2=CreateUnoService("com.sun.star.awt.DialogProvider2")
	'Thank you ms777 for the line below (http://www.oooforum.org/forum/viewtopic.phtml?t=84168)
	oCreateDialog2.initialize(Array(ThisComponent))
	 oDialog=oCreateDialog2.createDialogWithHandler("vnd.sun.star.script:Standard.Dialog1?location=document", _
		oSimpleComponent, StarDesktop.getActiveFrame() )
	oSimpleComponent.setDialog(oDialog)	
	oDialog.Execute()
	oDialog.dispose()
End Sub
 
'Sub increment
'  oSimpleComponent.increment()
'End Sub
 
'Sub decrement
'   oSimpleComponent.decrement()
'End Sub
 
'Sub getCount
'   Dim oTextField
'   oTextField = oDialog.getControl("TextField2")
'   oTextField.setText( oSimpleComponent.getCount())  
'End Sub
 
'Sub setCount
'	Dim oTextField
'	oTextField = oDialog.getControl("TextField1")
	'implicit conversion String to Integer
'	oSimpleComponent.setCount(oTextField.getText())
'End Sub

où vous voyez le "setDialog" juste avant l'exécution de la boîte de dialogue. Tous les anciens sous-programmes sont en commentaires maintenant. N'oubliez pas non plus de modifier votre boîte de dialogue pour qu'elle n'appelle plus des sous-programmes OOoBasic mais des méthodes.


Il existe une autre méthode pour appeler les méthodes d'un composant. Il s'agit d'implanter une interface com.sun.star.awt.XDialogEventHandler dans notre composant. Je ne sais pas si cette méthode est plus efficace que l'utilisation de l'introspection.

This Document Home Page

HomePageCpp.png Return to Document Home page

See also

Personal tools