Added edit and button portion of custom combo box.

This commit is contained in:
Patrick Simpson 2017-06-19 15:51:23 +02:00
parent 74d2d5857c
commit 833cb6e4d3
7 changed files with 690 additions and 2 deletions

View File

@ -220,6 +220,9 @@
<DependentUpon>KBusyIndicator.cs</DependentUpon>
</Compile>
<Compile Include="Controls\KCheckManager.cs" />
<Compile Include="Controls\KComboBox.cs">
<SubType>UserControl</SubType>
</Compile>
<Compile Include="Controls\KCopyLabel.cs">
<SubType>Component</SubType>
</Compile>
@ -251,6 +254,8 @@
<Compile Include="Controls\KTreeRendererVisualStyles.cs" />
<Compile Include="Controls\KUITask.cs" />
<Compile Include="Controls\KUIUtil.cs" />
<Compile Include="Controls\KVisualStateTracker.cs" />
<Compile Include="Controls\KVisualStyle.cs" />
<Compile Include="DebugOptions.cs" />
<Compile Include="Features\BCC\FeatureBCC.cs" />
<Compile Include="Features\DeliveryReceipts\FeatureDeliveryReceipts.cs" />
@ -319,6 +324,7 @@
<Compile Include="Native\MAPI\Property.cs" />
<Compile Include="Native\MAPI\Restriction.cs" />
<Compile Include="Native\MAPI\RestrictionEncoder.cs" />
<Compile Include="Native\UXTheme.cs" />
<Compile Include="OutlookConstants.cs" />
<Compile Include="SearchQuery.cs" />
<Compile Include="Stubs\Enums.cs" />
@ -403,7 +409,7 @@
<SubType>UserControl</SubType>
</Compile>
<Compile Include="UI\GABLookupControl.cs">
<SubType>Component</SubType>
<SubType>UserControl</SubType>
</Compile>
<Compile Include="UI\GABLookupControl.Designer.cs">
<DependentUpon>GABLookupControl.cs</DependentUpon>

View File

@ -0,0 +1,168 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace Acacia.Controls
{
public class KComboBox : UserControl
{
#region Properties
public ComboBoxStyle DropDownStyle
{
get;
set;
}
public string DisplayMember
{
get;
set;
}
#endregion
#region Components
private TextBox _edit;
#endregion
#region Init
public KComboBox()
{
SetupRenderer();
_edit = new TextBox();
_edit.BorderStyle = BorderStyle.None;
Controls.Add(_edit);
_state.AddControl(_edit);
}
#endregion
#region Drop down
private void Button_Clicked()
{
}
#endregion
#region Rendering
private enum State
{
// Values match those defined in vsstyles.h so no conversion is needed.
Normal = 1, Hot = 2, Pressed = 3, Disabled = 4
}
private KVisualStyle<COMBOBOXPARTS, State> _style = new KVisualStyle<COMBOBOXPARTS, State>("COMBOBOX");
// Enum from vsstyles.h
enum COMBOBOXPARTS
{
CP_DROPDOWNBUTTON = 1,
CP_BACKGROUND = 2,
CP_TRANSPARENTBACKGROUND = 3,
CP_BORDER = 4,
CP_READONLY = 5,
CP_DROPDOWNBUTTONRIGHT = 6,
CP_DROPDOWNBUTTONLEFT = 7,
CP_CUEBANNER = 8,
};
private KVisualStateTracker<State> _state;
private KVisualStateTracker<State>.Part _stateButton;
public void SetupRenderer(bool enableVisualStyles = true)
{
SetStyle(ControlStyles.AllPaintingInWmPaint, true);
SetStyle(ControlStyles.ContainerControl, true);
SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
SetStyle(ControlStyles.ResizeRedraw, true);
SetStyle(ControlStyles.Selectable, true);
SetStyle(ControlStyles.SupportsTransparentBackColor, true);
SetStyle(ControlStyles.UserMouse, true);
SetStyle(ControlStyles.UserPaint, true);
SetStyle(ControlStyles.Selectable, true);
_style[COMBOBOXPARTS.CP_DROPDOWNBUTTON].SetPadding(COMBOBOXPARTS.CP_DROPDOWNBUTTONLEFT,
COMBOBOXPARTS.CP_DROPDOWNBUTTONRIGHT);
_state = new KVisualStateTracker<State>(this, State.Normal, State.Disabled);
_state.Root.WithHot(State.Hot);
_state.Root.WithFocus(State.Hot);
_stateButton = _state.Root.AddPart().WithPressed(State.Pressed);
_stateButton.Clicked += Button_Clicked;
// TODO if (enableVisualStyles && Application.RenderWithVisualStyles)
}
protected override void OnPaint(PaintEventArgs e)
{
_style[COMBOBOXPARTS.CP_BORDER]?.DrawBackground(e.Graphics, _state.Root.State, ClientRectangle);
_style[COMBOBOXPARTS.CP_DROPDOWNBUTTON]?.DrawBackground(e.Graphics, _stateButton.State, _stateButton.Rectangle);
}
#endregion
#region Layout
protected override void OnLayout(LayoutEventArgs e)
{
base.OnLayout(e);
using (Graphics graphics = CreateGraphics())
{
// Determine the border insets
Padding insets = _style[COMBOBOXPARTS.CP_BORDER]?.GetMargins(graphics, _state.Root.State) ?? new Padding();
// Determine the button size
Size? buttonSize = _style[COMBOBOXPARTS.CP_DROPDOWNBUTTON]?.GetPartSize(graphics, _state.Root.State);
if (!buttonSize.HasValue)
buttonSize = new Size(ClientRectangle.Height, ClientRectangle.Height);
Rectangle buttonRect = new Rectangle();
buttonRect.X = ClientRectangle.Width - buttonSize.Value.Width;
buttonRect.Width = buttonSize.Value.Width;
buttonRect.Y = 0;
buttonRect.Height = ClientRectangle.Height;
_stateButton.Rectangle = buttonRect;
// Set the edit control
Rectangle editRect = new Rectangle(insets.Left, insets.Top, buttonRect.X - insets.Left,
ClientRectangle.Height - insets.Vertical);
editRect = editRect.CenterVertically(new Size(editRect.Width, _edit.PreferredHeight));
_edit.SetBounds(editRect.X, editRect.Y, editRect.Width, editRect.Height);
}
}
public override Size GetPreferredSize(Size proposedSize)
{
// TODO: cache sizes?
using (Graphics graphics = CreateGraphics())
{
Size editSize = _edit.GetPreferredSize(proposedSize);
Padding insets = _style[COMBOBOXPARTS.CP_BORDER]?.GetMargins(graphics, _state.Root.State) ?? new Padding();
Size prefSize = editSize.Expand(insets);
Size? buttonSize = _style[COMBOBOXPARTS.CP_DROPDOWNBUTTON]?.GetPartSize(graphics, _state.Root.State);
if (!buttonSize.HasValue)
buttonSize = new Size(prefSize.Height, prefSize.Height);
return new Size(prefSize.Width + buttonSize.Value.Width, Math.Max(prefSize.Height, buttonSize.Value.Height));
}
}
#endregion
}
}

