Stringgrid with buttons - delphi

1st Question:
How do you call the part in stringgrid that is not visible? You need to scroll to see it.
For example:
There are 20 rows in a stringgrid but you can see only 10 at a time. You need to scroll to see other 10. How are the "hidden" ones called?
2nd Question:
I know this is probably not the right way to do it so some pointers would be appreciated.
I have a string grid with 1 fixed row. I add ColorButtons at runtime. So I populate 1 column with buttons.
I use this buttons to "insert/delete" rows. As long as all of the grid is in the "visible" part this works well.
Problem occcurs when I "insert" new rows and move the buttons to the "hidden" part. The last button is then drawn to Cell[0,0]. Other buttons in the "hidden" part are drawn correctly. Any idea why this happens? Should I find a way to manage this problem in the OnDraw method or is there a better (correct) way to do this?
Code:
procedure Tform1.addButton(Grid : TStringGrid; ACol : Integer; ARow : Integer);
var
bt : TColorButton;
Rect : TRect;
index : Integer;
begin
Rect := Grid.CellRect(ACol,ARow);
bt := TColorButton.Create(Grid);
bt.Parent := Grid;
bt.BackColor := clCream;
bt.Font.Size := 14;
bt.Width := 50;
bt.Top := Rect.Top;
bt.Left := Rect.Left;
bt.Caption := '+';
bt.Name := 'bt'+IntToStr(ARow);
index := Grid.ComponentCount-1;
bt :=(Grid.Components[index] as TColorButton);
Grid.Objects[ACol,ARow] := Grid.Components[index];
bt.OnMouseUp := Grid.OnMouseUp;
bt.OnMouseMove := Grid.OnMouseMove;
bt.Visible := true;
end;
procedure MoveRowPlus(Grid : TStringGrid; Arow : Integer; stRow : Integer);
var
r, index : Integer;
bt : TColorButton;
Rect : TRect;
begin
Grid.RowCount := Grid.RowCount+stRow;
for r := Grid.RowCount - 1 downto ARow+stRow do
begin
Grid.Rows[r] := Grid.Rows[r-StRow];
end;
index := Grid.ComponentCount-1;
for r := Grid.RowCount - 1 downto ARow+stRow do
begin
bt :=(Grid.Components[index] as TColorButton);
Rect := Grid.CellRect(10,r);
bt.Top := Rect.Top;
bt.Left := Rect.Left;
Grid.Objects[10,r] := Grid.Components[index];
dec(index);
end;
for r := ARow to (ARow +stRow-1) do
begin
Grid.Rows[r].Clear;
end;
end;
procedure MoveRowMinus(Grid : TStringGrid; Arow : Integer; stRow : Integer);
var
r, index : Integer;
bt : TColorButton;
Rect : TRect;
begin
for r := ARow to Grid.RowCount-stRow-1 do
begin
Grid.Rows[r] := Grid.Rows[r+StRow];
end;
index := ARow-1;
for r := ARow to Grid.RowCount-stRow-1 do
begin
Rect := Grid.CellRect(10,r);
bt :=(Grid.Components[index] as TColorButton);
bt.Top := Rect.Top;
bt.Left := Rect.Left;
Grid.Objects[10,r] := Grid.Components[index];
bt.Visible := true;
inc(index);
end;
for r := Grid.RowCount-stRow to Grid.RowCount-1 do
begin
Grid.Rows[r].Clear;
end;
Grid.RowCount := Grid.RowCount-stRow;
end;

