Dynamic DBGrid Cell coloring - delphi

I'm using Delphi XE-2 and DBGrid from Jedi component group (TJvDBGrid). Now, I found it to be very easy to define cell color when values are known, for example:
OnGetCellParams event:
if DBGrid.Field.AsInteger = 0
then Background := clYellow;
but in my case, user can define what value will have what color, that is stored on separate table. And my question, is there a way to color cell by looking up if cell value has a color assign to it?
I would appreciate any help or guidance in the matter, thank you.

The easiest way would probably be to use the form's OnCreate to populate an array, and then access that in the OnGetCellParams event. The array should contain as many items as there are possible values, plus a default value for array index 0 in case a color hasn't been assigned. (Untested, off-the-cuff code follows!)
type
TForm1 = class(TForm)
...
procedure FormCreate(Sender: TObject);
private
FColors: array of TColor;
end;
implementation
procedure TForm1.FormCreate(Sender: TObject);
var
NumRows, i: Integer;
begin
// One row for each possible value for the integer column you're
// trying to color the cell for (eg., if the table can hold a value
// from 0-10, you need the same # of items in the array (array[0..10])
NumRows := NumberOfPossibleValues;
SetLength(FColors, NumberOfPossibleValues);
// Pre-fill the array with the default clWindow color,
// in case a custom color isn't assigned to a value
// (for instance, the user doesn't set a color for a value
// of 7).
for i := 0 to High(FColors) do
FColors[i] := clWindow;
// Assumes your color values are in a database called FieldColors,
// in a datamodule called dmAppData, and that there's a
// column named ColValue indicating the `Field.AsInteger`
// value and the corresponding TColor stored as an integer.
dmAppData.FieldColors.First;
while not dmAppData.FieldColors.Eof do
begin
i := dmAppData.FieldColors.FieldByName('ColValue').AsInteger;
// Might want to put a check here to make sure the value isn't
// more than the number of items in the array!!!
FColors[i] := TColor(dmAppData.FieldColors.FieldByName('Color').AsInteger);
dmAppData.FieldColors.Next;
end;
end;
In your OnGetCellParams event:
Background := FColors[DBGrid.Field.AsInteger];
You might want to use a local variable in the OnGetCellParams to make sure you stay within the array bounds:
Background := clWindow;
i := DBGrid.Field.AsInteger;
if (i > 0) and (i < Length(FColors)) then
Background := FColors[i];
The much slower way would be to do a Locate in the OnGetCellParams event for every single row:
In OnGetCellParams:
Background := clWindow;
if dmAppData.FieldColors.Locate('ColValue', DBGrid.Field.AsInteger, []) then
Background := TColor(dmAppData.FieldColors.FieldByName('Color').AsInteger);

Related

How to get the clicked column on TDBGrid.DblClick(Sender: TObject)?

When using the OnDblClick event of a TDBGrid, how can i know what column was double clicked ?
This is easy with the OnCellClick as it has a TColumn parameter, but not on OnDblClick.
During TDBGrid.OnDblClick the dataset is positioned to the clicked record and the column can be retrieved with the TDBGrid.SelectedIndex property. If you are interested in the underlying dataset field, you can directly access it with TDBGrid.SelectedField.
The OnDblClick event doesn't give you any information about the click, in particular where the click was performed, let alone which grid cell was clicked on. So, you will have to determine that information manually.
Try this:
Get the current mouse position within the grid, by passing Mouse.CursorPos to TDBGrid.ScreenToClient()
Then, use TDBGrid.MouseCoord() to determine the row/column indexes of the cell that is underneath the mouse.
Then, check if the cell row/column corresponds to a data cell, and if so then use the TDBGrid.SelectedIndex property to index into the TDBGrid.Columns property.
This is basically the same thing that TDBGrid does internally when firing the OnCellClick event, only it does this in response to a MouseUp event, which provides the mouse coordinates within the grid, thus skipping the 1st step above.
For example:
type
TDBGridAccess = class(TDBGrid)
end;
procedure TMyForm1.DBGrid1DblClick(Sender: TObject);
var
TitleOffset: Byte;
Pt: TPoint;
Cell: TGridCoord;
Column: TColumn;
begin
TitleOffset := Ord(dgTitles in DBGrid1.Options);
Pt := DBGrid1.ScreenToClient(Mouse.CursorPos);
Cell := DBGrid1.MouseCoord(Pt.X, Pt.Y);
if (Cell.X >= TDBGridAccess(DBGrid1).IndicatorOffset) and (Cell.Y >= TitleOffset) then
begin
Column := DBGrid1.Columns[DBGrid1.SelectedIndex];
// use Column as needed...
end;
end;
UPDATE: based on #UweRaabe's comments, you should be able to just use TDBGrid.SelectedIndex by itself:
procedure TMyForm1.DBGrid1DblClick(Sender: TObject);
var
Index: Integer;
Column: TColumn;
begin
Index := DBGrid1.SelectedIndex;
if (Index <> -1) then
begin
Column := DBGrid1.Columns[Index];
// use Column as needed...
end;
end;

