I based my setup in
Larger "Select Components" page in Inno Setup . I would like to know how to center the setup window when I go to components page, since in small resolutions bottom buttons are not visible.
Currently, there is no function to let you center the wizard form only in vertical direction. So, to make one you need to code a little bit. Here is function, that allows you to center forms in direction that you choose on the nearest monitor that the form covers the most:
[Code]
#ifdef UNICODE
#define AW "W"
#else
#define AW "A"
#endif
type
HMONITOR = THandle;
TMonitorInfo = record
cbSize: DWORD;
rcMonitor: TRect;
rcWork: TRect;
dwFlags: DWORD;
end;
const
MONITOR_DEFAULTTONULL = $0;
MONITOR_DEFAULTTOPRIMARY = $1;
MONITOR_DEFAULTTONEAREST = $2;
function GetMonitorInfo(hMonitor: HMONITOR; out lpmi: TMonitorInfo): BOOL;
external 'GetMonitorInfo{#AW}#user32.dll stdcall';
function MonitorFromWindow(hwnd: HWND; dwFlags: DWORD): HMONITOR;
external 'MonitorFromWindow#user32.dll stdcall';
procedure CenterForm(Form: TForm; Horz, Vert: Boolean);
var
X, Y: Integer;
Monitor: HMONITOR;
MonitorInfo: TMonitorInfo;
begin
if not (Horz or Vert) then
Exit;
Monitor := MonitorFromWindow(Form.Handle, MONITOR_DEFAULTTONEAREST);
MonitorInfo.cbSize := SizeOf(MonitorInfo);
if GetMonitorInfo(Monitor, MonitorInfo) then
begin
if not Horz then
X := Form.Left
else
X := MonitorInfo.rcWork.Left + ((MonitorInfo.rcWork.Right -
MonitorInfo.rcWork.Left) - Form.Width) div 2;
if not Vert then
Y := Form.Top
else
Y := MonitorInfo.rcWork.Top + ((MonitorInfo.rcWork.Bottom -
MonitorInfo.rcWork.Top) - Form.Height) div 2;
Form.SetBounds(X, Y, Form.Width, Form.Height);
end;
end;
To implement it in the code you used, you need to modify the part when the page is being changed:
...
procedure CurPageChanged(CurPageID: Integer);
begin
if CurpageID = wpSelectComponents then
begin
SaveComponentsPage(CompPagePositions);
LoadComponentsPage(CompPagePositions, 200);
CenterForm(WizardForm, False, True); // <- center the form only vertically
CompPageModified := True;
end
else
if CompPageModified then
begin
LoadComponentsPage(CompPagePositions, 0);
CenterForm(WizardForm, False, True); // <- center the form only vertically
CompPageModified := False;
end;
end;
Related
I would like to make new installer with WizardForm.Width is width of user monitor width and WizardForm.Height is height of user monitor height.
So, I already wrote new code, but there is one error like some black area.
This is my code that I have compiled:
[Code]
function GetSystemMetrics(nIndex:Integer):Integer;
external 'GetSystemMetrics#user32.dll stdcall';
procedure InitializeWizard();
var
width,height: Integer;
begin
MainForm.BorderStyle:= bsNone;
width:= GetSystemMetrics(0);
height:= GetSystemMetrics(1);
MainForm.Width:= width;
MainForm.Height:= height;
width:= MainForm.ClientWidth;
height:= MainForm.ClientHeight;
MainForm.Left := 0;
MainForm.Top := 0;
WizardForm.Position:= poScreenCenter;
WizardForm.BorderStyle:= bsNone;
WizardForm.Width:= MainForm.Width;
WizardForm.Height:= MainForm.Height;
WizardForm.ClientWidth:= MainForm.ClientWidth;
WizardForm.ClientHeight:= MainForm.ClientHeight;
MainForm.Visible:= True;
end;
I do not understand why you engage with MainForm. Just resize the WizardForm.
[Code]
function GetSystemMetrics(nIndex: Integer): Integer;
external 'GetSystemMetrics#user32.dll stdcall';
procedure InitializeWizard();
begin
WizardForm.Position := poScreenCenter;
WizardForm.BorderStyle := bsNone;
WizardForm.Width := GetSystemMetrics(0);
WizardForm.Height := GetSystemMetrics(1);
end;
Though the wizard is not designed to be stretched over whole screen anyway.
I am refactoring an old application to make it a bit more responsive and I have a form that is using devExpress components and it creates a custom grid using the CallbackCustomDrawPreviewCell, the problem is that this function is very slow it takes about 0.09s per call but it is call about 30 to 60 times each time the form is open so the form can take 2.8s to 5.6s to open.
I normally program with C# and Object-C/Swift where we can dispatch a block to be process in the background, but as far as my research go we don't have nothing similar in Delphi, it seems that normally in Delphi a new thread has to be a whole new and independent piece of code. Is my assumptions correct?
If so what is the best type of solution to improve speed in this kind of situation? (I am using Delphi XE)
(in case it helps: I also just bought AQTime to try help me figure out how to improve this but I had no luck so far with it, still need to dig into the manuals a little more. But it did help me find the problem in the speed in this particular callback)
Thanks in advance.
The function is:
procedure TtvdAvaOutageManagementForm.CallbackCustomDrawPreviewCell(Sender: TcxCustomTreeList; ACanvas: TcxCanvas;
AViewInfo: TcxTreeListEditCellViewInfo; var ADone: Boolean);
const
AlignFlag = DT_LEFT or DT_WORDBREAK or DT_EXPANDTABS or DT_NOPREFIX or DT_END_ELLIPSIS;
cnFontColor = clBlue;
var
AFaultId: variant;
aFault: TtvdFault;
aLocalities, aFaultLocalities: TStringList;
i: integer;
aLocality: string;
Rect: TRect;
size: TSize;
AText: string;
begin
{ colour the preview text blue }
ACanvas.Font.Color := cnFontColor;
AText := AViewInfo.DisplayValue;
aLocalities := TStringList.Create;
aFaultLocalities := TStringList.Create;
try
AFaultId := AViewInfo.Node.Values[FtvdTree.GetColumnByFieldName('FaultId').ItemIndex];
if (not VarIsNull(AFaultId)) then
begin
ACanvas.Brush.Color := COLOR_FAULT;
aFault := FtvdFaults.tvdGetFault(AFaultId);
if Assigned(aFault) then
begin
ACanvas.Brush.Color := aFault.tvdFaultColor;
ACanvas.Brush.Color := aFault.tvdFaultColor;
ACanvas.FillRect(AViewInfo.BoundsRect);
CopyRect(Rect, AViewInfo.BoundsRect);
InflateRect(Rect, -1, -1);
Inc(Rect.Left, FtvdTree.OptionsView.IndicatorWidth);
ACanvas.Font.Color := cnFontColor;
{ if all the localities are in the fault then bold the preview text,
else need to do it manually (i.e. only bold the localities that are
in the fault }
if aFault.tvdAllLocalities then
begin
ACanvas.Font.Style := [fsBold];
ACanvas.DrawTexT(AText, AViewInfo.BoundsRect, AlignFlag);
end
else
begin
CopyRect(Rect, AViewInfo.BoundsRect);
aLocalities.Text := StringReplace(AText, ', ', #13#10, [rfReplaceAll]);
aFaultLocalities.Text := StringReplace(aFault.tvdLocalities, ', ', #13#10, [rfReplaceAll]);
for i := 0 to aLocalities.Count - 1 do
begin
ACanvas.Font.Style := [];
{ draw a comma if this is not the first locality }
if i > 0 then
begin
size := ACanvas.TextExtent(',');
DrawText(ACanvas.Handle, ',', 1, Rect, DT_LEFT or DT_NOPREFIX);
Inc(Rect.Left, size.cx);
end;
aLocality := aLocalities[i];
if aFaultLocalities.IndexOf(aLocality) >= 0 then
begin
ACanvas.Font.Style := [fsBold];
end;
size := ACanvas.TextExtent(aLocality);
if (Rect.Left + size.cx) > Rect.Right then
begin
Rect.Left := AViewInfo.BoundsRect.Left;
Inc(Rect.Top, size.cy);
end;
{ draw the text item }
DrawText(ACanvas.Handle, pchar(aLocality), Length(aLocality), Rect, DT_LEFT or DT_NOPREFIX);
Inc(Rect.Left, size.cx);
end;
end;
ADone := true;
end;
end;
finally
aLocalities.Free;
aFaultLocalities.Free;
end;
end;
If you sum up my comments then it should be more or less this.
Try that at let us know how it worked out for you. Since I don't have a working example it might not be 100% correct.
procedure TtvdAvaOutageManagementForm.CallbackCustomDrawPreviewCell(Sender: TcxCustomTreeList; ACanvas: TcxCanvas;
AViewInfo: TcxTreeListEditCellViewInfo; var ADone: Boolean);
const
AlignFlag = DT_LEFT or DT_WORDBREAK or DT_EXPANDTABS or DT_NOPREFIX or DT_END_ELLIPSIS;
cnFontColor = clBlue;
var
AFaultId: variant;
aFault: TtvdFault;
aLocalities, aFaultLocalities: TStringList;
i: integer;
aLocality: string;
Rect: TRect;
size: TSize;
AText: string;
begin
{ colour the preview text blue }
ACanvas.Font.Color := cnFontColor;
AText := AViewInfo.DisplayValue;
aLocalities := TStringList.Create;
aFaultLocalities := TStringList.Create;
try
AFaultId := AViewInfo.Node.Values[FaultIdColumn.ItemIndex];
if not VarIsNull(AFaultId) then
begin
ACanvas.Brush.Color := COLOR_FAULT;
aFault := FtvdFaults.tvdGetFault(AFaultId);
if Assigned(aFault) then
begin
ACanvas.Brush.Color := aFault.tvdFaultColor;
ACanvas.Brush.Color := aFault.tvdFaultColor;
ACanvas.FillRect(AViewInfo.BoundsRect);
CopyRect(Rect, AViewInfo.BoundsRect);
InflateRect(Rect, -1, -1);
Inc(Rect.Left, FtvdTree.OptionsView.IndicatorWidth);
ACanvas.Font.Color := cnFontColor;
{ if all the localities are in the fault then bold the preview text,
else need to do it manually (i.e. only bold the localities that are
in the fault }
if aFault.tvdAllLocalities then
begin
ACanvas.Font.Style := [fsBold];
ACanvas.DrawTexT(AText, AViewInfo.BoundsRect, AlignFlag);
end
else
begin
CopyRect(Rect, AViewInfo.BoundsRect);
aLocalities.CommaText:= AText;
aFaultLocalities.CommaText := aFault.tvdLocalities;
aFaultLocalities.Sorted := True;
for i := 0 to aLocalities.Count - 1 do
begin
ACanvas.Font.Style := [];
{ draw a comma if this is not the first locality }
if i > 0 then
begin
size := ACanvas.TextExtent(',');
DrawText(ACanvas.Handle, ', ', 1, Rect, DT_LEFT or DT_NOPREFIX);
Inc(Rect.Left, size.cx);
end;
aLocality := aLocalities[i];
if aFaultLocalities.IndexOf(aLocality) >= 0 then
begin
ACanvas.Font.Style := [fsBold];
end;
size := ACanvas.TextExtent(aLocality);
if (Rect.Left + size.cx) > Rect.Right then
begin
Rect.Left := AViewInfo.BoundsRect.Left;
Inc(Rect.Top, size.cy);
end;
{ draw the text item }
DrawText(ACanvas.Handle, pchar(aLocality), Length(aLocality), Rect, DT_LEFT or DT_NOPREFIX);
Inc(Rect.Left, size.cx);
end;
end;
ADone := true;
end;
end;
finally
aLocalities.Free;
aFaultLocalities.Free;
end;
end;
I am developing an installer using Inno Setup targeting XP, Win7, 8. I need the app icon to be pinned to taskbar and the startmenu. So far I have been able to do that.
Now, when the user uninstalls this program, the pinned items should be unpinned. I haven't managed to find a solution to this.
Please guide.
You've said that you have used a function from this link. I assume the one from this post:
procedure zylPinAppToTaskbar(strPath, strApp: string);
var
vShell, vFolder, vFolderItem, vItemVerbs: Variant;
vPath, vApp: Variant;
i: Integer;
sItem: String;
h: LongInt;
szPinName: String;
filenameEnd : Integer;
filename : String;
strEnd : String;
begin
SetLength(szPinName, 255);
h := LoadLibrary(ExpandConstant('{sys}\Shell32.dll'));
LoadString(h, 5386, szPinName, 255);
FreeLibrary(h);
strEnd := #0;
filenameEnd := Pos(strEnd, szPinName);
filename := Copy(szPinName, 1, filenameEnd - 1);
if (Length(filename) > 0) then //WinXp or lower, no pin taskbar function
begin
vShell := CreateOleObject('Shell.Application');
vPath := strPath;
vFolder := vShell.NameSpace(vPath);
vApp := strApp;
vFolderItem := vFolder.ParseName(vApp);
vItemVerbs := vFolderItem.Verbs;
for i := 1 to vItemVerbs.Count do
begin
sItem := vItemVerbs.Item(i).Name;
if (sItem = filename) then
begin
// 63 63 72 75 6E 2E 63 6F 6D
vItemVerbs.Item(i).DoIt;
break;
end;
end;
end;
end;
That's really hacky way (which I wouldn't rely on). Let's focus now on what it actually does. The function loads the Shell32.dll library and reads from its string table the caption of the popup menu item that belongs to the Pin this program to taskbar feature (and stores it into the filename variable). Then it connects to Shell and creates the Folder object for the passed folder path (vFolder variable). For this folder object it then creates the FolderItem object (vFolderItem variable) and on this object iterates all the available verbs (vItemVerbs variable) and checks if the Name matches the one read from the Shell32.dll library. If it finds one, it invokes the action by the DoIt method and breaks the iteration.
Now if you know what the above code does, you can guess that the only thing you need to do to perform the unpin action is finding the caption of the popup menu item for that feature. I've looked into the string table of the Shell32.dll library and the Unpin this program from taskbar string has ID 5387, so the only thing to modify the above function for unpinning is changing this line:
// this magical 5386 value is the ID of the "Pin this program to taskbar"
// popup menu caption string in the Shell32.dll string table
LoadString(h, 5386, szPinName, 255);
To this:
// this magical 5387 value is the ID of the "Unpin this program from taskbar"
// popup menu caption string in the Shell32.dll string table
LoadString(h, 5387, szPinName, 255);
But I do not recommend that way. There is no official way to pin program to taskbar because that's been reserved for the user to decide.
As a bonus, I wrote the following wrapper for the above code:
[Code]
#ifdef UNICODE
#define AW "W"
#else
#define AW "A"
#endif
const
// these constants are not defined in Windows
SHELL32_STRING_ID_PIN_TO_TASKBAR = 5386;
SHELL32_STRING_ID_PIN_TO_STARTMENU = 5381;
SHELL32_STRING_ID_UNPIN_FROM_TASKBAR = 5387;
SHELL32_STRING_ID_UNPIN_FROM_STARTMENU = 5382;
type
HINSTANCE = THandle;
HMODULE = HINSTANCE;
TPinDest = (
pdTaskbar,
pdStartMenu
);
function LoadLibrary(lpFileName: string): HMODULE;
external 'LoadLibrary{#AW}#kernel32.dll stdcall';
function FreeLibrary(hModule: HMODULE): BOOL;
external 'FreeLibrary#kernel32.dll stdcall';
function LoadString(hInstance: HINSTANCE; uID: UINT;
lpBuffer: string; nBufferMax: Integer): Integer;
external 'LoadString{#AW}#user32.dll stdcall';
function TryGetVerbName(ID: UINT; out VerbName: string): Boolean;
var
Buffer: string;
BufLen: Integer;
Handle: HMODULE;
begin
Result := False;
Handle := LoadLibrary(ExpandConstant('{sys}\Shell32.dll'));
if Handle <> 0 then
try
SetLength(Buffer, 255);
BufLen := LoadString(Handle, ID, Buffer, Length(Buffer));
if BufLen <> 0 then
begin
Result := True;
VerbName := Copy(Buffer, 1, BufLen);
end;
finally
FreeLibrary(Handle);
end;
end;
function ExecVerb(const FileName, VerbName: string): Boolean;
var
I: Integer;
Shell: Variant;
Folder: Variant;
FolderItem: Variant;
begin
Result := False;
Shell := CreateOleObject('Shell.Application');
Folder := Shell.NameSpace(ExtractFilePath(FileName));
FolderItem := Folder.ParseName(ExtractFileName(FileName));
for I := 1 to FolderItem.Verbs.Count do
begin
if FolderItem.Verbs.Item(I).Name = VerbName then
begin
FolderItem.Verbs.Item(I).DoIt;
Result := True;
Exit;
end;
end;
end;
function PinAppTo(const FileName: string; PinDest: TPinDest): Boolean;
var
ResStrID: UINT;
VerbName: string;
begin
case PinDest of
pdTaskbar: ResStrID := SHELL32_STRING_ID_PIN_TO_TASKBAR;
pdStartMenu: ResStrID := SHELL32_STRING_ID_PIN_TO_STARTMENU;
end;
Result := TryGetVerbName(ResStrID, VerbName) and ExecVerb(FileName, VerbName);
end;
function UnpinAppFrom(const FileName: string; PinDest: TPinDest): Boolean;
var
ResStrID: UINT;
VerbName: string;
begin
case PinDest of
pdTaskbar: ResStrID := SHELL32_STRING_ID_UNPIN_FROM_TASKBAR;
pdStartMenu: ResStrID := SHELL32_STRING_ID_UNPIN_FROM_STARTMENU;
end;
Result := TryGetVerbName(ResStrID, VerbName) and ExecVerb(FileName, VerbName);
end;
And its possible usage, for pinning:
if PinAppTo(ExpandConstant('{sys}\calc.exe'), pdTaskbar) then
MsgBox('Calc has been pinned to the taskbar.', mbInformation, MB_OK);
if PinAppTo(ExpandConstant('{sys}\calc.exe'), pdStartMenu) then
MsgBox('Calc has been pinned to the start menu.', mbInformation, MB_OK);
And for unpinning:
if UnpinAppFrom(ExpandConstant('{sys}\calc.exe'), pdTaskbar) then
MsgBox('Calc is not pinned to the taskbar anymore.', mbInformation, MB_OK);
if UnpinAppFrom(ExpandConstant('{sys}\calc.exe'), pdStartMenu) then
MsgBox('Calc is not pinned to the start menu anymore.', mbInformation, MB_OK);
I was able to do it. Here's the code to pin and unpin from the startmenu and taskbar.
oShell := CreateOleObject('Shell.Application');
objFolder := oShell.Namespace(ExpandConstant('{localappdata}\My_Path'));
objFolderItem := objFolder.ParseName('MyApp.exe');
colVerbs := objFolderItem.Verbs();
for i := 0 to colverbs.count() do
begin
VerbName := lowercase(colverbs.item(i).name);
StringChangeEx(VerbName,'&','',true);
if (CompareText(Verbname, 'Pin to Start Menu') = 0) then
colverbs.item(i).DoIt
if (CompareText(Verbname, 'Pin to Taskbar') = 0) then
colverbs.item(i).DoIt
end;
Change the compare string to 'Unpin from Start Menu' and 'Unpin from Taskbar' at the time of unpinning.
Can I allow the user to only choose the drive in which the software will be installed?
For example they can choose the C or D drive:
C:\Software
D:\Software
But the user can not specify anything else,
Like they can't choose to install the software under Downloads or MyDocumnets … etc.
Is this possible?
How to restrict users to select only drive on which the software will be installed ?
There are hundreds of ways to design this restriction. I chose the one which creates a combo box with available paths that user can choose from. This code as first lists all fixed drives on the machine, and if there's at least one, it creates the combo box which is placed instead of original dir selection controls. It is filled with drive names followed by a fixed directory taken from the DefaultDirName directive value which must not contain a drive portion since it is already concatenated with found fixed drive roots:
[Setup]
AppName=My Program
AppVersion=1.5
DefaultDirName=My Program
[Messages]
SelectDirBrowseLabel=To continue, click Next.
[Code]
#ifdef UNICODE
#define AW "W"
#else
#define AW "A"
#endif
type
TDriveType = (
dtUnknown,
dtNoRootDir,
dtRemovable,
dtFixed,
dtRemote,
dtCDROM,
dtRAMDisk
);
TDriveTypes = set of TDriveType;
function GetDriveType(lpRootPathName: string): UINT;
external 'GetDriveType{#AW}#kernel32.dll stdcall';
function GetLogicalDriveStrings(nBufferLength: DWORD; lpBuffer: string): DWORD;
external 'GetLogicalDriveStrings{#AW}#kernel32.dll stdcall';
var
DirCombo: TNewComboBox;
#ifndef UNICODE
function IntToDriveType(Value: UINT): TDriveType;
begin
Result := dtUnknown;
case Value of
1: Result := dtNoRootDir;
2: Result := dtRemovable;
3: Result := dtFixed;
4: Result := dtRemote;
5: Result := dtCDROM;
6: Result := dtRAMDisk;
end;
end;
#endif
function GetLogicalDrives(Filter: TDriveTypes; Drives: TStrings): Integer;
var
S: string;
I: Integer;
DriveRoot: string;
begin
Result := 0;
I := GetLogicalDriveStrings(0, #0);
if I > 0 then
begin
SetLength(S, I);
if GetLogicalDriveStrings(Length(S), S) > 0 then
begin
S := TrimRight(S) + #0;
I := Pos(#0, S);
while I > 0 do
begin
DriveRoot := Copy(S, 1, I - 1);
#ifdef UNICODE
if (Filter = []) or
(TDriveType(GetDriveType(DriveRoot)) in Filter) then
#else
if (Filter = []) or
(IntToDriveType(GetDriveType(DriveRoot)) in Filter) then
#endif
begin
Drives.Add(DriveRoot);
end;
Delete(S, 1, I);
I := Pos(#0, S);
end;
Result := Drives.Count;
end;
end;
end;
procedure DriveComboChange(Sender: TObject);
begin
WizardForm.DirEdit.Text := DirCombo.Text;
end;
procedure InitializeWizard;
var
I: Integer;
StringList: TStringList;
begin
StringList := TStringList.Create;
try
if GetLogicalDrives([dtFixed], StringList) > 0 then
begin
WizardForm.DirEdit.Visible := False;
WizardForm.DirBrowseButton.Visible := False;
DirCombo := TNewComboBox.Create(WizardForm);
DirCombo.Parent := WizardForm.DirEdit.Parent;
DirCombo.SetBounds(WizardForm.DirEdit.Left, WizardForm.DirEdit.Top,
WizardForm.DirBrowseButton.Left + WizardForm.DirBrowseButton.Width -
WizardForm.DirEdit.Left, WizardForm.DirEdit.Height);
DirCombo.Style := csDropDownList;
DirCombo.OnChange := #DriveComboChange;
for I := 0 to StringList.Count - 1 do
DirCombo.Items.Add(StringList[I] + '{#SetupSetting('DefaultDirName')}');
DirCombo.ItemIndex := 0;
DirCombo.OnChange(nil);
end;
finally
StringList.Free;
end;
end;
And a screenshot:
How to create CustomPage in Inno Setup with Edit Boxes for Serial Number?
E.g. 6x5chars or 7x5chars?
Script should check if all boxes are filled before Next button become available.
It would be also good if there could be Copy/Paste function implemented that would allow to fill up all Edit Boxes if the clipboard content matches the serial number pattern.
Here is one approach that uses the custom page where the separate edit boxes are created. You only need to specify the value for the SC_EDITCOUNT constant where the number of edit boxes is defined and the SC_CHARCOUNT what is the number of characters that can be entered into these edit boxes. If you are in the first edit box you may paste the whole serial number if it's in the format by the pattern delimited by the - char (the TryPasteSerialNumber function here). To get the serial number from the edit boxes it's enough to call GetSerialNumber where you can specify also a delimiter for the output format (if needed).
[Setup]
AppName=Serial number project
AppVersion=1.0
DefaultDirName={pf}\Serial number project
[code]
function SetFocus(hWnd: HWND): HWND;
external 'SetFocus#user32.dll stdcall';
function OpenClipboard(hWndNewOwner: HWND): BOOL;
external 'OpenClipboard#user32.dll stdcall';
function GetClipboardData(uFormat: UINT): THandle;
external 'GetClipboardData#user32.dll stdcall';
function CloseClipboard: BOOL;
external 'CloseClipboard#user32.dll stdcall';
function GlobalLock(hMem: THandle): PAnsiChar;
external 'GlobalLock#kernel32.dll stdcall';
function GlobalUnlock(hMem: THandle): BOOL;
external 'GlobalUnlock#kernel32.dll stdcall';
var
SerialPage: TWizardPage;
SerialEdits: array of TEdit;
const
CF_TEXT = 1;
VK_BACK = 8;
SC_EDITCOUNT = 6;
SC_CHARCOUNT = 5;
SC_DELIMITER = '-';
function IsValidInput: Boolean;
var
I: Integer;
begin
Result := True;
for I := 0 to GetArrayLength(SerialEdits) - 1 do
if Length(SerialEdits[I].Text) < SC_CHARCOUNT then
begin
Result := False;
Break;
end;
end;
function GetClipboardText: string;
var
Data: THandle;
begin
Result := '';
if OpenClipboard(0) then
try
Data := GetClipboardData(CF_TEXT);
if Data <> 0 then
Result := String(GlobalLock(Data));
finally
if Data <> 0 then
GlobalUnlock(Data);
CloseClipboard;
end;
end;
function GetSerialNumber(ADelimiter: Char): string;
var
I: Integer;
begin
Result := '';
for I := 0 to GetArrayLength(SerialEdits) - 1 do
Result := Result + SerialEdits[I].Text + ADelimiter;
Delete(Result, Length(Result), 1);
end;
function TrySetSerialNumber(const ASerialNumber: string; ADelimiter: Char): Boolean;
var
I: Integer;
J: Integer;
begin
Result := False;
if Length(ASerialNumber) = ((SC_EDITCOUNT * SC_CHARCOUNT) + (SC_EDITCOUNT - 1)) then
begin
for I := 1 to SC_EDITCOUNT - 1 do
if ASerialNumber[(I * SC_CHARCOUNT) + I] <> ADelimiter then
Exit;
for I := 0 to GetArrayLength(SerialEdits) - 1 do
begin
J := (I * SC_CHARCOUNT) + I + 1;
SerialEdits[I].Text := Copy(ASerialNumber, J, SC_CHARCOUNT);
end;
Result := True;
end;
end;
function TryPasteSerialNumber: Boolean;
begin
Result := TrySetSerialNumber(GetClipboardText, SC_DELIMITER);
end;
procedure OnSerialEditChange(Sender: TObject);
begin
WizardForm.NextButton.Enabled := IsValidInput;
end;
procedure OnSerialEditKeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
var
Edit: TEdit;
EditIndex: Integer;
begin
Edit := TEdit(Sender);
EditIndex := Edit.TabOrder - SerialEdits[0].TabOrder;
if (EditIndex = 0) and (Key = Ord('V')) and (Shift = [ssCtrl]) then
begin
if TryPasteSerialNumber then
Key := 0;
end
else
if (Key >= 32) and (Key <= 255) then
begin
if Length(Edit.Text) = SC_CHARCOUNT - 1 then
begin
if EditIndex < GetArrayLength(SerialEdits) - 1 then
SetFocus(SerialEdits[EditIndex + 1].Handle)
else
SetFocus(WizardForm.NextButton.Handle);
end;
end
else
if Key = VK_BACK then
if (EditIndex > 0) and (Edit.Text = '') and (Edit.SelStart = 0) then
SetFocus(SerialEdits[EditIndex - 1].Handle);
end;
procedure CreateSerialNumberPage;
var
I: Integer;
Edit: TEdit;
DescLabel: TLabel;
EditWidth: Integer;
begin
SerialPage := CreateCustomPage(wpWelcome, 'Serial number validation',
'Enter the valid serial number');
DescLabel := TLabel.Create(SerialPage);
DescLabel.Top := 16;
DescLabel.Left := 0;
DescLabel.Parent := SerialPage.Surface;
DescLabel.Caption := 'Enter valid serial number and continue the installation...';
DescLabel.Font.Style := [fsBold];
SetArrayLength(SerialEdits, SC_EDITCOUNT);
EditWidth := (SerialPage.SurfaceWidth - ((SC_EDITCOUNT - 1) * 8)) div SC_EDITCOUNT;
for I := 0 to SC_EDITCOUNT - 1 do
begin
Edit := TEdit.Create(SerialPage);
Edit.Top := 40;
Edit.Left := I * (EditWidth + 8);
Edit.Width := EditWidth;
Edit.CharCase := ecUpperCase;
Edit.MaxLength := SC_CHARCOUNT;
Edit.Parent := SerialPage.Surface;
Edit.OnChange := #OnSerialEditChange;
Edit.OnKeyDown := #OnSerialEditKeyDown;
SerialEdits[I] := Edit;
end;
end;
procedure CurPageChanged(CurPageID: Integer);
begin
if CurPageID = SerialPage.ID then
WizardForm.NextButton.Enabled := IsValidInput;
end;
procedure InitializeWizard;
begin
CreateSerialNumberPage;
end;
And here is how it looks like:
You can make Inno prompt the user for a serial key by adding an CheckSerial() event function.
If you want more control over the page, you can use one of the stock pages (CreateInput...Page) or a custom page in the setup wizard using CreateCustomPage() and adding controls as you require.
See the codedlg.iss example included with Inno setup.
The simplest way to add a Serial key box, beneath the Name and Organisation text fields, is to add something like the following to your iss file.
[Code]
function CheckSerial(Serial: String): Boolean;
begin
// serial format is XXXX-XXXX-XXXX-XXXX
Serial := Trim(Serial);
if Length(Serial) = 19 then
result := true;
end;
This can be usefully combined with
[Setup]
DefaultUserInfoSerial={param:Serial}
which will fill in the serial if previously entered for the install.