{*******************************************************}
{                                                       }
{       Tocsg.Shell                                     }
{                                                       }
{       Copyright (C) 2022 kkuzil                       }
{                                                       }
{*******************************************************}

unit Tocsg.Shell;

interface

uses
  System.SysUtils, System.Classes, Winapi.Messages, Winapi.Windows,
  Vcl.Controls, Vcl.Graphics;

const
  TB_ENABLEBUTTON         = WM_USER + 1;
  TB_CHECKBUTTON          = WM_USER + 2;
  TB_PRESSBUTTON          = WM_USER + 3;
  TB_HIDEBUTTON           = WM_USER + 4;
  TB_INDETERMINATE        = WM_USER + 5;
  TB_MARKBUTTON           = WM_USER + 6;
  TB_ISBUTTONENABLED      = WM_USER + 9;
  TB_ISBUTTONCHECKED      = WM_USER + 10;
  TB_ISBUTTONPRESSED      = WM_USER + 11;
  TB_ISBUTTONHIDDEN       = WM_USER + 12;
  TB_ISBUTTONINDETERMINATE = WM_USER + 13;
  TB_ISBUTTONHIGHLIGHTED   = WM_USER + 14;
  TB_SETSTATE             = WM_USER + 17;
  TB_GETSTATE             = WM_USER + 18;
  TB_ADDBITMAP            = WM_USER + 19;
  TB_ADDBUTTONSA          = WM_USER + 20;
  TB_INSERTBUTTONA        = WM_USER + 21;
  TB_DELETEBUTTON         = WM_USER + 22;
  TB_GETBUTTON            = WM_USER + 23;
  TB_BUTTONCOUNT          = WM_USER + 24;
  TB_COMMANDTOINDEX       = WM_USER + 25;

  TB_SAVERESTOREA          = WM_USER + 26;
  TB_ADDSTRINGA            = WM_USER + 28;
  TB_GETBUTTONTEXTA        = WM_USER + 45;
//  TBN_GETBUTTONINFOA       = TBN_FIRST-0;

type
  TBBUTTON = packed record
    iBitmap: Integer;
    idCommand: Integer;
    fsState: BYTE;
    fsStyle: BYTE;
    bReserved: array [0..1] of BYTE;    // padding for alignment
    dwData: Pointer;                    //DWORD_PTR;
    iString: Pointer;                   //INT_PTR;
  end;

  TBBUTTON64 = packed record
    iBitmap: Integer;
    idCommand: Integer;
    fsState: BYTE;
    fsStyle: BYTE;
    bReserved: array [0..5] of BYTE;    // padding for alignment  64bit
    dwData: Pointer;                    //DWORD_PTR;
    iString: Pointer;                   //INT_PTR;
  end;

  TRAYDATA = packed record
    wnd: HWND;
    uID: UINT;
    uCallbackMessage: UINT;
    Reserved: array [0..1] of DWORD;
    hIcon: HICON;
  end;

function GetShellImageHandle(bSmall: Boolean = true; dwFileAttr: DWORD = 0): THandle;
function GetShellImageIndex_path(const sPath: String): Integer;

procedure OpenPath(const sPath: String; hParent: HWND = 0);
procedure ExecutePath(const sPath: String; sParam: String = ''; hParent: HWND = 0); inline;
procedure ExecutePath_hide(const sPath: String; sParam: String = ''); inline;
procedure ExecutePath_runAs(const sPath: String; sParam: String = '';
                nShowMode: Integer = SW_SHOWNORMAL);
procedure ExecuteExplorerOpen(const sPath: String); inline;

procedure ExplorerSelectedPath(sPath: String; bCheckFileExists: Boolean = true);

function GetShellExePathFromExt(sExt: String): String;
function GetTargetExeFromLink(const sPath: String): String;

function ClearZombieTray(hTray: HWND): Integer;
function AddFileSmallIconToImageList(aList: TImageList; const sPath: string): Integer;
function GetFileSmallIcon(const sPath: String): TIcon;

implementation

