// -----------------------------------------------------------------------------
//
// TFolderBrowser-Klasse
// Copyright (c) 2003-2005 Delphi-Forum
// Tino, Popov, Christian Stelzman (PL), Luckie, Aton, Mathias Simmack (msi)
//
// basiert auf den folgenden Beiträgen
//   - http://www.delphi-forum.de/viewtopic.php?t=11240
//   - http://www.delphi-forum.de/viewtopic.php?t=21471
//   * http://www.delphi-forum.de/viewtopic.php?t=25302
//   - http://www.delphi-forum.de/viewtopic.php?t=27010&start=0
//
// -----------------------------------------------------------------------------

// -- Revision history ---------------------------------------------------------
//
//   * ursprüngliche Version von PL (s. Link #3)
//   * Fehlerkorrekturen von Luckie
//       - Result bei Callback ergänzt
//       - Properties als private deklariert
//       - Bugs in "Execute"-Methode behoben
//   * Dateifilter in Callback-Funktion
//       - Idee (Aton)
//       - globale Stringvariable (msi)
//       - TFolderBrowser-Klasse (PL)
//   * Unterstützung für mehrere Filter ergänzt (msi)
//   * Unterstützung für verschiedene Root-Ordner (msi)
//   * Änderungen bei den Properties (msi)
//       - "SelFolder" in "SelectedItem" umbenannt
//       - "FNewFolder" als "NewFolderButton" verfügbar
//       - "FShowFiles" als "ShowFiles" verfügbar
//       - "FNoTT" als "NoTargetTranslation" verfügbar (XP-Flag)
//   * Funktion zum Ermitteln von Verknüpfungszielen ergänzt (msi)
//       - Ergänzung, um Umgebungsvariablen umzuwandeln
//   * "InitFolder" (s. Create) umbenannt in "PreSelectedFolder" (PL)
//   * "FNoTT" (NoTargetTranslation) standardmäßig auf TRUE gesetzt,
//     damit alle Windows-Versionen, inkl. XP, gleich reagieren (msi)
//   * "CoInitializeEx" (Execute & TranslateLink) geändert (msi)
//   * "TranslateMsiLink" (PL, msi)
//        - ermittelt Pfad/Programm aus MSI-Verknüpfungen (Office, Openoffice)
//        - benötigt installierten MSI
//
// -----------------------------------------------------------------------------
unit FolderBrowser;


interface

uses
  ShlObj, ActiveX, Windows, Messages;

type
  TFolderBrowser = class
  private
    // alles private gemacht; geht niemanden was an,
    // da nachträglicher Zugriff sinnlos (Luckie)
    FHandle      : THandle;
    FCaption     : string;
    FShowFiles   : boolean;
    FNewFolder   : boolean;
    FStatusText  : boolean;
    FNoTT        : boolean;
    FInitFolder  : string;
    FSelected    : string;
    FTop,
    FLeft        : integer;
    FPosChanged  : boolean;

    // mehrere Filter müssen durch #0 voneinander getrennt
    // werden, bspw. '*.txt'#0'*.*htm*'#0'*.xml'
    // der letzte Filter kann mit #0#0 enden, muss er aber
    // nicht, weil die Funktion "CheckFilter" diese beiden
    // Zeichen automatisch anhängt (Mathias)
    FFilter      : string;
    FRoot        : PItemIdList;
    procedure FreeItemIDList(var pidl: pItemIDList);
    procedure SetTopPosition(const Value: Integer);
    procedure SetLeftPosition(const Value: Integer);
  public
    constructor Create(Handle: THandle; const Caption: string;
      const PreSelectedFolder: string = ''; ShowFiles: Boolean = False;
      NewFolder: Boolean = False);
    destructor Destroy; override;
    function SetDefaultRoot: boolean;
    function SetRoot(const SpecialFolderId: integer): boolean; overload;
    function SetRoot(const Path: string): boolean; overload;
    function Execute: Boolean; overload;
    function TranslateLink(const LnkFile: string): string;
    function TranslateMsiLink(const LnkFile: string): string;
    property SelectedItem: string read FSelected;
    property Filter: string read FFilter write FFilter;
    property NewFolderButton: boolean read FNewFolder write FNewFolder;
    property ShowFiles: boolean read FShowFiles write FShowFiles;
    property StatusText: boolean read FStatusText write FStatusText;
    property NoTargetTranslation: boolean read FNoTT write FNoTT;
    property Top: integer read FTop write SetTopPosition;
    property Left: integer read FLeft write SetLeftPosition;
  end;

