TCustomListBox - How to introduce OnInsert and OnRemove methods? - delphi

I want to introduce some new methods into a custom control I am deriving from TCustomListBox.
What I want is a method that can be used when a item is added/inserted into the listbox, and a method for when an item is removed from the listbox.
What would be a good place to start with this? I know controls like TListView for example have an OnInsert event but I cannot see anything for listbox?
I wanted to introduce into my control for example:
OnInsert
OnRemove
Would I need to use some kind of API or Messages to detect when items are added/removed and then take it from there? Is there an easier way to do this or does it require some difficulty?
I tried looking at some of the VCL source but most of it is confusing for me.
Thanks in advance.

The API you need already exists.
If you inspect the source of a TCustomListBox you will see that the mechanism by which items are added, inserted or removed from a list is implemented using window messages. For example in TListBoxString.Add() as well as house-keeping code you will see that the string is eventually added by sending a message to the control:
Result := SendTextMessage(ListBox.Handle, LB_ADDSTRING, 0, S);
Delphi provides various mechanisms for providing handlers on control and window classes that respond to specific messages. Perhaps the most straightforward and appropriate, for adding a simple notification mechanism such as your require, is to implement a specific message handler method.
You provide a message handler procedure and declare which message it responds to. In your case, for example, you could add your own handling of the LB_ADDSTRING message:
TChattyList = class(TCustomListbox)
procedure LBAddString(var aMessage: TMessage); message LB_ADDSTRING;
end;
The parameters of the message (wParam and lParam) are packaged up inside the TMessage record passed as the by reference parameter to your handler). You will need to consult the Windows API documentation for the message in question to determine the use of these parameters.
You can do pretty much whatever you want in your message handler although you should always pay close attention to what a window is expected to do in response to documented messages, including any return values (set in the Result field of the TMessage parameter, which is why it is passed by reference, as var.
In this trivial example, the new handler calls inherited to ensure that the inherited implementation is allowed to respond by actually adding the new item string and then crudely pops up a message box to let us know that an item was added:
procedure TChattyList.LBAddString(var aMessage: TMessage);
begin
inherited;
ShowMessage('item added');
end;
In essence your event mechanism will do exactly the same, but instead of presenting a message box you would trigger your new event after allowing the inherited implementation to do it's work (and checking the result code set, to ensure that it was successful, according to the expected return values for the message in question):
procedure TChattyList.LBAddString(var aMessage: TMessage);
begin
inherited;
if (aMessage.Result = LB_ERR) or (aMessage.Result = LB_ERRSPACE) then
EXIT;
if Assigned(fOnInsert) then
fOnInsert(self);
end;
If the inherited handler failed to add an item then according to the documentation it should set the result to LB_ERR or LB_ERRSPACE, so we test for these values and exit if they are found. Otherwise we call the appropriate event handler, if one is assigned.
This assumes that for your purposes a simple TNotifyEvent is sufficient and that you do not discriminate between an item being inserted vs an item being added. You could of course have separate events or provide some indication in parameters to a specialised event type.
Which messages you choose to handle and expose as which sorts of events is then a question of exactly what your requirements are, but based on what you have stated in your question, at a minimum I think you will need message handlers for LB_ADDSTRING, LB_INSERTSTRING and LB_DELETESTRING.
You may need to handle additional messages and should consult the Windows API documentation for listbox controls for further information.

Related

Can ShortCut-Key, that has been processed by Delphi TAction, be allowed to propagate further to other components?

Core problem: I don't like the way how TcxGrid delete (that is initiated by the default Ctrl+Del shortcut key for the TcxGrid or by the Delete button of the cx-navigator) is working: it is simple and direct call to the delete of the underlying dataset, there is no TcxGrid(/DataController).OnDelete(Sender: TObject, var AHandled: Boolean) event for the cxGrid or cxGridDataView. And from https://www.devexpress.com/Support/Center/Question/Details/Q308755/tcxgrid-deleting-record I understand that I can only configure the confirmation message, but I have no control over the Delete itself, i.e., I can not implement custom Delete and therefore I don't even try to ask for the straight solution of this problem. My question is about workaround.
That is why I am refusing to use cx-navigator and I opt to introduce TAction that handles Ctrl+Del shortcut key for the entire form. I have multiple grids on the form each with its own problematic Delete procedure that should be handled in non-standard way by the custom procedure.
That is why my DeleteAction determines the ActiveControl (TcxGridSite, TcxGrid) initially and then calls relevant Delete procedure afterwards. All that is fine. But some other components (e. g. TcxDBTextEdit) have default Ctrl+Del handling as well and that handling is very, very good. But if shortcut key Ctrl+Del is handled by action (and it is marked as handled even in the case when no active grid can not be found and therefor even in the case when ActionExecute has not done anything useful) then further propagation is stopped. That can be observed empirically and that can be seen theoretically from http://edn.embarcadero.com/article/38447 .
But maybe still there is some workaround how the shortcut key that has been processed by the ActionExecute can still be propagated further to the default components if they are Active controls.
I know that TAction is for (form-wide global) menu functionality but TcxGrid is not extensible enough and that is why should try to stretch the Delphi TAction design as well.
Probably there would be various ways to achieve the processing you require, there are quite a few places you can intervene during key processing, as you've already read from Peter Below's article linked in your question.
A place to intervene that I can think of which would contain all the pieces together is the form's IsShortCut method. One probable disadvantage of such a method could be that you'd have to implement it on all the forms that you want their key handling behavior changed.
Normally key processing continues only if it's not handled by shortcuts. Below solution reports the shortcut to be not processed for that reason, but then has to cause the execution of the action manually. I tested the code with a regular edit as can be seen, but I guess it shouldn't make any difference.
function TForm1.IsShortCut(var Message: TWMKey): Boolean;
begin
if (ActiveControl is TEdit) and (Message.CharCode = VK_DELETE) and
(KeyDataToShiftState(Message.KeyData) = [ssCtrl]) then begin
Result := False; // if you don't require the action to be executed, just exist here
ActionList1.IsShortCut(Message); // executes the action, ignore result
// returning false will result in main form's shortcut handler to be called
// below is only required if this is the main form
if Application.MainForm = Self then
Message.CharCode := 0;
end else
Result := inherited IsShortCut(Message);
end;

How do i detect new columns added to a list-view control?

I am writing a component that descends from TListView, and I want to know when the developer using this component adds a column so I can react. How can I detect when a new column is added? Is there an event?
There is no such event, but since you are the component writer, events are off-limits to you anyway. Events are for the developers using your component.
Columns are added by sending the control lvm_InsertColumn messages. Override that message handler in your descendant control:
procedure LVMInsertColumn(var Msg: TMessage); message lvm_InsertColumn;
Implement it to call the inherited handler, and then do whatever you want.
procedure TListViewDescendant.LVMInsertColumn(var Msg: TMessage);
begin
inherited;
// TODO: custom handling
end;
The Delphi TListView control provides no such event, and the underlying Windows control does not provide a notification. The reason for that is that you the programmer are in charge of adding columns. Columns can only be added by calling Columns.Add in Delphi, or by sending a LVM_INSERTCOLUMN message to the underlying window.
So, you are in charge of adding columns, which presumably you do by calling Columns.Add. Route all code that adds columns through a method that calls Columns.Add, and add any special handling at that point.

Possible to change property write methods programmatically using RTTI for creating object-aware controls?

I have a business object that I would like to "connect" to my UI better. I've seen some partial solutions for making objects data-aware, but they all involved significant changes to my business object, including an extra layer of abstraction.
I've been looking into the improved RTTI in new versions of Delphi, and it looks very interesting and useful. I'm wondering if I could use it to programmatically inject new write methods for all properties.
The way this would work is that my TEdit descendant would by given a reference to an object property when the form is built. The TEdit would then insert a reference to itself in an attribute for that property (and of course remove itself on destructor or being given another reference). The TEdit would also ensure that the write method for the property is replaced by one that notifies the TEdit of changes after calling the original write method.
Is this feasible? The big show stopper would be that injecting a new write method isn't possible, hence the title for this question.
There are also potential problems with derived properties, but it should be possible to find a solution for that.
Your question already puts you ahead of me with programming skill so I'll just add how I might approach this:
If I were to try to write something like that I'd probably start with a TList for each field in your TBusinessObject. That list would be used to indicate what needed to be updated when you needed to push out a change.
So when the TEdit is created it would add itself to a list which was associated with a piece of data in your TBusinessObject. When the TBusinessObject updated that piece of data it would then run through the list of attached objects. It would see the TEdit and, knowing it was a TEdit, would run code to update the .Text. If I attached a TCaption then the code would update the .Caption.
The TEdit, as you indicated, would need to tell the TBusinessObject when it's value was updated. I guess this is the tricky spot - you could create a new TEdit and add in a TList to maintain who it should inform when it changes. If you used the .Tag to indicate a field number in the TBusinessObject then the OnChange (or whatever event) could then call something like TBusinessObject.FieldUpdate[TEdit.Tag, NewValue] which then triggers your business logic. That, in turn, might make the TBusinessObject update other fields, which may have their own TLists to fields to update.
Preventing circular updates would require that you have a way of updating a control without triggering events. For one program I wrote I had two methods to update the control: SetValue and ChangeValue. SetValue disabled any events (OnChange, OnValidate), updated the control's value and then reenabled the events. ChangeValue simply changed the value and allowed any of the control's events to fire as required.
There are probably slicker ways to do this but hopefully this gives you food for thought.
Possible to change property write methods programmatically using RTTI for creating object-aware controls?
No, it's not possible. RTTI gives you information, it doesn't give the ability to alter types at runtime.
The big show stopper would be that injecting a new write method isn't possible, hence the title for this question
In order for you to change this at runtime there should be something similar to an event handler that you can set. It's an easy concept, but it has some runtime overhead, both in call time (it would be an indirection where a direct call would normally suffice) and in terms of required memory (each property would require an extra TEvent style field). This is easy for you to implement if you need it, but it would be harmful if the compiler automatically generated such code for all classes "just in case".
If you're thinking of somehow patching the code in memory at runtime, that's not going to work and it would be, at best, unreliable.
In this post entitled Inducing The Great Divide, Cobus Kruger talked about business objects.
The solution he cooked is essentially compliant to your requirements:
Make use of advanced RTTI features introduced in recent Delphi version.
Separation of the business logic from presentation logic.
Any PODO (Plain Old Delphi Object) will do as business object !
The magic lays in the TObjectBinding class which ties any TWinControl with any business object.
Excerpt:
TObjectBinding = class
private
fCtx: TRttiContext;
fControlType: TRttiType;
fObjType: TRttiType;
fPropFieldMapping: TDictionary<TRttiProperty, TRttiField>; // Dictionary of object Properties & corresponding Fields
fControl: TWinControl; // The control (normally form)
fObj: TObject; // Object it represents.
procedure CreateMappings;
function FindField(Prop: TRttiProperty; out Field: TRttiField): Boolean;
function FieldClass(Field: TRttiField): TClass;
// Modify these to change the rules about what should be matched.
function IsValidField(Field: TRttiField): Boolean;
function IsValidProp(Prop: TRttiProperty): Boolean;
// Modify these to change the mappings of property type to VCL control class.
procedure AssignField(Prop: TRttiProperty; Field: TRttiField);
procedure AssignProp(Prop: TRttiProperty; Field: TRttiField);
// Used from AssignField/AssignProp. Extend these to support a wider range of properties.
function GetPropText(Prop: TRttiProperty): string;
procedure SetPropText(Prop: TRttiProperty; const Text: string);
public
constructor Create(Control: TWinControl; Obj: TObject);
destructor Destroy; override;
//
procedure Load;
procedure Save;
end;
I hope that this will be a good starting point for you.

what kind of messages VCL accept ?/

How to find out the list of messages that a certain VCL component can accept ???/
For example if i want to scroll the Memo1 by sending message to it
I probably will write the following lines of code knowing that the memo can accept EM_LINESCROLL
SendMessage(Memo1->Handle,EM_LINESCROLL,-1,0);
//Memo1->Perform(EM_SCROLL,SB_LINEUP,0);
Memo1->Perform(EM_SCROLL,SB_LINEDOWN,0);
How to find to find out if certain VCL comps can accept or do not accept messages???
All components accept all messages, but if a component have no assigned message handler, it just does nothing
If you want to discover if VCL component have special handler to certain Windows message, you have to look into VCL sources, which are usually provided with C++Builder (except Starter Edition's of XE and XE2).
VCL Sources are located in %CBuilderDir%\Sources\VCL (looking at my CBuilder5/6)
Sources are written in delphi, but it won't be difficult to find all what we need.
First, You'll have to find defininition of your target class. You can search through whole VCL source dir for file with line looking like
TMemo = Class (for your example with TMemo)
Open file where you found your class, (usually it will be stdctrls.pas or controls.pas - most useful components are located there), go to the line with class definition and scroll a little down until you find a group of procedures, looking like
procedure WMLButtonDown(var Message: TWMLButtonDown); message WM_LBUTTONDOWN;
procedure WMRButtonDown(var Message: TWMRButtonDown); message WM_RBUTTONDOWN;
...and so on. These procedures are called in response to certain messages, which id's are provided after procedure definition.
If a class have procedure for certain message, then it provides some response to it.
Message handlers are inherited in delphi, so if you didn't find handler for your message, you can look into base classes and their message handlers. To discover full class hierarchy you can simply look into the help file, or look at class definition again TMemo = class (TCustomMemo) and take parent class name from braces.
Then you can repeat search for message handler for all parent classes untill you reach TObject :-)
By the way. Simpy searching through VCL source dir of my CBuilder5 for any presense of EM_LINESCROLL I've figured than no VCL component processes it.
If you only need to provide special interaction for certain message, not trying to figure if a component already have or not have message handlers your can simply override WindowProc method of your component. All descendants of TControl have this method.
This Method process all messages received by component, and you can add response to additional system or user messages here.
void __fastcall TMyForm::NewWndProc(Messages::TMessage &Message)
{
if (Message.Msg == EM_LINESCROLL)
// Do something special for this message
else OldWndProc(Message);
}
Only thing that you'll need to do is to preserve value of old WindowProc, to call it in NewWndProc after you do all your stuff.
Its better to define and assign NewWndProc and store old WindowProc for TMemo in the form which holds your component, so you won't need to mess with making new inherited component from TMemo. So, define TWndMethod OldWndProc in form and put following, for example, in form OnCreate() handler
TWndMethod OldWndProc = MyMemo->WindowProc;
MyMemo->WindowProc = NewWndProc;
Also your can prevent firing of predefined handlers, by not passing certain messages to OldWndProc. Be careful, if you prevent processing of sensible system messages (like WM_CREATE) you'll get errors.
TMemo is a thin wrapper around a standard Win32 API multiline EDIT control. You have to read the MSDN documentation to see which messages an EDIT control natively handles. TMemo does not process EM_LINESCROLL directly, but Windows does.

Cancel / abort creating a new form in Delphi / C++Builder?

Is there any way to cancel or abort form creation from within the form's OnCreate event handler or C++Builder constructor?
Basically, I'd like to be able to call Close() from OnCreate or from the constructor and have it skip showing the form altogether. I have several forms that as part of their initialization may determine that they shouldn't be shown at all. (I realize that I could split apart this portion of the initialization or add extra checks from the calling form or similar, but if there's a way to cleanly do all of this from within OnCreate or the constructor, that seems simplest.)
Edit: In response to a few comments, some of the don't-show-at-all logic is UI logic and not business logic; the form might display a confirmation before showing, or it might use a common dialog box to get input for the form then abort if the user cancels that dialog. (Some of it is business logic and needs to be refactored, but it's often hard to find the time to refactor everything that needs it.)
You can always call Release in the OnCreate handler, but that will lead to the form quickly appearing and then being closed. Not a very professional thing.
So here's another idea. Let the forms have a public function or property to return whether they are in fact to be shown. Then where you would usually have
TheForm := TSomeForm.Create(Self);
TheForm.Show;
you would have
TheForm := TSomeForm.Create(Self);
if TheForm.ShouldAppear then
TheForm.Show
else
TheForm.Release;
Having said that - any other way of coding this (so you don't create a form that will be immediately destroyed) is surely better. Especially if you want to maintain a clear separation between UI and business layer it would be much better to have the code that decides whether the form is to be shown outside of the form. Create the form only after you have made the decision.
I would think it is much better to not even have to create the form in the first place. IF you're performing some logic which determines that the form is not even necessary, and that logic contains state which is important to the form, then re-factor the logic into a separate object (or even a data module) and pass the object to the form as a property. Here is a simple example (using the object approach):
UNIT1
type
TOFormTests = class
fStateData : string;
public
function IsForm1Needed( someparam : string) : boolean;
property StateData : string read fStateData write fStateData;
end;
UNIT2
uses
:
UNIT1;
type
TForm1 = class(tForm)
:
procedure SetFormTests(value : tOFormTests);
property FormTests : TOFormTests read fFormTests write SetFormTests;
end;
procedure SetFormTest(Value:TOFOrmTests);
begin
fFormTests := Value;
// perform gui setup logic here.
end;
then someplace in your code, where you are wanting to determine if you should show your gui or not use something like the following:
var
Tests : TOFormTests;
begin
tests := tOFormTests.create;
try
if Tests.IsForm1Needed('state data goes here') then
begin
Form1 := tForm1.create(nil);
try
Form1.FormTests := Tests;
if Form1.ShowModal = mrOk then
// handle any save state logic here.
;
finally
FreeAndNil(Form1);
end;
end;
finally
freeAndNil(Tests);
end;
end;
This also assumes that the form is NOT in the auto-create list and needs to be shown modal.
Use Abort in the constructor. It raises a silent exception. If an object has an exception in the constructor, then the destructor is called and the memory released. The advantage of Abort is then you don't need to worry about an exception dialog being displayed if you don't add exception handling code.
Add a class function that returns an instance when needed. Then the method that determines if the form should be shown is still in that class, but it can determine if it is necessary before the form is actually constructed. Call it like "CreateIfNeeded" and it will work just like a constructor, but won't actually construct the form if it isn't needed. Minimal code changes, and maximum flexibility.
Just raise an exception in OnCreate.
You'll need also redefine behavior of HandleCreateException method (as default is to display an error message, and not to cancel creation).
I would override ShowModal
function TfHtmlEditor.ShowModal: Integer;
begin
if TabControl1.Tabs.Count=0 then
Result := mrAbort
else
Result := inherited;
end;

Resources