TStringGrid hide column

procedure TFormMain.CaseListMyShares(uri: String);
var
i: Integer;
begin
myShares := obAkasShareApiAdapter.UserShares(uri);
MySharesGrid.RowCount:= Length(myShares) +1;
MySharesGrid.AddCheckBoxColumn(0, false);
MySharesGrid.AutoFitColumns;
for i := 0 to Length(myShares) -1 do
begin
MySharesGrid.Cells[1, i+1]:= myShares[i].patientCase;
MySharesGrid.Cells[2, i+1]:= myShares[i].sharedUser;
MySharesGrid.Cells[3, i+1]:= myShares[i].creationDate;
MySharesGrid.Cells[4, i+1]:= statusText[StrToInt(myShares[i].situation) -1];
end;
end;
I want to create an invisible column to myself. I have to know row's identifier to make some operations with REST API. But end-user does not need to see this identifier on table. How can i create an invisible column ?
myShares[i].identifier which i want to hide on every row. Is that possible ? Using TAG or anything ?
To assign the identifier:
MySharesGrid.Objects[0, i+1] := TObject(myShares[i].identifier)
To access the identifier (e.g. OnClick):
Integer(MySharesGrid.Objects[0, MySharesGrid.Row])
To answer the question, assign its ColWidths to -1, thusly:
StringGrid1.ColWidths[4] := -1;
You can show it again by setting that back to a positive value.
This is not about using a StringGrid to store data, but for the User Interface to show/hide columns - like a Collapse speedbutton in the title, with a corresponding Show All button to restore it.

How to change background color of FMX.TGrid row depend on value in XE4

I'd like to know how to change background color of whole row in Firemonkey TGrid/TColumn.
Saw bunch of similar questions, but none of them helped me. I'm using Delphi XE4. TGrid may contain TCheckColumn and TStringColumn.
TGrid row background style color is divided into two categories :
Focus color
Selection color
Focus color applies to the focused Cell. Selection color applies to the selected Row.
Changing focus color is a straight forward process:
procedure ChangeGridCellFocusColor(Grid: FMX.Grid.TGrid; NewColor: TAlphaColor);
var
T: TFmxObject;
begin
T := Grid.FindStyleResource('focus');
if (T <> nil) and (T is TRectangle) then
if TRectangle(T).Fill <> nil then
TRectangle(T).Fill.Color := NewColor;
Grid.Repaint;
end;
You apply it like this :
ChangeGridCellFocusColor(MyGrid1, TAlphaColors.Red);
Note that the Focus rectangle is semi-transparent so whatever color you assign it will mix with the Row Selection color.
It would be reasonable to assume that the Selection color can be changed in the same fashion but that is not the case.
When the Style is Applied the resource marked as selection is cloned, the original value discarded and the new value added to an internal TControlList.
This is why the same principle cannot be applied.
To change the row selection color do the following:
Interface
type
TcustomGridHelper = class helper for FMX.Grid.TCustomGrid
public
function getSelections: TControlList;
end;
{...}
Implementation
function TcustomGridHelper.getSelections: TControlList;
begin
Result := Self.fSelections;
end;
procedure ChangeGridRowSelectionColor(Grid: FMX.Grid.TGrid;
NewColor: TAlphaColor);
var
aList: TControlList;
Control: TControl;
begin
aList := Grid.getSelections;
if (aList <> nil) then
for Control in aList do
TRectangle(Control).Fill.Color := NewColor;
Grid.Repaint;
end;
You apply it like this :
ChangeGridRowSelectionColor(MyGrid1, TalphaColors.Green);

Why don't child controls of a TStringGrid work properly?

I am placing checkboxes (TCheckBox) in a string grid (TStringGrid) in the first column. The checkboxes show fine, positioned correctly, and respond to mouse by glowing when hovering over them. When I click them, however, they do not toggle. They react to the click, and highlight, but finally, the actual Checked property does not change. What makes it more puzzling is I don't have any code changing these values once they're there, nor do I even have an OnClick event assigned to these checkboxes. Also, I'm defaulting these checkboxes to be unchecked, but when displayed, they are checked.
The checkboxes are created along with each record which is added to the list, and is referenced inside a record pointer which is assigned to the object in the cell where the checkbox is to be placed.
String grid hack for cell highlighting:
type
THackStringGrid = class(TStringGrid); //used later...
Record containing checkbox:
PImageLink = ^TImageLink;
TImageLink = record
...other stuff...
Checkbox: TCheckbox;
ShowCheckbox: Bool;
end;
Creation/Destruction of checkbox:
function NewImageLink(const AFilename: String): PImageLink;
begin
Result:= New(PImageLink);
...other stuff...
Result.Checkbox:= TCheckbox.Create(nil);
Result.Checkbox.Caption:= '';
end;
procedure DestroyImageLink(AImageLink: PImageLink);
begin
AImageLink.Checkbox.Free;
Dispose(AImageLink);
end;
Adding rows to grid:
//...after clearing grid...
//L = TStringList of original filenames
if L.Count > 0 then
lstFiles.RowCount:= L.Count + 1
else
lstFiles.RowCount:= 2; //in case there are no records
for X := 0 to L.Count - 1 do begin
S:= L[X];
Link:= NewImageLink(S); //also creates checkbox
Link.Checkbox.Parent:= lstFiles;
Link.Checkbox.Visible:= Link.ShowCheckbox;
Link.Checkbox.Checked:= False;
Link.Checkbox.BringToFront;
lstFiles.Objects[0,X+1]:= Pointer(Link);
lstFiles.Cells[1, X+1]:= S;
end;
Grid's OnDrawCell Event Handler:
procedure TfrmMain.lstFilesDrawCell(Sender: TObject; ACol, ARow: Integer;
Rect: TRect; State: TGridDrawState);
var
Link: PImageLink;
CR: TRect;
begin
if (ARow > 0) and (ACol = 0) then begin
Link:= PImageLink(lstFiles.Objects[0,ARow]); //Get record pointer
CR:= lstFiles.CellRect(0, ARow); //Get cell rect
Link.Checkbox.Width:= Link.Checkbox.Height;
Link.Checkbox.Left:= CR.Left + (CR.Width div 2) - (Link.Checkbox.Width div 2);
Link.Checkbox.Top:= CR.Top;
if not Link.Checkbox.Visible then begin
lstFiles.Canvas.Brush.Color:= lstFiles.Color;
lstFiles.Canvas.Brush.Style:= bsSolid;
lstFiles.Canvas.Pen.Style:= psClear;
lstFiles.Canvas.FillRect(CR);
if lstFiles.Row = ARow then
THackStringGrid(lstFiles).DrawCellHighlight(CR, State, ACol, ARow);
end;
end;
end;
Here's how it looks when clicking...
What could be causing this? It's definitely not changing the Checked property anywhere in my code. There's some strange behavior coming from the checkboxes themselves when placed in a grid.
EDIT
I did a brief test, I placed a regular TCheckBox on the form. Check/unchecks fine. Then, in my form's OnShow event, I changed the Checkbox's Parent to this grid. This time, I get the same behavior, not toggling when clicked. Therefore, it seems that a TCheckBox doesn't react properly when it has another control as its parent. How to overcome this?
TStringGrid's WMCommand handler doesn't allow children controls to handle messages (except for InplaceEdit).
So you can use e.g. an interposed class (based on code by Peter Below) or draw controls by hands, as some people have adviced. Here is the code of the interposed class:
uses
Grids;
type
TStringGrid = class(Grids.TStringGrid)
private
procedure WMCommand(var AMessage: TWMCommand); message WM_COMMAND;
end;
implementation
procedure TStringGrid.WMCommand(var AMessage: TWMCommand);
begin
if EditorMode and (AMessage.Ctl = InplaceEditor.Handle) then
inherited
else
if AMessage.Ctl <> 0 then
begin
AMessage.Result := SendMessage(AMessage.Ctl, CN_COMMAND,
TMessage(AMessage).WParam, TMessage(AMessage).LParam);
end;
end;
In Delphi7 at least I do this:
You need to draw a checkbox on the cell, and keep it in sync with an array of boolean (here fChecked[]) that indicates the state of the checkbox in each row. Then, in the DrawCell part of the TStringGrid:
var
cbstate: integer;
begin
...
if fChecked[Arow] then cbState:=DFCS_CHECKED else cbState:=DFCS_BUTTONCHECK;
DrawFrameControl(StringGrid.canvas.handle, Rect, DFC_BUTTON, cbState);
...
end;
To get the checkbox to respond to the space-bar, use the KeyDown event, and force a repaint:
if (Key = VK_SPACE) And (col=ColWithCheckBox) then begin
fChecked[row]:=not fChecked[row];
StringGrid.Invalidate;
key:=0;
end;
A similar approach is needed for the OnClick method.
Can u use VirtualTreeView in toReportMode (TListView emulating) mode instead of grid ?
Can u use TDBGrid over some in-memory table like NexusDB or TClientDataSet ?
Ugly approach would be presenting checkbox like a letter with a custom font - like WinDings or http://fortawesome.github.com/Font-Awesome
This latter is most easy to implement, yet most ugly to see and most inflexible to maintain - business logic gets intermixed into VCL event handlers

Finding out position of a control inside TGridPanel

How I can find out the position (row and column index) of controls inside TGridPanel? I'd like to use common OnClick event for number of buttons and need to know the X,Y position of the button.
I'm using Delphi 2007.
Unfortunately, because of the magic of TGridPanel, it is a little more complicated than just getting the Top and Left properties...
This should do it for any Control, adapt it to your needs:
procedure GetRowColumn(const AControl: TControl; var ARow, AColumn: Integer);
var
I: Integer;
begin
if AControl.Parent is TGridPanel then
begin
I := TGridPanel(AControl.Parent).ControlCollection.IndexOf(AControl);
if I > -1 then
begin
ARow := TGridPanel(AControl.Parent).ControlCollection[I].Row;
AColumn := TGridPanel(AControl.Parent).ControlCollection[I].Column;
end;
end;
end;
procedure TForm1.ButtonClick(Sender: TObject);
var
Row, Column : Integer;
begin
GetRowColumn(Sender as TControl, Row, Column);
// do something with Row and Column
ShowMessage( Format('row=%d - col=%d',[Row, Column]));
end;
You can use Sender cast as a tButton and then ask it for its top and left for example:
Procedure TForm1.OnClick(Sender:tObject);
var
X,Y : Integer;
begin
if Sender is TButton then
begin
X := TButton(Sender).Top;
Y := TButton(Sender).Left;
// do something with X & Y
end;
end;
Or if your just wanting to know what button was pressed, you can also use the TAG property to insert a number into each button, and then retrieve the tag value in your onclick event. Just remember to first set the Tag property to something. You can do this in the form designer if your just dropping buttons into the grid panel or in the routine your using to create and insert your buttons.
Procedure TForm1.OnClick(Sender:tObject);
var
iButton : integer;
begin
if Sender is TComponent then
begin
iButton := TComponent(Sender).Tag;
// do something with iButton
end;
end;
You can also use the tag property to store more than just an integer, since a pointer currently uses the same memory size as the integer you can cast a pointer to an integer and insert that value into the tag property. Just be aware that any pointer you place in this field is still treated as an integer. You are responsible for the memory it points to, it will not be managed by the component.

Resources