TTabSet vs. TTabControl vs. TPageCtrl/TTabSheet? - delphi

I was wondering why Delphi (2007) provides three widgets that seem to do the same thing, and what the advantages/disadvantages are for each.
On the same topic, if I want to display different sets of controls, why should I favor eg. PageControl + TabSheets + Frames, instead of just displaying different frames directly on the parent form?
Thank you.

From the helpfile on TTabSet:
Tab set controls are commonly used to
display tabbed pages within a dialog
box. TTabSet is provided for backward
compatibility. Use TTabControl
component in 32-bit Windows
applications.
So the real question is, what's the difference between TTabControl and TPageControl? The difference is that TTabControl only has one "page", whereas TPageControl has one page for each tab. This makes them useful in different situations.
TPageControl is useful for dialogs where you want to fit more UI on the screen than you have screen space to fit it in. Organize your UI into categories and put each category on one page. You see this pattern a lot in Options dialogs, for example.
TTabControl, on the other hand, works well for working on an array/list of objects. Create a UI to display and edit the properties of a single object, and place it on a TTabControl, then create one tab for each object and set up the event handlers so it'll load a new object from the array into the controls whenever you change tabs.
As for the frames question, the main reason to use a TPageControl in conjunction with frames is because it provides a prebuilt way to decide which frame to display. That way you don't have to reinvent a mechanism for it.

One method that I have used with great success is to use frames with a TPageControl and late bind my frames to the tPageControl the first time the page is selected. This keeps the form load time down, by not creating frames which are never viewed, but yet allows the flexibility of being created, the state is maintained when changing between tabs. Recently I switched to using forms and embedding them instead of frames...but the concept is the same.
The same can be done using a single "mount point" on a TTabControl and switching it out as the tab is changed, but then the issue of how to deal with the tab state as tabs are switched back too comes up.
[EDIT] The question comes up how do I handle communication between the frame and the parent form. This actually is very easy to do using interfaces. Just create a new unit that will be shared by the form AND the frame and add two interfaces:
type
IFormInterface = interface
[guid]
procedure FormProc;
end;
IFrameInterface = interface
[guid]
procedure SetFormController(Intf:IFormInterface);
end;
Have the form implement the IFormInterface, and the frame implement the IFrameInterface. When you click on a tab and show a frame, then run code like the following:
var
FrameIntf : IFrameInterface;
begin
if Supports(FrameObj,IFrameINterface,FrameIntf) then
FrameIntf.SetFormController(Self);
end;
your frame implementation of the SetFormController method would then hold onto the reference passed, which would allow it to call upwards into the form.
procedure TFrame1.SetFormController(Intf:IFormInterface);
begin
fFormController := Intf;
end;
Procedure TFrame1.Destroy; override;
begin
fFormController := nil; // release the reference
inherited;
end;
Procedure TFrame1.Button1Click(Sender:tObject);
begin
if fFormController <> nil then
fFormController.FormProc
else
Raise Exception.Create('Form controller not set');
end;

Related

Reassign an OnCellClick event

I have an SQLite database with many Tables and one is named "tblAccounts"
I have a dlgCommon that has a TDBGrid on it with the dbgridAccounts.DataSource:=srcAccounts
I have several other Dialogs all of which at some time need to click a button and show the Accounts Grid to select an Account from. Rather than have many Forms all with their own TDBgrid.DataSource:=srcAccounts I am doing this...
procedure TdlgFolders.btnAcctSelClick(Sender: TObject);
begin
dlgCommon.pnlAccounts.Parent:=Self;
dlgCommon.pnlAccounts.Left:=dbedAccount.Left;
dlgCommon.pnlAccounts.Top:=dbedAccount.Top+dbedAccount.Height+2;
dlgCommon.pnlAccounts.Width:=190;
end;
When the user has the dlgFolders active and clicks "btnAcctSel" it all does as I need and shows the Grid. But, when the user clicks the Grid-Cell I am at a loss where/how to put the dbgridAccountsCellClick(Column: TColumn); Handler.
I tried putting it in the dlgCommon and it compiles, but is not used as that is no longer the Parent when the Grid is visible and Cell-clicked in one of the other Dialogs.
I would prefer to keep using the single-Grid approach as the user gets to set the column widths, Row-colors etc and I'd rather not make them do that in every Form where the Accounts Grid is needed.
How can I reassign the dlgCommon.AccountsCellClick so that the click is trapped and used in dlgFolders and other Dialogs that call it too?
I'm not sure I folllow your structure and design, but I would place the grid that shows the accounts on a TFrame. This TFrame would hold all event handlers you need for the grid, in addition to the grid itself.
Then, whenever you need to show the grid, you instantiate the frame, assign its parent and the grid and event handlers are ready for use.
On a second and third reading, if dlgCommon is a form with a hierarchial structure like
dlgCommon: TdlgCommon
pnlAccounts: TPanel
AccountsGrid: TDBGrid
it appears that you have tried to "rip-out" (by changing the parent) the pnlAccounts from that form and then the event handlers don't work, just as you have noticed.
The idea of changing a components parent like this is a really bad idea, because when you assign a new parent to the grid, it will ofcourse not anymore show up in dlgCommon. It can be visible in only one dialog at a time.
If you want the grid simultaneously visible on various forms for (at least) some period of time, I would still use a TFrame as I already suggested.
In this case you can add an OnCellClick event manually to the forms private section
procedure DBGridCellClick(Column: TColumn);
and implement it in the form
procedure TForm1.DBGridCellClick(Column: TColumn);
begin
// whatever you want to do
end;
And you instantiate the frame as follows:
procedure TForm1.Button2Click(Sender: TObject);
begin
frame:= TFrame3.Create(self);
frame.Parent := self;
frame.Left := 8;
frame.Top := 75;
frame.DBGrid1.OnCellClick := DBGridCellClick;
end;
If, on the other hand, the user needs to see the grid only briefly, to select an account (and be done with it), I would simply show the dlgCommon modally.

