I have dataset with 2 different sources - some are for goverment and others are for organization only. I need to distinct them somehow, like making some items bold or different color.
I tried to use DrawItem event, but couldn't figure it out.
For adding items I used:
while not (cdDataset1.Eof) do
begin
if ((cdDataset1.fieldbyName('displayName').value<> '') and (cdDataset1.fieldbyName('TyypId').value=1280781)) then
begin
cxDBCheckListBox1.Items.Add.Text:= cdDataset1.fieldbyName('displayName').value;
end;
cdDataset1.Next;
end;
cdDataset1.First;
while not (cdDataset1.Eof) do
begin
if ((cdDataset1.fieldbyName('displayName').value<> '') and (cdDataset1.fieldbyName('TyypId').value=1243501)) then
begin
cxDBCheckListBox1.Items.Add.Text:= cdDataset1.fieldbyName('displayName').value;
end;
cdDataset1.Next;
end;
This part works well. But can I use field TyypID for distinction on cxdbchecklistbox ?
It should look like this(checkboxs intead of bullets ofcourse):
Important option1
Impotant option2
extra info option1
I found the solution. First add tags . OnDrawItem creating canvas is needed:
//adding items to checklistbox where needed.
cxDBCheckListBox1.Items.Clear;
while not (cdDataset1.Eof) do
begin
if ((cdDataset1.fieldbyName('displayName').value<> '') and
(cdDataset1.fieldbyName('TyypId').value=28078)) then
begin
with cxDBCheckListBox1.Items.Add do
begin
Text:= cdDataset1.fieldbyName('displayName').value;
Tag := 1280781;
end;
end;
cdDataset1.Next;
end;
//Then on drawitem:
procedure TfmSample.cxDBCheckListBox1DrawItem(
Control: TWinControl; Index: Integer; Rect: TRect; State: TOwnerDrawState);
var
ACanvas: TcxCanvas;
AText: string;
ATextRect: TRect;
AGlyphWidth: Integer;
AListBox: TcxDBCheckListBox;
ACanvasFont: TFont;
AItemEnabled: Boolean;
AItemTag: Integer;
begin
AListBox := (Control as TcxDBCheckListBox);
ACanvas := AListBox.InnerCheckListBox.Canvas;
ACanvasFont := ACanvas.Font;
AItemTag := AListBox.Items[Index].Tag;
AItemEnabled := AListBox.Items[Index].Enabled;
case AItemTag of
1243501:
begin
ACanvasFont.Color := clBlue;
end;
1280781 :
begin
ACanvasFont.Style := [fsBold];
ACanvasFont.Color := clBlack;
end;
end;
ACanvas.Brush.Color := clWhite;
ACanvas.FillRect(Rect);
AText := AListBox.Items[Index].Text;
ATextRect := Rect;
ATextRect.Left := 20;
ACanvas.DrawTexT(AText, ATextRect, 0);
end;
Related
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
In a Delphi 10.1.2 VCL Forms Application, I fill a TComboBox with the Screen.Fonts list and display the font items using their own font-faces:
procedure TForm2.FormCreate(Sender: TObject);
begin
AddFontsToComboList;
end;
procedure TForm2.AddFontsToComboList;
var
i: Integer;
begin
ComboBox1.Items.BeginUpdate;
try
for i := 0 to Screen.Fonts.Count - 1 do
begin
ComboBox1.Items.Add(Screen.Fonts[i]);
end;
finally
ComboBox1.Items.EndUpdate;
end;
end;
procedure TForm2.ComboBox1DrawItem(Control: TWinControl; Index: Integer; Rect: TRect; State: TOwnerDrawState);
// ComboBox1.Style must be csOwnerDrawVariable
begin
with ComboBox1 do
begin
Canvas.fillrect(rect);
Canvas.Font.Style := [fsbold];
Canvas.Font.Name := ComboBox1.Items[Index];
Canvas.Textout(rect.Left, rect.Top, ComboBox1.Items[Index]);
end;
end;
This is the result:
You can see that there are some missing items. In the above screenshot, the missing font is Cambria Math.
So how can I filter out those empty items?
And how can I filter out those items which are unreadable?
And how can I filter out the fonts which contain only symbols?
You know what I mean.
I'd like to add DBLookupComboboxes to certain columns in a DBGrid. There is a nice article on About.com on how to do this here. The problem is that with a table having many columns, if you select from the DBLookupCombobox in one column and then try to scroll left, the combobox will move left too as shown in the included images. How can the About.com code can be changed to prevent this behavior? A web search showed two others complaining of the exact same problem with no solution. Note that I want to use a DBLookupCombobox to show a name but enter the id, so using a simple picklist will not do.
procedure TForm1.DBGrid1ColExit(Sender: TObject);
begin
if DBGrid1.SelectedField.FieldName = DBLookupComboBox1.DataField then
DBLookupComboBox1.Visible := False
end;
procedure TForm1.DBGrid1DrawColumnCell(Sender: TObject; const Rect: TRect;
DataCol: Integer; Column: TColumn; State: TGridDrawState);
begin
if (gdFocused in State) then
begin
if (Column.Field.FieldName = DBLookupComboBox1.DataField) then
with DBLookupComboBox1 do
begin
Left := Rect.Left + DBGrid1.Left + 2;
Top := Rect.Top + DBGrid1.Top + 2;
Width := Rect.Right - Rect.Left;
Width := Rect.Right - Rect.Left;
Height := Rect.Bottom - Rect.Top;
Visible := True;
end;
end
end;
procedure TForm1.DBGrid1KeyPress(Sender: TObject; var Key: Char);
begin
if (key = Chr(9)) then Exit;
if (DBGrid1.SelectedField.FieldName = DBLookupComboBox1.DataField) then
begin
DBLookupComboBox1.SetFocus;
SendMessage(DBLookupComboBox1.Handle, WM_Char, word(Key), 0);
end
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
with DBLookupComboBox1 do
begin
DataSource := DataSource1; // -> AdoTable1 -> DBGrid1
ListSource := DataSource2;
DataField := 'resource_id'; // from AdoTable1 - displayed in the DBGrid
KeyField := 'id';
ListField := 'resource_name; id';
Visible := False;
end;
DataSource2.DataSet := AdoQuery1;
AdoQuery1.Connection := AdoConnection1;
AdoQuery1.SQL.Text := 'SELECT id,resource_name FROM resources';
AdoQuery1.Open;
end;
Here is one solution using a neat hack from François.
type
// Hack to redeclare your TDBGrid here without the the form designer going mad
TDBGrid = class(DBGrids.TDBGrid)
procedure WMHScroll(var Msg: TWMHScroll); message WM_HSCROLL;
end;
TForm1 = class(TForm)
[...]
procedure TDBGrid.WMHScroll(var Msg: TWMHScroll);
begin
if Form1.DBGrid1.SelectedField.FieldName = Form1.DBLookupComboBox1.DataField then begin
case Msg.ScrollCode of
SB_LEFT,SB_LINELEFT,SB_PAGELEFT: begin
Form1.DBGrid1.SelectedIndex := Form1.DBGrid1.SelectedIndex-1;
Form1.DBLookupComboBox1.Visible := False;
end;
SB_RIGHT,SB_LINERIGHT,SB_PAGERIGHT: begin
Form1.DBGrid1.SelectedIndex := Form1.DBGrid1.SelectedIndex+1;
Form1.DBLookupComboBox1.Visible := False;
end;
end;
end;
inherited; // to keep the expected behavior
end;
Overview
I am trying to write my own simple property inspector but I am facing a difficult and rather confusing problem. First though let me say that my component is not meant to work with or handle component properties, instead it will allow adding custom values to it. The full source code of my component is further down the question and it should look something like this once you have installed it in a package and run it from a new empty project:
Problem (brief)
The issue is regarding the use of inplace editors and validating the property values. The idea is, if a property value is not valid then show a message to the user notifying them that the value cannot be accepted, then focus back to the row and inplace editor that was originally focused on.
We can actually use Delphi's very own Object Inspector to illustrate the behavior I am looking for, for example try writing a string in the Name property that cannot be accepted then click away from the Object Inspector. A message is shown and upon closing it, it will focus back to the Name row.
Source Code
The question becomes too vague without any code but due to the nature of the component I am trying to write it's also quite large. I have stripped it down as much as possible for the purpose of the question and example. I am sure there will be some comments asking me why I didn't do this or do that instead but it's important to know that I am no Delphi expert and often I make wrong decisions and choices but I am always willing to learn so all comments are welcomed, especially if it aids in finding my solution.
unit MyInspector;
interface
uses
Winapi.Windows,
Winapi.Messages,
System.Classes,
System.SysUtils,
Vcl.Controls,
Vcl.Dialogs,
Vcl.StdCtrls,
Vcl.Graphics,
Vcl.Forms;
type
TMyInspectorItems = class(TObject)
private
FPropertyNames: TStringList;
FPropertyValues: TStringList;
procedure AddItem(APropName, APropValue: string);
procedure Clear;
public
constructor Create;
destructor Destroy; override;
end;
TOnMouseMoveEvent = procedure(Sender: TObject; X, Y: Integer) of object;
TOnSelectRowEvent = procedure(Sender: TObject; PropName, PropValue: string; RowIndex: Integer) of object;
TMyCustomInspector = class(TGraphicControl)
private
FInspectorItems: TMyInspectorItems;
FOnMouseMove: TOnMouseMoveEvent;
FOnSelectRow: TOnSelectRowEvent;
FRowCount: Integer;
FNamesFont: TFont;
FValuesFont: TFont;
FSelectedRow: Integer;
procedure SetNamesFont(const AValue: TFont);
procedure SetValuesFont(const AValue: TFont);
procedure CalculateInspectorHeight;
function GetMousePosition: TPoint;
function MousePositionToRowIndex: Integer;
function RowIndexToMousePosition(ARowIndex: Integer): Integer;
function GetRowHeight: Integer;
function GetValueRowWidth: Integer;
function RowExists(ARowIndex: Integer): Boolean;
function IsRowSelected: Boolean;
protected
procedure Loaded; override;
procedure Paint; override;
procedure WMKeyDown(var Message: TMessage); message WM_KEYDOWN;
procedure WMMouseDown(var Message: TMessage); message WM_LBUTTONDOWN;
procedure WMMouseMove(var Message: TMessage); message WM_MOUSEMOVE;
procedure WMMouseUp(var Message: TMessage); message WM_LBUTTONUP;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
function RowCount: Integer;
property Items: TMyInspectorItems read FInspectorItems write FInspectorItems;
property OnMouseMove: TOnMouseMoveEvent read FOnMouseMove write FOnMouseMove;
property OnSelectRow: TOnSelectRowEvent read FOnSelectRow write FOnSelectRow;
published
property Align;
end;
TMyPropertyInspector = class(TScrollBox)
private
FInspector: TMyCustomInspector;
FInplaceStringEditor: TEdit;
FSelectedRowName: string;
FLastSelectedRowName: string;
FLastSelectedRow: Integer;
function SetPropertyValue(RevertToPreviousValueOnFail: Boolean): Boolean;
procedure InplaceStringEditorEnter(Sender: TObject);
procedure InplaceStringEditorExit(Sender: TObject);
procedure InplaceStringEditorKeyPress(Sender: TObject; var Key: Char);
procedure SelectRow(Sender: TObject; PropName, PropValue: string; RowIndex: Integer);
function ValidateStringValue(Value: string): Boolean;
protected
procedure Loaded; override;
procedure WMSize(var Message: TMessage); message WM_SIZE;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure AddItem(APropName, APropValue: string);
function GetSelectedPropertyName: string;
function GetSelectedPropertyValue: string;
function RowCount: Integer;
end;
var
FCanSelect: Boolean;
implementation
{ TMyInspectorItems }
constructor TMyInspectorItems.Create;
begin
inherited Create;
FPropertyNames := TStringList.Create;
FPropertyValues := TStringList.Create;
end;
destructor TMyInspectorItems.Destroy;
begin
FPropertyNames.Free;
FPropertyValues.Free;
inherited Destroy;
end;
procedure TMyInspectorItems.AddItem(APropName, APropValue: string);
begin
FPropertyNames.Add(APropName);
FPropertyValues.Add(APropValue);
end;
procedure TMyInspectorItems.Clear;
begin
FPropertyNames.Clear;
FPropertyValues.Clear;
end;
{ TMyCustomInspector }
constructor TMyCustomInspector.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FInspectorItems := TMyInspectorItems.Create;
FNamesFont := TFont.Create;
FNamesFont.Color := clWindowText;
FNamesFont.Name := 'Segoe UI';
FNamesFont.Size := 9;
FNamesFont.Style := [];
FValuesFont := TFont.Create;
FValuesFont.Color := clNavy;
FValuesFont.Name := 'Segoe UI';
FValuesFont.Size := 9;
FValuesFont.Style := [];
end;
destructor TMyCustomInspector.Destroy;
begin
FInspectorItems.Free;
FNamesFont.Free;
FValuesFont.Free;
inherited Destroy;
end;
procedure TMyCustomInspector.Loaded;
begin
inherited Loaded;
end;
procedure TMyCustomInspector.Paint;
procedure DrawBackground;
begin
Canvas.Brush.Color := clWindow;
Canvas.Brush.Style := bsSolid;
Canvas.FillRect(Rect(0, 0, Self.Width, Self.Height));
end;
procedure DrawNamesBackground;
begin
Canvas.Brush.Color := clWindow;
Canvas.Brush.Style := bsSolid;
Canvas.FillRect(Rect(0, 0, Self.Width div 2, Self.Height));
end;
procedure DrawNamesSelection;
begin
if (FRowCount > -1) and (RowExists(MousePositionToRowIndex)) then
begin
Canvas.Brush.Color := $00E0E0E0;
Canvas.Brush.Style := bsSolid;
Canvas.FillRect(Rect(0, RowIndexToMousePosition(FSelectedRow),
Self.Width div 2, RowIndexToMousePosition(FSelectedRow) + GetRowHeight));
end;
end;
procedure DrawNamesText;
var
I: Integer;
Y: Integer;
begin
FRowCount := FInspectorItems.FPropertyNames.Count;
Canvas.Brush.Style := bsClear;
Canvas.Font.Color := FNamesFont.Color;
Canvas.Font.Name := FNamesFont.Name;
Canvas.Font.Size := FNamesFont.Size;
Y := 0;
for I := 0 to FInspectorItems.FPropertyNames.Count -1 do
begin
Canvas.TextOut(2, Y, FInspectorItems.FPropertyNames.Strings[I]);
Inc(Y, GetRowHeight);
end;
end;
procedure DrawValuesBackground;
begin
Canvas.Brush.Color := clWindow;
Canvas.Brush.Style := bsSolid;
Canvas.FillRect(Rect(Self.Width div 2, 0, Self.Width, Self.Height));
end;
procedure DrawValuesSelection;
begin
if (FRowCount > -1) and (RowExists(MousePositionToRowIndex)) then
begin
Canvas.DrawFocusRect(Rect(Self.Width div 2, RowIndexToMousePosition(FSelectedRow),
Self.Width, RowIndexToMousePosition(FSelectedRow) + GetRowHeight));
end;
end;
procedure DrawValues;
var
I, Y: Integer;
begin
FRowCount := FInspectorItems.FPropertyValues.Count;
Y := 0;
for I := 0 to FInspectorItems.FPropertyValues.Count -1 do
begin
Canvas.Brush.Style := bsClear;
Canvas.Font.Color := FValuesFont.Color;
Canvas.Font.Name := FValuesFont.Name;
Canvas.Font.Size := FValuesFont.Size;
Canvas.TextOut(Self.Width div 2 + 2, Y + 1, FInspectorItems.FPropertyValues.Strings[I]);
Inc(Y, GetRowHeight);
end;
end;
begin
DrawNamesBackground;
DrawNamesSelection;
DrawNamesText;
DrawValuesBackground;
DrawValuesSelection;
DrawValues;
end;
procedure TMyCustomInspector.WMKeyDown(var Message: TMessage);
begin
inherited;
case Message.WParam of
VK_DOWN:
begin
end;
end;
end;
procedure TMyCustomInspector.WMMouseDown(var Message: TMessage);
begin
inherited;
Parent.SetFocus;
FSelectedRow := MousePositionToRowIndex;
if FSelectedRow <> -1 then
begin
if Assigned(FOnSelectRow) then
begin
FOnSelectRow(Self, FInspectorItems.FPropertyNames.Strings[FSelectedRow],
FInspectorItems.FPropertyValues.Strings[FSelectedRow], FSelectedRow);
end;
end;
Invalidate;
end;
procedure TMyCustomInspector.WMMouseMove(var Message: TMessage);
begin
inherited;
if Assigned(FOnMouseMove) then
begin
FOnMouseMove(Self, GetMousePosition.X, GetMousePosition.Y);
end;
end;
procedure TMyCustomInspector.WMMouseUp(var Message: TMessage);
begin
inherited;
end;
procedure TMyCustomInspector.SetNamesFont(const AValue: TFont);
begin
FNamesFont.Assign(AValue);
Invalidate;
end;
procedure TMyCustomInspector.SetValuesFont(const AValue: TFont);
begin
FValuesFont.Assign(AValue);
Invalidate;
end;
procedure TMyCustomInspector.CalculateInspectorHeight;
var
I, Y: Integer;
begin
FRowCount := FInspectorItems.FPropertyNames.Count;
Y := GetRowHeight;
for I := 0 to FRowCount -1 do
begin
Inc(Y, GetRowHeight);
end;
if Self.Height <> Y then
Self.Height := Y;
end;
function TMyCustomInspector.GetMousePosition: TPoint;
var
Pt: TPoint;
begin
Pt := Mouse.CursorPos;
Pt := ScreenToClient(Pt);
Result := Pt;
end;
function TMyCustomInspector.MousePositionToRowIndex: Integer;
begin
Result := GetMousePosition.Y div GetRowHeight;
end;
function TMyCustomInspector.RowIndexToMousePosition(
ARowIndex: Integer): Integer;
begin
Result := ARowIndex * GetRowHeight;
end;
function TMyCustomInspector.GetRowHeight: Integer;
begin
Result := FNamesFont.Size * 2 + 1;
end;
function TMyCustomInspector.GetValueRowWidth: Integer;
begin
Result := Self.Width div 2;
end;
function TMyCustomInspector.RowCount: Integer;
begin
Result := FRowCount;
end;
function TMyCustomInspector.RowExists(ARowIndex: Integer): Boolean;
begin
Result := MousePositionToRowIndex < RowCount;
end;
function TMyCustomInspector.IsRowSelected: Boolean;
begin
Result := FSelectedRow <> -1;
end;
{ TMyPropertyInspector }
constructor TMyPropertyInspector.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
Self.DoubleBuffered := True;
Self.Height := 150;
Self.HorzScrollBar.Visible := False;
Self.TabStop := True; // needed to receive focus
Self.Width := 250;
FInspector := TMyCustomInspector.Create(Self);
FInspector.Parent := Self;
FInspector.Align := alTop;
FInspector.Height := 0;
FInspector.OnSelectRow := SelectRow;
FInplaceStringEditor := TEdit.Create(Self);
FInplaceStringEditor.Parent := Self;
FInplaceStringEditor.BorderStyle := bsNone;
FInplaceStringEditor.Color := clWindow;
FInplaceStringEditor.Height := 0;
FInplaceStringEditor.Left := 0;
FInplaceStringEditor.Name := 'MyPropInspectorInplaceStringEditor';
FInplaceStringEditor.Top := 0;
FInplaceStringEditor.Visible := False;
FInplaceStringEditor.Width := 0;
FInplaceStringEditor.Font.Assign(FInspector.FValuesFont);
FInplaceStringEditor.OnEnter := InplaceStringEditorEnter;
FInplaceStringEditor.OnExit := InplaceStringEditorExit;
FInplaceStringEditor.OnKeyPress := InplaceStringEditorKeyPress;
FCanSelect := True;
end;
destructor TMyPropertyInspector.Destroy;
begin
FInspector.Free;
FInplaceStringEditor.Free;
inherited Destroy;
end;
procedure TMyPropertyInspector.Loaded;
begin
inherited Loaded;
end;
procedure TMyPropertyInspector.WMSize(var Message: TMessage);
begin
FInspector.Width := Self.Width;
Invalidate;
end;
procedure TMyPropertyInspector.AddItem(APropName, APropValue: string);
begin
FInspector.CalculateInspectorHeight;
FInspector.Items.AddItem(APropName, APropValue);
FInspector.Invalidate;
Self.Invalidate;
end;
function TMyPropertyInspector.GetSelectedPropertyName: string;
begin
Result := '';
if FInspector.FSelectedRow <> -1 then
begin
Result := FInspector.FInspectorItems.FPropertyNames.Strings[FInspector.FSelectedRow];
end;
end;
function TMyPropertyInspector.GetSelectedPropertyValue: string;
begin
Result := '';
if FInspector.FSelectedRow <> -1 then
begin
Result := FInspector.FInspectorItems.FPropertyValues.Strings[FInspector.FSelectedRow];
end;
end;
function TMyPropertyInspector.RowCount: Integer;
begin
Result := FInspector.RowCount;
end;
procedure TMyPropertyInspector.InplaceStringEditorEnter(Sender: TObject);
begin
FCanSelect := False;
FLastSelectedRow := FInplaceStringEditor.Tag;
end;
procedure TMyPropertyInspector.InplaceStringEditorExit(Sender: TObject);
begin
if SetPropertyValue(True) then
begin
FCanSelect := True;
end;
end;
procedure TMyPropertyInspector.InplaceStringEditorKeyPress(Sender: TObject;
var Key: Char);
begin
if Key = Chr(VK_RETURN) then
begin
Key := #0;
FInplaceStringEditor.SelectAll;
end;
end;
procedure TMyPropertyInspector.SelectRow(Sender: TObject; PropName, PropValue: string; RowIndex: Integer);
begin
FSelectedRowName := PropName;
FLastSelectedRowName := PropName;
FInplaceStringEditor.Height := FInspector.GetRowHeight - 2;
FInplaceStringEditor.Left := Self.Width div 2;
FInplaceStringEditor.Tag := RowIndex;
FInplaceStringEditor.Text := GetSelectedPropertyValue;
FInplaceStringEditor.Top := FInspector.RowIndexToMousePosition(FInspector.FSelectedRow) + 1 - Self.VertScrollBar.Position;
FInplaceStringEditor.Visible := True;
FInplaceStringEditor.Width := FInspector.GetValueRowWidth - 3;
FInplaceStringEditor.SetFocus;
FInplaceStringEditor.SelectAll;
end;
function TMyPropertyInspector.SetPropertyValue(
RevertToPreviousValueOnFail: Boolean): Boolean;
var
S: string;
begin
Result := False;
S := FInplaceStringEditor.Text;
if ValidateStringValue(S) then
begin
Result := True;
end
else
begin
ShowMessage('"' + S + '"' + 'is not a valid value.');
Result := False;
end;
end;
function TMyPropertyInspector.ValidateStringValue(Value: string): Boolean;
begin
// a quick and dirty way of testing for a valid string value, here we just
// look for strings that are not zero length.
Result := Length(Value) > 0;
end;
end.
Problem (detailed)
The confusion I have all comes down to who receives focus first and how to handle and respond to it correctly. Because I am custom drawing my rows I determine where the mouse is when clicking on the inspector control and then I draw the selected row to show this. When handling the inplace editors however, especially the OnEnter and OnExit event I have been facing all kinds of funky problems where in some cases I have been stuck in a cycle of the validate error message repeatedly showing for example (because focus is switching from my inspector to the inplace editor and back and forth).
To populate my inspector at runtime you can do the following:
procedure TForm1.Button1Click(Sender: TObject);
begin
MyPropertyInspector1.AddItem('A', 'Some Text');
MyPropertyInspector1.AddItem('B', 'Hello World');
MyPropertyInspector1.AddItem('C', 'Blah Blah');
MyPropertyInspector1.AddItem('D', 'The Sky is Blue');
MyPropertyInspector1.AddItem('E', 'Another String');
end;
A little something you may try:
Click on a row
Delete the contents from the inplace editor
Select another row
The validate error message box appears (don't close it yet)
With the message box still visible, move your mouse over another row
Now press Enter to close the message box
You will notice the selected row has now moved to where the mouse was
What I need is after the validate message box has shown and closed, I need to set the focus back to the row that was been validated in the first place. It gets confusing because it seems (or so I think) that the inplace editors OnExit is been called after the WMMouseDown(var Message: TMessage); code of my inspector.
To put it as simple as I can if the question remains unclear, the behavior of the Delphi Object Inspector is what I am trying to implement into my component. You enter a value into the inplace editors, if it fails the validation then display a messagebox and then focus back to the row that was last selected. The inplace editor validation should occur as soon as focus is switched away from the inplace editor.
I just can't seem to figure out what is been called first and what is blocking events been fired, it becomes confusing because the way I draw my selected row is determined by where the mouse was when clicking on the inspector control.
This is your flow of events:
TMyCustomInspector.WMMouseDown is called
Therein, Parent.SetFocus is called
The focus is removed from the Edit control and TMyPropertyInspector.InplaceStringEditorExit is called
The message dialog is shown by SetPropertyValue
FSelectedRow is being reset
TMyPropertyInspector.SelectRow is called (via TMyCustomInspector.FOnSelectRow) which resets the focus to the replaced Edit control.
What you need to is to prevent FSelectedRow being reset in case of validation did not succeed. All needed ingredients are already there, just add this one condition:
if FCanSelect then
FSelectedRow := MousePositionToRowIndex;
A few remarks:
Make FCanSelect a protected or private field of TMyCustomInspector,
You need to check for limits in TMyCustomInspector.MousePositionToRowIndex in order to return -1.
Your problem is very interesting. From what I gather, you want a way to reset the focus to the invalid row, when the evaluation is false. Where I see you do this is in your SetPropertyValue function. I believe if you do the following, you will be able to reset the focus after the user clicks "OK" in the message:
function TMyPropertyInspector.SetPropertyValue(
RevertToPreviousValueOnFail: Boolean): Boolean;
var
S: string;
begin
Result := False;
S := FInplaceStringEditor.Text;
if ValidateStringValue(S) then
begin
Result := True;
end
else
begin
if (MessageDlg('"' + S + '"' + 'is not a valid value.', mtError, [mbOK], 0)) = mrOK then
begin
SelectRow(nil, FSelectedRowName, FInplaceStringEditor.Text, FInplaceStringEditor.Tag);
end;
Result := False;
end;
end;
Changing the ShowMessage to MessageDlg will allow for an action to occur when the button is pressed. Then calling your SelectRow function with (what I believe are) global variables representing the information about the last row will set that focus to the bad cell.
How can I have TComboBox with some items that are disabled? I need the user to see these items, but not be able to select them.
Thanks!
Yes, and this is how to do it:
Drop a TComboBox on your form, and set Style to csOwnerDrawFixed. Then add the event handlers
procedure TForm1.ComboBox1DrawItem(Control: TWinControl; Index: Integer;
Rect: TRect; State: TOwnerDrawState);
const
INDENT = 3;
begin
with TComboBox(Control) do
begin
FillRect(Canvas.Handle, Rect, GetStockObject(WHITE_BRUSH));
inc(Rect.Left, INDENT);
if boolean(Items.Objects[Index]) then
SetTextColor(Canvas.Handle, clBlack)
else
SetTextColor(Canvas.Handle, clGray);
DrawText(Canvas.Handle,
PChar(Items[Index]),
length(Items[Index]),
Rect,
DT_SINGLELINE or DT_LEFT or DT_VCENTER or DT_END_ELLIPSIS)
end;
end;
and
procedure TForm1.ComboBox1CloseUp(Sender: TObject);
begin
with TComboBox(Sender) do
if (ItemIndex <> -1) and not boolean(Items.Objects[ItemIndex]) then
begin
beep;
Perform(CB_SHOWDROPDOWN, integer(true), 0);
end;
end;
Also, in the interface section of your form, prior to the declaration of the form class, add
TComboBox = class(StdCtrls.TComboBox)
protected
procedure WndProc(var Message: TMessage); override;
end;
and implement the WndProc as
procedure TComboBox.WndProc(var Message: TMessage);
function NextItemIsDisabled: boolean;
begin
result := (ItemIndex < Items.Count - 1) and
not boolean(Items.Objects[ItemIndex + 1]);
end;
procedure SelectNextEnabledItem;
var
i: Integer;
begin
for i := ItemIndex + 1 to Items.Count - 1 do
if boolean(Items.Objects[i]) then
begin
ItemIndex := i;
Exit;
end;
beep;
end;
procedure KillMessages;
var
msg: TMsg;
begin
while PeekMessage(msg,
Handle,
WM_KEYFIRST,
WM_KEYLAST,
PM_REMOVE) do;
end;
function PrevItemIsDisabled: boolean;
begin
result := (ItemIndex > 0) and
not boolean(Items.Objects[ItemIndex - 1]);
end;
procedure SelectPrevEnabledItem;
var
i: Integer;
begin
for i := ItemIndex - 1 downto 0 do
if boolean(Items.Objects[i]) then
begin
ItemIndex := i;
Exit;
end;
beep;
end;
begin
case Message.Msg of
WM_KEYDOWN:
case Message.WParam of
VK_DOWN:
if NextItemIsDisabled then
begin
SelectNextEnabledItem;
KillMessages;
Exit;
end;
VK_UP:
if PrevItemIsDisabled then
begin
SelectPrevEnabledItem;
KillMessages;
Exit;
end;
end;
end;
inherited;
end;
To test the combo box, write, for example
procedure TForm1.FormCreate(Sender: TObject);
begin
ComboBox1.Items.AddObject('Alpha', TObject(true));
ComboBox1.Items.AddObject('Beta', TObject(true));
ComboBox1.Items.AddObject('Gamma', TObject(false));
ComboBox1.Items.AddObject('Delta', TObject(true));
end;
I think you get the meaning of true and false here -- it simply means enabled.
It's not easy (and it's a bad idea, since that's not how comboboxes behave on Windows).
You'd have to owner draw the combobox yourself. Use the Items.Objects array to store whether or not the item is enabled or disabled, and check that array before drawing each item in order to set the colors appropriately.
You'd also need to handle the OnChange and OnClick events, and add a way to track the last selected ItemIndex. In OnChange/OnClick, you disconnect the event handler, check the Objects[ItemIndex] value to see if a selection is allowed, if not set the ItemIndex back to the last selected ItemIndex, and then re-enable the event handler.