Click here to Skip to main content
15,886,509 members
Articles / Programming Languages / C#

Hacking the Combo Box to give it horizontal scrolling

Rate me:
Please Sign up or sign in to vote.
4.82/5 (41 votes)
31 Jan 2005CPOL14 min read 262.1K   14.8K   71  
How to hack the combo box to give it a horizontal scroll bar, thus giving a polished look with ease + simplicity.
using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Runtime.InteropServices;


namespace TB.ExtComboBox{
	public class Form : System.Windows.Forms.Form {

		private System.ComponentModel.IContainer components;
		private COMBOBOXINFO cbi;
		private System.Windows.Forms.Button btnCancel;
		private System.Windows.Forms.Label label1;
		private int pixelWidth = -1;
		private RECT cboListRect;
		private SCROLLINFO si;
		private SubClass scList = null;
		private int xMaxScroll = 0;
		private int xCurrentScroll = 0;
		private int xMinScroll = 0;
		private int xNewSize = 0;
		private System.Windows.Forms.Label label3;
		private System.Windows.Forms.ComboBox cboBoxEnhanced;
		private System.Windows.Forms.ComboBox cboBoxStandard;
		private System.Windows.Forms.Label lblDropDownStyle;
		private System.Windows.Forms.Label lblStyle;
		private System.Windows.Forms.RadioButton radioBtnDropDown;
		private System.Windows.Forms.RadioButton radioBtnDropDownList;
		private bool gotCBI = false;

		#region Win32 Imports
		[DllImport("user32")] public static extern bool GetComboBoxInfo(IntPtr hwndCombo, ref COMBOBOXINFO info);
		[DllImport("user32")] public static extern int SendMessage(IntPtr hwnd, int wMsg, int wParam, IntPtr lParam);
		[DllImport("user32")] public static extern int GetWindowLong(IntPtr hwnd, int nIndex);
		[DllImport("user32")] public static extern int SetWindowLong(IntPtr hwnd, int nIndex, int dwNewLong);
		[DllImport("user32")] public static extern int GetClientRect(IntPtr hwnd, ref RECT lpRect);
		[DllImport("user32")] public static extern int SetScrollInfo(IntPtr hwnd, int n, ref SCROLLINFO lpcScrollInfo, bool redraw);
		#endregion

		#region Win32 Constants
		public const int WS_HSCROLL = 0x100000;
		public const int GWL_STYLE = (-16);
		public const int LB_SETHORIZONTALEXTENT = 0x194;
		public const int WM_SIZE = 0x5;
		public const int WM_HSCROLL = 0x114;
		#endregion

		#region Scroll Bar Messages - Private Constant Ints
		private const int SB_LINEUP = 0;
		private const int SB_LINELEFT = 0;
		private const int SB_LINEDOWN = 1;
		private const int SB_LINERIGHT = 1;
		private const int SB_PAGEUP = 2;
		private const int SB_PAGELEFT = 2;
		private const int SB_PAGEDOWN = 3;
		private const int SB_PAGERIGHT = 3;
		private const int SB_THUMBPOSITION = 4;
		private const int SB_THUMBTRACK = 5;
		private const int SB_TOP = 6;
		private const int SB_LEFT = 6;
		private const int SB_BOTTOM = 7;
		private const int SB_RIGHT = 7;
		private const int SB_ENDSCROLL = 8;
		private const int SB_HORZ = 0;
		private const int SB_VERT = 1;
		private const int SB_CTL = 2;
		private const int SB_BOTH = 3;
		private const int SBM_ENABLE_ARROWS = 0x00E4; /*not in win3.1 */
		private const int SBM_SETSCROLLINFO = 0x00E9;
		private const int SBM_GETSCROLLINFO = 0x00EA;
		private const int SIF_RANGE = 0x0001;
		private const int SIF_PAGE = 0x0002;
		private const int SIF_POS = 0x0004;
		private const int SIF_DISABLENOSCROLL = 0x0008;
		private const int SIF_TRACKPOS = 0x0010;
		private const int SIF_ALL = (SIF_RANGE | SIF_PAGE | SIF_POS | SIF_TRACKPOS);
		#endregion
		//
		#region Form Constructor
		public Form() {
			InitializeComponent();
			InitComboStrings(this.cboBoxEnhanced);
			InitComboStrings(this.cboBoxStandard);
			//
			this.gotCBI = this.InitComboBoxInfo(this.cboBoxEnhanced);
			if (this.gotCBI){
				this.cboListRect = new RECT();
				this.si = new SCROLLINFO();
				this.scList = new SubClass(this.cbi.hwndList, false);
				this.scList.SubClassedWndProc += new SubClass.SubClassWndProcEventHandler(scList_SubClassedWndProc);
			}
		}
		#endregion

