Autor Beitrag
Tino
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Veteran
Beiträge: 9839
Erhaltene Danke: 45

Windows 8.1
Delphi XE4
BeitragVerfasst: So 17.08.03 15:38 
Property Sheets mit Delphi

Autor: MathiasSimmack

Inhalt:
  • Projekt-Grundlagen
  • Der Dialog für unser Projekt
  • Das COM-Objekt für unser Projekt
  • Den oder die Dateinamen herausfinden
  • Den Dialog anzeigen
  • Die Shell-Erweiterung im System registrieren
  • Das Schlusswort: Optimierungen


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
  • Bitte nicht vergessen, dass wir eine Erweiterung des OS entwickeln. Für einen Testlauf müssen wir sie also erst im System registrieren. Und natürlich muss sie danach wieder entfernt werden, damit wir z.B. eine neue Version erstellen können. Das Problem dürfte hierbei sein, dass das System die benutzte Datei auch nach der De-Registrierung nicht sofort löschen kann. Ein einfaches Ab- und wieder Anmelden genügt allerdings schon (zumindest unter Win98). Ein Neustart ist glücklicherweise nicht erforderlich.


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:
ausblenden 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
  • Besondere Eigenschaften oder Angaben in der Titelzeile sind unnötig. Später wird nur der Dialog-Inhalt (sozusagen die "Client Area") dargestellt. Es ist nur darauf zu achten, dass der Dialog nicht zu breit wird, weil sonst einige Elemente u.U. gar nicht mehr oder nicht vollständig zu sehen sind.

Nach dem Erstellen der Ressourcendatei (*.res) ist diese natürlich noch in den Projektquelltext aufzunehmen:
ausblenden 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:
  • Instantiierung = Mehrere Instanzen
  • Threading-Model = Apartment
  • Typbibliothek einschließen = Ja
  • Schnittstelle als OLE-Automation = Ja

(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:
ausblenden 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:
ausblenden 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:
ausblenden 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:
ausblenden 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:
ausblenden volle Höhe 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:
ausblenden 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:
ausblenden 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:
ausblenden 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:
ausblenden 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:
ausblenden 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.
ausblenden 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":
ausblenden 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:
ausblenden 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:
ausblenden 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:
ausblenden 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:
ausblenden 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
  • In unserem Beispiel greifen wir auf einen vorhandenen und bekannten Dateityp (*.txt = txtfile) zu. Wer einen eigenen Dateityp registrieren will, darf dessen Erweiterung natürlich nicht vergessen. Das Original-Beispiel von Andreas Kosch zeigt dieses Prinzip und registriert die Shell-Erweiterung für den Dateityp "OSTest" mit der Endung ".ost".

Okay, jetzt müssen wir den o.g. Initialisierungscode ändern, damit statt des Standards unsere neue Deklaration verwendet wird:
ausblenden 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:
ausblenden 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:
ausblenden 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.