I need to get the number of displayed lines in TMemo (this include the lines that was wrapped because WordWrap is set to true). I need this to auto adjust the height of the Tmemo to it's content.
lines.count of course don't care about wrapped lines so i can't use it. strangely TextPosToPos also don't care about wrapped lines so i can't use it too ...
I m under firemonkey and delphi Berlin
Edit after I learned about ContentsBounds. My original (and obsolete) answer is still visible in the revisions.
Why do you need the number of displayed lines to adjust the height of the TMemo? This resizes the TMemo to its content:
Memo1.Height := Memo1.ContentBounds.Height + 5; // Small const to allow for rendering margins
It also takes word-wrap into consideration.
I don't know why using ContentBounds is "not really ideal". Here's how I do it:
uses
FMX.TextLayout, FMX.Graphics;
function MeasureTextHeight(const AFont: TFont; const AText: string): Single;
var
LLayout: TTextLayout;
begin
LLayout := TTextLayoutManager.DefaultTextLayout.Create;
try
LLayout.BeginUpdate;
try
LLayout.WordWrap := False;
LLayout.Font.Assign(AFont);
LLayout.Text := AText;
finally
LLayout.EndUpdate;
end;
Result := LLayout.TextHeight;
finally
LLayout.Free;
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
LTextHeight: Single;
LLines: Integer;
begin
LTextHeight := MeasureTextHeight(Memo1.TextSettings.Font, Memo1.Text);
LLines := Round(Memo1.ContentBounds.Height / LTextHeight);
end;
This is the rough way i use to calculate the number of lines an TMemo (styled) under firemonkey :
type
_TStyledMemoProtectedAccess = class(TStyledMemo);
procedure TALStyledMemo.OnApplyStyleLookupImpl(sender: Tobject);
Var I, j, k, l: integer;
begin
// TALStyledMemo
// TStyledMemo
// TLayout
// TActiveStyleObject
// TLayout
// TScrollBar
// TScrollBar
// TLayout
// TSmallScrollBar
// TSmallScrollBar
// TScrollContent
for I := 0 to controls.Count - 1 do begin
if (controls[i] is TStyledMemo) then begin // << TStyledMemo
fStyledMemo := TStyledMemo(controls[i]);
end;
end;
end;
function TALStyledMemo.getLineHeight: Single;
begin
if fStyledMemo <> nil then result := _TStyledMemoProtectedAccess(fStyledMemo).GetLineHeight
else result := 0;
end;
function TALStyledMemo.getLineCount: integer;
var aLineHeight: Single;
begin
aLineHeight := getLineHeight;
if compareValue(aLineHeight, 0, Tepsilon.Position) > 0 then result := round(ContentBounds.Height / aLineHeight)
else result := 0;
end;
Related
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;
}
There's a problem with the TScrollBox in Delphi 5 when using Cirtix, on some systems, when a user scrolls by clicking the button at the top or bottom of the end of scrollbar the whole application freezes. We had the issue in QucikReports previews initially and got round it by implementing our own scrollbars in the TScrollBox.
We now have a piece of bespoke work that uses a TScrollBox and the client is reporting a similar problem so I'm working round it in the same way. I hide the TScrollBox scrollbars and add in my own. When those are clicked I call the following.
Note, this test code is not currently running in Citrix, I've tested on XP and Window 7.
I am turning off redrawing of the control, moving all the child controls, then turning drawing back on and calling Invalidate. I would expect invalidate to fully redraw the control but that's not happening.
procedure TScrollBoxScrollReplacement.ScrollControls(x: Integer; y: Integer);
var
I: Integer;
begin
if (x = 0) and (y = 0) then
Exit;
// Stop the control from repaining while we're updating it
try
SendMessage(FScrollBox.Handle, WM_SETREDRAW, 0, 0);
for I := 0 to FScrollBox.ControlCount - 1 do
begin
if (FScrollBox.Controls[I] = FVScrollBar) or (FScrollBox.Controls[I] = FHScrollBar) then
Continue;
FScrollBox.Controls[I].Left := FScrollBox.Controls[I].Left + x;
FScrollBox.Controls[I].Top := FScrollBox.Controls[I].Top + y;
end;
finally
// Turn on painting again
SendMessage(FScrollBox.Handle, WM_SETREDRAW, 1, 0);
end;
// Redraw everything
InvalidateEverything(FScrollBox);
end;
Code to redraw controls
procedure TScrollBoxScrollReplacement.InvalidateEverything(AControl: TControl);
var
I: Integer;
begin
AControl.Invalidate();
if (AControl is TWinControl) then
for I := 0 to TWinControl(AControl).ControlCount - 1 do
InvalidateEverything(TWinControl(AControl).Controls[I]);
end;
I added in the Invalidate, Refresh and Reapint and loop through all child controls in an effort to get it working, but still no luck. The edit boxes look as follows:
If I set Visible to false and back to true then they'll redraw correctly, but there is obviously a horrible flicker. They also redraw correctly if I minimise the maximise the window, or drag it off and on the screen.
Any help would be much appreciated.
edit : Some info about the answers.
Users looking for a solution, I'd recommend you try both. David's and Sertac's. David's looks like it is the correct solution according to Microsoft's documentation. However, with the Delphi scrollbox, labels placed directly in the scrollbox flicker, where are labels placed in groupboxes in the scrollbox are perfectly smooth. I think this might be an issue with all components that don't descend from TWinControl. Scrolling itself is smoother with David's solution, but there's less flicking using WM_SETREDRAW and RedrawWindow. I would have liked to accept both as answers as both have their advantages and disadvantages.
edit : Code for the whole class below
To test just add a scrollbox with some controls to your form and call
TScrollBoxScrollReplacement.Create(ScrollBox1);
.
unit ScrollBoxScrollReplacement;
interface
uses extctrls, stdctrls, SpScrollBox, forms, Controls, classes, Messages, Windows, Sysutils, Math;
type
TScrollBoxScrollReplacement = class(TComponent)
private
FLastVScrollPos: Integer;
FLastHScrollPos: Integer;
FScrollBox: TScrollBox;
FVScrollBar: TScrollBar;
FHScrollBar: TScrollBar;
FVScrollBarVisible: Boolean;
FHScrollBarVisible: Boolean;
FCornerPanel: TPanel;
FMaxRight: Integer;
FMaxBottom: Integer;
FOriginalResizeEvent: TNotifyEvent;
FOriginalCanResizeEvent: TCanResizeEvent;
FInScroll: Boolean;
function GetHScrollHeight: Integer;
function GetVScrollWidth: Integer;
procedure ReplaceScrollBars;
function SetUpScrollBar(AControlScrollBar: TControlScrollBar; AKind: TScrollBarKind): TScrollBar;
procedure ScrollBoxResize(Sender: TObject);
procedure ScrollBarEnter(Sender: TObject);
procedure PositionScrollBars;
procedure Scroll(Sender: TObject; ScrollCode: TScrollCode; var ScrollPos: Integer);
procedure ScrollControls(x, y: Integer);
procedure CalculateControlExtremes();
procedure ResetVScrollBarRange;
procedure ResetHScrollBarRange;
function IsReplacementControl(AControl: TControl): Boolean;
property HScrollHeight: Integer read GetHScrollHeight;
property VScrollWidth: Integer read GetVScrollWidth;
procedure ScrollBoxCanResize(Sender: TObject; var NewWidth,
NewHeight: Integer; var Resize: Boolean);
public
constructor Create(AScrollBox: TScrollBox); reintroduce; overload;
destructor Destroy(); override;
procedure ResetScrollBarRange();
procedure BringReplacementControlsToFront();
end;
implementation
{ TScrollBoxScrollReplacement }
constructor TScrollBoxScrollReplacement.Create(AScrollBox: TScrollBox);
begin
// Set up the scrollbox as our owner so we're destroyed when the scrollbox is
inherited Create(AScrollBox);
FScrollBox := AScrollBox;
ReplaceScrollBars();
// We make a note of any existing resize and can resize events so we can call them to make sure we don't break anything
FOriginalResizeEvent := FScrollBox.OnResize;
FScrollBox.OnResize := ScrollBoxResize;
FOriginalCanResizeEvent := FScrollBox.OnCanResize;
FScrollBox.OnCanResize := ScrollBoxCanResize;
end;
// This is called (unintuitively) when controls are moved within the scrollbox. We can use this to reset our scrollbar ranges
procedure TScrollBoxScrollReplacement.ScrollBoxCanResize(Sender: TObject; var NewWidth,
NewHeight: Integer; var Resize: Boolean);
begin
if (not FInScroll) then
begin
ResetScrollBarRange();
BringReplacementControlsToFront();
end;
if (Assigned(FOriginalCanResizeEvent)) then
FOriginalCanResizeEvent(Sender, NewWidth, NewHeight, Resize);
end;
procedure TScrollBoxScrollReplacement.ScrollBoxResize(Sender: TObject);
begin
if (Assigned(FOriginalResizeEvent)) then
FOriginalResizeEvent(Sender);
ResetScrollBarRange();
end;
// Hides the original scrollbars and adds in ours
procedure TScrollBoxScrollReplacement.ReplaceScrollBars();
begin
FVScrollBar := SetUpScrollBar(FScrollBox.VertScrollBar, sbVertical);
FVScrollBarVisible := FVScrollBar.Visible;
FHScrollBar := SetUpScrollBar(FScrollBox.HorzScrollBar, sbHorizontal);
FHScrollBarVisible := FHScrollBar.Visible;
FCornerPanel := TPanel.Create(FScrollBox);
FCornerPanel.Parent := FScrollBox;
ResetScrollBarRange();
end;
procedure TScrollBoxScrollReplacement.PositionScrollBars();
begin
// Align our scrollbars correctly
FVScrollBar.Top := 0;
FVScrollBar.Left := FScrollBox.ClientWidth - FVScrollBar.Width;
FVScrollBar.Height := FScrollBox.ClientHeight - HScrollHeight;
// FVScrollBar.BringToFront();
FHScrollBar.Left := 0;
FHScrollBar.Top := FScrollBox.ClientHeight - FHScrollBar.Height;
FHScrollBar.Width := FScrollBox.ClientWidth - VScrollWidth;
// FHScrollBar.BringToFront();
// If both scrollbars are visible we'll put a panel in the corner so we can't see components through it
if (FVScrollBar.Visible) and (FHScrollBar.Visible) then
begin
FCornerPanel.Left := FHScrollBar.Width;
FCornerPanel.Top := FVScrollBar.Height;
FCornerPanel.Width := FVScrollBar.Width;
FCornerPanel.Height := FHScrollBar.Height;
FCornerPanel.Visible := True;
// FCornerPanel.BringToFront();
end
else
FCornerPanel.Visible := False;
end;
procedure TScrollBoxScrollReplacement.ResetScrollBarRange();
begin
CalculateControlExtremes();
ResetVScrollBarRange();
ResetHScrollBarRange();
PositionScrollBars();
end;
procedure TScrollBoxScrollReplacement.ResetVScrollBarRange();
var
ScrollMax: Integer;
ScrollAmount: Integer;
begin
// If all the controls fit to the right of the screen, but there are controls off the left then we'll scroll right.
ScrollMax := FMaxBottom - FScrollBox.ClientHeight + FHScrollBar.Height;
if (ScrollMax < 0) and (FLastVScrollPos > 0) then
begin
ScrollAmount := Min(Abs(ScrollMax), FLastVScrollPos);
ScrollControls(0, ScrollAmount);
FLastVScrollPos := FLastVScrollPos - ScrollAmount;
CalculateControlExtremes();
end;
FVScrollBar.Max := Max(FMaxBottom - FScrollBox.ClientHeight + FHScrollBar.Height + FLastVScrollPos, 0);
FVScrollBar.Visible := (FVScrollBar.Max > 0) and FVScrollBarVisible;
end;
procedure TScrollBoxScrollReplacement.ResetHScrollBarRange();
var
ScrollMax: Integer;
ScrollAmount: Integer;
begin
// If all the controls fit to the bottom of the screen, but there are controls off the top then we'll scroll up.
ScrollMax := FMaxRight - FScrollBox.ClientWidth + FVScrollBar.Width;
if (ScrollMax < 0) and (FLastHScrollPos > 0) then
begin
ScrollAmount := Min(Abs(ScrollMax), FLastHScrollPos);
ScrollControls(ScrollAmount, 0);
FLastHScrollPos := FLastHScrollPos - ScrollAmount;
CalculateControlExtremes();
end;
FHScrollBar.Max := Max(FMaxRight - FScrollBox.ClientWidth + FVScrollBar.Width + FLastHScrollPos, 0);
FHScrollBar.Visible := (FHScrollBar.Max > 0) and FHScrollBarVisible;
end;
function TScrollBoxScrollReplacement.SetUpScrollBar(AControlScrollBar: TControlScrollBar; AKind: TScrollBarKind): TScrollBar;
begin
Result := TScrollBar.Create(FScrollBox);
Result.Visible := AControlScrollBar.Visible;
Result.Parent := FScrollBox;
Result.Kind := AKind;
Result.Ctl3D := False;
Result.Max := AControlScrollBar.Range;
Result.OnEnter := ScrollBarEnter;
Result.OnScroll := Scroll;
Result.SmallChange := 5;
Result.LargeChange := 20;
AControlScrollBar.Visible := False;
end;
destructor TScrollBoxScrollReplacement.Destroy;
begin
inherited;
end;
procedure TScrollBoxScrollReplacement.ScrollBarEnter(Sender: TObject);
begin
// We just call this here to make sure our ranges are set correctly - a backup in case things go wrong
ResetScrollBarRange();
end;
procedure TScrollBoxScrollReplacement.Scroll(Sender: TObject;
ScrollCode: TScrollCode; var ScrollPos: Integer);
var
Change: Integer;
begin
ResetScrollBarRange();
if (Sender = FVScrollBar) then
begin
Change := FLastVScrollPos - ScrollPos;
ScrollControls(0, Change);
FLastVScrollPos := ScrollPos;
end
else if (Sender = FHScrollBar) then
begin
Change := FLastHScrollPos - ScrollPos;
ScrollControls(Change, 0);
FLastHScrollPos := ScrollPos;
end;
end;
// Moves all the controls in the scrollbox except for the scrollbars we've added
{procedure TScrollBoxScrollReplacement.ScrollControls(x: Integer; y: Integer);
var
I: Integer;
begin
if (x = 0) and (y = 0) then
Exit;
// Stop the control from repaining while we're updating it
SendMessage(FScrollBox.Handle, WM_SETREDRAW, 0, 0);
FInScroll := True;
try
for I := 0 to FScrollBox.ControlCount - 1 do
begin
if IsReplacementControl(FScrollBox.Controls[I]) then
Continue;
FScrollBox.Controls[I].Left := FScrollBox.Controls[I].Left + x;
FScrollBox.Controls[I].Top := FScrollBox.Controls[I].Top + y;
end;
finally
// Turn on painting again
FInScroll := False;
SendMessage(FScrollBox.Handle, WM_SETREDRAW, 1, 0);
end;
// Redraw everything
RedrawWindow(FSCrollBox.Handle, nil, 0, RDW_ERASE or RDW_INVALIDATE or RDW_ALLCHILDREN);
end; }
procedure TScrollBoxScrollReplacement.ScrollControls(x: Integer; y: Integer);
var
I: Integer;
Control: TControl;
WinControl: TWinControl;
hWinPosInfo: HDWP;
begin
if (x = 0) and (y = 0) then
Exit;
hWinPosInfo := BeginDeferWindowPos(0);
Win32Check(hWinPosInfo<>0);
try
for I := 0 to FScrollBox.ControlCount - 1 do
begin
Control := FScrollBox.Controls[I];
if (Control = FVScrollBar) or (Control = FHScrollBar) then
Continue;
if Control is TWinControl then
begin
WinControl := FScrollBox.Controls[I] as TWinControl;
hWinPosInfo := DeferWindowPos(
hWinPosInfo,
WinControl.Handle,
0,
WinControl.Left + x,
WinControl.Top + y,
WinControl.Width,
WinControl.Height,
SWP_NOZORDER or SWP_NOOWNERZORDER or SWP_NOACTIVATE
);
Win32Check(hWinPosInfo<>0);
end
else
Control.SetBounds(Control.Left + x, Control.Top + y, Control.Width, Control.Height);
end;
finally
EndDeferWindowPos(hWinPosInfo);
end;
end;
// works out where our right most and bottom most controls are so we can set the scrollbars correctly
procedure TScrollBoxScrollReplacement.CalculateControlExtremes();
var
I: Integer;
Right: Integer;
Bottom: Integer;
begin
FMaxRight := 0;
FMaxBottom := 0;
for I := 0 to FScrollBox.ControlCount - 1 do
begin
if IsReplacementControl(FScrollBox.Controls[I]) then
Continue;
Right := FScrollBox.Controls[I].Left + FScrollBox.Controls[I].Width;
Bottom := FScrollBox.Controls[I].Top + FScrollBox.Controls[I].Height;
FMaxRight := Max(FMaxRight, Right);
FMaxBottom := Max(FMaxBottom, Bottom);
end;
end;
function TScrollBoxScrollReplacement.GetHScrollHeight: Integer;
begin
if (FHScrollBar.Visible) then
Result := FHScrollBar.Height
else
Result := 0;
end;
function TScrollBoxScrollReplacement.GetVScrollWidth: Integer;
begin
if (FVScrollBar.Visible) then
Result := FVScrollBar.Width
else
Result := 0;
end;
// Returns true if the passed control is one of the controls we've added
function TScrollBoxScrollReplacement.IsReplacementControl(
AControl: TControl): Boolean;
begin
Result := (AControl = FVScrollBar) or (AControl = FHScrollBar) or (AControl = FCornerPanel);
end;
procedure TScrollBoxScrollReplacement.BringReplacementControlsToFront;
begin
FVScrollBar.BringToFront();
FHScrollBar.BringToFront();
FCornerPanel.BringToFront();
end;
end.
I found that your code started working once I remove the two WM_SETREDRAW messages. That's your fundamental problem. You will need to remove the WM_SETREDRAW messages.
That will no doubt mean you still need to solve your problem with flickering, but that's a different problem. My quick experiments suggest that DeferWindowPos could solve that problem. For example:
procedure TScrollBoxScrollReplacement.ScrollControls(x: Integer; y: Integer);
var
I: Integer;
Control: TControl;
WinControl: TWinControl;
hWinPosInfo: HDWP;
begin
if (x = 0) and (y = 0) then
Exit;
hWinPosInfo := BeginDeferWindowPos(0);
Win32Check(hWinPosInfo<>0);
try
for I := 0 to FScrollBox.ControlCount - 1 do
begin
Control := FScrollBox.Controls[I];
if (Control = FVScrollBar) or (Control = FHScrollBar) then
Continue;
if Control is TWinControl then
begin
WinControl := FScrollBox.Controls[I] as TWinControl;
hWinPosInfo := DeferWindowPos(
hWinPosInfo,
WinControl.Handle,
0,
WinControl.Left + x,
WinControl.Top + y,
WinControl.Width,
WinControl.Height,
SWP_NOZORDER or SWP_NOOWNERZORDER or SWP_NOACTIVATE
);
Win32Check(hWinPosInfo<>0);
end
else
Control.SetBounds(Control.Left + x, Control.Top + y, Control.Width, Control.Height);
end;
finally
EndDeferWindowPos(hWinPosInfo);
end;
end;
Your non-windowed controls will still flicker, but you can make them windowed, or indeed put the whole content of the scroll box inside a windowed control. Heck, if you just did that, it would be enough to solve the problem!
For what it is worth, my trials indicate that DeferWindowPos gives smoother scrolling than WM_SETREDRAW and RedrawWindow. But these tests were hardly exhaustive and you might find different outcomes in your app.
Some asides regarding your code:
Your use of try/finally is incorrect. The pattern must be:
BeginSomething;
try
Foo;
finally
EndSomething;
end;
You get that wrong with your calls to SendMessage.
And you use an incorrect cast in InvalidateEverything. You cannot blindly cast a TControl to TWinControl. That said, that function does no good. You can remove it altogether. What it is attempting to do can be performed with a single call to Invalidate of the parent control.
You can replace your
FScrollBox.Invalidate();
with
RedrawWindow(FSCrollBox.Handle, nil, 0,
RDW_ERASE or RDW_INVALIDATE or RDW_ALLCHILDREN);
to have all controls invalidated and updated properly. RDW_ERASE is for erasing the previous positions of controls and RDW_ALLCHILDREN is for taking care of windowed controls inside. Non-win controls like labels should already be repainted because of RDW_INVALIDATE.
Although this approach may help avoiding the flicker that you observe, it may also cause some loss of smoothness of scrolling while thumb tracking. That's because the scroll position might need to be updated more often than a paint cycle is processed. To circumvent this, instead of invalidating you can update the control positions immediately:
RedrawWindow(FSCrollBox.Handle, nil, 0,
RDW_ERASE or RDW_INVALIDATE or RDW_UPDATENOW or RDW_ALLCHILDREN);
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:
I have grid panel 16 x 4 like this one:
Sometimes i want to hide some rows and to move bottom rows up. When I set component visible property to false the layout is not updated:
Nevertheless the row size type is set to auto:
Why the component don't set row height to zero when there is nothing to display?
Why the component don't set row height to zero when there is nothing to display ?
Because the row is considered as empty only if there are no components in all columns in that row, not if they're visible or not. So the same returns the IsRowEmpty method. To workaround this, you'd need to be notified by the cell component about its visibility change. When this notification is generated, you can check the row just like the IsRowEmpty method does, except you'll check if the controls are visible, not if they're assigned. Based on the result of such method, you can then set the size of the Value to 0 to hide the row.
With a help of interposed class, the method for checking if all controls in a row or column are visible, you might write something like this. Those methods return True, when all existing controls in a certain row or column are visible, False otherwise:
uses
ExtCtrls, Consts;
type
TGridPanel = class(ExtCtrls.TGridPanel)
public
function IsColContentVisible(ACol: Integer): Boolean;
function IsRowContentVisible(ARow: Integer): Boolean;
end;
implementation
function TGridPanel.IsColContentVisible(ACol: Integer): Boolean;
var
I: Integer;
Control: TControl;
begin
Result := False;
if (ACol > -1) and (ACol < ColumnCollection.Count) then
begin
for I := 0 to ColumnCollection.Count -1 do
begin
Control := ControlCollection.Controls[I, ACol];
if Assigned(Control) and not Control.Visible then
Exit;
end;
Result := True;
end
else
raise EGridPanelException.CreateFmt(sInvalidColumnIndex, [ACol]);
end;
function TGridPanel.IsRowContentVisible(ARow: Integer): Boolean;
var
I: Integer;
Control: TControl;
begin
Result := False;
if (ARow > -1) and (ARow < RowCollection.Count) then
begin
for I := 0 to ColumnCollection.Count -1 do
begin
Control := ControlCollection.Controls[I, ARow];
if Assigned(Control) and not Control.Visible then
Exit;
end;
Result := True;
end
else
raise EGridPanelException.CreateFmt(sInvalidRowIndex, [ARow]);
end;
And the usage shown for the first row:
procedure TForm1.Button1Click(Sender: TObject);
begin
// after you update visibility of controls in the first row...
// if any of the controls in the first row is not visible, change the
// row's height to 0, what makes it hidden, otherwise set certain height
if not GridPanel1.IsRowContentVisible(0) then
GridPanel1.RowCollection[0].Value := 0
else
GridPanel1.RowCollection[0].Value := 50;
end;
I've got a hacky solution ... keeping the Autosizing
Procedure ShowHideControlFromGrid(C:TControl);
begin
if C.Parent = nil then
begin
c.Parent := TWinControl(c.Tag)
end
else
begin
c.Tag := NativeInt(C.Parent);
c.Parent := nil;
end;
end;
procedure TForm4.Button1Click(Sender: TObject);
begin // e.g. Call
ShowHideControlFromGrid(Edit5);
ShowHideControlFromGrid(Edit6);
ShowHideControlFromGrid(Edit7);
ShowHideControlFromGrid(Label1);
end;
This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
Can I make a TMemo size itself to the text it contains?
Need to do autosize memo: height and width.
I autosize the height as follows:
function TForm1.AutoSizeMemoY(Memo: TMemo): word;
begin
Canvas.Font := Memo.Font;
Result := Canvas.TextExtent(Memo.Lines.Strings[0]).cy * Memo.Lines.Count +
Canvas.TextExtent(Memo.Lines.Strings[0]).cy;
end;
But I do not know how to do autosize the width. I have an idea: if the scrollbar is activated, then increase the width until it becomes inactive, but I do not know how to implement that.
Not the best solution but it works:
function GetTextWidth(F: TFont; s: string): integer;
var
l: TLabel;
begin
l := TLabel.Create(nil);
try
l.Font.Assign(F);
l.Caption := s;
l.AutoSize := True;
result := l.Width + 8;
finally
l.Free;
end;
end;
And add following code to the end of Memo1.Onchange event in this answer
LineInd := Memo1.Perform(EM_LINEFROMCHAR, Memo1.SelStart, 0);//focused Memo1 line Index
Wd := GetTextWidth(Memo1.Font, Memo1.Lines[LineInd]);
//MaxWidthLineInd = index of the line which has the largest width.
//Init value of MaxWidthLineInd = 0
if MaxWidthLineInd = LineInd then
Memo1.Width := Wd
else begin
if Wd > Memo1.Width then
begin
Memo1.Width := Wd;
MaxWidthLineInd := LineInd;
end;
end;