/// Project : Kopano OL Extension /// /// Copyright 2016 Kopano b.v. /// /// This program is free software: you can redistribute it and/or modify /// it under the terms of the GNU Affero General Public License, version 3, /// as published by the Free Software Foundation. /// /// This program is distributed in the hope that it will be useful, /// but WITHOUT ANY WARRANTY; without even the implied warranty of /// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the /// GNU Affero General Public License for more details. /// /// You should have received a copy of the GNU Affero General Public License /// along with this program.If not, see. /// /// Consult LICENSE file for details using Acacia.Native; using Acacia.Utils; using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Drawing; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace Acacia.Controls { //[Designer(typeof(KopanoTreeViewDesigner))] public class KTree : UserControl { #region Checkboxes public class CheckStateChangedEventArgs : EventArgs { public readonly KTreeNode Node; public CheckStateChangedEventArgs(KTreeNode node) { this.Node = node; } } public delegate void CheckStateChangedHandler(object sender, CheckStateChangedEventArgs e); public event CheckStateChangedHandler CheckStateChanged; internal void OnCheckStateChanged(KTreeNode node) { if (CheckStateChanged != null) { CheckStateChanged(this, new CheckStateChangedEventArgs(node)); } } private KCheckManager _checkManager; [Browsable(false)] public KCheckManager CheckManager { get { return _checkManager; } set { _checkManager = value; Rerender(); } } public KCheckStyle CheckStyle { get { return _checkManager == null ? KCheckStyle.None : _checkManager.CheckStyle; } set { switch(value) { case KCheckStyle.TwoState: _checkManager = new KCheckManager.TwoState(); break; case KCheckStyle.ThreeState: _checkManager = new KCheckManager.ThreeState(); break; case KCheckStyle.Recursive: _checkManager = new KCheckManager.Recursive(); break; case KCheckStyle.RecursiveThreeState: _checkManager = new KCheckManager.RecursiveThreeState(); break; default: _checkManager = null; break; } } } private void ToggleCheck(KTreeNode node) { if (_checkManager == null || node == null) return; if (!SelectedNodes.Contains(node) || SelectedNodes.Count == 1) { // Update the single node if it's not part of the selection, or it's the only selection _checkManager.ToggleCheck(node); } else { // Update all selected nodes BeginUpdate(); try { _checkManager.ToggleCheck(SelectedNodes); } finally { EndUpdate(); } } } #endregion #region Properties private Padding _nodePadding = new Padding(2, 4, 2, 4); public Padding NodePadding { get { return _nodePadding; } set { _nodePadding = value; Rerender(); } } private int _nodeIdent = 8; public int NodeIndent { get { return _nodeIdent; } set { _nodeIdent = value; Rerender(); } } #endregion #region Images private ImageList _images; public ImageList Images { get { return _images; } set { _images = value; Rerender(); } } #endregion #region Nodes private readonly KTreeNodes _rootNodes; [Browsable(false)] public KTreeNodes RootNodes { get { return _rootNodes; } } #endregion #region Creation public KTree() { SetStyle(ControlStyles.ResizeRedraw, true); SetStyle(ControlStyles.OptimizedDoubleBuffer, true); SetStyle(ControlStyles.AllPaintingInWmPaint, true); SetStyle(ControlStyles.Selectable, true); BackColor = SystemColors.Window; _rootNodes = new KTreeNodes(this); SetupRenderer(); InitScrollBars(); } #endregion #region Selection private bool _fullRowSelect = true; public bool FullRowSelect { get { return _fullRowSelect; } set { _fullRowSelect = value; Rerender(); } } internal KTreeNode ActiveNode { get; private set; } private int ActiveNodeIndex { get { return ActiveNode == null ? -1 : _presentNodes.IndexOf(ActiveNode); } } private KSelectionManager _selectionManager = new KSelectionManager.Multiple(); public bool MultipleSelection { get { return _selectionManager is KSelectionManager.Multiple; } set { _selectionManager = value ? new KSelectionManager.Multiple() : (KSelectionManager)new KSelectionManager.Single(); } } [Browsable(false)] public IReadOnlyCollection SelectedNodes { get { return _selectionManager.CurrentSelection; } } /// /// Selects a single node and makes it the active node. /// /// The node. Pass null to deselect any nodes /// Set to a specific mode to scroll the node into view public void SelectNode(KTreeNode node, ScrollMode scroll = ScrollMode.None) { DoSelectNode(node, SelectAction.Set, scroll); } private enum SelectAction { Set, Toggle, Range, AddRange, Activate } private KTreeNode _selectRangeAnchor; private void DoSelectNode(KTreeNode node, SelectAction action, ScrollMode scroll) { if (action == SelectAction.Set || action == SelectAction.Range) _selectionManager.Clear(); if (node != null) { switch(action) { case SelectAction.Range: case SelectAction.AddRange: if (_selectRangeAnchor == null) { _selectRangeAnchor = node; _selectionManager.Add(node); break; } // Select any nodes from the anchor to the current node int activeIndex = Math.Max(0, _presentNodes.IndexOf(_selectRangeAnchor)); int nodeIndex = _presentNodes.IndexOf(node); // Keep to order just in case the selection manager wants it if (activeIndex > nodeIndex) { for (int i = nodeIndex; i <= activeIndex; ++i) if (_presentNodes[i].IsSelectable) _selectionManager.Add(_presentNodes[i]); } else { for (int i = activeIndex; i <= nodeIndex; ++i) if (_presentNodes[i].IsSelectable) _selectionManager.Add(_presentNodes[i]); } break; case SelectAction.Set: _selectRangeAnchor = node; _selectionManager.Add(node); break; case SelectAction.Toggle: _selectRangeAnchor = node; _selectionManager.Toggle(node); break; } if (scroll != ScrollMode.None) ScrollIntoView(node, scroll); } ActiveNode = node; // Must rerender // TODO: affected nodes only Rerender(); // Raise event if needed CheckSelectionChanged(); } public class SelectionChangedEventArgs : EventArgs { public readonly KTreeNode[] SelectedNodes; public SelectionChangedEventArgs(KTreeNode[] selectedNodes) { this.SelectedNodes = selectedNodes; } } public delegate void SelectionChangedDelegate(object sender, SelectionChangedEventArgs e); public event SelectionChangedDelegate SelectionChanged; private readonly List _previousSelection = new List(); private void CheckSelectionChanged() { if (_updateCount != 0) return; IReadOnlyCollection selection = _selectionManager.CurrentSelection; if (!selection.SameElements(_previousSelection)) { _previousSelection.Clear(); _previousSelection.AddRange(selection); OnSelectionChanged(new SelectionChangedEventArgs(selection.ToArray())); } } virtual protected void OnSelectionChanged(SelectionChangedEventArgs e) { if (SelectionChanged != null) SelectionChanged(this, e); } #endregion #region Mouse handling protected override void OnMouseDown(MouseEventArgs e) { base.OnMouseDown(e); if ((e.Button & MouseButtons.Left) != 0) { HitTestResult? hit = HitTest(e.Location); if (hit != null && hit.Value.Node.IsSelectable) { switch(hit.Value.Part) { case KTreeNodeMeasurements.Part.Expander: hit.Value.Node.ToggleExpanded(); break; case KTreeNodeMeasurements.Part.CheckBox: ToggleCheck(hit.Value.Node); break; case KTreeNodeMeasurements.Part.Text: case KTreeNodeMeasurements.Part.None: case KTreeNodeMeasurements.Part.Image: DoSelectNode(hit.Value.Node, ActionFromModifiers(false), ScrollMode.Auto); break; } } } } private SelectAction ActionFromModifiers(bool isKeyboard) { if (ModifierKeys == (Keys.Shift | Keys.Control)) return SelectAction.AddRange; else if (ModifierKeys == Keys.Shift) return SelectAction.Range; else if (ModifierKeys == Keys.Control) { if (isKeyboard) return SelectAction.Activate; else return SelectAction.Toggle; } else return SelectAction.Set; } protected override void OnMouseDoubleClick(MouseEventArgs e) { if ((e.Button & MouseButtons.Left) != 0) { HitTestResult? hit = HitTest(e.Location, KTreeNodeMeasurements.Part.Text, KTreeNodeMeasurements.Part.Image); if (hit != null && hit.Value.Node.IsSelectable) { hit.Value.Node.ToggleExpanded(); } } } private KTreeNode _highlightNode; private KTreeNodeMeasurements.Part? _highlightPart; protected override void OnMouseMove(MouseEventArgs e) { CheckMouseHighlight(); } protected override void OnMouseLeave(EventArgs e) { // The mouse might be over a control hosted in a node, so check highlighting again CheckMouseHighlight(); } private void CheckMouseHighlight() { HitTestResult? hit = HitTest(PointToClient(MousePosition)); HighlightNode(hit?.Node, hit?.Part); } private void HighlightNode(KTreeNode newHighlight, KTreeNodeMeasurements.Part? newPart) { if (newHighlight != _highlightNode || _highlightPart != newPart) { KTreeNode old = _highlightNode; if (newHighlight != null && !newHighlight.IsSelectable) { _highlightNode = null; _highlightPart = null; } else { _highlightNode = newHighlight; _highlightPart = newPart; } // Render old node without highlight if (old != null) Rerender(old); // Render new node if (_highlightNode != null) Rerender(_highlightNode); } } protected override void OnMouseWheel(MouseEventArgs e) { // Let the scrollbar handle the scrolling if (HaveVerticalScrollBar) _verticalScrollBar.ForwardMouseWheel(e); } #endregion #region Hit testing private struct HitTestResult { public KTreeNode Node; public KTreeNodeMeasurements.Part Part; } private HitTestResult? HitTest(Point location, params KTreeNodeMeasurements.Part[] wanted) { if (location.X < 0 || location.X >= ViewRectangle.Width) return null; KTreeNode node = NodeAtY(location.Y); if (node == null) return null; KTreeNodeMeasurements.Part? part = node.EffectiveDimension.HitTest(location.X + _horizontalScrollBar.Value); if (part != null) { // Check if it's the part we're interested in if (wanted.Length > 0 && !wanted.Contains(part.Value)) return null; // Part.None is valid only if full row selection is enabled if (!FullRowSelect && part.Value == KTreeNodeMeasurements.Part.None) return null; // Success return new HitTestResult() { Node = node, Part = part.Value }; } return null; } private KTreeNode NodeAtY(int y) { int index = NodeIndexAtY(y); if (index < 0 || index >= _presentNodes.Count) return null; return _presentNodes[index]; } private int NodeIndexAtY(int y) { // TODO: use a secondary index, or assume all rows are the same height? y += _verticalScrollBar.Value; if (y < 0) return -1; for(int i = 0; i < _presentNodes.Count; ++i) { if (_presentNodes[i].EffectiveDimension.NodeRect.ContainsY(y)) return i; } return _presentNodes.Count; } #endregion #region Keyboard handling protected override void OnPreviewKeyDown(PreviewKeyDownEventArgs e) { switch (e.KeyCode) { case Keys.Up: KeySelect(-1); break; case Keys.Down: KeySelect(1); break; case Keys.PageUp: KeySelect(-PageSize); break; case Keys.PageDown: KeySelect(PageSize); break; case Keys.Home: KeySelect(-_presentNodes.Count); break; case Keys.End: KeySelect(_presentNodes.Count); break; case Keys.Left: KeyExpand(false); break; case Keys.Right: KeyExpand(true); break; case Keys.Space: if (ModifierKeys == Keys.Control || _checkManager == null) { DoSelectNode(ActiveNode, SelectAction.Toggle, ScrollMode.Auto); } else if (_checkManager != null) { ToggleCheck(ActiveNode); } break; default: return; } e.IsInputKey = true; } private int PageSize { get { int firstVisible = NodeIndexAtY(0); int count = 0; for (int i = 0; i < _presentNodes.Count; ++i) { Rectangle nodeRect = _presentNodes[i].EffectiveDimension.NodeRect; if (nodeRect.Bottom > ViewRectangle.Bottom) break; else if (nodeRect.Top >= ViewRectangle.Top) ++count; } return count; } } private void KeyExpand(bool expand) { if (ActiveNode == null) return; if (expand) { if (ActiveNode.ChildLoader.NeedsExpander) { if (!ActiveNode.IsExpanded) ActiveNode.IsExpanded = true; else DoSelectNode(ActiveNode.Children.First(), ActionFromModifiers(true), ScrollMode.Auto); } } else { if (ActiveNode.IsExpanded) ActiveNode.IsExpanded = false; else if (ActiveNode.Parent != null) DoSelectNode(ActiveNode.Parent, ActionFromModifiers(true), ScrollMode.Auto); } } private void KeySelect(int dir) { int currentIndex = ActiveNodeIndex; for (;;) { currentIndex = currentIndex + dir; currentIndex = Math.Max(Math.Min(currentIndex, _presentNodes.Count - 1), 0); KTreeNode node = currentIndex < _presentNodes.Count ? _presentNodes[currentIndex] : null; if (node != null && !node.IsSelectable) continue; DoSelectNode(node, ActionFromModifiers(true), dir > 0 ? ScrollMode.Bottom : ScrollMode.Top); break; } } #endregion #region Columns // TODO /* private readonly TreeViewColumnCollection _columns; [Category("Columns")] [Browsable(true)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] public TreeViewColumnCollection Columns { get { return _columns; } }*/ #endregion #region Rendering private KTreeRenderer _renderer; public void SetupRenderer(bool enableVisualStyles = true) { if (enableVisualStyles && Application.RenderWithVisualStyles) _renderer = new KTreeRendererVisualStyles(); else _renderer = new KTreeRendererDefault(); Rerender(); } /// /// The nodes that are currently present, i.e. their parents are expanded. /// private readonly List _presentNodes = new List(); internal void OnNodeExpandedChanged(KTreeNode node) { BeginUpdate(); try { if (node.IsExpanded) { int index = _presentNodes.IndexOf(node); if (index < 0) return; // Add the child nodes InsertChildNodes(node, index + 1); } else { RemoveChildNodes(node); } } finally { EndUpdate(); } } private void RemoveChildNodes(KTreeNode parent) { int index = _presentNodes.IndexOf(parent); if (index < 0) return; // Remove any node that's deeper than the current node int depth = parent.Depth; int first = index + 1; int past = first; while (past < _presentNodes.Count) { if (_presentNodes[past].Depth > depth) ++past; else break; } if (past > first) _presentNodes.RemoveRange(first, past - first); } internal void OnNodeChildrenChanged(KTreeNode node) { Rerender(node); } internal void OnNodeAdded(KTreeNode parent, KTreeNode node) { BeginUpdate(); try { if (parent == null) { // TODO: this probably leads to wrong order _presentNodes.Add(node); } else { if (!parent.IsExpanded) return; int index = _presentNodes.IndexOf(parent); if (index < 0) return; ++index; int depth = parent.Depth; while (index < _presentNodes.Count) { if (_presentNodes[index].Depth <= depth) break; ++index; } _presentNodes.Insert(index, node); } if (node.IsExpanded) { OnNodeExpandedChanged(node); } } finally { EndUpdate(); } } internal void OnNodeRemoved(KTreeNode parent, KTreeNode node) { throw new NotImplementedException(); } internal void OnNodeCleared(KTreeNode parent) { BeginUpdate(); try { if (parent == null) { // Root node cleared, means no more nodes _presentNodes.Clear(); } else { RemoveChildNodes(parent); } } finally { EndUpdate(); } } private int InsertChildNodes(KTreeNode parent, int index) { if (parent.IsExpanded) { foreach (KTreeNode node in parent.Children) { _presentNodes.Insert(index, node); index = InsertChildNodes(node, index + 1); } } return index; } private int _updateCount; public void BeginUpdate() { ++_updateCount; } public void EndUpdate() { --_updateCount; if (_updateCount == 0) { Rerender(); CheckSelectionChanged(); } } internal void Rerender(KTreeNode node = null) { if (_updateCount != 0) return; // TODO: use node MeasureNodes(); UpdateScrollBars(); CheckMouseHighlight(); Invalidate(); } private void MeasureNodes() { _renderer.Init(ViewRectangle, this); using (Graphics graphics = CreateGraphics()) { foreach (KTreeNode node in _presentNodes) { _renderer.MeasureNode(graphics, node); } } } private readonly List _nodeControls = new List(); protected override void OnPaint(PaintEventArgs e) { int firstVisibleNode = NodeIndexAtY(0); List visibleControls = new List(); for (int i = firstVisibleNode; i < _presentNodes.Count; ++i) { KTreeNode node = _presentNodes[i]; // Stop rendering when we're out of view if (node.EffectiveDimension.NodeRect.Y - _verticalScrollBar.Value >= ViewRectangle.Bottom) break; // Render the node _renderer.RenderNode(e.Graphics, node, new Point(_horizontalScrollBar.Value, _verticalScrollBar.Value), node == _highlightNode ? _highlightPart : null); // May have to add the control if (node.Control != null) { if (node.Control.Parent == null) { _nodeControls.Add(node.Control); node.Control.Parent = this; } visibleControls.Add(node.Control); } } // Check if any controls became invisible for(int i = 0; i < _nodeControls.Count;) { if (!visibleControls.Contains(_nodeControls[i])) { _nodeControls[i].Parent = null; _nodeControls.RemoveAt(i); } else { ++i; } } // Fill in a rectangle below the scrollbars, as that may be rendered. If they are not visible, the width or height // automatically becomes 0 e.Graphics.FillRectangle(SystemBrushes.Control, ClientSize.Width - VerticalScrollBarWidth, ClientSize.Height - HorizontalScrollBarHeight, VerticalScrollBarWidth, HorizontalScrollBarHeight); } #endregion #region Scrollbars private class VScrollBar2 : VScrollBar { public VScrollBar2() { } internal void ForwardMouseWheel(MouseEventArgs e) { OnMouseWheel(e); } } private class HScrollBar2 : HScrollBar { internal void ForwardMouseWheel(MouseEventArgs e) { OnMouseWheel(e); } } private readonly VScrollBar2 _verticalScrollBar = new VScrollBar2(); private readonly HScrollBar2 _horizontalScrollBar = new HScrollBar2(); private void InitScrollBars() { _verticalScrollBar.Scroll += _scrollBar_Scroll; _verticalScrollBar.PreviewKeyDown += _verticalScrollBar_PreviewKeyDown; Controls.Add(_verticalScrollBar); _horizontalScrollBar.Scroll += _scrollBar_Scroll; _horizontalScrollBar.PreviewKeyDown += _verticalScrollBar_PreviewKeyDown; Controls.Add(_horizontalScrollBar); } private void _verticalScrollBar_PreviewKeyDown(object sender, PreviewKeyDownEventArgs e) { } private void _scrollBar_Scroll(object sender, ScrollEventArgs e) { // Mouse might be over different node now CheckMouseHighlight(); // Repaint Invalidate(); } protected override void OnSizeChanged(EventArgs e) { base.OnSizeChanged(e); MeasureNodes(); UpdateScrollBars(); } // TODO: this goes wrong if resizing makes a scrollbar appear/disappear private void UpdateScrollBars() { // May happen during init if (ViewRectangle.Height < 0) return; // Update scrollbar ranges _verticalScrollBar.Minimum = 0; _verticalScrollBar.Maximum = _renderer.TotalRect.Height; _horizontalScrollBar.Minimum = 0; _horizontalScrollBar.Maximum = _renderer.TotalRect.Width; // Set change sizes if (_presentNodes.Count > 0) _verticalScrollBar.SmallChange = _presentNodes.First().EffectiveDimension.NodeRect.Height; _verticalScrollBar.LargeChange = ViewRectangle.Height; _horizontalScrollBar.SmallChange = NodeIndent; _horizontalScrollBar.LargeChange = Math.Max(0, ViewRectangle.Width); // Negative on miminize if (_verticalScrollBar.LargeChange >= _verticalScrollBar.Maximum) _verticalScrollBar.Value = 0; // Set the positions, make them size 0 if not required _verticalScrollBar.SetBounds(ClientSize.Width - VerticalScrollBarWidth, 0, VerticalScrollBarWidth, ClientSize.Height - HorizontalScrollBarHeight); _horizontalScrollBar.SetBounds(0, ClientSize.Height - HorizontalScrollBarHeight, ClientSize.Width - VerticalScrollBarWidth, HorizontalScrollBarHeight); } private bool HaveVerticalScrollBar { get { return _verticalScrollBar.LargeChange < _verticalScrollBar.Maximum; } } private int VerticalScrollBarWidth { get { return HaveVerticalScrollBar ? SystemInformation.VerticalScrollBarWidth : 0; } } private bool HaveHorizontalScrollBar { get { return _horizontalScrollBar.LargeChange < _horizontalScrollBar.Maximum; } } private int HorizontalScrollBarHeight { get { return HaveHorizontalScrollBar ? SystemInformation.HorizontalScrollBarHeight : 0; } } private Rectangle ViewRectangle { get { Rectangle r = ClientRectangle; r.Width -= VerticalScrollBarWidth; r.Height -= HorizontalScrollBarHeight; return r; } } private Rectangle ScrolledRectangle { get { Rectangle r = ClientRectangle; r.Width -= VerticalScrollBarWidth; r.Height -= HorizontalScrollBarHeight; r.X += _horizontalScrollBar.Value; r.Y += _verticalScrollBar.Value; return r; } } public enum ScrollMode { None, Auto, Top, Middle, Bottom } public void ScrollIntoView(KTreeNode node, ScrollMode mode) { if (mode == ScrollMode.None) return; if (!node.IsVisible) { //return; foreach (KTreeNode parent in node.Ancestors) { if (!parent.IsExpanded) { parent.IsExpanded = true; } else { break; } } } // Number of pixels from edge to keep node in Y direction. // TODO: this assumes all nodes are the same height int scrollBorderY = node.EffectiveDimension.NodeRect.Height; // Vertical // Do nothing if the node is already fully visible if (!ScrolledRectangle.ContainsY(node.EffectiveDimension.NodeRect.Top - scrollBorderY) || !ScrolledRectangle.ContainsY(node.EffectiveDimension.NodeRect.Bottom + scrollBorderY)) { if (mode == ScrollMode.Auto) { if (node.EffectiveDimension.NodeRect.Top + scrollBorderY < ScrolledRectangle.Y) mode = ScrollMode.Top; else mode = ScrollMode.Bottom; } switch (mode) { case ScrollMode.Top: SetVScroll(node.EffectiveDimension.NodeRect.Top - ViewRectangle.Top - scrollBorderY); break; case ScrollMode.Middle: SetVScroll((node.EffectiveDimension.NodeRect.Top + node.EffectiveDimension.NodeRect.Height / 2) - (ViewRectangle.Top + ViewRectangle.Height / 2)); break; case ScrollMode.Bottom: SetVScroll(node.EffectiveDimension.NodeRect.Bottom - ViewRectangle.Bottom + scrollBorderY); break; } } // Horizontal if (!ScrolledRectangle.ContainsX(node.EffectiveDimension.NodeRect.Left) || !ScrolledRectangle.ContainsX(node.EffectiveDimension.NodeRect.Right)) { // Align left or right, depending on which is the smallest change int alignLeft = node.EffectiveDimension.NodeRect.Left - ViewRectangle.Left; int alignRight = node.EffectiveDimension.NodeRect.Right - ViewRectangle.Right; if (Math.Abs(alignLeft - _horizontalScrollBar.Value) < Math.Abs(alignRight - _horizontalScrollBar.Value)) SetHScroll(alignLeft); else SetHScroll(alignRight); } // Check current highlight CheckMouseHighlight(); } private void SetVScroll(int value) { _verticalScrollBar.Value = Math.Max(_verticalScrollBar.Minimum, Math.Min(value, _verticalScrollBar.Maximum - _verticalScrollBar.LargeChange + 1)); } private void SetHScroll(int value) { _horizontalScrollBar.Value = Math.Max(_horizontalScrollBar.Minimum, Math.Min(value, _horizontalScrollBar.Maximum - _horizontalScrollBar.LargeChange + 1)); } #endregion #region Focus protected override void OnGotFocus(EventArgs e) { base.OnGotFocus(e); Invalidate(); } protected override void OnLostFocus(EventArgs e) { base.OnLostFocus(e); Invalidate(); } #endregion #region Winforms Autogenerated private void InitializeComponent() { this.SuspendLayout(); // // KTree // this.Name = "KTree"; this.ResumeLayout(false); } #endregion #region Disabled state protected override void OnEnabledChanged(EventArgs e) { base.OnEnabledChanged(e); RedrawBorder(); } protected override void WndProc(ref Message m) { if (m.Msg == (int)WM.NCPAINT) { WmNcPaint(ref m); return; } base.WndProc(ref m); } private void WmNcPaint(ref Message m) { if (BorderStyle == BorderStyle.None) return; IntPtr hDC = User32.GetWindowDC(m.HWnd); try { using (Graphics g = Graphics.FromHdc(hDC)) { _renderer.RenderControlBorder(g, new Rectangle(0, 0, Width, Height)); } } finally { User32.ReleaseDC(m.HWnd, hDC); } } protected override void OnResize(EventArgs e) { base.OnResize(e); RedrawBorder(); } private void RedrawBorder() { // Force NCPaint update User32.RedrawWindow(this.Handle, IntPtr.Zero, IntPtr.Zero, User32.RedrawWindowFlags.Frame | User32.RedrawWindowFlags.Invalidate /*| User32.RedrawWindowFlags.UpdateNow*/); } #endregion } }