Search code examples
delphidelphi-7sptbxlib

TSpTbxDock vs TSpTbxMultiDock


In my current project I have a TTbxDock with toolbars and panels (TTbxDockablePanel). After moving TBX-->SpTBX I can't place a panel (TSpTbxDockablePanel) on a TSpTbxDock. I see a runtime error message saying I can't place a panel on a dock.

Error, 'Cannot insert TSpTBXDockablePanel into TSpTBXDock.'

  • Do I need to use a TSpTbxMultiDock instead for panels? Why?
  • Can I place toolbars on a TSpTbxMultiDock instead of a TSpTBXDock?

i.e. why such code is written:

procedure TSpTBXCustomMultiDock.ValidateInsert(AComponent: TComponent);
begin
  inherited;
  if not (AComponent is TSpTBXCustomDockablePanel) then
    raise EInvalidOperation.CreateFmt('Cannot insert %s into MultiDock', [AComponent.ClassName]);
end;

procedure TSpTBXCustomDockablePanel.ValidateContainer(AComponent: TComponent);
begin
  inherited;
  if (AComponent is TTBDock) and not (AComponent is TSpTBXCustomMultiDock) then
    raise EInvalidOperation.CreateFmt('Cannot insert %s into %s. Place it on a MultiDock instead', [Self.ClassName, AComponent.ClassName]);
end;

Why can't I just use one type of dock?


Solution

  • "I need to use TSpTbxMultiDock instead for panels? Why?"

    The short answer is, because the TSpTBXMultiDock component is written to handle and dock only TSpTBXDockablePanels.

    The long answer is that the TSpTBXMultiDock component is written to handle and dock only TSpTBXDockablePanels because the docking code relies on specific methods / properties in the controls it is docking. See TSpTBXCustomDockablePanel.SetParent, which looks after this: it accesses the properties and methods TSpTBXCustomMultiDock.

    • UpdateDockablePanelsDockPos
    • ClientAreaWidth
    • ClientAreaHeight

    In other words, the docking and layout code needs to know certain information and ask the docked controls to do certain things. The easiest way to do this is to constrain the type of the controls it can dock to descendants of a specific class which declares and implements the required interface (using interface loosely, there is no (class) interface declared, it's just an informal set of methods / properties.)

    From scanning the code quickly, I think only the first of these, UpdateDockablePanelsDockPos, is really essential. I may have missed something. But this method gets a list of all docked panels and updates each one's DockPos, which is the one-dimensional position of the dock in the panel. That is, for a horizontal panel it is the left / horizontal start, and for a vertical panel it is the top / vertical start. It also updates the total so the panel knows how big is has to be, or can be if it chooses.

    I'm not quite sure I've answered your question. I feel I've given a correct answer but not a helpful answer (the above are technical reasons, not conceptual reasons), which requires insight into what you're doing. My guess is you are asking this because you're struggling with a TBX -> SpTBX migration and you want docking areas which can handle both toolbars and dockable panels. Luckily, both these lead to your second question...

    "Can I place toolbars on a TSpTbxMultiDock?"

    I'm stepping into remembered territory here and I can't guarantee that this part of the answer is correct, because I converted our app from TBX to SpTBX a long, long time ago.

    First of all, no, you cannot place a TSpTBXToolbar on a dockable panel. Nor can you dock a TSpTBXDockablePanel on a TSpTBXDock. A toolbar can dock into a dock, and a dockable panel can dock into a multidock.

    The reason for this is guesswork, but I would guess it's because of the different behaviour of the two docked controls and their purpose.

    Toolbars:

    • Are meant to contain SpTBX items (buttons, dropdown, etc, not any normal VCL controls, buttons, etc) and can dynamically change from being toolbars to menus etc, eg as the form is resized and the toolbar shrinks.
    • Stack: as well as having any amount of space between them (unlike docked panels, which always sit adjacent to each other) toolbars can have space between them; they sit whereever the user put them. They also often sit in several rows, and the one dock expands and contains several rows.
    • Are designed to shrink: a form can get too small or a user can drag a toolbar over another, and a toolbar will show only the items it can, and either hide the others or show a chevron (a tiny >> button to open a menu containing the other items.)

    Dockable panels:

    • Are meant to hold complex controls, including standard VCL controls - they're meant as a toolbar and docking system-compatible way to build complex docked forms. (We design ours as frames and client-align a frame to the dockable panel.)
    • Rarely are on several rows. This is up to you as the UI implementer, but many panels docked next to each other could rapidly fill up form space. We keep LimitToOneRow turned on for dockable panels, so they they can only fill up one dimension (ie, on the left and right side of the form, dockable panels can only sit above and below each other, not to the left and right of each other.)
    • Resizing behaviour is different: they are always adjacent with no gap in between (toolbars can be positioned anywhere) and usually stretch one or more of the panel to fill the whole size of the dock (toolbars don't, unless MenuBar is set (see below)) and in fact toolbars are designed to shrink, not grow, and show a chevron and a drop-down menu. Panels don't have that behaviour.

    After that diversion, back to your question. When we were converting our app I remember wondering what would happen if a user wanted to create a layout such that there were toolbars, then a dockable panel, and then toolbars again, in order to get as close as possible to mixing the two types. Doing so would require a dock, then a multidock, and then a dock again, but in practice this actually leads to users being able to create quite complex and confusing layouts. One problem with dockable UIs is that many users find them confusing (honestly, you'd be surprised), and can accidentally move or dock items where they don't want them. (It's good practice to contrain the menu bar to the top, for example - don't allow that one to be moved. It's just confusing.)

    Our current design is that each side of our main window has two docks: one normal dock and then next to that that (closer to the center of the form) a multidock. The resulting behaviour is that all toolbars, such as menus and normal toolbars, can only dock next to the edge of the window. Dockable panels are then closer to the inside of the form than that. Keeping pairs of docks and multidocks next to each other on each form edge lets both types of controls be docked at any side of the form, but constrains toolbars to always be on the very edge of the form. This makes UI sense, actually: dockable panels tend to contain more complex things inside them than a toolbar, and it follows that they are more central controls. Of course, if one particular edge of the form has only a dockable panel on it, not a dockable toolbar, then the toolbar dock is invisible (width or height of 0) and so the panel sits next to the edge of the window too.

    My recommended SpTBX dock layout

    Written as text, it's hard to explain the previous two paragraphs (how this scheme works, and why this is okay / good.) You need to try it in practice and have a sense for user's behaviour. But basically, I recommend having both types of docks on your form, and keep the TSpTBXMultiDocks aligned 'inside' or 'inner to' the normal toolbar docks. Hopefully this image helps.

    Other

    You might be interested in the following properties:

    • DockableTo (toolbar and dockable panel): constrains which sides (top, bottom, left, right) a toolbar or panel can dock. Use this to make some controls only able to be docked in certain specific docks - a main menu to only dock on the top, for example.
    • DefaultDock (again, both): when users double-click to dock, rather than drag-docking, where does it go?
    • CurrentDock: I think you've already found this, but set it in the designer to move a toolbar or panel between docks, and read at runtime (or, I suppose, set at runtime if you programatically want to move things to specific docks.)
    • DockMode: controls whether a toolbar or panel can float (be undocked and be its own window), or not, including whether it can change docks. An example of using this is the main-menu constraint again.
    • FixedDockSize (panels only): stops the panel being resizable, either by the user or by the docking code referred to above.
    • MenuBar (toolbars only): gives the toolbar slightly different behaviour, I think that it always takes up the full width of the dock and doesn't allow other toolbars to be docked next to it. It may also change how items render slightly.