情况:
大家好,我使用 Inno Setup 5.5.6(u) 来创建我的设置。
它必须是 5.x 版本才能支持某些较旧的操作系统。
默认情况下,UAC 请求提升,如果单击“否”,它会以不提升的方式运行 - 我使用 Martin Prikryl 的第二个 解决方案来让它工作。
我之所以没有将其放在他的评论中,是因为这个主题对我来说似乎有点不同,而且对我来说它看起来更像是 Inno Setup 的普遍问题。
问题:
当没有管理员组成员资格的帐户运行代码并使用来自任何管理员帐户的凭据的“可选”UAC 时,会出现以下问题:Inno Setup 为您的管理员帐户保留“非管理安装模式”(例如卸载条目、桌面图标)。仅输入凭据。由于 IsAdminLoggedOn
,程序本身将很好地安装在 ProgramFiles 下。
任何其他用户(包括执行安装的受限用户帐户)都无法调用桌面图标或卸载。到目前为止我尝试过的:
那么,有人可以帮助我从 [Code] 中手动切换到管理安装模式吗?请不要提出以下建议,因为我已经尝试过但没有成功:
PrivilegesRequiredOverridesAllowed
PrivilegesRequired=admin
PrivilegesRequired=none
- 如果我使用它们,我将无法捕获用户拒绝的高度并提供未升高的安装。不起作用:命令行参数/ALLUSERS
Function IsAdminInstallMode
IsAdminLoggedOn
https://stackoverflow.com/a/65990073/22271111不起作用:预处理器解决方案目前也没有选择。我尝试设置
PrivilegesRequired={#PrivilegesRequired}
#define PrivilegesRequired "{code:VerifyAdmin}"
,具体取决于代码中的 IsAdminLoggedOn
是否为真。但后来我发现这个 [Setup] 设置是在安装程序中静态构建的,运行时的更改不会被识别(至少不会识别代码部分中的更改)。Inno 设置指南本身和谷歌结果没有解决这个问题(包括 Stackoverflow),这就是我在这里创建一个问题的原因。
如果一切顺利,如果运行时接受 UAC,Inno Setup 应该切换到管理安装模式(伪命令:set {#SetupSetting("PrivilegesRequired")} := 'admin')。如果我可以使用一些 [Code] 部分命令告诉编译器这样做,我会很高兴。
确实,运行时似乎没有办法在管理模式和非管理模式之间切换。但这里有一个针对最相关常量的解决方法。它包括
Martin Prikryl 的解决方案,但还有更多内容,似乎不属于最初的讨论:
; Written and compatible with Inno Setup 5.5.6 unicode
#define MyAppName "[GAMENAME]"
#define app "{pf}\" + MyAppPublisher + "\" + MyAppName + "\"
#define IfPrivilegesRequiredLowestTryAdmin_YES_NO "YES"
#define AppDirLimitToProgramFilesFolder_YES_NO "NO"
[Setup]
AppId={{A957445F-DB7F-4F72-974C-E2CDC11AA492}-{code:ElevationProgramName}-{code:GetAuthorIfUnelevated}
PrivilegesRequired=lowest
UninstallDisplayIcon={app}\{#MyAppExeName}
UninstallDisplayName={#MyAppName} {code:ElevationProgramName}
UsePreviousLanguage=no
[Icons]
;--- first/second entry if increased rights (from Vista / up to XP), third for if limited rights
;Start menu shortcut (to start)
Name: "{%programdata}\Microsoft\Windows\Start Menu\Programs\{#MyAppName}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Check: IsAdminLoggedOn; MinVersion: 6.0;
Name: "{code:startmenupath}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Check: IsAdminLoggedOn; OnlyBelowVersion: 6.0;
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Check: "not IsAdminLoggedOn";
;Start menu shortcut (for uninstalling)
Name: "{%programdata}\Microsoft\Windows\Start Menu\Programs\{#MyAppName}\{#MyAppName} {code:UninstallAppTitle}"; Filename: "{uninstallexe}"; IconFilename: "{app}\Icons\Uninstall.ico"; Check: IsAdminLoggedOn; MinVersion: 6.0;
Name: "{code:startmenupath}\{#MyAppName} {code:UninstallAppTitle}"; Filename: "{uninstallexe}"; IconFilename: "{app}\Icons\Uninstall.ico"; Check: IsAdminLoggedOn; OnlyBelowVersion: 6.0;
Name: "{group}\{#MyAppName} {code:UninstallAppTitle}"; Filename: "{uninstallexe}"; IconFilename: "{app}\Icons\Uninstall.ico"; Check: "not IsAdminLoggedOn";
;Desktop shortcut (to start)
Name: "{sd}\Users\Public\Desktop\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon; Check: IsAdminLoggedOn; MinVersion: 6.0;
Name: "{%allusersprofile}\Desktop\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon; Check: IsAdminLoggedOn; OnlyBelowVersion: 6.0;
Name: "{userdesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon; Check: "not IsAdminLoggedOn";
[CustomMessages]
ProgramDirErrorX64=The path entered must be a subdirectory of:%n"%1" OR "%2"
ProgramDirErrorX32=The path entered must be a subdirectory of:%n"%1"
UserDirError=Not allowed: "%1"%nRestart setup with restricted rights or change the path.
NoAdminAndOSTooOldforUAC=Elevated rights not available!%n%nSince Windows XP cannot start the UAC from the context, your account%nmust be a member of the Administrators group to be allowed to perform installations.%n%nThe installation wizard will now close.
NoAdminAndOSTooOldforUACOfferLimitedInstall=Elevated rights not available!%n%nSince Windows XP cannot start the UAC from the context, your account%nmust be a member of the Administrators group to be allowed to perform installations.%nThe program can also be installed without administrator rights for the current user only.%n%nWould you like to continue with this installation under the condition?
UninstallAppTitle=uninstall
___DummyThatLastMessageAtTheEndDoesNotHaveAnEmptyLine=Test
[Code]
// -------------------------------------------------------------
// VARIABLES THAT ARE IMPORTANT FOR [SETUP]
function IsFilenameValid(Path: string): Boolean;
begin
if (Pos('\', Path) + Pos('/', Path) + Pos(':', Path) + Pos('*', Path) + Pos(ExpandConstant('?'), Path) + Pos('"', Path) + Pos('<', Path) + Pos('>', Path) + Pos('|', Path)) = 0 then
begin
Result := True;
end
else
begin
Result := False;
end;
end;
function BeginsWith(SubText, Text: string): Boolean;
var
BeginStr: string;
begin
BeginStr := Copy(Text, 0, Length(SubText));
Result := (CompareText(SubText, BeginStr) = 0);
end;
function ElevationProgramName(Value: string): string;
begin
try
if (IsAdminLoggedOn) then
begin
Result := '(All Users)';
end
else
begin
Result := '(Current User)';
end;
except
end;
end;
procedure RegisterPreviousData(PreviousDataKey: Integer);
var
UninstallMode: string;
begin
// this will store the value under the specified key; except uninstaller you
// can read the values stored this way in installer
SetPreviousData(PreviousDataKey, 'UninstallMode', ElevationProgramName(UninstallMode));
end;
function WbemQuery(WbemServices: Variant; Query: string): Variant;
var
WbemObjectSet: Variant;
begin
Result := Null;
WbemObjectSet := WbemServices.ExecQuery(Query);
if not VarIsNull(WbemObjectSet) and (WbemObjectSet.Count > 0) then
begin
Result := WbemObjectSet.ItemIndex(0);
end;
end;
function RetrieveCurrentUsername: string;
var
Query: Variant;
WbemLocator: Variant;
WbemServices: Variant;
ComputerSystem: Variant;
WbemObjectSet: Variant;
begin
WbemLocator := CreateOleObject('WbemScripting.SWbemLocator');
WbemServices := WbemLocator.ConnectServer('.', 'root\CIMV2');
Query := 'SELECT UserName FROM Win32_ComputerSystem';
ComputerSystem := WbemQuery(WbemServices, Query);
if not VarIsNull(ComputerSystem) then
begin
Result := ExtractFileName(ComputerSystem.UserName);
//Userdomain := ExtractFilePath(ComputerSystem.UserName);
//StringChangeEx(Userdomain, '\', '', True);
end;
end;
function GetAuthorIfUnelevated(Value: string): string;
begin
if IsAdminLoggedOn then
begin
Result := '';
end
else
begin
Result := RetrieveCurrentUsername;
end;
end;
const
CSIDL_COMMON_PROGRAMS = $0017;
function startmenupath(Value: string): string;
begin
if not IsWindowsVistaOrNewer then
begin
Result := GetShellFolderByCSIDL(CSIDL_COMMON_PROGRAMS, True) + '\{#MyAppName}';
end;
end;
function UninstallAppTitle(Value: string): string;
begin
if IsFilenameValid(ExpandConstant('{cm:UninstallAppTitle}')) then
begin
Result := ExpandConstant('{cm:UninstallAppTitle}');
end
else
begin
Result := 'uninstall';
end;
end;
// -------------------------------------------------------------
// -------------------------------------------------------------
// ERLAUBE DIE INSTALLATION NUR BEI KORREKTEM PFAD
function AnythingAfterPrefix(S: string; Prefix: string): Boolean;
begin
Result :=
(Copy(S, 1, Length(Prefix)) = Prefix) and
(Length(S) > Length(Prefix));
end;
procedure OnDirEditChange(Sender: TObject);
var
MySecretDir1: string;
MySecretDir2: string;
Status2: string;
Status1: string;
SelectDirBrowseLabel: variant;
PathErrorX64: string;
PathErrorX32: string;
begin
if ('{#AppDirLimitToProgramFilesFolder_YES_NO}' = 'YES') then
begin
MySecretDir1 := ExpandConstant('{pf}');
if IsWin64 then MySecretDir2 := ExpandConstant('{pf64}');
SelectDirBrowseLabel := SetupMessage(msgSelectDirBrowseLabel);
// ↓ Name ↓ %1 ↓ %2
if IsWin64 then PathErrorX64 := FmtMessage(CustomMessage('ProgramDirErrorX64'), [ExpandConstant('{pf}'+'\'), ExpandConstant('{pf64}'+'\')]);
PathErrorX32 := FmtMessage(CustomMessage('ProgramDirErrorX32'), [ExpandConstant('{pf}'+'\')]);
// CONDITION TO CHECK IF THE SELECTED INSTALLATION PATH IS A SUBORDINATOR OF %ProgramFiles% OR %ProgramW6432%:
if WizardForm.NextButton.Enabled = True then Status1 := 'True';
if WizardForm.NextButton.Enabled = False then Status1 := 'False';
if not AnythingAfterPrefix(TrimRight(WizardDirValue), MySecretDir1) AND (WizardDirValue > MySecretDir1) then WizardForm.NextButton.Enabled := True;
WizardForm.NextButton.Enabled := Pos(MySecretDir1, WizardDirValue) OR Pos(MySecretDir2, WizardDirValue) = 1;
if not AnythingAfterPrefix(TrimRight(WizardDirValue), MySecretDir1 + '\') AND AnythingAfterPrefix(TrimRight(WizardDirValue + '\'), MySecretDir2) then WizardForm.NextButton.Enabled := False;
if not (WizardDirValue <= MySecretDir2) AND AnythingAfterPrefix(WizardDirValue, MySecretDir2 + '\') then Wizardform.NextButton.Enabled := True;
if WizardForm.NextButton.Enabled = True then Status2 := 'True';
if WizardForm.NextButton.Enabled = False then Status2 := 'False';
if (Status1 = 'True') AND (Status2 = 'False') then
begin
if IsWin64 then WizardForm.SelectDirBrowseLabel.Caption := PathErrorX64;
if not IsWin64 then WizardForm.SelectDirBrowseLabel.Caption := PathErrorX32;
WizardForm.SelectDirBrowseLabel.Font.Style := [fsBold]; // Use bold font
WizardForm.DirEdit.Color := clRed; // Use red background color for the input field of the program path
end;
if (Status1 = 'False') AND (Status2 = 'True') then
begin
WizardForm.SelectDirBrowseLabel.Caption := SelectDirBrowseLabel;
WizardForm.SelectDirBrowseLabel.Font.Style := []; // Use normal font
WizardForm.DirEdit.Color := $fffbff; // -Use -FontColor- for the input field of the program path (here: white)
end;
end;
if (IsAdminLoggedOn) AND not ('{#AppDirLimitToProgramFilesFolder_YES_NO}' = 'YES') then
begin
MySecretDir1 := ExtractFilePath(ExpandConstant('{%userprofile}'));
SelectDirBrowseLabel := SetupMessage(msgSelectDirBrowseLabel);
// ↓ Name ↓ %1 ↓ %2
PathErrorX32 := FmtMessage(CustomMessage('UserDirError'), [ExtractFilePath(ExpandConstant('{%userprofile}'))]);
// REQUIRE TO CHECK IF THE SELECTED INSTALLATION PATH IS SUBORDINATE TO %systemdrive%\Users\:
if WizardForm.NextButton.Enabled = True then Status1 := 'True';
if WizardForm.NextButton.Enabled = False then Status1 := 'False';
if BeginsWith(ExtractFilePath(ExpandConstant('{%userprofile}')), WizardDirValue) OR (Copy(ExtractFilePath(ExpandConstant('{%userprofile}')), 0, Length(ExtractFilePath(ExpandConstant('{%userprofile}'))) -1) = WizardDirValue) then
begin
WizardForm.NextButton.Enabled := False;
end
else
begin
WizardForm.NextButton.Enabled := True;
end;
if WizardForm.NextButton.Enabled = True then Status2 := 'True';
if WizardForm.NextButton.Enabled = False then Status2 := 'False';
if (Status1 = 'True') AND (Status2 = 'False') then
begin
WizardForm.SelectDirBrowseLabel.Caption := PathErrorX32;
WizardForm.SelectDirBrowseLabel.Font.Style := [fsBold]; // Use bold font
WizardForm.DirEdit.Color := clRed; // Use red background color for the input field of the program path
end;
if (Status1 = 'False') AND (Status2 = 'True') then
begin
WizardForm.SelectDirBrowseLabel.Caption := SelectDirBrowseLabel;
WizardForm.SelectDirBrowseLabel.Font.Style := []; // Use normal font
WizardForm.DirEdit.Color := $fffbff; // -Use -FontColor- for the input field of the program path (here: white)
end;
end;
end;
// -------------------------------------------------------------
// -------------------------------------------------------------
// IF IfPrivilegesRequiredLowestTryAdmin_YES_NO = "YES", TRY FIRST WITH ADMIN RIGHTS AND IF UAC = NO, START WITH USER RIGHTS
function IsWinVista: Boolean;
begin
Result := (GetWindowsVersion >= $06000000);
end;
function HaveWriteAccessToApp: Boolean;
var
FileName: string;
begin
FileName := AddBackslash(WizardDirValue) + 'writetest.tmp';
Result := SaveStringToFile(FileName, 'test', False);
if Result then
begin
Log(Format(
'Have write access to the last installation path [%s]', [WizardDirValue]));
DeleteFile(FileName);
end
else
begin
Log(Format('Does not have write access to the last installation path [%s]', [
WizardDirValue]));
end;
end;
procedure ExitProcess(uExitCode: UINT);
external '[email protected] stdcall';
function ShellExecute(hwnd: HWND; lpOperation: string; lpFile: string;
lpParameters: string; lpDirectory: string; nShowCmd: Integer): THandle;
external '[email protected] stdcall';
function Elevate: Boolean;
var
I: Integer;
RetVal: Integer;
Params: string;
S: string;
begin
{ Collect current instance parameters }
for I := 1 to ParamCount do
begin
S := ParamStr(I);
{ Unique log file name for the elevated instance }
if CompareText(Copy(S, 1, 5), '/LOG=') = 0 then
begin
S := S + '-elevated';
end;
{ Do not pass our /SL5 switch }
if CompareText(Copy(S, 1, 5), '/SL5=') <> 0 then
begin
Params := Params + AddQuotes(S) + ' ';
end;
end;
{ ... and add selected language }
Params := Params + '/ALLUSERS=YES ' + '/LANG=' + ActiveLanguage;
Log(Format('Elevating setup with parameters [%s]', [Params]));
RetVal := ShellExecute(0, 'runas', ExpandConstant('{srcexe}'), Params, '', SW_SHOW);
Log(Format('Running elevated setup returned [%d]', [RetVal]));
Result := (RetVal > 32);
{ if elevated executing of this setup succeeded, then... }
if Result then
begin
Log('Elevation succeeded');
{ exit this non-elevated setup instance }
//Beende Setup mit "Abort()" anstatt mit "ExitProcess(0)", damit temporäre Dateien trotzdme noch gelöscht werden (saubere Methode)
//ExitProcess(0);
Abort()
end
else
begin
Log(Format('Elevation failed [%s]', [SysErrorMessage(RetVal)]));
end;
end;
const
PathToRegistryUninstall = '\Software\Microsoft\Windows\CurrentVersion\Uninstall\' + '{#emit StringChange(SetupSetting("AppId"),"{{","{")}' + '_is1';
// -------------------------------------------------------------
procedure InitializeWizard;
var
NoClicked: boolean;
S: string;
Upgrade: Boolean;
RightsChecked: boolean;
message2: string;
WelcomeLabelCombinedText: string;
message: string;
WelcomeLabel3: TNewStaticText;
Userprofile: String;
begin
begin
WizardForm.DirEdit.OnChange := @OnDirEditChange;
if ('{#IfPrivilegesRequiredLowestTryAdmin_YES_NO}' = 'YES') then
begin
Upgrade :=
RegQueryStringValue(HKLM, PathToRegistryUninstall, '{app}', S) or
RegQueryStringValue(HKCU, PathToRegistryUninstall, '{app}', S);
{ elevate }
if not IsWinVista then
begin
Log(Format('This version of Windows [%x] does not support elevation', [
GetWindowsVersion]));
end
else
if IsAdminLoggedOn then
begin
Log('Running elevated');
//MsgBox(ExpandConstant('At this point -AdministrativeInstallMode- should have been set, if something like this had been possible: {#SetupSetting("PrivilegesRequired")}'), mbInformation, mb_OK);
end
else
begin
Log('Running non-elevated');
if Upgrade then
begin
if not HaveWriteAccessToApp then
begin
Elevate;
end;
end
else
begin
if not Elevate then
//UAC answered with "No".'
begin
if MsgBox(ExpandConstant(CustomMessage('UACDeniedOptionalUserMode')), mbConfirmation, MB_YESNO or MB_DEFBUTTON1) = IDNO then
begin
//Question about non-admin installation also answered with "No"
NoClicked := true
Abort()
end
else
WizardForm.DirEdit.Text := ExpandConstant('{localappdata}\{#MyAppName}');
Log(Format('Falling back to local application user folder [%s]', [
WizardForm.DirEdit.Text]));
end;
end;
end;
end;
// Treat such old OS's like Windows 2000/XP, which do not yet have a UAC for responding:
RightsChecked := not(IsAdminLoggedOn);
if (RightsChecked = true) AND (not IsWindowsVistaOrNewer) then
begin
if ('{#IfPrivilegesRequiredLowestTryAdmin_YES_NO}' = 'NO') then
begin
//Question about non-admin installation also answered with "No"
NoClicked := true
MsgBox(ExpandConstant('{cm:NoAdminAndOSTooOldforUAC}'), mbCriticalError, MB_OK);
Abort()
end;
if ('{#IfPrivilegesRequiredLowestTryAdmin_YES_NO}' = 'YES') then
begin
message2 := CustomMessage('NoAdminAndOSTooOldforUACOfferLimitedInstall');
if MsgBox(ExpandConstant(message2), mbError, MB_YESNO or MB_DEFBUTTON1) = IDYES then
begin
WizardForm.DirEdit.Text := ExpandConstant('{localappdata}\{#MyAppName}');
Log(Format('Falling back to local application user folder [%s]', [
WizardForm.DirEdit.Text]));
end
else
//XP query rejected after installation due to restricted rights:
Abort()
end;
end;
end;
// -------------------------------------------------------------
// MANUALLY CHANGE UNINSTALL-REGISTRY PATH FROM HKCU TO HKLM
var
AppId: string;
function MoveHKCUUninstallKeyToHKLM: Boolean;
var
UninstallKey: string;
I: Integer;
ValueNames: TArrayOfString;
ValueName: string;
ValueStr: string;
ValueDWord: Cardinal;
begin
if ExpandConstant('{{#emit StringChange(SetupSetting("AppId"),"{{","{")}') <> '' then
begin
AppId := ExpandConstant('{{#emit StringChange(SetupSetting("AppId"),"{{","{")}');
end
else
begin
AppId := ExpandConstant('{{#emit StringChange(SetupSetting("AppName"),"{{","{")}');
end;
Result := False;
if AppId = '' then
begin
Log('Cannot identify AppId');
end
else
begin
UninstallKey := 'Software\Microsoft\Windows\CurrentVersion\Uninstall\' + AppId + '_is1';
Log(Format(
'AppId identified as "%s", using uninstall key "%s"', [AppId, UninstallKey]));
if not RegKeyExists(HKCU, UninstallKey) then
begin
Log('HKCU uninstall key not found');
end
else
if RegKeyExists(HKLM, UninstallKey) then
begin
Log('HKLM uninstall key exists already');
end
else
begin
Log('HKCU uninstall key found and HKLM key not exists yet');
if not RegGetValueNames(HKCU, UninstallKey, ValueNames) then
begin
Log('Cannot list uninstall key values');
end
else
begin
I := 0;
Result := True;
while (I < GetArrayLength(ValueNames)) and Result do
begin
ValueName := ValueNames[I];
if RegQueryStringValue(HKCU, UninstallKey, ValueName, ValueStr) then
begin
if not RegWriteStringValue(HKLM, UninstallKey, ValueName, ValueStr) then
begin
Log(Format('Error moving "%s" string value', [ValueName]));
Result := False;
end
else
begin
Log(Format('Moved "%s" string value', [ValueName]));
end;
end
else
if RegQueryDWordValue(HKCU, UninstallKey, ValueName, ValueDWord) then
begin
if not RegWriteDWordValue(HKLM, UninstallKey, ValueName, ValueDWord) then
begin
Log(Format('Error moving "%s" dword value', [ValueName]));
Result := False;
end
else
begin
Log(Format('Moved "%s" dword value', [ValueName]));
end;
end
else
begin
{ All uninstall values written by Inno Setup are either string or dword }
Log(Format('Value "%s" is neither string nor dword', [ValueName]));
Result := False;
end;
Inc(I);
end;
if Result then
begin
if not RegDeleteKeyIncludingSubkeys(HKCU, UninstallKey) then
begin
Log('Error removing HKCU uninstall key');
Result := False;
end
else
begin
Log('Removed HKCU uninstall key');
end;
end;
if not Result then
begin
if not RegDeleteKeyIncludingSubkeys(HKCU, UninstallKey) then
begin
Log('Failed to move uninstall key to HKLM, ' +
'and also failed to rollback the changes');
end
else
begin
Log('Failed to move uninstall key to HKLM, rolled back the changes');
end;
end;
end;
end;
end;
end;
// -------------------------------------------------------------
procedure CurStepChanged(CurStep: TSetupStep);
var
InstallMode: string;
begin
InstallMode := GetPreviousData('UninstallMode', '(Current User)');
if (CurStep = ssPostInstall) AND (InstallMode = '(All Users)') then
begin
MoveHKCUUninstallKeyToHKLM;
end;
end;
// -------------------------------------------------------------
// REMOVE THE UNINSTALL FLAG IF ELEVATED RIGHTS
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
var
SettingsFile: string;
SettingsFolder: string;
UninstallMode: string;
InstallUsername: string;
CurrentUsername: string;
CurrentSID: string;
begin
if CurUninstallStep = usUninstall then
begin
UninstallMode := GetPreviousData('UninstallMode', '(Current User)');
AppId := ExpandConstant('{#SetupSetting("AppId")}');
if (UninstallMode = '(All Users)') then
begin
StringChangeEx(AppId, '(Current User)', '(All Users)', True);
RegDeleteKeyIncludingSubkeys(HKLM, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\' + AppId + '_is1');
end;
if (UninstallMode = '(Current User)') then
begin
StringChangeEx(AppId, '(All Users)', '(Current User)', True);
//When calling the uninstaller with elevated rights, HKCU is not removed. To do this here, I would need the SID of the currently logged on user - but there is no suitable solution for this (minor thing, so do not deal with it further)
//RegDeleteKeyIncludingSubkeys(HKU, '???\Software\Microsoft\Windows\CurrentVersion\Uninstall\' + AppId + '_is1');
end;
end;
end;
// -------------------------------------------------------------
也许有人会发现这很有用,特别是因为它被设计为向后兼容到 Windows 2000。