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:
Related
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'm trying to emulate pushing the keyboard V key down while using BASS library to automate Voice Activation push to talk. I've got the BASS library working, just can't get the keyboard to simulate holding down a Key for any length of time!
Edit:
I am trying to get another application ('TeamSpeak 3') to recognize my Key Press & hold as a hardware based Key Press & Hold rather than a software based Key Press & Hold. To help simulate a Push to Talk via my application. I will openly have the source code for anyone that wants it, but I will not be publishing my application for any reason. It's for my personal use and It's out of curiosity if it would work? I understand that any kind of abuse of this kind of app I take as my own personal responsibility.
Edit2: I have done extensive research. I figure I'm going to have to either use my old Android handheld or a Raspberry Pi. I have a Raspberry Pi Zero, so I am going to see if I can create it as a hardware keyboard. I'll write a program in Delphi to interface it (I have Delphi 10.4.1 Enterprise and hope it will work with Raspberry Pi's linux version.) I have a vmware Debian and Ubuntu os on my computer that I could pre-compile it with? Anyhow the article is here: https://randomnerdtutorials.com/raspberry-pi-zero-usb-keyboard-hid/
I'm going to go ahead an allow the answer below, because it basically does what my previous request says. To go further than my request requires a lot of work. I'll give an update if I can get it working properly.
(Delphi 10.4.1 / Target Windows 32-bit)
Here's my current source code:
unit Unit1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.MPlayer, System.UITypes, BASS,
Vcl.ExtCtrls;
type
TForm1 = class(TForm)
Button1: TButton;
Button2: TButton;
Memo1: TMemo;
Timer1: TTimer;
ComboBox1: TComboBox;
Timer2: TTimer;
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure Timer1Timer(Sender: TObject);
procedure ComboBox1Change(Sender: TObject);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
procedure Timer2Timer(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
function RecordingCallback(h:HRECORD; b:Pointer; l,u: DWord): boolean; stdcall;
end;
var
Form1: TForm1;
rchan: HRECORD; // recording channel
level2: dword;
LoudEnough: boolean = FALSE;
threshold: DWORD = 500; // trigger level
MicON_Timer, Counter1: Cardinal;
MicON_Bool : Boolean;
implementation
{$R *.dfm}
(* This function called while recording audio *)
function TForm1.RecordingCallback(h:HRECORD; b:Pointer; l,u: DWord): boolean; stdcall;
//var level:dword;
begin
level2:=BASS_ChannelGetLevel(h);
LoudEnough := (LoWord(level2) >= threshold) or (HiWord(level2) >= threshold);
//Memo1.Lines.add('Loword ' + IntToStr(LoWord(level))+' - HiWord '+IntToStr(HiWord(level)));
Result := True;
end;
// START BUTTON
procedure TForm1.Button1Click(Sender: TObject);
begin
{
if BASS_RecordSetDevice(0) = false then
begin
memo1.Lines.Add('BASS_RecordSetDevice ERROR = '+ BASS_ErrorGetCode().ToString);
end;}
Counter1 := 0;
MicON_Timer := 0;
Timer1.Enabled := true;
ComboBox1Change(Self);
rchan := BASS_RecordStart(44100, 1, 0, #TForm1.RecordingCallback, nil);
end;
procedure TForm1.Button2Click(Sender: TObject);
begin
Timer1.Enabled := false;
rchan := BASS_RecordStart(44100, 1, BASS_RECORD_PAUSE, #TForm1.RecordingCallback, nil);
//BASS_Free();
end;
procedure TForm1.ComboBox1Change(Sender: TObject);
var
i: Integer;
r: Boolean;
begin
// enable the selected input
r := True;
i := 0;
// first disable all inputs, then...
while r do
begin
r := BASS_RecordSetInput(i, BASS_INPUT_OFF, -1);
Inc(i);
end;
// ...enable the selected.
BASS_RecordSetInput(ComboBox1.ItemIndex, BASS_INPUT_ON, -1);
//UpdateInputInfo; // update info
end;
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
BASS_RecordFree;
BASS_Free();
end;
procedure TForm1.FormCreate(Sender: TObject);
var
i: Integer;
dName: PAnsiChar;
level: Single;
flags: dWord;
deviceInfo: BASS_DEVICEINFO;
info: BASS_INFO;
begin
// check the correct BASS was loaded
if (HIWORD(BASS_GetVersion) <> BASSVERSION) then
begin
MessageBox(0,'An incorrect version of BASS.DLL was loaded', nil,MB_ICONERROR);
Halt;
end;
if (not BASS_RecordInit(-1)) or (not BASS_Init(-1, 44100, 0, Handle, nil)) then
begin
BASS_RecordFree;
BASS_Free();
MessageDlg('Cannot start default recording device!', mtError, [mbOk], 0);
Halt;
end;
i := 0;
// dName := BASS_RecordGetInputName(i);
//dName := (BASS_RecordGetDeviceInfo(i,deviceInfo));
while (BASS_RecordGetDeviceInfo(i,deviceInfo)) do
begin
//BASS_GetInfo(info);
ComboBox1.Items.Add(String(deviceInfo.name));
// is this one currently "on"?
//flags := BASS_RecordGetInput(i, level);
//if (flags and BASS_INPUT_TYPE_MASK) = BASS_INPUT_TYPE_MIC then
if (BASS_RecordGetInput(i, level) and BASS_INPUT_OFF) = 0 then
ComboBox1.ItemIndex := i;
Inc(i);
//dName := BASS_RecordGetInputName(i);
end;
ComboBox1Change(Self); // display info
end;
procedure TForm1.Timer1Timer(Sender: TObject);
var
eu: array [0..1] of TInput;
//S: String;
begin
//S:='v';
level2:=BASS_ChannelGetLevel(rchan);
inc(Counter1);
LoudEnough := (LoWord(level2) >= threshold) or (HiWord(level2) >= threshold);
if (LoudEnough = true) then
begin
inc(MicON_Timer);
if (MicON_Bool = false) then
begin
MicON_Bool := true;
//keybd_event(ord('v'), MapVirtualKey(ord('v'), 0), KEYEVENTF_KEYUP, 0);
//keybd_event(ord('v'), MapVirtualKey(ord('v'), 0), 0, 0);
ZeroMemory(#eu,sizeof(eu));
eu[0].Itype := INPUT_KEYBOARD;
eu[0].ki.dwFlags := KEYEVENTF_UNICODE;
eu[0].ki.wVk := 0;
eu[0].ki.wScan := ord('v');
eu[0].ki.Time := 0;
SendInput(1,eu[0],sizeof(TInput));
Memo1.Lines.add('Push to Talk ON');
Timer2.Enabled := true;
end;
end;
//if LoudEnough then Memo1.Lines.add('Push to Talk ON')
//else Memo1.Lines.add('Push to Talk OFF');
//Memo1.Lines.add('Loword ' + LoWord(level2).ToString +' - HiWord '+ HiWord(level2).ToString + ' - AVG: ' + MicON_Timer.ToString);
end;
procedure TForm1.Timer2Timer(Sender: TObject);
var
eu: array [0..1] of TInput;
begin
dec(MicON_Timer);
if MicON_Timer <= 0 then
begin
Memo1.Lines.add('Push to Talk OFF');
//keybd_event(ord('v'), MapVirtualKey(ord('v'), 0), KEYEVENTF_KEYUP, 0);
ZeroMemory(#eu,sizeof(eu));
eu[0].Itype := INPUT_KEYBOARD;
eu[0].ki.dwFlags := KEYEVENTF_UNICODE or KEYEVENTF_KEYUP;
eu[0].ki.wVk := 0;
eu[0].ki.wScan := ord('v');
eu[0].ki.Time := 0;
SendInput(1,eu[0],sizeof(TInput));
MicON_Bool := false;
Counter1 := 0;
MicON_Timer := 0;
Timer2.Enabled := false;
end;
end;
end.
I designed a simple example where when the user click the mouse on a TButton, it simulate a keystroke every 250mS until the user release the mouse button.
The OnMouseButtonDown starts a 250mS timer, the OnMouseButtonUp stop the timer. The OnTimer send the keyboard event. The timer is also stopped when the mouse leave the form.
The .PAS file:
unit KbdEmulDemoMain;
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
TForm1 = class(TForm)
Button1: TButton;
Timer1: TTimer;
Memo1: TMemo;
procedure Button1MouseDown(Sender: TObject; Button: TMouseButton; Shift:
TShiftState; X, Y: Integer);
procedure Button1MouseLeave(Sender: TObject);
procedure Button1MouseUp(Sender: TObject; Button: TMouseButton; Shift:
TShiftState; X, Y: Integer);
procedure Timer1Timer(Sender: TObject);
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.Button1MouseDown(
Sender : TObject;
Button : TMouseButton;
Shift : TShiftState;
X, Y : Integer);
begin
// Set focus on Memo1 so that it will receive keyboard input
Memo1.SetFocus;
// Start the timer sending keyboard event
Timer1.Interval := 250;
Timer1.Enabled := TRUE;
// Call OnTimer immediately to key first key event right now
Timer1.OnTimer(nil);
end;
procedure TForm1.Button1MouseUp(
Sender : TObject;
Button : TMouseButton;
Shift : TShiftState;
X, Y : Integer);
begin
// Stop timer, this will stop key event
Timer1.Enabled := FALSE;
end;
procedure TForm1.Button1MouseLeave(Sender: TObject);
begin
// Stop timer, this will stop key event
Timer1.Enabled := FALSE;
end;
procedure TForm1.Timer1Timer(Sender: TObject);
var
Eu: array [0..1] of TInput;
begin
ZeroMemory(#Eu, SizeOf(Eu));
Eu[0].Itype := INPUT_KEYBOARD;
Eu[0].ki.dwFlags := KEYEVENTF_UNICODE;
Eu[0].ki.wVk := 0;
Eu[0].ki.wScan := Ord('v');
Eu[0].ki.Time := 0;
SendInput(1, Eu[0], Sizeof(TInput));
end;
end.
And the DFM file:
object Form1: TForm1
Left = 0
Top = 0
Caption = 'Form1'
ClientHeight = 299
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 Button1: TButton
Left = 24
Top = 28
Width = 75
Height = 25
Caption = 'Button1'
TabOrder = 0
OnMouseDown = Button1MouseDown
OnMouseLeave = Button1MouseLeave
OnMouseUp = Button1MouseUp
end
object Memo1: TMemo
Left = 20
Top = 76
Width = 605
Height = 213
Lines.Strings = (
'Memo1')
TabOrder = 1
end
object Timer1: TTimer
OnTimer = Timer1Timer
Left = 168
Top = 24
end
end
What about SendKeys.Send ?
Supposing the target application has no DCOM equivalent, as SendKeys.Send targets the active application, so if the focus is changed by another application you do not obtain the desired results.
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.
Currently I have followed the example present in the documentation.
Specifically the example named:
Example: Stored Procedure Returning Multiple Rowsets
This is the delphi code translated from the above example:
//Copied from ADODB.pas
function CreateADOObject(const ClassID: TGUID): IUnknown;
var
Status: HResult;
FPUControlWord: Word;
begin
asm
FNSTCW FPUControlWord
end;
Status := CoCreateInstance(ClassID, nil, CLSCTX_INPROC_SERVER or
CLSCTX_LOCAL_SERVER, IUnknown, Result);
asm
FNCLEX
FLDCW FPUControlWord
end;
if (Status = REGDB_E_CLASSNOTREG) then
raise Exception.CreateRes(#SADOCreateError) else
OleCheck(Status);
end;
var
Con: TADOConnection;
RSet: _Recordset;
Cmd: _Command;
P1, P2: _Parameter;
i: integer;
RecordsAffected: OleVariant;
begin
Con := TADOConnection.Create(nil);
try
Con.ConnectionString := PromptDataSource(Handle, '');
Con.Open('user', 'password');
Cmd := CreateADOObject(CLASS_Command) as _Command;
try
Cmd.Set_ActiveConnection(Con.ConnectionObject);
P1 := Cmd.CreateParameter('P1', adSmallInt, adParamInput, 0, 1);
Cmd.Parameters.Append(P1);
P2 := Cmd.CreateParameter('P2', adSmallInt, adParamOutput, 0, EmptyParam);
Cmd.Parameters.Append(P2);
Cmd.Properties['PLSQLRSet'].Value := True;
Cmd.CommandType := adCmdText;
Cmd.CommandText := '{CALL Employees.GetEmpRecords(?, ?)}';
RSet:= CreateADOObject(CLASS_Recordset) as _RecordSet;
try
//If I use Execute, the CursorLocation will be adUseServer
RSet.Open(Cmd, EmptyParam, adUseClient, adOpenStatic, adCmdText);
ShowMessage(IntToStr(RSet.RecordCount));
for I:= 0 to RSet.Fields.Count-1 do
begin
showmessage(vartostr(RSet.Fields.Item[i].Name)+': '+vartostr(RSet.Fields.Item[i].Value));
end;
Cmd.Properties['PLSQLRSet'].Value := False;
finally
RSet := nil;
end;
finally
Cmd := nil;
end;
finally
Con.Free;
end;
end;
NOTE: I changed the procedure to return only one cursor!
Question:
1.a) How Can I fill a DataSet from the recordset?
Or
1.b) Is there another way to capture out cursor parameters from an oracle procedure through ADO in Delphi?
UPDATE 1:
As suggested by MartynA I tried to use TADQuery, but unsuccessfully so far:
object ADOQuery1: TADOQuery
Connection = ADOConnection1
CursorType = ctStatic
Parameters = <
item
Name = 'param0'
DataType = ftUnknown
Direction = pdOutput
Size = -1
Value = Null
end
item
Name = 'param1'
DataType = ftSmallint
Size = -1
Value = 7084
end
item
Name = 'param2'
DataType = ftSmallint
Direction = pdOutput
Size = -1
Value = Null
end>
SQL.Strings = (
'{CALL Employees.GetEmpRecords(:param0, :param1, :param2)}')
Left = 560
Top = 96
end
procedure TForm1.ADOConnection1WillExecute(Connection: TADOConnection;
var CommandText: WideString; var CursorType: TCursorType;
var LockType: TADOLockType; var CommandType: TCommandType;
var ExecuteOptions: TExecuteOptions; var EventStatus: TEventStatus;
const Command: _Command; const Recordset: _Recordset);
begin
CommandType := cmdText;
Command.Properties['PLSQLRSet'].Value := True;
end;
You can assign directly your recordset to an ADODataset
ADODataset := TADODataset.Create(nil);
ADODataset.Recordset := RecordsetData;
It is very simple to answer the first :
RSet.Open(Cmd, EmptyParam, adUseClient, adOpenStatic, adCmdText);
DataSet := TADOQuery.Create(nil);
try
DataSet.Recordset := RSet;
...
When you 'SET' the RecordSet into the DataSet, it is automatically filled with recordset data.
Please see the attached screenshot which illustrates a TToolBar from one of my programs:
Notice the last two images of the Toolbar, they are disabled. The way they have been drawn to appear disabled is not very appealing, in fact in the Delphi IDE some of the images look the same.
The issue I have with it is I want my application to look a lot cleaner. The way the disabled items are drawn doesn't look very good. The TToolBar allows to set a disabled TImageList, I tried making my images black & white but they didn't look right, and would rather not have to always make the images black and white (time and effort). This problem also shows in my menus and popup menus, which don't allow for disabled images anyway.
Is there a way to paint the disabled items to look better on the eye?
If possible I would rather not look to use 3rd Party Controls. I know the Jedi Components allow disabled images for the menu etc, but would prefer a way to not resort too 3rd Party Components, when possible I would much prefer to use the standard issue VCL, especially as sometimes I use the TActionMainMenuBar to draw Office Style menus, which match the TToolBar when DrawingStyle is set to gradient.
EDIT
I have accepted RRUZ's answer, is it possible though to accept David's answer as well, both are very good answers and would like the answer to be shared between them if possible.
Thanks.
Sometime Ago i wrote a patch to fix this behavior. the key is patch the code of the TCustomImageList.DoDraw function, the technique used is similar to the used by the delphi-nice-toolbar app, but instead of patch a bpl IDE in this case we patch the function in memory.
Just include this unit in your project
unit uCustomImageDrawHook;
interface
uses
Windows,
SysUtils,
Graphics,
ImgList,
CommCtrl,
Math;
implementation
type
TJumpOfs = Integer;
PPointer = ^Pointer;
PXRedirCode = ^TXRedirCode;
TXRedirCode = packed record
Jump: Byte;
Offset: TJumpOfs;
end;
PAbsoluteIndirectJmp = ^TAbsoluteIndirectJmp;
TAbsoluteIndirectJmp = packed record
OpCode: Word;
Addr: PPointer;
end;
TCustomImageListHack = class(TCustomImageList);
var
DoDrawBackup : TXRedirCode;
function GetActualAddr(Proc: Pointer): Pointer;
begin
if Proc <> nil then
begin
if (Win32Platform = VER_PLATFORM_WIN32_NT) and (PAbsoluteIndirectJmp(Proc).OpCode = $25FF) then
Result := PAbsoluteIndirectJmp(Proc).Addr^
else
Result := Proc;
end
else
Result := nil;
end;
procedure HookProc(Proc, Dest: Pointer; var BackupCode: TXRedirCode);
var
n: DWORD;
Code: TXRedirCode;
begin
Proc := GetActualAddr(Proc);
Assert(Proc <> nil);
if ReadProcessMemory(GetCurrentProcess, Proc, #BackupCode, SizeOf(BackupCode), n) then
begin
Code.Jump := $E9;
Code.Offset := PAnsiChar(Dest) - PAnsiChar(Proc) - SizeOf(Code);
WriteProcessMemory(GetCurrentProcess, Proc, #Code, SizeOf(Code), n);
end;
end;
procedure UnhookProc(Proc: Pointer; var BackupCode: TXRedirCode);
var
n: Cardinal;
begin
if (BackupCode.Jump <> 0) and (Proc <> nil) then
begin
Proc := GetActualAddr(Proc);
Assert(Proc <> nil);
WriteProcessMemory(GetCurrentProcess, Proc, #BackupCode, SizeOf(BackupCode), n);
BackupCode.Jump := 0;
end;
end;
procedure Bitmap2GrayScale(const BitMap: TBitmap);
type
TRGBArray = array[0..32767] of TRGBTriple;
PRGBArray = ^TRGBArray;
var
x, y, Gray: Integer;
Row : PRGBArray;
begin
BitMap.PixelFormat := pf24Bit;
for y := 0 to BitMap.Height - 1 do
begin
Row := BitMap.ScanLine[y];
for x := 0 to BitMap.Width - 1 do
begin
Gray := (Row[x].rgbtRed + Row[x].rgbtGreen + Row[x].rgbtBlue) div 3;
Row[x].rgbtRed := Gray;
Row[x].rgbtGreen := Gray;
Row[x].rgbtBlue := Gray;
end;
end;
end;
//from ImgList.GetRGBColor
function GetRGBColor(Value: TColor): DWORD;
begin
Result := ColorToRGB(Value);
case Result of
clNone:
Result := CLR_NONE;
clDefault:
Result := CLR_DEFAULT;
end;
end;
procedure New_Draw(Self: TObject; Index: Integer; Canvas: TCanvas; X, Y: Integer; Style: Cardinal; Enabled: Boolean);
var
MaskBitMap : TBitmap;
GrayBitMap : TBitmap;
begin
with TCustomImageListHack(Self) do
begin
if not HandleAllocated then Exit;
if Enabled then
ImageList_DrawEx(Handle, Index, Canvas.Handle, X, Y, 0, 0, GetRGBColor(BkColor), GetRGBColor(BlendColor), Style)
else
begin
GrayBitMap := TBitmap.Create;
MaskBitMap := TBitmap.Create;
try
GrayBitMap.SetSize(Width, Height);
MaskBitMap.SetSize(Width, Height);
GetImages(Index, GrayBitMap, MaskBitMap);
Bitmap2GrayScale(GrayBitMap);
BitBlt(Canvas.Handle, X, Y, Width, Height, MaskBitMap.Canvas.Handle, 0, 0, SRCERASE);
BitBlt(Canvas.Handle, X, Y, Width, Height, GrayBitMap.Canvas.Handle, 0, 0, SRCINVERT);
finally
GrayBitMap.Free;
MaskBitMap.Free;
end;
end;
end;
end;
procedure HookDraw;
begin
HookProc(#TCustomImageListHack.DoDraw, #New_Draw, DoDrawBackup);
end;
procedure UnHookDraw;
begin
UnhookProc(#TCustomImageListHack.DoDraw, DoDrawBackup);
end;
initialization
HookDraw;
finalization
UnHookDraw;
end.
and the result will be
I submitted a QC report for a related issue over a year ago, but that was for menus. I've never seen this for TToolbar since it is a wrapper to the common control and the drawing is handled by Windows.
However, the images you are seeing are clearly as result of the VCL calling TImageList.Draw and passing Enabled=False – nothing else looks that bad! Are you 100% sure this really is a TToolbar?
The fix will surely be to avoid TImageList.Draw and call ImageList_DrawIndirect with the ILS_SATURATE.
You may need to modify some VCL source. First find the location where the toolbar is being custom drawn and call this routine instead of the calls to TImageList.Draw.
procedure DrawDisabledImage(DC: HDC; ImageList: TCustomImageList; Index, X, Y: Integer);
var
Options: TImageListDrawParams;
begin
ZeroMemory(#Options, SizeOf(Options));
Options.cbSize := SizeOf(Options);
Options.himl := ImageList.Handle;
Options.i := Index;
Options.hdcDst := DC;
Options.x := X;
Options.y := Y;
Options.fState := ILS_SATURATE;
ImageList_DrawIndirect(#Options);
end;
An even better fix would be to work out why the toolbar is being custom drawn and find a way to let the system do it.
EDIT 1
I've looked at the Delphi source code and I'd guess that you are custom drawing the toolbar, perhaps because it has a gradient. I never even knew that TToolbar could handle custom drawing but I'm just a plain vanilla kind of guy!
Anyway, I can see code in TToolBar.GradientDrawButton calling the TImageList.Draw so I think the explanation above is on the right track.
I'm fairly sure that calling my DrawDisabledImage function above will give you better results. If could find a way to make that happen when you call TImageList.Draw then that would, I suppose, be the very best fix since it would apply wholesale.
EDIT 2
Combine the function above with #RRUZ's answer and you have an excellent solution.
Solution from #RRUZ dosn't work if you use LargeImages in ActionToolBar. I made changes to the #RRUZ code to work with LargeImages in ActionToolBar.
unit unCustomImageDrawHook;
interface
uses
Windows,
SysUtils,
Graphics,
ImgList,
CommCtrl,
Math,
Vcl.ActnMan,
System.Classes;
implementation
type
TJumpOfs = Integer;
PPointer = ^Pointer;
PXRedirCode = ^TXRedirCode;
TXRedirCode = packed record
Jump: Byte;
Offset: TJumpOfs;
end;
PAbsoluteIndirectJmp = ^TAbsoluteIndirectJmp;
TAbsoluteIndirectJmp = packed record
OpCode: Word;
Addr: PPointer;
end;
TCustomImageListHack = class(TCustomImageList);
TCustomActionControlHook = class(TCustomActionControl);
var
DoDrawBackup : TXRedirCode;
DoDrawBackup2 : TXRedirCode;
function GetActualAddr(Proc: Pointer): Pointer;
begin
if Proc <> nil then
begin
if (Win32Platform = VER_PLATFORM_WIN32_NT) and (PAbsoluteIndirectJmp(Proc).OpCode = $25FF) then
Result := PAbsoluteIndirectJmp(Proc).Addr^
else
Result := Proc;
end
else
Result := nil;
end;
procedure HookProc(Proc, Dest: Pointer; var BackupCode: TXRedirCode);
var
n: SIZE_T;
Code: TXRedirCode;
begin
Proc := GetActualAddr(Proc);
Assert(Proc <> nil);
if ReadProcessMemory(GetCurrentProcess, Proc, #BackupCode, SizeOf(BackupCode), n) then
begin
Code.Jump := $E9;
Code.Offset := PAnsiChar(Dest) - PAnsiChar(Proc) - SizeOf(Code);
WriteProcessMemory(GetCurrentProcess, Proc, #Code, SizeOf(Code), n);
end;
end;
procedure UnhookProc(Proc: Pointer; var BackupCode: TXRedirCode);
var
n: SIZE_T;
begin
if (BackupCode.Jump <> 0) and (Proc <> nil) then
begin
Proc := GetActualAddr(Proc);
Assert(Proc <> nil);
WriteProcessMemory(GetCurrentProcess, Proc, #BackupCode, SizeOf(BackupCode), n);
BackupCode.Jump := 0;
end;
end;
procedure Bitmap2GrayScale(const BitMap: TBitmap);
type
TRGBArray = array[0..32767] of TRGBTriple;
PRGBArray = ^TRGBArray;
var
x, y, Gray: Integer;
Row : PRGBArray;
begin
BitMap.PixelFormat := pf24Bit;
for y := 0 to BitMap.Height - 1 do
begin
Row := BitMap.ScanLine[y];
for x := 0 to BitMap.Width - 1 do
begin
Gray := (Row[x].rgbtRed + Row[x].rgbtGreen + Row[x].rgbtBlue) div 3;
Row[x].rgbtRed := Gray;
Row[x].rgbtGreen := Gray;
Row[x].rgbtBlue := Gray;
end;
end;
end;
//from ImgList.GetRGBColor
function GetRGBColor(Value: TColor): DWORD;
begin
Result := ColorToRGB(Value);
case Result of
clNone:
Result := CLR_NONE;
clDefault:
Result := CLR_DEFAULT;
end;
end;
procedure New_Draw(Self: TObject; Index: Integer; Canvas: TCanvas; X, Y: Integer; Style: Cardinal; Enabled: Boolean);
var
MaskBitMap : TBitmap;
GrayBitMap : TBitmap;
begin
with TCustomImageListHack(Self) do
begin
if not HandleAllocated then Exit;
if Enabled then
ImageList_DrawEx(Handle, Index, Canvas.Handle, X, Y, 0, 0, GetRGBColor(BkColor), GetRGBColor(BlendColor), Style)
else
begin
GrayBitMap := TBitmap.Create;
MaskBitMap := TBitmap.Create;
try
GrayBitMap.SetSize(Width, Height);
MaskBitMap.SetSize(Width, Height);
GetImages(Index, GrayBitMap, MaskBitMap);
Bitmap2GrayScale(GrayBitMap);
BitBlt(Canvas.Handle, X, Y, Width, Height, MaskBitMap.Canvas.Handle, 0, 0, SRCERASE);
BitBlt(Canvas.Handle, X, Y, Width, Height, GrayBitMap.Canvas.Handle, 0, 0, SRCINVERT);
finally
GrayBitMap.Free;
MaskBitMap.Free;
end;
end;
end;
end;
procedure New_Draw2(Self: TObject; const Location: TPoint);
var
ImageList: TCustomImageList;
DrawEnabled: Boolean;
LDisabled: Boolean;
begin
with TCustomActionControlHook(Self) do
begin
if not HasGlyph then Exit;
ImageList := FindImageList(True, LDisabled, ActionClient.ImageIndex);
if not Assigned(ImageList) then Exit;
DrawEnabled := LDisabled or Enabled and (ActionClient.ImageIndex <> -1) or
(csDesigning in ComponentState);
ImageList.Draw(Canvas, Location.X, Location.Y, ActionClient.ImageIndex,
dsTransparent, itImage, DrawEnabled);
end;
end;
procedure HookDraw;
begin
HookProc(#TCustomImageListHack.DoDraw, #New_Draw, DoDrawBackup);
HookProc(#TCustomActionControlHook.DrawLargeGlyph, #New_Draw2, DoDrawBackup2);
end;
procedure UnHookDraw;
begin
UnhookProc(#TCustomImageListHack.DoDraw, DoDrawBackup);
UnhookProc(#TCustomActionControlHook.DrawLargeGlyph, DoDrawBackup2);
end;
initialization
HookDraw;
finalization
UnHookDraw;
end.
Take a look at this Delphi IDE fix. Maybe you can mimic it's implementation.
Use TActionToolbar , TActionmanager , Timagelist
Set action managers image list to a Timagelist. and set Disabledimages to another imagelist