		#region Dispose
		protected override void Dispose(bool disposing){
			if(disposing){
				if(components != null){
					components.Dispose();
				}
				if (this.scList != null){
					this.scList.ReleaseHandle();
				}
			}
			base.Dispose(disposing);
		}
		#endregion

		#region InitComboBoxInfo Method - Private
		private bool InitComboBoxInfo(System.Windows.Forms.ComboBox cbo){
			this.cbi = new COMBOBOXINFO();
			this.cbi.cbSize = Marshal.SizeOf(this.cbi);
			if (!GetComboBoxInfo(cbo.Handle, ref this.cbi)){
				return false;
			}
			return true;
		}
		#endregion

		#region Windows Form Designer generated code
		/// <summary>
		/// Required method for Designer support - do not modify
		/// the contents of this method with the code editor.
		/// </summary>
		private void InitializeComponent() {
			this.cboBoxEnhanced = new System.Windows.Forms.ComboBox();
			this.btnCancel = new System.Windows.Forms.Button();
			this.label1 = new System.Windows.Forms.Label();
			this.label3 = new System.Windows.Forms.Label();
			this.cboBoxStandard = new System.Windows.Forms.ComboBox();
			this.lblDropDownStyle = new System.Windows.Forms.Label();
			this.lblStyle = new System.Windows.Forms.Label();
			this.radioBtnDropDown = new System.Windows.Forms.RadioButton();
			this.radioBtnDropDownList = new System.Windows.Forms.RadioButton();
			this.SuspendLayout();
			// 
			// cboBoxEnhanced
			// 
			this.cboBoxEnhanced.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
			this.cboBoxEnhanced.DropDownWidth = 150;
			this.cboBoxEnhanced.Location = new System.Drawing.Point(96, 32);
			this.cboBoxEnhanced.Name = "cboBoxEnhanced";
			this.cboBoxEnhanced.Size = new System.Drawing.Size(112, 21);
			this.cboBoxEnhanced.TabIndex = 3;
			this.cboBoxEnhanced.DropDown += new System.EventHandler(this.cboBoxEnhanced_DropDown);
			this.cboBoxEnhanced.DropDownStyleChanged += new System.EventHandler(this.cboBoxEnhanced_DropDownStyleChanged);
			// 
			// btnCancel
			// 
			this.btnCancel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
			this.btnCancel.Cursor = System.Windows.Forms.Cursors.Hand;
			this.btnCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel;
			this.btnCancel.FlatStyle = System.Windows.Forms.FlatStyle.Popup;
			this.btnCancel.Location = new System.Drawing.Point(130, 104);
			this.btnCancel.Name = "btnCancel";
			this.btnCancel.TabIndex = 4;
			this.btnCancel.Text = "Cancel";
			this.btnCancel.Click += new System.EventHandler(this.btnCancel_Click);
			// 
			// label1
			// 
			this.label1.AutoSize = true;
			this.label1.Location = new System.Drawing.Point(0, 33);
			this.label1.Name = "label1";
			this.label1.Size = new System.Drawing.Size(98, 16);
			this.label1.TabIndex = 2;
			this.label1.Text = "Enhanced Combo:";
			this.label1.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
			// 
			// label3
			// 
			this.label3.AutoSize = true;
			this.label3.Location = new System.Drawing.Point(0, 8);
			this.label3.Name = "label3";
			this.label3.Size = new System.Drawing.Size(93, 16);
			this.label3.TabIndex = 0;
			this.label3.Text = "Standard Combo:";
			this.label3.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
			// 
			// cboBoxStandard
			// 
			this.cboBoxStandard.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
			this.cboBoxStandard.Location = new System.Drawing.Point(96, 8);
			this.cboBoxStandard.Name = "cboBoxStandard";
			this.cboBoxStandard.Size = new System.Drawing.Size(112, 21);
			this.cboBoxStandard.TabIndex = 1;
			this.cboBoxStandard.DropDown += new System.EventHandler(this.cboBoxStandard_DropDown);
			// 
			// lblDropDownStyle
			// 
			this.lblDropDownStyle.AutoSize = true;
			this.lblDropDownStyle.Location = new System.Drawing.Point(0, 64);
			this.lblDropDownStyle.Name = "lblDropDownStyle";
			this.lblDropDownStyle.Size = new System.Drawing.Size(89, 16);
			this.lblDropDownStyle.TabIndex = 5;
			this.lblDropDownStyle.Text = "DropDown Style:";
			this.lblDropDownStyle.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
			// 
			// lblStyle
			// 
			this.lblStyle.AutoSize = true;
			this.lblStyle.Location = new System.Drawing.Point(88, 64);
			this.lblStyle.Name = "lblStyle";
			this.lblStyle.Size = new System.Drawing.Size(29, 16);
			this.lblStyle.TabIndex = 6;
			this.lblStyle.Text = "Style";
			this.lblStyle.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
			// 
			// radioBtnDropDown
			// 
			this.radioBtnDropDown.Checked = true;
			this.radioBtnDropDown.Location = new System.Drawing.Point(0, 80);
			this.radioBtnDropDown.Name = "radioBtnDropDown";
			this.radioBtnDropDown.Size = new System.Drawing.Size(80, 18);
			this.radioBtnDropDown.TabIndex = 7;
			this.radioBtnDropDown.TabStop = true;
			this.radioBtnDropDown.Text = "DropDown";
			this.radioBtnDropDown.Click += new System.EventHandler(this.radioBtnDropDown_Click);
			// 
			// radioBtnDropDownList
			// 
			this.radioBtnDropDownList.Location = new System.Drawing.Point(96, 80);
			this.radioBtnDropDownList.Name = "radioBtnDropDownList";
			this.radioBtnDropDownList.Size = new System.Drawing.Size(104, 18);
			this.radioBtnDropDownList.TabIndex = 8;
			this.radioBtnDropDownList.Text = "DropDownList";
			this.radioBtnDropDownList.Click += new System.EventHandler(this.radioBtnDropDown_Click);
			// 
			// Form
			// 
			this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
			this.CancelButton = this.btnCancel;
			this.ClientSize = new System.Drawing.Size(210, 130);
			this.Controls.Add(this.radioBtnDropDownList);
			this.Controls.Add(this.radioBtnDropDown);
			this.Controls.Add(this.lblStyle);
			this.Controls.Add(this.lblDropDownStyle);
			this.Controls.Add(this.cboBoxStandard);
			this.Controls.Add(this.label3);
			this.Controls.Add(this.btnCancel);
			this.Controls.Add(this.cboBoxEnhanced);
			this.Controls.Add(this.label1);
			this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow;
			this.Name = "Form";
			this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
			this.Text = "Combo Demo";
			this.Load += new System.EventHandler(this.Form_Load);
			this.ResumeLayout(false);

		}
		#endregion

