How to eliminate header artifacts during tracking - HDN_TRACK? - delphi

When doing "real time" tracking, the header control occasionally leaves artifacts behind, as the image below shows:
The first two images are from the attached program. The third image (without the blue coloring) is from Windows Explorer.
To get the artifacts, simply drag the separator off the right side of program's window right edge and bring it back quickly into view. It may take a couple of tries, depending on how quickly you bring the separator back into the window.
Windows Explorer avoids the problem by having the header not paint that black vertical bar when dragging.
EDIT: As Sertac below pointed out, Windows Explorer uses a different control, which is why it does not exhibit the problem.
I have two (2) questions:
How does one tell the header control not to paint that vertical black bar? I couldn't find anything in the documentation on that.
If getting rid of the black bar is not possible without "owner-drawing" the header, is there some way to prevent the artifact from appearing?
The program I am using to test the header control is below.
{$LONGSTRINGS OFF}
{$WRITEABLECONST ON}
{$ifdef WIN32} { tell Windows we want v6 of commctrl }
{$R Manifest32.res}
{$endif}
{$ifdef WIN64}
{$R Manifest64.res}
{$endif}
program _Header_Track;
uses Windows, Messages, CommCtrl;
const
ProgramName = 'Header_Track';
{-----------------------------------------------------------------------------}
{$ifdef VER90} { Delphi 2.0 }
type
ptrint = longint;
ptruint = dword;
const
ICC_WIN95_CLASSES = $000000FF; { missing in Delphi 2 }
type
TINITCOMMONCONTROLSEX = packed record
dwSize : DWORD;
dwICC : DWORD;
end;
PINITCOMMONCONTROLSEX = ^TINITCOMMONCONTROLSEX;
function InitCommonControlsEx(var InitClasses : TINITCOMMONCONTROLSEX)
: BOOL; stdcall; external comctl32;
{$endif}
{$ifdef VER90}
// for Delphi 2.0 define GetWindowLongPtr and SetWindowLongPtr as synonyms of
// GetWindowLong and SetWindowLong respectively.
function GetWindowLongPtr(Wnd : HWND;
Index : ptrint)
: ptruint; stdcall; external 'user32' name 'GetWindowLongA';
function SetWindowLongPtr(Wnd : HWND;
Index : ptrint;
NewLong : DWORD)
: ptruint; stdcall; external 'user32' name 'SetWindowLongA';
function GetClassLongPtr(Wnd : HWND;
Index : ptrint)
: ptruint; stdcall; external 'user32' name 'GetClassLongA';
function SetClassLongPtr(Wnd : HWND;
Index : ptrint;
NewLong : ptruint)
: ptruint; stdcall; external 'user32' name 'SetClassLongA';
{$endif}
{$ifdef FPC}
{ make the FPC definitions match Delphi's }
type
THDLAYOUT = record
Rect : PRECT;
WindowPos : PWINDOWPOS;
end;
PHDLAYOUT = ^THDLAYOUT;
function Header_Layout(Wnd : HWND; Layout : PHDLAYOUT) : WINBOOL; inline;
begin
Header_Layout := WINBOOL(SendMessage(Wnd, HDM_LAYOUT, 0, ptruint(Layout)));
end;
{$endif}
{-----------------------------------------------------------------------------}
function WndProc (Wnd : HWND; Msg : UINT; wParam, lParam : ptrint)
: ptrint; stdcall;
{ main application/window handler function }
const
HEADER_ID = 1000;
HEADER_ITEMS_WIDTH = 100;
Header : HWND = 0;
HeaderText : packed array[0..2] of pchar =
(
'Name',
'Date modified',
'Type'
);
var
ControlsInit : TINITCOMMONCONTROLSEX;
HeaderPos : TWINDOWPOS;
HeaderRect : TRECT;
HeaderNotification : PHDNOTIFY absolute lParam; { note overlay on lParam }
HeaderLayout : THDLAYOUT;
HeaderItem : THDITEM;
ClientRect : TRECT;
Style : ptruint;
i : integer;
begin
WndProc := 0;
case Msg of
WM_CREATE:
begin
{ initialize the common controls library }
with ControlsInit do
begin
dwSize := sizeof(ControlsInit);
dwICC := ICC_WIN95_CLASSES; { includes headers }
end;
InitCommonControlsEx(ControlsInit);
{ create the header control }
Header := CreateWindowEx(0,
WC_HEADER, { class name }
nil, { caption }
HDS_BUTTONS or
WS_CHILD or
WS_VISIBLE or
WS_CLIPCHILDREN or
WS_CLIPSIBLINGS,
0, { at parent x = 0 }
0, { y = 0 }
0, { width }
0, { height }
Wnd, { parent }
HEADER_ID, { child id }
hInstance,
nil);
if Header = 0 then
begin
MessageBox(Wnd,
'Couldn''t create a header',
'Main Window - WM_CREATE',
MB_ICONERROR or MB_OK);
WndProc := -1; { abort window creation }
exit;
end;
{ remove the annoying double click behavior of the header buttons }
Style := GetClassLongPtr(Header, GCL_STYLE);
Style := Style and (not CS_DBLCLKS);
SetClassLongPtr(Header, GCL_STYLE, Style);
{ tell the header which font to use }
SendMessage(Header, WM_SETFONT, GetStockObject(DEFAULT_GUI_FONT), 0);
{ insert the column header in the header control }
with HeaderItem do
for i := low(HeaderText) to high(HeaderText) do
begin
mask := HDI_FORMAT or HDI_TEXT or HDI_WIDTH;
pszText := HeaderText[i];
fmt := HDF_STRING;
cxy := HEADER_ITEMS_WIDTH; { width }
Header_InsertItem(Header, i, HeaderItem);
end;
exit;
end;
WM_SIZE:
begin
{ update the header size and location }
with HeaderLayout do
begin
WindowPos := #HeaderPos;
Rect := #HeaderRect;
end;
GetClientRect(Wnd, ClientRect);
CopyRect(HeaderRect, ClientRect);
ZeroMemory(#HeaderPos, sizeof(HeaderPos));
Header_Layout(Header, #HeaderLayout); { updates HeaderPos }
{ use HeaderPos to place the header where it should be in the window }
with HeaderPos do
begin
SetWindowPos(Header,
Wnd, x, y, cx, cy,
Flags);
end;
exit;
end; { WM_SIZE }
WM_NOTIFY:
begin
case HeaderNotification^.Hdr.Code of
HDN_BEGINTRACK:
begin
{ Allow dragging using the left mouse button only }
if HeaderNotification^.Button <> 0 then
begin
WndProc := ptrint(TRUE); { don't track }
exit;
end;
exit;
end;
HDN_TRACK:
begin
{ tell the header to resize itself }
Header_SetItem(Header,
HeaderNotification^.Item,
HeaderNotification^.pitem^);
exit;
end;
end;
end;
WM_DESTROY:
begin
PostQuitMessage(0);
exit;
end;
end; { case msg }
WndProc := DefWindowProc (Wnd, Msg, wParam, lParam);
end;
{-----------------------------------------------------------------------------}
function InitAppClass: WordBool;
{ registers the application's window classes }
var
cls : TWndClassEx;
begin
cls.cbSize := sizeof(TWndClassEx); { must be initialized }
if not GetClassInfoEx (hInstance, ProgramName, cls) then
begin
with cls do
begin
style := CS_BYTEALIGNCLIENT;
lpfnWndProc := #WndProc;
cbClsExtra := 0;
cbWndExtra := 0;
hInstance := system.hInstance;
hIcon := 0;
hCursor := LoadCursor(0, IDC_ARROW);
hbrBackground := COLOR_WINDOW + 1;
lpszMenuName := nil;
lpszClassName := ProgramName;
hIconSm := 0;
end;
InitAppClass := WordBool(RegisterClassEx(cls));
end
else InitAppClass := TRUE;
end;
{-----------------------------------------------------------------------------}
function WinMain : integer;
{ application entry point }
var
Wnd : HWND;
Msg : TMsg;
begin
if not InitAppClass then Halt (255); { register application's class }
{ Create the main application window }
Wnd := CreateWindowEx(WS_EX_CLIENTEDGE,
ProgramName, { class name }
ProgramName, { window caption text }
ws_OverlappedWindow or { window style }
ws_SysMenu or
ws_MinimizeBox or
ws_ClipSiblings or
ws_ClipChildren or { don't affect children }
ws_visible, { make showwindow unnecessary }
20, { x pos on screen }
20, { y pos on screen }
600, { window width }
200, { window height }
0, { parent window handle }
0, { menu handle 0 = use class }
hInstance, { instance handle }
nil); { parameter sent to WM_CREATE }
if Wnd = 0 then Halt; { could not create the window }
while GetMessage (Msg, 0, 0, 0) do { wait for message }
begin
TranslateMessage (Msg); { key conversions }
DispatchMessage (Msg); { send to window procedure }
end;
WinMain := Msg.wParam; { terminate with return code }
end;
begin
WinMain;
end.

This is an artifact caused by attempting to use the control in two different functionality modes at the same time. That, and of course fast mouse movement...
The black vertical line is actually the indicator that hints the final separator position when the mouse button will be released. Of course this indicator is only to be used when the header control does not reflect column resizing at real time.
You are, however, resizing the column at real time responding to the tracking notification. You should instead use the header control in live column drag mode and so that the indicator will not be drawn at all.
In summary, include HDS_FULLDRAG control style:
Header := CreateWindowEx(0,
WC_HEADER, { class name }
nil, { caption }
HDS_BUTTONS or
WS_CHILD or
WS_VISIBLE or
WS_CLIPCHILDREN or
WS_CLIPSIBLINGS or
HDS_FULLDRAG,
0, { at parent x = 0 }
0, { y = 0 }
0, { width }
0, { height }
Wnd, { parent }
HEADER_ID, { child id }
hInstance,
nil);
and leave out the track notification:
...
{ // don't tell the header to resize, it will do it itself
HDN_TRACK:
begin
// tell the header to resize itself
Header_SetItem(Header,
HeaderNotification^.Item,
HeaderNotification^.pitem^);
exit;
end;
}
...

Related

TScrollBox with dynamically created Memos issue [duplicate]

I have a TScrollBox that has a RichEdit that is bigger than the scrollbox, so both side scrollbars appear in the scrollbox. Then I have a function DoTask that calls RichEdit.SetFocus.
When I scroll down to where I want to see part of the text control, and then call DoTask, the ScrollBox will automatically scroll to the top of the RichEdit. How can I avoid that?
As you wish, here are some suggestions:
Override SetFocusedControl in the form:
function TForm1.SetFocusedControl(Control: TWinControl): Boolean;
begin
if Control = RichEdit then
Result := True
else
Result := inherited SetFocusedControl(Control);
end;
Or:
type
TCustomMemoAccess = class(TCustomMemo);
function TForm1.SetFocusedControl(Control: TWinControl): Boolean;
var
Memo: TCustomMemoAccess;
Scroller: TScrollingWinControl;
Pt: TPoint;
begin
Result := inherited SetFocusedControl(Control);
if (Control is TCustomMemo) and (Control.Parent <> nil) and
(Control.Parent is TScrollingWinControl) then
begin
Memo := TCustomMemoAccess(Control);
Scroller := TScrollingWinControl(Memo.Parent);
SendMessage(Memo.Handle, EM_POSFROMCHAR, Integer(#Pt), Memo.SelStart);
Scroller.VertScrollBar.Position := Scroller.VertScrollBar.Position +
Memo.Top + Pt.Y;
end;
end;
Interpose TScrollBox:
type
TScrollBox = class(Forms.TScrollBox)
protected
procedure AutoScrollInView(AControl: TControl); override;
end;
procedure TScrollBox.AutoScrollInView(AControl: TControl);
begin
if not (AControl is TCustomMemo) then
inherited AutoScrollInView(AControl);
end;
Or:
procedure TScrollBox.AutoScrollInView(AControl: TControl);
begin
if (AControl.Top > VertScrollBar.Position + ClientHeight) xor
(AControl.Top + AControl.Height < VertScrollBar.Position) then
inherited AutoScrollInView(AControl);
end;
Or use any creative combination of all of the above. How and when you like it to be scrolled only you know.
the simpliest solution would be
var a, b : Integer;
begin
a := ScrollBox1.VertScrollBar.Position;
b := ScrollBox1.HorzScrollBar.Position;
richEdit1.SetFocus;
ScrollBox1.VertScrollBar.Position:=a ;
ScrollBox1.HorzScrollBar.Position:=b ;
end;
Without hacking into VCL/deriving custom components there's only one solution - TForm.SetFocusedControl override + re-setting the positions of scrollbars as said above. One thing I added is disabling/enabling window redraw to avoid ugly jumps.
Here's my final snippet:
sbContainer is TScrollBox and NoScrCtrl is a control laying inside it which gets focus but we don't want it to be scrolled-in-view.
function TForm1.SetFocusedControl(Control: TWinControl): Boolean;
var hpos, vpos: integer;
begin
if Control = NoScrCtrl then
begin
sbContainer.Perform(WM_SETREDRAW, WPARAM(False), 0);
hpos := sbContainer.HorzScrollBar.Position;
vpos := sbContainer.VertScrollBar.Position;
Result := inherited SetFocusedControl(Control);
sbContainer.HorzScrollBar.Position := hpos;
sbContainer.VertScrollBar.Position := vpos;
sbContainer.Perform(WM_SETREDRAW, WPARAM(True), 0);
sbContainer.Refresh;
end
else
Result := inherited SetFocusedControl(Control);
end;
To disable scroll-into-view behavior from my main form, I used this solution: (C++Builder)
bool __fastcall TMainForm::SetFocusedControl(TWinControl *Control) {
LockWindowUpdate(Handle);
int vpos = VertScrollBar->Position;
int hpos = HorzScrollBar->Position;
bool result = TForm::SetFocusedControl(Control);
if (VertScrollBar->Position != vpos) {
VertScrollBar->Position = vpos;
}
if (HorzScrollBar->Position != hpos) {
HorzScrollBar->Position = hpos;
}
LockWindowUpdate(0);
return result;
}

Tabs and colored lines in Listbox

I am using a Tabbed Listbox component that was written by Fredric Rylander back in 1999 and it has been serving me well since then. :) Can't seem to find him anymore.
I now have an application that needs both Tabbed Data and alternating colored lines in the Listbox.
I can include the Component here for perusal if desired.
I tried coloring the lines from here
http://delphi.about.com/cs/adptips2002/a/bltip0602_4.htm
But then it eats the Tabs, but I do get the alternating colored lines.
Can someone please show me how to incorporate the two.
Thanks
Here's the Component
unit myListBoxTabbed;
{
Copyright © 1999 Fredric Rylander
You can easily add a header control to this list box: drop a header
control onto the form (it's default align property is set to alTop, if
it's not--set it); then set the myTabbedListBox's aligned property
to alClient; now, add the following two events and their code.
1) HeaderControl's OnSectionResize event:
var
i, last: integer;
begin
last := 0;
for i:=0 to HeaderControl1.Sections.Count-1 do begin
last := last + HeaderControl1.Sections[i].Width;
myTabbedListBox1.TabStops[i] := last;
end;
end;
2) Main form's OnCreate event:
var
i, last: integer;
begin
last := 0;
for i:=0 to HeaderControl1.Sections.Count-1 do begin
last := last + HeaderControl1.Sections[i].Width;
myTabbedListBox1.TabStops[i] := last;
end;
for i:=HeaderControl1.Sections.Count to MaxNumSections do
myTabbedListBox1.TabStops[i] := 2000;
end;
To get tab characters into the list box items either use the
string list property editor in the Delphi GUI and press
Ctrl + Tab or add tab characters (#9) in strings as so:
myTabbedListBox1.Items.Add( Edit1.Text + #9 + Edit2.Text );
I hope you find this tutorial helpful! :^)
(!) This is not a retail product, it's a tutorial and don't claim to
meet a potential user's demands.
If you find anything that seems odd (or incorrect even) don't hesitate to
write me a line. You can communicate with me at fredric#rylander.nu.
The source is available for you to use, abuse, modify and/or improve.
Happy trails!
/ Fredric
___________________________________F_r_e_d_r_i_c__R_y_l_a_n_d_e_r__
fredric#rylander.nu : www.rylander.nu : 6429296#pager.mirabilis.com
"power to the source sharing community"
}
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls;
type
TTabsArray = array[0..9] of integer;
type
TmyTabbedListBox = class( TListBox )
private
{ Private declarations }
fTabStops: TTabsArray;
function GetTabStops( iIndex: integer ): integer;
procedure SetTabStops( iIndex, iValue: integer);
function GetTabsString: string;
procedure SetTabsString( const sValue: string );
protected
{ Protected declarations }
procedure UpdateTabStops;
public
{ Public declarations }
procedure CreateParams( var cParams: TCreateParams ); override;
procedure CreateWnd; override;
property TabStops[ iIndex: integer ]: integer
read GetTabStops write SetTabStops;
published
{ Published declarations }
property TabsString: string
read GetTabsString write SetTabsString;
end;
procedure Register;
resourcestring
STR_ALPHA_UPPERLOWER = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
CHAR_SEMICOLON = ';';
implementation
procedure Register;
begin
RegisterComponents('Additional', [TmyTabbedListBox]);
end;
{ myTabbedListBox }
procedure TmyTabbedListBox.CreateParams(var cParams: TCreateParams);
begin
inherited CreateParams( cParams );
// add the window style LBS_USETABSTOPS to accept tabs
cParams.Style := cParams.Style or LBS_USETABSTOPS;
end;
procedure TmyTabbedListBox.CreateWnd;
var
i: integer;
begin
inherited CreateWnd;
// set all the tabs into the box
for i := Low( fTabStops ) to High( fTabStops ) do
fTabStops[i] := i * 100;
// show the real tab positions
UpdateTabStops;
end;
function TmyTabbedListBox.GetTabsString: string;
var
sBuffer: string;
i: integer;
begin
// init var
sBuffer := SysUtils.EmptyStr;
// set all tabstops to the string (separated by ';'-char)
for i := Low( fTabStops ) to High( fTabStops ) do
sBuffer := sBuffer + IntToStr( fTabStops[i] ) + CHAR_SEMICOLON;
// and here we have the results
Result := sBuffer;
end;
function TmyTabbedListBox.GetTabStops( iIndex: integer ): integer;
begin
// nothing funny here
Result := fTabStops[iIndex];
end;
procedure TmyTabbedListBox.SetTabsString( const sValue: string );
var
sBuffer: string;
i, len: integer;
begin
// copy value into buffer
sBuffer := sValue;
// set the tabstops as specified
for i := Low( fTabStops ) to High( fTabStops ) do begin
len := Pos( sBuffer, CHAR_SEMICOLON );
fTabStops[i] := StrToIntDef( Copy( sBuffer, 1, len ), 0 );
Delete( sBuffer, 1, len );
end;
// show/redraw the results
UpdateTabStops;
Invalidate;
end;
procedure TmyTabbedListBox.SetTabStops( iIndex, iValue: integer );
begin
// do we really need to update?
if fTabStops[iIndex] <> iValue then begin
// oki, let's then
fTabStops[iIndex] := iValue;
// show/redraw the results
UpdateTabStops;
Invalidate;
end;
end;
procedure TmyTabbedListBox.UpdateTabStops;
var
i, iHUnits: integer;
arrConvertedTabs: TTabsArray;
begin
// convert dialog box units to pixels.
// dialog box unit = average character width/height div 4/8
// determine the horizontal dialog box units used by the
// list box (which depend on its current font)
Canvas.Font := Font;
iHUnits := Canvas.TextWidth( STR_ALPHA_UPPERLOWER ) div 52;
// convert the array of tab values
for i := Low( arrConvertedTabs ) to High( arrConvertedTabs ) do
arrConvertedTabs[i] := ( fTabStops[i] * 4 ) div iHUnits;
// activate the tabs stops in the list box,
// sending a Windows list box message
SendMessage( Handle, LB_SETTABSTOPS,
1 + High( arrConvertedTabs ) - Low( arrConvertedTabs ),
LongInt( #arrConvertedTabs ) );
end;
end.
Here's an example using a standard TListBox and it's OnDrawItem event, based on the code from the link you provided and tested in Delphi 2007. Note you need to set the ListBox.Style to lbOwnerDrawFixed. You can perhaps use this as a base for modifying the component (or just abandon it altogether).
procedure TForm1.ListBox1DrawItem(Control: TWinControl; Index: Integer;
Rect: TRect; State: TOwnerDrawState);
var
LB: TListBox;
NewColor: TColor;
NewBrush: TBrush;
R: TRect;
Fmt: Cardinal;
ItemText: string;
begin
NewBrush := TBrush.Create;
LB := (Control as TListBox);
if (odSelected in State) then
begin
NewColor := LB.Canvas.Brush.Color;
end
else
begin
if not Odd(Index) then
NewColor := clSilver
else
NewColor := clYellow;
end;
NewBrush.Style := bsSolid;
NewBrush.Color := NewColor;
// This is the ListBox.Canvas brush itself, not to be
// confused with the NewBrush we've created above
LB.Canvas.Brush.Style := bsClear;
R := Rect;
ItemText := LB.Items[Index];
Fmt := DT_EXPANDTABS or DT_CALCRECT or DT_NOCLIP;
DrawText(LB.Canvas.Handle, PChar(ItemText), Length(ItemText),
R, Fmt);
// Note we need to FillRect on the original Rect and not
// the one we're using in the call to DrawText
Windows.FillRect(LB.Canvas.Handle, Rect, NewBrush.Handle) ;
DrawText(LB.Canvas.Handle, PChar(ItemText), Length(ItemText),
R, DT_EXPANDTABS);
NewBrush.Free;
end;
Here's the output of the above code:

Delphi: sliding (animated) panel

Is there a sliding (animated) panel component for Delphi?
For instance it can be found in Raize Components (a left panel with a "Hot Spot" or hide/show button).
I need not a resizeable panel but a panel that can slide horizontally and smoothly from the left to the right + that has a hide/show button (it's not a big deal if without that button).
Thanks!
Try NLDSideBar, a container component written by myself that is collapsable and aligned along the left or right side of its parent.
Interface:
property Align: TSideBarAlign default alLeft;
property AutoHide: Boolean default False;
property Hint: String;
property MinWidth: Integer default DefWidth;
property OnAutoHideChanged: TNotifyEvent;
property OnHide: TNotifyEvent;
property PinButtonDownHint: String;
property PinButtonUpHint: String;
property PinButtonVisible: Boolean default True;
property Resizable: Boolean default True;
property SideButtonWidth: Integer default DefSideButtonWidth;
property Caption;
property Color default clBtnFace;
property Font;
property ParentColor default False;
property ParentFont;
property ParentShowHint;
property PopupMenu;
property TabOrder;
property TabStop;
Or maybe this older version which is animated. Free to use, free to modify.
Sorry for being self-promotive, but I think it's an answer to the question.
We ended up building our own control. We could not find anything that worked quite how we wanted. It ended up not being that hard. I'm sure there are situations we are not handling correctly, but so for this is working good for us.
The code below is using cxGroupBox because we needed that look to match the rest of our application. That can be switched out for a normal GroupBox.
We are using this in two places. In one case we have a number of these panels inside a standard Delphi Flow Panel (I'm not sure what version that was added). When our DynPanel collapses everything automatically moves up and fills the space.
In the other case we have a window that is split between a main section and a toolbox. The two are separated by a standard splitter. The main window is set to align to client. When our panel collapses or expands. the splitter automatically moves and expands the main section.
We never did quite get the "container" control stuff to work so items you add to the panel can be moved outside the bounds you would normally expect in a group box. But that does not cause us any major problems so we just left it. This also does not account for DPI changes in relation to the button size. The caption will get bigger but the button will not.
unit DynPanel;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
ExtCtrls, cxGroupBox;
const
DEFAULTBUTTONWIDTH = 16;
DEFAULTWIDTH = 161;
DEFAULTHEIGHT = 81;
cButtonPadding = 8;
cCollapsePadding = 3;
cCaptionPadding = ' ';
cCollapsedSize = DEFAULTBUTTONWIDTH + cCollapsePadding;
cAutoCollapseSize = DEFAULTBUTTONWIDTH + cButtonPadding;
type
TCollapseDirection = (cdUp, cdRight, cdLeft);
TMinDemension = cAutoCollapseSize..High(Integer);
TDynPanel = class(TPanel)
private
FGroupBox: TcxGroupBox;
FButtonPanel: TPanel;
FButtonImage: TImage;
FExpand: Boolean;
FOldHeight: Integer;
FOldWidth: Integer;
FCollapseDirection: TCollapseDirection;
FOrigGroupBoxCaption: String;
FAutoCollapseHeight: TMinDemension;
FAutoCollapseWidth: TMinDemension;
FButtonPadding: integer;
FCollapsePadding: integer;
FCollapsedSize: integer;
procedure SetExpand(Value: Boolean);
procedure SetGroupBoxCaption(Value: string);
procedure ButtonMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
procedure CMShowingChanged(var Message: TMessage); message CM_SHOWINGCHANGED;
procedure EnableControls(Value: Boolean);
procedure SetCollapseDirection(Value: TCollapseDirection);
procedure ConfigurePanel;
procedure SetMinHeight(Value: TMinDemension);
procedure SetMinWidth(Value: TMinDemension);
procedure UpdateImage();
protected
procedure Resize; override;
procedure ChangeScale(M, D: Integer); override;
public
constructor Create(AOwner: TComponent); override;
property OldHeight: Integer read FOldHeight write FOldHeight;
property OldWidth: Integer read FOldWidth write FOldWidth;
property GroupBox: TcxGroupBox read FGroupBox;
published
property Caption: string read FOrigGroupBoxCaption write SetGroupBoxCaption;
property Expand: Boolean read FExpand write SetExpand;
property BevelOuter default bvNone;
property CollapseDirection: TCollapseDirection read FCollapseDirection write SetCollapseDirection default cdUp;
property AutoCollapseHeight: TMinDemension read FAutoCollapseHeight write SetMinHeight default cAutoCollapseSize;
property AutoCollapseWidth: TMinDemension read FAutoCollapseWidth write SetMinWidth default cAutoCollapseSize;
end;
procedure Register;
implementation
{$R 'ButtonImages\ButtonImages.res' 'ButtonImages\ButtonImages.rc'}
uses cxEdit;
procedure Register;
begin
RegisterComponents('AgWare', [TDynPanel]);
end;
{ TDynPanel }
{
TDynPanel.Create
---------------------------------------------------------------------------
}
constructor TDynPanel.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
Self.ControlStyle := ControlStyle - [csSetCaption];
Self.Width := DEFAULTWIDTH;
Self.Height := DEFAULTHEIGHT;
BevelOuter := bvNone;
FExpand := True;
FOldHeight := Self.Height;
FOldWidth := Self.Width;
FOrigGroupBoxCaption := 'AgDynPanel';
FCollapseDirection := cdUp;
FAutoCollapseHeight := cAutoCollapseSize;
FAutoCollapseWidth := cAutoCollapseSize;
FGroupBox := TcxGroupBox.Create(Self);
FGroupBox.Parent := Self;
FGroupBox.Align := alClient;
FGroupBox.Alignment := alTopLeft;
FButtonPanel := TPanel.Create(Self);
FButtonPanel.Parent := Self;
FButtonPanel.Top := 0;
FButtonPanel.Width := DEFAULTBUTTONWIDTH;
FButtonPanel.Height := DEFAULTBUTTONWIDTH;
FButtonPanel.Left := Width - DEFAULTBUTTONWIDTH - FButtonPadding;
FButtonPanel.OnMouseDown := ButtonMouseDown;
FButtonImage := TImage.Create(Self);
FButtonImage.Parent := FButtonPanel;
FButtonImage.Align := alClient;
FButtonImage.Stretch := false;
FButtonImage.Center := true;
FButtonImage.OnMouseDown := ButtonMouseDown;
UpdateImage;
// The click should also work for the entire top of the group box.
FGroupBox.OnMouseDown := ButtonMouseDown;
FGroupBox.Caption := FOrigGroupBoxCaption;
FGroupBox.Style.Font.Style := FGroupBox.Style.Font.Style + [fsBold];
FButtonPadding := cButtonPadding;
FCollapsePadding := cCollapsePadding;
FCollapsedSize := cCollapsedSize;
end;
{
TDynPanel.SetGroupBoxCaption
---------------------------------------------------------------------------
}
procedure TDynPanel.SetGroupBoxCaption(Value: String);
begin
FOrigGroupBoxCaption := Value;
ConfigurePanel;
end;
{
TDynPanel.SetMinHeight
---------------------------------------------------------------------------
}
procedure TDynPanel.SetMinHeight(Value: TMinDemension);
begin
if Value = FAutoCollapseHeight then
Exit; // >>----->
FAutoCollapseHeight := Value;
if Showing then
Resize;
end;
{
TDynPanel.SetMinWidth
---------------------------------------------------------------------------
}
procedure TDynPanel.SetMinWidth(Value: TMinDemension);
begin
if Value = FAutoCollapseWidth then
Exit; // >>----->
FAutoCollapseWidth := Value;
if Showing then
Resize;
end;
{
TDynPanel.ButtonMouseDown
---------------------------------------------------------------------------
}
procedure TDynPanel.ButtonMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
if Button mbLeft then
Exit; // >>----->
if ((FExpand = True) and (Y FCollapsePadding)) or
((FExpand = False) and (FCollapseDirection = cdLeft) and (X >----->
FExpand := Value;
//ConfigurePanel;
//--------------------------------------------------------------------------
// Set the group box size
//--------------------------------------------------------------------------
//
// I chose to do the resizing of the control here rather than in
// ConfigurePanel because if you do it there the SetBounds will call ReSize
// which will call ConfigurePanel again so that you would need to keep track
// of a boolean variable to determine if you are making recursive calls into
// ConfigurePanel. That is one reason. Another is that when the dfm values
// are streamed in and the properties get set you will resize the control
// before the actual Height and Width properties are set. This will cause
// bogus default values to be stored for FOldHeight and FOldWidth and when
// the control is displayed the dimensions will be wrong. If you size the
// control here then, on creation, Resize will not get called and the
// FOldHeight and FOldWidth values will not get saved off until
// CMShowingChanged will explicitly call ReSize after the dimensions are
// properly set. If you move this code into ConfigurePanel then when the
// caption is streamed in and set from the dfm then ConfigurePanel would get
// called, we would SetBounds there and then Resize would fire storing off the
// default invalid values for the FOld variables as mentioned above.
// Hope this makes sense. Leave the SetBounds calls here and make your life
// easier. :)
//--------------------------------------------------------------------------
// Changing to Expanded
if FExpand = True then
begin
// Up
if FCollapseDirection = cdUp then
SetBounds(Left, Top, Width, FOldHeight)
// Right
else if FCollapseDirection = cdRight then
SetBounds((Left + Width) - FOldWidth, Top, FOldWidth, Height)
// Left
else if FCollapseDirection = cdLeft then
SetBounds(Left, Top, FOldWidth, Height);
end
// Changing to Collapsed
else
begin
// Up
if FCollapseDirection = cdUp then
begin
// Reset the AutoCollapseHeight just to make sure we don't try to
// recollapse on resize.
if FAutoCollapseHeight FGroupBox) and
(Self.Controls[i] FButtonPanel) then
begin
Self.Controls[i].Enabled := Value;
Self.Controls[i].Visible := Value;
end;
end;
end;
{
TDynPanel.CMShowingChanged
---------------------------------------------------------------------------
}
procedure TDynPanel.CMShowingChanged(var Message: TMessage);
begin
inherited;
if Showing then
Resize;
end;
{
TDynPanel.Resize
---------------------------------------------------------------------------
}
procedure TDynPanel.Resize;
begin
if FExpand = True then
begin
if (FCollapseDirection = cdUp) and (Height FAutoCollapseHeight then
begin
FOldHeight := Height;
Expand := True;
end
else
Height := FCollapsedSize;
end
else if (FCollapseDirection = cdLeft) or (FCollapseDirection = cdRight) then
begin
if (Width > FAutoCollapseWidth) then
begin
FOldWidth := Width;
Expand := True;
end
else
Width := FCollapsedSize;
end;
end;
ConfigurePanel;
end;
{
TDynPanel.ChangeScale
---------------------------------------------------------------------------
}
procedure TDynPanel.ChangeScale(M, D: Integer);
begin
FAutoCollapseHeight := MulDiv(FAutoCollapseHeight, M, D);
FAutoCollapseWidth := MulDiv(FAutoCollapseWidth, M, D);
FButtonPadding := MulDiv(FButtonPadding, M, D);
FCollapsePadding := MulDiv(FCollapsePadding, M, D);
FCollapsedSize := MulDiv(FCollapsedSize, M, D);
FOldHeight := MulDiv(FOldHeight, M, D);
FOldWidth := MulDiv(FOldWidth, M, D);
// inherited will cause resize to be called. I need to update
// my internal values before that happens, otherwise I will resize based
// on the old values.
inherited;
end;
{
TDynPanel.SetCollapseDirection
---------------------------------------------------------------------------
}
procedure TDynPanel.SetCollapseDirection(Value: TCollapseDirection);
begin
if Value = FCollapseDirection then
Exit; // >>----->
FCollapseDirection := Value;
ConfigurePanel;
end;
{
TDynPanel.ConfigurePanel
---------------------------------------------------------------------------
}
procedure TDynPanel.ConfigurePanel;
begin
//--------------------------------------------------------------------------
// Set the group box style, caption alignment, caption, button position, and
// button image
//--------------------------------------------------------------------------
// Changing to Expanded
if FExpand = True then
begin
FGroupBox.Style.Color := clWhite;
// Up
if FCollapseDirection = cdUp then
begin
FGroupBox.Alignment := alTopLeft;
FGroupBox.Caption := FOrigGroupBoxCaption;
FButtonPanel.Top := 0;
FButtonPanel.Left := Width - FButtonPanel.Width - FButtonPadding;
end
// Right
else if FCollapseDirection = cdRight then
begin
FGroupBox.Alignment := alTopLeft;
FGroupBox.Caption := ' ' + FOrigGroupBoxCaption;
FButtonPanel.Top := 0;
FButtonPanel.Left := FButtonPadding;
end
// Left
else if FCollapseDirection = cdLeft then
begin
FGroupBox.Alignment := alTopLeft;
FGroupBox.Caption := FOrigGroupBoxCaption;
FButtonPanel.Top := 0;
FButtonPanel.Left := Width - FButtonPanel.Width - FButtonPadding;
end;
end
// Changing to Collapsed
else
begin
FGroupBox.Style.Color := clGradientActiveCaption;
// Up
if FCollapseDirection = cdUp then
begin
FGroupBox.Alignment := alTopLeft;
FGroupBox.Caption := FOrigGroupBoxCaption;
FButtonPanel.Top := 0;
FButtonPanel.Left := Width - FButtonPanel.Width - FButtonPadding;
end
// Right
else if FCollapseDirection = cdRight then
begin
FGroupBox.Alignment := alRightTop;
FGroupBox.Caption := ' ' + FOrigGroupBoxCaption;
FButtonPanel.Top := FButtonPadding;
FButtonPanel.Left := FCollapsePadding;
end
// Left
else if FCollapseDirection = cdLeft then
begin
FGroupBox.Alignment := alLeftTop;
FGroupBox.Caption := FOrigGroupBoxCaption + ' ';
FButtonPanel.Top := FButtonPadding;
FButtonPanel.Left := 0;
end;
end;
UpdateImage;
// Now draw the button and invalidate Self
Self.Invalidate;
end;
{
TDynPanel.UpdateImage
---------------------------------------------------------------------------
}
procedure TDynPanel.UpdateImage();
begin
case FCollapseDirection of
cdUp:
begin
if FExpand = true then
FButtonImage.Picture.Bitmap.LoadFromResourceName(HInstance, 'ButtonImageUp')
else
FButtonImage.Picture.Bitmap.LoadFromResourceName(HInstance, 'ButtonImageDown');
end;
cdLeft:
begin
if FExpand = true then
FButtonImage.Picture.Bitmap.LoadFromResourceName(HInstance, 'ButtonImageLeft')
else
FButtonImage.Picture.Bitmap.LoadFromResourceName(HInstance, 'ButtonImageRight');
end;
cdRight:
begin
if FExpand = true then
FButtonImage.Picture.Bitmap.LoadFromResourceName(HInstance, 'ButtonImageRight')
else
FButtonImage.Picture.Bitmap.LoadFromResourceName(HInstance, 'ButtonImageLeft');
end;
end;
end;
end.
Close to the Left
Close to the Top
The new Delphi version will include such kind of sliding panels ( trough the integration of FireMonkey, formely vgScene/dxScene ). You'll just have to click on height or position props and an option will allow to create a animation for this, with various option ( interpolation kind, duration etc).
Since version 2009, there is a TCategoryPanelGroup where you add TCategoryPanel.
FoldingPanel
See FoldingPanel v1.3 here: http://www.torry.net/authorsmore.php?id=2386
I used it for years.
It comes with nice chevron bitmap too.
Small issue: it does not support composite controls (like TLabeledEdit).
Advantage: The component comes as a single PAS file (easy to install into the Palette). They say that it is for Delphi 5 but I have it installed in XE7 and works without problems (which indicates quality).
Freeware
TSplitView
If you have a newer version of Delphi (like Tokyo) you can use TSplitView.
Note: The Align property it missing. And at the beginning it might seems that it can only be aligned to Left. But it is not true. Instead of an Align property it has a Placement property (with two values: svpRight / svpLeft).
Note: It has some small glitches related to control size/placement.
Note: It is not (NOT EVEN BY FAR) as complete as FoldingPanel. You still have to write some cod eto implement some kind of chevron to collapse/expend the panel.
https://www.youtube.com/watch?v=3hUG8o7PpCU
Freeware (if you have Delphi Tokyo).
TCategoryPanelGroup
Also take a look at TCategoryPanelGroup. It might or might not work, depending on what you need it for.
Freeware (if you have Delphi XE7)

Cannot make DragAcceptFiles work when using a dialog box

I am using Delphi to code a simple app that needs to accept dropped directory names onto it. Though I am using Delphi, this program is straight Windows API (no VCL, no object oriented stuff) just as it would be in plain C.
I had some code that already worked using a normal window (as created by CreateWindowEx). When I converted that code to use a dialog box, the drag and drop functionality broke and I cannot figure out why.
This is (part of) the code I had that worked fine:
function WndProc (Wnd : hWnd; Msg, wParam, lParam : DWORD) : DWORD; stdcall;
{ main application/window handler function }
const
DROPPED_FILE_COUNT = -1;
var
FileCnt : integer; { number of files dropped }
Filename : packed array[0..MAX_PATH] of char; { filename buffer }
DropInfo : HDROP absolute wParam; { wParam points to Drop...}
I : integer; { for loop }
begin
WndProc := 0;
{ let the Windows default handler take care of everything }
case msg of
WM_CREATE:
begin
InitCommonControls;
if CreateWindowEx(WS_EX_CLIENTEDGE or WS_EX_RIGHTSCROLLBAR,
'LISTBOX',
nil,
WS_CHILD or WS_HSCROLL or WS_VSCROLL or
WS_CLIPSIBLINGS or WS_CLIPCHILDREN or
WS_VISIBLE or LBS_NOINTEGRALHEIGHT,
0,
0,
0,
0,
Wnd,
IDC_LISTBOX,
hInstance,
nil) = 0 then
begin
MessageBox(Wnd,
'Couldn''t create the listbox', 'Main Window', MB_OK);
WndProc := -1;
end;
{ let Windows know that we accept files being dragged over our client}
{ area. }
DragAcceptFiles(Wnd, TRUE);
{ tell the listbox to use a nice font }
SendMessage(GetDlgItem(Wnd, IDC_LISTBOX),
WM_SETFONT, GetStockObject(DEFAULT_GUI_FONT), 0);
exit;
end; { WM_CREATE }
WM_DROPFILES:
begin
{ one or more files have been dropped on us! }
FileCnt := DragQueryFile(DropInfo, DROPPED_FILE_COUNT, nil, 0);
for I := 0 to FileCnt - 1 do
begin
{ get the dropped files names and add them to the listbox }
DragQueryFile(DropInfo, I, Filename, sizeof(Filename));
ListBox_AddString(GetDlgItem(Wnd, IDC_LISTBOX), Filename);
end;
{ tell Windows that we are done grabbing the dropped files }
DragFinish(DropInfo);
exit;
end; { WM_DROPFILES }
... followed by other stuff that has nothing to do with drag and drop ...
I converted that to this:
procedure ExecWM_INITDIALOG(wnd : hWnd);
begin
{ ensure that the listbox isn't accepting dropped files }
DragAcceptFiles(GetDlgItem(Wnd, IDC_DIRECTORIES), FALSE);
{ ensure the main dialog window accept dropped files }
DragAcceptFiles(Wnd, TRUE);
{ load the current values in the registry }
LoadRegistrySettings(RegistryKey, RegistrySettings, RegistrySettingsCount)
end;
procedure ExecWM_DROPFILES(Wnd : hWnd; wParam : word; lParam : longint);
const
DROPPED_FILE_COUNT = -1;
var
FileCnt : integer; { number of files dropped }
Filename : packed array[0..MAX_PATH] of char; { filename buffer }
DropInfo : HDROP absolute wParam; { wParam points to Drop...}
I : integer; { for loop }
begin
{ one or more files have been dropped on us! }
{ -->>> The problem seems to show up at this statement: }
{ the DropInfo (wParam) has a value that seems too low ($20) }
{ when using the code that works the value is much larger }
FileCnt := DragQueryFile(DropInfo, DROPPED_FILE_COUNT, nil, 0);
for I := 0 to FileCnt - 1 do
begin
{ get the dropped files names and add them to the listbox }
DragQueryFile(DropInfo, I, Filename, sizeof(Filename));
ListBox_AddString(GetDlgItem(Wnd, IDC_DIRECTORIES), Filename);
end;
{ tell Windows that we are done grabbing the dropped files }
DragFinish(DropInfo);
end;
function BackupConfigProc(wnd : hWnd;
msg : word;
wParam : word;
lParam : longint) : bool; stdcall;
begin
BackupConfigProc := FALSE; { default return value }
case msg of
WM_COMMAND:
begin
ExecWM_COMMAND (wnd, wParam, lParam);
BackupConfigProc := TRUE;
end;
WM_DROPFILES:
begin
ExecWM_DROPFILES(Wnd, wParam, lParam);
//BackupConfigProc := TRUE; { this was a shot in the dark }
end;
WM_INITDIALOG:
begin
ExecWM_INITDIALOG (wnd);
BackupConfigProc := TRUE;
end;
WM_CLOSE:
begin
EndDialog(wnd, 0); { and return the default FALSE }
end;
end;
end;
begin
DialogBox(hInstance, 'BackupConfigDlg', 0, #BackupConfigProc);
end.
It looks like the value of the DropInfo (wParam) received by the dialog box is not valid (as it is much lower in value than the one received by the non dialog code that works).
I will be grateful to all who can shed some light as to why the Dialog version does not work and what needs to be done to make it work.
Thank you,
John.
procedure ExecWM_DROPFILES(Wnd : hWnd; wParam : word; lParam : longint);
wParam should not be typed word but Windows.WPARAM which is actually a long int (on Win32).

Is there any way to get all the controls on a container control?

I've got a form with a bunch of controls on it, and I wanted to iterate through all the controls on a certain panel and enable/disable them.
I tried this:
var component: TComponent;
begin
for component in myPanel do
(component as TControl).Enabled := Value;
end;
But that did nothing. Turns out all components are in the form's component collection, not their parent object's. So does anyone know if there's any way to get all the controls inside a control? (Besides an ugly workaround like this, which is what I ended up having to do):
var component: TComponent;
begin
for component in myPanel do
if (component is TControl) and (TControl(component).parent = myPanel) then
TControl(component).Enabled := Value;
end;
Someone please tell me there's a better way...
You're looking for the TWinControl.Controls array and the accompanying ControlCount property. Those are for a control's immediate children. To get grandchildren etc., use standard recursive techniques.
You don't really want the Components array (which is what the for-in loop iterates over) since it has nothing to do, in general, with the parent-child relationship. Components can own things that have no child relationship, and controls can have children that they don't own.
Also note that disabling a control implicitly disables all its children, too. You cannot interact with the children of a disabled control; the OS doesn't send input messages to them. To make them look disabled, though, you'll need to disable them separately. That is, to make a button have grayed text, it's not enough to disable its parent, even though the button won't respond to mouse clicks. You need to disable the button itself to make it paint itself "disabledly."
If you disable a panel, al controls on it are disabled too.
Recursive solution with anonymous methods:
type
TControlProc = reference to procedure (const AControl: TControl);
procedure TForm6.ModifyControl(const AControl: TControl;
const ARef: TControlProc);
var
i : Integer;
begin
if AControl=nil then
Exit;
if AControl is TWinControl then begin
for i := 0 to TWinControl(AControl).ControlCount-1 do
ModifyControl(TWinControl(AControl).Controls[i], ARef);
end;
ARef(AControl);
end;
procedure TForm6.Button1Click(Sender: TObject);
begin
ModifyControl(Panel1,
procedure (const AControl: TControl)
begin
AControl.Enabled := not Panel1.Enabled;
end
);
end;
Here is a Delphi 2007 way:
procedure TForm6.ModifyControl(const AControl: TControl; const value: Boolean);
var
i: Integer;
begin
if AControl=nil then Exit;
if AControl is TWinControl then begin
for i := 0 to TWinControl(AControl).ControlCount-1 do
ModifyControl(TWinControl(AControl).Controls[i], value);
end;
Acontrol.Enabled := value;
end;
procedure TForm6.Button1Click(Sender: TObject);
begin
ModifyControl(Panel1, true); // true or false
end;
Simply
Panel.Enabled := Value;
This one finds all controls, also nested in frames etc, and points to them via the list.
Be aware to free the list afterwards.
Function AllControls(form : tForm) : tList<tControl>;
Procedure Add(Control : tControl );
var i : integer;
begin
if Control is TWinControl then
with TWinControl(Control) do
for i := 0 to Controlcount-1 do
Add(Controls[i]);
if Control <> form then
result.Add(Control);
end;
begin
result := tlist<tControl>.create;
add(form);
end;
var contrls : tlist<tcontrol>;
c : tcontrol;
begin
try
contrls := AllControls(form1);
for c in ctrls do Visit(c); // Do something
finally
contrls.free;
end;
end;
And if you want a generic version, where you can ask for a specific control type, you can use this:
Procedure TForm1.Addcontrols( control : tcontrol; list : tlist<tcontrol>);
var i : integer;
begin
if control is twincontrol then
with twincontrol(control) do
for i := 0 to controlcount-1 do
addControl(controls[i], list);
list.Add(control)
end;
Function TForm1.GetControls<T>(f : tform) : tlist<T>;
var list : tlist<tcontrol>;
c : tcontrol;
begin
list := tlist<tcontrol>.Create;
addControls(f, list);
result := tlist<t>.create;
for c in list do
if c <> f then
if c is t then
result.Add(c);
list.free;
end;
procedure TForm1.FormCreate(Sender: TObject);
VAR List : TList<TRadioButton>;
begin
List := GetControls<TRadioButton>(self);
end;
end.
Use
List := GetControls<TControl>(self);
to get all controls..
I know this post is a little old but I came here based on a search for the same information. Here is some C++ code that I worked out for anyone interested.
// DEV-NOTE: GUIForm flattens the VCL controls
// VCL controls are nested. I.E. Controls on a
// Panel would have the Panel as a parent and if
// that Panel is on a TForm, TForm's control count
// does not account for the nested controls on the
// Panel.
//
// GUIControl is passed a Form pointer and an index
// value, the index value will walk the controls on the
// form and any child controls counting up to the idx
// value passed in. In this way, every control has a
// unique index value
//
// You can use this to iterate over every single control
// on a form. Here is example code:
//
// int count = 0;
// TForm *pTForm = some_form
// TControl *pCtrl = 0;
// do
// {
// pCtrl = GUIControl(pTForm, count++);
//
// }while(pCtrl);
TControl *GUIControl(TForm *F, int idx)
{
TControl *rval = 0;
int RunCount = 0;
for(int i=0; i<F->ControlCount && !rval; i++)
{
TControl *pCtl = F->Controls[i];
if(RunCount == idx )
rval = pCtl;
else
rval = GUIChildControl( pCtl, RunCount, idx);
RunCount++;
}
return(rval);
}
TControl *GUIChildControl(TControl *C, int &runcount, int idx)
{
TControl *rval = 0;
TWinControl *pC = dynamic_cast<TWinControl *>(C);
if(pC)
{
for(int i=0; i<pC->ControlCount && !rval; i++)
{
TControl *pCtrl = pC->Controls[i];
runcount++;
if( runcount == idx)
rval = pCtrl;
else
{
TWinControl *pCC = dynamic_cast<TWinControl *>(pCtrl);
if(pCC)
{
if( pCC->ControlCount )
rval = GUIChildControl(pCtrl, runcount, idx);
}
}
}
}
return(rval);
}

Resources