Entries tagged with 'winforms'

Adding a horizontal scrollbar to a ComboBox using C#

In our WebCopy application we decided to update the User Agent configuration to allow selection from a predefined list of common agents, but still allow the user to enter their own custom agent if required.

Rather than use two separate fields, we choose to use a ComboBox in simple mode, which is both a textbox and a listbox in a single control. This mode seems somewhat out of fashion, I think the only place I see it used is in the Font common dialog, virtually unchanged since Windows 3.1.

The problem was immediately apparent however on firing up WebCopy and going to select a user agent - the agent strings can be very long, far longer than the width of the control.

Unfortunately however, the .NET ComboBox doesn't allow you to directly enable horizontal scrolling. So we'll do it the old fashioned way using the Windows API.

In order for a window to support horizontal scrolling, it needs to have the WS_HSCROLL style applied to it. And to setup the horizontal scrollbar, we need to call the SendMessage API with the CB_SETHORIZONTALEXTENT message.

As usual, we'll be starting off by creating a new Component, which we'll inherit from ComboBox.

Traditionally, you would call GetWindowLong and SetWindowLong API's with the GWL_STYLE or GWL_EXSTYLE flags. However, we can more simply override the CreateParams property of our component and set the new style when the control is created.

protected override CreateParams CreateParams
{
  get
  {
    CreateParams createParams;

    createParams = base.CreateParams;
    createParams.Style |= WS_HSCROLL;

    return createParams;
  }
}

With that done, we can now inform Windows of the size of the horizontal scroll area, and it will automatically add the scrollbar if required. To do this, I'll add two new methods to the component. The first will set the horizontal extent to a given value. The second will calculate the length of the longest piece of text in the control and then set the extent to match.

public void SetHorizontalExtent()
{
  int maxWith;

  maxWith = 0;
  foreach (object item in this.Items)
  {
    Size textSize;

    textSize = TextRenderer.MeasureText(item.ToString(), this.Font);
    if (textSize.Width > maxWith)
      maxWith = textSize.Width;
  }

  this.SetHorizontalExtent(maxWith);
}

public void SetHorizontalExtent(int width)
{
  SendMessage(this.Handle, CB_SETHORIZONTALEXTENT, new IntPtr(width), IntPtr.Zero);
}

The first overload of SetHorizontalExtent iterates through all the items in the control and uses the TextRenderer object to measure the size of the text. Once it has found the largest piece of text, it calls the second overload with the size.

The second overload does the actual work of notifying Windows using the SendMessage call, CB_SETHORIZONTALEXTENT message and the given width. SendMessage takes two configuration parameters per message, but CB_SETHORIZONTALEXTENT only requires one, and so we send 0 for the second.

The above function works with all display modes of the ComboBox.

For completeness, here are the API declarations we are using:

private const int WS_HSCROLL = 0x100000;
private const int CB_SETHORIZONTALEXTENT = 0x015E;

[DllImport("user32.dll")]
private static extern IntPtr SendMessage(IntPtr hWnd, UInt32 msg, IntPtr wParam, IntPtr lParam);

As usual, a demonstration project is available from the link below.

Downloads:

  • horizontallyscrollingcombobox.zip

    (11.88 KB | 13 July 2010 )

    Sample C# project showing how to add a horizontal scrollbar to a ComboBox in C# using the WS_HSCROLL style and CB_SETHORIZONTALEXTENT message.

3 comments | | Trackback specific URL for this entry

Creating a Windows Forms RadioButton that supports the double click event

Another of the peculiarities of Windows Forms is that the RadioButton control doesn't support double clicking. Granted, it is not often you require the functionality but it's a little odd it's not supported.

As an example, one of our earlier products which never made it to production uses a popup dialog to select a zoom level for a richtext box. Common zoom levels are provided via a list of radio buttons. Rather than the user having to first click a zoom level and then click the OK button, we wanted the user to be able to simply double click an option to have it selected and the dialog close.

However, once again with a simple bit of overriding magic we can enable this functionality.

Create a new component and paste in the code below (using and namespace statements omitted for clarity).

  public partial class RadioButton : System.Windows.Forms.RadioButton
  {
    public RadioButton()
    {
      InitializeComponent();

      this.SetStyle(ControlStyles.StandardClick | ControlStyles.StandardDoubleClick, true);
    }

    [EditorBrowsable(EditorBrowsableState.Always), Browsable(true)]
    public new event MouseEventHandler MouseDoubleClick;

    protected override void OnMouseDoubleClick(MouseEventArgs e)
    {
      base.OnMouseDoubleClick(e);

      // raise the event
      if (this.MouseDoubleClick != null)
        this.MouseDoubleClick(this, e);
    }
  }

This new component inherits from the standard RadioButton control and unlocks the functionality we need.