For the visible part there exist the VisibleRowCount and VisibleColCount properties. The TGridAxisDrawInfo record type names the visible part Boundary and all parts together Extent (or vice versa, I never remember). So there is no specific by the VCL declared name for the unvisible part of a string grid. It just is the unvisible part.
I think you are making a logical error: the buttons are not moved when you scroll the grid. Though it may seem like they move, that is just the result of moving the device context contents due to an internal call to ScrollWindow. The scroll bars in the string grid component are custom added, and do not work like those of e.g. a TScrollBox.
To always show all buttons on the locations where they really are, repaint the string grid in the OnTopLeftChanged event:
procedure TForm1.StringGrid1TopLeftChanged(Sender: TObject);
begin
StringGrid1.Repaint;
end;
When the row heights of all rows and the height of string grid never change, then it is sufficient to create all buttons only once, and let them stay where they are. This means that every button no longer is "attached" to a row, and storing them in the Objects property has no significance any more. When a button is pressed, simply calculate the intended row index from the position of the button in combination with the TopRow property of the string grid which specifies the index of the first visible scrollable row in the grid.
If the grid can resize, e.g. by anchors, then update the button count in the parent's OnResize event. And if the row count of the string grid may become less then the maximum visible row count, then also update the (visible) button count.
If you want more of an answer, then please update your question and explain how the MoveRowPlus and the MoveRowMinus routines are called due to interaction with the grid and or buttons, because now I do not fully understand what it is that you want.
And about CellRect giving the wrong coordinates: that is because CellRect only works on full (or partial) visible cells. To quote the documentation:
If the indicated cell is not visible, CellRect returns an empty rectangle.
Addition due to OP's comments
I think the following code does what you want. The original row index of every button is stored in the Tag property.
unit Unit1;
interface
uses
Windows, Classes, Controls, Forms, StdCtrls, Grids;
type
TForm1 = class(TForm)
Grid: TStringGrid;
procedure GridTopLeftChanged(Sender: TObject);
procedure FormCreate(Sender: TObject);
private
FPrevTopRow: Integer;
procedure CreateGridButtons(ACol: Integer);
procedure GridButtonClick(Sender: TObject);
procedure RearrangeGridButtons;
function GetInsertRowCount(ARow: Integer): Integer;
function GridButtonToRow(AButton: TButton): Integer;
procedure MoveGridButtons(ButtonIndex, ARowCount: Integer);
end;
implementation
{$R *.dfm}
type
TStringGridAccess = class(TStringGrid);
procedure TForm1.FormCreate(Sender: TObject);
begin
FPrevTopRow := Grid.TopRow;
CreateGridButtons(2);
end;
procedure TForm1.CreateGridButtons(ACol: Integer);
var
R: TRect;
I: Integer;
Button: TButton;
begin
R := Grid.CellRect(ACol, Grid.FixedRows);
Inc(R.Right, Grid.GridLineWidth);
Inc(R.Bottom, Grid.GridLineWidth);
for I := Grid.FixedRows to Grid.RowCount - 1 do
begin
Button := TButton.Create(Grid);
Button.BoundsRect := R;
Button.Caption := '+';
Button.Tag := I;
Button.ControlStyle := [csClickEvents];
Button.OnClick := GridButtonClick;
Button.Parent := Grid;
Grid.Objects[0, I] := Button;
OffsetRect(R, 0, Grid.DefaultRowHeight + Grid.GridLineWidth);
end;
end;
procedure TForm1.GridButtonClick(Sender: TObject);
var
Button: TButton absolute Sender;
N: Integer;
I: Integer;
begin
N := GetInsertRowCount(Button.Tag);
if Button.Caption = '+' then
begin
Button.Caption := '-';
Grid.RowCount := Grid.RowCount + N;
for I := 1 to N do
TStringGridAccess(Grid).MoveRow(Grid.RowCount - 1,
GridButtonToRow(Button) + 1);
MoveGridButtons(Button.Tag, N);
end
else
begin
Button.Caption := '+';
for I := 1 to N do
TStringGridAccess(Grid).MoveRow(GridButtonToRow(Button) + 1,
Grid.RowCount - 1);
Grid.RowCount := Grid.RowCount - N;
MoveGridButtons(Button.Tag, -N);
end;
end;
procedure TForm1.GridTopLeftChanged(Sender: TObject);
begin
RearrangeGridButtons;
FPrevTopRow := Grid.TopRow;
end;
procedure TForm1.RearrangeGridButtons;
var
I: Integer;
Shift: Integer;
begin
Shift := (Grid.TopRow - FPrevTopRow) *
(Grid.DefaultRowHeight + Grid.GridLineWidth);
for I := 0 to Grid.ControlCount - 1 do
begin
Grid.Controls[I].Top := Grid.Controls[I].Top - Shift;
Grid.Controls[I].Visible := Grid.Controls[I].Top > 0;
end;
end;
function TForm1.GetInsertRowCount(ARow: Integer): Integer;
begin
//This function should return the number of rows which is to be inserted
//below ARow. Note that ARow refers to the original row index, that is:
//without account for already inserted rows. For now, assume three rows:
Result := 3;
end;
function TForm1.GridButtonToRow(AButton: TButton): Integer;
begin
for Result := 0 to Grid.RowCount - 1 do
if Grid.Objects[0, Result] = AButton then
Exit;
Result := -1;
end;
procedure TForm1.MoveGridButtons(ButtonIndex, ARowCount: Integer);
var
I: Integer;
begin
for I := 0 to Grid.ControlCount - 1 do
if Grid.Controls[I].Tag > ButtonIndex then
Grid.Controls[I].Top := Grid.Controls[I].Top +
ARowCount * (Grid.DefaultRowHeight + Grid.GridLineWidth);
end;
end.
But may I say that this is also possible without the use of button controls: I suggest drawing fake button controls in the string grid's OnDrawCell event.

