Sourcecode - ControlRow

Download ControlRow.cs

// Copyright (C) 2004  Daniel Grunwald
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// For more information read the file "LICENSE.txt".

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

namespace Grunwald.Gui.Controls
{
	/// <summary>
	/// The ControlRow automatically aligns all it's children in a row or column.
	/// </summary>
	public class ControlRow : Panel
	{
		#region Constructor
		/// <summary>
		/// Creates a new ControlRow.
		/// </summary>
		public ControlRow()
		{
			AutoScroll = true;
		}
		#endregion
		
		#region Properties
		bool arrangeActive = false;
		
		/// <summary>
		/// Gets/Sets the if the arranging is active.
		/// </summary>
		/// <exception cref="InvokeRequiredException">This method is called from an other thread than the thread that created this control.</exception>
		/// <exception cref="ObjectDisposedException">This control was already disposed.</exception>
		[Bindable(true), Category("Layout"), DefaultValue(false),
		 Description("Specifies if the automatic arranging is active. You should use ActivateArrange() when you use the form designer!")]
		public bool ArrangeActive {
			get { return arrangeActive; }
			set {
				if (arrangeActive == value) return;
				arrangeActive = value;
				if (arrangeActive) ReArrange();
			}
		}
		
		bool autoSize = false;
		
		/// <summary>
		/// Gets/Sets if the panel should automatically adjust its own size to the size its
		/// children need.
		/// </summary>
		/// <exception cref="InvokeRequiredException">This method is called from an other thread than the thread that created this control.</exception>
		/// <exception cref="ObjectDisposedException">This control was already disposed.</exception>
		[Bindable(true), Category("Layout"), DefaultValue(false),
		 Description("Specifies if the panel should automatically adjust its own size to the size its children need.")]
		public bool AutoSize {
			get { return autoSize; }
			set {
				if (autoSize == value) return;
				autoSize = value;
				if (autoSize) ReArrange();
			}
		}
		
		bool fullWidth;
		
		/// <summary>
		/// Gets/Sets if the children of this panel should use the full width/height available.
		/// </summary>
		/// <exception cref="InvokeRequiredException">This method is called from an other thread than the thread that created this control.</exception>
		/// <exception cref="ObjectDisposedException">This control was already disposed.</exception>
		[Bindable(true), Category("Layout"), DefaultValue(false),
		 Description("Specifies if the children of this panel should use the full width/height available.")]
		public bool FullWidth {
			get { return fullWidth; }
			set {
				if (fullWidth == value) return;
				fullWidth = value;
				ReArrange();
			}
		}
		
		Direction direction = Direction.TopDown;
		
		/// <summary>
		/// Gets/Sets the <see cref="ArrangeDirection">Direction</see> used to arrange the
		/// child controls.
		/// </summary>
		/// <exception cref="InvalidEnumArgumentException">
		/// Value is not a member of <see cref="Direction"/>.
		/// </exception>
		/// <exception cref="InvokeRequiredException">This method is called from an other thread than the thread that created this control.</exception>
		/// <exception cref="ObjectDisposedException">This control was already disposed.</exception>
		[Bindable(true), Category("Layout"), DefaultValue(Direction.TopDown),
		 Description("Specifies the direction used to arrange the child controls.")]
		public Direction Direction {
			get { return direction; }
			set {
				if (direction == value) return;
				if (!Enum.IsDefined(typeof(Direction), value))
					throw new InvalidEnumArgumentException();
				direction = value;
				ReArrange();
			}
		}
		
		ContentAlignment contentAlignment = ContentAlignment.TopCenter;
		
		/// <summary>
		/// Gets/Sets the <see cref="ContentAlignment"/> used to arrange the
		/// child controls.
		/// </summary>
		/// <exception cref="InvalidEnumArgumentException">
		/// Value is not a member of <see cref="ContentAlignment"/>.
		/// </exception>
		/// <exception cref="InvokeRequiredException">This method is called from an other thread than the thread that created this control.</exception>
		/// <exception cref="ObjectDisposedException">This control was already disposed.</exception>
		[Bindable(true), Category("Layout"), DefaultValue(ContentAlignment.TopCenter),
		 Description("Specifies the content alignment used to arrange the child controls.")]
		public ContentAlignment ContentAlignment {
			get { return contentAlignment; }
			set {
				if (contentAlignment == value) return;
				if (!Enum.IsDefined(typeof(ContentAlignment), value))
					throw new InvalidEnumArgumentException();
				contentAlignment = value;
				ReArrange();
			}
		}
		
		int space = 4;
		