uses
  Winapi.ShellAPI, Tocsg.Exception, System.Win.Registry, Tocsg.Safe,
  Tocsg.Strings, Tocsg.Registry, Tocsg.Process, Winapi.ShlObj, Winapi.ActiveX,
  Tocsg.Trace;

function GetShellImageHandle(bSmall: Boolean = true; dwFileAttr: DWORD = 0): THandle;
var
  SFI: TSHFileInfo;
  dwIconSize: DWORD;
begin
  if bSmall then
    dwIconSize := SHGFI_SMALLICON
  else
    dwIconSize := SHGFI_LARGEICON;

  Result := SHGetFileInfo('', dwFileAttr, SFI, SizeOf(SFI), SHGFI_ICON or SHGFI_SYSICONINDEX or dwIconSize);
end;

// 11.1 기준, 64비트에서는 Range check error 에러가 뜬다...
// 10.x에서 만든 .dproj로 프로젝트를 다시 만들면 해결된다.. 델파이 버그...
// SearchLight.dproj로 해결함 22_0713 11:31:14 kku
function GetShellImageIndex_path(const sPath: String): Integer;
var
  SFI: TSHFileInfo;
  dwResult: DWORD;
begin
  Result := 0;

  try
    // 이걸로 체크하면 가상파일, 재문 분석이 필요한 파일 (드롭박스, 구글드라이브)의 경우
    // 파일이 바로 동기화 되어버린다. 23_0111 09:41:13 kku
//    if FileExists(sPath) or DirectoryExists(sPath) then

    if GetFileAttributes(PChar(sPath)) <> INVALID_FILE_ATTRIBUTES then
    begin
      ZeroMemory(@SFI, SizeOf(SFI));
      dwResult := SHGetFileInfo(PChar(sPath), 0, SFI, SizeOf(SFI),
//                                SHGFI_ICON or SHGFI_SYSICONINDEX or dwIconSize);
                                SHGFI_SYSICONINDEX); // 이렇게 해야 엄청 많이 시도했을때 out of system resources 오류 안난다... 22_0628 14:37:54 kku

//      if Succeeded(dwResult) then // 64 환경에서는 계속 실패 뜬다.. 그런데 SFI.iIcon 값은 잘 나옴 22_0705 17:02:16 kku
        Result := SFI.iIcon;
    end;
  except
    on E: Exception do
      ETgException.TraceException(E, 'Fail .. GetShellImageIndex_path()');
  end;
end;

procedure OpenPath(const sPath: String; hParent: HWND = 0);
//var
//  sCmd: String;
//  StrList: TStringList;
begin
//  Guard(StrList, TStringList.Create);
//
//  StrList.Add(Format('start "" "%s"', [sPath]));
//  StrList.Add('pause');
//
//  sCmd := 'C:\ProgramData\HE\test.cmd';
//  StrList.SaveToFile(sCmd, TEncoding.ANSI);
//  ShellExecute(hParent, nil, PChar(sCmd), nil, nil, SW_SHOWNORMAL);

