Zurueck zum Inhaltsverzeichnis
Kapitel 6 - Menüs und Eigenschaftsfenster
6.1 Ein Rahmen mit Menü wird erstellt
Ein Eigenschaftsfenster (Propertysheet) besteht aus mehreren auf der gleichen Fläche dargestellten Dialogseiten. Ein Beispiel ist das Eigenschaftsfenster "System" der "Systemsteuerung" von Windows 98:
Abb. 6.1: Das Eigenschaftsfenster "Eigenschaften von System" von Windows 98
Dieses Eigenschaftsfenster aus Abb. 6.1 umfaßt vier Dialogseiten, die durch die Registerkarten "Allgemein", "Geräte-Manager", "Hardwareprofile" und "Leistungsmerkmale" ausgewählt werden können. Innerhalb der einzelnen Dialogseiten findet man die von Dialogfenstern bekannten Steuerelemente. Das Verlassen des Eigenschaftsfensters erfolgt erfolgt in diesem Fall über OK, "Abbrechen" oder das Systemmenü.
Wenn Sie selbst solche Eigenschaftsfenster erstellen, erhalten Sie die volle Unterstützung der MFC. Das Eigenschaftsfenster erzeugt man mit Hilfe der MFC-Klasse CPropertySheet, die sich direkt von CWnd ableitet. Die Dialogseiten werden wie gewöhnliche Dialoge erstellt, jedoch nicht von der Klasse CDialog, sondern von CPropertyPage, einer spezialisierten Kindklasse von CDialog, abgeleitet.
Das Einbinden der Dialogseiten in das Eigenschaftsfenster erfolgt mit der Funktion void CPropertySheet::AddPage( CPropertyPage*pPage ). Diese Funktion fügt die mittels Zeiger angegebenen Seiten von links nach rechts zu, d.h. die aktuelle Seite wird jeweils rechts an die bestehenden Seiten angefügt.
Die Darstellung der Eigenschaftsseite erfolgt modal mit
virtual int CPropertySheet::DoModal()
oder moduslos mit
BOOL CPropertySheet::Create( CWnd* pParentWnd = NULL, DWORD dwStyle = (DWORD) –1, DWORD dwExStyle = 0 ).
Wir testen dies nun an einem kleinen Beispiel in der Praxis. Hierbei fügen wir unserem Eigenschaftsfenster drei Dialogseiten hinzu. Die Klasse des Eigenschaftsfensters werden wir CReg1 und die der Dialogseiten CReg1Seite1, CReg1Seite2 und CReg1Seite3 nennen. Der Aufruf des Eigenschaftsfensters soll über ein Menü ausgehend von einem SDI-Rahmenfenster erfolgen. Dies ist ein realistisches Szenario, da Eigenschaftsfenster oft zum Setzen einer größeren Zahl von Parametern eingesetzt werden. Hierzu ein Beispiel aus MS Excel, das man über das Menü Format / Zellen erreicht:
Abb. 6.2: Ein komplexes Eigenschaftsfenster aus MS Excel mit sechs Dialogseiten
Nun zu unserem Beispiel: Erstellen Sie mit dem Anwendungs-Assistenten(exe) ein Projekt mit Namen "Eigenschaftsfenster". In Schritt 1 wählen Sie "Einzelnes Dokument (SDI)" und deaktivieren bitte die Unterstützung der Dokument-/Ansichts-Architektur, damit die Klassenvielfalt gut überschaubar bleibt. In Schritt 2 und 3 übernehmen Sie die Standardvorgaben. In Schritt 4 deaktivieren Sie "Andockbare Symbolleiste". In den nächsten Schritten nehmen Sie keine Änderungen vor.
Nach dem Kompilieren erhalten Sie folgende SDI-Anwendung:
Abb. 6.3: Der Anwendungs-Assistent hat eine SDI-Anwendung erstellt
Nachdem nun der Anwendungsrahmen erstellt ist, fügen Sie einen Menü-Punkt für das Eigenschaftsfenster ein. Hierzu wechseln Sie zu den Ressourcen und öffnen unter dem Knoten "Menu" die einzige Menü-Ressource IDR_MAINFRAME. Jetzt fassen Sie mit der linken Maustaste das Fragezeichen und ziehen es ganz nach rechts. Zwischen den Menü-Punkten Datei, Bearbeiten, Ansicht und dem Fragezeichen ist nun eine Lücke entstanden. Auf diese Lücke doppelklicken Sie mit der linken Maustaste und erhalten folgendes "Eigenschaftsfenster" zur Eingabe:
Abb. 6.4: Wir fügen einen Menü-Titel ein
Wie in Abb. 6.4 gezeigt, nennen Sie den Menü-Titel "Eigenschaftsfenster". Eine ID oder einen Statuszeilentext können Sie nicht eingeben, da es sich hier um die Kopfzeile eines Popup-Menüs handelt. Schließen Sie dieses kleine Fenster und doppelklicken Sie nun mit der linken Maustaste auf die Lücke unter dem Menü "Eigenschaftsfenster". Geben Sie als ID nun ID_PS_MODAL, als Titel "Modale Darstellung" und als Statuszeilentext "Modale Darstellung eines Eigenschaftsfensters" ein.
Abb. 6.5: Wir fügen einen Menü-Punkt mit Statuszeilentext ein
Nun kommt der Klassenassistent ins Spiel. Wählen Sie im Menü den Punkt "Modale Darstellung" aus und geben Sie Strg + W ein. Der Klassenassistent meldet sich:
Abb. 6.6: Der Klassenassistent hilft bei der Zuordnung einer Member-Funktion zu einer Menü-ID
Lassen Sie sich an dieser Stelle Zeit und betrachten Sie dieses "Eigenschaftsfenster" des Klassenassistenten genau. Da Sie nun eine Funktion erstellen wollen, die beim Anwählen des Menü-Punktes ID_PS_MODAL ausgeführt wird, sind wir in der Dialogseite "Nachrichtenzuordnungstabellen" genau richtig. In der Auswahlliste "Klassenname" haben wir bisher die Auswahl zwischen vier Klassen:
Bei Objekt-IDs sollte ID_PS_MODAL angewählt sein. Nun doppelklicken Sie links auf COMMAND im Feld Nachrichten. Der Klassenassistent erstellt nun einen Vorschlag für den Namen der neuen Member-Funktion, den er aus ID_PS_MODAL ableitet. Hier sollte man gut überlegen, bevor man OK klickt.
Eine nachträgliche Namensänderung ist umständlich, da der Klassenassistent den Funktionsnamen an mehreren Stellen ablegt. Wir akzeptieren das vorgeschlagene "OnPsModal", da es ausreichend aussagekräftig ist. Ein Doppelklick in das untere Fenster auf die ausgewählte Member-Funktion, und Sie befinden sich mitten in der neuen Funktion (die TODO-Anweisung können Sie sofort entfernen).
void CChildView::OnPsModal()
{
}
Wenn Sie nun in der Deklaration der Klasse CChildView nachschauen, finden Sie folgenden Code, der für die neue Funktion wichtig ist:
class CChildView : public CWnd
{ ...
// Generierte Funktionen für
die Nachrichtentabellen
protected:
//{{AFX_MSG(CChildView)
afx_msg void OnPaint();
afx_msg void OnPsModal();
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};
Im Quellcode ChildView.cpp finden sich diese
Eintragungen:
...
BEGIN_MESSAGE_MAP(CChildView,CWnd
)
//{{AFX_MSG_MAP(CChildView)
ON_WM_PAINT()
ON_COMMAND(ID_PS_MODAL, OnPsModal)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
...
// Nachrichtenbehandlungsroutinen
von CChildView
...
void CChildView::OnPsModal()
{
}
Die Verknüpfung zwischen ID_PS_MODAL und der
Funktion OnPsModal(...) wird in der Message-Map mit ON_COMMAND(ID_PS_MODAL,
OnPsModal) hergestellt. Sie sehen, daß zwischen dem Aufruf
über
Menü und einem Aufruf durch die Maus, z.B. mit WM_LBUTTONDOWN,
kein
wesentlicher Unterschied besteht. Entscheidend ist hier nur die
Einbindung
der Menü-Ressource mit Hilfe des MFC-Makros ON_COMMAND( Menü-ID,
Funktion ). Zusätzlich sind Sie in der Wahl des Namens
für
die Funktion, abgesehen vom Vorschlag des Klassenassistenten,
völlig
frei. Trotz dieser Freiheit sei Ihnen empfohlen, den Vorschlag nur
geringfügig
abzuändern, denn die Idee, den Namen von den
Menü-Bezeichnungen
abzuleiten, ist sicher sinnvoll. Hierbei treten jedoch zum Teil
Verkürzungen
auf, die Sie ausbügeln sollten.
6.2 Die Dialogseiten werden erstellt
Der nächste Schritt ist einfach. Sie erstellen zunächst drei Dialog-Ressourcen. Gehen Sie hierzu zu den Ressourcen, klicken Sie mit der rechten Maustaste auf den Knoten Dialog und wählen Sie "Dialog einfügen". Nun können Sie die erste Dialogseite erstellen. Die ID belassen Sie bitte bei IDD_DIALOG1. Belassen Sie bitte auch die Größe des Feldes. Worauf kommt es nun bei einer Dialogseite für das Eigenschaftsfenster an? Zunächst entfernen Sie bitte die Steuerelemente OK und "Abbrechen". Fügen Sie zur Erkennung der Seite ein Textelement mit dem Titel "Dies ist Dialogseite 1" ein. Den Titel des Dialoges ändern Sie auf "Dialogseite 1". Dieser Titel dient später im Eigenschaftsfenster als "Kartenreiter". Unter Dialog-Eigenschaften / Formate ändern Sie den Stil auf "Untergeordnet" und den Rand auf "Dünn". Darüber hinaus deaktivieren Sie "Systemmenü". Bei Dialog-Eigenschaften / Weitere Formate setzen Sie ein Häkchen bei "Deaktiviert". Jetzt geben Sie Strg + W ein, um den Klassenassistent zu aktivieren. Bestätigen Sie "Neue Klasse erstellen" und geben Sie als Name "CReg1Seite1" ein. Vorsicht! Nicht schnell OK drücken. Sie müssen die Basisklasse von CDialog auf CPropertyPage abändern. Jetzt können Sie zweimal OK drücken, um den Klassenassistenten zu verabschieden. Schalten Sie kurz in die Klassenansicht um. Im Header Reg1Seite1.h finden Sie (ohne Kommentare des Assistenten) folgende Deklaration der von CPropertyPage abgeleiteten Klasse:
class CReg1Seite1 : public
CPropertyPage
{
DECLARE_DYNCREATE(CReg1Seite1)
public:
CReg1Seite1();
~CReg1Seite1();
enum { IDD =
IDD_DIALOG1
};
protected:
virtual void
DoDataExchange(CDataExchange*
pDX);
protected:
DECLARE_MESSAGE_MAP()
};
Vielleicht haben Sie sich schon über diese Zeile enum { IDD = IDD_DIALOG1 } gewundert. Hier wird mit Hilfe des C++-Schlüselwortes enum der symbolischen Konstante IDD der Wert von IDD_DIALOG1 zugeordnet. Hierdurch werden Header und Quellcode bezüglich der Ressourcen-ID entkoppelt, da diese im Header auf IDD normiert wird. Die eigentliche Kopplung erfolgt dann nur noch über die #include-Anweisung für die Header-Datei. Da der Assistent bei Dialogen immer so vorgeht, sollten Sie dieses Vorgehen akzeptieren, obwohl es nicht unbedingt notwendig ist.
Ansonsten finden Sie die Deklaration von Konstruktor und Destruktor sowie der Member-Funktion DoDataExchange(...), die den DDX-/DDV-Mechanismus mittels UpdateData(...) erlaubt. DECLARE_MESSAGE_MAP() deklariert die Nachrichtentabelle.
Das Makro DECLARE_DYNCREATE( Klasse ) erlaubt es, daß Objekte von Klassen, die von CObject abgeleitet sind, dynamisch zur Laufzeit erzeugt werden können (wichtig für die Serialisierung von Objekten). Im entsprechenden Quellcode der Klasse findet man dann das Makro IMPLEMENT_DYNCREATE. Der Assistent vergibt diese Paarung für Document-, View- und Frame-Klassen.
Nun zum Quellcode Reg1Seite1.cpp (ohne Kommentare des Assistenten):
#include "stdafx.h"
#include "Eigenschaftsfenster.h"
#include "Reg1Seite1.h"
IMPLEMENT_DYNCREATE(CReg1Seite1, CPropertyPage)
CReg1Seite1::CReg1Seite1() : CPropertyPage(CReg1Seite1::IDD) { }
CReg1Seite1::~CReg1Seite1() { }
void CReg1Seite1::DoDataExchange(CDataExchange* pDX)
{
CPropertyPage::DoDataExchange(pDX);
}
BEGIN_MESSAGE_MAP(CReg1Seite1, CPropertyPage)
END_MESSAGE_MAP()
Hier finden Sie die Baustelle (den Konstruktor) für ein Objekt der Klasse CReg1Seite1. Die drei #include-Anweisungen sind wichtig, da stdafx.h die Datei afxext.h einschließt. Diese enthält nämlich die für die Klassen CPropertyPage und CPropertySheet wichtige Anweisung #include <afxdlgs.h> mit den entsprechenden Deklarationen. Eigenschaftsfenster.h umfaßt "resource.h", und Reg1Seite1.h beinhaltet die Deklarationen für CReg1Seite1.
Der Konstruktor ruft den Basiskonstruktor CPropertyPage:: CPropertyPage(...) auf. In afxdlgs.h finden Sie hierzu folgende Varianten:
CPropertyPage();
CPropertyPage(UINT nIDTemplate,
UINT nIDCaption = 0);
CPropertyPage(LPCTSTR
lpszTemplateName,
UINT nIDCaption = 0);
Nun wiederholt sich das Spiel für die beiden
anderen Dialogseiten IDD_DIALOG2 und IDD_DIALOG3. Achten Sie auf die
Klassennamen
CReg1Seite2 und CReg1Seite3 sowie auf die Basisklasse CPropertyPage
anstelle
CDialog und auf die richtigen Eigenschaften der Dialog-Ressourcen.
6.3 Die Dialogseiten werden in das Eigenschaftsfenster eingebunden
Nun folgen die wesentlichen Schritte zur Erstellung des Eigenschaftsfensters. Wechseln Sie in die Klassenansicht und öffnen Sie bei "Eigenschaftsfenster Klassen" mit der rechten Maus das Menü zum Erstellen einer neuen Klasse.
Den Klassentyp belassen Sie auf MFC-Klasse. Als Name wählen Sie CReg1 und als Basisklasse CPropertySheet. Automatisierung: Keine. Lassen Sie uns die erzeugten Dateien anschauen. Zunächst die Header-Datei Reg1.h (bereinigte Darstellung):
class CReg1 : public
CPropertySheet
{
DECLARE_DYNAMIC(CReg1)
public:
CReg1(UINT
nIDCaption,
CWnd* pParentWnd = NULL, UINT iSelectPage = 0);
CReg1(LPCTSTR
pszCaption, CWnd* pParentWnd = NULL, UINT iSelectPage = 0);
public:
virtual ~CReg1();
DECLARE_MESSAGE_MAP()
};
Wesentlich ist hier die zweite Variante des Konstruktors. Er erwartet einen Titel für das Eigenschaftsfenster. Sie können auch mittels iSelectPage eine Dialogseite als obere Seite auswählen. Die Zählung beginnt links mit null.
Nun zur Quellcode-Datei Reg1.cpp (bereinigte Darstellung):
#include "stdafx.h"
#include "Eigenschaftsfenster.h"
#include "Reg1.h"
IMPLEMENT_DYNAMIC(CReg1,
CPropertySheet)
CReg1::CReg1(UINT nIDCaption, CWnd*
pParentWnd, UINT iSelectPage)
:CPropertySheet(nIDCaption,
pParentWnd,
iSelectPage) { }
CReg1::CReg1(LPCTSTR pszCaption,
CWnd* pParentWnd, UINT iSelectPage)
:CPropertySheet(pszCaption,
pParentWnd,
iSelectPage) { }
CReg1::~CReg1() { }
BEGIN_MESSAGE_MAP(CReg1,
CPropertySheet)
END_MESSAGE_MAP()
Sie sehen hier bewußt die von den Eintragungen des Assistenten bereinigte Darstellung, damit Sie sich auf den wesentlichen Code konzentrieren können. Darüber hinaus spüren Sie sicher, daß Sie dies auch per Hand erzeugen könnten.
Wichtig ist hier die zweite Variante des Konstruktors. Auch hier wird der Basiskonstruktor aufgerufen. In afxdlgs.h finden Sie hierzu folgende Varianten:
CPropertySheet();
CPropertySheet(UINT nIDCaption,
CWnd*
pParentWnd = NULL, UINT iSelectPage = 0);
CPropertySheet(LPCTSTR
pszCaption,
CWnd* pParentWnd = NULL, UINT iSelectPage = 0);
Bis zu diesem Zeitpunkt konnten Sie mit dem grafischen Ressourcen-Editor und den Assistenten arbeiten. Nun müssen Sie manuell den restlichen Programmcode generieren. Die wesentlichen Schritte sind nun folgende:
Wechseln Sie über die Klassenansicht zur zweiten Variante des Konstruktors CReg1::CReg1(...) und ergänzen Sie dort folgendes:
CReg1::CReg1(LPCTSTR
pszCaption,
CWnd* pParentWnd, UINT iSelectPage)
:CPropertySheet(pszCaption,
pParentWnd, iSelectPage)
{
AddPage(
&m_Seite1 );
AddPage(
&m_Seite2 );
AddPage(
&m_Seite3 );
}
Die Member-Variablen m_Seite... müssen natürlich in der Header-Datei entsprechend deklariert werden:
// Reg1.h : Header-Datei
#include "Reg1Seite1.h"
#include "Reg1Seite2.h"
#include "Reg1Seite3.h"
class CReg1 : public
CPropertySheet
{
...
// Attribute
public:
CReg1Seite1 m_Seite1;
CReg1Seite2 m_Seite2;
CReg1Seite3 m_Seite3;
...
Nun kommt der Schlußakkord, nämlich der Programmcode zur Darstellung des Eigenschaftsfensters in der Funktion CChildView::OnPsModal(). In ChildView.h müssen Sie jedoch zunächst #include "Reg1.h" einbinden, damit die Klasse CReg1 dort überhaupt bekannt ist:
...
#include "Reg1.h"
/////////////////////////////////////////////////////////////////////////////
// CChildView-Fenster
class CChildView : public
CWnd
{
...
Nun folgt der Programmcode für die modale Darstellung in Reg1.cpp:
void CChildView::OnPsModal()
{
CReg1
Eigenschaftsseite1( _T("Eigenschaftsfenster") );
Eigenschaftsseite1.DoModal();
}
Nach dem Speichern / Kompilieren sollten Sie folgendes Eigenschaftsfenster auf Knopfdruck (Menü) erhalten:
Abb. 6.7: Das Eigenschaftsfenster erscheint
Wie Sie sehen, erhält unser Eigenschaftsfenster automatisch drei Schaltflächen:
OK - Abbrechen - Übernehmen (z.Z.
noch
deaktiviert). Die Titelzeile der Dialogseiten wird als "Kartenreiter"
benutzt.
6.4 Es geht noch einfacher - Eigenschaftsfenster fast vollautomatisch
Es ist wichtig, daß Sie verstehen, wie Eigenschaftsfenster erstellt werden, damit Sie Änderungen durchführen können, wenn Sie dies möchten. Im vorherigen Beispiel haben Sie mit Hilfe des Klassenassistenten für jede Dialogseite eine eigene Klasse erstellt und diese dann in einem Eigenschaftsfenster mit eigener Klasse kombiniert. Vielleicht könnten Sie das ganze auch schon komplett von Hand erstellen, also ohne jeden Assistenten.
Nun werden Sie sehen, wie man ein Eigenschaftsfenster auf bequemere Weise zu einer bestehenden Anwendung hinzufügt. Der Schlüssel hierzu ist der Menübefehl "Dem Projekt hinzufügen".
Sie erproben dies an einem praktischen Beispiel. Nehmen Sie an, Sie wollen ein Eigenschaftsfenster mit neun Dialogseiten erstellen und dieses vom Menü aus aufrufen. Erstellen Sie mit dem MFC-Anwendungs-Assistent (exe) ein Projekt namens PS_mit_Gallery. Wählen Sie SDI und belassen Sie alle Standardeinstellungen. Nun kommt der entscheidende Schritt: Wählen Sie im Menü Projekt / Dem Projekt hinzufügen / Komponenten und Steuerelemente. Sie erhalten folgenden Auswahldialog:
Abb. 6.8: Das Eigenschaftsfenster wird über die Komponentensammlung eingefügt
Hier befinden Sie sich im Verzeichnis "Gallery". Sie wählen den Ordner "Visual C++ Components" und in der nachfolgenden Liste "Eigenschaftsblatt". Die Abfrage "Komponente ... einfügen?" beantworten Sie mit ja. Nun haben Sie die Auswahl zwischen "Eigenschaftsblatt" und "Assistent". Wählen Sie an dieser Stelle "Eigenschaftsblatt" (= Eigenschaftsfenster). Verändern Sie in der nächsten Abfrage nichts (ohne Vorschau, mit Unterstützungsmodus). Geben Sie im nächsten Dialog der Klasse C...View den Zugriff. Wählen Sie anschließend neun Dialogseiten, und akzeptieren Sie die Klassennamen. Schließen Sie die Komponentensammlung. In der Klassenansicht finden Sie nun neun von CPropertyPage und eine von CPropertySheet abgeleitete Klassen. In der Klasse C...View finden Sie die Deklaration und Definition einer Funktion OnProperties(), die den Code zur modalen Darstellung des Eigenschaftsfensters bereits enthält. Es fehlt nur noch ein Menüeintrag und die entsprechende Verknüpfung. Ergänzen Sie in der Menü-Ressource unter dem Fragezeichen den Punkt "PS zeigen". Hieraus wird automatisch der Bezeichner ID__PSZEIGEN erzeugt. Jetzt gibt es ein kleines Problem. Sie haben einen Bezeichner und eine Funktion. Sie müssen also nur noch die Verknüpfung herstellen. Es gibt nun zwei Wege. Beide sind leider nicht sonderlich elegant.
Methode 1:
Suchen Sie die Message-Map und geben Sie entgegen
allen Warnhinweisen (!) die Zeile
ON_COMMAND( ID__PSZEIGEN, OnProperties )
ein:
BEGIN_MESSAGE_MAP(CPS_mit_GalleryView,
CView)
//{{AFX_MSG_MAP(CPS_mit_GalleryView)
// HINWEIS - Hier werden
Mapping-Makros
vom Klassen-Assistenten eingefügt und entfernt.
// Innerhalb dieser
generierten
Quelltextabschnitte NICHTS VERÄNDERN!
ON_COMMAND( ID__PSZEIGEN,
OnProperties )
//}}AFX_MSG_MAP
// Standard-Druckbefehle
ON_COMMAND(ID_FILE_PRINT,
CView::OnFilePrint)
ON_COMMAND(ID_FILE_PRINT_DIRECT,
CView::OnFilePrint)
ON_COMMAND(ID_FILE_PRINT_PREVIEW,
CView::OnFilePrintPreview)
END_MESSAGE_MAP()
Dieser Eingriff in die Integrität des Klassenassistenten ist nicht nur unschön, sondern es besteht auch die Möglichkeit von Fehleingaben.
Methode 2:
Aktivieren Sie innerhalb der Menü-Ressource
den Punkt "PS zeigen" und geben Sie Strg + W ein. Wählen Sie unter
Klassenname die C...View-Klasse. Unter Objekt-Ids muß ID_PSZEIGEN
aktiviert sein. Doppelklicken Sie unter Nachrichten auf COMMAND. Der
Klassenassistent
will die Funktion OnPszeigen() erzeugen. Akzeptieren Sie diesen Namen.
Nun verschieben Sie den Code aus OnProperties() einfach in die Funktion
OnPszeigen():
void
CPS_mit_GalleryView::OnPszeigen()
{
CMyPropertySheet
propSheet;
propSheet.DoModal();
}
Sie sollten jetzt noch aufräumen:
Löschen
Sie bitte die Funktion OnProperties() in der Klassenansicht C...View.
Wenn Sie nun Kompilieren / Starten durchführen,
zeigen beide Methoden Erfolg:
Abb. 6.9: Das Eigenschaftsfenster mit seinen (maximal) neun Seiten
Abgesehen vom Schlußakkord zeigt sich
VisualC++
hier von seiner feinen Seite. Sie sollen von Standardaufgaben entlastet
werden, damit Sie sich verstärkt der Ausgestaltung und
Funktionalität
des Eigenschaftsfensters zuwenden können.