Problem
I like to use the TActionManager component as a better way of managing events and building the menu interfaces either using TMainMenu or TActionMainMenuBar, for this though we would be using the TActionMainMenuBar because of the ActionBars property in the TActionManager.
One annoying issue I have is with the image indexes sometimes getting lost and more often then not it means having to go through each ActionBar item and manually entering the image index again, a pain if you are adding/deleting actions and images etc.
To solve this I came up with the idea of iterating through each TActionClientItem and assigning the image index which is determined by its assigned TAction.
Here is what I came up with so far:
procedure TMyComponent.ReassignActionImages;
var
I, J, K: Integer;
Manager: TActionManager;
BarItem: TActionBarItem;
Client: TActionClientItem;
Action: TAction;
begin
for I := 0 to Owner.ComponentCount -1 do
begin
if (Owner.Components[I].ClassType = TActionManager) then
begin
Manager := TActionManager(Owner.Components[I]);
for J := 0 to Manager.ActionBars.Count -1 do
begin
BarItem := Manager.ActionBars.ActionBars[J];
if BarItem.ActionBar <> nil then
begin
for K := 0 to BarItem.Items.Count -1 do
begin
Client := BarItem.Items[K];
if Client.Action <> nil then
begin
Action := TAction(Client.Action);
if Action <> nil then
begin
Client.ImageIndex := Action.ImageIndex;
//ShowMessage('Has Action: ' + Manager.Name + ' - ' + Action.Name + ' - ' + Client.Caption);
end;
end;
end;
end;
end;
end;
end;
end;
While this does seem to work, it does not appear to process child items. I think I need some kind of recursive procedure but I am not sure how to implement that. From what I understand from recursion it basically means running the same procedure within a procedure?
Steps to reproduce lost TActionClientItem image indexes using one scenario:
Create a New Project.
Add TActionManager to Form.
Add TAction to the TActionManager, leave ImageIndex as -1.
Add TActionMainMenuBar to Form.
Select the TActionManager in the Form Designer then edit the ActionBars property.
Add a new TActionBarItem.
In the Object Inspector assign the TActionMainMenuBar as the ActionBar.
Still in the Object Inspector click the Items (TActionClients) property.
Add a new TActionClientItem, give it a caption such as &File.
Click Items (TActionClients) property again.
Add a new TActionClientItem.
In the Object Inspector assign the TAction (eg, Action1) to the Action property.
Drop a TImageList on the Form, and add a bitmap.
Select the TActionManager in the Form Designer again and assign the TImageList to the Images property.
Double Click the TActionManager and for Action1 change the ImageIndex to 0
Run the application, notice there is no image showing.
The &File menu item will also be disabled so just edit the .OnExecute event for the TAction if you wish.
Run again, still no image showing.
Solution for the above scenario:
To make the image visible, edit the ActionBars and find the TActionClientItem manually setting the Image index to 0.
Run the application and the image will show.
So the above is one scenario I can reproduce that shows how the imageindex of a TAction is not updated to reflect the changes in a TActionClientItem.
There are other times that this has happened (usually when adding, deleting or editing TActions and images) where a manual update of the TActionClientItem is required.
This is why I decided to try and make a simple component which would ensure the images are syncronized at all times, and that no matter if the TActionClientItem imageindex is lost or becomes redundant, if it has a TAction assigned that has a imageindex I want that imageindex linked back with the TActionClientItem.
I am actually a bit pleased because for once I am able to solve a not so obvious simple problem!
The solution is to call IterateClients on the TActionManager.ActionBars and by using a callback procedure we can then access each TActionClientItem.
Useful documentation links:
http://docwiki.embarcadero.com/VCL/XE/en/ActnMan.TActionBars
http://docwiki.embarcadero.com/VCL/XE/en/ActnMan.TActionBars_Inherited_Members
http://docwiki.embarcadero.com/VCL/XE/en/ActnMan.TActionClientsCollection.IterateClients
In code this what I managed to do:
TMyComponent = class(TComponent)
protected
procedure Loaded; override;
procedure ActionCallBack(Client: TActionClient);
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
end;
and
procedure TMyComponent.Loaded;
procedure SyncImages;
var
I: Integer;
Manager: TActionManager;
begin
for I := 0 to Owner.ComponentCount -1 do
begin
if (Owner.Components[I].ClassType = TActionManager) then
begin
Manager := TActionManager(Owner.Components[I]);
Manager.ActionBars.IterateClients(Manager.ActionBars, ActionCallBack);
end;
end;
end;
begin
inherited Loaded;
SyncImages;
end;
{ ---------------------------------------------------------------------------- }
procedure TMyComponent.ActionCallBack(Client: TActionClient);
begin
if (Client is TActionClientItem) then
begin
with TActionClientItem(Client) do
begin
if Action <> nil then
begin
Caption := TAction(Action).Caption; // if you want to sync caption
ImageIndex := TAction(Action).ImageIndex;
end;
end;
end;
end;
Related
I'm trying to iterate between all components inside a TTabsheet. Problem is, inside this tab there are only a memo and a edit, but my code iterate between components in all form. What am I'm missing ?
var i : integer;
begin
with PageControl1.ActivePage do
for i := 0 to componentcount-1 do
begin
// componentcount should be 2, but actually is 95
components[i].doSomething;
end;
end;
I had something like this, where a button click caused code to traverse over all the controls that were on a tabsheet that was on a page control, using the components array. Later on I changed it to the following, that uses the controls array of the given tabsheet.
procedure TShowDocsByRank.CleanBtnClick(Sender: TObject);
var
i: integer;
begin
for i:= 0 to tabsheet1.controlcount - 1 do
if tabsheet1.controls[i] is TLabeledEdit
then TLabeledEdit (tabsheet1.controls[i]).text:= ''
else if tabsheet1.controls[i] is TComboBox
then TComboBox (tabsheet1.controls[i]).text:= '-';
end;
Perhaps 'with' might have something to do about it. You cant really tell what the return value for 'componentcount' will be (might even return number of components for the form itself?).
I would like to create a non visual component (like TTimer for example) that I can drop on the form and that I can set up directly from the Object Inspector, but I don't want to see its icon on the form (it'd just obstruct anything). For example TFloatAnimation works like this but I don't understand how.
The GExperts library (http://www.gexperts.org/) has a plug-in which can toggle the visibility
of non-visual components on a form, and it is apparently not Delphi-version-specific but it is
not exactly trivial.
The method which does this is
procedure THideNonVisualCompsExpert.ToggleNonVisualVisible(Form: TCustomForm);
const
NonVisualClassName = 'TContainer';
var
VisibleState: Boolean;
FormHandle: THandle;
CompHandle: THandle;
WindowClass: string;
FirstCompFound: Boolean;
WinControl: TWinControl;
ChildControl: TWinControl;
i: Integer;
begin
Assert(Assigned(Form));
Assert(Form.Handle > 0);
FirstCompFound := False;
WinControl := Form;
if InheritsFromClass(WinControl.ClassType, 'TWinControlForm') then
begin
for i := WinControl.ComponentCount - 1 downto 0 do
begin
if WinControl.Controls[i] is TWinControl then
begin
ChildControl := WinControl.Controls[i] as TWinControl;
if InheritsFromClass(ChildControl.ClassType, 'TCustomFrame') then
begin
WinControl := ChildControl;
Break;
end;
end;
end;
end;
FormHandle := GetWindow(WinControl.Handle, GW_CHILD);
CompHandle := GetWindow(FormHandle, GW_HWNDLAST);
VisibleState := False;
GxOtaClearSelectionOnCurrentForm;
while (CompHandle <> 0) do
begin
WindowClass := GetWindowClassName(CompHandle);
if AnsiSameText(WindowClass, NonVisualClassName) then
begin
if not FirstCompFound then
begin
VisibleState := not IsWindowVisible(CompHandle);
FirstCompFound := True;
end;
if VisibleState then
ShowWindow(CompHandle, SW_SHOW)
else
ShowWindow(CompHandle, SW_HIDE);
end;
CompHandle := GetWindow(CompHandle, GW_HWNDPREV);
end;
end;
in the unit GX_HideNonVisualComps.Pas.
As written, it toggles the visibility of all the non-visual components on the
target form, but looking at the code of the ToggleNonVisualVisible method it looks like it
ought to be possible (but I have not tried) to adapt it to operate on a selected component class and
force instances of the class to a non-visible state. Once you have done that, you would probably
need to experiment with how and when to invoke the method at design-time; if I was doing it, I would probably start
with somewhere like the target component's Loaded method.
(I would feel more comfortable posting this "answer" as a comment but obviously it would be too long)
I have thought about this. A Non Visual Component does not do any painting, in a Windows environment (like the IDE) it has no Window, and therefore cannot influence how the IDE chooses to render it.
One approach would be to derive from TWinControl, making your component a Visual Component, and then to ensure that it is not drawn. Try setting the positioning properties to be non-published, and when you are parented, always set your position outside the parent window. This means that your control is always clipped and never painted.
I haven't tried this, but I can see no reason why it wouldn't work.
You can also use this approach to have an apparently non visual component that renders information in the IDE at designtime, but not at runtime.
I have these controls TDateTimePicker, TComboBox, Tedit and TButton. TButton is disabled by default. What I would like to achieve is to enable TButton when all the other controls are filled or not null.
With the following codes, all the 3 controls starting with TDateTimePicker when filled I don't have any issues it works as expected.
The error comes when I fill TComboxBox followed by TEdit, it enables the TButton even TDateTimePicker is not filled yet. Or vise versa, I will fill TEdit followed by TComboBox, it enables the TButton.
From the codes below, I expect the TButton will not enable unless all the 3 controls are filled.
I've been trying to figure out (all day) how this error come to happen.
I will appreciate anyone there help me figure this out.
procedure TfrmHolidays.EnableSaveButton;
begin
if (edtHolidayName.Text <> NullAsStringValue) and (cmbHolidayType.ItemIndex <> -1)and (dtpHolidayDate.Date <> 0) then
begin
btnHolidaySave.Enabled := True;
end
else
begin
btnHolidaySave.Enabled := False;
end;
end;
procedure TfrmHolidays.dtpHolidayDateChange(Sender: TObject);
begin
EnableSaveButton;
end;
procedure TfrmHolidays.cmbHolidayTypeChange(Sender: TObject);
begin
EnableSaveButton;
end;
procedure TfrmHolidays.edtHolidayNameChange(Sender: TObject);
begin
EnableSaveButton; // triggers enable btnHolidaySave button
end;
By the way, I have more code related to making TDateTimePicker a blank and I supposed there's no issues with that. I also tried nesting within If Statement each condition and I am still getting the error. Further, I tested each condition at a time and It works fine.
Updates:
Here's how I initialized the dtpHolidayDate.Date:
procedure TfrmHolidays.FormCreate(Sender: TObject);
begin
DateTime_SetFormat(dtpHolidayDate.Handle, ' ');
FDTMDateEmpty := True;
end;
procedure TfrmHolidays.dtpHolidayDateCloseUp(Sender: TObject);
begin
DateTime_SetFormat(dtpHolidayDate.Handle, PChar('MMM dd yyyy (ddd)'));
end;
procedure TfrmHolidays.dtpHolidayDateChange(Sender: TObject);
begin
FDTMDateEmpty := False;
EnableSaveButton; // same and updated procedure above
end;
As pointed out in the comments above, you do not have an initialised value for the TDateTimePicker.
What you want is to have it default to a sensible date, rather than set to 0 - that is not at all helpful to users.
I would introduce a Boolean flag that you set yourself once the TDateTimePicker has been set.
You could set this flag in the OnChange event handler.
So something like:
interface
protected
blMyDTFlag: Boolean;
...
implementation
function TfrmHolidays.dtpHolidayDateChange(Sender: TObject);
begin
Self.blMyDTFlag:=True;
end
procedure TfrmHolidays.EnableSaveButton;
begin
if (edtHolidayName.Text <> '') and
(cmbHolidayType.ItemIndex <> -1) and
(Self.dtMyDTFlag) then
btnHolidaySave.Enabled := True
else
btnHolidaySave.Enabled := False;
end;
Although not shown in the question, I can guess that you initialize the date by
dtpHolidayDate.Date := 0;
After this, testing the date against 0 will (most likely) fail because the time portion still contains the time that the control is created.
You can initialize by
dtpHolidayDate.DateTime := 0;
then you can test the date as you do.
Alternatively you can use SameDate to do the comparison.
uses
dateutils;
if (edtHolidayName.Text <> NullAsStringValue) and (cmbHolidayType.ItemIndex <> -1)
and (not SameDate(dtpHolidayDate.Date, 0)) then
I have ComboBox4,ComboBox1 and Button5
When I click Button5 program should remove component selected in combobox4 from the ComboBox4 and ComboBox1 components' list. But I get list out of bounds error with the following code...
procedure TForm1.Button5Click(Sender: TObject);
var
cat : Integer;
trinti: TComponent;
catT : String;
begin
catT := ComboBox4.Text;
cat := ComboBox4.Items.IndexOf(catT);
trinti := ComboBox4.Components[cat];
ComboBox1.Items.BeginUpdate;
ComboBox4.Items.BeginUpdate;
ComboBox4.RemoveComponent(trinti);
ComboBox1.RemoveComponent(trinti);
ComboBox1.Items.EndUpdate;
ComboBox4.Items.EndUpdate;
removeCat(catT);
end;
Please help :(
The Components property, and the RemoveComponent method are the wrong things to use here. These are for ownership and lifetime management. Typically the only thing on your form that owns anything is the form itself. So using Components on the combo box will always results in an error.
Instead you need to use the Items property of the combo box, and its Delete method. It might look like this:
var
Index: Integer;
....
catT := ComboBox4.Text;
Index := ComboBox4.Items.IndexOf(catT);
if Index <> -1 then
ComboBox4.Items.Delete(Index);
i have a little problem. I'm trying to create in Delphi7 a list of components at run-time and to resize them with form's .OnResize event but no use... i can't figure out how to do it.
Here's my code:
procedure TForm1.Button1Click(Sender: TObject);
var
//ExtCtrls
panel: TPanel;
memo: TMemo;
splitter: TSplitter;
list: TListBox;
begin
panel := TPanel.Create(Self);
list := TListBox.Create(Self);
splitter := TSplitter.Create(Self);
memo := TMemo.Create(Self);
with panel do
begin
Parent := Form1;
BevelOuter := bvNone;
Top := 12;
Left := 12;
Height := Form1.Clientheight - 24;
Width := Form1.Clientwidth - 24;
end;
with list do
begin
Parent := panel;
Align := alClient;
Top := 0;
Height := panel.Height;
end;
with splitter do
begin
Parent := panel;
Top := 0;
Width := 12;
Align := alLeft;
end;
with memo do
begin
Parent := panel;
Top := 0;
Left := 0;
Width := round(panel.Width / 4) * 3;
Height := panel.Height;
Align := alLeft;
end;
end;
Do i have to somehow register their names in order to use them in form's event? Or maybe, to create a class and include them?
Any kind of help is really appreciated! Thank you in advance.
Your variables are local to the procedure where they are created so you can't refer to them using those variables when outside that procedure. The solution is to make them fields of the form class.
type
TForm1 = class(TForm)
procedure FormResize(Sender: TObject);
procedure Button1Click(Sender: TObject);
private
FPanel: TPanel;
FMemo: TMemo;
FSplitter: TSplitter;
FList: TListBox;
end;
Then your FormResize event handler can refer to them.
procedure TForm1.FormResize(Sender: TObject);
begin
if Assigned(FPanel) then
begin
...
end;
end;
Don't forget to remove the local variables from Button1Click and use the fields instead.
procedure TForm1.Button1Click(Sender: TObject);
begin
FPanel := TPanel.Create(Self);
...
end;
Although David's answer is also very correct, I thought I would take a moment and go into some more detail. By the looks of it, you seem to be very new with Delphi. There is a very common issue with beginners, which David doesn't address in his answer, pertaining to creating and freeing these objects. Any and every time you ever call 'Create' on a class, at some point, when you're done with it, you have to also 'Free' that class. Failure to free anything will result in a memory leak, and no one wants that. Freeing is just as simple as creating - until you get into the subject of keeping a list of objects (which you don't need right now).
Let's say you wanted to create a text box (TEdit) control and place it in the center of your form. Now first of all, the Delphi IDE allows you to simply drop these controls in your form, just making sure you know. You don't necessarily need to create/free them yourself, unless there's some special scenario. But doing this is dangerous. For the sake of this example, we're assuming that this TEdit control will be there for the entire duration of your application.
First, you need to declare a variable somewhere for this control. The most reasonable place for this is inside the class where it will be used (in this case, your form which we'll call Form1). When working with variables (aka Fields) in your form, make sure you do not put anything above the private section. Everything above private is intended for auto-generated code by Delphi for anything which has been dropped (and is visual) in your form. Otherwise, any manually created things must go under either private or under public. The public area would be a good place for your control...
type
TForm1 = class(TForm)
private
public
MyEdit: TEdit;
end;
Now that it's declared, we have to create (and free) it. It's a good practice that any and every time you ever create something, that you immediately put the code to also free it before you continue working. Make an event handler for your form's OnCreate and OnDestroy events...
procedure TForm1.FormCreate(Sender: TObject);
begin
MyEdit:= TMyEdit.Create(nil);
MyEdit.Parent:= Self;
MyEdit.Left:= (ClientWidth div 2) - (Width div 2);
MyEdit.Top:= (ClientHeight div 2) - (Height div 2);
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
if assigned(MyEdit) then MyEdit.Free;
end;
If this object is not created (before creation or after destruction), then you will get an "Access Violation" when trying to use it. This is because your application tries to access an area of the computer's memory which is not allocated or not matching with the type you meant to get.
Well, that's the basics to fix your scenario. However one more thing to show you. Suppose you need to just create an object for a short time, for the duration of a procedure. There's a different approach for this. In your code above, you declared your variable directly within the procedure. This example will show you when it is necessary to do this...
procedure TForm1.Button1Click(Sender: TObject);
var
MyObject: TMyObject;
begin
MyObject:= TMyObject.Create;
try
MyObject.DoSomething;
Caption:= MyObject.GetSomething;
finally
MyObject.Free;
end;
end;
You see, as long as MyObject will only be used in this one call to this procedure, then you can declare it here. But if the object is expected to stay in memory after this procedure is over and done with, then things get more complicated. Again, in your case, stick with putting this in the form's class until you're more familiar with dynamically creating objects.
A final note, as mentioned above, you do have the ability to place the TEdit control directly on your form in design-time without writing your own code. If you do this, you need to remember NOT to try to create or free these ones. This is also the case when Delphi will automatically put the code above the private section - is when there's something which you're not supposed to play with.
I don't think I am eligible to "comment", so I'm phrasing this as an "answer". If you want to resize your runtime components when their parent changes size, take a good look at the Anchors property. It can save you a lot of work.