		/// <summary>
		/// Gets/Sets the space between the child controls.
		/// </summary>
		/// <exception cref="ArgumentOutOfRangeException">
		/// Value is less than zero.
		/// </exception>
		/// <exception cref="InvokeRequiredException">This method is called from an other thread than the thread that created this control.</exception>
		/// <exception cref="ObjectDisposedException">This control was already disposed.</exception>
		[Bindable(true), Category("Layout"), DefaultValue(4),
		 Description("Specifies the space between the child controls.")]
		public int Space {
			get { return space; }
			set {
				if (space == value) return;
				if (space < 0)
					throw new ArgumentOutOfRangeException("space", value, "Space can't be less than zero.");
				space = value;
				ReArrange();
			}
		}
		#endregion
		
		#region GetChildSize
		/// <summary>
		/// Gets the combined size of all child items including spacing.
		/// </summary>
		public Size GetChildSize()
		{
			bool vertical = direction == Direction.TopDown || direction == Direction.BottomUp;
			
			int minWidth = 0; // minimal "width" (=size orthogonal to the arrange direction)
			int height = 0;   // "height"        (=size in arrange direction)
			
			foreach (Control ctl in controls) {
				if (!ctl.Visible) continue;
				minWidth = Math.Max(minWidth, vertical ? ctl.Width : ctl.Height);
				if (height > 0) height += space;
				height += vertical ? ctl.Height : ctl.Width;
			}
			
			if (vertical)
				return new Size(minWidth, height);
			else
				return new Size(height, minWidth);
		}
		#endregion
		
		#region ReArrange
		int arrangeCount;
		private void ReArrange()
		{
			Check();
			if (!arrangeActive) { return; }
			if (!this.Visible) return;
			int controlcount = controls.Count;
			++arrangeCount;
			// Console.Write(arrangeCount + " ");
			if (controlcount == 0) return;
			Size size = GetChildSize();
			Size sizeWithMargin = new Size(size.Width + 2 * space, size.Height + 2 * space);
			Size available;
			if (autoSize)
				ClientSize = sizeWithMargin;
			available = ClientSize;
			
			// gets alignment values: 0 = left/top, 1 = center, 2 = right/bottom
			int xLayout = GetXLayout();
			int yLayout = GetYLayout();
			
			Size useSize = new Size(Math.Max(available.Width, sizeWithMargin.Width),
			                        Math.Max(available.Height, sizeWithMargin.Height));
			
			bool reverse = direction == Direction.BottomUp || direction == Direction.RightToLeft;
			bool vertical = direction == Direction.TopDown || direction == Direction.BottomUp;
			
			int pos; // start position
			if (vertical)
				pos = DoLayout(yLayout, useSize.Height, size.Height);
			else
				pos = DoLayout(xLayout, useSize.Width, size.Width);
			
			for (int i = reverse ? controlcount - 1 : 0; i >= 0 && i < controlcount; i += reverse ? -1 : 1) {
				Control ctl = (Control)controls[i];
				if (!ctl.Visible) continue;
				AnchorStyles anchor = AnchorStyles.None;
				if (xLayout == 0)
					anchor |= AnchorStyles.Left;
				else if (xLayout == 2)
					anchor |= AnchorStyles.Right;
				if (yLayout == 0)
					anchor |= AnchorStyles.Top;
				else if (yLayout == 2)
					anchor |= AnchorStyles.Bottom;
				if (ctl.Anchor != anchor)
					ctl.Anchor = anchor;
				int top = ctl.Top;
				int left = ctl.Left;
				int width = ctl.Width;
				int height = ctl.Height;
				if (vertical) {
					left = DoLayout(xLayout, useSize.Width, ctl.Width);
					top = pos;
				} else {
					left = pos;
					top = DoLayout(yLayout, useSize.Height, ctl.Height);
				}
				ctl.Bounds = new Rectangle(left, top, width, height);
				pos += space + (vertical ? ctl.Height : ctl.Width);
			}
		}
		
		private int DoLayout(int layout, int available, int use)
		{
			if (layout == 0)
				return space;
			else if (layout == 1)
				return (available - use) / 2;
			else
				return available - use - space;
		}
		
		private int GetXLayout()
		{
			switch (contentAlignment) {
				case ContentAlignment.TopLeft:
				case ContentAlignment.MiddleLeft:
				case ContentAlignment.BottomLeft:
					return 0;
				case ContentAlignment.TopCenter:
				case ContentAlignment.MiddleCenter:
				case ContentAlignment.BottomCenter:
					return 1;
				case ContentAlignment.TopRight:
				case ContentAlignment.MiddleRight:
				case ContentAlignment.BottomRight:
					return 2;
				default:
					throw new InvalidEnumArgumentException();
			}
		}
		