Related

How to get text from found component?

I have a problem with Text inside of a found TEdit.
This is my code:
function TfrmGenerateExam.zlicz_liczby(Component: TControl): integer;
var
i, j: integer;
begin
Result := 0;
for i := 0 to Component.ComponentCount - 1 do
begin
for j := 0 to Panel.ComponentCount - 1 do
begin
if Components[j] is TEdit then
begin
Result := Result + ???;
end;
end;
end;
end;
In a nutshell:
I create dynamic panels with ComboBoxes, Edits, Buttons etc.
When I have some panels, I want to count the edits which are in panels, which are in ScrollBox:
What do I need to put here?
if Components[j] is TEdit then
begin
Result := Result + ???;
end;
The code provided does not match the screenshot shown. What is being passed as the Component parameter to zlicz_liczby()? Is it the Form itself? The ScrollBox? A specified Panel?
Let's just iterate the Panels in the ScrollBox directly. Try something more like this:
function TfrmGenerateExam.zlicz_liczby: Integer;
var
i, j: integer;
Panel: TPanel;
begin
Result := 0;
for i := 0 to ScrollBox1.ControlCount - 1 do
begin
Panel := ScrollBox1.ControlCount[i] as TPanel;
for j := 0 to Panel.ControlCount - 1 do
begin
if Panel.Controls[j] is TEdit then
Result := Result + StrToIntDef(TEdit(Panel.Controls[j]).Text, 0);
end;
end;
end;
That being said, as #AndreasRejbrand stated in comments, you should use an array instead. When you create a new TPanel with a TEdit on it, put its TEdit into a TList<TEdit>, for instance. If you destroy the TPanel, remove its TEdit from the list. And then you can simply loop through that list whenever needed, without having to hunt for the TEdit controls at all. For example:
private
Edits: TList<TEdit>;
procedure TfrmGenerateExam.FormCreate(Sender: TObject);
begin
Edits := TList<TEdit>.Create;
end;
procedure TfrmGenerateExam.FormDestroy(Sender: TObject);
begin
Edits.Free;
end;
function TfrmGenerateExam.FillScrollBox;
var
Panel: TPanel;
Edit: TEdit;
begin
...
Panel := TPanel.Create(Self);
Panel.Parent := ScrollBox1;
...
Edit := TEdit.Create(Panel);
Edit.Parent := Panel;
...
Edits.Add(Edit);
...
end;
function TfrmGenerateExam.zlicz_liczby: Integer;
var
i: integer;
begin
Result := 0;
for i := 0 to Edits.Count - 1 do
Result := Result + StrToInt(Edits[i].Text);
end;

Color Listbox.Item[N] where N is generated by code