The first thing we do in the constructor is to modify the components ControlStyles to enable the StandardDoubleClick style. At the same time we also set the StandardClick style as the MSDN documentation states that StandardDoubleClick will be ignored if StandardClick is not set.

As you can't override an event, we declare a new version of the MouseDoubleClick event using the new keyword. To this new definition we add the EditorBrowsable and Browsable attributes so that the event appears in the IDE property inspectors and intellisense.

Finally, we override the OnMouseDoubleClick method and invoke the MouseDoubleClick event whenever this method is called.

And there we have it. Three short steps and we now have a radio button that you can double click.

Post a Comment | | Trackback specific URL for this entry

Creating a Windows Forms Label that wraps with C#

One of the few annoyances I occasionally get with C# is the lack of a word wrap facility for the standard Label control.

Instead, if the AutoSize property is set to True, the label will just get wider and wider. In order to wrap it, you have to disable auto resize then manually ensure the height of the label is sufficient.

The base Control class has method named GetPreferredSize which is overridden by derived classes. This method will calculate the size of a control based on a suggested value. By calling this method and overriding the OnTextChanged and OnResize methods, we can very easily create a custom label that automatically wraps and resizes itself vertically to fit its contents.

Paste in the following code into a new Component to have a read-to-run wrappable label.

using System;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;

namespace Cyotek.Windows.Forms
{
  public partial class WrapLabel : Label
  {
		#region  Public Constructors  

    public WrapLabel()
    {
      this.AutoSize = false;
    }

		#endregion  Public Constructors  

		#region  Protected Overridden Methods  

    protected override void OnResize(EventArgs e)
    {
      base.OnResize(e);

      this.FitToContents();
    }

    protected override void OnTextChanged(EventArgs e)
    {
      base.OnTextChanged(e);

      this.FitToContents();
    }

		#endregion  Protected Overridden Methods  

		#region  Protected Virtual Methods  

    protected virtual void FitToContents()
    {
      Size size;

      size = this.GetPreferredSize(new Size(this.Width, 0));

      this.Height = size.Height;
    }

		#endregion  Protected Virtual Methods  

		#region  Public Properties  

    [DefaultValue(false), Browsable(false), EditorBrowsable(EditorBrowsableState.Never), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public override bool AutoSize
    {
      get { return base.AutoSize; }
      set { base.AutoSize = value; }
    }

		#endregion  Public Properties  
  }
}

So, what is the code doing? It's very straightforward.

In the constructor, we are disabling the built in auto resize functionality, otherwise you won't be able to resize the control in the designer.

Next, we want to overide the OnTextChanged and OnResize methods to call our new resize functionality. By overriding these, we can ensure that the control will correctly resize as required.

Now to implement the actual resize functionality. The FitToContents method calls the label's GetPreferredSize method, passing in the width of the control. This method returns a Size structure which is large enough to hold the entire contents of the control. We take the Height of this (but not the width) and apply it to the label to make it resize vertically.

When calling GetPreferredSize, the size we passed in only had the width specified, which will be the maximum width returning. As we passed in zero for the height, the method defines its own maximum height.

Finally, you'll note that we have overridden the AutoSize property itself and added a number of attributes to it to make sure it doesn't appear in any property or code windows, and to prevent its value from being serialized.

3 comments | | Trackback specific URL for this entry

Creating a GroupBox containing an image and a custom display rectangle

One of our applications required a GroupBox which was more like the one featured in the Options dialog of Microsoft Outlook 2003. This article describes how to create a custom GroupBox component which allows this type of user interface, and also a neat trick on adjusting the client area so that when you drag controls inside the GroupBox, the handy little margin guides allow you to position without overlapping the icon.

Add a new Component class to your project, and inherit this from the standard GroupBox.

    [ToolboxItem(true)]
    [DefaultEvent("Click"), DefaultProperty("Text")]
    public partial class GroupBox : System.Windows.Forms.GroupBox

I personally don't like assigning variables at the same time as defining them, so I've added a default constructor to assign the defaults and also to set-up the component as we need to set a few ControlStyles.

    public GroupBox()
    {
      _iconMargin = new Size(0, 6);
      _lineColorBottom = SystemColors.ButtonHighlight;
      _lineColorTop = SystemColors.ButtonShadow;

      this.SetStyle(ControlStyles.DoubleBuffer | ControlStyles.AllPaintingInWmPaint | ControlStyles.ResizeRedraw |
                    ControlStyles.UserPaint | ControlStyles.SupportsTransparentBackColor, true);

      this.CreateResources();
    }

Although this is a simple component, we need at the minimum an Image property to specify the image. We're also adding color properties in case we decide to use the component in a non-standard interface later on.

    private Size _iconMargin;
    private Image _image;
    private Color _lineColorBottom;
    private Color _lineColorTop;
    
    [Category("Appearance"), DefaultValue(typeof(Size), "0, 6")]
    public Size IconMargin
    {
      get { return _iconMargin; }
      set
      {
        _iconMargin = value;
        this.Invalidate();
      }
    }