// BSOne DRM 파일을 열람 하려면 start "" "경로" 이렇게 해야 실시간 복호화 열람이 가능하다
// 실행 기준 디렉토리를 일반사용자 권한으로 접근 가능한 곳으로 설정해야 정상 동작한다.
  ShellExecute(hParent, nil, 'cmd.exe', PChar(Format('/c start "" "%s"', [sPath])), 'C:\ProgramData\', SW_HIDE);
end;

procedure ExecutePath(const sPath: String; sParam: String = ''; hParent: HWND = 0);
begin
  ShellExecute(hParent, nil, PChar(sPath), PChar(sParam), '', SW_SHOWNORMAL);
end;

procedure ExecutePath_hide(const sPath: String; sParam: String = '');
begin
  ShellExecute(0, nil, PChar(sPath), PChar(sParam), '', SW_HIDE);
end;

procedure ExecutePath_runAs(const sPath: String; sParam: String = '';
                nShowMode: Integer = SW_SHOWNORMAL);
const
  RUN_AS: String = 'runas';
var
  ShellExecuteInfo: TShellExecuteInfo;
begin
  ZeroMemory(@ShellExecuteInfo, SizeOf(ShellExecuteInfo));
  with ShellExecuteInfo do
  begin
    cbSize := SizeOf(ShellExecuteInfo);
    fMask := SEE_MASK_FLAG_DDEWAIT or SEE_MASK_FLAG_NO_UI;
    Wnd := 0;
    lpVerb := PChar(RUN_AS);
    lpFile := PChar(Format('"%s"', [sPath]));
    lpParameters := PChar(sParam);
    lpDirectory := nil;
    nShow := nShowMode;
    hInstApp := 0;
  end;
  ShellExecuteEx(@ShellExecuteInfo);
end;

procedure ExecuteExplorerOpen(const sPath: String); inline;
begin
//  ShellExecute(0, 'open', PChar(sPath), PChar(sVPath), '', SW_SHOWNORMAL);
end;

procedure ExplorerSelectedPath(sPath: String; bCheckFileExists: Boolean = true);
begin
  if not bCheckFileExists or (bCheckFileExists and FileExists(sPath)) then
  begin
    sPath := '/select, "' + sPath + '"';
    ShellExecute(0, 'open', 'explorer.exe', PChar(sPath), nil, SW_SHOWNORMAL);
  end;
end;

function GetShellExePathFromExt(sExt: String): String;
var
  Reg: TRegistry;
  sVal,
  sUserSid: String;
  StrList: TStringList;
begin
  Result := '';
  if sExt = '' then
    exit;

  if sExt[1] <> '.' then
    sExt := '.' + sExt;

  sUserSid := GetProcesssUserSidFromName('explorer.exe');
  if sUserSid = '' then
    sUserSid := GetRegRecentUserSid;

  Guard(Reg, TRegistry.Create);

  sVal := '';
  if sUserSid <> '' then
  begin
    Reg.RootKey := HKEY_USERS;

    if Reg.OpenKeyReadOnly(Format('%s\SOFTWARE\Microsoft\Windows\CurrentVersion\' +
                  'Explorer\FileExts\%s\UserChoice', [sUserSid, sExt])) then
      sVal := Reg.ReadString('ProgId');

    Reg.CloseKey;
  end;

  Reg.RootKey := HKEY_CLASSES_ROOT;
  if sVal = '' then
  begin
    if not Reg.OpenKeyReadOnly(sExt) then
      exit;

    sVal := Reg.ReadString('');
    if sVal = '' then
      exit;

    Reg.CloseKey;
  end;

  if not Reg.OpenKeyReadOnly(sVal + '\shell\Open\command') then
    exit;

  sVal := Reg.ReadString('');
  if sVal = '' then
    exit;

  Guard(StrList, TStringList.Create);
  SplitString2(sVal, ' ', StrList);
  if StrList.Count > 0 then
    Result := StringReplace(StrList[0], '"', '', [rfReplaceAll]);
end;

function GetTargetExeFromLink(const sPath: String): String;
var
  psl: IShellLink;
  ppf: IPersistFile;
  info: array [0..MAX_PATH] of Char;
  wfs: TWin32FindData;
begin
  Result := '';

  try
    if not FileExists(sPath) then
      exit;

    CoCreateInstance(CLSID_SHELLLINK, nil, CLSCTX_INPROC_SERVER, IShellLink, psl);
    if (psl <> nil) and (psl.QueryInterface(IPersistFile, ppf) = 0) then
    begin
      ppf.Load(PWideChar(sPath), STGM_READ);
      psl.GetPath((@info), MAX_PATH, wfs, SLGP_UNCPRIORITY);
//      psl.GetPath((@info), MAX_PATH, wfs, SLGP_RELATIVEPRIORITY);
      Result := info;
//      if Result = '' then
//      begin
//        psl.GetPath((@info), MAX_PATH, wfs, SLGP_RELATIVEPRIORITY);
//        Result := info;
//      end;
    end;
  except
    on E: Exception do
      ETgException.TraceException(E, 'Fail .. GetTargetExeFromLink()');
  end;
end;

function ClearZombieTray(hTray: HWND): Integer;
var
  h: HWND;
  i, nCnt: Integer;
  dwTrayPid, dwPid: DWORD;
  hTrayProc: THandle;
  pData: ^TBBUTTON64; //^TBBUTTON;
  tb: TBBUTTON64; // TBBUTTON;
  tray: TRAYDATA;
  IconData: NOTIFYICONDATA;
  nRead: NativeUInt;
  sPName: String;
begin
  Result := 0;
  try
    nCnt := SendMessage(h, TB_BUTTONCOUNT, 0, 0);

    dwTrayPid := 0;
    GetWindowThreadProcessId(h, dwTrayPid);
    if dwTrayPid = 0 then
      exit;

    hTrayProc := OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwTrayPid);
    if hTrayProc = 0 then
      exit;

    try
      pData := VirtualAllocEx(hTrayProc, nil, sizeof(TBBUTTON), MEM_COMMIT, PAGE_READWRITE);
      if pData = nil then
        exit;

      for i := 0 to nCnt - 1 do
      begin
        SendMessage(h, TB_GETBUTTON, i, NativeUInt(pData));

        ReadProcessMemory(hTrayProc, pData, @tb, sizeof(TBBUTTON), nRead);
        ReadProcessMemory(hTrayProc, tb.dwData, @tray, sizeof(tray), nRead);

        dwPid := 0;
        GetWindowThreadProcessId(tray.Wnd, dwPid);
        if dwPid <> 0 then
        begin
  //        sPName := GetProcessNameByPid(dwPid);
  //        mmInfo.Lines.Add(sPName);
        end else begin
          Inc(Result);
  //        mmInfo.Lines.Add('삭제됨 : ' + IntToStr(tray.wnd));
  //        ZeroMemory(@IconData, SizeOf(IconData));
  //        IconData.cbSize := SizeOf(IconData);
  //        IconData.hIcon := tray.hIcon;
  //        IconData.Wnd := tray.wnd;
  //        IconData.uCallbackMessage := tray.uCallbackMessage;
  //        IconData.uID := tray.uID;
  //        Shell_NotifyIcon(NIM_DELETE, @IconData);
        end;
      end;
    finally
      if pData <> nil then
        VirtualFreeEx(hTrayProc, pData, 0, MEM_RELEASE);
      if hTrayProc <> 0 then
        CloseHandle(hTrayProc);
    end;
  except
    on E: Exception do
      ETgException.TraceException(E, 'Fail .. ClearZombieTray()');
  end;