Set DevExpress Grid Tableview Editable property and determine its parent via RTTI

I have a project with multiple TcxGrids on. The class hierarchy structure of the grids in my project are as follows:
TForm->TPanel->TcxPageControl->TcxTabSheet(subclassed)->TcxGrid->TcxGridLevel->TcxGridDBBandedTableView
In my subclassed TcxTabSheet I have added a new property "ReadOnly" that when set loops over the tabsheets controls and sets them to enabled/disabled respectively.
When a TcxGrid controls enabled property is set to true, the user can no longer interact with the grid at all (including navigating).
It seems I need to set the OptionsData.Editing property on the TcxGridDBBandedTableView to achieve a readonly but still navigable grid control.
Simple enough until you factor in that I want to do this in a loose coupled manner which I think leaves me the option of RTTI.
I have written the following code that loops over the forms controls (looping over the tabs controls or components doesn't give me access to the TcxGridDBBandedTableView). Once the control is found I can set its editing property via RTTI. I just don't seem to able to determine if that TcxGridDBBandedTableView belongs to the TabSheet it sits on.
var
compIdx: Integer;
begin
for compIdx := 0 to Pred(ComponentCount) do
if (Components[compIdx].ClassNameIs('TcxGridDBBandedTableView')) then
SetOrdProp(GetObjectProp(Components[compIdx], 'OptionsData'), 'Editing', Ord(not FReadOnly));
end;
TL;DR
How can I determine what pagectrl tab a cxgrid is on and set its TableView.OptionsData.Editable property without adding any devexpress units to the uses clause of the unit.
You are iterating over the components owned by the form. I think that's the wrong approach. You should be looking at the parent/child relationship rather than ownership. Not least because it's perfectly possible for a form to contain controls that it does not own. So, your approach can fail to find controls, particularly dynamically created controls.
So, if you have a tabsheet (or indeed any windowed control), you can iterate over its children like this:
for i := 0 to TabSheet.ControlCount-1 do
DoSomething(TabSheet.Controls[i]);
If your target grid control is a direct descendent of the tabsheet then this will be enough. If it is more than one level deep in the hierarchy then you will need a recursive solution. I will leave that recursive solution as an exercise for you.
Suppose that you have a control and want to find the tabsheet that it sits inside, then you need to walk up the parent chain. Like this:
function GetParentOfClass(Control: TControl; AClass: TWinControlClass): TWinControl;
var
Control: TWinControl;
begin
while Assigned(Control) and not (Control is AClass) do
Control := Control.Parent;
Result := TWinControl(Control);
end;

How to detach a panel and show it in a separate window?

Let's say I have form A that contains a panel (with many other controls in it) and a form B that it is empty.
Can I programmatically detach the panel from form A and move it in form B (and maybe back to form A)?
I know that I can change the Owner of the panel but does it work between different forms?
Update:
After some Googling I see that there is a ParentWindow property.
As noted by others, there are several problems with changing the parent window of a control without changing the ownership, and changing a controls owner can be difficult if it has multiple controls sitting on it...
One way around it is to use a frame instead. A frame owns all of it's sub-controls, so all you would need to do is change the owner and parent of the frame, and everything else will come along with it. This approach also allows you to keep all the event handlers and glue code in a single place as well.
N#
You have to take ownership into account, otherwise the destruction of form A would lead to the disappearance (i.e. destruction) of your panel on form B, or worse.
type
TForm2 = class(TForm)
public
InsertedPanel: TControl; // or TPanel
.
procedure RemoveComponents(AForm: TComponent; AControl: TWinControl);
var
I: Integer;
begin
for I := 0 to AControl.ControlCount - 1 do
begin
if AControl.Controls[I] is TWinControl then
RemoveComponents(AForm, TWinControl(AControl.Controls[I]));
if AControl.Controls[I].Owner = AForm then
AForm.RemoveComponent(AControl.Controls[I]);
end;
AForm.RemoveComponent(AControl);
end;
procedure TForm1.Button3Click(Sender: TObject);
begin
Form2.InsertedPanel := Panel1;
Panel1.Parent := nil;
RemoveComponents(Self, Panel1);
Form2.InsertComponent(Form2.InsertedPanel); // < this is not necessary
Form2.InsertedPanel.Parent := Form2; // as long as Parent is set
Panel1 := nil; // or if you free the panel
end; // manually
The extra reference may seem a bit silly: Form2.InsertedPanel and Panel1 point to the same object, but it's kind of semantically preferred. Maybe a central controlled variable is better.
Update:
I falsely assumed that RemoveComponent cascaded to the child controls on the panel. It doesn't, of course, so only removing the panel from form A would leave all the child controls of the panel still owned by form A. So I added the RemoveComponents routine to remove ownership from all the child controls of the panel.
Note that the child controls of the panel don't have an owner at this time. But since they are parented controls of the panel, destruction of the panel will free those controls. So be sure the panel has a parent, or free the panel explicitly.
All of the above only applies to a design time created panel, placed design time on the form, which was my assumption. Since this changing parents behaviour is apparently wanted or needed, you might want to consider to implement it completely at runtime. To keep the abbility to design the panel designtime, I suggest to create a Frame on which you can design that panel, and jump the Frame around your forms.
You can easily have something appear as if it was a panel, and also as a form, by really using a TForm for what you would have used the panel for. Then dock the form at runtime into the place where you have a blank panel left for that purpose, and undock it at runtime, by the same manner.
You can't undock a TPanel and have it appear as a top-level form window, but you can take a top level form window and dock it in code. To get the appearance and functionality you want you must use the correct tools (TForm, in this case).
Incidentally, component libraries like Toolbar 2000 do allow floating toolbar windows based on toolbar panels, so if you really insist on having all the designtim elements remain in one form, at desigtime, you should look into how it works in Toolbar 2000. It has a lot of code in there to render the toolbar in "undocked/floating" mode, and to handle the mouse-driven docking and undocking of toolbars into toolbar docks.
If the panel and child components are created at runtime, you can just set the Parent of the Panel to FormB:
Panel1.Parent := FormB;
Note that FormB has to have been created already before you can do this.
For more info, see the Delphi Wiki page here.

Delphi. How to shift Frames using TreeView?

Please help me with my question.
I have TreeView and Frames, how can I shift them if I click on an item of TreeView?
Is it better to use PageControl (PageControl1.Pages[i].TabVisible := false;) instead of Frames or Frames fit better?
Thank you very much!
To answer your first question "how to ... using a TreeView?" : Implement the OnChange event of the TreeView. The node parameter refers to the newly selected item.
About your second question "Should I use Frames or a PageControl?" : Well, one does not exclude the other and you perfectly can use both. Indeed, I advice to do so when you use the contents of such a TabPage multiple times. In those cases, place the Frame with Align = alClient on your TabPage.
Frames are usefull to design an arbitrary reusable container. For instance: you could set the same FrameType on every Page of the PageControl, assuming they all look the same but each working with different data.
Another possible minor advantage of using frames is not to get confused about all the controls on the TabPages.
But if every TabPage is unique in terms of visual style or control layout, then it's perfectly ok to not use frames and design the pages on the PageControl directly.
And about the shifting part: I don't exactly understand what you want to accomplish by setting the visibility of a tab, but shifting to another page (depending entirely on your implementation) based on the node could be as simple as:
procedure TForm1.TreeView1Change(Sender: TObject; Node: TTreeNode);
begin
PageControl1.ActivePageIndex := Node.Index;
end;

Adding Custom control to another custom control

What i am trying to accomplish is to create new touchkeyboard.
First i created buttons which i derive from speed buttons.
That done, now i need to create multiple buttons and layout them somewhere. This is were i get stuck.
I created a new component which i derive from TGraphicControl (this should be my new touchkeyboard), but i don't know how to add components to canvas. I actually don't know whether i'm supposed to add them to canvas or to some other component (eg. panel)!?!
Is my approach OK?
Thanks in advance.
If you're creating a custom visual control, you need to create the buttons and position them manually. For example:
TOnScreenKeyboard = class(TWinControl)
public
constructor Create(AOwner: TComponent);
end;
[...]
constructor TOnScreenKeyboard.Create(AOwner : TComponent)
var
TempButton : TSpeedButton;
begin
inherited;
TempButton := TSpeedButton.Create(self);
TempButton.Parent := self;
TempButton.Top := 10;
TempButton.Left := 15;
TempButton.Caption := 'A';
end;
You can put the button creation into a loop and position each one according to where it should be.
(I wrote this off the top of my head, and I don't write a lot of Pascal anymore, so there may be some minor mistakes! But it should get you started.)
Because of your wording and confusion between Panel, Canvas and custom controls in general, I assume you're a Delphi beginner. You need to learn about frames: embarcadero docwiki link on frames
Frames allow you to create re-usable portions of GUI. You use the IDE to "draw" the frame, you can then place that composite control (the frame) onto forms or other frames. It's a very powerful feature and it's conceptually very close to what other languages call "custom controls" (very close to what asp.net or WPF consider a custom control to be).
In the Delphi world, when you say "custom control", people would normally expect you to want to create an reusable control that's placed in a package and it's installed in the IDE. It's an fairly advanced subject. If that's what you want then I misunderstood the question, sorry.

Resources