implementation


//
// erweiterte SHBrowseForFolder-Eigenschaften
// (Deklaration ist notwendig, weil u.U. nicht in jeder Delphi-Version
// bekannt und verfügbar)
//
const
  BIF_NEWDIALOGSTYLE     = $0040;
  BIF_USENEWUI           = BIF_NEWDIALOGSTYLE or BIF_EDITBOX;
  BIF_BROWSEINCLUDEURLS  = $0080;
  BIF_UAHINT             = $0100;
  BIF_NONEWFOLDERBUTTON  = $0200;
  BIF_NOTRANSLATETARGETS = $0400;
  BIF_SHAREABLE          = $8000;

  BFFM_IUNKNOWN          = 5;
  BFFM_SETOKTEXT         = WM_USER + 105; // Unicode only
  BFFM_SETEXPANDED       = WM_USER + 106; // Unicode only


// -- helper functions ---------------------------------------------------------

function fileexists(const FileName: string): boolean;
var
  Handle   : THandle;
  FindData : TWin32FindData;
begin
  Handle   := FindFirstFile(pchar(FileName),FindData);
  Result   := (Handle <> INVALID_HANDLE_VALUE);

  if(Result) then FindClose(Handle);
end;

function CheckFilter(const Path, Filter: string): boolean;
var
  p      : pchar;