I have a Listbox. I populate it with a file using this:
IF Opendialog1.Execute then
BEGIN
Listbox1.Items.LoadfromFile(OpenDialog1.FileName);
END;
The file loaded contains numbers, and numbers only (I assume).
To be 100 pct. sure, I now starts a scan: (pseudocode :)
for N := 0 til Listbox1.Items.Count -1 DO
BEGIN
NUM := ScanForNotNumberInListbox1Item(Listbox1.Items[N]);
//
// returns NUM = -1 if non digit is met..
//
IF NUM <> 0 then
begin
LISTBOX1.Items[N].BackGroundColor := RED;
Exit; (* or terminate *)
END;
END;
I know I have to use LIstbox1.DrawItem (); and have tried several af the examples shown here in Stack Exchange, but none of the used examples seems to be code-generated.
So how Can I do that ?
Kris
Introduction
You can store additional information about each list item in its associated "object". This can be a (pointer to a) real object, or you can use this pointer-sized integer to encode any simple information you want.
As a simple example, let's put the item's background colour in this field (uses Math):
procedure TForm1.Button1Click(Sender: TObject);
var
i: Integer;
begin
ListBox1.Items.BeginUpdate;
try
ListBox1.Clear;
for i := 1 to 100 do
ListBox1.Items.AddObject(i.ToString, TObject(IfThen(Odd(i), clSkyBlue, clMoneyGreen)));
finally
ListBox1.Items.EndUpdate;
end;
end;
procedure TForm1.ListBox1DrawItem(Control: TWinControl; Index: Integer;
Rect: TRect; State: TOwnerDrawState);
var
ListBox: TListBox;
Canvas: TCanvas;
S: string;
begin
ListBox := Control as TListBox;
Canvas := ListBox.Canvas;
Canvas.Brush.Color := TColor(ListBox.Items.Objects[Index]);
Canvas.FillRect(Rect);
S := ListBox.Items[Index];
Canvas.TextRect(Rect, S, [tfSingleLine, tfVerticalCenter]);
end;
Don't forget to set the list box's Style property to lbOwnerDrawFixed (say).
A more "advanced" approach would be to associate an actual object with each item:
type
TItemFormat = class
BackgroundColor: TColor;
TextColor: TColor;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
i: Integer;
ItemFormat: TItemFormat;
begin
ListBox1.Items.BeginUpdate;
try
ListBox1.Clear;
for i := 1 to 100 do
begin
ItemFormat := TItemFormat.Create;
ItemFormat.BackgroundColor := IfThen(Odd(i), clSkyBlue, clMoneyGreen);
ItemFormat.TextColor := IfThen(Odd(i), clNavy, clGreen);
ListBox1.Items.AddObject(i.ToString, ItemFormat);
end;
finally
ListBox1.Items.EndUpdate;
end;
end;
procedure TForm1.ListBox1DrawItem(Control: TWinControl; Index: Integer;
Rect: TRect; State: TOwnerDrawState);
var
ListBox: TListBox;
Canvas: TCanvas;
ItemFormat: TItemFormat;
S: string;
begin
ListBox := Control as TListBox;
Canvas := ListBox.Canvas;
ItemFormat := ListBox.Items.Objects[Index] as TItemFormat;
Canvas.Brush.Color := ItemFormat.BackgroundColor;
Canvas.FillRect(Rect);
S := ListBox.Items[Index];
Canvas.Font.Color := ItemFormat.TextColor;
Canvas.TextRect(Rect, S, [tfSingleLine, tfVerticalCenter]);
end;
(In this case, you own the objects, so you are responsible for freeing them when they are no longer needed.)
Putting everything in action
In your particular case, I'd try something like
procedure TForm1.Button1Click(Sender: TObject);
var
i, dummy, FirstInvalidIndex: Integer;
begin
with TOpenDialog.Create(Self) do
try
Filter := 'Text files (*.txt)|*.txt';
Options := [ofPathMustExist, ofFileMustExist];
if Execute then
ListBox1.Items.LoadFromFile(FileName);
finally
Free;
end;
FirstInvalidIndex := -1;
ListBox1.Items.BeginUpdate;
try
for i := 0 to ListBox1.Count - 1 do
if not TryStrToInt(ListBox1.Items[i], dummy) then
begin
ListBox1.Items.Objects[i] := TObject(1);
if FirstInvalidIndex = -1 then
FirstInvalidIndex := i;
end;
finally
ListBox1.Items.EndUpdate;
end;
if FirstInvalidIndex <> -1 then
begin
ListBox1.ItemIndex := FirstInvalidIndex;
MessageBox(Handle, 'An invalid row was found.', PChar(Caption), MB_ICONERROR);
end;
end;
procedure TForm1.ListBox1DrawItem(Control: TWinControl; Index: Integer;
Rect: TRect; State: TOwnerDrawState);
var
ListBox: TListBox;
Canvas: TCanvas;
S: string;
begin
ListBox := Control as TListBox;
Canvas := ListBox.Canvas;
Canvas.Font.Assign(ListBox.Font);
if odSelected in State then
begin
Canvas.Brush.Color := clHighlight;
Canvas.Font.Color := clHighlightText;
end
else
begin
Canvas.Brush.Color := clWindow;
Canvas.Font.Color := clWindowText;
end;
if ListBox.Items.Objects[Index] = TObject(1) then
begin
Canvas.Font.Color := clRed;
Canvas.Font.Style := [fsBold, fsStrikeOut]
end;
Canvas.FillRect(Rect);
S := ListBox.Items[Index];
Canvas.TextRect(Rect, S, [tfSingleLine, tfVerticalCenter]);
end;
The fine print: Notice that the above snippets are only simple examples intended to demonstrate the basic approach. In a real application, you need to be more careful about the details. For instance, you cannot use a hard-coded red text colour if the background colour is a system colour (because that colour might very well be red too!).
In addition, what happens if the text file is empty (try it!)?
Set lbOwnerDrawFixed (or another ownerdraw) style for Listbox
Listbox items have auxiliary property Objects[] and you can set Objects[i] to non-nil value for invalid items
IF NUM <> 0 then
LISTBOX1.Objects[N] := TObject(1);
Use some example for OnDrawItem event treatment and use Objects[] to define background color during drawing

Creating my own ListControl, some problems in Delphi

