I use a TScrollBox as a list and a TFrame as the Items and I will generate the frames in runtime. The Frame I'm using consists a 3.6KB SVG-Image and some Lables and EditBoxes. As a test, I generated the list with 1000 items in FormShow like this:
var
i: Integer;
begin
for i := 1 to 1000 do
with TFrameCDG.Create(Self) do
begin
Name := 'cdgFrame' + IntToStr(i);
Parent := sbScrollBoxLeft;
end;
end;
Note that I have set the Align property of the frame to alTop and controlled the background color using the events OnExit, OnEnter, OnClick, etc. to make the list look better.
The problem is that the form loads after 38 seconds, resizes in 12 seconds (Maximize), and scrolls very heavily. My cpu is i7-4790, gpu Radeon R7 430, 16GB RAM and I'm using windows 11 and Delphi 10 Seattle.
What's wrong with what I've done?
I deleted the SVG-Image and it took 29 seconds to load. I tried DoubleBuffered and that did not help as I thought.
This list is going to have no more than 50 Items but it is very heavy and slow. How can I accelerate such graphical UI to be smooth like (or near to) what wpf in c# can do?
I created a new project and hier is a minimal example to look at:
program Project1;
uses
Vcl.Forms,
Unit1 in 'Unit1.pas' {Form1},
Unit2 in 'Unit2.pas' {Frame2: TFrame};
{$R *.res}
begin
Application.Initialize;
Application.MainFormOnTaskbar := True;
Application.CreateForm(TForm1, Form1);
Application.Run;
end.
unit Unit1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Unit2;
type
TForm1 = class(TForm)
ScrollBox1: TScrollBox;
procedure FormShow(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.FormShow(Sender: TObject);
var
i: Integer;
begin
for i := 0 to 1000 do
with TFrame2.Create(Self) do
begin
Name := 'Framea' + IntToStr(i);
Parent := ScrollBox1;
end;
end;
end.
unit Unit2;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls, Vcl.ComCtrls;
type
TFrame2 = class(TFrame)
ProgressBar1: TProgressBar;
Label1: TLabel;
Edit1: TEdit;
Bevel1: TBevel;
Edit2: TEdit;
Label2: TLabel;
Edit3: TEdit;
Label3: TLabel;
Button1: TButton;
procedure FrameClick(Sender: TObject);
procedure FrameEnter(Sender: TObject);
procedure FrameExit(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
implementation
{$R *.dfm}
procedure TFrame2.FrameClick(Sender: TObject);
begin
Self.SetFocus;
end;
procedure TFrame2.FrameEnter(Sender: TObject);
begin
Color := clBlue;
end;
procedure TFrame2.FrameExit(Sender: TObject);
begin
Color := clTeal;
end;
end.
object Form1: TForm1
Left = 0
Top = 0
Caption = 'Form1'
ClientHeight = 660
ClientWidth = 1333
Color = clBtnFace
DoubleBuffered = True
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'Tahoma'
Font.Style = []
OldCreateOrder = False
OnShow = FormShow
PixelsPerInch = 96
TextHeight = 13
object ScrollBox1: TScrollBox
Left = 0
Top = 0
Width = 1333
Height = 660
HorzScrollBar.Visible = False
VertScrollBar.Smooth = True
VertScrollBar.Tracking = True
Align = alClient
TabOrder = 0
end
end
object Frame2: TFrame2
Left = 0
Top = 0
Width = 451
Height = 117
Align = alTop
Color = clTeal
Font.Charset = ANSI_CHARSET
Font.Color = clWindowText
Font.Height = -19
Font.Name = 'Segoe UI'
Font.Style = []
ParentBackground = False
ParentColor = False
ParentFont = False
TabOrder = 0
OnClick = FrameClick
OnEnter = FrameEnter
OnExit = FrameExit
DesignSize = (
451
117)
object Label1: TLabel
Left = 24
Top = 16
Width = 55
Height = 25
Caption = 'Label1'
Font.Charset = ANSI_CHARSET
Font.Color = clWhite
Font.Height = -19
Font.Name = 'Segoe UI'
Font.Style = []
ParentFont = False
end
object Bevel1: TBevel
Left = 0
Top = 0
Width = 451
Height = 17
Align = alTop
Shape = bsTopLine
ExplicitLeft = -44
ExplicitTop = 24
end
object Label2: TLabel
Left = 131
Top = 16
Width = 55
Height = 25
Caption = 'Label1'
Font.Charset = ANSI_CHARSET
Font.Color = clWhite
Font.Height = -19
Font.Name = 'Segoe UI'
Font.Style = []
ParentFont = False
end
object Label3: TLabel
Left = 238
Top = 16
Width = 55
Height = 25
Caption = 'Label1'
Font.Charset = ANSI_CHARSET
Font.Color = clWhite
Font.Height = -19
Font.Name = 'Segoe UI'
Font.Style = []
ParentFont = False
end
object ProgressBar1: TProgressBar
Left = 352
Top = 73
Width = 77
Height = 21
Anchors = [akLeft, akRight, akBottom]
TabOrder = 0
end
object Edit1: TEdit
Left = 24
Top = 55
Width = 101
Height = 38
BevelInner = bvNone
BevelOuter = bvNone
BorderStyle = bsNone
Color = 11184810
Ctl3D = True
ParentCtl3D = False
TabOrder = 1
Text = 'Edit1'
end
object Edit2: TEdit
Left = 131
Top = 55
Width = 101
Height = 38
BevelInner = bvNone
BevelOuter = bvNone
BorderStyle = bsNone
Color = 11184810
Ctl3D = True
ParentCtl3D = False
TabOrder = 2
Text = 'Edit1'
end
object Edit3: TEdit
Left = 238
Top = 55
Width = 101
Height = 38
BevelInner = bvNone
BevelOuter = bvNone
BorderStyle = bsNone
Color = 11184810
Ctl3D = True
ParentCtl3D = False
TabOrder = 3
Text = 'Edit1'
end
object Button1: TButton
Left = 354
Top = 36
Width = 75
Height = 25
Anchors = [akTop, akRight]
Caption = 'Button1'
TabOrder = 4
end
end
Reading the useful comments, I decided to change my code to obtain a better (not the best) solution. I bring it here because maybe it is useful for others. The logic is that it creates the frames without settin their parents (in memory not on the form) and it is very fast. Then it will set the parent of the could-be-visible frames to Panel1 and also set the right top, on the ScrollChange of the ScrollBar.
By the way, as I mentioned before, I tryed to create so many frames just because I wanted to test the vcl, however the code below works for me good even with 1000 frames:
...
var
Form1: TForm1;
InvisibleFrames: TArray<TFrame2>;
NumberOfVisibleFrames: Integer;
NumberOfInvisibleFrames: Integer;
const
TrackingPrecision = 20;
...
procedure TForm1.btnCreateClick(Sender: TObject);
var
i: Integer;
begin
NumberOfInvisibleFrames := 1000;
SetLength(InvisibleFrames, NumberOfInvisibleFrames * SizeOf(TFrame2));
for i := 0 to NumberOfInvisibleFrames - 1 do
begin
InvisibleFrames[i] := TFrame2.Create(Self);
InvisibleFrames[i].Name := '';
InvisibleFrames[i].Label1.Caption := 'Frame: ' + IntToStr(i + 1);
end;
Panel1.OnResize := Panel1Resize;
Panel1Resize(Sender);
end;
procedure TForm1.Panel1Resize(Sender: TObject);
begin
NumberOfVisibleFrames := Panel1.Height div InvisibleFrames[0].Height + 1;
ScrollBar1.Min := 0;
ScrollBar1.Max := Max((NumberOfInvisibleFrames - NumberOfVisibleFrames) * TrackingPrecision, 0);
ScrollBar1.Enabled := ScrollBar1.Max > 0;
ScrollBar1.LargeChange := TrackingPrecision * (NumberOfVisibleFrames - 1);
ScrollBar1.SmallChange := TrackingPrecision;
ScrollBar1Change(Sender);
end;
procedure TForm1.ScrollBar1Change(Sender: TObject);
var
i: Integer;
n: Integer;
begin
SendMessage(Panel1.Handle, WM_SETREDRAW, WPARAM(False), 0);
try
Panel1.Hide;
for i := 0 to NumberOfInvisibleFrames - 1 do
begin
with InvisibleFrames[i] do
begin
Parent := nil;
end;
end;
n := ScrollBar1.Position div TrackingPrecision;
for i := n to n + NumberOfVisibleFrames do
begin
if Assigned(InvisibleFrames[i]) then
with InvisibleFrames[i] do
begin
Parent := Panel1;
Name := '';
Left := 0;
Width := Panel1.ClientWidth;
if ScrollBar1.Enabled then
Top := Ceil((i - ScrollBar1.Position / TrackingPrecision) * Height +
(ScrollBar1.Position / ScrollBar1.Max) * (Panel1.Height mod Height - Height))
else
Top := i * Height;
end;
end;
finally
SendMessage(Panel1.Handle, WM_SETREDRAW, WPARAM(True), 0);
Panel1.Show;
end;
end;
It should need some modifications which I will make in my real project, like better error checking, controlling the Items, or releasing the memory and so on.
Or maybe I would make a component from it calling TFrameListBox, if I would have time.
Try to use TPanel as a container instead of TFrame.
Call ScrollBox.DisableAlign once before adding the panels and ScrollBox.EnableAlign after the last panel has been added.
I think you could have some funny behavior if the total panels height reach 32768px. That would require an alternative approach.
Related
In a 32-bit VCL Application in Windows 10 in Delphi 11 Alexandria, I show an INPUT DIALOG to the user:
var aNewFolderName: string := 'New Project Folder';
if Vcl.Dialogs.InputQuery('New Project Folder', 'Enter the name of the new Project Folder:', aNewFolderName) then
begin
// Todo: Create the folder if everything went OK, ELSE REPEAT the input action :-(
end;
Is there a way to VALIDATE the user's input BEFORE he clicks the OK button? (E.g., checking for not allowed characters, existing folder, etc.). Just repeating the whole input action in the case of invalid input AFTER the user clicked OK is not very smart and efficient:
var aNewFolderName: string := 'New Project Folder';
var InputIsValid: Boolean;
repeat
if Vcl.Dialogs.InputQuery('New Project Folder', 'Enter the name of the new Project Folder:', aNewFolderName) then
begin
InputIsValid := CheckInput(aNewFolderName);
if InputIsValid then CreateTheFolder(aNewFolderName);
end
else
BREAK;
until InputIsValid;
Also, with this method, there is no feedback for the user about the specific cause of any invalid input.
Although it is possible to solve this problem by using repeated dialogs, I don't think that is a particularly elegant solution from a UX perspective.
I'd rather make my own dialog and do something like this:
procedure TForm1.btnSetPasswordClick(Sender: TObject);
begin
var psw := '';
if SuperInput(
Self,
'Frog Simulator',
'Please enter the new frog password:',
psw,
function(const Text: string; out AErrorMessage: string): Boolean
begin
if Text.Length < 8 then
begin
AErrorMessage := 'The password''s length must be at least 8 characters.';
Exit(False);
end;
if not StrHasChrOfType(Text, TCharacter.IsLetter) then
begin
AErrorMessage := 'The password must contain at least one letter.';
Exit(False);
end;
if not StrHasChrOfType(Text, TCharacter.IsDigit) then
begin
AErrorMessage := 'The password must contain at least one digit.';
Exit(False);
end;
if not StrHasChrOfType(Text, TCharacter.IsPunctuation) then
begin
AErrorMessage := 'The password must contain at least one punctuation character.';
Exit(False);
end;
Result := True;
end)
then
lblNewPassword.Caption := psw;
end;
Here's the code:
unit SuperInputDlg;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls;
type
TValidator = reference to function(const Text: string;
out AErrorMessage: string): Boolean;
TSuperInputForm = class(TForm)
lblCaption: TLabel;
shClient: TShape;
Edit: TEdit;
pbErrorIcon: TPaintBox;
lblError: TLabel;
Validator: TTimer;
btnOK: TButton;
btnCancel: TButton;
procedure FormCreate(Sender: TObject);
procedure pbErrorIconPaint(Sender: TObject);
procedure EditChange(Sender: TObject);
procedure ValidatorTimer(Sender: TObject);
procedure btnOKClick(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
FErrorIcon: HICON;
FLIWSD: Boolean;
FValidator: TValidator;
function DoValidate: Boolean;
public
end;
function SuperInput(AOwnerForm: TCustomForm; const ACaption, AMainInstruction: string;
var AText: string; AValidator: TValidator = nil): Boolean;
implementation
{$R *.dfm}
function Scale(X: Integer): Integer;
begin
Result := MulDiv(X, Screen.PixelsPerInch, 96);
end;
procedure TSuperInputForm.btnOKClick(Sender: TObject);
begin
if DoValidate then
ModalResult := mrOK;
end;
function TSuperInputForm.DoValidate: Boolean;
begin
var LErrMsg: string;
var LIsValid := not Assigned(FValidator) or FValidator(Edit.Text, LErrMsg);
btnOK.Enabled := LIsValid;
if not LIsValid then
lblError.Caption := LErrMsg;
pbErrorIcon.Visible := not LIsValid;
lblError.Visible := not LIsValid;
Result := LIsValid;
end;
procedure TSuperInputForm.EditChange(Sender: TObject);
begin
Validator.Enabled := False;
Validator.Enabled := True;
end;
procedure TSuperInputForm.FormCreate(Sender: TObject);
var
ComCtl: HMODULE;
LoadIconWithScaleDown: function(hinst: HINST; pszName: LPCWSTR; cx: Integer;
cy: Integer; var phico: HICON): HResult; stdcall;
begin
ComCtl := LoadLibrary('ComCtl32.dll');
if ComCtl <> 0 then
begin
try
LoadIconWithScaleDown := GetProcAddress(ComCtl, 'LoadIconWithScaleDown');
if Assigned(LoadIconWithScaleDown) then
LoadIconWithScaleDown(0, IDI_ERROR, Scale(16), Scale(16), FErrorIcon);
finally
FreeLibrary(ComCtl);
end;
end;
FLIWSD := FErrorIcon <> 0;
if FErrorIcon = 0 then
FErrorIcon := LoadIcon(0, IDI_ERROR);
end;
procedure TSuperInputForm.FormDestroy(Sender: TObject);
begin
if FLIWSD then
DestroyIcon(FErrorIcon);
end;
procedure TSuperInputForm.pbErrorIconPaint(Sender: TObject);
begin
if FErrorIcon <> 0 then
DrawIconEx(pbErrorIcon.Canvas.Handle, 0, 0, FErrorIcon,
Scale(16), Scale(16), 0, 0, DI_NORMAL);
end;
procedure TSuperInputForm.ValidatorTimer(Sender: TObject);
begin
DoValidate;
end;
function SuperInput(AOwnerForm: TCustomForm; const ACaption, AMainInstruction: string;
var AText: string; AValidator: TValidator = nil): Boolean;
begin
var LFrm := TSuperInputForm.Create(AOwnerForm);
try
LFrm.Caption := ACaption;
LFrm.lblCaption.Caption := AMainInstruction;
LFrm.Edit.Text := AText;
LFrm.FValidator := AValidator;
LFrm.DoValidate;
Result := LFrm.ShowModal = mrOk;
if Result then
AText := LFrm.Edit.Text;
finally
LFrm.Free;
end;
end;
end.
and DFM:
object SuperInputForm: TSuperInputForm
Left = 0
Top = 0
Caption = 'Input Box'
ClientHeight = 166
ClientWidth = 469
Color = clBtnFace
Constraints.MaxHeight = 204
Constraints.MinHeight = 204
Constraints.MinWidth = 400
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -12
Font.Name = 'Segoe UI'
Font.Style = []
OldCreateOrder = False
Position = poOwnerFormCenter
OnCreate = FormCreate
OnDestroy = FormDestroy
DesignSize = (
469
166)
PixelsPerInch = 96
TextHeight = 15
object shClient: TShape
Left = 0
Top = 0
Width = 468
Height = 127
Anchors = [akLeft, akTop, akRight, akBottom]
Pen.Style = psClear
ExplicitWidth = 499
ExplicitHeight = 175
end
object lblCaption: TLabel
Left = 24
Top = 24
Width = 65
Height = 21
Caption = 'Input Box'
Font.Charset = DEFAULT_CHARSET
Font.Color = 10040064
Font.Height = -16
Font.Name = 'Segoe UI'
Font.Style = []
ParentFont = False
end
object pbErrorIcon: TPaintBox
Left = 24
Top = 88
Width = 16
Height = 16
OnPaint = pbErrorIconPaint
end
object lblError: TLabel
Left = 50
Top = 88
Width = 3
Height = 15
end
object Edit: TEdit
Left = 24
Top = 51
Width = 418
Height = 23
Anchors = [akLeft, akTop, akRight]
TabOrder = 0
OnChange = EditChange
ExplicitWidth = 449
end
object btnOK: TButton
Left = 286
Top = 133
Width = 75
Height = 25
Anchors = [akRight, akBottom]
Caption = 'OK'
Default = True
TabOrder = 1
OnClick = btnOKClick
ExplicitLeft = 317
ExplicitTop = 181
end
object btnCancel: TButton
Left = 367
Top = 133
Width = 75
Height = 25
Anchors = [akRight, akBottom]
Cancel = True
Caption = 'Cancel'
ModalResult = 2
TabOrder = 2
ExplicitLeft = 398
ExplicitTop = 181
end
object Validator: TTimer
OnTimer = ValidatorTimer
Left = 136
Top = 120
end
end
Please note that this is only a sketch I did in ten minutes -- in a real app, you'd spend a bit more time on this one.
Appendix 1
type
TChrTestFcn = function(C: Char): Boolean;
function StrHasChrOfType(const AText: string; ATestFcn: TChrTestFcn): Boolean;
begin
for var S in AText do
if ATestFcn(S) then
Exit(True);
Result := False;
end;
No, there is no way. But there are alternatives:
Look up InputQuery's code, write similar code and then make modifications where you want them (e.g. setting an OnChange handler for the TEdit). You'll notice it has been done in a quite simple way.
Design your own form for it, instead of (like InputQuery does) creating one on the fly:
You can either use your own logic for getting the user's choice and input when the form is closed, or
you set the form's ModalResult property (via code) anytime, and you can assign a ModalResult to each button (e.g. mrCancel), so it automatically sets the form's ModalResult property. Call your own form just as if it's MessageBox():
case MyForm.ShowModal() of
IDOK: begin
// User wants to proceed AND no error was found
end;
IDCANCEL: begin
// User gave up
end;
else
// Unexpected result
end;
For me it would make nearly no difference if any feedback is given with any form still visible or not, as long as it is given. And you can give it, because nothing keeps you from doing so:
var
aNewFolderName,
sErrorMsg: String;
begin
repeat
aNewFolderName:= 'New Project Folder';
// User cancelled: leaving loop
if not Vcl.Dialogs.InputQuery( 'New project folder', 'Enter the name of the new project folder:', aNewFolderName ) then break;
// Already errors before the attempt?
sErrorMsg:= CheckInput( aNewFolderName );
// Actual attempt, which may still go wrong for various reasons
if sErrorMsg= '' then sErrorMsg:= CreateTheFolder( aNewFolderName );
// Anything to report?
if sErrorMsg<> '' then begin
sErrorMsg:= sErrorMsg+ #13#10
+ 'Click "Retry" to try a different new project name.'+ #13#10
+ 'Click "Cancel" to not create any new project.';
if MessageBox( sErrorMsg, 'Error', MB_RETRYCANCEL )= IDCANCEL then break;
end else begin
break; // Otherwise we're done (CreateTheFolder was successful)
end;
until FALSE;
end;
The easiest and most versatile option is to write your own input dialog and either include the validation code in the dialog or alternatively pass a callback method that does the validation.
But to answer the question as you asked it:
"Is there a way to VALIDATE the user's input BEFORE he clicks the OK button?"
No, there is no way, as far as I know.
Thanks to the users who explicitly confirmed that there is no predesigned way to validate the input before the user clicks OK. Now the remaining question is: How to provide a meaningful and efficient feedback about the invalidation cause to the user? IMO, showing an ADDITIONAL error dialog does not meet these criteria. So I implemented a solution where the prompt is replaced by the validation error message in the reappearing InputQuery dialog:
procedure TformMain.menuitemCreateASubProjectGroupClick(Sender: TObject);
begin
var aNewFolderName: string := 'New Project Group';
var InputIsValid: Boolean;
var InputPrompt: string := 'Enter the name of the new Project Group:';
repeat
if Vcl.Dialogs.InputQuery('New Project Group', InputPrompt, aNewFolderName) then
begin
var NewPrompt: string;
InputIsValid := CheckInput(aNewFolderName, NewPrompt);
if not InputIsValid then
InputPrompt := NewPrompt
else
begin
CreateNewFolder(aNewFolderName);
end;
end
else
BREAK;
until InputIsValid;
end;
function PAIsFilenameValid(const AFilename: string; var errorchar: Char): Boolean;
begin
Result := True;
for var i := 1 to Length(AFilename) do
begin
if not System.IOUtils.TPath.IsValidFileNameChar(AFilename[i]) then
begin
errorchar := AFilename[i];
Result := False;
BREAK;
end;
end;
end;
function TformMain.CheckInput(const aNewFolderName: string; out aNewPrompt: string): Boolean;
begin
Result := True;
var errorchar: Char;
if Trim(aNewFolderName) = '' then
begin
aNewPrompt := 'Error: You must enter a Group name:';
Result := False;
end
else if not PAIsFilenameValid(aNewFolderName, errorchar) then
begin
aNewPrompt := 'Error: No illegal characters (' + errorchar + '):';
Result := False;
end;
// Todo: else...
end;
Example screenshots:
I made application in Delphi and it is running on Windows Server 2019 per user basis. Those users connect with Remote Desktop to the user session (group policy) and run the application.
Is it possible to open a configuration file on a shared network map only with my application and not for example with Notepad?
More in general. What is the best way to store configuration data which in fact is secret for the user? I was thinking to put sensitive data just in the database but still it is nice no put for example server information somehwere in a config file instead of "baking" in.
This is my first post an I am aware it is between programming and server configuration. Else my translation scales seem no to get a hit for "only application open a file". My excuses if this post isnt perfect.
I see several possibilities:
1°
If you don't want the user to "see" your data, then you have to encrypt the file content. There are a lot of Delphi encryption/decryption libraries. I suggest you start with Delphi Encryption Compendium which is available for free on GitHub.
You can store the data in an in-memory structure such as an XML or JSON (Delphi has built-in routine to handle both XML and JSON). Before writing to disc, you encrypt it and after having reloaded the encrypted file, you decrypt it before accessing it the standard way.
2° Use a file accessible from another account and make your program impersonate that account when access to the file is required.
I wrote some code for use to ease and demo that way. I created a class TImpersonateUser having two methods Logon and Logoff which will make the program connect to a given user account and disconnect from it.
To test, first logon using another account and create a file somewhere, for example in the documents. Then logon back to your normal user code and launch the demo program (code below). Fill username, domain and password (For domain, "." will authenticate only on local computer). Fill the filename with complete path of the file you created previously. The click "file access". It should answer "file not found". Then click "Impersonate" and again "File Access". Now you should have access to the file in the other account. Click on "Revert to self" and try again "File Access" it should fail again.
In summary, for your question, the data the user cannot see must be created under another account and you application impersonate that other account when it needs to access the data. Don't forget to somehow hide username and password in your program.
Note: Once you get a handle (file or stream opened), you can RevertToSelf and still use the handle (or stream). It keeps the security context (the account used) until closed. This means you can call Logon before opening the file, call logoff right after opening (or failure of opening) and continue to access the file.
EDIT: I wrote a blog post with more code.
unit ImpersonateUser;
interface
uses
Winapi.Windows, System.Classes;
const
LOGON32_LOGON_NEW_CREDENTIALS = 9; // Missing in Delphi
type
TImpersonateUser = class(TComponent)
protected
FUserToken : THandle;
FErrorCode : DWORD;
public
destructor Destroy; override;
function Logon(const UserName : String;
const Domain : String;
const Password : String) : Boolean;
procedure Logoff();
property ErrorCode : DWORD read FErrorCode;
end;
implementation
{ TImpersonateUser }
destructor TImpersonateUser.Destroy;
begin
if FUserToken <> 0 then begin
CloseHandle(FUserToken);
FUserToken := 0;
end;
inherited Destroy;
end;
procedure TImpersonateUser.Logoff;
begin
if FUserToken <> 0 then begin
RevertToSelf(); // Revert to our user
CloseHandle(FUserToken);
FUserToken := 0;
end;
end;
function TImpersonateUser.Logon(
const UserName : String;
const Domain : String;
const Password : String): Boolean;
var
LoggedOn : Boolean;
begin
Result := FALSE;
if FUserToken <> 0 then
Logoff();
if UserName = '' then begin // Must at least provide a user name
FErrorCode := ERROR_BAD_ARGUMENTS;
Exit;
end;
if Domain <> '' then
LoggedOn := LogonUser(PChar(UserName),
PChar(Domain),
PChar(Password),
LOGON32_LOGON_INTERACTIVE,
LOGON32_PROVIDER_DEFAULT,
FUserToken)
else
LoggedOn := LogonUser(PChar(UserName),
PChar(Domain),
PChar(Password),
LOGON32_LOGON_NEW_CREDENTIALS,
LOGON32_PROVIDER_WINNT50,
FUserToken);
if not LoggedOn then begin
FErrorCode := GetLastError();
Exit;
end;
if not ImpersonateLoggedOnUser(FUserToken) then begin
FErrorCode := GetLastError();
Exit;
end;
FErrorCode := S_OK;
Result := TRUE;
end;
end.
Simple demo:
unit ImpersonateUserDemoMain;
interface
uses
Winapi.Windows, Winapi.Messages,
System.SysUtils, System.Variants, System.Classes,
Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls,
ImpersonateUser;
type
TImpersonateUserMainForm = class(TForm)
Label1: TLabel;
Label2: TLabel;
Label3: TLabel;
UserNameEdit: TEdit;
DomainEdit: TEdit;
PasswordEdit: TEdit;
ImpersonateButton: TButton;
Label4: TLabel;
FileNameEdit: TEdit;
RevertToSelfButton: TButton;
FileAccessButton: TButton;
procedure FileAccessButtonClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure ImpersonateButtonClick(Sender: TObject);
procedure RevertToSelfButtonClick(Sender: TObject);
private
FImpersonate : TImpersonateUser;
end;
var
ImpersonateUserMainForm: TImpersonateUserMainForm;
implementation
{$R *.dfm}
procedure TImpersonateUserMainForm.FileAccessButtonClick(Sender: TObject);
var
Stream : TFileStream;
begin
try
if not FileExists(FileNameEdit.Text) then
ShowMessage('File not found')
else begin
Stream := TFileStream.Create(FileNameEdit.Text, fmOpenRead);
try
ShowMessage('File opened');
finally
Stream.Free;
end;
end;
except
on E:Exception do
ShowMessage(E.Classname + ': ' + E.Message);
end;
end;
procedure TImpersonateUserMainForm.FormCreate(Sender: TObject);
begin
UserNameEdit.Text := 'YourUsername';
DomainEdit.Text := '.';
PasswordEdit.Text := 'YourPassword';
FilenameEdit.Text := 'C:\Users\AnotherUser\Documents\HelloWorld.txt';
FImpersonate := TImpersonateUser.Create(Self);
end;
procedure TImpersonateUserMainForm.ImpersonateButtonClick(Sender: TObject);
begin
if not FImpersonate.Logon(UserNameEdit.Text,
DomainEdit.Text,
PasswordEdit.Text) then begin
ShowMessage(Format('Failed with error 0x%X', [FImpersonate.ErrorCode]));
end
else
ShowMessage('Logon OK');
end;
procedure TImpersonateUserMainForm.RevertToSelfButtonClick(Sender: TObject);
begin
FImpersonate.Logoff;
ShowMessage('Reverted to self');
end;
end.
The DFM file:
object ImpersonateUserMainForm: TImpersonateUserMainForm
Left = 0
Top = 0
Caption = 'ImpersonateUserMainForm'
ClientHeight = 142
ClientWidth = 331
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'Tahoma'
Font.Style = []
OldCreateOrder = False
OnCreate = FormCreate
PixelsPerInch = 96
TextHeight = 13
object Label1: TLabel
Left = 16
Top = 20
Width = 49
Height = 13
Caption = 'UserName'
end
object Label2: TLabel
Left = 16
Top = 48
Width = 35
Height = 13
Caption = 'Domain'
end
object Label3: TLabel
Left = 12
Top = 76
Width = 46
Height = 13
Caption = 'Password'
end
object Label4: TLabel
Left = 16
Top = 104
Width = 16
Height = 13
Caption = 'File'
end
object UserNameEdit: TEdit
Left = 80
Top = 16
Width = 121
Height = 21
TabOrder = 0
Text = 'UserNameEdit'
end
object DomainEdit: TEdit
Left = 80
Top = 44
Width = 121
Height = 21
TabOrder = 1
Text = 'DomainEdit'
end
object PasswordEdit: TEdit
Left = 80
Top = 72
Width = 121
Height = 21
TabOrder = 2
Text = 'PasswordEdit'
end
object ImpersonateButton: TButton
Left = 232
Top = 14
Width = 75
Height = 25
Caption = 'Impersonate'
TabOrder = 3
OnClick = ImpersonateButtonClick
end
object FileNameEdit: TEdit
Left = 80
Top = 99
Width = 121
Height = 21
TabOrder = 4
Text = 'FileNameEdit'
end
object RevertToSelfButton: TButton
Left = 232
Top = 45
Width = 75
Height = 25
Caption = 'Revert to self'
TabOrder = 5
OnClick = RevertToSelfButtonClick
end
object FileAccessButton: TButton
Left = 232
Top = 76
Width = 75
Height = 25
Caption = 'File access'
TabOrder = 6
OnClick = FileAccessButtonClick
end
end
I have a strage effekt in Delphi XE8 and would like to know if anyone can reproduce this and has an explanation for it!
I'm calling the windows api function GetClipboardFormatName with a local variable as a buffer to receive the clipboard format names.
When this is done from a TButton Click Handler it works as expected, when it's done from a TToolButton Click Handler then it does not work and getlasterror returns 998 / ERROR_NOACCESS / Invalid access to memory location.
This did not happen under Delphi 7!
I'm not looking for a workaround, i just would like to know what is going on here. Am I doing something wrong? Is there a problem with our IDE installation (2 Developers)? Is it a BUG in XE8?
Here is a demo unit:
DFM File
object Form3: TForm3
Left = 0
Top = 0
Caption = 'Form3'
ClientHeight = 311
ClientWidth = 643
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'Tahoma'
Font.Style = []
OldCreateOrder = False
PixelsPerInch = 96
TextHeight = 13
object Panel1: TPanel
Left = 0
Top = 0
Width = 643
Height = 41
Align = alTop
Caption = 'Panel1'
TabOrder = 0
object Button1: TButton
Left = 16
Top = 10
Width = 148
Height = 25
Caption = 'Standard TButton ==> OK'
TabOrder = 0
OnClick = Button1Click
end
end
object Memo1: TMemo
Left = 0
Top = 70
Width = 643
Height = 241
Align = alClient
Lines.Strings = (
'Memo1')
TabOrder = 1
end
object ToolBar1: TToolBar
Left = 0
Top = 41
Width = 643
Height = 29
ButtonHeight = 21
ButtonWidth = 289
Caption = 'ToolBar1'
ShowCaptions = True
TabOrder = 2
object ToolButton1: TToolButton
Left = 0
Top = 0
Caption = 'Standard TToolBar / TToolButton ==> ERROR_NOACCESS'
ImageIndex = 0
OnClick = ToolButton1Click
end
end
end
PAS File
unit Unit3;
interface
uses
Winapi.Windows,
Winapi.Messages,
System.SysUtils,
System.Variants,
System.Classes,
Vcl.Graphics,
Vcl.Controls,
Vcl.Forms,
Vcl.Dialogs,
Vcl.StdCtrls,
Vcl.ExtCtrls,
Vcl.ComCtrls,
Vcl.ToolWin;
type
TForm3 = class(TForm)
Panel1: TPanel;
Memo1: TMemo;
Button1: TButton;
ToolBar1: TToolBar;
ToolButton1: TToolButton;
procedure Button1Click(Sender: TObject);
procedure ToolButton1Click(Sender: TObject);
private
procedure say(s: string);
procedure ListFormats;
function GetRegisteredClipBoardFormatName(Format: word): string;
function IsPredefinedFormat(format: word): boolean;
{ Private-Deklarationen }
public
{ Public-Deklarationen }
end;
var
Form3: TForm3;
implementation
uses
clipbrd;
const arPredefinedFormats: array[0..27] of word = (
CF_TEXT,
CF_BITMAP,
CF_METAFILEPICT,
CF_SYLK,
CF_DIF,
CF_TIFF,
CF_OEMTEXT,
CF_DIB,
CF_PALETTE,
CF_PENDATA,
CF_RIFF,
CF_WAVE,
CF_UNICODETEXT,
CF_ENHMETAFILE,
CF_HDROP,
CF_LOCALE,
CF_MAX,
CF_DIBV5,
CF_MAX_XP,
CF_OWNERDISPLAY,
CF_DSPTEXT,
CF_DSPBITMAP,
CF_DSPMETAFILEPICT,
CF_DSPENHMETAFILE,
CF_PRIVATEFIRST,
CF_PRIVATELAST,
CF_GDIOBJFIRST,
CF_GDIOBJLAST);
{$R *.dfm}
procedure TForm3.ToolButton1Click(Sender: TObject);
begin
ListFormats;
end;
procedure TForm3.Button1Click(Sender: TObject);
begin
ListFormats;
end;
procedure TForm3.ListFormats;
var
index: integer;
begin
for index := 0 to clipboard.formatcount - 1 do
begin
if not IsPredefinedFormat(clipboard.formats[index]) then
begin
say('Format: ' + inttostr(clipboard.formats[index]));
say('Name: ' + GetRegisteredClipBoardFormatName(clipboard.formats[index]));
end;
end;
end;
procedure TForm3.say(s: string);
begin
memo1.lines.add(s);
end;
function TForm3.IsPredefinedFormat(format: word): boolean;
var
index: integer;
begin
for index := low(arPredefinedFormats) to high(arPredefinedFormats) do
begin
if arPredefinedFormats[index] = format then
begin
result := true;
exit;
end;
end;
result := false;
end;
//------------------------------------------------------------------------------------------
(*
Strange effekt in function GetClipboardFormatName
when compiled with Delphi XE8 und Win 7.
If this function is called from tbutton click, then everything ist ok!
If this function is called from ttoolbutton click (and perhaps other controls...?)
then the call to GetClipboardFormatName fails with getlasterror = 998
which means
ERROR_NOACCESS
998 (0x3E6)
Invalid access to memory location.
which indicates that there is a problem with the local variable fmtname.
Some Facts...
* effekt happens under delphi xe8
* effekt did not happen under delphi 7
* it doesn't matter if I zero the memory of fmtname before using it.
* it doesn't matter if I call OpenClipboard / CloseClipboard
* if I use a local variable, then it does not work with ttoolbutton. The memorylocation of the local variable is
slightly different from the case when it's called from tbutton.
* if I use a global variable instead of a local variable, then it works with tbutton and ttoolbutton
since it's the same memorylocation for both calls
I'm NOT LOOKING FOR A WORKAROUND, I just would like to know if anybody can
reproduce the behaviour and has an explanation as to why this is happening.
Is there something wrong with using local variables for windows api calls in general?
*)
//------------------------------------------------------------------------------------------
function TForm3.GetRegisteredClipBoardFormatName(Format: word): string;
var
fmtname: array[0..1024] of Char;
begin
if OpenClipboard(self.handle) then //<--- does not make a difference if called or not
begin
if GetClipboardFormatName(Format, fmtname, SizeOf(fmtname)) <> 0 then
begin
result := fmtname;
end else
begin
result := 'Unknown Clipboard Format / GetLastError= ' + inttostr(getlasterror);
end;
CloseClipboard;
end else say('OpenClipboard failed');
end;
//------------------------------------------------------------------------------------------
end.
Your code is broken. The error is here:
GetClipboardFormatName(Format, fmtname, SizeOf(fmtname))
The documentation of GetClipboardFormatName describes the cchMaxCount parameter like this:
The maximum length, in characters, of the string to be copied to the buffer. If the name exceeds this limit, it is truncated.
You are passing the length in bytes rather than the length in characters. In Delphi 7 Char is an alias for AnsiChar, an 8 bit type, but in Unicode Delphi, 2009 and later, Char is an alias for WideChar, a 16 bit type.
As a consequence, under XE8 which is a Unicode Delphi, you are claiming that the buffer is twice as long as it actually is.
You must replace SizeOf(fmtname) with Length(fmtname).
I should also mention that the change from 8 bit ANSI to 16 bit UTF-16 Unicode in Delphi 2009 should always be your first suspect when you find behaviour difference between ANSI Delphi and Unicode Delphi. In your question you wondered whether this was a Delphi bug, or an installation issue, but the first thought into your head should have been an issue with text encoding. With the reported symptoms that is going to be the culprit almost every time.
As an aside, it makes no real sense for GetRegisteredClipBoardFormatName to be an instance method of a GUI form. It doesn't refer to Self and it has nothing at all to do with your form class. This should be a low-level helper method that is not part of a GUI form type.
I expect to see fully initialized UI with a label showing "Data is not loaded", then after 5 seconds "Data is loaded".
I've taken an example from there: What is the best way to autostart an action after OnShow event?
but I see nothing, and then only "Data is loaded", after the delay.
Code is listed below:
StartupAction.dpr
program StartupAction;
uses
Vcl.Forms,
MainForm in 'MainForm.pas' {Form1};
{$R *.res}
begin
Application.Initialize;
Application.MainFormOnTaskbar := True;
Application.CreateForm(TForm1, Form1);
Application.Run;
end.
MainForm.dfm
object Form1: TForm1
Left = 0
Top = 0
Caption = 'Form1'
ClientHeight = 254
ClientWidth = 635
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'Tahoma'
Font.Style = []
OldCreateOrder = False
PixelsPerInch = 96
TextHeight = 13
object lbl1: TLabel
Left = 104
Top = 40
Width = 87
Height = 13
Caption = 'Data is not loaded'
end
end
MainForm.pas
unit MainForm;
//Load data after full UI initialization
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;
type
TForm1 = class(TForm)
lbl1: TLabel;
private const
WM_STARTUP = WM_USER;
protected
procedure DoShow(); override;
private
procedure WMStartup(var Msg: TMessage); message WM_STARTUP;
procedure _loadData();
public
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.DoShow();
begin
inherited;
PostMessage(Handle, WM_STARTUP, 0, 0);
end;
procedure TForm1.WMStartup(var Msg: TMessage);
begin
inherited;
_loadData();
end;
procedure TForm1._loadData();
begin
Sleep(5000);
lbl1.Caption := 'Data is loaded';
end;
end.
The only solution that works to some extent is using of TTimer with 500 ms delay, but it is not a true solution.
I need to draw teechart look like the picture.
how to draw red line on teechart of delphi?
In addition to a TLineSeries for the line data, add a TFastLineSeries with the Stairs property set to true to create the dashed lines that mark the specified data point. (An image of the resulting chart is attached at the end of answer).
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, ExtCtrls, TeeProcs, TeEngine, Chart, Series;
type
TForm1 = class(TForm)
Chart1: TChart;
LineSeries: TLineSeries;
StairSeries: TFastLineSeries;
procedure FormCreate(Sender: TObject);
procedure StairSeriesGetMarkText(Sender: TChartSeries; ValueIndex: Integer; var MarkText: string);
private
BottomMinimum : double;
LeftMinimum : double;
DataPoint : double;
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
function Fx(x : double) : double;
begin
Result := ((x - 1.5) * 20) + 17;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
LineSeries.Clear;
StairSeries.Clear;
LeftMinimum := 12;
BottomMinimum := 1.25;
DataPoint := 1.68;
LineSeries.AddXY( 1.5, Fx(1.5) );
LineSeries.AddXY( 1.6, Fx(1.6) );
LineSeries.AddXY( 1.7, Fx(1.7) );
StairSeries.AddXY( BottomMinimum, Fx(DataPoint) );
StairSeries.AddXY( DataPoint, Fx(DataPoint) );
StairSeries.AddXY( DataPoint, LeftMinimum );
Chart1.LeftAxis.SetMinMax( LeftMinimum, LeftMinimum + 12 );
Chart1.BottomAxis.SetMinMax( BottomMinimum, BottomMinimum + 0.75 );
end;
procedure TForm1.StairSeriesGetMarkText(Sender: TChartSeries; ValueIndex: Integer; var MarkText: string);
begin
if valueIndex = 0 then
MarkText := ' '+Format('%5.2n',[ Fx(DataPoint) ])
else if valueIndex = 1 then
MarkText := ''
else
MarkText := ' '+Format('%5.2n',[ DataPoint ])
end;
end.
The DFM:
object Form1: TForm1
Left = 234
Top = 127
Width = 602
Height = 533
Caption = 'Form1'
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'Tahoma'
Font.Style = []
OldCreateOrder = False
OnCreate = FormCreate
PixelsPerInch = 96
TextHeight = 13
object Chart1: TChart
Left = 56
Top = 24
Width = 465
Height = 409
BackWall.Brush.Color = clWhite
BackWall.Brush.Style = bsClear
Title.Text.Strings = (
'TChart')
Legend.Visible = False
View3D = False
TabOrder = 0
object LineSeries: TLineSeries
Marks.ArrowLength = 8
Marks.Visible = False
SeriesColor = clBlue
LinePen.Color = clBlue
LinePen.Width = 4
Pointer.InflateMargins = True
Pointer.Style = psCircle
Pointer.Visible = True
XValues.DateTime = False
XValues.Name = 'X'
XValues.Multiplier = 1.000000000000000000
XValues.Order = loAscending
YValues.DateTime = False
YValues.Name = 'Y'
YValues.Multiplier = 1.000000000000000000
YValues.Order = loNone
end
object StairSeries: TFastLineSeries
Marks.ArrowLength = 8
Marks.Transparent = True
Marks.Frame.Visible = False
Marks.Visible = True
SeriesColor = clRed
OnGetMarkText = StairSeriesGetMarkText
LinePen.Color = clRed
LinePen.Style = psDash
LinePen.Width = 2
XValues.DateTime = False
XValues.Name = 'X'
XValues.Multiplier = 1.000000000000000000
XValues.Order = loAscending
YValues.DateTime = False
YValues.Name = 'Y'
YValues.Multiplier = 1.000000000000000000
YValues.Order = loNone
end
end
end
Note: This is a Delphi 7 example that I originally built using Delphi 10.2 and tested it with Delphi 2007. I downgraded the code to Delphi 7.
To use in Delphi 2007 or higher change the code that sets the Axes MinMax to this:
Chart1.Axes.Left.SetMinMax(LeftMinimum, LeftMinimum+12);
Chart1.Axes.Bottom.SetMinMax(BottomMinimum, BottomMinimum+0.75);
The resulting chart should look like this: