I have a check box which will be enabled/disabled at run time. I just want to show different tool tips if it is enabled/disabled. I was thinking about overriding OnMouseEnter event and handle it there but OnMouseEnter will be called only if the control is enabled. How can i possible achieve that behavior? Any help would be appreciated.
I tried to handle OnMouseMove of the form and do something like this
procedure Tdlg.pnlTopMouseMove(Sender: TObject;Shift: TShiftState; X, Y: Integer);
var
point: TPoint;
checkBoxCursorPos: TPoint;
begin
inherited;
point.X := X;
point.Y := Y;
checkBoxCursorPos := chkBx.ScreenToClient(point);
if (PtInRect(chkBx.ClientRect, checkBoxCursorPos)) then
begin
if(chkBx.Enabled) then
chkBx.Hint := 'Enabled'
else
chkBx.Hint := 'Disabled' ;
Application.ShowHint := True;
end;
end;
but the condition PtinRect is not satisfied. What i am doing wrong?
There is a simple solution: place an empty TLabel over the checkbox and set its Hint to the value for the disabled checkbox state. The label has to be AutoSize off and you can enforce position and size by its BoundsRect property set to that of the CheckBox.
When the CheckBox is enabled the Hint of the Checkbox is used, while the Hint of the Label is used when the CheckBox is disabled.
Update: just saw that Bummi mentions a similar idea in his comment.
The official answer: you can’t.
The workaround: you could try using the form's MouseMove-event (assuming that won’t be disabled, of course), and if the mouse cursor is over the relevant control, display the appropriate hint.
Here is a unit that can show hints for disabled controls.
I used it like this:
TATHintControl.Create(self).HintStyleController := GlobalHintStyleController;
GlobalHintStyleController is a DevExpress stylecontroller.
Then the unit
unit ATHintControl;
{
The purpose of this component is to show hints for disabled controls (VCL doesn't)
It uses timestamp comparison instead of Timers to save resources
}
interface
uses
// VCL
Classes,
Controls,
Forms,
AppEvnts,
Messages,
Windows,
// DevEx
cxHint;
type
TGetHintForControlEvent = function(AControl: TControl): string of object;
THandleControlEvent = function(AControl: TControl): boolean of object;
TATHintControl = class(TComponent)
private
fHintTimeStamp: TDateTime;
fHintHideTimeStamp: TDateTime;
fHintControl: TControl;
fHintVisible: boolean;
FHintStyleController: TcxHintStyleController;
FHintShowDelay: Integer;
FHintHideDelay: Integer;
fGetHintForControlEvent: TGetHintForControlEvent;
fHandleControlEvent: THandleControlEvent;
fApplicationEvents: TApplicationEvents;
procedure IdleHandler(Sender: TObject; var Done: Boolean);
procedure ShortcutHandler(var Msg: TWMKey; var Handled: Boolean);
procedure SetHintStyleController(const Value: TcxHintStyleController);
procedure HideHint;
function GetCursorPos(out APoint: TPoint): Boolean;
function HandleHint: boolean;
protected
function GetHintForControl(AControl: TControl): string; virtual;
function HandleControl(AControl: TControl): boolean; virtual;
public
procedure AfterConstruction; override;
published
property HintStyleController: TcxHintStyleController read FHintStyleController write SetHintStyleController;
property OnGetHintForControl: TGetHintForControlEvent read fGetHintForControlEvent write fGetHintForControlEvent;
property OnHandleControl: THandleControlEvent read fHandleControlEvent write fHandleControlEvent;
end;
implementation
uses
Types,
SysUtils,
DateUtils;
const
cHintShowDelay: Integer = 500; // msec
cHintHideDelay: Integer = 3 * 1000; // 3 sec
{ TATHintControl }
procedure TATHintControl.AfterConstruction;
begin
inherited;
fApplicationEvents := TApplicationEvents.Create(self);
fApplicationEvents.OnIdle := IdleHandler;
fApplicationEvents.OnShortCut := ShortcutHandler;
fHintShowDelay := cHintShowDelay;
fHintHideDelay := cHintHideDelay;
end;
function TATHintControl.GetCursorPos(out APoint: TPoint): Boolean;
begin
{$WARN SYMBOL_PLATFORM OFF}
result := Windows.GetCursorPos(APoint);
{$WARN SYMBOL_PLATFORM ON}
end;
function TATHintControl.GetHintForControl(AControl: TControl): string;
begin
if Assigned(OnGetHintForControl) then
result := OnGetHintForControl(AControl)
else
result := AControl.Hint;
end;
procedure TATHintControl.HideHint;
begin
HintStyleController.HideHint;
fHintTimeStamp := 0;
fHintVisible := false;
fHintHideTimeStamp := 0;
end;
procedure TATHintControl.IdleHandler(Sender: TObject; var Done: Boolean);
begin
if Assigned(HintStyleController) then
Done := HandleHint;
end;
procedure TATHintControl.SetHintStyleController(
const Value: TcxHintStyleController);
begin
FHintStyleController := Value;
end;
procedure TATHintControl.ShortcutHandler(var Msg: TWMKey; var Handled: Boolean);
begin
fHintControl := nil; // clear the HintControl so that keypress causes it to be shown again w/o having to move the mouse
end;
function TATHintControl.HandleControl(AControl: TControl): boolean;
begin
if Assigned(OnHandleControl) then
result := OnHandleControl(AControl)
else
result := not AControl.Enabled;
end;
function TATHintControl.HandleHint: boolean;
var
vNow: TDateTime;
vScreenPos: TPoint;
vClientPos: TPoint;
vControl: TControl;
vHintString: string;
vForm: TForm;
vWinControl: TWinControl;
begin
result := (fHintTimeStamp = 0);
vForm := Screen.ActiveForm;
if not Assigned(vForm) then
exit;
if not boolean(GetCursorPos(vScreenPos)) then
exit;
vNow := Now;
vControl := nil;
vWinControl := vForm as TWinControl;
while Assigned(vWinControl) do
try
vClientPos := vWinControl.ScreenToClient(vScreenPos);
vControl := vWinControl.ControlAtPos(vClientPos, true, true, true);
if not Assigned(vControl) then
begin
vControl := vWinControl;
break;
end
else
if vControl is TWinControl then
vWinControl := vControl as TWinControl
else
vWinControl := nil;
except
exit; // in some cases ControlAtPos can fail with EOleError: Could not obtain OLE control window handle.
end;
if (fHintControl <> vControl) then
begin
if fHintVisible then
HideHint;
if Assigned(vControl) and HandleControl(vControl) then
begin
fHintControl := vControl;
fHintTimeStamp := vNow; // starts timer for hint to show
end
else
begin
fHintTimeStamp := 0;
fHintControl := nil;
end;
end
else
begin
if fHintVisible and (vNow > fHintHideTimeStamp) then
begin
HideHint;
end
else // we check HandleControl again here to make sure we still want to show the hint
if not fHintVisible and Assigned(vControl) and HandleControl(vControl) and (fHintTimeStamp > 0) and (vNow > IncMillisecond(fHintTimeStamp, fHintShowDelay)) then
begin
vHintString := GetHintForControl(vControl);
if vHintString = '' then
exit;
HintStyleController.ShowHint(vScreenPos.X + 0, vScreenPos.Y + 18, '', vHintString);
fHintTimeStamp := vNow;
fHintControl := vControl;
fHintVisible := true;
// base hide delay + dynamic part based on length of the hint string, 500 msec per 30 characters
fHintHideTimeStamp := vNow + IncMillisecond(0, fHintHideDelay) + ((Length(vHintString) div 20) * EncodeTime(0,0,0,500));
end
end;
result := (fHintTimeStamp = 0);
end;
end.
Related
I want capture the url of active window based in a substring and add to Memo only if sActiveURL is different of sOldURL.
The trouble in my code is that always is added to Memo the same url ignoring the verification if sActiveURL <> sOldURL.
How fix this?
Main:
type
TForm1 = class(TForm)
tmr1: TTimer;
mmo1: TMemo;
procedure tmr1Timer(Sender: TObject);
private
{ Private declarations }
sActiveURL,sOldURL : string;
public
{ Public declarations }
end;
var
Form1: TForm1;
Flag: Boolean;
implementation
uses
UIAutomationClient_TLB, Activex, StrUtils;
{$R *.dfm}
function GetURL(hTargetWnd: HWND): string;
function Enumerar(pParent: IUIAutomationElement; Scope: TreeScope; pCondition: IUIAutomationCondition): String;
var
found : IUIAutomationElementArray;
ALen : Integer;
i : Integer;
iElement : IUIAutomationElement;
retorno: integer;
value : WideString;
iInter: IInterface;
ValPattern : IUIAutomationValuePattern;
begin
Result := '';
Flag := false;
if pParent = nil then
Exit;
pParent.FindAll(Scope, pCondition, found);
found.Get_Length(ALen);
for i := 1 to ALen - 1 do
begin
found.GetElement(i, iElement);
iElement.Get_CurrentControlType(retorno);
if (
(retorno = UIA_EditControlTypeId) or
(retorno = UIA_GroupControlTypeId)
) then
begin
iElement.GetCurrentPattern(UIA_ValuePatternId, iInter);
if Assigned(iInter) then
begin
if iInter.QueryInterface(IID_IUIAutomationValuePattern, ValPattern) = S_OK then
begin
ValPattern.Get_CurrentValue(value);
Result := trim(value);
Flag := true;
Break;
end;
end;
end;
if not Flag then
begin
Result := Enumerar(iElement, Scope, pCondition);
end;
end;
end;
var
UIAuto : IUIAutomation;
Ret : Integer;
RootElement : IUIAutomationElement;
Scope : TreeScope;
varProp : OleVariant;
pCondition : IUIAutomationCondition;
begin
Result := '';
try
UIAuto := CoCUIAutomation.Create;
if Succeeded(UIAuto.ElementFromHandle(hTargetWnd, RootElement)) then
begin
TVariantArg(varProp).vt := VT_BOOL;
TVariantArg(varProp).vbool := True;
UIAuto.CreatePropertyCondition(UIA_IsControlElementPropertyId,
varProp,
pCondition);
Scope := TreeScope_Element or TreeScope_Children;
Result := Enumerar(RootElement, Scope, pCondition);
end;
except
Result := '';
end;
end;
procedure TForm1.tmr1Timer(Sender: TObject);
begin
sActiveURL := GetURL(GetForegroundWindow);
if sActiveURL <> sOldURL then
begin
if AnsiContainsText(sActiveURL, 'stackoverflow.com') then
begin
sOldURL := sActiveURL;
mmo1.Lines.Add('['+sActiveURL+']<'+DateToStr(Date)+'>');
end;
end;
end;
UIAutomationClient_TLB.pas
EDITION:
On debug i discovered that none value is attrib to sOldURL variable.
procedure TForm1.tmr1Timer(Sender: TObject);
var
sActiveURL,sOldURL : string;
begin
sActiveURL := GetURL(GetForegroundWindow);
mmo1.Lines.Add('[sOldURL = '+sOldURL+' ]');
mmo1.Lines.Add('[sActiveURL = '+sActiveURL+' ]');
mmo1.Lines.Add('');
if sActiveURL <> sOldURL then
begin
if AnsiContainsText(sActiveURL, 'stackoverflow.com') then
begin
sOldURL := sActiveURL;
mmo1.Lines.Add(sActiveURL);
mmo1.Lines.Add('');
mmo1.Lines.Add('');
end;
end;
end;
The reason is as I shortly described in comments and is visible when the focused window is not your browser, for example your applications wiindow with the mmo1: TMemo:
GetForegroundWindow() returns the window that has focus.
Your GetURL(GetForegroundWindow) searches for an edit control (UIA_EditControlTypeId), of the focused window, and finds your memo control and returns the content of the memo.
Further, if you change focus to your browser, its URL will be correctly recorded in the memo, and if you return focus to your application, the condition if AnsiContainsText(sActiveURL, 'stackoverflow.com') will be true.
You then write to the memo, adding what you think is a real URL, and then this will be repeated for every timer event.
You need to only check the real browser window (skip all other) for its current URL. Try this, if you are using IE, otherwise you must modify FindWindow():
procedure TForm24.tmr1Timer(Sender: TObject);
var //
hIEWnd: HWND; //
begin
hIEWnd := FindWindow('IEFrame', nil); //
sActiveURL := GetURL(hIEWnd); //
// sActiveURL := GetURL(GetForegroundWindow);
if sActiveURL <> sOldURL then
begin
if AnsiContainsText(sActiveURL, 'stackoverflow.com') then
begin
sOldURL := sActiveURL;
mmo1.Lines.Add('[' + sActiveURL + ']<' + DateToStr(Date) + '>');
end;
end;
end;
Modified lines are marked with //
Every now and then I use the watch window to display strings which contain sql statements.
Now I select Copy Value from the context menu and get
'SELECT NAME FROM SAMPLE_TABLE WHERE FIRST_NAME = ''George'''#$D#$A
Of course, this statement has to be reformatted if I want to execute it in a sql tool displaying the results. This is a little bit annoying.
Is there a trick / workaround for that?
I thought it would be amusing to try and work out a way to do this by adding something inside the IDE, mainly because when you posted your q, I didn't have a clue how to. It turns out that you can do it quite easily using a custom OTA package containing a unit like the one below.
Btw, I'm particularly obliged to Rob Kennedy for pointing out in another SO question that the IDE has a Screen object just like any other. That provides an easy way into the problem, bypassing the maze of OTA interfaces I've usually had to work with to code an IDE add-in.
It works by
Finding the Watch Window,
Finding the Copy Watch value item in its context menu & adding a new menu item after it
Using the OnClick handler of the new item to pick up the value from the Watch Window's focused item, re-formatting it as required, then pasting it to the Clipboard.
So far as using OTA services is concerned, it doesn't do anything fancy, but with the IDE I think the KISS principle applies.
Code:
unit IdeMenuProcessing;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, ExtCtrls, ToolsAPI, Menus, ClipBrd, ComCtrls;
type
TOtaMenuForm = class(TForm)
Memo1: TMemo;
procedure FormCreate(Sender: TObject);
private
OurMenuItem : TMenuItem;
WatchWindow : TForm;
WWListView : TListView;
procedure GetWatchValue(Sender : TObject);
end;
var
OtaMenuForm: TOtaMenuForm;
procedure Register;
implementation
{$R *.dfm}
procedure ShowMenus;
begin
OtaMenuForm := TOtaMenuForm.Create(Nil);
OtaMenuForm.Show;
end;
procedure Register;
begin
ShowMenus;
end;
procedure TOtaMenuForm.FormCreate(Sender: TObject);
var
i : Integer;
S : String;
PM : TPopUpMenu;
Item : TMenuItem;
begin
// First create a menu item to insert in the Watch Window's context menu
OurMenuItem := TMenuItem.Create(Self);
OurMenuItem.OnClick := GetWatchValue;
OurMenuItem.Caption := 'Get processed watch value';
WatchWindow := Nil;
WWListView := Nil;
// Next, iterate the IDE's forms to find the Watch Window
for i := 0 to Screen.FormCount - 1 do begin
S := Screen.Forms[i].Name;
if CompareText(S, 'WatchWindow') = 0 then begin // < Localize if necessary
WatchWindow := Screen.Forms[i];
Break;
end;
end;
Assert(WatchWindow <> Nil);
if WatchWindow <> Nil then begin
// Next, scan the Watch Window's context menu to find the existing "Copy watch value" entry
// and insert our menu iem after it
PM := WatchWindow.PopUpMenu;
for i:= 0 to PM.Items.Count - 1 do begin
Item := PM.Items[i];
if CompareText('Copy Watch &Value', Item.Caption) = 0 then begin // < Localize if necessary
PM.Items.Insert(i + 1, OurMenuItem);
Break;
end;
end;
// Now, find the TListView in the Watch Window
for i := 0 to WatchWindow.ComponentCount - 1 do begin
if WatchWindow.Components[i] is TListView then begin
WWListView := WatchWindow.Components[i] as TListView;
Break;
end;
end;
Assert(WWListView <> Nil);
end;
end;
procedure TOtaMenuForm.GetWatchValue(Sender: TObject);
var
WatchValue : String;
begin
// This is called when the Watch Window menu item we added is clicked
if WWListView.ItemFocused = Nil then begin
Memo1.Lines.Add('no Watch selected');
exit;
end;
WatchValue := WWListView.ItemFocused.SubItems[0];
WatchValue := StringReplace(WatchValue, #$D#$A, ' ', [rfreplaceAll]);
if WatchValue[1] = '''' then
Delete(WatchValue, 1, 1);
if WatchValue[Length(WatchValue)] = '''' then
WatchValue := Copy(WatchValue, 1, Length(WatchValue) - 1);
// [etc]
ClipBoard.AsText := WatchValue;
Memo1.Lines.Add('>' + WatchValue + '<');
end;
initialization
finalization
if Assigned(OTAMenuForm) then begin
OTAMenuForm.Close;
FreeAndNil(OTAMenuForm);
end;
end.
Btw, I wrote this in D7 because I use that as a sort of lowest common denominator for SO answers because its quite obvious that a large number of people here still use it. Later versions have additional string functions, such as the AniDequotedStr mentioned in a comment, which might be helpful in reformatting the watch value.
Update: According to the OP, the above doesn't work with XE3 because the watch window is implemented using a TVirtualStringTree rather than a TListView. The reason I used the ListView was that I found that picking up the Watch value from the Clipboard (after simulating a click on the context menu's Copy Watch Value) to process it wasn't very reliable. That seems to have improved in XE4 (I don't have XE3 to test), so here's a version that seems to work in XE4:
Update #2: The OP mentioned that the previous version of the code below failed the WatchWindow <> Nil assertion when Delphi is first started. I imagine the reason is that the code is called before the Watch Window has been created in the IDE. I've re-arranged the code an added an OTANotifier that's used to get the notification that a project desktop has been loaded, ad uses that to called the new SetUp routine.
unit IdeMenuProcessing;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, ExtCtrls, ToolsAPI, Menus, ClipBrd, ComCtrls;
type
TIdeNotifier = class(TNotifierObject, IOTANotifier, IOTAIDENotifier)
protected
procedure AfterCompile(Succeeded: Boolean);
procedure BeforeCompile(const Project: IOTAProject; var Cancel: Boolean);
procedure FileNotification(NotifyCode: TOTAFileNotification;
const FileName: string; var Cancel: Boolean);
end;
TOtaMenuForm = class(TForm)
Memo1: TMemo;
procedure FormCreate(Sender: TObject);
private
IsSetUp : Boolean;
ExistingMenuItem,
OurMenuItem : TMenuItem;
WatchWindow : TForm;
Services: IOTAServices;
Notifier : TIdeNotifier;
NotifierIndex: Integer;
procedure GetWatchValue(Sender : TObject);
procedure SetUp;
end;
var
OtaMenuForm: TOtaMenuForm;
procedure Register;
implementation
{$R *.dfm}
procedure ShowMenus;
begin
OtaMenuForm := TOtaMenuForm.Create(Nil);
OtaMenuForm.Services := BorlandIDEServices as IOTAServices;
OtaMenuForm.NotifierIndex := OtaMenuForm.Services.AddNotifier(TIdeNotifier.Create);
OtaMenuForm.Show;
end;
procedure Register;
begin
ShowMenus;
end;
procedure TOtaMenuForm.SetUp;
var
i : Integer;
S : String;
PM : TPopUpMenu;
Item : TMenuItem;
begin
if IsSetUp then exit;
// First create a menu item to insert in the Watch Window's context menu
OurMenuItem := TMenuItem.Create(Self);
OurMenuItem.OnClick := GetWatchValue;
OurMenuItem.Caption := 'Get processed watch value';
WatchWindow := Nil;
// Next, iterate the IDE's forms to find the Watch Window
for i := 0 to Screen.FormCount - 1 do begin
S := Screen.Forms[i].Name;
if CompareText(S, 'WatchWindow') = 0 then begin
WatchWindow := Screen.Forms[i];
Break;
end;
end;
Assert(WatchWindow <> Nil);
if WatchWindow <> Nil then begin
// Next, scan the Watch Window's context menu to find the existing "Copy watch value" entry
// and insert our menu item after it
PM := WatchWindow.PopUpMenu;
for i:= 0 to PM.Items.Count - 1 do begin
Item := PM.Items[i];
if CompareText('Copy Watch &Value', Item.Caption) = 0 then begin
ExistingMenuItem := Item;
PM.Items.Insert(i + 1, OurMenuItem);
if ExistingMenuItem.Action <> Nil then
Memo1.Lines.Add('Has action')
else
Memo1.Lines.Add('No action');
Break;
end;
end;
end;
Caption := 'Setup complete';
IsSetUp := True;
end;
procedure TOtaMenuForm.FormCreate(Sender: TObject);
begin
IsSetUp := False;
end;
procedure TOtaMenuForm.GetWatchValue(Sender: TObject);
var
S,
WatchValue : String;
TL : TStringList;
i : Integer;
begin
// This is called when the Watch Window menu item we added is clicked
ExistingMenuItem.Click;
WatchValue := ClipBoard.AsText;
WatchValue := StringReplace(WatchValue, '#$D#$A', #$D#$A, [rfreplaceAll]);
if WatchValue <> '' then begin
TL := TStringList.Create;
try
TL.Text := WatchValue;
WatchValue := '';
for i := 0 to TL.Count - 1 do begin
S := TL[i];
if S[1] = '''' then
Delete(S, 1, 1);
if S[Length(S)] = '''' then
S := Copy(S, 1, Length(S) - 1);
if WatchValue <> '' then
WatchValue := WatchValue + ' ';
WatchValue := WatchValue + S;
end;
finally
TL.Free;
end;
// [etc]
end;
ClipBoard.AsText := WatchValue;
Memo1.Lines.Add('>' + WatchValue + '<');
end;
{ TIdeNotifier }
procedure TIdeNotifier.AfterCompile(Succeeded: Boolean);
begin
end;
procedure TIdeNotifier.BeforeCompile(const Project: IOTAProject;
var Cancel: Boolean);
begin
end;
procedure TIdeNotifier.FileNotification(NotifyCode: TOTAFileNotification;
const FileName: string; var Cancel: Boolean);
begin
if NotifyCode = ofnProjectDesktopLoad then
OTAMenuForm.SetUp
end;
initialization
finalization
if Assigned(OTAMenuForm) then begin
OTAMenuForm.Services.RemoveNotifier(OTAMenuForm.NotifierIndex);
OTAMenuForm.Close;
FreeAndNil(OTAMenuForm);
end;
end.
I'm posting this as a separate answer because it uses a different implementation
based on the ToolsAPI's debugger visualizers. There are examples in the Visualizers
sub-folder of the Delphi source code. The one which looked most promising as a
starting point is the example in the StringListVisualizer.Pas file. However, I found
that impenetrable on the first few readings and it turned out that it didn't actually
do what I was hoping for.
The code below, which of course needs to be compiled into an IDE package which
requires the rtl and designide units, is based upon the much simpler DateTime
sample visualizer, but adapted to the Text property of TStrings objects. This adaptation still required quite a lot of work, and that's the main reason I'm posting this additional answer, to save others some head-scratching.
Normally, the Text property of a TStrings variable is displayed in the Watch Window as one or more text lines surrounded by single quotes and separated by the string #$D#$A. The code removes the single quotes and replaces the #$D#$A by a space. This isdone inside the GetReplacementValue function near the top of the code. The rest of the code is just the baggage that you need to include to implement a visualizer, and there's quite a lot of it, even in this rather minimalist implementation.
Once the package is installed, as well as being displayed in the Watch Window,
the Text property can be pasted to the Clipboard using the Copy Watch Value
entry on the Watch Window's context menu.
Code (written for and tested in XE4):
{*******************************************************}
{ }
{ RadStudio Debugger Visualizer Sample }
{ Copyright(c) 2009-2013 Embarcadero Technologies, Inc. }
{ }
{*******************************************************}
{Adapted by Martyn Ayers, Bristol, UK Oct 2015}
unit SimpleTStringsVisualizeru;
interface
procedure Register;
implementation
uses
Classes, Forms, SysUtils, ToolsAPI;
resourcestring
sVisualizerName = 'TStrings Simple Visualizer for Delphi';
sVisualizerDescription = 'Simplifies TStrings Text property format';
const
CRLFReplacement = ' ';
type
TDebuggerSimpleTStringsVisualizer = class(TInterfacedObject,
IOTADebuggerVisualizer, IOTADebuggerVisualizerValueReplacer,
IOTAThreadNotifier, IOTAThreadNotifier160)
private
FNotifierIndex: Integer;
FCompleted: Boolean;
FDeferredResult: string;
public
{ IOTADebuggerVisualizer }
function GetSupportedTypeCount: Integer;
procedure GetSupportedType(Index: Integer; var TypeName: string;
var AllDescendants: Boolean);
function GetVisualizerIdentifier: string;
function GetVisualizerName: string;
function GetVisualizerDescription: string;
{ IOTADebuggerVisualizerValueReplacer }
function GetReplacementValue(const Expression, TypeName, EvalResult: string): string;
{ IOTAThreadNotifier }
procedure EvaluteComplete(const ExprStr: string; const ResultStr: string;
CanModify: Boolean; ResultAddress: Cardinal; ResultSize: Cardinal;
ReturnCode: Integer);
procedure ModifyComplete(const ExprStr: string; const ResultStr: string;
ReturnCode: Integer);
procedure ThreadNotify(Reason: TOTANotifyReason);
procedure AfterSave;
procedure BeforeSave;
procedure Destroyed;
procedure Modified;
{ IOTAThreadNotifier160 }
procedure EvaluateComplete(const ExprStr: string; const ResultStr: string;
CanModify: Boolean; ResultAddress: TOTAAddress; ResultSize: LongWord;
ReturnCode: Integer);
end;
TTypeLang = (tlDelphi, tlCpp);
// The following function is the one which actually changes the TStrings
// representation in the Watch Window
//
// Normally, the Text property of TStrings variable is displayed in the Watch Window
// and Evaluate window as one or more text lines surrounded by single quotes
// and separated by the string #$D#$A
//
// This implementation removes the single quotes and replaces the #$D#$A
// by a space
//
// Note the addition of '.Text' to the expression which gets evaluated; this is to
// produce the desired result when using the 'Copy Watch Value' item in the
// Watch Window context menu.
function TDebuggerSimpleTStringsVisualizer.GetReplacementValue(
const Expression, TypeName, EvalResult: string): string;
var
Lang: TTypeLang;
i: Integer;
CurProcess: IOTAProcess;
CurThread: IOTAThread;
ResultStr: array[0..4095] of Char; // was 255
CanModify: Boolean;
ResultAddr, ResultSize, ResultVal: LongWord;
EvalRes: TOTAEvaluateResult;
DebugSvcs: IOTADebuggerServices;
function FormatResult(const Input: string; out ResStr: string): Boolean;
var
TL : TStringList;
i : Integer;
S : String;
const
CRLFDisplayed = '#$D#$A';
begin
Result := True;
ResStr := '';
TL := TStringList.Create;
try
S := Input;
S := StringReplace(S, CRLFDisplayed, #13#10, [rfReplaceAll]);
TL.Text := S;
for i := 0 to TL.Count - 1 do begin
S := TL[i];
if S <> '' then begin
if S[1] = '''' then // Remove single quote at start of line
Delete(S, 1, 1);
if S[Length(S)] = '''' then // Remove single quote at end of line
S := Copy(S, 1, Length(S) - 1);
end;
if ResStr <> '' then
ResStr := ResStr + CRLFReplacement;
ResStr := ResStr + S;
end;
finally
TL.Free;
end;
end;
begin
Lang := tlDelphi;
if Lang = tlDelphi then
begin
if Supports(BorlandIDEServices, IOTADebuggerServices, DebugSvcs) then
CurProcess := DebugSvcs.CurrentProcess;
if CurProcess <> nil then
begin
CurThread := CurProcess.CurrentThread;
if CurThread <> nil then
begin
EvalRes := CurThread.Evaluate(Expression + '.Text', #ResultStr, Length(ResultStr),
CanModify, eseAll, '', ResultAddr, ResultSize, ResultVal, '', 0);
if EvalRes = erOK then
begin
Result := ResultStr;
end else if EvalRes = erDeferred then
begin
FCompleted := False;
FDeferredResult := '';
FNotifierIndex := CurThread.AddNotifier(Self);
while not FCompleted do
DebugSvcs.ProcessDebugEvents;
CurThread.RemoveNotifier(FNotifierIndex);
FNotifierIndex := -1;
if (FDeferredResult = '') then
Result := EvalResult
else
FormatResult(FDeferredResult, Result);
end;
end;
end;
end
else
;
end;
procedure TDebuggerSimpleTStringsVisualizer.AfterSave;
begin
// don't care about this notification
end;
procedure TDebuggerSimpleTStringsVisualizer.BeforeSave;
begin
// don't care about this notification
end;
procedure TDebuggerSimpleTStringsVisualizer.Destroyed;
begin
// don't care about this notification
end;
procedure TDebuggerSimpleTStringsVisualizer.Modified;
begin
// don't care about this notification
end;
procedure TDebuggerSimpleTStringsVisualizer.ModifyComplete(const ExprStr,
ResultStr: string; ReturnCode: Integer);
begin
// don't care about this notification
end;
procedure TDebuggerSimpleTStringsVisualizer.EvaluteComplete(const ExprStr,
ResultStr: string; CanModify: Boolean; ResultAddress, ResultSize: Cardinal;
ReturnCode: Integer);
begin
EvaluateComplete(ExprStr, ResultStr, CanModify, TOTAAddress(ResultAddress),
LongWord(ResultSize), ReturnCode);
end;
procedure TDebuggerSimpleTStringsVisualizer.EvaluateComplete(const ExprStr,
ResultStr: string; CanModify: Boolean; ResultAddress: TOTAAddress; ResultSize: LongWord;
ReturnCode: Integer);
begin
FCompleted := True;
if ReturnCode = 0 then
FDeferredResult := ResultStr;
end;
function TDebuggerSimpleTStringsVisualizer.GetSupportedTypeCount: Integer;
begin
Result := 1;
end;
procedure TDebuggerSimpleTStringsVisualizer.GetSupportedType(Index: Integer; var TypeName: string;
var AllDescendants: Boolean);
begin
AllDescendants := True;
TypeName := 'TStrings';
end;
function TDebuggerSimpleTStringsVisualizer.GetVisualizerDescription: string;
begin
Result := sVisualizerDescription;
end;
function TDebuggerSimpleTStringsVisualizer.GetVisualizerIdentifier: string;
begin
Result := ClassName;
end;
function TDebuggerSimpleTStringsVisualizer.GetVisualizerName: string;
begin
Result := sVisualizerName;
end;
procedure TDebuggerSimpleTStringsVisualizer.ThreadNotify(Reason: TOTANotifyReason);
begin
// don't care about this notification
end;
var
TStringsVis: IOTADebuggerVisualizer;
procedure Register;
begin
TStringsVis := TDebuggerSimpleTStringsVisualizer.Create;
(BorlandIDEServices as IOTADebuggerServices).RegisterDebugVisualizer(TStringsVis);
end;
procedure RemoveVisualizer;
var
DebuggerServices: IOTADebuggerServices;
begin
if Supports(BorlandIDEServices, IOTADebuggerServices, DebuggerServices) then
begin
DebuggerServices.UnregisterDebugVisualizer(TStringsVis);
TStringsVis := nil;
end;
end;
initialization
finalization
RemoveVisualizer;
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.
I'm trying to write a simple unit containing a class TMainWindow to improve my knowledge about the native Windows API.
I'd like to use this class like this:
var
MainWindow: TMainWindow;
begin
MainWindow := TMainWindow.Create;
try
MainWindow.ShowModal;
finally
MainWindow.Free;
end;
end.
I got an almost working prototype, but I can't find the problem, here is the code I have written so far:
unit NT.Window;
interface
uses
Windows, Messages, Classes, SysUtils;
type
PObject = ^TObject;
TMainWindow = class(TObject)
private
FChild : HWND; { Optional child window }
FHandle : HWND;
procedure WMCreate (var Msg: TWMCreate); message WM_CREATE;
procedure WMDestroy (var Msg: TWMDestroy); message WM_DESTROY;
procedure WMNcCreate (var Msg: TWMNCCreate); message WM_NCCREATE;
procedure WMPaint (var Msg: TWMPaint); message WM_PAINT;
procedure WMPrintClient (var Msg: TWMPrintClient); message WM_PRINTCLIENT;
procedure WMSize (var Msg: TWMSize); message WM_SIZE;
procedure PaintContent(const APaintStruct: TPaintStruct);
function HandleMessage(var Msg: TMessage): Integer;
public
constructor Create;
procedure DefaultHandler(var Message); override;
function ShowModal: Boolean;
end;
implementation
var
WindowByHwnd: TStringList;
function PointerToStr(APointer: Pointer): string;
begin
Result := IntToStr(NativeInt(APointer));
end;
function StrToPointerDef(AString: string; ADefault: Pointer): Pointer;
begin
Result := Pointer(StrToIntDef(AString, Integer(ADefault)));
end;
function GetWindowByHwnd(hwnd: HWND): TMainWindow;
begin
Result := TMainWindow(StrToPointerDef(WindowByHwnd.Values[IntToStr(hwnd)], nil));
end;
procedure StoreWindowByHwnd(hwnd: HWND; AWindow: TMainWindow);
begin
AWindow.FHandle := hwnd;
WindowByHwnd.Add(IntToStr(hwnd) + '=' + PointerToStr(Pointer(AWindow)));
end;
function WndProc(hwnd: HWND; uiMsg: UINT; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;
var
Msg : TMessage;
Window : TMainWindow;
begin
Msg.Msg := uiMsg;
Msg.WParam := wParam;
Msg.LParam := lParam;
Msg.Result := 0;
if uiMsg = WM_NCCREATE then begin
StoreWindowByHwnd(hwnd, TMainWindow(TWMNCCreate(Msg).CreateStruct.lpCreateParams))
end;
Window := GetWindowByHwnd(hwnd);
if Window = nil then begin
Result := DefWindowProc(hwnd, Msg.Msg, Msg.WParam, Msg.LParam);
end else begin
Result := Window.HandleMessage(Msg);
end;
end;
{ TMainWindow }
constructor TMainWindow.Create;
var
wc: WNDCLASS;
begin
inherited Create;
wc.style := 0;
wc.lpfnWndProc := #WndProc;
wc.cbClsExtra := 0;
wc.cbWndExtra := 0;
wc.hInstance := HInstance;
wc.hIcon := 0;
wc.hCursor := LoadCursor(0, IDC_ARROW);
wc.hbrBackground := HBRUSH(COLOR_WINDOW + 1);
wc.lpszMenuName := nil;
wc.lpszClassName := 'Scratch';
if Windows.RegisterClass(wc) = 0 then begin
raise Exception.Create('RegisterClass failed: ' + SysErrorMessage(GetLastError));
end;
if CreateWindow(
'Scratch', { Class Name }
'Scratch', { Title }
WS_OVERLAPPEDWINDOW, { Style }
Integer(CW_USEDEFAULT),
Integer(CW_USEDEFAULT), { Position }
Integer(CW_USEDEFAULT),
Integer(CW_USEDEFAULT), { Size }
0, { Parent }
0, { No menu }
HInstance, { Instance }
#Self { No special parameters }
) = 0 then begin
raise Exception.Create('CreateWindow failed: ' + SysErrorMessage(GetLastError));
end;
end;
procedure TMainWindow.DefaultHandler(var Message);
var
Msg: TMessage;
begin
Msg := TMessage(Message);
Msg.Result := DefWindowProc(FHandle, Msg.Msg, Msg.WParam, Msg.LParam);
end;
function TMainWindow.HandleMessage(var Msg: TMessage): Integer;
begin
// Dispatch(Msg);
case Msg.Msg of
WM_CREATE : WMCreate( TWMCreate(Msg));
WM_DESTROY : WMDestroy( TWMDestroy(Msg));
WM_NCCREATE : WMNcCreate( TWMNCCreate(Msg));
WM_PAINT : WMPaint( TWMPaint(Msg));
WM_PRINTCLIENT : WMPrintClient(TWMPrintClient(Msg));
WM_SIZE : WMSize( TWMSize(Msg));
else
// DefaultHandler(Msg);
Msg.Result := DefWindowProc(FHandle, Msg.Msg, Msg.WParam, Msg.LParam);
end;
Result := Msg.Result;
end;
procedure TMainWindow.PaintContent(const APaintStruct: TPaintStruct);
begin
end;
function TMainWindow.ShowModal: Boolean;
var
msg_ : MSG;
begin
ShowWindow(FHandle, CmdShow);
while GetMessage(msg_, 0, 0, 0) do begin
TranslateMessage(msg_);
DispatchMessage(msg_);
end;
Result := True;
end;
procedure TMainWindow.WMCreate(var Msg: TWMCreate);
begin
Msg.Result := 0;
end;
procedure TMainWindow.WMDestroy(var Msg: TWMDestroy);
begin
PostQuitMessage(0);
end;
procedure TMainWindow.WMNcCreate(var Msg: TWMNCCreate);
begin
Msg.Result := Ord(True);
end;
procedure TMainWindow.WMPaint(var Msg: TWMPaint);
var
ps: PAINTSTRUCT;
begin
BeginPaint(FHandle, ps);
PaintContent(ps);
EndPaint(FHandle, ps);
end;
procedure TMainWindow.WMPrintClient(var Msg: TWMPrintClient);
var
ps: PAINTSTRUCT;
begin
ps.hdc := Msg.DC;
GetClientRect(FHandle, ps.rcPaint);
PaintContent(ps);
end;
procedure TMainWindow.WMSize(var Msg: TWMSize);
begin
if FChild <> 0 then begin
MoveWindow(FChild, 0, 0, Msg.Width, Msg.Height, True);
end;
end;
initialization
WindowByHwnd := TStringList.Create;
finalization
WindowByHwnd.Free;
end.
The code is partially based on the scratch program by Raymond Chen:
http://blogs.msdn.com/b/oldnewthing/archive/2003/07/23/54576.aspx
I'm using a TStringList to look up the instance of TMainWindow in the WndProc function which is pretty inefficient, but should work.
The program crashes as is and also crashes when I use Dispatch in the HandleMessage function.
Why does it crash right after leaving the constructor or in the modified version in the Dispatch call?
You call CreateWindow like this:
CreateWindow(
'Scratch', { Class Name }
'Scratch', { Title }
WS_OVERLAPPEDWINDOW, { Style }
Integer(CW_USEDEFAULT),
Integer(CW_USEDEFAULT), { Position }
Integer(CW_USEDEFAULT),
Integer(CW_USEDEFAULT), { Size }
0, { Parent }
0, { No menu }
HInstance, { Instance }
#Self { No special parameters }
)
In addition to the comment on the final parameter being wrong, the value is wrong. The expression #Self is a pointer to the local Self variable. A pointer to a local variable. That's bound to turn out badly. You thought you were passing a pointer to the object being created, but that's given by the value of Self directly. Remove the #.
There are a few more direct ways of associating an object reference with a window handle instead of converting both the handle and the reference to strings and doing name=value lookups.
For starters, you could use a more type-safe associative container, like a TDictionary<HWnd, TMainWindow>. That at least gets you away from all the string conversions.
You can associate the object reference directly with the window handle using SetWindowLongPtr and GetWindowLongPtr. You could modify your code as follows:
constructor TMainWindow.Create;
// ...
wc.cbWndExtra := SizeOf(Self);
function GetWindowByHwnd(hwnd: HWnd): TMainWindow;
begin
Result := TMainWindow(GetWindowLongPtr(hwnd, 0));
end;
procedure StoreWindowByHwnd(hwnd: HWND; AWindow: TMainWindow);
begin
AWindow.FHandle := hwnd;
SetWindowLongPtr(hwnd, 0, IntPtr(AWindow));
end;
Since you're use the "extra window bytes," you need to make sure that descendants of your window class don't try to use the same space for something else. You'd want to provide some sort of mechanism for descendants to "register" that they want space, add up all the descendants' requests, and put the total in the cbWndExtra field. Then have a way for descendants to load and store data at the slots they reserved.
You can use window properties. Store the object reference in a property value with SetProp in the wm_NCCreate message, and remove it with RemoveProp in the wm_NCDestroy message.
Pick a property name unlikely to be used by descendant classes.
Finally, you could do what the VCL does, which is to allocate a new "stub" window procedure for every object. It has a template procedure jumps to the address of the regular window procedure; it allocates memory for a new stub, fills in the template with the current object reference, and then uses that stub pointer when it calls RegisterClassEx.
I'm creating an instance of my custom DragObject on StartDrag:
procedure TForm1.GridStartDrag(Sender: TObject;
var DragObject: TDragObject);
begin
DragObject := TMyDragControlObject.Create(Sender as TcxGridSite);
end;
Lately on another grid on DragOver:
procedure TForm1.SecondGridDragOver(Sender, Source: TObject; X,
Y: Integer; State: TDragState; var Accept: Boolean);
begin
Accept := False;
if Source is TMyDragControlObject then
with TMyDragControlObject(Source) do
// using TcxGrid
if (Control is TcxGridSite) or (Control is TcxGrid) then begin
Accept := True
// checking the record value on grid
// the label of drag cursor will be different
// getting the record value works fine!
if RecordOnGrid.Value > 5 then
DragOverPaint(FImageList, 'You can drop here!');
else begin
Accept := false;
DragOverPaint(FImageList, 'You can''t drop here!');
end
end;
end;
My DragOverPaint procedure:
procedure TForm1.DragOverPaint(ImageList: TImageList; AValue: string);
var ABmp: TBitmap;
begin
if not Assigned(ImageList) then Exit;
ABmp := TBitmap.Create();
try
with ABmp.Canvas do begin
ABmp.Width := TextWidth(AValue);
ABmp.Height := TextHeight(AValue);
TextOut(0, 0, AValue);
end;
ImageList.BeginUpdate;
ImageList.Clear;
ImageList.Width := ABmp.Width;
ImageList.Height := ABmp.Height;
ImageList.AddMasked(ABmp, clNone);
ImageList.EndUpdate;
finally
ABmp.Free();
end;
Repaint;
end;
I want it to repaint DragImageList depending on the grid record value, but the image list doesn't refresh when it's already painted.
Once the ImageList has started dragging, you cannot change the drag image by changing the ImageList because Windows creates another temporarily blended ImageList specially for the dragging. So you have to end, change and start the ImageList dragging again (this is not equal to ending and starting the complete VCL dragging operation, just the WinAPI ImageList). The result/downside is a slight quiver at the transition of the images.
The moment of changing the images is when Accepted changes (in this specific case). It is possible to deal with this in OnDragOver, but since you create an own DragObject already, you can also override the therefor designed methods of TDragObject:
type
TControlAccess = class(TControl);
TMyDragControlObject = class(TDragControlObjectEx)
private
FDragImages: TDragImageList;
FPrevAccepted: Boolean;
protected
function GetDragCursor(Accepted: Boolean; X, Y: Integer): TCursor; override;
function GetDragImages: TDragImageList; override;
public
destructor Destroy; override;
end;
{ TMyDragControlObject }
destructor TMyDragControlObject.Destroy;
begin
FDragImages.Free;
inherited Destroy;
end;
function TMyDragControlObject.GetDragCursor(Accepted: Boolean; X,
Y: Integer): TCursor;
begin
if FPrevAccepted <> Accepted then
with FDragImages do
begin
EndDrag;
SetDragImage(Ord(Accepted), 0, 0);
BeginDrag(GetDesktopWindow, X, Y);
end;
FPrevAccepted := Accepted;
Result := inherited GetDragCursor(Accepted, X, Y);
end;
function TMyDragControlObject.GetDragImages: TDragImageList;
const
SNoDrop = 'You can''t drop here!!';
SDrop = 'You can drop here.';
Margin = 20;
var
Bmp: TBitmap;
begin
if FDragImages = nil then
begin
FDragImages := TDragImageList.Create(nil);
Bmp := TBitmap.Create;
try
Bmp.Canvas.Font.Assign(TControlAccess(Control).Font);
Bmp.Width := Bmp.Canvas.TextWidth(SNoDrop) + Margin;
Bmp.Height := Bmp.Canvas.TextHeight(SNoDrop);
Bmp.Canvas.TextOut(Margin, 0, SNoDrop);
FDragImages.Width := Bmp.Width;
FDragImages.Height := Bmp.Height;
FDragImages.Add(Bmp, nil);
Bmp.Canvas.FillRect(Rect(0, 0, Bmp.Width, Bmp.Height));
Bmp.Canvas.TextOut(Margin, 0, SDrop);
FDragImages.Add(Bmp, nil);
FDragImages.SetDragImage(0, 0, 0);
finally
Bmp.Free;
end;
end;
Result := FDragImages;
end;
{ TForm1 }
procedure TForm1.FormCreate(Sender: TObject);
begin
Grid1.ControlStyle := Grid1.ControlStyle + [csDisplayDragImage];
Grid2.ControlStyle := Grid2.ControlStyle + [csDisplayDragImage];
end;
procedure TForm1.Grid1StartDrag(Sender: TObject; var DragObject: TDragObject);
begin
DragObject := TMyDragControlObject.Create(Sender as TStringGrid);
end;
procedure TForm1.Grid2DragOver(Sender, Source: TObject; X, Y: Integer;
State: TDragState; var Accept: Boolean);
begin
Accept := False;
if IsDragObject(Source) then
with TMyDragControlObject(Source) do
if Control is TGrid then
{ Just some condition for testing }
if Y > Control.Height div 2 then
Accept := True;
end;
As NGLN pointed out, the reason for the change not taking effect is that Windows creates a temporary image list while dragging. As a slightly different solution, you can directly change the image in this temporary list.
The below is the modified DragOverPaint accordingly. Note that you should still make use of some kind of a flag for not repopulating the list with every mouse move as in NGLN's answer.
procedure TForm1.DragOverPaint(ImageList: TImageList; AValue: string);
var
ABmp: TBitmap;
ImgList: HIMAGELIST; // <- will get the temporary image list
begin
if not Assigned(ImageList) then Exit;
ABmp := TBitmap.Create();
try
with ABmp.Canvas do begin
ABmp.Width := TextWidth(AValue);
ABmp.Height := TextHeight(AValue);
TextOut(0, 0, AValue);
end;
// ImageList.BeginUpdate; // do not fiddle with the image list,
// ImageList.Clear; // it's not used while dragging
// ImageList.Width := ABmp.Width;
// ImageList.Height := ABmp.Height;
// ImageList.AddMasked(ABmp, clNone);
// ImageList.EndUpdate;
// get the temporary image list
ImgList := ImageList_GetDragImage(nil, nil);
// set the dimensions for images and empty the list
ImageList_SetIconSize(ImgList, ABmp.Width, ABmp.Height);
// add the text as the first image
ImageList_AddMasked(ImgList, ABmp.Handle, ColorToRGB(clWhite));
finally
ABmp.Free();
end;
// Repaint; // <- No need to repaint the form
end;