		#region BootStrapper
		[STAThread]
		static void Main() {
			Application.Run(new Form());
		}
		#endregion

		#region btnCancel Click Event Handler
		private void btnCancel_Click(object sender, System.EventArgs e) {
			this.Close();
		}
		#endregion

		#region Initialise the Combo Box with strings
		private void InitComboStrings(ComboBox cbo){
			cbo.Items.AddRange(new string[]{
											   "Ottawa, Ontario", "St. John's, Newfoundland and Labrador",
											   "Halifax, Nova Scotia", "Charlottetown, Prince Edward Island",
											   "Fredericton, New Brunswick", "Qu�bec, Qu�bec", "Toronto, Ontario",
											   "Winnipeg, Manitoba", "Regina, Saskatchewan", "Edmonton, Alberta", "Victoria, British Colombia",
											   "Whitehorse, Yukon", "Yellowknife, Northwest Territories", "Iqaluit, Nunavut", 
											   "lllllllllllllll, lllllllllllllllllllllllll",
											   "WWWWWWWWWWWWWWW, WWWWWWWWWWWWWWWWWWWWWWWW"});
		}
		#endregion

		#region GetLargestTextExtent - Obtain largest string in pixels
		private void GetLargestTextExtent(System.Windows.Forms.ComboBox cbo, ref int largestWidth){
			int maxLen = -1;
			if (cbo.Items.Count >= 1){
				using (Graphics g = cbo.CreateGraphics()){
					int vertScrollBarWidth = 0;
					if (cbo.Items.Count > cbo.MaxDropDownItems){
						vertScrollBarWidth = SystemInformation.VerticalScrollBarWidth;
					}
					for (int nLoopCnt = 0; nLoopCnt < cbo.Items.Count; nLoopCnt++){
						int newWidth = (int) g.MeasureString(cbo.Items[nLoopCnt].ToString(), cbo.Font).Width + vertScrollBarWidth; 
						if (newWidth > maxLen) {
							maxLen = newWidth;
						}
					}
				}
			}
			largestWidth = maxLen;
		}
		#endregion

