AGB  ·  Datenschutz  ·  Impressum  







Anmelden
Nützliche Links
Registrieren
Zurück Delphi-PRAXiS Tutorials Delphi Property Sheets mit Delphi

Property Sheets mit Delphi

Ein Tutorial von MathiasSimmack · begonnen am 27. Jun 2002 · letzter Beitrag vom 12. Jan 2012
Antwort Antwort
Seite 1 von 2  1 2   
MathiasSimmack
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.
 
MathiasSimmack
 
#2
  Alt 27. Jun 2002, 11:07
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:
Code:
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.
  Mit Zitat antworten Zitat
MathiasSimmack
 
#3
  Alt 27. Jun 2002, 11:08
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:
Code:
library psheet;
...

{$R dialog.RES}

begin
end.
  Mit Zitat antworten Zitat
MathiasSimmack
 
#4
  Alt 27. Jun 2002, 11:10
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:
Code:
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:
Code:
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:
Code:
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.
  Mit Zitat antworten Zitat
MathiasSimmack
 
#5
  Alt 27. Jun 2002, 11:11
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:
Code:
if (DragQueryFile(StgMedium.hGlobal, $FFFFFFFF, nil, 0) = 1) then 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:
Code:
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 = nil) then 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, $ffffffff, nil, 0) - 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:
Code:
if(DragQueryFile(StgMedium.hGlobal, $ffffffff, nil, 0) = 0) then 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:
Code:
initialization
  SetLength(szOpenedTextfile,0);
finalization
  SetLength(szOpenedTextfile,0);
end.
  Mit Zitat antworten Zitat
MathiasSimmack
 
#6
  Alt 27. Jun 2002, 11:12
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:
Code:
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:
Code:
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:
Code:
var
  hPage : HPropSheetPage;
begin
  ...
  hPage           := CreatePropertySheetPage(aPSP);
  if(hPage <> nil) then
    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.
Code:
WM_INITDIALOG:
  begin
    if(length(szOpenedTextfile) > 0) then
      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":
Code:
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) > 0) then
              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:
Code:
WM_NOTIFY:
  if(PNMHDR(lp).code = PSN_APPLY) then
    MessageBox(0,'Übernehmen-Button geklickt','Information',
      MB_ICONINFORMATION or MB_OK);
  Mit Zitat antworten Zitat
MathiasSimmack
 
#7
  Alt 27. Jun 2002, 11:13
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:
Code:
initialization
  TTypedComObjectFactory.Create(ComServer, TPSheetTest, Class_PSheetTest,
    ciMultiInstance, tmApartment);
end.
Wir erstellen stattdessen aber einen neuen Typ auf folgender Basis:
Code:
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:
Code:
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:
Code:
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:
Code:
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:
Code:
regsvr32 /u psheet.dll
  Mit Zitat antworten Zitat
MathiasSimmack
 
#8
  Alt 27. Jun 2002, 11:15
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.
  Mit Zitat antworten Zitat
MathiasSimmack
 
#9
  Alt 1. Mai 2003, 17:20
Ich habe dieses Tutorial entfernt (auch aus den Tutorials), weil es kein echtes NonVCL war. Ich hätte euch stattdessen gern an die Demo von Andreas Kosch im Entwickler-Forum verwiesen, aber leider fiel sie einem Serverausfall, -crash o.ä. zum Opfer. Was es genau war, weiß ich nicht. Jedenfalls soll das Beispiel dort nicht mehr zu finden sein.

Darum habe ich mein altes "Tutorial" rausgekramt und stelle es hier inkl. Beispiel online. Allerdings ohne Support.

Gruß.
Angehängte Dateien
Dateityp: zip propsheet.zip (33,4 KB, 220x aufgerufen)
  Mit Zitat antworten Zitat
Benutzerbild von Codewalker
Codewalker

 
Delphi XE2 Professional
 
#10
  Alt 2. Jun 2011, 20:05
* Thread entstaub *

Mangels anderer Quellen die Frage: Weiß jemand, wie man das Nicht-Non-VCL umsetzt, so dass man den ganz normalen Form-Designer nutzen kann?
Thomas
  Mit Zitat antworten Zitat
Themen-Optionen Tutorial durchsuchen
Tutorial durchsuchen:

Erweiterte Suche
Ansicht

Forumregeln

Es ist dir nicht erlaubt, neue Themen zu verfassen.
Es ist dir nicht erlaubt, auf Beiträge zu antworten.
Es ist dir nicht erlaubt, Anhänge hochzuladen.
Es ist dir nicht erlaubt, deine Beiträge zu bearbeiten.

BB-Code ist an.
Smileys sind an.
[IMG] Code ist an.
HTML-Code ist aus.
Trackbacks are an
Pingbacks are an
Refbacks are aus

Gehe zu:

Impressum · AGB · Datenschutz · Nach oben
Alle Zeitangaben in WEZ +2. Es ist jetzt 12:54 Uhr.
Powered by vBulletin® Copyright ©2000 - 2021, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2021 by Daniel R. Wolf