begin
  // Standardergebnis
  Result := false;
  if(Path = '') or (Filter = '') then exit;

  // #0#0 an den Filter anhängen, damit später das Ende
  // korrekt erkannt wird
  p      := pchar(Filter + #0#0);
  while(p[0] <> #0) do
  begin
    // Datei mit entsprechendem Filter wurde gefunden, ...
    if(fileexists(Path + '\' + p)) then
    begin
    // ... Ergebnis auf TRUE setzen, und Schleife abbrechen
      Result := true;
      break;
    end;

    // ansonsten zum nächsten Filter
    inc(p,lstrlen(p) + 1);
  end;
end;

function SHGetIDListFromPath(const Path: string; out pidl: PItemIDList):
  boolean;
var
  ppshf        : IShellFolder;
  wpath        : array[0..MAX_PATH]of widechar;
  pchEaten,
  dwAttributes : Cardinal;
begin
  // Standardergebnis
  Result       := false;

  // IShellFolder-Handle holen
  if(SHGetDesktopFolder(ppshf) = S_OK) then
  try
    if(StringToWideChar(Path,wpath,sizeof(wpath)) <> nil) then
    begin
      // Pfadname in "PItemIdList" umwandeln
      ppshf.ParseDisplayName(0,nil,wpath,pchEaten,pidl,dwAttributes);
      Result   := pidl <> nil;
    end;
  finally
    ppshf      := nil;
  end;
end;

//
// "CreateComObject" (modifizierte Version; Mathias)
//
function CreateComObject(const ClassID: TGUID;
  out OleResult : HRESULT): IUnknown;
begin
  OleResult := CoCreateInstance(ClassID,nil,CLSCTX_INPROC_SERVER or
    CLSCTX_LOCAL_SERVER,IUnknown,Result);
end;

//
// "ExpandEnvStr"
//
function ExpandEnvStr(const szInput: string): string;
const
  MAXSIZE = 32768;
begin
  SetLength(Result,MAXSIZE);
  SetLength(Result,ExpandEnvironmentStrings(pchar(szInput),
    @Result[1],length(Result)));
end;



// -----------------------------------------------------------------------------
//
// TFolderBrowser-Klasse
//
// -----------------------------------------------------------------------------

function FolderCallback(wnd: HWND; uMsg: UINT; lp, lpData: LPARAM): LRESULT;
  stdcall;
var
  path : array[0..MAX_PATH + 1]of char;
  fb   : TFolderBrowser;
begin
  fb   := TFolderBrowser(lpData);

  case uMsg of
    // Dialog wurde initialisiert
    BFFM_INITIALIZED:
      begin
        // Ordner auswählen, ...
        if(fb.FInitFolder <> '') then
          SendMessage(wnd,BFFM_SETSELECTION,WPARAM(true),
          LPARAM(pchar(fb.FInitFolder)));

        // ... & OK-Button deaktivieren, wenn Filter benutzt werden
        SendMessage(wnd,BFFM_ENABLEOK,0,LPARAM(fb.FFilter = ''));
        // oder anders gesagt: OK-Button aktivieren, wenn keine
        // Filter benutzt werden. ;o)
        // (Mathias)

        // Dialog neu positionieren
        if(fb.FPosChanged) then
          SetWindowPos(wnd,0,fb.Left,fb.Top,0,0,SWP_NOSIZE or SWP_NOZORDER);
      end;
    BFFM_SELCHANGED:
      if(PItemIdList(lp) <> nil) and (fb.FFilter <> '') then
      begin
        // den aktuellen Pfadnamen holen, ...
        ZeroMemory(@path,sizeof(path));
        if(SHGetPathFromIdList(PItemIdList(lp),path)) then
        begin
        // ... & anzeigen
          SendMessage(wnd,BFFM_SETSTATUSTEXT,0,LPARAM(@path));

        // gibt´s Dateien mit dem Filter?
        // nur dann wird der OK-Button des Dialogs aktiviert
          SendMessage(wnd,BFFM_ENABLEOK,0,LPARAM(CheckFilter(path,fb.FFilter)));
        end;
      end;
  end;

  Result := 0; // von Luckie hinzugefügt, hatte ich vergessen (oops)
end;


constructor TFolderBrowser.Create(Handle: THandle; const Caption: string;
  const PreSelectedFolder: string = ''; ShowFiles: Boolean = False;
  NewFolder: Boolean = False);
begin
  FHandle     := Handle;
  FCaption    := Caption;
  FInitFolder := PreSelectedFolder;
  FShowFiles  := ShowFiles;
  FNewFolder  := NewFolder;
  FStatusText := true;
  FNoTT       := true;
  FFilter     := '';
  FRoot       := nil;
  FTop        := 0;
  FLeft       := 0;
  FPosChanged := false;
end;

destructor TFolderBrowser.Destroy;
begin
  // ggf. belegte "PItemIdList" freigeben
  if(FRoot <> nil) then
    self.FreeItemIdList(FRoot);

  inherited Destroy;
end;

procedure TFolderBrowser.SetTopPosition(const Value: integer);
begin
  FPosChanged := true;
  FTop        := Value;
end;

procedure TFolderBrowser.SetLeftPosition(const Value: integer);
begin
  FPosChanged := true;
  FLeft       := Value;
end;

function TFolderBrowser.SetDefaultRoot: boolean;
begin
  // altes Objekt freigeben
  if(FRoot <> nil) then
    self.FreeItemIDList(FRoot);

  // und alles zurücksetzen
  FRoot  := nil;
  Result := true;
end;

function TFolderBrowser.SetRoot(const SpecialFolderId: integer): boolean;
begin
  // altes Objekt freigeben
  if(FRoot <> nil) then
    self.FreeItemIDList(FRoot);

  // SpecialFolderId kann eine der CSIDL_*-Konstanten sein,
  //   CSIDL_DESKTOP
  //   CSIDL_STARTMENU
  //   CSIDL_PERSONAL
  //   ...
  // s. PSDK

  // neuen Root setzen
  Result := SHGetSpecialFolderLocation(FHandle,SpecialFolderId,FRoot) = S_OK;
end;

function TFolderBrowser.SetRoot(const Path: string): boolean;
begin
  // altes Objekt freigeben
  if(FRoot <> nil) then
    self.FreeItemIDList(FRoot);

  // neuen Root setzen
  Result := SHGetIDListFromPath(Path,FRoot);
end;

function TFolderBrowser.Execute: Boolean;
var
  hr           : HRESULT;
  BrowseInfo   : TBrowseInfo;
  pidlResult   : PItemIDList;
  DisplayName,
  Path         : array[0..MAX_PATH + 1]of char;
begin
  Result       := false;

  hr           := CoInitializeEx(nil,COINIT_APARTMENTTHREADED);
  // Wenn die COM-Bibliothek noch nicht initialisiert ist,
  // dann ist das Ergebnis S_OK; ist sie bereits initialisiert
  // ist sie S_FALSE
  if(hr = S_OK) or (hr = S_FALSE) then
  try
    // "BrowseInfo" mit Werten füllen
    ZeroMemory(@BrowseInfo,sizeof(BrowseInfo));
    BrowseInfo.hwndOwner      := FHandle;
    BrowseInfo.pidlRoot       := FRoot;
    BrowseInfo.pszDisplayName := @Displayname;
    BrowseInfo.lpszTitle      := pchar(FCaption);
    BrowseInfo.lpfn           := @FolderCallBack;

    // TFolderBrowser-Klasse als Referenz für Callback-Funktion
    // übergeben (PL)
    BrowseInfo.lParam         := LPARAM(self);

    // Flags
    if(FStatusText) then
      BrowseInfo.ulFlags      := BrowseInfo.ulFlags or BIF_STATUSTEXT;


    // BIF_USENEWUI sorgt dafür dass besagter Button immer angezeigt wird,
    // egal, ob BIF_BROWSEINCLUDEFILES gesetzt wird oder nicht, daher
    // rausgenommen (Luckie)
    if(FShowFiles) then
      BrowseInfo.ulFlags      := BrowseInfo.ulFlags or BIF_BROWSEINCLUDEFILES;

    // Button zum Erstellen neuer Ordner anzeigen? (Luckie, PL)
    if(FNewFolder) then
      BrowseInfo.ulFlags      := BrowseInfo.ulFlags or BIF_NEWDIALOGSTYLE
    else
      BrowseInfo.ulFlags      := BrowseInfo.ulFlags or BIF_NONEWFOLDERBUTTON;

    // Windows XP sucht automatisch die Verknüpfungsziele von
    // Shortcuts heraus; soll stattdessen aber der Name der
    // Verknüpfung angezeigt werden, ist das Flag BIF_NOTRANSLATETARGETS
    // erforderlich; Sinn macht es nur unter Windows XP
    if(FNoTT) then
      BrowseInfo.ulFlags      := BrowseInfo.ulFlags or BIF_NOTRANSLATETARGETS;
    // für die älteren Windows-Versionen gibt es mit der Funktion
    // "TranslateLink" (s. weiter unten) eine Entsprechung, um die
    // Ziele von Shortcuts zu ermitteln (Mathias)


    // Dialog aufrufen
    pidlResult := SHBrowseForFolder(BrowseInfo);
    if(pidlResult <> nil) then
    begin
      if(FSelected = '') then
        if(SHGetPathFromIdList(pidlResult,Path)) and
          (Path[0] <> #0) then
        begin
          FSelected := Path;
          Result    := true;
        end;

      self.FreeItemIdList(pidlResult);
    end;
  finally
    CoUninitialize;
  end;
end;

function TFolderBrowser.TranslateLink(const LnkFile: string): string;
var
  link       : IShellLink;
  hr         : HRESULT;
  afile      : IPersistFile;
  pwcLnkFile : array[0..MAX_PATH]of widechar;
  szData     : array[0..MAX_PATH]of char;
  FindData   : TWin32FindData;
begin
  // Standardergebnis
  Result     := '';
  link       := nil;
  afile      := nil;

  hr         := CoInitializeEx(nil,COINIT_APARTMENTTHREADED);
  if(hr = S_OK) or (hr = S_FALSE) then
  try
    // IShellLink-Interface erzeugen, ...
    link   := CreateComObject(CLSID_ShellLink,hr) as IShellLink;
    if(hr = S_OK) and (link <> nil) then
    begin
    // ... & Verknüpfung laden
      StringToWideChar(LnkFile,pwcLnkFile,sizeof(pwcLnkFile));
      afile := link as IPersistFile;

      if(afile <> nil) and
        (afile.Load(pwcLnkFile,STGM_READ) = S_OK) then
      begin
        ZeroMemory(@szData,sizeof(szData));

    // Pfad + Dateiname ermitteln, ...
        if(link.GetPath(szData,sizeof(szData),FindData,
          SLGP_RAWPATH) = S_OK) then
        begin
          SetString(Result,szData,lstrlen(szData));
    // ... & evtl. Umgebungsvariablen filtern
          Result := ExpandEnvStr(Result);
        end;
      end;
    end;
  finally
    if(afile <> nil) then afile := nil;
    if(link <> nil) then link := nil;

    CoUninitialize;
  end;
end;

procedure TFolderBrowser.FreeItemIDList(var pidl: pItemIDList);
var
  ppMalloc : iMalloc;
begin
  if(SHGetMalloc(ppMalloc) = S_OK) then
  try
    ppMalloc.Free(pidl);
    pidl     := nil;
  finally
    ppMalloc := nil;
  end;
end;


const
  MsiDllName                = 'msi.dll';

  INSTALLSTATE_ABSENT       =  2;    // uninstalled
  INSTALLSTATE_LOCAL        =  3;    // installed on local drive
  INSTALLSTATE_SOURCE       =  4;    // run from source, CD or net
  INSTALLSTATE_SOURCEABSENT = -4;    // run from source, source is unavailable
  INSTALLSTATE_NOTUSED      = -7;    // component disabled
  INSTALLSTATE_INVALIDARG   = -2;    // invalid function argument
  INSTALLSTATE_UNKNOWN      = -1;    // unrecognized product or feature

type
  INSTALLSTATE              = LongInt;

  TMsiGetShortcutTarget     = function(szShortcutTarget, szProductCode,
    szFeatureId, szComponentCode: PAnsiChar): uint; stdcall;
  TMsiGetComponentPath      = function(szProduct, szComponent: PAnsiChar;
    lpPathBuf: PAnsiChar; pcchBuf: pdword): INSTALLSTATE; stdcall;
var
  MsiGetShortcutTarget      : TMsiGetShortcutTarget = nil;
  MsiGetComponentPath       : TMsiGetComponentPath  = nil;
  MsiDll                    : dword = 0;

function TFolderBrowser.TranslateMsiLink(const LnkFile: string): string;
var
  ProductCode,
  FeatureId,
  ComponentCode : array[0..MAX_PATH]of char;
  Path          : array[0..MAX_PATH]of char;
  PathLen       : dword;
begin
  Result := '';
  if(@MsiGetShortcutTarget = nil) or (@MsiGetComponentPath = nil) then exit;

  ZeroMemory(@ProductCode, sizeof(ProductCode));
  ZeroMemory(@FeatureId, sizeof(FeatureId));
  ZeroMemory(@ComponentCode, sizeof(ComponentCode));

  if(MsiGetShortcutTarget(PAnsiChar(LnkFile), ProductCode, FeatureId,
    ComponentCode) = ERROR_SUCCESS) then
  begin
    ZeroMemory(@Path, sizeof(Path));
    PathLen := sizeof(Path);

    case MsiGetComponentPath(ProductCode, ComponentCode, Path, @PathLen) of
      INSTALLSTATE_LOCAL,
      INSTALLSTATE_SOURCE:
        SetString(Result, Path, lstrlen(Path));
    end;
  end;
end;


initialization
  MsiDll                     := GetModuleHandle(MsiDllName);
  if(MsiDll = 0) then MsiDll := LoadLibrary(MsiDllName);

  if(MsiDll <> 0) then
  begin
    MsiGetShortcutTarget     := GetProcAddress(MsiDll, 'MsiGetShortcutTargetA');
    MsiGetComponentPath      := GetProcAddress(MsiDll, 'MsiGetComponentPathA');

    if(@MsiGetShortcutTarget = nil) or
      (@MsiGetComponentPath  = nil) then
    begin
      FreeLibrary(MsiDll);
      MsiDll := 0;
    end;
  end;
finalization
  if(MsiDll <> 0) then FreeLibrary(MsiDll);
end.
