Skip to content

Instantly share code, notes, and snippets.

@ArtemAvramenko
Last active August 12, 2019 15:30
Show Gist options
  • Save ArtemAvramenko/e260420b86564cf13d2e to your computer and use it in GitHub Desktop.
Save ArtemAvramenko/e260420b86564cf13d2e to your computer and use it in GitHub Desktop.
WinForms Splitter without flickering
using System.ComponentModel;
using System.Drawing;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Security.Permissions;
namespace System.Windows.Forms
{
/// <summary>
/// Represents a splitter control that enables the user to resize docked controls.
/// Fixes <see cref="T:System.Windows.Forms.Splitter"/> problems:
/// <list type="number">
/// <item>removes Application.DoEvents;</item>
/// <item>avoids flickering;</item>
/// <item>sets ControlStyles.OptimizedDoubleBuffer.</item>
/// </list>
/// </summary>
[DefaultEvent("SplitterMoved"), DefaultProperty("Dock"), ClassInterface(ClassInterfaceType.AutoDispatch), ComVisible(true)]
public class SplitterEx : Control
{
#region PInvoke
private const int WS_BORDER = 0x00800000;
private const int WS_EX_CLIENTEDGE = 0x00000200;
private const int DCX_CACHE = 2;
private const int DCX_LOCKWINDOWUPDATE = 0x400;
private const int PATINVERT = 0x5A0049;
[DllImport("user32.dll")]
private static extern IntPtr GetCapture();
[DllImport("user32.dll")]
private static extern IntPtr SetCapture(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern int ReleaseCapture();
[DllImport("user32.dll")]
private static extern IntPtr GetDCEx(IntPtr hWnd, IntPtr hrgnClip, int flags);
[DllImport("user32.dll")]
private static extern IntPtr ReleaseDC(IntPtr hWnd, IntPtr hDC);
[DllImport("user32.dll")]
private static extern bool SubtractRect(out RECT lprcDst, [In] ref RECT lprcSrc1, [In] ref RECT lprcSrc2);
[DllImport("gdi32.dll")]
private static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);
[DllImport("gdi32.dll")]
private static extern bool PatBlt(IntPtr hdc, int nXLeft, int nYLeft, int nWidth, int nHeight, int dwRop);
[DllImport("gdi32.dll")]
private static extern bool DeleteObject(IntPtr hObject);
[DllImport("gdi32.dll")]
private static extern IntPtr CreateBitmap(int nWidth, int nHeight, int cPlanes, int cBitsPerPel, byte[] lpvBits);
[DllImport("gdi32.dll")]
private static extern IntPtr CreateBrushIndirect(ref LOGBRUSH lplb);
[StructLayout(LayoutKind.Sequential)]
private struct RECT
{
public int left;
public int top;
public int right;
public int bottom;
}
[StructLayout(LayoutKind.Sequential)]
private struct LOGBRUSH
{
public int lbStyle;
public int lbColor;
public int lbHatch;
}
#endregion PInvoke
private const int DRAW_START = 1;
private const int DRAW_MOVE = 2;
private const int DRAW_END = 3;
private const int defaultWidth = 3;
private static readonly object EVENT_MOVING = new object();
private static readonly object EVENT_MOVED = new object();
private BorderStyle borderStyle = BorderStyle.None;
private int minSize = 25;
private int minExtra = 25;
private Point anchor = Point.Empty;
private Control splitTarget;
private int splitSize = -1;
private int splitterThickness = 3;
private int initTargetSize;
private int lastDrawSplit = -1;
private int maxSize;
private SplitterMessageFilter splitterMessageFilter = null;
/// <summary>
/// Initializes a new instance of the <see cref="T:System.Windows.Forms.SplitterEx"/> class.
/// </summary>
public SplitterEx()
{
base.SetStyle(ControlStyles.Selectable, false);
base.SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
base.TabStop = false;
this.Dock = DockStyle.Left;
}
/// <summary>
/// Occurs when the splitter control is in the process of moving.
/// </summary>
public event SplitterEventHandler SplitterMoving
{
add
{
base.Events.AddHandler(EVENT_MOVING, value);
}
remove
{
base.Events.RemoveHandler(EVENT_MOVING, value);
}
}
/// <summary>
/// Occurs when the splitter control is moved.
/// </summary>
public event SplitterEventHandler SplitterMoved
{
add
{
base.Events.AddHandler(EVENT_MOVED, value);
}
remove
{
base.Events.RemoveHandler(EVENT_MOVED, value);
}
}
/// <summary>
/// Gets or sets the style of border for the control.
/// </summary>
/// <returns>
/// One of the <see cref="T:System.Windows.Forms.BorderStyle"/> values. The default is BorderStyle.None.
/// </returns>
/// <exception cref="T:System.ComponentModel.InvalidEnumArgumentException">
/// The value of the property is not one of the
/// <see cref="T:System.Windows.Forms.BorderStyle"/> values.
/// </exception>
[DefaultValue(BorderStyle.None), DispId(-504)]
public BorderStyle BorderStyle
{
get
{
return this.borderStyle;
}
set
{
if (value < BorderStyle.None || value > BorderStyle.Fixed3D)
{
throw new InvalidEnumArgumentException("value", (int)value, typeof(BorderStyle));
}
if (this.borderStyle != value)
{
this.borderStyle = value;
base.UpdateStyles();
}
}
}
/// <summary>
/// Gets or sets which <see cref="T:System.Windows.Forms.SplitterEx"/> borders are docked to
/// its parent control and determines how a <see cref="T:System.Windows.Forms.SplitterEx"/> is
/// resized with its parent.
/// </summary>
/// <returns>
/// One of the <see cref="T:System.Windows.Forms.DockStyle"/> values. The default is <see cref="F:System.Windows.Forms.DockStyle.Left"/>.
/// </returns>
[DefaultValue(DockStyle.Left), Localizable(true)]
public override DockStyle Dock
{
get
{
return base.Dock;
}
set
{
if (value < DockStyle.Top || value > DockStyle.Right)
{
throw new ArgumentException("Splitter control must be docked left, right, top, or bottom.");
}
int requestedSize = splitterThickness;
base.Dock = value;
switch (Dock)
{
case DockStyle.Top:
case DockStyle.Bottom:
if (splitterThickness != -1)
{
Height = requestedSize;
}
break;
case DockStyle.Left:
case DockStyle.Right:
if (splitterThickness != -1)
{
Width = requestedSize;
}
break;
}
}
}
/// <summary>
/// Gets or sets the minimum distance that must remain between the splitter control and the
/// edge of the opposite side of the container (or the closest control docked to that side).
/// </summary>
/// <returns>
/// The minimum distance, in pixels, between the
/// <see cref="T:System.Windows.Forms.SplitterEx"/> control and the edge of the opposite side
/// of the container (or the closest control docked to that side). The default is 25.
/// </returns>
[DefaultValue(25), Localizable(true)]
public int MinExtra
{
get
{
return this.minExtra;
}
set
{
if (value < 0)
{
value = 0;
}
this.minExtra = value;
}
}
/// <summary>
/// Gets or sets the minimum distance that must remain between the splitter control and the
/// container edge that the control is docked to.
/// </summary>
/// <returns>
/// The minimum distance, in pixels, between the
/// <see cref="T:System.Windows.Forms.SplitterEx"/> control and the container edge that the
/// control is docked to. The default is 25.
/// </returns>
[DefaultValue(25), Localizable(true)]
public int MinSize
{
get
{
return this.minSize;
}
set
{
if (value < 0)
{
value = 0;
}
this.minSize = value;
}
}
/// <summary>
/// Gets or sets the distance between the splitter control and the container edge that the
/// control is docked to.
/// </summary>
/// <returns>
/// The distance, in pixels, between the <see cref="T:System.Windows.Forms.SplitterEx"/>
/// control and the container edge that the control is docked to. If the
/// <see cref="T:System.Windows.Forms.Splitter"/> control is not bound to a control, the
/// value is -1.
/// </returns>
[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public int SplitPosition
{
get
{
if (this.splitSize == -1)
{
this.splitSize = this.CalcSplitSize();
}
return this.splitSize;
}
set
{
SplitData splitData = this.CalcSplitBounds();
if (value > this.maxSize)
{
value = this.maxSize;
}
if (value < this.minSize)
{
value = this.minSize;
}
this.splitSize = value;
this.DrawSplitBar(DRAW_END);
if (splitData.target == null)
{
this.splitSize = -1;
return;
}
Rectangle bounds = splitData.target.Bounds;
switch (this.Dock)
{
case DockStyle.Top:
bounds.Height = value;
break;
case DockStyle.Bottom:
bounds.Y += bounds.Height - this.splitSize;
bounds.Height = value;
break;
case DockStyle.Left:
bounds.Width = value;
break;
case DockStyle.Right:
bounds.X += bounds.Width - this.splitSize;
bounds.Width = value;
break;
}
splitData.target.Bounds = bounds;
this.OnSplitterMoved(new SplitterEventArgs(base.Left, base.Top, base.Left + bounds.Width / 2, base.Top + bounds.Height / 2));
}
}
/// <summary>
/// Gets the default size of the control.
/// </summary>
/// <returns>
/// A <see cref="T:System.Drawing.Size"/> that represents the default size of the control.
/// </returns>
protected override Size DefaultSize
{
get
{
return new Size(defaultWidth, defaultWidth);
}
}
/// <summary>
/// Gets or sets the default cursor for the control.
/// </summary>
/// <returns>
/// An object of type <see cref="T:System.Windows.Forms.Cursor"/> representing the current
/// default cursor.
/// </returns>
protected override Cursor DefaultCursor
{
get
{
switch (this.Dock)
{
case DockStyle.Top:
case DockStyle.Bottom:
return Cursors.HSplit;
case DockStyle.Left:
case DockStyle.Right:
return Cursors.VSplit;
default:
return base.DefaultCursor;
}
}
}
/// <summary>
/// Returns the parameters needed to create the handle.
/// </summary>
/// <returns>
/// A <see cref="T:System.Windows.Forms.CreateParams"/> that contains the required creation
/// parameters when the handle to the control is created.
/// </returns>
protected override CreateParams CreateParams
{
[SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
get
{
CreateParams createParams = base.CreateParams;
createParams.ExStyle &= ~WS_EX_CLIENTEDGE;
createParams.Style &= ~WS_BORDER;
switch (this.borderStyle)
{
case BorderStyle.FixedSingle:
createParams.Style |= WS_EX_CLIENTEDGE;
break;
case BorderStyle.Fixed3D:
createParams.ExStyle |= WS_BORDER;
break;
}
return createParams;
}
}
/// <summary>
/// Gets the default Input Method Editor (IME) mode supported by this control.
/// </summary>
/// <returns>One of the <see cref="T:System.Windows.Forms.ImeMode"/> values.</returns>
protected override ImeMode DefaultImeMode
{
get
{
return ImeMode.Disable;
}
}
private bool Horizontal
{
get
{
DockStyle dock = this.Dock;
return dock == DockStyle.Left || dock == DockStyle.Right;
}
}
private bool CaptureInternal
{
get
{
return base.IsHandleCreated && GetCapture() == base.Handle;
}
set
{
if (this.CaptureInternal != value)
{
if (value)
{
SetCapture(base.Handle);
return;
}
ReleaseCapture();
}
}
}
/// <summary>
/// Returns a string that represents the <see cref="T:System.Windows.Forms.Splitter"/> control.
/// </summary>
/// <returns>A string that represents the current <see cref="T:System.Windows.Forms.Splitter"/>.</returns>
public override string ToString()
{
string text = base.ToString();
return string.Concat(new string[]
{
text,
", MinExtra: ",
this.MinExtra.ToString(CultureInfo.CurrentCulture),
", MinSize: ",
this.MinSize.ToString(CultureInfo.CurrentCulture)
});
}
/// <summary>
/// Raises the <see cref="E:System.Windows.Forms.Control.MouseDown"/> event.
/// </summary>
/// <param name="e">
/// A <see cref="T:System.Windows.Forms.MouseEventArgs"/> that contains the event data.
/// </param>
protected override void OnMouseDown(MouseEventArgs e)
{
base.OnMouseDown(e);
if (e.Button == MouseButtons.Left && e.Clicks == 1)
{
this.SplitBegin(e.X, e.Y);
}
}
/// <summary>
/// Raises the <see cref="E:System.Windows.Forms.Control.MouseMove"/> event.
/// </summary>
/// <param name="e">
/// A <see cref="T:System.Windows.Forms.MouseEventArgs"/> that contains the event data.
/// </param>
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
if (this.splitTarget != null)
{
int x = e.X + base.Left;
int y = e.Y + base.Top;
var splitLine = this.CalcSplitLine(this.GetSplitSize(e.X, e.Y), 0);
int splitX = splitLine.left;
int splitY = splitLine.top;
this.OnSplitterMoving(new SplitterEventArgs(x, y, splitX, splitY));
}
}
/// <summary>
/// Raises the <see cref="E:System.Windows.Forms.Control.MouseUp"/> event.
/// </summary>
/// <param name="e">
/// A <see cref="T:System.Windows.Forms.MouseEventArgs"/> that contains the event data.
/// </param>
protected override void OnMouseUp(MouseEventArgs e)
{
base.OnMouseUp(e);
if (this.splitTarget != null)
{
this.CalcSplitLine(this.GetSplitSize(e.X, e.Y), 0);
this.SplitEnd(true);
}
}
/// <summary>
/// Raises the <see cref="E:System.Windows.Forms.Splitter.SplitterMoving"/> event.
/// </summary>
/// <param name="sevent">
/// A <see cref="T:System.Windows.Forms.SplitterEventArgs"/> that contains the event data.
/// </param>
protected virtual void OnSplitterMoving(SplitterEventArgs sevent)
{
SplitterEventHandler splitterEventHandler = (SplitterEventHandler)base.Events[EVENT_MOVING];
if (splitterEventHandler != null)
{
splitterEventHandler(this, sevent);
}
if (this.splitTarget != null)
{
this.SplitMove(sevent.SplitX, sevent.SplitY);
}
}
/// <summary>
/// Raises the <see cref="E:System.Windows.Forms.Splitter.SplitterMoved"/> event.
/// </summary>
/// <param name="sevent">
/// A <see cref="T:System.Windows.Forms.SplitterEventArgs"/> that contains the event data.
/// </param>
protected virtual void OnSplitterMoved(SplitterEventArgs sevent)
{
SplitterEventHandler splitterEventHandler = (SplitterEventHandler)base.Events[EVENT_MOVED];
if (splitterEventHandler != null)
{
splitterEventHandler(this, sevent);
}
if (this.splitTarget != null)
{
this.SplitMove(sevent.SplitX, sevent.SplitY);
}
}
/// <param name="x">
/// The new <see cref="P:System.Windows.Forms.Control.Left"/> property value of the control.
/// </param>
/// <param name="y">
/// The new <see cref="P:System.Windows.Forms.Control.Top"/> property value of the control.
/// </param>
/// <param name="width">
/// The new <see cref="P:System.Windows.Forms.Control.Width"/> property value of the control.
/// </param>
/// <param name="height">
/// The new <see cref="P:System.Windows.Forms.Control.Height"/> property value of the control.
/// </param>
/// <param name="specified">
/// A bitwise combination of the <see cref="T:System.Windows.Forms.BoundsSpecified"/> values.
/// </param>
protected override void SetBoundsCore(int x, int y, int width, int height, BoundsSpecified specified)
{
if (this.Horizontal)
{
if (width < 1)
{
width = defaultWidth;
}
this.splitterThickness = width;
}
else
{
if (height < 1)
{
height = defaultWidth;
}
this.splitterThickness = height;
}
base.SetBoundsCore(x, y, width, height, specified);
}
private void DrawSplitBar(int mode)
{
var newRect = default(RECT);
if (mode != DRAW_END)
{
newRect = CalcSplitLine(splitSize, defaultWidth);
}
if (lastDrawSplit >= 0 && mode != DRAW_START)
{
var oldRect = CalcSplitLine(lastDrawSplit, defaultWidth);
if (mode != DRAW_END)
{
var rect = newRect;
SubtractRect(out newRect, ref newRect, ref oldRect);
SubtractRect(out oldRect, ref oldRect, ref rect);
}
DrawSplitHelper(oldRect);
}
if (mode == DRAW_END)
{
lastDrawSplit = -1;
}
else
{
DrawSplitHelper(newRect);
lastDrawSplit = splitSize;
}
}
private RECT CalcSplitLine(int splitSize, int minWeight)
{
Rectangle bounds = base.Bounds;
Rectangle targetBounds = this.splitTarget.Bounds;
switch (this.Dock)
{
case DockStyle.Top:
if (bounds.Height < minWeight)
{
bounds.Height = minWeight;
}
bounds.Y = targetBounds.Y + splitSize;
break;
case DockStyle.Bottom:
if (bounds.Height < minWeight)
{
bounds.Height = minWeight;
}
bounds.Y = targetBounds.Y + targetBounds.Height - splitSize - bounds.Height;
break;
case DockStyle.Left:
if (bounds.Width < minWeight)
{
bounds.Width = minWeight;
}
bounds.X = targetBounds.X + splitSize;
break;
case DockStyle.Right:
if (bounds.Width < minWeight)
{
bounds.Width = minWeight;
}
bounds.X = targetBounds.X + targetBounds.Width - splitSize - bounds.Width;
break;
}
var result = default(RECT);
result.left = bounds.Left;
result.top = bounds.Top;
result.right = bounds.Right;
result.bottom = bounds.Bottom;
return result;
}
private int CalcSplitSize()
{
Control control = this.FindTarget();
if (control == null)
{
return -1;
}
Rectangle bounds = control.Bounds;
switch (this.Dock)
{
case DockStyle.Top:
case DockStyle.Bottom:
return bounds.Height;
case DockStyle.Left:
case DockStyle.Right:
return bounds.Width;
default:
return -1;
}
}
private SplitData CalcSplitBounds()
{
SplitData splitData = new SplitData();
Control targetControl = this.FindTarget();
splitData.target = targetControl;
if (targetControl != null)
{
switch (targetControl.Dock)
{
case DockStyle.Top:
case DockStyle.Bottom:
this.initTargetSize = targetControl.Bounds.Height;
break;
case DockStyle.Left:
case DockStyle.Right:
this.initTargetSize = targetControl.Bounds.Width;
break;
}
Control parentInternal = this.Parent;
Control.ControlCollection controls = parentInternal.Controls;
int count = controls.Count;
int dockWidth = 0;
int dockHeight = 0;
for (int i = 0; i < count; i++)
{
Control childControl = controls[i];
if (childControl != targetControl)
{
switch (childControl.Dock)
{
case DockStyle.Top:
case DockStyle.Bottom:
dockHeight += childControl.Height;
break;
case DockStyle.Left:
case DockStyle.Right:
dockWidth += childControl.Width;
break;
}
}
}
Size clientSize = parentInternal.ClientSize;
if (this.Horizontal)
{
this.maxSize = clientSize.Width - dockWidth - this.minExtra;
}
else
{
this.maxSize = clientSize.Height - dockHeight - this.minExtra;
}
splitData.dockWidth = dockWidth;
splitData.dockHeight = dockHeight;
}
return splitData;
}
private void DrawSplitHelper(RECT rectangle)
{
if (this.splitTarget == null)
{
return;
}
IntPtr handle = Parent.Handle;
IntPtr dCEx = GetDCEx(handle, IntPtr.Zero, DCX_LOCKWINDOWUPDATE | DCX_CACHE);
var brush = default(LOGBRUSH);
brush.lbColor = 0x2A2A2A; // Invert alternate bits except highest: ..#.#.#.
//brush.lbColor = 0x555555;
IntPtr brushHandle = CreateBrushIndirect(ref brush);
IntPtr oldObject = SelectObject(dCEx, brushHandle);
PatBlt(
dCEx,
rectangle.left,
rectangle.top,
rectangle.right - rectangle.left,
rectangle.bottom - rectangle.top,
PATINVERT);
SelectObject(dCEx, oldObject);
DeleteObject(brushHandle);
ReleaseDC(handle, dCEx);
}
private Control FindTarget()
{
Control parentInternal = Parent;
if (parentInternal == null)
{
return null;
}
Control.ControlCollection controls = parentInternal.Controls;
int count = controls.Count;
DockStyle dock = this.Dock;
for (int i = 0; i < count; i++)
{
Control control = controls[i];
if (control != this)
{
switch (dock)
{
case DockStyle.Top:
if (control.Bottom == base.Top)
{
return control;
}
break;
case DockStyle.Bottom:
if (control.Top == base.Bottom)
{
return control;
}
break;
case DockStyle.Left:
if (control.Right == base.Left)
{
return control;
}
break;
case DockStyle.Right:
if (control.Left == base.Right)
{
return control;
}
break;
}
}
}
return null;
}
private int GetSplitSize(int x, int y)
{
int delta;
if (this.Horizontal)
{
delta = x - this.anchor.X;
}
else
{
delta = y - this.anchor.Y;
}
int val = 0;
switch (this.Dock)
{
case DockStyle.Top:
val = this.splitTarget.Height + delta;
break;
case DockStyle.Bottom:
val = this.splitTarget.Height - delta;
break;
case DockStyle.Left:
val = this.splitTarget.Width + delta;
break;
case DockStyle.Right:
val = this.splitTarget.Width - delta;
break;
}
return Math.Max(Math.Min(val, this.maxSize), this.minSize);
}
private void SplitBegin(int x, int y)
{
SplitData splitData = this.CalcSplitBounds();
if (splitData.target != null && this.minSize < this.maxSize)
{
this.anchor = new Point(x, y);
this.splitTarget = splitData.target;
this.splitSize = this.GetSplitSize(x, y);
if (this.splitterMessageFilter != null)
{
this.splitterMessageFilter = new SplitterMessageFilter(this);
}
Application.AddMessageFilter(this.splitterMessageFilter);
CaptureInternal = true;
this.DrawSplitBar(DRAW_START);
}
}
private void SplitEnd(bool accept)
{
this.DrawSplitBar(DRAW_END);
this.splitTarget = null;
CaptureInternal = false;
if (this.splitterMessageFilter != null)
{
Application.RemoveMessageFilter(this.splitterMessageFilter);
this.splitterMessageFilter = null;
}
if (accept)
{
this.ApplySplitPosition();
}
else if (this.splitSize != this.initTargetSize)
{
this.SplitPosition = this.initTargetSize;
}
this.anchor = Point.Empty;
}
private void ApplySplitPosition()
{
this.SplitPosition = this.splitSize;
}
private void SplitMove(int x, int y)
{
int size = this.GetSplitSize(x - base.Left + this.anchor.X, y - base.Top + this.anchor.Y);
if (this.splitSize != size)
{
this.splitSize = size;
this.DrawSplitBar(DRAW_MOVE);
}
}
private class SplitterMessageFilter : IMessageFilter
{
private SplitterEx owner;
public SplitterMessageFilter(SplitterEx splitter)
{
this.owner = splitter;
}
[SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
public bool PreFilterMessage(ref Message m)
{
if (m.Msg >= 256 && m.Msg <= 264)
{
if (m.Msg == 256 && (int)((long)m.WParam) == 27)
{
this.owner.SplitEnd(false);
}
return true;
}
return false;
}
}
private class SplitData
{
public int dockWidth = -1;
public int dockHeight = -1;
internal Control target;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment