Magic Docking - VS.NET Style






4.94/5 (149 votes)
Sep 30, 2002
10 min read

985376

42552
Docking Windows mimicking VS.NET feature
What are Docking Windows?
One of the first things you notice when using Visual Studio .NET is the clever docking windows implementation. This allows the user to reposition various tool windows such as the Solution Explorer and Properties to dock against different application edges. You can even make them float or become tabbed within the same docking window. The Magic Library provides an implementation that mimics this clever behaviour and allows you to quickly and easily add the same feature to your own applications.Downloads
The first download Docking Sample contains a example application that uses the docking windows implementation from the Magic Library. This allows you to experiment and test out the feature. The actual source code is inside the separate second download. At nearly 1MB in size, I thought people would prefer to download the sample before deciding if they want to install the entire source code project!How do I add docking windows to my own application?
First you need to create an instance of theDockingManager
class to be
and associate it with each ContainerControl
derived object you want to
have a docking capability. Most of the time this will be your applications
top-level application Form
.
As well as a ContainerControl
reference the constructor takes a
parameter indicating the visual style required. Currently two display styles
are supported, VisualStyle.IDE
for the Visual Studio .NET appearance and VisualStyle.Plain
for the older Visual C++ Version 6 appearance.
The following code shows how to add docking support to a Form
using Crownwood.Magic.Common;
using Crownwood.Magic.Docking;
public class MyForm : Form
{
protected Crownwood.Magic.Docking.
DockingManager _dockingManager = null;
public MyForm()
{
InitializeComponent();
// Create the object that manages the
// docking state
_dockingManager = new DockingManager(
this, VisualStyle.IDE);
}
}
Now we need to provide the docking manager with descriptions of each dockable
unit. This is the purpose of the Content
class. Each Content
object
creates an association between a title, image and Control
derived
object. You should read the full documentation of this important class which
can be found in the full download.
The following code shows how to create a Content
instance and add it to
the docking manager inside the form constructor. It creates a RichTextBox
control that will act as a dockable notepad for use by the user.
public MyForm()
{
InitializeComponent();
_dockingManager = new DockingManager(this,
VisualStyle.IDE);
Content notePad =new Content(_dockingManager);
notePad.Title = "Notepad";
notePad.Control = new RichTextBox();
notePad.ImageList = _internalImages;
notePad.ImageIndex = _imageIndex;
_dockingManager.Contents.Add(notePad);
}
As this is such a common operation the process has been streamlined. There are
several overrides of the Contents.Add
method that will create the
required Content
instance for you during the Add
process. Here is
the recommended approach.
public MyForm()
{
InitializeComponent();
_dockingManager = new DockingManager(this,
VisualStyle.IDE);
// This will create and add a new Content
// object all in one operation
_dockingManager.Contents.Add(
new RichTextBox(), "Notepad",
_internalImages, _imageIndex);
}
Showing and Hiding Contents
Just adding aContent
instance will not make it visible to the user. We
want our application to make this instance visible immediately, so we use the ShowContent
method as shown below: -
public MyForm()
{
InitializeComponent();
_dockingManager = new DockingManager(this,
VisualStyle.IDE);
_dockingManager.Contents.Add(new RichTextBox(),
"Notepad",
_internalImages,
_imageIndex);
// Make the content with title 'Notepad'
// visible
_dockingManager.ShowContent(
_dockingManager.Contents["Notepad"]);
}
This shows how to find a reference to a Content
object by using the
string indexer of the Contents
collection. In this particular case it is
a little inefficient as we could have stored the Content
reference that
is returned from the call to Contents.Add
. Here is a more efficient
example: -
public MyForm()
{
InitializeComponent();
_dockingManager = new DockingManager(this,
VisualStyle.IDE);
Content notePad = _dockingManager.
Contents.Add(new RichTextBox(),
"Notepad", _internalImages,
_imageIndex);
_dockingManager.ShowContent(notePad);
}
At some point in the future you may want to hide this instance again in which
case you can use the HideContent
method. To make all the Content
instances
visible or invisible use the ShowAllContent
and HideAllContent
methods
respectively.
Accurate Creation
Three lines of code and we have a docking window made visible to the user which can be redocked and resized. However, at no point so far have we specified exactly where the newContent
gets shown. The docking position for a Content
made visible is the saved position from when it was last hidden. In our case
the instance has never been hidden because it has just been created.
The constructor for the Content
will default the saved docking position
to be the left edge. Therefore our last example above will display the content
inside a docking window which is docked against the left edge of the
application Form
. The value of the Content.DisplaySize
will be
used to decide how wide the docking window should be, this defaults to 150, 150
.
If you want to dock against a different edge or even begin in the floating
state then you need to do a little more work. The following code shows the use
of the AddContentWithState
method to show the content with a defined
initial state: -
public MyForm()
{
InitializeComponent();
_dockingManager = new DockingManager(this,
VisualStyle.IDE);
Content notePad =
_dockingManager.Contents.Add(
new RichTextBox(), "Notepad",
_internalImages, _imageIndex);
// Request new Docking window be created and
// displayed on the right edge
_dockingManager.AddContentWithState(notePad,
State.DockRight);
}
Create in same window
Using the above method allows a docking window to be made visible and its position defined. But it does have the drawback that it will always create a new docking window to host theContent
instance. What if we want two or
more Content
objects to be hosted inside the same docking window? To
achieve this we need to bring another method called AddContentToWindowContent
into use.
Each content is always hosted inside a WindowContent
derived object. We
can remember the reference of the newly created WindowContent
object and
reuse it as the destination for other Content
instances. The following
example creates notePad
instances that are placed inside the same
docking window, when this happens the docking window will adopt a tabbed
appearance: -
public MyForm()
{
InitializeComponent();
_dockingManager = new DockingManager(this,
VisualStyle.IDE);
Content notePad1 =
_dockingManager.Contents.Add(
new RichTextBox(), "Notepad1",
_internalImages, _imageIndex);
Content notePad2 =
_dockingManager.Contents.Add(
new RichTextBox(), "Notepad2",
_internalImages, _imageIndex);
WindowContent wc =
_dockingManager.AddContentWithState(
notePad1, State.DockRight)
as WindowContent;
// Add the second notePad2 to the same
// WindowContent
_dockingManager.AddContentToWindowContent(
notePad2, wc);
}
Create in same Column/Row
There is only one more ability we need to add so that any docking configuration can be constructed at start-up. We need the ability to place docking windows in the same column or row. To do this we have to understand more about the actual structure of objects maintained by the docking code. Docking is supported by providing three levels of object. EachContent
object
exists inside a Window
derived object which itself exists inside a Zone
derived object. The WindowContent
class is a specialization of the Window
base class that has special knowledge about how to handle Content objects.
It is easiest to explain by providing some examples.
The AddContentWithState
method creates a new WindowContent
instance
and adds to it the provided Content
parameter. Next a Zone
is
created and the WindowContent
instance placed inside it. The Zone
is then added to the hosting Form
and positioned according to the State
parameter.
The AddContentToWindowContent
adds the provided Content
parameter
to the existing WindowContent
instance.
The AddContentToZone
method creates a new WindowContent
instance
and adds to it the provided Content
parameter. It then adds the new WindowContent
to the provided Zone
in the correct relative position.
The following example shows how to create three notePad
objects, where
the first two are added to the same WindowContent
causing a tabbed
appearance to occur. The final notePad
is created in its own WindowContent
and then added to the same Zone
. The position value of 0 will
make the second WindowContent
be positioned first in the Zone
.
public MyForm()
{
InitializeComponent();
_dockingManager = new DockingManager(this,
VisualStyle.IDE);
Content notePad1 =
_dockingManager.Contents.Add(
new RichTextBox(), "Notepad1",
_internalImages, _imageIndex);
Content notePad2 =
_dockingManager.Contents.Add(
new RichTextBox(), "Notepad2",
_internalImages, _imageIndex);
Content notePad3 =
_dockingManager.Contents.Add(
new RichTextBox(), "Notepad3",
_internalImages, _imageIndex);
WindowContent wc =
_dockingManager.AddContentWithState(
notePad1, State.DockRight)
as WindowContent;
_dockingManager.AddContentToWindowContent(
notePad2, wc);
// Add a new WindowContent to the existing
// Zone already created
_dockingManager.AddContentToZone(notePad3,
wc.ParentZone, 0);
}
You can use the Content.ParentWindowContent
property to discover which WindowContent
a Content
instance is currently placed inside. Likewise, the WindowContent.ParentZone
property indicates the Zone
a WindowContent
instance is inside.
Using these and the above-described methods should allow any start up
configuration to be constructed. Note that the Content.ParentWindowContent
property returns null
if the Content
is not currently visible.
Control where docking can occur
If you use theSampleDocking
application you will notice that you cannot
redock a docking window between a Form
edge and either the MenuControl
or StatusBar
controls. In order to achieve this effect we need to define
a couple of the docking manager properties.
The OuterControl
property needs to be set to the first control in the Forms.Control
collection that represents the group of controls that the manager must not dock
between. Remember that the order of controls in the Form.Control
collection
determines the sizing and positioning of them. So the last control in the
collection is the one that is positioned and sized first, the second to last
control will be positioned and sized in the space that remains.
As the MenuControl
is the most important and needs to be positioned
first it will be last in the collection. The StatusBar
is the next most
important and so is second to last in the collection. In this scenario the OuterControl
would be set to a reference of the StatusBar
control. This will prevent
the docking manager from reordering any window after the StatusBar
in
the collection. If the StatusBar
was last in the list and the MenuControl
second to last then the OuterControl
would need to reference the MenuControl
instead.
The InnerControl
property needs to be set to the last control in the Forms.Control
collection that represents the group of controls that the manager must not dock
after. This might seem odd, as there is unlikely to be a docked window you
would not want the docking windows to be placed inside of. However there is a
situation where this becomes important.
If you have a control that is defined as having the Dock
property of DockStyle.Fill
then this control must always occur in the Form.Control
collection
before any docking windows. Otherwise you can get the situation where the
control with the DockStyle.Fill
value is not sized according to the
actual space left over when all docking windows have been placed. Because this
control is further up the list of controls it calculates its size without
taking into account any docking windows that occur earlier in the collection.
The following shows a MenuControl
, StatusBar
and a RichTextBox
being created and added to the Form.Control
collection. It then sets the
correct InnerControl
and OuterControl
values to generate the
expected runtime operation.
public MyFormConstructor()
{
// This block would normally occur inside
// a call to:-
// InitializeComponent();
{
RichTextBox filler = new RichTextBox();
filler.Dock = DockStyle.Fill;
Controls.Add(filler);
StatusBar status = new StatusBar();
status.Dock = DockStyle.Bottom;
Controls.Add(status);
MenuControl menu = new MenuControl();
menu.Dock = DockStyle.Top;
Controls.Add(menu);
}
_dockingManager = new DockingManager(this,
VisualStyle.IDE);
_dockingManager.InnerControl = filler;
_dockingManager.OuterControl = status;
// Now create and setup my Content objects
...
}
Persistence
Many applications need to be able to remember several different docking configurations of theContent
objects and be able to switch between them
at runtime. You might also want to save the configuration when the application
is shutdown so that it can be restored at start-up. The code to save and load
configurations is as follows:-
// Save the current configuration to a named file
_dockingManager.SaveConfigToFile(
"MyFileName.xml");
...
// Load a saved configuration and apply
// immediately
_dockingManager.LoadConfigFromFile(
"MyFileName.xml");
There are a couple of issues to remember though. The saving process does not
save the actual Content
objects but just the state information it needs
in order to restore that Content
to the same docking size/position
later. So the Content
object must already exist and be part of the
docking manager when the load operation takes place because loading will not
recreate those objects.
The second point is that the save and load use the Title
property of the Content
to identify the information. If you change the Title
of a Content
object between saving and loading then the latter process will fail to
associate the saved information to the object. This will not cause an exception
but that Content
will not be updated with the required configuration.
If you need to save the configuration information into a database or simply
save it internally then you do not have to save into a file. There are matching
methods for saving and loading into byte arrays which are easy to store within
your application or to a database. For even greater control use the methods
that take a stream object instance, in which case you must create and provide
the stream object instance, but this gives the developer complete control over
the storage medium.
Some developers might find it useful to save and load some additional custom
details inside the configuration data. This prevents the need for two sets of
saved data which then need to be maintained in parallel. The SaveCustomConfig
event is generated when all the docking information has been written and allows
you to add additional information at the end.
On loading the LoadCustomConfig
is event is generated so that the
corresponding information can be read back in again. The following sample code
shows a trivial example of this:-
public void InitialiseComponent()
{
...
// Setup custom config handling at appropriate
// place in code
_manager.SaveCustomConfig +=
new DockingManager.SaveCustomConfigHandler
(OnSaveConfig);
_manager.LoadCustomConfig +=
new DockingManager.LoadCustomConfigHandler
(OnLoadConfig);
}
protected void OnSaveConfig(XmlTextWriter xmlOut)
{
// Add an extra node into the config to store
// some example information
xmlOut.WriteStartElement("MyCustomElement");
xmlOut.WriteAttributeString("ExampleData1",
"Hello");
xmlOut.WriteAttributeString("ExampleData2",
"World!");
xmlOut.WriteEndElement();
}
protected void OnLoadConfig(XmlTextReader xmlIn)
{
// We are expecting our custom element to be
// the current one
if (xmlIn.Name == "MyCustomElement")
{
// Read in both the expected attributes
string attr1 = xmlIn.GetAttribute(0);
string attr2 = xmlIn.GetAttribute(1);
// Must move past our element
xmlIn.Read();
}
}
Known problems
One of nice features of the docking windows is the ability to take aForm
derived class and use it as the content of a docking window. You need to be
careful though, as sometimes a Form
you generate will have the AutoScaleBaseSize
property define. This can cause the Form
instance to be sized
incorrectly when it is shown for the first time. If someone comes up with an
answer to this problem then please let me know!
A second problem is the use of the RichTextBox
. If you use this as the
content for the docking window and then move the docking window to a different
edge it will sometimes cause the control to be recreated. In this case it loses
any coloring information. So your blue/red text inside the RichTextBox
suddenly
becomes the default black. Again, if anyway knows how to fix this issue then
please let me know.
Revision History
20 Sept 2002 - Initial revision