View File

@ -35,6 +35,12 @@ namespace Acacia.Controls
return new Rectangle(x, y, size.Width, size.Height);
}
public static Rectangle CenterVertically(this Rectangle _this, Size size)
{
int y = _this.Y + (_this.Height - size.Height) / 2;
return new Rectangle(_this.X, y, size.Width, size.Height);
}
public static Rectangle Expand(this Rectangle _this, Padding padding)
{
Rectangle r = _this;
@ -75,6 +81,51 @@ namespace Acacia.Controls
return new Size((int)(_this.Width * graphics.DpiX / 96), (int)(_this.Height * graphics.DpiY / 96));
}
/// <summary>
/// Adds the sizes horizontally. The height is the maximum of any of the elements' height.
/// Any of the heights may be null.
/// </summary>
/// <returns>
/// The added sizes. Null is returned only if all sizes are null.
/// </returns>
public static Size? AddHorizontally(this Size? _this, params Size?[] add)
{
Size? s = _this;
foreach(Size? s2 in add)
{
if (s2.HasValue)
{
if (!s.HasValue)
s = s2;
else
s = new Size(s.Value.Width + s2.Value.Width, Math.Max(s.Value.Height, s2.Value.Height));
}
}
return s;
}
public static Size? AddHorizontally(this Size _this, params Size?[] add)
{
return AddHorizontally((Size?)_this, add);
}
public static Size Expand(this Size _this, Padding padding)
{
Size r = _this;
r.Width += padding.Horizontal;
r.Height += padding.Vertical;
return r;
}
public static Size Shrink(this Size _this, Padding padding)
{
Size r = _this;
r.Width -= padding.Horizontal;
r.Height -= padding.Vertical;
return r;
}
#endregion
}
}

View File

