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 the
DockingManager
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();
_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);
_dockingManager.Contents.Add(
new RichTextBox(), "Notepad",
_internalImages, _imageIndex);
}
Showing and Hiding Contents
Just adding a
Content
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);
_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 new
Content
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);
_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 the
Content
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;
_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. Each
Content
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);
_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 the
SampleDocking
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()
{
{
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;
...
}
Persistence
Many applications need to be able to remember several different docking
configurations of the
Content
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:-
_dockingManager.SaveConfigToFile(
"MyFileName.xml");
...
_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()
{
...
_manager.SaveCustomConfig +=
new DockingManager.SaveCustomConfigHandler
(OnSaveConfig);
_manager.LoadCustomConfig +=
new DockingManager.LoadCustomConfigHandler
(OnLoadConfig);
}
protected void OnSaveConfig(XmlTextWriter xmlOut)
{
xmlOut.WriteStartElement("MyCustomElement");
xmlOut.WriteAttributeString("ExampleData1",
"Hello");
xmlOut.WriteAttributeString("ExampleData2",
"World!");
xmlOut.WriteEndElement();
}
protected void OnLoadConfig(XmlTextReader xmlIn)
{
if (xmlIn.Name == "MyCustomElement")
{
string attr1 = xmlIn.GetAttribute(0);
string attr2 = xmlIn.GetAttribute(1);
xmlIn.Read();
}
}
Known problems
One of nice features of the docking windows is the ability to take a
Form
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