Some time ago I have decided to create my own ListControl. What is mean under ListControl - is a control that similar to standard TListBox in Delphi.
I know, it is 'reinventing a wheel', but I want to finish my control.
So, I implemented not so much features in that control like TListBox has, but my control allows:
Add items;
Select item;
Navigate through items via keyboard (arrow keys Up an Down).
I plan to implement my ScrollBar, but this is another topic.
But I have a problem: when summary height of items is more than control's height and last item selected and I try to increase control's height I got a 'blank space', but I want to 'scroll' items down to fill blank space.
At the picture above you can see that control has lack of items to draw them onto 'blank space'.
May be I explain my problem not so clear, but do next:
Put standard TListBox on form and set its height equal 100 px;
Put standard TrackBar on form, set Max value to 100 and in event OnChange write this:
ListBox1.Height := ListBox1.Height + TrackBar1.Position;
Add 12 items at this Listbox;
Compile project and select last item in Listbox, then begin to change its height via TrackBar. You will see, that 'invisible top items' are come from top to down one by one.
That effect I want to add in my control, but I have no idea why.
Control's code
unit aListBox;
interface
uses
Windows,
Messages,
SysUtils,
Classes,
Graphics,
Controls,
Forms,
StdCtrls,
ExtCtrls,
StrUtils,
Dialogs,
Math;
type
{ main class }
TaListBox = class;
{>>>>>>>>>>>>>>>>>>>>>>>>>}
TaListBox = class(TCustomControl)
private
{ Private declarations }
protected
{ Protected declarations }
FItemBmp: TBitmap;
FEnabled: Boolean;
FSelected: Boolean;
FItems: TStringList;
FItemHeight: Integer;
FCurrentItemIndex: Integer;
FMode: Integer;
FGlobalY: Integer;
FScrollOffset: Integer;
FDownScroll: Integer;
procedure SetItems(value: TStringList);
procedure WMSIZE(var Message: TWMSize); message WM_SIZE;
procedure WMGETDLGCODE(var Message: TWMGETDLGCODE); message WM_GETDLGCODE;
procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); override;
procedure KeyDown(var Key: Word; Shift: TShiftState); override;
function GetItemIndex: Integer;
function GetVisibleItemsCount: Integer;
function GetScrollItemIndex: Integer;
procedure PaintItemStandard(BmpInOut: TBitmap; AMode, AIndex: Integer);
procedure PaintControlStandard(ACanvas: TCanvas; AMode: Integer);
procedure Paint; override;
public
{ Public declarations }
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure Click; override;
property ItemIndex : Integer read FCurrentItemIndex;
published
{ Published declarations }
property Items : TStringList read FItems write FItems;
property OnClick;
end;
{<<<<<<<<<<<<<<<<<<<<<<<<<}
implementation
{ TaListBox }
procedure Register;
begin
RegisterComponents('MyControl', [TaListBox]);
end;
constructor TaListBox.Create(AOwner: TComponent);
begin
Inherited Create(AOwner);
{ standard declarations }
ControlStyle := ControlStyle + [csOpaque, csCaptureMouse, csDoubleClicks];
Width := 100;
Height := 120;
DoubleBuffered := true;
{ control's declarations }
FItemBmp := TBitmap.Create;
FEnabled := true;
FSelected := false;
FItems := TStringList.Create;
FItemHeight := 20;
FCurrentItemIndex := -1;
FScrollOffset := 0;
FDownScroll := 0;
FMode := 1;
end;
destructor TaListBox.Destroy;
begin
FreeAndNil(FItemBmp);
FreeAndNil(FItems);
Inherited Destroy;
end;
procedure TaListBox.Click;
begin
if FEnabled then
Inherited Click
else
Exit;
end;
procedure TaListBox.SetItems(value: TStringList);
begin
Invalidate;
end;
procedure TaListBox.WMSize(var Message: TWMSize);
var
LScrollIndex, LVisibleCount: Integer;
begin
inherited;
LScrollIndex := FScrollOffset div FItemHeight;
LVisibleCount := GetVisibleItemsCount;
if (FItems.Count - LScrollIndex) < LVisibleCount then
FScrollOffset := FItemHeight * max(0, FItems.Count - GetVisibleItemsCount);
end;
procedure TaListBox.WMGETDLGCODE(var Message: TWMGETDLGCODE);
begin
Inherited;
Message.Result := DLGC_WANTARROWS;
end;
procedure TaListBox.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
if Button = mbLeft then
begin
Windows.SetFocus(Handle);
if PtInRect(Rect(1, 1, Width - 1, Height - 1), Point(X, Y)) then
FGlobalY := Y - 2;
if GetItemIndex > FItems.Count - 1 then
Exit
else
begin
FSelected := true;
FCurrentItemIndex := GetItemIndex;
// prevent selecting next item if height too low
if Height >= FItemHeight then
if PtInRect(Rect(1, Height - FDownScroll - 1, Width - 1, Height - 1), Point(X, Y)) then
FScrollOffset := FScrollOffset + FItemHeight;
Invalidate;
end;
end;
Inherited MouseDown(Button, Shift, X, Y);
end;
procedure TaListBox.KeyDown(var Key: Word; Shift: TShiftState);
var
ScrollIndex: Integer;
begin
Inherited KeyDown(Key, Shift);
if FEnabled then
begin
case Key of
VK_UP:
begin
if FCurrentItemIndex = 0 then
Exit
else
begin
if (FCurrentItemIndex + 1) > 0 then
begin
Dec(FCurrentItemIndex);
ScrollIndex := FScrollOffset div FItemHeight;
if FCurrentItemIndex < ScrollIndex then
FScrollOffset := FScrollOffset - FItemHeight;
end;
end;
end;
VK_DOWN:
begin
if FCurrentItemIndex = FItems.Count - 1 then
Exit
else
begin
if (FCurrentItemIndex + 1) < FItems.Count then
begin
Inc(FCurrentItemIndex);
ScrollIndex := FScrollOffset div FItemHeight;
if (FCurrentItemIndex - GetVisibleItemsCount + 1) > ScrollIndex then
FScrollOffset := FScrollOffset + FItemHeight;
end;
end;
end;
end;
Invalidate;
end
else
Exit;
end;
function TaListBox.GetItemIndex: Integer;
begin
Result := (FGlobalY + FScrollOffset) div FItemHeight;
end;
function TaListBox.GetVisibleItemsCount: Integer;
begin
Result := Height div FItemHeight;
end;
function TaListBox.GetScrollItemIndex: Integer;
begin
Result := FScrollOffset div FItemHeight;
end;
procedure TaListBox.PaintItemStandard(BmpInOut: TBitmap; AMode, AIndex: Integer);
var
Text: String;
R: TRect;
begin
BmpInOut.Width := Width - 2;
BmpInOut.Height := FItemHeight;
case AMode of
1:
begin
if FSelected then
begin
BmpInOut.Canvas.Brush.Color := clWebCrimson;
BmpInOut.Canvas.Font.Color := clWhite;
end
else
begin
BmpInOut.Canvas.Brush.Color := clWhite;
BmpInOut.Canvas.Font.Color := clBlack;
end;
BmpInOut.Canvas.Pen.Color := clGray;
end;
4:
begin
BmpInOut.Canvas.Brush.Color := clSilver;
BmpInOut.Canvas.Pen.Color := clGray;
BmpInOut.Canvas.Font.Color := clBlack;
end;
end;
BmpInOut.Canvas.FillRect(BmpInOut.Canvas.ClipRect);
// paint item's text
if AIndex = - 1 then
Exit
else
BmpInOut.Canvas.TextOut(18, 2, FItems.Strings[AIndex]);
end;
procedure TaListBox.PaintControlStandard(ACanvas: TCanvas; AMode: Integer);
var
i: Integer;
OldSelected: Boolean;
TempBmp: TBitmap;
begin
case AMode of
1:
begin
ACanvas.Brush.Color := clWhite;
ACanvas.Pen.Color := clBlack;
end;
4:
begin
ACanvas.Brush.Color := clSilver;
ACanvas.Pen.Color := clBlack;
end;
end;
ACanvas.Rectangle(Rect(0, 0, Width, Height));
// calculate DownButton size
FDownScroll := Height - GetVisibleItemsCount * FItemHeight - 1 {top border pixel} - 1 {bottom border pixel};
// create output bitmap
TempBmp := TBitmap.Create;
TempBmp.Width := Width - 2;
TempBmp.Height := Height - 2;
// turn off selected flag
OldSelected := FSelected;
FSelected := false;
for i:=0 to FItems.Count - 1 do
begin
PaintItemStandard(FItemBmp, FMode, i);
TempBmp.Canvas.Draw(0, 0 + (FItemHeight * i) - FScrollOffset, FItemBmp);
end;
// output result
ACanvas.Draw(1, 1, TempBmp);
// restore selected flag
FSelected := OldSelected;
if FSelected then
begin
// paint selected item
PaintItemStandard(FItemBmp, FMode, FCurrentItemIndex);
ACanvas.Draw(1, 1 + (FItemHeight * FCurrentItemIndex) - FScrollOffset, FItemBmp);
end;
// free resources
FreeAndNil(TempBmp);
end;
procedure TaListBox.Paint;
begin
if FEnabled then
PaintControlStandard(Canvas, 1)
else
PaintControlStandard(Canvas, 4);
end;
end.
I hope I can find some help here.
Thank you for your attention!
P.S.
In the source code was added implementation of scrolling items by changing control's size, written by Tom Brunberg.
P.S.S.
Thanks to user fantaghirocco for formatting my question ;)
Following your directions to create a standard TListBox I noted, as you said, that the number of visible items increased when increasing the list box (regardless of any item being selected).
But, decreasing the size did not scroll items up again, regardless of any item being selected. I understand that you ask about the same functionality, since you refer to the standard TListBox.
Add to the uses clause and to the TaListBox class declaration:
uses ... Math;
...
TaListBox = class(TCustomControl)
private
procedure WMSize(var Message: TWMSize); message WM_SIZE;
and to the implementation
procedure TaListBox.WMSize(var Message: TWMSize);
var
LScrollIndex, LVisibleCount: Integer;
begin
inherited;
LScrollIndex := FScrollOffset div FItemHeight;
LVisibleCount := GetVisibleItemsCount;
if (FItems.Count - LScrollIndex) < LVisibleCount then
FScrollOffset := FItemHeight * max(0, FItems.Count - GetVisibleItemsCount);
end;
A side note: you use the following kind of expressions in many places, e.g.
Round(FScrollOffset div FItemHeight);
The div operator means integer division. It always returns an integer, thus the call to Round is meaningless. Read about div and also mod in the documentation.
The idea is simple:
Always know how many items can be displayed when your control is a certain height. That means if your clientheight is 100px and an item's height is 10px then you obviously will be able to display 10 items completely whitout anyone being clipped. Save that amount in a variable. Keep as a float because sometimes an item will be clipped. (Visible Count)
Keep a variable of which direction you scrolled last. This is important as that will help you decide whether to bring items into view from the bottom or from the top or whether to hide items at the top or bottom when the control's height decreases/increases.
Keep an index of the item that is at the top or bottom the last time you scrolled. Whether to keep the top one or the bottom one will depend on which direction you last scrolled (point 2). It will obviously change as you add items, etc.
So let's say the situation is that you have more items than can be displayed and the last time you scrolled was up, so you will keep the item index of the top most visible item. If that index is 0 (zero) then obviously you just need to bring items into view from the bottom. But if that index is for example; 5, then you will keep bringing items into view also from the bottom but only until the Visible Count grows as large or larger than the Item Count in which case you wil start to bring as many items into view from the top as is needed to fill the client area.
You just have to adapt according to last scroll direction and whether the height increases or decreases