@ -0,0 +1,299 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace Acacia.Controls
{
internal class KVisualStateTracker<StateTypeId>
where StateTypeId: struct, IConvertible
{
public class Part
{
private KVisualStateTracker<StateTypeId> _tracker;
private readonly StateTypeId? _normalState;
private readonly StateTypeId? _disabledState;
private StateTypeId? _hotState;
private StateTypeId? _pressedState;
private StateTypeId? _focusState;
private bool _mouseOver;
private bool _mousePressed;
private bool _focused;
private readonly Part _parent;
private readonly List<Part> _children = new List<Part>();
private Rectangle? _rectangle;
public Action Clicked { get; set; }
public Part(KVisualStateTracker<StateTypeId> tracker, StateTypeId? normalState, StateTypeId? disabledState)
{
this._tracker = tracker;
this._normalState = normalState;
this._disabledState = disabledState;
}
public Part(KVisualStateTracker<StateTypeId> tracker, Part parent)
{
this._tracker = parent._tracker;
this._parent = parent;
}
public StateTypeId State
{
get
{
if (_parent != null && !_mouseOver)
{
return _parent.State;
}
if (!_tracker._control.Enabled)
{
return DisabledState;
}
if (_focused && FocusedState.HasValue)
return FocusedState.Value;
if (_mouseOver && _mousePressed)
return _pressedState.Value;
if (_mouseOver && HotState.HasValue)
return HotState.Value;
return NormalState;
}
}
public Rectangle Rectangle
{
get
{
if (_rectangle.HasValue)
return _rectangle.Value;
return _tracker._control.ClientRectangle;
}
set
{
if (_parent == null)
throw new InvalidOperationException("Cannot set rectangle on root element");
_rectangle = value;
}
}
private StateTypeId DisabledState
{
get
{
if (_disabledState.HasValue)
return _disabledState.Value;
return _parent.DisabledState;
}
}
private StateTypeId NormalState
{
get
{
if (_normalState.HasValue)
return _normalState.Value;
return _parent.NormalState;
}
}
private StateTypeId? HotState
{
get
{
if (_hotState.HasValue)
return _hotState.Value;
return _parent.HotState;
}
}
private StateTypeId? FocusedState
{
get
{
if (_focusState.HasValue)
return _focusState.Value;
return _parent.FocusedState;
}
}
private bool MouseOver
{
get { return _mouseOver; }
set
{
if (_mouseOver != value)
{
_mouseOver = value;
Invalidate();
}
}
}
private bool MousePressed
{
get { return _mousePressed; }
set
{
if (_mousePressed != value)
{
_mousePressed = value;
Invalidate();
}
}
}
private bool Focused
{
get { return _focused; }
set
{
if (_focused != value)
{
_focused = value;
Invalidate();
}
}
}
private void Invalidate()
{
_tracker.Invalidate();
}
internal void GotFocus(object sender, EventArgs e)
{
Focused = true;
}
internal void LostFocus(object sender, EventArgs e)
{
Focused = false;
}
internal void MouseDown(object sender, MouseEventArgs e)
{
if (_pressedState != null && e.Button.HasFlag(MouseButtons.Left))
{
MousePressed = Rectangle.Contains(e.Location);
}
foreach (Part child in _children)
{
child.MouseDown(sender, e);
}
}
internal void MouseUp(object sender, MouseEventArgs e)
{
foreach (Part child in _children)
{
child.MouseUp(sender, e);
}
if (e.Button.HasFlag(MouseButtons.Left))
{
if (_pressedState != null)
{
MousePressed = false;
}
if (MouseOver && Clicked != null)
{
Clicked();
}
}
}
internal void MouseMove(object sender, MouseEventArgs e)
{
MouseOver = Rectangle.Contains(e.Location);
foreach(Part child in _children)
{
child.MouseMove(sender, e);
}
}
internal void MouseLeave(object sender, EventArgs e)
{
MouseOver = false;
foreach (Part child in _children)
{
child.MouseLeave(sender, e);
}
}
public Part AddPart()
{
Part child = new Part(_tracker, this);
_children.Add(child);
return child;
}
public Part WithPressed(StateTypeId? pressedState)
{
this._pressedState = pressedState;
return this;
}
public Part WithHot(StateTypeId? hotState)
{
this._hotState = hotState;
return this;
}
public Part WithFocus(StateTypeId? focusState)
{
this._focusState = focusState;
return this;
}
}
private readonly Control _control;
private readonly List<Control> _additionalControls = new List<Control>();
public readonly Part Root;
public KVisualStateTracker(Control control, StateTypeId normalState, StateTypeId disabledState)
{
this._control = control;
Root = new Part(this, normalState, disabledState);
AddControl(_control);
_control.EnabledChanged += Control_EnabledChanged;
}
public void AddControl(Control child)
{
if (child != _control)
{
_additionalControls.Add(child);
}
child.MouseLeave += Root.MouseLeave;
child.MouseMove += Root.MouseMove;
child.MouseDown += Root.MouseDown;
child.MouseUp += Root.MouseUp;
child.GotFocus += Root.GotFocus;
child.LostFocus += Root.LostFocus;
}
private void Control_EnabledChanged(object sender, EventArgs e)
{
Invalidate();
}
private void Invalidate()
{
_control.Invalidate();
}
}
}