    [Category("Appearance"), DefaultValue(typeof(Image), "")]
    public Image Image
    {
      get { return _image; }
      set
      {
        _image = value;
        this.Invalidate();
      }
    }

    [Category("Appearance"), DefaultValue(typeof(Color), "ButtonHighlight")]
    public Color LineColorBottom
    {
      get { return _lineColorBottom; }
      set
      {
        _lineColorBottom = value;
        this.CreateResources();
        this.Invalidate();
      }
    }

    [Category("Appearance"), DefaultValue(typeof(Color), "ButtonShadow")]
    public Color LineColorTop
    {
      get { return _lineColorTop; }
      set
      {
        _lineColorTop = value;
        this.CreateResources();
        this.Invalidate();
      }
    }

    [DefaultValue("")]
    public override string Text
    {
      get { return base.Text; }
      set
      {
        base.Text = value;
        this.Invalidate();
      }
    }

If you wanted you could create and destroy required GDI objects every time the control is painted, but in this example I've opted to create them once for the lifetime of the control. Therefore I've added CreateResources and CleanUpResources to create and destroy these. Although not demonstrated in this in-line listing, CleanUpResources is also called from the components Dispose method. You'll also notice CreateResources is called whenever a property value changes, and that it first releases resources in use.

    private void CleanUpResources()
    {
      if (_topPen != null)
        _topPen.Dispose();

      if (_bottomPen != null)
        _bottomPen.Dispose();

      if (_textBrush != null)
        _textBrush.Dispose();
    }

    private void CreateResources()
    {
      this.CleanUpResources();

      _topPen = new Pen(_lineColorTop);
      _bottomPen = new Pen(_lineColorBottom);
      _textBrush = new SolidBrush(this.ForeColor);
    }

Now that all the initialization is performed, we're going to add our drawing routine which is to simply override the OnPaint method.

Remember that as we are overriding an existing component, we should override the base components methods whenever possible - this means overriding OnPaint and not hooking into the Paint event.

    protected override void OnPaint(PaintEventArgs e)
    {
      SizeF size;
      int y;

      size = e.Graphics.MeasureString(this.Text, this.Font);
      y = (int)(size.Height + 3) / 2;

      // draw the header text and line
      e.Graphics.DrawString(this.Text, this.Font, _textBrush, 1, 1);
      e.Graphics.DrawLine(_topPen, size.Width + 3, y, this.Width - 5, y);
      e.Graphics.DrawLine(_bottomPen, size.Width + 3, y + 1, this.Width - 5, y + 1);

      // draw the image
      if ((_image != null))
        e.Graphics.DrawImage(_image, this.Padding.Left + _iconMargin.Width, this.Padding.Top + (int)size.Height + _iconMargin.Height, _image.Width, _image.Height);

      //draw a designtime outline
      if (this.DesignMode)
      {
        Pen pen;
        pen = new Pen(SystemColors.ButtonShadow);
        pen.DashStyle = DashStyle.Dot;
        e.Graphics.DrawRectangle(pen, 0, 0, Width - 1, Height - 1);
        pen.Dispose();
      }
    }

In the code above you'll also notice a block specifically for design time. As this control only has borders at the top of the control, at design time it may not be obvious where the boundaries of the control are when laying out your interface. This code adds a dotted outline to the control at design time, and is ignored at runtime.

Another method we are overriding is OnSystemColorsChanged. As our default colors are based on system colors, should these change we need to recreate our objects and repaint the control.

    protected override void OnSystemColorsChanged(EventArgs e)
    {
      base.OnSystemColorsChanged(e);

      this.CreateResources();
      this.Invalidate();
    }

The client area of a standard group box accounts for the text header and the borders. Our component however, needs an additional offset on the left to account for the icon. If you try and place controls into the group box, you will see the snapping guides appear in the "wrong" place.

Fortunately however, it is very easy for us to suggest our own client area via the DisplayRectangle property. We just override this and provide a new rectangle which includes provisions for the width of the image.

    public override Rectangle DisplayRectangle
    {
      get
      {
        Size clientSize;
        int fontHeight;
        int imageSize;

        clientSize = base.ClientSize;
        fontHeight = this.Font.Height;

        if (_image != null)
          imageSize = _iconMargin.Width + _image.Width + 3;
        else
          imageSize = 0;

        return new Rectangle(3 + imageSize, fontHeight + 3, Math.Max(clientSize.Width - (imageSize + 6), 0), Math.Max((clientSize.Height - fontHeight) - 6, 0));
      }
    }

Now as you can see the snapping guides suggest a suitable left margin based on the current image width.

You can download the complete source for the GroupBox component below.

Downloads:

Post a Comment | | Trackback specific URL for this entry

Recent News

Recent Articles

Most Popular

Tags

Advertisments