The ColorGrid
control is a fairly useful control for selecting from a predefined list of colours. However, it can take up quite a bit of screen real estate depending on how many colours it contains. This article describes how you can host a ColorGrid
in a standard ToolStrip
control, providing access to both the ColorGrid
and the ColorPickerDialog
.
The ToolStrip
control makes this surprisingly easy to accomplish. First, we're going to need a component to host the ColorGrid
which we can ably achieve by inheriting from ToolStripDropDown
. So lets get started!
The Drop Down
The ToolStripDropDown
class "represents a control that allows the user to select a single item from a list that is displayed when the user clicks a ToolStripDropDownButton" and is just what we need to save use reinventing at least one wheel. This class will essentially manage the interactions to the ColorGrid
.
internal class ToolStripColorPickerDropDown : ToolStripDropDown { [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public ColorGrid Host { get; private set; } public ToolStripColorPickerDropDown() { this.Host = new ColorGrid { AutoSize = true, Columns = 10, Palette = ColorPalette.Office2010 }; this.Host.MouseClick += this.HostMouseClickHandler; this.Host.KeyDown += this.HostKeyDownHandler; this.Items.Add(new ToolStripControlHost(this.Host)); } }
When the ToolStripColorPickerDropDown
is created we automatically create a ColorGrid
control, set some default properties and then add it to the ToolStripItemCollection
of the ToolStripDropDown
.
If we simply bound the ColorChanged
event of the ColorGrid
to select a colour, then you'd probably have great difficulty in using the control properly - keyboard support is immediately out of the question, and even some mouse support would be affected.
For this reason, I'm binding the MouseClick
and KeyDown
events to allow for a nicer editing experience. I'll also add a Color
property so that I can track color independently of the ColorGrid
, to enable cancel support.
private void HostKeyDownHandler(object sender, KeyEventArgs e) { switch (e.KeyCode) { case Keys.Enter: this.Close(ToolStripDropDownCloseReason.Keyboard); this.Color = this.Host.Color; break; case Keys.Escape: this.Close(ToolStripDropDownCloseReason.Keyboard); break; } }
In the key handler, I'm closing the drop down if either the Enter or Escape keys are pressed. If it's the former, we update our true Color
property. If the latter, we don't. This way a user can cancel the drop down without updating anything.
private void HostMouseClickHandler(object sender, MouseEventArgs e) { ColorHitTestInfo info; info = this.Host.HitTest(e.Location); if (info.Index != ColorGrid.InvalidIndex) { this.Close(ToolStripDropDownCloseReason.ItemClicked); this.Color = info.Color; } }
The mouse handling is fairly similar, with the exception we don't cover a cancel case. If the user clicks outside the bounds of the drop down it will be automatically closed.
Here we do a hit test, and if a colour was clicked, we close the drop down and update the internal colour.
Notice that I close the drop down before setting the colour. This is deliberate, as originally I had it the other way around (as would seem more logical). The problem with that is that change events will be raised for the modified colour - but the drop down palette is still visible on the screen which I found a hindrance while debugging.
I also noted that when the drop down opened, the ColorGrid
did not have focus. That was easy enough to resolve by overriding OnOpened
.
protected override void OnOpened(EventArgs e) { base.OnOpened(e); this.Host.Focus(); }
Now that the drop down is handled, we need a new ToolStripItem
to interact with it.
A custom ToolStripSplitButton
For the actual button, I choose to inherit from ToolStripSplitButton
. This gives me two interactions, a drop down, and a button. We will display the ColorGrid
via the drop down, and the ColorPickerDialog
via the button, giving the user both a simple and an advanced way of choosing a colour.
[DefaultProperty("Color")] [DefaultEvent("ColorChanged")] [ToolStripItemDesignerAvailability(ToolStripItemDesignerAvailability.ToolStrip | ToolStripItemDesignerAvailability.StatusStrip)] public class ToolStripColorPickerSplitButton : ToolStripSplitButton { public ToolStripColorPickerSplitButton() { this.Color = Color.Black; } [Category("Data")] [DefaultValue(typeof(Color), "Black")] public virtual Color Color { get { return _color; } set { if (this.Color != value) { _color = value; this.OnColorChanged(EventArgs.Empty); } } } }
As with the ToolStripColorPickerDropDown
class, our new ToolStripColorPickerSplitButton
also has a dedicated colour property. The reason for this is I don't want to create the drop down component unless it's actually going to be used. After all, why waste resources creating objects we're not going to need?
The ToolStripSplitButton
class calls CreateDefaultDropDown
in order to set the DropDown
property if it doesn't have a value. We'll override this to create our custom drop down.
private ToolStripColorPickerDropDown _dropDown; protected override ToolStripDropDown CreateDefaultDropDown() { this.EnsureDropDownIsCreated(); return _dropDown; } private void EnsureDropDownIsCreated() { if (_dropDown == null) { _dropDown = new ToolStripColorPickerDropDown(); _dropDown.ColorChanged += this.DropDownColorChangedHandler; } }
In order to allow the developer to customise the ColorGrid
if required, we need to expose the control so they can access it.
[Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public ColorGrid Host { get { this.EnsureDropDownIsCreated(); return _dropDown.Host; } }
The Browsable
attribute prevents it from appearing in property grids, while DesignerSerializationVisibility
prevents the property from being serialized.
Both the Host
property and the CreateDefaultDropDown
method make use of the private EnsureDropDownIsCreated
method, so that the drop down is created on demand.
This means you can only customise the control from actual code (such as from your forms Load
event, not by setting properties on the designer.
ToolStrip Designer Support
As long as the ToolStripColorPickerSplitButton
is public
, the existing designers will automatically detect it and allow you to add them to your ToolStrip
or StatusStrip
controls. (Although interestingly it seems to automatically remove the "ToolStrip" prefix).
There is a caveat however - the ToolStripColorPickerSplitButton
class must be public
. Originally I had it as internal
(as it is part of a non-library project) but then it never showed up in designers.
If you display the drop down at design time, you'll find that you can continue to add items to the drop down underneath the hosted
ColorGrid
. I couldn't find a way to disable this, unless I created a new designer myself.
Displaying the ColorPickerDialog
Once the DropDown
property of a ToolStripSplitButton
has been set, it will take care of the details of showing it, so there's nothing more for us to do there. However, we do need to add some code to display the ColorPickerDialog
if a user clicks the main body of the button. This can be done by overriding OnButtonClick
.
protected override void OnButtonClick(EventArgs e) { base.OnButtonClick(e); using (ColorPickerDialog dialog = new ColorPickerDialog()) { dialog.Color = this.Color; if (dialog.ShowDialog(this.GetCurrentParent()) == DialogResult.OK) { this.Color = dialog.Color; } } }
Custom Painting
Typically, buttons which display an editor for a colour also display a preview of the active colour as a thick band underneath the buttons icon. Although the ToolStripSplitButton
makes this a little harder than it should, we can add this to our ToolStripColorPickerSplitButton
class by overriding the OnPaint
method.
The difficulty comes from the fact that the class doesn't give us access to its internal layout information, so we have to guess where the image is in order to draw our line. As there are quite a few display styles for these items, it can be a little tricky.
protected override void OnPaint(PaintEventArgs e) { Rectangle underline; base.OnPaint(e); underline = this.GetUnderlineRectangle(e.Graphics); using (Brush brush = new SolidBrush(this.Color)) { e.Graphics.FillRectangle(brush, underline); } } private Rectangle GetUnderlineRectangle(Graphics g) { int x; int y; int w; int h; // TODO: These are approximate values and may not work with different font sizes or image sizes etc h = 4; // static height! x = this.ContentRectangle.Left; y = this.ContentRectangle.Bottom - (h + 1); if (this.DisplayStyle == ToolStripItemDisplayStyle.ImageAndText && this.Image != null && !string.IsNullOrEmpty(this.Text)) { int innerHeight; innerHeight = this.Image.Height - h; // got both an image and some text to deal with w = this.Image.Width; y = this.ButtonBounds.Top + innerHeight + ((this.ButtonBounds.Height - this.Image.Height) / 2); switch (this.TextImageRelation) { case TextImageRelation.TextBeforeImage: x = this.ButtonBounds.Right - (w + this.ButtonBounds.Left + 2); break; case TextImageRelation.ImageAboveText: x = this.ButtonBounds.Left + ((this.ButtonBounds.Width - this.Image.Width) / 2); y = this.ButtonBounds.Top + innerHeight + 2; break; case TextImageRelation.TextAboveImage: x = this.ButtonBounds.Left + ((this.ButtonBounds.Width - this.Image.Width) / 2); y = this.ContentRectangle.Bottom - h; break; case TextImageRelation.Overlay: x = this.ButtonBounds.Left + ((this.ButtonBounds.Width - this.Image.Width) / 2); y = this.ButtonBounds.Top + innerHeight + ((this.ButtonBounds.Height - this.Image.Height) / 2); break; } } else if (this.DisplayStyle == ToolStripItemDisplayStyle.Image && this.Image != null) { // just the image w = this.Image.Width; } else if (this.DisplayStyle == ToolStripItemDisplayStyle.Text && !string.IsNullOrEmpty(this.Text)) { // just the text w = TextRenderer.MeasureText(g, this.Text, this.Font).Width; } else { // who knows, use what we have // TODO: ButtonBounds (and SplitterBounds for that matter) seem to return the wrong // values when painting first occurs, so the line is too narrow until after you // hover the mouse over the button w = this.ButtonBounds.Width - (this.ContentRectangle.Left * 2); } return new Rectangle(x, y, w, h); }
The GetUnderlineRectangle
method show above does a decent job of guessing where the image should be and should work without much in the way of tinkering.
If you are drawing a custom underline, you should make sure the bottom four pixels of your image are blank, as any details in these will be covered over by the image.
Downloading the full source
The full source code can be found in the demonstration program for the ColorPicker controls on GitHub. Just add the ToolStripColorPickerDropDown.cs
and ToolStripColorPickerSplitButton.cs
files to your project and you should be good to go!
All content Copyright (c) by Cyotek Ltd or its respective writers. Permission to reproduce news and web log entries and other RSS feed content in unmodified form without notice is granted provided they are not used to endorse or promote any products or opinions (other than what was expressed by the author) and without taking them out of context. Written permission from the copyright owner must be obtained for everything else.
Original URL of this content is https://www.cyotek.com/blog/hosting-a-colorgrid-control-in-a-toolstrip?source=rss.