Entwickler-Ecke

Delphi Tutorials - Property Sheets mit Delphi


Tino - So 17.08.03 15:38
Titel: Property Sheets mit Delphi
Property Sheets mit Delphi

Autor: MathiasSimmack [http://www.delphi-forum.de/profile.php?mode=viewprofile&u=432]

Inhalt:


In diesem Mini-Tutorial geht es um eine Gruppe der Shell-Erweiterungen: die sog. "Property Sheets", die bei einigen Dateitypen eingeblendet werden, wenn man deren Eigenschaften anzeigen lässt. Das gesamte Beispiel basiert auf der Grundlage von Andreas Kosch, die man im Entwickler-Forum downloaden kann. Ich will aber zusätzlich zeigen, welche Units man entfernen muss, damit am Ende eine Datei (DLL) von weniger als 100k entsteht.

Außerdem ist dieser erste Schritt (IMHO) unbedingt wichtig, da man nicht einfach ein vorhandenes, lauffähiges Projekt nehmen und für andere Zwecke erneut verwenden sollte. Das liegt daran, dass die meisten Shell-Erweiterungen intern eine eindeutige GUID benutzen, mit der sie später auch im System identifiziert werden.
Logische Folge: wenn ich zwei Shell-Erweiterungen verwende, die die selbe GUID haben, dann kommt es mit großer Wahrscheinlichkeit zu Problemen. Also, ich bitte darum, dieser Anleitung genau zu folgen und für jedes Projekt tatsächlich neuen Code zu erstellen. Die relevanten Code-Teile, die in jeder Erweiterung vorkommen, können dann natürlich problemlos kopiert und wiederverwendet werden.

Bevor wir uns an ein solches Projekt wagen, sollten wir uns natürlich darüber im Klaren sein, was wir erreichen wollen. Üblicherweise stellen wir auf den Eigenschaftenseiten ergänzende Informationen zur Verfügung, die beispielsweise auch von einem Programm angezeigt werden. Damit hat der Anwender die Möglichkeit, sich schnell über den Inhalt, den Typ, spezielle Eigenschaften usw. zu informieren, ohne erst das eigentlich passende Programm starten zu müssen.
In unserem Beispiel wollen wir eine zusätzliche Seite bei den Textdateien (*.txt) anzeigen lassen, die uns neben dem genauen Dateinamen auch eine Möglichkeit zum Öffnen des Texteditors bietet. Damit wird das Prinzip auf recht einfache Weise verdeutlicht.

Hinweis


Projekt-Grundlagen

Eine "Eigenschaftenseite" ist in den bekannten Windows-Systemen grundsätzlich als DLL ausgeführt, so dass wir für unser Projekt ebenfalls ein leeres DLL-Grundgerüst wählen müssen. Zu beachten ist allerdings, dass es sich dabei um eine sog. "ActiveX-Bibliothek" handelt, die wir über das Menü "Datei/Neu/ActiveX/ActiveX-Bibliothek" aufrufen können:

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
library psheet;
uses
  ComServ;

exports
  DllGetClassObject,
  DllCanUnloadNow,
  DllRegisterServer,
  DllUnregisterServer;

begin
end.

Standardmäßig wird auch die Anweisung "{$R *.res}" in den Quelltext eingetragen. Diesen Eintrag können wir entfernen, da wir ohnehin unseren eigenen Dialog ergänzen werden.

Der Dialog für unser Projekt

Schauen wir uns den Dialog doch gleich mal an! Da das nur ein Beispiel ist, machen wir es uns einfach und lassen uns nur den oder die Dateinamen in einer Listbox anzeigen. Des Weiteren kommt ein Button hinzu, der - durch Klick - die markierten Dateien mit "ShellExecute" startet.

Weil das Beispiel ja heruntergeladen werden kann, spare ich mir an der Stelle einfach mal Screenshot und RC-Quelltext.

Hinweis

Nach dem Erstellen der Ressourcendatei (*.res) ist diese natürlich noch in den Projektquelltext aufzunehmen:

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
library psheet;
...

{$R dialog.RES}

begin
end.


Das COM-Objekt für unser Projekt

Jetzt benötigen wir ein COM-Objekt, das wir über das Menü "Datei/Neu/ActiveX/COM-Objekt" in unsere leere DLL einfügen. Der tatsächliche Name und die Beschreibung hängt letzten Endes natürlich vom Einsatzzweck ab. Wichtig wäre aber, dass folgende Optionen unverändert übernommen werden:

(In der Offline-Version dieses Tutorials gibt es einen Screenshot des Dialogs, in dem man die Auswahl noch ein bisschen besser erkennen kann!)

So, damit haben wir den Grundlagen-Code, den wir bei jeder Shell-Erweiterung bitte auf diese Art erzeugen! Ich verweise noch mal auf meinen Satz am Anfang, in dem es um die eindeutigen GUIDs ging. Da wir jetzt diesen Grundlagen-Code mit eindeutigen GUIDs besitzen, können wir nun auch problemlos die benötigten Funktionen aus vorhandenen Shell-Erweiterungen nehmen und für unser jeweils aktuelles Projekt anpassen.

Das Dialogfenster der Typbibliothek können wir im Hintergrund verschwinden lassen. Uns interessiert nur der Code des COM-Objektes. Zunächst deklarieren wir die Unit "ShlObj.pas" und ergänzen unser COM-Objekt wie folgt:

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
type
  TPSheetTest = class(TTypedComObject, IPSheetTest,
    IShellExtInit, IShellPropSheetExt { <- beide Angaben ergänzen})
  protected
    {IPSheetTest-Methoden hier deklarieren}
  end;

Jetzt bekommen wir eine Fehlermeldung beim Kompilieren, weil wir "IShellExtInit" und "IShellPropSheetExt" deklariert, nicht aber deren Eigenschaften benutzt haben. Wir erweitern also:

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
protected
  function IShellExtInit.Initialize = ShellExtInitialize;
  function ShellExtInitialize(pidlFolder: PItemIDList;
    lpdobj: IDataObject; hKeyProgID: HKEY): HResult; stdcall;
  function AddPages(lpfnAddPage: TFNAddPropSheetPage;
    lParam: LPARAM): HResult; stdcall;
  function ReplacePage(uPageID: UINT; lpfnReplaceWith: TFNAddPropSheetPage;
    lParam: LPARAM): HResult; stdcall;
end;

Ebenfalls ergänzt werden muss die Unit "CommCtrl", da wir "TFNAddPropSheetPage" benutzt haben, bzw. benutzen müssen.

Eine dieser Funktion (Methoden) wollen wir bereits mit Leben füllen:

Delphi-Quelltext
1:
2:
3:
4:
5:
function TPSheetTest.ReplacePage(uPageID: UINT;
  lpfnReplaceWith: TFNAddPropSheetPage; lParam: LPARAM): HResult;
begin
  Result := E_NOTIMPL; // Dummy
end;

Die wird uns nämlich in unserem Beispiel nicht weiter interessieren. Es ist eine Dummy-Funktion, die wir allerdings deklarieren und mit Code versehen müssen. Andernfalls meckert der Compiler. Nur brauchen wir sie in unserem Beispiel nicht.

Den oder die Dateinamen herausfinden

Das Prinzip der Shell-Erweiterung ist folgendes: man markiert im Explorer eine Datei und wählt dann aus dem Kontextmenü die Eigenschaften dieser Datei. Wir brauchen also den Namen. Und das können aber auch mehrere sein, denn die mehrfache Auswahl ist im Windows-Explorer ja ebenfalls möglich.

Als Grundlage kann hier das "ShellExt"-Demo von Borland herangezogen werden. Dabei wird im Kontextmenü einer DPR-Datei der neue Punkt "Compile" erzeugt, der den Delphi-Compiler startet. Und in diesem Beispiel steht:

Delphi-Quelltext
1:
2:
3:
4:
if (DragQueryFile(StgMedium.hGlobal, $FFFFFFFFnil0) = 1then begin
  DragQueryFile(StgMedium.hGlobal, 0, FFileName, SizeOf(FFileName));
  Result := NOERROR;
end

Damit haben wir bereits beide Aufgaben erledigt: in der ersten Zeile wird demonstriert, wie man die Anzahl der Dateien bestimmt, und in der zweiten Zeile wird der erste Dateiname gelesen. Wir brauchen dazu aber die Unit "ShellAPI.pas", und dann schreiben wir unsere erste neue Methode:

Delphi-Quelltext
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:
var
  szOpenedTextfile : array of string;

function TPSheetTest.ShellExtInitialize(pidlFolder: PItemIDList;
  lpdobj: IDataObject; hKeyProgID: HKEY): HResult;
var
  StgMedium : TStgMedium;
  FormatEtc : TFormatEtc;
  i         : integer;
  FFileName : array[0..MAX_PATH] of Char;
begin
  Result := E_INVALIDARG;
  if(lpdobj = nilthen exit;

  with FormatEtc do
    begin
      cfFormat := CF_HDROP;
      ptd      := nil;
      dwAspect := DVASPECT_CONTENT;
      lindex   := -1;
      tymed    := TYMED_HGLOBAL;
    end;

  Result := lpdobj.GetData(FormatEtc, StgMedium); if Failed(Result) then exit;

  // Dateinamen lesen
  for i := 0 to DragQueryFile(StgMedium.hGlobal, $ffffffffnil0) - 1 do
    begin
      DragQueryFile(StgMedium.hGlobal, i, FFileName, sizeof(FFilename));
      // in das String-Array eintragen
      SetLength(szOpenedTextfile,i + 1);
      szOpenedTextfile[i] := FFileName;
    end;

  Result := NOERROR;
end;

Wer auf Nummer sicher gehen will, kann auch vorher prüfen ob überhaupt Dateien übergeben worden sind. Dazu genügt vor der for-Schleife folgende Zeile:

Delphi-Quelltext
1:
if(DragQueryFile(StgMedium.hGlobal, $ffffffffnil0) = 0then exit                    


Ach so: ich habe hier ein String-Array benutzt. Das hat den Vorteil, dass wir auf die Unit "Classes" und die TStringList verzichten können. Das kommt am Ende dann auch der Größe der DLL entgegen. Wir sollten das Array aber beim Start einmal initialisieren. Das Prinzip entspricht dabei etwa dem Erzeugen und Freigeben von richtigen Stringlisten:

Delphi-Quelltext
1:
2:
3:
4:
5:
initialization
  SetLength(szOpenedTextfile,0);
finalization
  SetLength(szOpenedTextfile,0);
end.


Den Dialog anzeigen

Um nun endlich unsere eigene Eigenschaftenseite anzeigen zu können, ziehen wir die Funktion "AddPages" heran, bei der wir zwei neue Variablen benötigen. Die eine Variable ist ein Handle auf die erzeugte Seite, die zweite ist eine Struktur, die wir zum Erzeugen der Seite benötigen. Mal ein bisschen Microsoft-Code:

Quelltext
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:
typedef struct _PROPSHEETPAGE {
    DWORD dwSize;                   // Größe der Struktur
    DWORD dwFlags;                  // Flags
    HINSTANCE hInstance;            // Instanz
    union {
        LPCSTR pszTemplate;         // Dialog-ID
        LPCDLGTEMPLATE pResource;
        };
    union {
        HICON hIcon;
        LPCSTR pszIcon;
        };
    LPCSTR pszTitle;                // Titel
    DLGPROC pfnDlgProc;             // Dialog-Funktion
    LPARAM lParam;
    LPFNPSPCALLBACK pfnCallback;
    UINT *pcRefParent;
#if (_WIN32_IE >= 0x0500)
    LPCTSTR pszHeaderTitle;
    LPCTSTR pszHeaderSubTitle;
#endif
#if (_WIN32_WINNT >= 0x0501)
    HANDLE hActCtx;
#endif
}

Nun interessiert uns nicht alles davon, wir brauchen nur:

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
var
  aPSP : TPropSheetPage;
begin
  fillchar(aPSP, sizeof(TPropSheetPage),#0);
  aPSP.dwSize      := sizeof(TPropSheetPage);
  aPSP.dwFlags     := PSP_USETITLE;
  aPSP.hInstance   := hInstance;
  aPSP.pszTemplate := MakeIntResource(IDD_PROPDLG);
  aPSP.pszTitle    := 'Beispielseite'// Titel der Seite
  aPSP.pfnDlgProc  := @propdlgproc;    // Dialogfunktion
  aPSP.pfnCallback := nil;
  aPSP.lParam      := 0;
end;

Jetzt können wir die Seite erstellen lassen, wozu wir die Funktion "CreatePropertySheetPage" benutzen:

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
var
  hPage : HPropSheetPage;
begin
  ...
  hPage            := CreatePropertySheetPage(aPSP);
  if(hPage <> nilthen
    if(lpfnAddPage(hPage,lParam) = FALSE) then
      DestroyPropertySheetPage(hPage);
end;


Die Dialog-Funktion

Unser Dialog benutzt eine typische Nachrichtenfunktion, in der wir Einfluss auf die Anzeige nehmen können. Diese Funktion ist NonVCL-Entwicklern sicher bekannt - andernfalls empfehle ich einen Blick in Luckies "NonVCL-Tutorials", wobei die Themen Dialogressourcen und Listbox besonders interessant sind, da wir beides in unserem Beispiel benötigen.

Schauen wir uns also mal an, wie die Dateinamen aus dem String-Array in unsere Listbox kommen.

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
WM_INITDIALOG:
  begin
    if(length(szOpenedTextfile) > 0then
      for i := 0 to length(szOpenedTextFile) - 1 do
        begin
          ZeroMemory(@buffer,sizeof(buffer));
          lstrcpy(buffer,pchar(szOpenedTextfile[i]));
          SendDlgItemMessage(hDlg,IDC_LISTBOX,
            LB_ADDSTRING,0,integer(@buffer));
        end;
  end;

Nichts weltbewegendes, denke ich. - Fehlt noch der Klick auf den Button: hier müssen wir herausfinden, ob und welche Dateien alles markiert sind, und dann übergeben wir sie einfach an "ShellExecute":

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
WM_COMMAND:
  if(HIWORD(wp) = BN_CLICKED) then
    case LOWORD(wp) of
      IDC_OPENBTN:
        begin
          // Anzahl der Einträge bestimmen
          items := SendDlgItemMessage(hDlg,IDC_LISTBOX,
            LB_GETCOUNT,0,0);

          // ausgewählte Einträge auslesen
          for i := 0 to items do
            if(SendDlgItemMessage(hDlg,IDC_LISTBOX,LB_GETSEL,i,0) > 0then
              begin
                ZeroMemory(@buffer,sizeof(buffer));
                SendDlgItemMessage(hDlg,IDC_LISTBOX,LB_GETTEXT,i,integer(@buffer));

                // und via "ShellExecute" starten
                ShellExecute(0,nil,buffer,nil,nil,SW_SHOWNORMAL);
              end;
        end;
    end;

Wer mit diesem Code nichts anfangen kann, sollte wirklich einen Blick in die erwähnten Tutorials werfen. Aber ich glaube, in so einem Fall ist eine Shell-Erweiterung vielleicht ein zu hartes Projekt als Einstieg.

Noch ein Wort zu "ShellExecute": Wie man sehen kann, verwende ich als zweiten Parameter nil. Ich überlasse also dem System die Entscheidung, was mit den Textdateien passiert. In den meisten Fällen sollte die Standardaktion aber "open" sein, was den Start eines Texteditors und die Anzeige der Datei bedeutet. Es wäre aber auch denkbar, dass die Standardaktion auf Drucken eingestellt ist ... was auch immer ... Also, Obacht!

Änderungen übernehmen

Schauen wir uns noch eine Eigenschaft unserer "Property Sheet" an, die in dem Fall zwar weniger wichtig ist, die aber ihr möglicherweise brauchen werdet: den "Übernehmen"-Button. Der ist normalerweise deaktiviert, aber durch irgendwelche Änderungen wird er aktiviert.
Soll die eigene Shell-Erweiterung auf den Klick dieses Buttons reagieren und irgendwelche Aktionen ausführen, dann ist dazu die Nachricht "WM_NOTIFY" abzufangen:

Delphi-Quelltext
1:
2:
3:
4:
WM_NOTIFY:
  if(PNMHDR(lp).code = PSN_APPLY) then
    MessageBox(0,'Übernehmen-Button geklickt','Information',
      MB_ICONINFORMATION or MB_OK);


Die Shell-Erweiterung im System registrieren

Eigentlich wäre unsere neue Eigenschaftenseite damit schon lauffähig. Allerdings müssen wir noch eine kleine Änderung vornehmen, damit wir die Shell-Erweiterung ganz gezielt für einen bestimmten Dateityp (in unserem Fall: die Textdateien) registrieren können. Dazu schauen wir uns den Initialisierungscode der Unit an, in der standardmäßig das steht:

Delphi-Quelltext
1:
2:
3:
4:
initialization
  TTypedComObjectFactory.Create(ComServer, TPSheetTest, Class_PSheetTest,
    ciMultiInstance, tmApartment);
end.

Wir erstellen stattdessen aber einen neuen Typ auf folgender Basis:

Delphi-Quelltext
1:
2:
3:
4:
5:
type
  TPSheetTestFactory = class(TComObjectFactory)
  public
    procedure UpdateRegistry(Register: Boolean); override;
  end;

In der Funktion "UpdateRegistry" können wir nun angeben, wo der Eintrag unserer "Property Sheet" vorgenommen werden soll:

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
procedure TPSheetTestFactory.UpdateRegistry(Register: Boolean);
const
  szTestExtension = 'txtfile\shellex\PropertySheetHandlers\';
begin
  inherited;
  if Register then
    CreateRegKey(szTestExtension + ClassName,'',GUIDToString(ClassID))
  else
    DeleteRegKey(szTestExtension + ClassName);
end;

Hinweis

Okay, jetzt müssen wir den o.g. Initialisierungscode ändern, damit statt des Standards unsere neue Deklaration verwendet wird:

Delphi-Quelltext
1:
2:
3:
4:
initialization
  TPSheetTestFactory.Create(ComServer, TPSheetTest, Class_PSheetTest,
    'PropertySheetTest''', ciMultiInstance, tmApartment);
end.

Fertig!

Die Shell-Erweiterung registrieren und benutzen

Registriert wird unsere Eigenschaftenseite durch einen Aufruf des Programms "regsvr32.exe", dem wir den Namen unserer DLL als Parameter übergeben:

Delphi-Quelltext
1:
regsvr32 psheet.dll                    

Wenn alles geklappt hat, sollte bei den Eigenschaften der Textdateien jetzt die neue Seite zu sehen sein. Die Auswahl der Dateien in der Listbox und der Button-Klick sollten natürlich auch funktionieren.


Die Shell-Erweiterung entfernen

Entfernt wird unser Shell-Erweiterung durch einen erneuten Aufruf von "regsvr32.exe", wobei wir aber diesmal zusätzlich den Parameter "/u" angeben:

Delphi-Quelltext
1:
regsvr32 /u psheet.dll                    


Das Schlusswort: Optimierungen

Unsere Beispiel-DLL ist damit fertig, aber leider auch ca. 315k groß. Das liegt hauptsächlich an einigen Units, auf die wir aber verzichten können. Auswirkungen auf die DLL und ihre Funktionalität hat das nicht - sie wird eben nur kleiner.

Die erste Änderung nehmen wir in der Unit "*_TLB.pas" vor. Der tatsächliche Dateiname richtet sich dabei nach dem Projektnamen, in meinem Fall also "psheet_TLB.pas". Hier können alle Units - bis auf die "ActiveX.pas" - ausgeklammert werden, und schon ist unsere DLL auf 81,5k geschrumpft.

Änderung #2 passiert in der Unit mit dem COM-Objekt-Code. In meinem Fall heißt sie "psheet_IMP.pas". Hier entfernen wir die Units "Classes.pas" und "StdVcl.pas", und unsere DLL ist nach dem Kompilieren nur noch 68k groß.


Im Gegensatz zu den 315k ein recht annehmbares Ergebnis. Da ich aber nur ein relativ simples Beispiel geschrieben habe, hängt die tatsächliche Größe eurer eigenen Shell-Erweiterungen natürlich davon ab, was ihr mit ihnen vorhabt. Außerdem bitte ich zu bedenken, dass mein Lösungsweg kein reines NonVCL ist. Möglich also, dass eure Shell-Erweiterungen etwas größer oder aber auch etwas kleiner werden als mein Beispiel.

Das war´s.
Fehlermeldungen und/oder Ergänzungen bitte hier rein, oder an mich!

Gruß,
Mathias.