		private int GetYLayout()
		{
			switch (contentAlignment) {
				case ContentAlignment.TopCenter:
				case ContentAlignment.TopLeft:
				case ContentAlignment.TopRight:
					return 0;
				case ContentAlignment.MiddleCenter:
				case ContentAlignment.MiddleLeft:
				case ContentAlignment.MiddleRight:
					return 1;
				case ContentAlignment.BottomCenter:
				case ContentAlignment.BottomLeft:
				case ContentAlignment.BottomRight:
					return 2;
				default:
					throw new InvalidEnumArgumentException();
			}
		}
		
		private void Check()
		{
			if (base.IsDisposed) throw new ObjectDisposedException("ErrorReporter");
			if (base.InvokeRequired) throw new InvokeRequiredException();
		}
		#endregion
		
		#region Control Events
		private void ControlSizeChanged(object sender, EventArgs e)
		{
			Control ctl = (Control)sender;
			if (!ctl.Visible) return;
			//ReArrange();
		}
		
		private void ControlVisibleChanged(object sender, EventArgs e)
		{
			ReArrange();
		}
		
		private void ControlSettingsChanged(object sender, EventArgs e)
		{
			ReArrange();
		}
		#endregion
		
		#region Overridden Events
		ArrayList controls = new ArrayList();
		
		/// <summary>
		/// Raises the <see cref="Control.ControlRemoved">ControlRemoved event</see> and deregisters the
		/// control in the <see cref="ControlEventArgs"/> for automatic positioning.
		/// </summary>
		protected override void OnControlRemoved(System.Windows.Forms.ControlEventArgs e)
		{
			try {
				Control ctl = e.Control;
				controls.Remove(ctl);
				ctl.SizeChanged -= new EventHandler(ControlSizeChanged);
				ctl.VisibleChanged -= new EventHandler(ControlVisibleChanged);
			} finally {
				base.OnControlRemoved(e);
			}
		}
		
		/// <summary>
		/// Raises the <see cref="Control.ControlRemoved">ControlAdded event</see> and registers the
		/// control in the <see cref="ControlEventArgs"/> for automatic positioning.
		/// </summary>
		protected override void OnControlAdded(System.Windows.Forms.ControlEventArgs e)
		{
			try {
				Control ctl = e.Control;
				controls.Add(ctl);
				ctl.SizeChanged += new EventHandler(ControlSizeChanged);
				ctl.VisibleChanged += new EventHandler(ControlVisibleChanged);
			} finally {
				base.OnControlAdded(e);
			}
		}
		#endregion
		
		#region ActivateArrange
		/// <summary>
		/// This method rearranges the internal order of the controls to match their
		/// current visual location and then actives the auto-arranging.
		/// </summary>
		public void ActivateArrange()
		{
			Check();
			bool reverse = direction == Direction.BottomUp || direction == Direction.RightToLeft;
			bool vertical = direction == Direction.TopDown || direction == Direction.BottomUp;
			
			controls.Sort(new ControlComparer(reverse, vertical));
			
			ArrangeActive = true;
		}
		
		private class ControlComparer : IComparer
		{
			bool reverse;
			bool vertical;
			
			public ControlComparer(bool reverse, bool vertical)
			{
				this.reverse = reverse;
				this.vertical = vertical;
			}
			
			int IComparer.Compare(object x, object y)
			{
				Control a = x as Control;
				Control b = y as Control;
				/*if (a == b) return 0;
				if (a == null) return 1;
				if (b == null) return -1;*/
				if (vertical) {
					if (a.Top == b.Top) return 0;
					return ((a.Top > b.Top) ^ reverse) ? 1 : -1;
				} else {
					if (a.Left == b.Left) return 0;
					return ((a.Left > b.Left) ^ reverse) ? 1 : -1;
				}
			}
		}
		#endregion
	}
	
	/// <summary>
	/// Specifies a direction for arranging elements.
	/// </summary>
	public enum Direction
	{
		/// <summary>
		/// Arranges the elements starting from the top, going down.
		/// </summary>
		TopDown,
		/// <summary>
		/// Arranges the elements left to right.
		/// </summary>
		LeftToRight,
		/// <summary>
		/// Arranges the elements bottom up.
		/// </summary>
		BottomUp,
		/// <summary>
		/// Arranges the elements right to left.
		/// </summary>
		RightToLeft
	}
}