		#region cboBoxEnhanced_DropDown Event Handler
		private void cboBoxEnhanced_DropDown(object sender, System.EventArgs e) {
			this.GetLargestTextExtent(this.cboBoxEnhanced, ref this.pixelWidth);
			if ((this.pixelWidth != -1) && (this.pixelWidth > this.cboBoxEnhanced.DropDownWidth) && this.gotCBI){
				// Adjust our dropdown list box to include the horizontal scroll!
				int listStyle = GetWindowLong(this.cbi.hwndList, GWL_STYLE);
				listStyle |= WS_HSCROLL;
				listStyle = SetWindowLong(this.cbi.hwndList, GWL_STYLE, listStyle);
				// Switch on the Subclassing....
				this.scList.SubClassed = true;
				// Set the horizontal extent for the listbox!
				SendMessage(this.cbi.hwndList, LB_SETHORIZONTALEXTENT, this.pixelWidth, IntPtr.Zero);
			}
		}
		#endregion

		#region scList_SubClassedWndProc Event Handler
		private void scList_SubClassedWndProc(ref Message m) {
			switch (m.Msg){
				case WM_SIZE:
					GetClientRect(this.cbi.hwndList, ref this.cboListRect);
					this.xNewSize = this.scList.LoWord(m.LParam.ToInt32());
					this.xMaxScroll = Math.Max(this.pixelWidth - this.xNewSize, 0);
					this.xCurrentScroll = Math.Min(this.xCurrentScroll, this.xMaxScroll);
					this.si.cbSize = Marshal.SizeOf(this.si);
					this.si.nMax = this.xMaxScroll;
					this.si.nMin = this.xMinScroll;
					this.si.nPos = this.xCurrentScroll;
					this.si.nPage = this.xNewSize;
					this.si.fMask = SIF_RANGE | SIF_PAGE | SIF_POS;
					SetScrollInfo(this.cbi.hwndList, SB_HORZ, ref this.si, false);
					break;
				case WM_HSCROLL:
					int xDelta = 0;
					int xNewPos = 0;
					int modulo = (this.xNewSize > this.pixelWidth) ? (this.xNewSize % this.pixelWidth) : (this.pixelWidth % this.xNewSize);
					switch (this.scList.LoWord(m.WParam.ToInt32())){
						case SB_PAGEUP:
							xNewPos = this.xCurrentScroll - modulo;
							break;
						case SB_PAGEDOWN:
							xNewPos = this.xCurrentScroll + modulo;
							break;
						case SB_LINEUP:
							xNewPos = this.xCurrentScroll - 1;
							break;
						case SB_LINEDOWN:
							xNewPos = this.xCurrentScroll + 1;
							break;
						case SB_THUMBPOSITION:
							xNewPos = this.scList.HiWord(m.WParam.ToInt32());
							break;
						default:
							xNewPos = this.xCurrentScroll;
							break;
					}
					xNewPos = Math.Max(0, xNewPos);
					xNewPos = Math.Min(xMaxScroll, xNewPos);
					if (xNewPos == this.xCurrentScroll) break;
					xDelta = xNewPos - this.xCurrentScroll;
					this.xCurrentScroll = xNewPos;
					this.si.cbSize = Marshal.SizeOf(this.si);
					this.si.fMask = SIF_POS;
					this.si.nPos = this.xCurrentScroll;
					SetScrollInfo(this.cbi.hwndList, SB_HORZ, ref this.si, true);
					break;
			}
		}
		#endregion

		#region cboBoxStandard_DropDown Event Handler
		private void cboBoxStandard_DropDown(object sender, System.EventArgs e) {
			int pw = -1;
			this.GetLargestTextExtent(this.cboBoxStandard, ref pw);
			this.cboBoxStandard.DropDownWidth = pw;
		}
		#endregion

		// Proof to myself that this darn thing works in a singular code base such as this.
		// Irrespective of dropdown styles it works for both DropDown + DropDownList!
		#region cboBoxEnhanced_DropDownStyleChanged Event Handler
		private void cboBoxEnhanced_DropDownStyleChanged(object sender, System.EventArgs e) {
			this.lblStyle.Text = this.cboBoxEnhanced.DropDownStyle.ToString();
		}
		#endregion