end;

function AddFileSmallIconToImageList(aList: TImageList; const sPath: string): Integer;
var
  ico: TICon;
  FileInfo: TSHFileInfo;
  dwFlags: UINT;
begin
  Result := -1;

  try
    dwFlags := SHGFI_ICON or SHGFI_USEFILEATTRIBUTES;
    dwFlags := dwFlags or SHGFI_SMALLICON;
  //  Flags := Flags or SHGFI_LARGEICON;

    // SHGFI_USEFILEATTRIBUTES를 써야 실제 파일이 없어도 확장자만으로 아이콘을 얻을 수 있음
    if SHGetFileInfo(PChar(sPath), FILE_ATTRIBUTE_NORMAL, FileInfo, SizeOf(FileInfo), dwFlags) <> 0 then
    begin
      Guard(ico, TIcon.Create);
      ico.Handle := FileInfo.hIcon;
      Result := aList.AddIcon(ico);
    end;
  except
  // ..
  end;
end;

function GetFileSmallIcon(const sPath: String): TIcon;
var
  FileInfo: TSHFileInfo;
  dwFlags: UINT;
begin
  Result := nil;

  try
    dwFlags := SHGFI_ICON or SHGFI_USEFILEATTRIBUTES;
    dwFlags := dwFlags or SHGFI_SMALLICON;
  //  Flags := Flags or SHGFI_LARGEICON;

    // SHGFI_USEFILEATTRIBUTES를 써야 실제 파일이 없어도 확장자만으로 아이콘을 얻을 수 있음
    if SHGetFileInfo(PChar(sPath), FILE_ATTRIBUTE_NORMAL, FileInfo, SizeOf(FileInfo), dwFlags) <> 0 then
    begin
      Result := TIcon.Create;
      Result.Handle := FileInfo.hIcon;
    end;
  except
  // ..
  end;
end;

end.