DBGRID with Row Height variable

I would like to show in a DBGRID as follows:
Imagine "Grid" as follows:
ID - DESCRIPTION
1 - Line 1 of the grid
2 - Line 2 of the grid
3 - Line 3 of the grid
Now, suppose the size of the DESCRIPTION column is changed and no longer appear the words "GRID";
I would like to stay as well DBGRID
ID - DESCRIPTION
1 - Line 1 of the
grid
2 - Line 2 of the
grid
3 - Line 3 of the
grid
is there any possibility that ??
Not what you're asking, but might help... I once used this code to show complete Memo fields in the standard DBGrid:
TMyForm = class(TForm)
...
private
FormMemoRect: TRect;
MemoGrid: TDBGrid;
BMemo: TBitBtn;
...
Procedure TMyForm.FormMemoDeactivate(Sender: TObject);
Begin
(Sender As TForm).Close;
Sender.Free;
End;
Procedure TMyForm.BMemoClick(Sender: TObject);
Var FormMemo: TForm;
Begin
MemoGrid.SetFocus;
FormMemo := TForm.Create(Self);
With TMemo.Create(FormMemo) Do Begin
Parent := FormMemo;
Align := alClient;
ReadOnly := True;
WordWrap := True;
ScrollBars := ssVertical;
Lines.Text := MemoGrid.DataSource.DataSet.Fields[TComponent(Sender).Tag].AsString;
End;
With FormMemo Do Begin
OnDeactivate := FormMemoDeactivate;
Left := FormMemoRect.Left;
Top := FormMemoRect.Top;
Width := Max(FormMemoRect.Right - FormMemoRect.Left, 300);
Height := FormMemoRect.Bottom - FormMemoRect.Top;
BorderStyle := bsNone;
Show;
End;
End;
Procedure TMyForm.GrdMemoDrawColumnCell(Sender: TObject; Const Rect: TRect; DataCol: Integer; Column: TColumn; State: TGridDrawState);
Begin
If (gdFocused In State) Then Begin
If Column.Field.DataType In [ftBlob, ftMemo] Then Begin
{Desenha botão para visualização do Memo}
FormMemoRect.Left := TWinControl(Sender).ClientToScreen(Rect.TopLeft).X;
FormMemoRect.Right := TWinControl(Sender).ClientToScreen(Rect.BottomRight).X;
FormMemoRect.Top := TWinControl(Sender).ClientToScreen(Rect.BottomRight).Y;
FormMemoRect.Bottom := FormMemoRect.Top + 100;
If Not Assigned(BMemo) Then
BMemo := TBitBtn.Create(Self);
BMemo.Parent := TWinControl(Sender).Parent;
BMemo.Width := 16;
BMemo.Height := 16;
BMemo.Caption := '...';
BMemo.OnClick := BMemoClick;
BMemo.Tag := Column.Field.Index;
BMemo.Left := TWinControl(Sender).Left + Rect.Right - BMemo.Width + 1;
BMemo.Top := TWinControl(Sender).Top + Rect.Top + 2;
MemoGrid := TDBGrid(Sender);
End
Else
FreeAndNil(BMemo);
End;
End;
For Blob/Memo Fields, you may also find it useful to do some custom GetText to show something directly in the Grid:
Procedure TMyForm.DataSetMemoGetText(Sender: TField; var Text: String; DisplayText: Boolean);
Begin
Text := Copy(Sender.AsString, 1, 50);
If Text <> Sender.AsString Then
Text := Text + '...';
End;
This is how the result looks like.
PS: Sorry for non-standard code style.

TListview is not correctly painted when OnDrawItem event is used

I'm using the OnDrawItem event in the TlistView component to draw the content using custom colors, but when scroll the listview some artifacts appears.
This is the code used.
procedure TForm35.FormCreate(Sender: TObject);
var
i, j : integer;
Item : TListItem;
s : string;
begin
for i:= 0 to 99 do
begin
Item:=ListView1.Items.Add;
for j:= 0 to ListView1.Columns.Count-1 do
begin
s:= Format('Row %d Column %d',[i+1, j+1]);
if j=0 then
Item.Caption :=s
else
Item.SubItems.Add(s);
end;
end;
end;
procedure TForm35.ListView1DrawItem(Sender: TCustomListView; Item: TListItem;
Rect: TRect; State: TOwnerDrawState);
var
x, y, i: integer;
begin
if odSelected in State then
begin
TListView(Sender).Canvas.Brush.Color := clYellow;
TListView(Sender).Canvas.Font.Color := clBlack;
end
else
begin
TListView(Sender).Canvas.Brush.Color := clLtGray;
TListView(Sender).Canvas.Font.Color := clGreen;
end;
TListView(Sender).Canvas.FillRect(Rect);
x := 5;
y := (Rect.Bottom - Rect.Top - TListView(Sender).Canvas.TextHeight('Hg')) div 2 + Rect.Top;
TListView(Sender).Canvas.TextOut(x, y, Item.Caption);
for i := 0 to Item.SubItems.Count - 1 do
begin
inc(x, TListView(Sender).Columns[i].Width);
TListView(Sender).Canvas.TextOut(x, y, Item.SubItems[i]);
end;
end;
I tested this code in Delphi 2007 and XE3, but I'm getting the same results. How i can prevent this issue?
Ok. Change X := 5 to X := Rect.Left;
And another solution (may be more accuracy):
uses
Graphics;
//... Form or something else declarations ...
implementation
procedure TForm35.ListView1DrawItem(Sender: TCustomListView; Item: TListItem; Rect: TRect; State: TOwnerDrawState);
var
s: string;
ts: TTextStyle; // Text style (used for drawing)
begin
// inherited;
// Clear target rectangle
// Set Canvas'es Font, Pen, Brush according for Item state
// Get into s variable text value of the Cell.
ts.Alignment := taLeftJustify; // Horz left alignment
ts.Layout := tlCenter; // Vert center alignment
ts.EndEllipsis := True; // End ellipsis (...) If line of text is too long too fit between left and right boundaries
// Other fields see in the Graphics.TTextStyle = packed record
ListView1.Canvas.TextRect(
Rect,
Rect.Left, // Not sure, but there are a small chance of this values equal to zero instead of Rect...
Rect.Top,
s,
ts)
end;
end.
And to prevent some flicking...
...
var
b: TBitmap;
begin
b := TBitmap.Create;
try
b.Widht := Rect.Right - Rect.Left;
b.Height := Rect.Bottom - Rect.Top;
//...
// Draw content on the b.Canvas
//...
ListView.Canvas.Draw(Rect.Left, Rect.Top, b);
finally
b.Free;
end;
end;

Resources