		#region radioBtnDropDown_Click Event Handler
		private void radioBtnDropDown_Click(object sender, System.EventArgs e) {
			if (radioBtnDropDown.Checked){
				this.cboBoxEnhanced.DropDownStyle = ComboBoxStyle.DropDown;
			}else{
				this.cboBoxEnhanced.DropDownStyle = ComboBoxStyle.DropDownList;
			}
			// We need to call this again since we've changed the dropdown style.
			this.gotCBI = this.InitComboBoxInfo(this.cboBoxEnhanced);
			if (this.gotCBI){
				// No need to call new on scroll class etc..
				this.scList = new SubClass(this.cbi.hwndList, false);
				this.scList.SubClassedWndProc += new SubClass.SubClassWndProcEventHandler(scList_SubClassedWndProc);
			}
		}
		#endregion

		private void Form_Load(object sender, System.EventArgs e) {
			radioBtnDropDown.PerformClick();
		}
	}

	#region RECT struct
	[StructLayout(LayoutKind.Sequential)]
	public struct RECT {
		public int Left;
		public int Top;
		public int Right;
		public int Bottom;
	}
	#endregion

	#region COMBOBOXINFO Struct
	[StructLayout(LayoutKind.Sequential)]
	public struct COMBOBOXINFO {
		public int cbSize;
		public RECT rcItem;
		public RECT rcButton;
		public IntPtr stateButton;
		public IntPtr hwndCombo;
		public IntPtr hwndEdit;
		public IntPtr hwndList;
	}
	#endregion

	#region SCROLLINFO struct
	[StructLayout(LayoutKind.Sequential)]
	public struct SCROLLINFO {
		public int cbSize;
		public int fMask;
		public int nMin;
		public int nMax;
		public int nPage;
		public int nPos;
		public int nTrackPos;
	}
	#endregion

	#region SubClass Classing Handler Class
	public class SubClass : System.Windows.Forms.NativeWindow{
		public delegate void SubClassWndProcEventHandler(ref System.Windows.Forms.Message m);
		public event SubClassWndProcEventHandler SubClassedWndProc;
		private bool IsSubClassed = false;

		public SubClass(IntPtr Handle, bool _SubClass){
			base.AssignHandle(Handle);
			this.IsSubClassed = _SubClass;
		}

		public bool SubClassed{
			get{ return this.IsSubClassed; }
			set{ this.IsSubClassed = value; }
		}

		protected override void WndProc(ref Message m) {
			if (this.IsSubClassed){
				OnSubClassedWndProc(ref m);
			}
			base.WndProc (ref m);
		}

		#region HiWord Message Cracker
		public int HiWord(int Number) {
			return ((Number >> 16) & 0xffff);
		}
		#endregion

		#region LoWord Message Cracker
		public int LoWord(int Number) {
			return (Number & 0xffff);
		}
		#endregion

		#region MakeLong Message Cracker
		public int MakeLong(int LoWord, int HiWord) { 
			return (HiWord << 16) | (LoWord & 0xffff); 
		} 
		#endregion
 
		#region MakeLParam Message Cracker
		public IntPtr MakeLParam(int LoWord, int HiWord) { 
			return (IntPtr) ((HiWord << 16) | (LoWord & 0xffff)); 
		} 
		#endregion

		private void OnSubClassedWndProc(ref Message m){
			if (SubClassedWndProc != null){
				this.SubClassedWndProc(ref m);
			}
		}
	}
	#endregion

}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
Ireland Ireland
B.Sc. in Information Systems.
Languages: C, Assembly 80x86, VB6, Databases, .NET, Linux, Win32 API.

Short Note:
Having worked with IT systems spanning over 14 years, he still can remember writing a TSR to trap the three-finger salute in the old days of DOS with Turbo C. Smile | :) Having worked or hacked with AS/400 system while in his college days, graduating to working with Cobol on IBM MVS/360, to RS/6000 AIX/C. He can remember obtaining OS/2 version 2 and installing it on an antique 80386. Boy it ran but crawled! Smile | :) Keen to dabble in new technologies. A self-taught programmer, he is keen to relinquish and acquire new bits and bytes, but craves for the dinosaur days when programmers were ultimately in control over the humble DOS and hacking it!! Smile | :)

Comments and Discussions