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.
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.
The issue is related to regular TComboBox component.
The problem exists when accessing drop list from the combo-box and next when scrolling through the form, fields list remains in the same place. It's really annoying from the user perspective.
Is there any way to automatically lose the control focus when scrolling through the form? Any other ideas?
Please see sample code below to recreate the issue.
unit Unit10;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, vcl.wwcheckbox, Vcl.Mask, vcl.wwdbedit, Vcl.ExtCtrls,
vcl.wwdotdot, vcl.wwdbcomb, vcl.wwdbdatetimepicker;
type
TForm10 = class(TForm)
grpPanels: TCategoryPanelGroup;
pnl1: TCategoryPanel;
pnl2: TCategoryPanel;
ComboBox1: TComboBox;
procedure FormMouseWheel(Sender: TObject; Shift: TShiftState; WheelDelta: Integer; MousePos: TPoint;
var Handled: Boolean);
procedure FormShow(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form10 : TForm10;
implementation
{$R *.dfm}
procedure TForm10.FormMouseWheel(Sender: TObject; Shift: TShiftState; WheelDelta: Integer; MousePos: TPoint;
var Handled: Boolean);
begin
if WheelDelta > 0 then
grpPanels.Perform(WM_VSCROLL, SB_LINEUP, 0)
else
grpPanels.Perform(WM_VSCROLL, SB_LINEDOWN, 0);
end;
procedure TForm10.FormShow(Sender: TObject);
var
i: Integer;
begin
for i := 0 to 5 do
ComboBox1.Items.Add(IntToStr(i));
end;
end.
Is there any way to automatically lose the control focus when scrolling through the form? Any other ideas?
Add to the beginning of FormMouseWheel() either of the following:
Assure the combo loses focus:
if ActiveControl is TComboBox then
FocusControl(nil);
Alternative, assure the dropdown is closed, but retain focus
if ActiveControl is TComboBox then
TComboBox(ActiveControl).DroppedDown := False;
I've found another way to handle this issue and want to share it with you.
The thing is the problem also exists for other WIN32 vcl components like TDateTimePicker, TComboBox etc.
To handle all this cases with not losing control focus we can call CM_CANCELMODE message at the beginning of FormMouseWheel() :
for i := 0 to ComponentCount - 1 do
if Components[i] is TWinControl then
TWinControl(Components[i]).Perform(CM_CANCELMODE, 0, 0);
I want to change the caption of a panel when it is out of view when scrolling in a TScrollBox. I have a scrollbox where all the all the categories are listed under each other and as the title of each category scrolls past i want the top panel to change to show which category I am currently scrolling through. How exactly can i do this?
To see if any pixel of a child control ChildCtrl currently is visible in the parent TScrollBox control named ScrollBox, check
ScrollBox.ClientRect.IntersectsWith(ChildCtrl.BoundsRect)
This is just one definition of "not out of view", however. If you instead want to check if the entire control is visible, instead check
ScrollBox.ClientRect.Contains(ChildCtrl.BoundsRect)
To detect scrolling, you would love a published OnScroll property of TScrollBox, but unfortunately there is no such property. Instead, you must intercept the scroll messages yourself, as detailed in this Q&A.
Here is a complete example (just quick and dirty to show how it is done -- in a real app, you would refactor it):
unit Unit1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, StdCtrls;
type
TScrollBox = class(Vcl.Forms.TScrollBox)
procedure WMVScroll(var Message: TWMVScroll); message WM_VSCROLL;
end;
TForm1 = class(TForm)
ScrollBox1: TScrollBox;
lblTitle: TLabel;
procedure FormCreate(Sender: TObject);
private
{ Private declarations }
FButtons: TArray<TButton>;
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.FormCreate(Sender: TObject);
const
N = 30;
var
i, y: Integer;
btn: TButton;
begin
// First, populate the scroll box with some sample buttons
SetLength(FButtons, N);
y := 10;
for i := 0 to N - 1 do
begin
btn := TButton.Create(ScrollBox1);
btn.Parent := ScrollBox1;
btn.Left := 10;
btn.Top := y;
btn.Caption := 'Button ' + (i + 1).ToString;
Inc(y, 3*btn.Height div 2);
FButtons[i] := btn;
end;
end;
{ TScrollBox }
procedure TScrollBox.WMVScroll(var Message: TWMVScroll);
var
i: Integer;
begin
inherited;
for i := 0 to High(Form1.FButtons) do
if Form1.ScrollBox1.ClientRect.Contains(Form1.FButtons[i].BoundsRect) then
begin
Form1.lblTitle.Caption := Form1.FButtons[i].Caption;
Break;
end;
end;
end.
Don't forget to set TScrollBox.VertScrollBar.Tracking to True!
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'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...