View File

@ -0,0 +1,141 @@
using Acacia.Native;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Windows.Forms.VisualStyles;
namespace Acacia.Controls
{
internal class KVisualStyle<PartTypeId, StateTypeId>
where PartTypeId : struct, IConvertible
where StateTypeId : struct, IConvertible
{
public class Part
{
private readonly KVisualStyle<PartTypeId, StateTypeId> _style;
private readonly PartTypeId _partId;
private Dictionary<StateTypeId, VisualStyleRenderer> _renderers;
private Part _paddingLeft;
private Part _paddingRight;
public Part(KVisualStyle<PartTypeId, StateTypeId> style, PartTypeId id)
{
this._style = style;
this._partId = id;
}
public void DrawBackground(Graphics graphics, StateTypeId state, Rectangle rect)
{
VisualStyleRenderer r = GetRenderer(state);
if (r != null)
{
r.DrawBackground(graphics, rect);
}
}
public void DrawText(Graphics graphics, StateTypeId state, Rectangle rect, string text)
{
VisualStyleRenderer r = GetRenderer(state);
if (r != null)
{
r.DrawText(graphics, rect, text); // TODO: disabled
}
}
private VisualStyleRenderer GetRenderer(StateTypeId state)
{
InitRenderers();
VisualStyleRenderer renderer;
_renderers.TryGetValue(state, out renderer);
return renderer;
}
private void InitRenderers()
{
if (_renderers == null)
{
_renderers = new Dictionary<StateTypeId, VisualStyleRenderer>();
foreach (StateTypeId entry in Enum.GetValues(typeof(StateTypeId)))
{
try
{
int id = _partId.ToInt32(null);
int entryId = entry.ToInt32(null);
_renderers.Add(entry, new VisualStyleRenderer(_style.ClassName, id, entryId));
}
catch (Exception e) { Logger.Instance.Trace(this, "Renderer not supported: {0}", e); }
}
}
}
public Size? GetPartSize(Graphics graphics, StateTypeId state)
{
VisualStyleRenderer renderer = GetRenderer(state);
if (renderer == null)
return null;
Size size = renderer.GetPartSize(graphics, ThemeSizeType.True);
return size.AddHorizontally(_paddingLeft?.GetPartSize(graphics, state),
_paddingRight?.GetPartSize(graphics, state));
}
public Padding? GetMargins(Graphics graphics, StateTypeId state)
{
VisualStyleRenderer renderer = GetRenderer(state);
if (renderer == null)
return null;
// VisualStyleRenderer.GetMargins always throws an exception, make an explicit API call
int stateId = state.ToInt32(null);
UXTheme.MARGINS margins;
IntPtr hdc = graphics.GetHdc();
try
{
UXTheme.GetThemeMargins(renderer.Handle, hdc, this._partId.ToInt32(null), stateId,
(int)MarginProperty.SizingMargins, IntPtr.Zero, out margins);
// TODO: include padding
return new Padding(margins.cxLeftWidth, margins.cyTopHeight, margins.cxRightWidth, margins.cyBottomHeight);
}
finally
{
graphics.ReleaseHdc(hdc);
}
}
public void SetPadding(PartTypeId paddingLeft, PartTypeId paddingRight)
{
this._paddingLeft = _style[paddingLeft];
this._paddingRight = _style[paddingRight];
}
}
private readonly string ClassName;
private readonly Dictionary<PartTypeId, Part> _parts = new Dictionary<PartTypeId, Part>();
public KVisualStyle(string name)
{
this.ClassName = name;
}
public Part this[PartTypeId index]
{
get
{
Part part;
if (!_parts.TryGetValue(index, out part))
{
part = new Part(this, index);
_parts.Add(index, part);
}
return part;
}
}
}
}

View File

@ -111,7 +111,6 @@
//
this.gabLookup.DisplayMember = "DisplayName";
resources.ApplyResources(this.gabLookup, "gabLookup");
this.gabLookup.FormattingEnabled = true;
this.gabLookup.GAB = null;
this.gabLookup.Name = "gabLookup";
this.gabLookup.SelectedUserChanged += new Acacia.UI.GABLookupControl.SelectedUserEventHandler(this.gabLookup_SelectedUserChanged);

View File

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace Acacia.Native
{
public static class UXTheme
{
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct MARGINS
{
public int cxLeftWidth;
public int cxRightWidth;
public int cyTopHeight;
public int cyBottomHeight;
}
[DllImport("uxtheme.dll")]
public static extern int GetThemeMargins(IntPtr hTheme, IntPtr hdc, int iPartId, int iStateId, int iPropId, IntPtr prc, out MARGINS pMargins);
}
}