I'm trying to implement the following functionality:
when mouse comes over the combobox, it opens automatically.
when mouse leaves the combobox area (not only the combo, but dropdown list too), it closes automatically.
First point was quite easy:
procedure TForm1.ComboTimeUnitsMouseEnter(Sender: TObject);
begin
ComboTimeUnits.DroppedDown := True;
end;
The second point, though, I cannot do it. I tried:
procedure TForm1.ComboTimeUnitsMouseLeave(Sender: TObject);
begin
ComboTimeUnits.DroppedDown := False;
end;
But when the mouse is over the combobox, it acts very strange, appearing and disappearing, becoming unusable.
I tried AutoCloseUp property, with no result. Now I'm out of ideas and google couldn't help.
Can someone point me in the right direction?
There is no simple solution to your Combo Box (CB) request. I recall that the drop down List of a Windows CB is child to the screen and not the CB. The reason for this is to be able to display the drop down list outside of the client window as illustrated below. Pretty good stuff if you ask me.
Suggested solution
Here's a go at trying to use the existing TComboBox. TLama's "ugly code" is more elegant than mine because he uses an interceptor class. My suggestion below does however solve an additional case, namely the listbox does not roll up when the mouse moves up and crosses the boundry between the ListBox back to the Combobox.
unit main;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.AppEvnts;
type
TFormMain = class(TForm)
ComboBox1: TComboBox;
Label1: TLabel;
Label2: TLabel;
procedure ComboBox1MouseEnter(Sender: TObject);
procedure ComboBox1CloseUp(Sender: TObject);
procedure FormCreate(Sender: TObject);
private
FActiveCb : TComboBox; //Stores a reference to the currently active CB. If nil then no CB is in use
FActiveCbInfo : TComboBoxInfo; //stores relevant Handles used by the currently active CB
procedure ApplicationEvents1Idle(Sender: TObject; var Done: Boolean);
public
{ Public declarations }
end;
var
FormMain: TFormMain;
implementation
{$R *.dfm}
procedure TFormMain.FormCreate(Sender: TObject);
begin
FActiveCb := nil;
FActiveCbInfo.cbSize := sizeof(TComboBoxInfo);
Application.OnIdle := Self.ApplicationEvents1Idle;
end;
procedure TFormMain.ComboBox1CloseUp(Sender: TObject);
begin
FActiveCb := nil;
end;
procedure TFormMain.ComboBox1MouseEnter(Sender: TObject);
begin
FActiveCb := TComboBox(Sender);
FActiveCb.DroppedDown := true;
GetComboBoxInfo(FActiveCb.Handle, FActiveCbInfo); //Get CB's handles
end;
procedure TFormMain.ApplicationEvents1Idle(Sender: TObject; var Done: Boolean);
var w : THandle;
begin
//Check if the mouse cursor is within the CB, it's Edit Box or it's List Box
w := WindowFromPoint(Mouse.CursorPos);
with FActiveCbInfo do
if Assigned(FActiveCb) and (w <> hwndList) and (w <> hwndCombo) and (w <> hwndItem) then
FActiveCb.DroppedDown := false;
end;
end.
How to add additional CBs
Drop a new combobox on the form.
Assign ComboBox1MouseEnter proc to the OnMouseEnter event
Assign ComboBox1CloseUp proc to the OnCloseUp event
Issues
There are however certain issues that remain to be solved:
ListBox dissapears when user clicks
Text in the CB cannot be selected using the mouse
For sure more issues...
Related
I've written a program in Delphi 10.4. The main part of the UI is just a TMemo. When the user types something in it, the app will automatically copy the text in the TMemo to clipboard. It looks like this:
This auto copy part works well. However, I also want to let the user change dark theme or light theme by a shortcut. I enabled a dark theme and a light theme.
The code looks like this:
unit Unit1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Clipbrd, System.Actions,
Vcl.ActnList, Vcl.Themes;
type
TForm1 = class(TForm)
txt: TMemo;
ActionList1: TActionList;
act_change_theme: TAction;
procedure txtChange(Sender: TObject);
procedure act_change_themeExecute(Sender: TObject);
procedure FormCreate(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
var
is_dark: Boolean;
implementation
{$R *.dfm}
function ShortCut(Key: Word; Shift: TShiftState): TShortCut;
begin
Result := 0;
if HiByte(Key) <> 0 then
Exit; // if Key is national character then it can't be used as shortcut
Result := Key;
if ssShift in Shift then
Inc(Result, scShift); // this is identical to "+" scShift
if ssCtrl in Shift then
Inc(Result, scCtrl);
if ssAlt in Shift then
Inc(Result, scAlt);
end;
procedure TForm1.act_change_themeExecute(Sender: TObject);
begin
if is_dark then
begin
TStyleManager.TrySetStyle('Windows', false);
is_dark := false;
end
else
begin
TStyleManager.TrySetStyle('Carbon', false);
is_dark := true;
end;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
is_dark := false;
act_change_theme.ShortCut := ShortCut(Word('d'), [ssCtrl]);
end;
procedure TForm1.txtChange(Sender: TObject);
begin
try
Clipboard.AsText := txt.Lines.GetText;
except
on E: Exception do
end;
end;
end.
However, when I press ctrl+d, nothing happened. I tried to debug it and I found that ctrl+d never triggers the action's shortcut. Why this happened? How to fix it? I've used the shortcut function in the past and it worked.
Try Word('D'), or the constant vkD, instead of Word('d'). Shortcuts use virtual key codes, and letters are represented as virtual keys using their capital values. Typing an Uppercase or Lowercase letter into an edit control uses the same virtual key, it is the current shift state that determines the case of the letter when the key is translated into a text character.
Also note that the VCL has its own ShortCut() function (and also TextToShortCut()) in the Vcl.Menus unit for creating TShortCut values, so you don't need to write your own function.
See Representing Keys and Shortcuts, especially Representing Shortcuts as Instances of TShortCut.
Also, your TAction is clearly placed on the Form at design-time, so you should simply assign its ShortCut using the Object Inspector, rather than in code. Then these details would be handled for you automatically by the framework.
I'm experiencing a strange behavior with WM_NCHITTEST messages.
In summary, what happens is that as soon as I have the mouse over the target (ie: Hooked) control and leave the mouse still (or idle), I receive endlessly hundred's of WM_NCHITTEST messages per second. This happens whether I subclass the WndProc of that control with WindowProc(), or if I override the WndProc method in a descendant class (I subclass in the code below for simplicity).
As far as I could find from online Win32 API docs and other sources, I doubt that this message fires at this frequency, but I might be wrong. Or maybe there is an obvious explanation that I completely missed, or maybe something changed in the APIs that I am not aware of. In any event, I would really like to know what it is, or what is going on.
I've tested the same code (the example below) on two different systems with the same result, though both systems are in the same Delphi/OS version and configuration. I've tried running the app outside of the IDE (so no debugging hook), in both debug and release configurations (latter with no debug info), target both 32-bit and 64-bit, and I always get the same result.
I am developing with Delphi XE7 Enterprise under Win10 Pro 64-bit, version 20H2 (the latest Windows version I believe).
Here is a very simplistic program to reproduce what I am experiencing: a TForm with a TPanel, a TCheckBox, and a TLabel. The panel is the control being hooked when the checkbox is checked, and the label is displaying how many WM_NCHITTEST messages are received by the WndProc() method:
unit Unit5;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.ExtCtrls, Vcl.StdCtrls;
type
TForm5 = class(TForm)
CheckBox1: TCheckBox;
Label1: TLabel;
Panel1: TPanel;
procedure FormDestroy(Sender: TObject);
procedure CheckBox1Click(Sender: TObject);
private
FHookedCtrl: TControl;
FHookedCtrlWndProc: TWndMethod;
FMessageCount: Integer;
procedure SetHookedCtrl(const Value: TControl);
public
procedure ControlWndProc(var Message: TMessage);
property HookedCtrl: TControl read FHookedCtrl write SetHookedCtrl;
end;
var
Form5: TForm5;
implementation
{$R *.dfm}
{ TForm5 }
procedure TForm5.CheckBox1Click(Sender: TObject);
begin
//checkbox activates or deactivates the hook
if CheckBox1.Checked then
//hook the panel's WndProc by subclassing
HookedCtrl := Panel1
//release the hook on WndProc
else HookedCtrl := nil;
end;
procedure TForm5.ControlWndProc(var Message: TMessage);
begin
case Message.Msg of
WM_NCHITTEST:
begin
//show how many messages received with the label's caption
Inc(FMessageCount);
Label1.Caption := FormatFloat('##,##0 messages', FMessageCount);
end;
end;
//not really handling the messsage, just counting.
FHookedCtrlWndProc(Message);
end;
procedure TForm5.FormDestroy(Sender: TObject);
begin
//make sure to clear the hook if assigned
HookedCtrl := nil;
end;
procedure TForm5.SetHookedCtrl(const Value: TControl);
begin
if (Value <> FHookedCtrl) then
begin
if Assigned(FHookedCtrl) then
begin
//release the hook
FHookedCtrl.WindowProc := FHookedCtrlWndProc;
FHookedCtrlWndProc := nil;
FMessageCount := 0;
end;
FHookedCtrl := Value;
if Assigned(FHookedCtrl) then
begin
//hook the panel (i.e. Value)
FHookedCtrlWndProc := FHookedCtrl.WindowProc;
FHookedCtrl.WindowProc := ControlWndProc;
end;
end;
end;
end.
To reproduce: run the app, check the CheckBox, hover the mouse over the panel and leave it idle (still). In my case, I receive 100's of WM_NCHITTEST messages per second, and it never stops coming. Should this happen?
Can someone explain what's happening here?
I used Microsoft Spy++ tool to see what happens and when.
It is the following line in the WM_NCHITTEST handler
Label1.Caption := FormatFloat('##,##0 messages', FMessageCount);
which causes the issue. When you remove it, there is no more all those WM_NCHITTEST messages. To see the number of messages, use a TTimer with a 1 second interval and display the message count in the label. You'll see that you get a WM_NCHITTEST each time the timer fires (You still get a message if you have an empty OnTimer handler) and of course when the mouse is moving.
Here is the code I used:
unit Unit5;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.ExtCtrls, Vcl.StdCtrls;
type
TForm5 = class(TForm)
Label1: TLabel;
CheckBox1: TCheckBox;
Panel1: TPanel;
Timer1: TTimer;
procedure FormDestroy(Sender: TObject);
procedure CheckBox1Click(Sender: TObject);
procedure Timer1Timer(Sender: TObject);
private
FHookedCtrl: TControl;
FHookedCtrlWndProc: TWndMethod;
FMessageCount: Integer;
procedure SetHookedCtrl(const Value: TControl);
public
procedure ControlWndProc(var Message: TMessage);
property HookedCtrl: TControl read FHookedCtrl write SetHookedCtrl;
end;
var
Form5: TForm5;
implementation
{$R *.dfm}
{ TForm5 }
procedure TForm5.CheckBox1Click(Sender: TObject);
begin
//checkbox activates or deactivates the hook
if CheckBox1.Checked then
//hook the panel's WndProc by subclassing
HookedCtrl := Panel1
else
//release the hook on WndProc
HookedCtrl := nil;
end;
procedure TForm5.ControlWndProc(var Message: TMessage);
begin
case Message.Msg of
WM_NCHITTEST:
//Count how many messages received
Inc(FMessageCount);
end;
//not really handling the messsage, just counting.
FHookedCtrlWndProc(Message);
end;
procedure TForm5.FormDestroy(Sender: TObject);
begin
//make sure to clear the hook if assigned
HookedCtrl := nil;
end;
procedure TForm5.SetHookedCtrl(const Value: TControl);
begin
if (Value <> FHookedCtrl) then begin
if Assigned(FHookedCtrl) then begin
//release the hook
FHookedCtrl.WindowProc := FHookedCtrlWndProc;
FHookedCtrlWndProc := nil;
FMessageCount := 0;
end;
FHookedCtrl := Value;
if Assigned(FHookedCtrl) then begin
//hook the panel (i.e. Value)
FHookedCtrlWndProc := FHookedCtrl.WindowProc;
FHookedCtrl.WindowProc := ControlWndProc;
end;
end;
end;
procedure TForm5.Timer1Timer(Sender: TObject);
begin
// Show how many message received
Label1.Caption := FormatFloat('##,##0 messages', FMessageCount);
end;
end.
I have an simple application with a panel contains 2 labels (eg. A & B) and a button C created by Delphi 2009.
Label A will display the cursor position when I move the mouse inside the area of panel. Label B just display the static text (caption will not change during app run)
If I move the mouse inside the panel, the label A will flicker.
When I enable 'Double buffer' of form, the flicker is lost. Button C wil demonstrate to enable/disable 'Double buffer' property
I would like to question 'Why does the label in panel flicker? What is the root cause? How can we solve this problem thoroughly?'
Here is my code:
unit DemoFlicker;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
System.StrUtils,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls;
type
TForm1 = class(TForm)
pnlCtr: TPanel;
btnDoubleBuffer: TButton;
lblName: TLabel;
lblNumber: TLabel;
procedure btnDoubleBufferClick(Sender: TObject);
procedure pnlCtrMouseMove(Sender: TObject; Shift: TShiftState; X,
Y: Integer);
procedure FormCreate(Sender: TObject);
private
{ Private declarations }
FDoubleBuffer: Boolean;
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.btnDoubleBufferClick(Sender: TObject);
begin
FDoubleBuffer := not FDoubleBuffer;
Self.DoubleBuffered := FDoubleBuffer;
if FDoubleBuffer then
begin
btnDoubleBuffer.Caption := 'Not Apply Double Buffer';
end
else
begin
btnDoubleBuffer.Caption := 'Apply Double Buffer';
end;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
FDoubleBuffer := False;
Self.DoubleBuffered := False;
end;
procedure TForm1.pnlCtrMouseMove(Sender: TObject; Shift: TShiftState; X,
Y: Integer);
var
mousePos: string;
begin
mousePos := Format('(X=%d, Y=%d)', [Mouse.CursorPos.X, Mouse.CursorPos.Y]);
lblNumber.Caption := mousePos ;
end;
end.
Flicker occurs because of how the drawing is done. Without double buffer, the background is drawn (painted) and then the label is drawn. So at a moment you see only the background only and then you seen the label above the background. If you repeat the update, it flickers.
When using "double buffer", the drawing is done in an invisible buffer and then when drawing is complete, the buffer is rendered on screen. So you only see the complete image at once and no flickering.
To solve the problem, use double buffering as you found it by yourself.
You may also create a new component which does all drawing itself in his Paint procedure.
I've had the same problem, Delphi 10.4
Setting the enclosing TPanel in this way -
DoubleBuffered = True
ParentBackground = False
seems to fix it.
The VCL has become way too complicated in its implementation, especially since Themes were introduced, making its behavior very buggy and unpredictable IMHO.
I would like to have a KeyPreview functionality within Frames, I mean, that when the input (say, one of the controls of the frame is selected, or the mouse is inside) is in a frame (which would have several panels and other controls) then the keys pressed by the user are first processed by the frame.
Is there a way to do this? I haven't found a property similar to KeyPreview in TFrame.
I'm using version XE5 of RAD Studio, altough I mostly work with C++Builder.
Thanks to my recent "When does a ShortCut fire"-investigation, I have worked out a stand alone solution for your Frame.
In short: all key messages enter in TWinControl.CNKeyDwon of the active control. That method calls TWinControl.IsMenuKey which traverses all parents while determining whether the message is a ShortCut. Is does so by calling its GetPopupMenu.IsShortCut method. I have overridden the Frame's GetPopupMenu method by creating one if it is not present. Note that at all time you still can add a PopupMenu to the Frame yourself. By subclassing TPopupMenu and overriding the IsShortCut method, the Frame's KeyDown method is called, which serves as the KeyPreview functionality you require. (I could also have assigned the OnKeyDdown event handler).
unit Unit2;
interface
uses
Winapi.Messages, System.Classes, Vcl.Controls, Vcl.Forms, Vcl.Menus,
Vcl.StdCtrls;
type
TPopupMenu = class(Vcl.Menus.TPopupMenu)
public
function IsShortCut(var Message: TWMKey): Boolean; override;
end;
TFrame2 = class(TFrame)
Label1: TLabel;
Edit1: TEdit;
private
FPreviewPopup: TPopupMenu;
protected
function GetPopupMenu: Vcl.Menus.TPopupMenu; override;
procedure KeyDown(var Key: Word; Shift: TShiftState); override;
end;
implementation
{$R *.dfm}
{ TPopupMenu }
function TPopupMenu.IsShortCut(var Message: TWMKey): Boolean;
var
ShiftState: TShiftState;
begin
ShiftState := KeyDataToShiftState(Message.KeyData);
TFrame2(Owner).KeyDown(Message.CharCode, ShiftState);
Result := Message.CharCode = 0;
if not Result then
Result := inherited IsShortCut(Message);
end;
{ TFrame2 }
function TFrame2.GetPopupMenu: Vcl.Menus.TPopupMenu;
begin
Result := inherited GetPopUpMenu;
if Result = nil then
begin
if FPreviewPopup = nil then
FPreviewPopup := TPopupMenu.Create(Self);
Result := FPreviewPopup;
end;
end;
procedure TFrame2.KeyDown(var Key: Word; Shift: TShiftState);
begin
if (Key = Ord('X')) and (ssCtrl in Shift) then
begin
Label1.Caption := 'OH NO, DON''T DO THAT!';
Key := 0;
end;
end;
end.
If you only have one frame on the form at the time you could make use of forms KeyPreview ability and forward the necessary information to the frame.
If you are only forwarding the information you don't need to make any changes to original VCL code just make a modified TFrame class. So there is no wory that you might break whole VCL doing it.
Here is a quick code example:
MainForm code:
unit Unit2;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Unit3, Vcl.ExtCtrls, Vcl.StdCtrls;
type
TForm2 = class(TForm)
Panel1: TPanel;
ModifiedFrame: TModifiedFrame;
Edit1: TEdit;
procedure FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
procedure FormCreate(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form2: TForm2;
implementation
{$R *.dfm}
procedure TForm2.FormCreate(Sender: TObject);
begin
//This is required since I'm asigning frames OnKeyDown event method manually
ModifiedFrame.OnKeyDown := ModifiedFrame.FrameKeyDown;
end;
procedure TForm2.FormKeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
begin
//Forward key down information to ModifiedFrame
ModifiedFrame.DoKeyDown(Sender, Key, Shift);
if Key = 0 then
MessageDlg('Key was handled by the modified frame!',mtInformation,[mbOK],0)
else
MessageDlg('Key was not handled!',mtInformation,[mbOK],0);
end;
end.
ModifiedFrame code:
unit Unit3;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;
type
TModifiedFrame = class(TFrame)
Edit1: TEdit;
//Normally this method would be added by the Delphi IDE when you set the
//OnKeyDown event but here I created this manually in order to avoid crating
//design package with modified frame
procedure FrameKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
private
{ Private declarations }
FOnKeyDown: TKeyEvent;
public
{ Public declarations }
//This is used to recieve forwarded key down information from the Form
procedure DoKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
published
//Property to alow setting the OnKeyDown event at design-time
//NOTE: In order for this to work properly you have to put this modified
//frame class into separate unti and register it as new design time component
property OnKeyDown: TKeyEvent read FOnKeyDown write FOnKeyDown;
end;
implementation
{$R *.dfm}
procedure TModifiedFrame.DoKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
//Check to see if OnKeyDownEvent has been assigned. If it is foward the key down
//information to the event procedure
if Assigned(FOnKeyDown) then FOnKeyDown(Self, Key, Shift);
end;
procedure TModifiedFrame.FrameKeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
begin
//Do something
if Key = VK_RETURN then
begin
MessageBeep(0);
Key := 0;
end;
end;
end.
Using simillar approach you can forward other key events.
It is doable, if you are willing to change VCL code.
KeyPreview is handled in TWinControl.DoKeyDown method. As it can be seen from the code control that has focus will lookup its parent form and invoke its DoKeyDown method if KeyPreview is turned on.
function TWinControl.DoKeyDown(var Message: TWMKey): Boolean;
var
ShiftState: TShiftState;
Form, FormParent: TCustomForm;
LCharCode: Word;
begin
Result := True;
// Insert modification here
{ First give the immediate parent form a try at the Message }
Form := GetParentForm(Self, False);
if (Form <> nil) and (Form <> Self) then
begin
if Form.KeyPreview and TWinControl(Form).DoKeyDown(Message) then
Exit;
{ If that didn't work, see if that Form has a parent (ie: it is docked) }
if Form.Parent <> nil then
begin
FormParent := GetParentForm(Form);
if (FormParent <> nil) and (FormParent <> Form) and
FormParent.KeyPreview and TWinControl(FormParent).DoKeyDown(Message) then
Exit;
end;
end;
with Message do
begin
ShiftState := KeyDataToShiftState(KeyData);
if not (csNoStdEvents in ControlStyle) then
begin
LCharCode := CharCode;
KeyDown(LCharCode, ShiftState);
CharCode := LCharCode;
if LCharCode = 0 then Exit;
end;
end;
Result := False;
end;
To change that behavior, you would need to either change TWinControl.DoKeyDown code to scan for frames too or intercept WM_KEYDOWN and WM_SYSKEYDOWN for every TWinControl descendant you want to use, and finally add KeyPreview field to base Frame class.
Probably best option would be to declare IKeyPreview interface and when scanning for parent forms/frames test if parent implements that interface. If none found, you can fall back to original code. That would contain VCL code changes only to TWinControl.DoKeyDown method, and you can easily implement interface in Frames where needed.
Note: On Windows control that has focus receives key events. So above modifications would be able to find frame only if some of its controls has focus. Whether or not mouse would be over the frame or not would not have any influence on functionality.
More detailed code would look like this:
Interface definition that Frame would have to implement:
IKeyPreview = interface
['{D7318B16-04FF-43BE-8E99-6BE8663827EE}']
function GetKeyPreview: boolean;
property KeyPreview: boolean read GetKeyPreview;
end;
Function for finding parent frame that implements IKeyPreview interface, should be put somewhere in Vcl.Controls implementation section:
function GetParentKeyPreview(Control: TWinControl): IKeyPreview;
var
Parent: TWinControl;
begin
Result := nil;
Parent := Control.Parent;
while Assigned(Parent) do
begin
if Parent is TCustomForm then Parent := nil
else
if Supports(Parent, IKeyPreview, Result) then Parent := nil
else Parent := Parent.Parent;
end;
end;
TWinControl.DoKeyDown modification (insert in above original code):
var
PreviewParent: IKeyPreview;
PreviewParent := GetParentKeyPreview(Self);
if PreviewParent <> nil then
begin
if PreviewParent.KeyPreview and TWinControl(PreviewParent).DoKeyDown(Message) then
Exit;
end;
I am trying to create a data aware control. I have a TFieldDataLink object with a DataSource and Field hooked up. Everything seemed to be going OK until I tried to edit the value.
I am using the OnDataChange and OnUpdateData events for the TFieldDataLink. It looks like I need to call TFieldDataLink.Edit if I want the OnUpdateData event to be called before moving to a new record or posting. In the sample code below am trying to call .Edit in the OnExit field of the control if changes were made. In my actual app the control consists of several DevExpress lookup combo boxes and I am trying to call .Edit in OnEditValueChanged.
My problem is the call to TFieldDataLink.Edit causes the OnDataChange event to fire again. That forces a reload of my edit with the original value. If I make a second change after the Dataset is already in edit mode then a OnDataChange event is not fired.
Here is a test unit I that has everything on one form. In my actual app this is split out into a more complicated component.
When should I be calling .Edit without getting OnUpdateData to change? I know I could set a member variable to stop the reload or unhook the events before calling .Edit. It feels like there is something I don't understand about the TFieldDataLink object and I should not need to resort to those tricks.
unit Unit1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, uADStanIntf, uADStanOption, uADStanParam, uADStanError,
uADDatSManager, uADPhysIntf, uADDAptIntf, Data.DB, uADCompDataSet, uADCompClient, Vcl.StdCtrls,
Vcl.DBCtrls, Vcl.Mask, Vcl.ExtCtrls, Vcl.Grids, Vcl.DBGrids;
type
TForm1 = class(TForm)
Edit1: TEdit;
DataSource1: TDataSource;
ADMemTable1: TADMemTable;
ADMemTable1test: TStringField;
Button1: TButton;
DBEdit1: TDBEdit;
DBGrid1: TDBGrid;
DBNavigator1: TDBNavigator;
procedure FormCreate(Sender: TObject);
procedure Edit1Exit(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
FMyDataLink: TFieldDataLink;
procedure MyDataChange(Sender: TObject);
procedure MyUpdateData(Sender: TObject);
public
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.FormCreate(Sender: TObject);
begin
AdMemTable1.CreateDataSet;
FMyDataLink := TFieldDataLink.Create();
FMyDataLink.DataSource := DataSource1;
FMyDataLink.FieldName := 'test';
FMyDataLink.OnDataChange := MyDataChange;
FMyDataLink.OnUpdateData := MyUpdateData;
AdMemTable1.Append;
AdMemTable1.FieldByName('test').AsString := 'my test';
AdMemTable1.Post;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
FMyDataLink.OnDataChange := nil;
FMyDataLink.OnUpdateData := nil;
FMyDataLink.Free;
end;
procedure TForm1.Edit1Exit(Sender: TObject);
begin
if Edit1.Modified = true then
begin
FMyDataLink.Edit;
FMyDataLink.Modified;
end;
end;
procedure TForm1.MyDataChange(Sender: TObject);
begin
Edit1.Text := FMyDataLink.Field.AsString;
Edit1.Modified := false;
end;
procedure TForm1.MyUpdateData(Sender: TObject);
begin
FMyDataLink.Field.AsString := Edit1.Text
end;
end.
TFieldDataLink.Edit only sets the DataSource in editing state (just like DataSet.Edit). You do not need it here, but example usage could be:
procedure TMyCustomControl.DoPaste;
begin
FMyDataLink.Edit;
inherited DoPaste;
FMyDataLink.Modified;
end;
What you want instead on exit of the control is to update the record, if it is modified:
procedure TForm1.Edit1Exit(Sender: TObject);
begin
if Edit1.Modified then
try
FMyDataLink.UpdateRecord;
except
Edit1.SetFocus;
raise;
end;
end;
As for when TFieldDataLink.Modified should be called, that's when you have updated the field value:
procedure TForm1.MyUpdateData(Sender: TObject);
begin
FMyDataLink.Field.AsString := Edit1.Text;
FMyDataLink.Modified;
end;
It's an old question, but for those who are encountering the same problem,
you have to override the data-aware control's KeyPress method and call the FieldDataLink.Edit; after Inherited; if the Key is valid for the input (incl. del/c&p/bs/etc..).
At this point the current data is not yet modified. Calling .Edit later than this point is too late.