I have a custom Panel for laying out text. There is a DependancyProperty called "Text" and when that value changes, this piece of code runs:
if( !string.IsNullOrEmpty(Text))
{
Children.Clear();
foreach (char ch in Text)
{
TextBlock textBlock = new TextBlock();
textBlock.Text = ch.ToString();
textBlcok.Foreground = Foreground;
//The rest of these are DPs in the panel
textBlock.FontFamily = FontFamily;
textBlock.FontStyle = FontStyle;
textBlock.FontWeight = FontWeight;
textBlock.FontStretch = FontStretch;
textBlock.FontSize = FontSize;
Children.Add(textBlock);
}
}
}
Now, with font size of 15 and font Arial, these should be giving me a desired size of around 8 width and 10 height. However, when I do a Measure() and check the desired size, I get 40,18 every time!
So in trying to figure out what could've possibly changed the size, I put this code before and after the Children.Add in the code above:
textBlock.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
Console.WriteLine(ch.ToString() + ": " + textBlock.DesiredSize);
Children.Add(textBlock);
textBlock.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
Console.WriteLine(ch.ToString() + ": " + textBlock.DesiredSize);
What this gave me, was the proper desired size before it's added to the children collection, and a size of 40,18 (regardless of letter) after it's added to the collection.
What is causing this to happen?
Edit: You can find the full source for the control here:
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using IQ.Touch.Resources.Classes.Helpers;
/* TextOnAPath.cs
*
* A slightly modified version of the control found at
* http://www.codeproject.com/KB/WPF/TextOnAPath.aspx
*/
namespace IQ.Touch.Resources.Controls
{
public class TextOnAPath : Panel
{
// Fields
PathFigureHelper pathFigureHelper = new PathFigureHelper();
Size totalSize;
// Dependency properties
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text",
typeof(string),
typeof(TextOnAPath),
new PropertyMetadata(OnFontPropertyChanged));
public static readonly DependencyProperty FontFamilyProperty =
DependencyProperty.Register("FontFamily",
typeof(FontFamily),
typeof(TextOnAPath),
new PropertyMetadata(new FontFamily("Portable User Interface"), OnFontPropertyChanged));
public static readonly DependencyProperty FontStyleProperty =
DependencyProperty.Register("FontStyle",
typeof(FontStyle),
typeof(TextOnAPath),
new PropertyMetadata(FontStyles.Normal, OnFontPropertyChanged));
public static readonly DependencyProperty FontSizeProperty =
DependencyProperty.Register("FontSize",
typeof(double),
typeof(TextOnAPath),
new PropertyMetadata(12.0, OnFontPropertyChanged));
public static readonly DependencyProperty FontWeightProperty =
DependencyProperty.Register("FontWeight",
typeof(FontWeight),
typeof(TextOnAPath),
new PropertyMetadata(FontWeights.Normal, OnFontPropertyChanged));
public static readonly DependencyProperty FontStretchProperty =
DependencyProperty.Register("FontStretch",
typeof(FontStretch),
typeof(TextOnAPath),
new PropertyMetadata(FontStretches.Normal, OnFontPropertyChanged));
public static readonly DependencyProperty ForegroundProperty =
DependencyProperty.Register("Foreground",
typeof(Brush),
typeof(TextOnAPath),
new PropertyMetadata(new SolidColorBrush(Colors.Black), OnFontPropertyChanged));
public static readonly DependencyProperty PathFigureProperty =
DependencyProperty.Register("PathFigure",
typeof(PathFigure),
typeof(TextOnAPath),
new PropertyMetadata(OnPathFigureChanged));
// Properties
public string Text
{
set { SetValue(TextProperty, value); }
get { return (string)GetValue(TextProperty); }
}
public FontFamily FontFamily
{
set { SetValue(FontFamilyProperty, value); }
get { return (FontFamily)GetValue(FontFamilyProperty); }
}
public FontStyle FontStyle
{
set { SetValue(FontStyleProperty, value); }
get { return (FontStyle)GetValue(FontStyleProperty); }
}
public double FontSize
{
set { SetValue(FontSizeProperty, value); }
get { return (double)GetValue(FontSizeProperty); }
}
public FontWeight FontWeight
{
set { SetValue(FontWeightProperty, value); }
get { return (FontWeight)GetValue(FontWeightProperty); }
}
public FontStretch FontStretch
{
set { SetValue(FontStretchProperty, value); }
get { return (FontStretch)GetValue(FontStretchProperty); }
}
public Brush Foreground
{
set { SetValue(ForegroundProperty, value); }
get { return (Brush)GetValue(ForegroundProperty); }
}
public PathFigure PathFigure
{
set { SetValue(PathFigureProperty, value); }
get { return (PathFigure)GetValue(PathFigureProperty); }
}
// Property-changed handlers
static void OnFontPropertyChanged(DependencyObject obj,
DependencyPropertyChangedEventArgs args)
{
(obj as TextOnAPath).OnFontPropertyChanged(args);
}
void OnFontPropertyChanged(DependencyPropertyChangedEventArgs args)
{
Children.Clear();
if (String.IsNullOrEmpty(Text))
return;
foreach (char ch in Text)
{
TextBlock textBlock = new TextBlock();
textBlock.Text = ch.ToString();
textBlock.FontFamily = FontFamily;
textBlock.FontStyle = FontStyle;
textBlock.FontWeight = FontWeight;
textBlock.FontStretch = FontStretch;
textBlock.FontSize = FontSize;
textBlock.Foreground = Foreground;
textBlock.HorizontalAlignment = HorizontalAlignment.Center;
textBlock.VerticalAlignment = VerticalAlignment.Bottom;
textBlock.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
Console.WriteLine(ch.ToString() + ": " + textBlock.DesiredSize);
Children.Add(textBlock);
textBlock.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
Console.WriteLine(ch.ToString() + ": " + textBlock.DesiredSize);
}
CalculateTransforms();
InvalidateMeasure();
}
static void OnPathFigureChanged(DependencyObject obj,
DependencyPropertyChangedEventArgs args)
{
(obj as TextOnAPath).OnPathFigureChanged(args);
}
void OnPathFigureChanged(DependencyPropertyChangedEventArgs args)
{
pathFigureHelper.SetPathFigure(args.NewValue as PathFigure);
CalculateTransforms();
InvalidateMeasure();
}
void CalculateTransforms()
{
double pathLength = pathFigureHelper.Length;
double textLength = 0;
double textDesiredWidth = 9;
totalSize = new Size();
foreach (UIElement child in Children)
{
child.Measure(new Size(Double.PositiveInfinity,
Double.PositiveInfinity));
textLength += child.DesiredSize.Width;
}
//textLength = Children.Count * textDesiredWidth;
if (pathLength == 0 || textLength == 0)
return;
//double scalingFactor = pathLength / textLength;
double baseline = FontSize; // * FontFamily.Baseline;
double progress = 0;
if (textLength <= pathLength)
{
progress = ((pathLength - textLength) / 2) / pathLength;
}
foreach (UIElement child in Children)
{
double width = child.DesiredSize.Width;
//double width = textDesiredWidth;
progress += width / 2 / pathLength;
Point point, tangent;
pathFigureHelper.GetPointAtFractionLength(progress,
out point, out tangent);
TransformGroup transformGroup = new TransformGroup();
//ScaleTransform scaleTransform = new ScaleTransform();
//scaleTransform.ScaleX = scalingFactor;
//scaleTransform.ScaleY = scalingFactor;
//transformGroup.Children.Add(scaleTransform);
RotateTransform rotateTransform = new RotateTransform();
rotateTransform.Angle = Math.Atan2(tangent.Y, tangent.X) * 180 / Math.PI;
rotateTransform.CenterX = width / 2;
rotateTransform.CenterY = baseline;
transformGroup.Children.Add(rotateTransform);
TranslateTransform translateTransform = new TranslateTransform();
translateTransform.X = point.X - width / 2;
translateTransform.Y = point.Y - baseline;
transformGroup.Children.Add(translateTransform);
child.RenderTransform = transformGroup;
BumpUpTotalSize(transformGroup.Value, new Point(0, 0));
BumpUpTotalSize(transformGroup.Value, new Point(0, child.DesiredSize.Height));
BumpUpTotalSize(transformGroup.Value, new Point(child.DesiredSize.Width, 0));
BumpUpTotalSize(transformGroup.Value, new Point(child.DesiredSize.Width, child.DesiredSize.Height));
progress += width / 2 / pathLength;
}
Point endPoint, endTangent;
pathFigureHelper.GetPointAtFractionLength(1, out endPoint, out endTangent);
totalSize.Width = Math.Max(totalSize.Width, endPoint.X);
}
void BumpUpTotalSize(Matrix matrix, Point point)
{
point = matrix.Transform(point);
totalSize.Width = Math.Max(totalSize.Width, point.X);
totalSize.Height = Math.Max(totalSize.Height, point.Y);
}
protected override Size MeasureOverride(Size availableSize)
{
foreach (UIElement child in Children)
child.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
// return the size calculated during CalculateTransforms
return totalSize;
}
protected override Size ArrangeOverride(Size finalSize)
{
foreach (UIElement child in Children)
child.Arrange(new Rect(new Point(0, 0), child.DesiredSize));
return finalSize;
}
}
}
You should horizontally align you TextBox to the left, right, or center. It is aligned as strech per default, thus expandig it to the available area.
Edit
Just testet it with a little class:
public class TextOnAPath : Panel
{
public TextOnAPath() {
var textBlock = new TextBlock();
textBlock.Text = "Test";
textBlock.Background = Brushes.Blue;
textBlock.VerticalAlignment = System.Windows.VerticalAlignment.Top;
textBlock.HorizontalAlignment = System.Windows.HorizontalAlignment.Left;
this.Children.Add(textBlock);
this.Background = Brushes.Gray;
}
protected override Size MeasureOverride(Size availableSize) {
return availableSize;
}
protected override Size ArrangeOverride(Size finalSize) {
foreach (UIElement child in this.Children)
child.Arrange(new Rect(0, 0, finalSize.Width, finalSize.Height));
return finalSize;
}
}
Removing the alignments takes up all available space ... could it be that your CalculateTransforms method causes the effect? Especially, as the outcome is then used in the MeasureOverride method.
I figured out the problem, it turns out that the problem was not related to the control at all, but somewhere in the code the default template for a textblock got changed, and MinWidth and MinHeight got set to 40,18 for some reason... Now to find the suspect and to yell at them.
Thanks guys
Related
There is a "Custom Trackbar", which can take negative and positive values. If you set Min = -50, Max = 100, the slider moves outside the scrollbar. I need it to behave in the same way as "Standard Trackbar" (it should not go beyond the scrollbar boundaries). How to do it?
The screenshot shows 2 Trackbars for both I set (Minimum = -50, Maximum = 100, Value = -50), but after building the project I got the following picture:
If we set (Minimum = 0, Maximum = 100, Value = 25), we get the following:
[Code Custom Trackbar]
[DefaultEvent("ValueChanged")]
public class HandyHTrackbarWorked : Control {
#region Установка начальных параметров
public HandyHTrackbarWorked() {
SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.UserPaint
| ControlStyles.AllPaintingInWmPaint | ControlStyles.ResizeRedraw, true); UpdateStyles();
Size = new Size(250, 12);
ThumbSize = new Size(ThumbRect.Width = 15, ThumbRect.Height = 12);
}
#endregion
#region Основные свойства
private double _value;
public double Value {
get { return _value; }
set {
_value = value;
//if (_value < Minimum) { _value = Minimum; }
//if (_value > Maximum) { _value = Maximum; }
OnScroll(); Refresh();
}
}
private double minimum;
public double Minimum {
get { return minimum; }
set { minimum = value; Invalidate(); }
}
private double maximum = 100;
public double Maximum {
get { return maximum; }
set { maximum = value; Invalidate(); }
}
private double smallStep = 1;
public double SmallStep {
get { return smallStep; }
set {
smallStep = (value > 0) ? value : 1;
}
}
#endregion
#region Свойства, отвечающие за оформление
[Description("Размер ползунка")]
private Size thumbSize;
public Size ThumbSize {
get { return thumbSize; }
set {
thumbSize = value;
//if (thumbSize.Width % 2 == 0 && thumbSize.Width > 0) thumbSize.Width += 1;
Invalidate();
}
}
[Description("Цвет ползунка")]
private Color thumbBackColor = Color.FromArgb(255, 255, 255);
public Color ThumbBackColor {
get { return thumbBackColor; }
set { thumbBackColor = value; Invalidate(); }
}
private Color trackBackColor = Color.Transparent;
public Color TrackBackColor {
get { return trackBackColor; }
set { trackBackColor = value; Invalidate(); }
}
private Color trackBorderColor = Color.FromArgb(221, 0, 49);
public Color TrackBorderColor {
get { return trackBorderColor; }
set { trackBorderColor = value; Invalidate(); }
}
private Color trackBorderColor2 = Color.FromArgb(64, 64, 64);
public Color TrackBorderColor2 {
get { return trackBorderColor2; }
set { trackBorderColor2 = value; Invalidate(); }
}
[Description("Толщина")]
private int trackThickness = 2;
public int TrackThickness {
get { return trackThickness; }
set { trackThickness = value; Invalidate(); }
}
public new Padding Padding {
get { return base.Padding; }
set { base.Padding = value; Invalidate(); }
}
public Rectangle ThumbRect;
#endregion
#region Основные события
public event EventHandler ValueChanged;
#endregion
#region Обработчики событий
private Point startMouseClickPosition;
private Point currentMousePosition;
protected override void OnCreateControl() {
base.OnCreateControl();
this.MouseDown += (sender, e) => {
// When clicking on the ScrollBar, center the Thumb relative to the mouse cursor
if (!ThumbRect.Contains(e.Location)) {
MoveThumb(e, false);
}
// When clicking on Thumb, determine the startMouseClickPosition
if (ThumbRect.Contains(e.Location)) {
startMouseClickPosition.X = e.X - ThumbRect.Left; // OR ... - ThumbRect.X
ThumbBackColor = Color.Green;
}
};
this.MouseMove += (sender, e) => {
ThumbBackColor = ThumbRect.Contains(e.Location)
? ThumbBackColor = Color.Orange : ThumbBackColor = Color.Gray;
if (e.Button == MouseButtons.Left) {
ThumbBackColor = Color.Green; MoveThumb(e);
}
};
this.MouseLeave += (sender, e) => { ThumbBackColor = Color.Gray; };
}
int PaddingLR = 10;
// padding(left/right) must be the same,
// if the orientation of the scroll bar is HORIZONTAL
private void MoveThumb(MouseEventArgs e, bool useStartMouseClickPosition = true) {
double newValue;
if (useStartMouseClickPosition) {
currentMousePosition.X = e.X - startMouseClickPosition.X;
// works correctly
newValue = Maximum * (currentMousePosition.X - (ThumbSize.Width / 2) + (ThumbSize.Width / 2) - PaddingLR)
/ (Width - ThumbSize.Width - PaddingLR * 2);
} else {
newValue = Maximum * (e.X - ThumbSize.Width / 2 - PaddingLR)
/ (Width - ThumbSize.Width - PaddingLR * 2);
}
// does NOT work correctly (although the calculation result is the same)
//double newValue = Maximum * (newThumbLeft + (ThumbSize.Width / 2) - PaddingLR) /
// (Width - ThumbSize.Width - PaddingLR * 2);
Value = Math.Max(0, Math.Min(Maximum, newValue));
}
public void OnScroll() {
ValueChanged?.Invoke(this, EventArgs.Empty);
}
#endregion
#region Отрисовка элементов управления
protected override void OnPaint(PaintEventArgs e) {
ThumbRect = new Rectangle(
Convert.ToInt32(Value * (Width - ThumbSize.Width - Padding.Left * 2) / Maximum + Padding.Right),
0 + Padding.Top,
ThumbSize.Width, // fixed slider width
Height - Padding.Bottom - Padding.Top // dynamic slider height
// (example) Height - 4, means to move the slider by 2 px above and below
);
// Filling the scroll bar
using (SolidBrush brush = new SolidBrush(TrackBackColor)) {
e.Graphics.FillRectangle(brush, new Rectangle(0, 0, Width, Height));
}
// The colored line in front of the slider
using (Pen pen = new Pen(TrackBorderColor2, TrackThickness)) {
e.Graphics.DrawLine(pen, Padding.Left, Height / 2, Width - Padding.Right, Height / 2);
}
// The colored line following the slider
using (Pen pen = new Pen(TrackBorderColor, TrackThickness)) {
e.Graphics.DrawLine(pen, Padding.Left, Height / 2, ThumbRect.Right, Height / 2);
}
// Filling the slider
using (SolidBrush brush2 = new SolidBrush(Color.FromArgb(100, 0, 0, 0))) {
e.Graphics.FillRectangle(brush2, ThumbRect);
}
}
#endregion
}
Thanks to the help of user #IVSoftware, in writing auxiliary methods calcValueFromPosition() and calcXfromValue(), the following solution was obtained, which allows you to set different paddings:
[Code Custom Trackbar]
namespace Handy_UI.Controls.Trackbars {
[DefaultEvent("ValueChanged")]
public class HandyHTrackbarWorked : Control {
#region Setting the initial parameters
public HandyHTrackbarWorked() {
SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.UserPaint
| ControlStyles.AllPaintingInWmPaint | ControlStyles.ResizeRedraw, true); UpdateStyles();
Size = new Size(250, 12);
ThumbSize = new Size(15, 12);
}
#endregion
#region Main features
private double _value;
public double Value {
get { return _value; }
set {
_value = value;
OnScroll(); Refresh();
}
}
private double minimum;
public double Minimum {
get { return minimum; }
set { minimum = value; Invalidate(); }
}
private double maximum = 100;
public double Maximum {
get { return maximum; }
set { maximum = value; Invalidate(); }
}
private double smallStep = 1;
public double SmallStep {
get { return smallStep; }
set {
smallStep = (value > 0) ? value : 1;
}
}
#endregion
#region Properties responsible for design
private Size thumbSize;
public Size ThumbSize {
get { return thumbSize; }
set {
thumbSize = value;
Invalidate();
}
}
private Color thumbBackColor = Color.FromArgb(255, 255, 255);
public Color ThumbBackColor {
get { return thumbBackColor; }
set { thumbBackColor = value; Invalidate(); }
}
private Color trackBackColor = Color.Transparent;
public Color TrackBackColor {
get { return trackBackColor; }
set { trackBackColor = value; Invalidate(); }
}
private Color trackBorderColor = Color.FromArgb(221, 0, 49);
public Color TrackBorderColor {
get { return trackBorderColor; }
set { trackBorderColor = value; Invalidate(); }
}
private Color trackBorderColor2 = Color.FromArgb(64, 64, 64);
public Color TrackBorderColor2 {
get { return trackBorderColor2; }
set { trackBorderColor2 = value; Invalidate(); }
}
private int trackThickness = 2;
public int TrackThickness {
get { return trackThickness; }
set { trackThickness = value; Invalidate(); }
}
public new Padding Padding {
get { return base.Padding; }
set { base.Padding = value; Invalidate(); }
}
#endregion
#region Key Events
public event EventHandler ValueChanged;
#endregion
#region Обработчики событий
private Point startMouseClickPosition;
private Point currentMousePosition;
protected override void OnCreateControl() {
base.OnCreateControl();
this.MouseDown += (sender, e) => {
// When clicking on the ScrollBar, center the Thumb relative to the mouse cursor
if (!ThumbRect.Contains(e.Location)) {
MoveThumb(e, false);
}
// When clicking on Thumb, determine the startMouseClickPosition
if (ThumbRect.Contains(e.Location)) {
startMouseClickPosition.X = e.X - ThumbRect.Left; // OR ... - ThumbRect.X
ThumbBackColor = Color.Green;
}
};
this.MouseMove += (sender, e) => {
ThumbBackColor = ThumbRect.Contains(e.Location)
? ThumbBackColor = Color.Orange : ThumbBackColor = Color.Gray;
if (e.Button == MouseButtons.Left) {
ThumbBackColor = Color.Green; MoveThumb(e);
}
};
this.MouseLeave += (sender, e) => { ThumbBackColor = Color.Gray; };
//this.SizeChanged += (sender, e) => { TrackThickness = Height - Padding.Bottom - Padding.Top; };
}
private void MoveThumb(MouseEventArgs e, bool useStartMouseClickPosition = true) {
Point currentMousePosition = new Point(0, 0);
if (useStartMouseClickPosition) {
currentMousePosition.X = e.X - startMouseClickPosition.X + ThumbSize.Width / 2
- Padding.Right - (Padding.Left - Padding.Right);
} else currentMousePosition.X = e.X - Padding.Left;
Value = calcValueFromPosition(currentMousePosition);
}
private double calcValueFromPosition(Point e) {
var mouseRange = Width - (Padding.Left + Padding.Right);
var pct = e.X / (double)mouseRange;
var controlRange = Maximum - Minimum;
var relative = pct * controlRange;
var value = Minimum + relative;
value = Math.Max(Minimum, value);
value = Math.Min(Maximum, value);
return value;
}
public void OnScroll() {
ValueChanged?.Invoke(this, EventArgs.Empty);
}
#endregion
#region Drawing controls
public int calcXfromValue() {
var range = Maximum - Minimum;
var relative = Value - Minimum;
var pct = relative / range;
var width = Width - (Padding.Left + Padding.Right);
var pos = pct * width;
var x = pos + Padding.Left - (ThumbSize.Width / 2);
if (x < 0 + Padding.Left) x = 0 + Padding.Left;
else if (x > Width - ThumbSize.Width - Padding.Right) x = Width - ThumbSize.Width - Padding.Right;
return (int)x;
}
public Rectangle ThumbRect => new Rectangle(
x: calcXfromValue(), y: 0 + Padding.Top,
width: ThumbSize.Width, // fixed slider width
height: Height - Padding.Bottom - Padding.Top // dynamic slider width
);
protected override void OnPaint(PaintEventArgs e) {
// Filling the scroll bar
using (SolidBrush brush = new SolidBrush(TrackBackColor)) {
e.Graphics.FillRectangle(brush, new Rectangle(0, 0, Width, Height));
}
// The colored line in front of the slider
using (Pen pen = new Pen(TrackBorderColor2, TrackThickness)) {
e.Graphics.DrawLine(pen, Padding.Left, Height / 2, Width - Padding.Right, Height / 2);
}
// The colored line following the slider
using (Pen pen = new Pen(TrackBorderColor, TrackThickness)) {
e.Graphics.DrawLine(pen, Padding.Left, Height / 2, ThumbRect.Right, Height / 2);
}
// Filling the slider
using (SolidBrush brush2 = new SolidBrush(Color.FromArgb(100, 0, 0, 0))) {
e.Graphics.FillRectangle(brush2, ThumbRect);
}
}
#endregion
}
}
I've put together a custom control in WPF in C# based on the Selector primitive:
public class PadControl : Selector
{
private const int padWidth = 28;
private const int padHeight = 18;
private const int padGap = 5;
private double _width;
private double _height;
static PadControl()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(PadControl), new FrameworkPropertyMetadata(typeof(PadControl),
FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender));
}
public PadControl()
{
}
protected override Size MeasureOverride(Size constraint)
{
_width = constraint.Width;
_height = constraint.Height;
return base.MeasureOverride(constraint);
}
protected override void OnRender(DrawingContext dc)
{
base.OnRender(dc);
var numHorz = _width / (padWidth + padGap);
var numVert = Math.Min(_height / (padHeight + padGap), 16);
var pen = new Pen(Brushes.Black, 1.0);
var brush = new SolidColorBrush(Color.FromRgb(192, 192, 192));
for(int bar = 0; bar < numHorz; bar++)
{
for(int track = 0; track < numVert; track++)
{
var rect = GetPadRect(bar, track);
dc.DrawRectangle(brush, pen, rect);
}
}
if (Items == null)
return;
brush = new SolidColorBrush(Colors.LightYellow);
var typeface = new Typeface("Tahoma");
foreach(PadViewModel item in Items)
{
var rect = GetPadRect(item.Bar, item.Track);
dc.DrawRectangle(brush, pen, rect);
var formatted = new FormattedText(item.Text, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeface, 12, Brushes.Black, 1.0)
{
MaxTextWidth = padWidth,
TextAlignment = TextAlignment.Center
};
dc.DrawText(formatted, GetPadPoint(item.Bar, item.Track));
}
}
private Rect GetPadRect(int bar, int track)
{
var rect = new Rect(bar * (padWidth + padGap) + padGap, track * (padHeight + padGap) + padGap, padWidth, padHeight);
return rect;
}
private Point GetPadPoint(int bar, int track)
{
var point = new Point(bar * (padWidth + padGap) + padGap, track * (padHeight + padGap) + padGap);
return point;
}
}
This draws as I'd like it, but it doesn't draw the items until I resize the control.
When the control renders for the first time, it doesn't render any Items as that is empty. Items is populated indirectly with this:
<controls:PadControl Grid.Column="0" Grid.Row="0" ItemsSource="{Binding Pads}"/>
The problem is, I don't see an update come through after ItemsSource has been set. So the question is, how do I attach an event handler to catch this? Do I attach it to Items or to ItemsSource?
The answer is to override OnItemsSourceChanged as described by Keith Stein in the comments above.
I am working on a project wherein I need to add a Control with the shape of a Circle with some text in the middle.
My problem is the circle is too small, when I resize it, it overlaps other controls. I want to draw the circle same width as the square.
Otherwise. how can I make the Control's background transparent?
I am using the code below:
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
using (Bitmap bitmap = new Bitmap(this.Width, this.Height))
{
using (Graphics graphics = Graphics.FromImage(bitmap))
{
graphics.SmoothingMode = SmoothingMode.HighQuality;
graphics.Clear(this.BackColor);
using (SolidBrush brush = new SolidBrush(this._FillColor))
{
graphics.FillEllipse(brush, 0x18 - 6, 0x18 - 6, (this.Width - 0x30) + 12, (this.Height - 0x30) + 12);
}
Brush FontColor = new SolidBrush(this.ForeColor);
SizeF MS = graphics.MeasureString(Convert.ToString(Convert.ToInt32((100 / _Maximum) * _Value)), Font);
graphics.DrawString(Convert.ToString(Convert.ToInt32((100 / _Maximum) * _Value)), Font, FontColor, Convert.ToInt32((Width / 2 - MS.Width / 2) + 2), Convert.ToInt32((Height / 2 - MS.Height / 2) + 3));
bitmap.MakeTransparent(this.BackColor);
e.Graphics.DrawImage(bitmap, 0, 0);
graphics.Dispose();
bitmap.Dispose();
}
}
}
This is a Custom Control derived from Control, which can be made translucent.
The interface is a colored circle which can contain a couple of numbers.
The Control exposes these custom properties:
Opacity: The level of opacity of the control BackGround [0, 255]
InnerPadding: The distance between the inner rectangle, which defines the circle bounds and the control bounds.
FontPadding: The distance between the Text and the Inner rectangle.
Transparency is obtained overriding CreateParams, then setting ExStyle |= WS_EX_TRANSPARENT;
The Control.SetStyle() method is used to modify the control behavior, adding these ControlStyles:
▶ ControlStyles.Opaque: prevents the painting of a Control's BackGround, so it's not managed by the System. Combined with CreateParams to set the Control's Extended Style to WS_EX_TRANSPARENT, the Control becomes completely transparent.
▶ ControlStyles.SupportsTransparentBackColor the control accepts Alpha values for it's BackGround color. Without also setting ControlStyles.UserPaint it won't be used to simulate transparency. We're doing that ourselves with other means.
To see it at work, create a new Class file, substitute all the code inside with this code preserving the NameSpace and build the Project/Solution.
The new Custom Control will appear in the ToolBox. Drop it on a Form. Modify its custom properties as needed.
A visual representation of the control:
Note and disclaimer:
This is a prototype Control, the custom Designer is missing (cannot post that here, too much code, also connected to a framework).
As presented here, it can be used to completely overlap other Controls in a Form or other containers. Partial overlapping is not handled in this simplified implementation.
The Font is hard-coded to Segoe UI, since this Font has a base-line that simplifies the position of the text in the middle of the circular area.
Other Fonts have a different base-line, which requires more complex handling.
See: TextBox with dotted lines for typing for the base math.
using System;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Text;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Windows.Forms;
[DesignerCategory("Code")]
public class RoundCenterLabel : Label, INotifyPropertyChanged, ISupportInitialize
{
private const int WS_EX_TRANSPARENT = 0x00000020;
private bool IsInitializing = false;
private Point MouseDownLocation = Point.Empty;
private readonly int fontPadding = 4;
private Font m_CustomFont = null;
private Color m_BackGroundColor;
private int m_InnerPadding = 0;
private int m_FontPadding = 25;
private int m_Opacity = 128;
public event PropertyChangedEventHandler PropertyChanged;
public RoundCenterLabel() => InitializeComponent();
private void InitializeComponent()
{
SetStyle(ControlStyles.Opaque |
ControlStyles.SupportsTransparentBackColor |
ControlStyles.ResizeRedraw, true);
SetStyle(ControlStyles.OptimizedDoubleBuffer, false);
m_CustomFont = new Font("Segoe UI", 50, FontStyle.Regular, GraphicsUnit.Pixel);
BackColor = Color.LimeGreen;
ForeColor = Color.White;
}
protected override CreateParams CreateParams {
get {
var cp = base.CreateParams;
cp.ExStyle |= WS_EX_TRANSPARENT;
return cp;
}
}
public new Font Font
{
get => m_CustomFont;
set {
m_CustomFont = value;
if (IsInitializing) return;
FontAdapter(value, DeviceDpi);
NotifyPropertyChanged();
}
}
public override string Text {
get => base.Text;
set { base.Text = value;
NotifyPropertyChanged();
}
}
public int InnerPadding {
get => m_InnerPadding;
set {
if (IsInitializing) return;
m_InnerPadding = ValidateRange(value, 0, ClientRectangle.Height - 10);
NotifyPropertyChanged(); }
}
public int FontPadding {
get => m_FontPadding;
set {
if (IsInitializing) return;
m_FontPadding = ValidateRange(value, 0, ClientRectangle.Height - 10);
NotifyPropertyChanged();
}
}
public int Opacity {
get => m_Opacity;
set { m_Opacity = ValidateRange(value, 0, 255);
UpdateBackColor(m_BackGroundColor);
NotifyPropertyChanged();
}
}
public override Color BackColor {
get => m_BackGroundColor;
set { UpdateBackColor(value);
NotifyPropertyChanged();
}
}
protected override void OnLayout(LayoutEventArgs e)
{
base.OnLayout(e);
base.AutoSize = false;
}
private void NotifyPropertyChanged([CallerMemberName] string PropertyName = null)
{
InvalidateParent();
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(PropertyName));
}
protected override void OnMouseDown(MouseEventArgs e)
{
base.OnMouseDown(e);
MouseDownLocation = e.Location;
}
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
if (e.Button == MouseButtons.Left) {
var loc = new Point(Left + (e.X - MouseDownLocation.X), Top + (e.Y - MouseDownLocation.Y));
InvalidateParent();
BeginInvoke(new Action(() => Location = loc));
}
}
private void InvalidateParent()
{
Parent?.Invalidate(Bounds, true);
Invalidate();
}
protected override void OnPaint(PaintEventArgs e)
{
using (var format = new StringFormat(StringFormatFlags.LineLimit | StringFormatFlags.NoWrap, CultureInfo.CurrentUICulture.LCID))
{
format.LineAlignment = StringAlignment.Center;
format.Alignment = StringAlignment.Center;
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
e.Graphics.TextRenderingHint = TextRenderingHint.AntiAliasGridFit;
using (var circleBrush = new SolidBrush(m_BackGroundColor))
using (var foreBrush = new SolidBrush(ForeColor))
{
FontAdapter(m_CustomFont, e.Graphics.DpiY);
RectangleF rect = InnerRectangle();
e.Graphics.FillEllipse(circleBrush, rect);
e.Graphics.DrawString(Text, m_CustomFont, foreBrush, rect, format);
};
};
}
public void BeginInit() => IsInitializing = true;
public void EndInit()
{
IsInitializing = false;
Font = new Font("Segoe UI", 50, FontStyle.Regular, GraphicsUnit.Pixel);
FontPadding = m_FontPadding;
InnerPadding = m_InnerPadding;
}
private RectangleF InnerRectangle()
{
(float Min, _) = GetMinMax(ClientRectangle.Height, ClientRectangle.Width);
var size = new SizeF(Min - (m_InnerPadding / 2), Min - (m_InnerPadding / 2));
var position = new PointF((ClientRectangle.Width - size.Width) / 2,
(ClientRectangle.Height - size.Height) / 2);
return new RectangleF(position, size);
}
private void FontAdapter(Font font, float dpi)
{
RectangleF rect = InnerRectangle();
float fontSize = ValidateRange(
(int)(rect.Height - m_FontPadding), 6,
(int)(rect.Height - m_FontPadding)) / (dpi / 72.0F) - fontPadding;
m_CustomFont.Dispose();
m_CustomFont = new Font(font.FontFamily, fontSize, font.Style, GraphicsUnit.Pixel);
}
private void UpdateBackColor(Color color)
{
m_BackGroundColor = Color.FromArgb(m_Opacity, Color.FromArgb(color.R, color.G, color.B));
base.BackColor = m_BackGroundColor;
}
private int ValidateRange(int Value, int Min, int Max)
=> Math.Max(Math.Min(Value, Max), Min); // (Value < Min) ? Min : ((Value > Max) ? Max : Value);
private (float, float) GetMinMax(float Value1, float Value2)
=> (Math.Min(Value1, Value2), Math.Max(Value1, Value2));
}
I have a model called "ListItem", and I'm printing a list of them. I used to just use a ListView, but I wanted to print them horizontally, so I created a custom WrapLayout.
The ListView had simple ways to perform actions when the items were tapped or long-pressed. When a MenuItem was tapped, PutBack was called, and the ListItem was passed through as parameters. When a MenuItem was long-pressed, a button reading "delete" appeared in the toolbar, and when that was tapped, DeleteListItem was called and the ListItem was passed through as parameters.
Old Xaml:
<ListView x:Name="inactiveList" ItemTapped="PutBack" >
<ListView.ItemTemplate>
<DataTemplate>
<TextCell Text="{Binding Name}">
<TextCell.ContextActions>
<MenuItem Command="{Binding Source={x:Reference ListPage}, Path=DeleteListItem}"
CommandParameter="{Binding .}" Text="delete" />
</TextCell.ContextActions>
</TextCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
I'm having trouble recreating this functionality with the buttons generated by my custom WrapLayout. Is this possible to do? I tried adding a Clicked property to the buttons, but VS tells me I can only set Clicked with += or -=, which doesn't make any sense to me.
New Xaml:
<local:WrapLayout x:Name="inactiveList" Spacing="5" />
New Code-Behind:
namespace Myapp
{
public partial class ListPage
{
public Command DeleteListItem { get; set; }
public ListPage()
{
InitializeComponent();
ObservableCollection<ListItem> inactiveItems =
new ObservableCollection<ListItem>(
App.ListItemRepo.GetInactiveListItems());
inactiveList.ItemsSource = inactiveItems;
DeleteListItem = new Command(async (parameter) => {
ListItem item = (ListItem)parameter as ListItem;
App.ListItemRepo.DeleteListItemAsync(item);
ObservableCollection<ListItem> deleteInactiveItems =
new ObservableCollection<ListItem>(
await App.ListItemRepo.GetInactiveListItemsAsync());
inactiveList.ItemsSource = deleteInactiveItems;
});
}
public async void PutBack(object sender, ItemTappedEventArgs e)
{
var selectedListItem = e.Item as ListItem;
await App.ListItemRepo.SetListItemActive(selectedListItem);
ObservableCollection<ListItem> inactiveItems =
new ObservableCollection<ListItem>(
await App.ListItemRepo.GetInactiveListItemsAsync());
inactiveList.ItemsSource = inactiveItems;
}
}
public class WrapLayout : Layout<View>
{
public ObservableCollection<ListItem> ItemsSource
{
get { return (ObservableCollection<ListItem>)GetValue(ItemSourceProperty); }
set { SetValue(ItemSourceProperty, value); }
}
public static readonly BindableProperty ItemSourceProperty =
BindableProperty.Create
(
"ItemsSource",
typeof(ObservableCollection<ListItem>),
typeof(WrapLayout),
propertyChanged: (bindable, oldvalue, newvalue) => ((WrapLayout)bindable).AddViews()
);
// THIS IS WHERE THE BUTTONS ARE SET
void AddViews()
{
Children.Clear();
foreach (ListItem s in ItemsSource)
{
Button button = new Button();
button.Text = s.Name;
Children.Add(button);
}
}
public static readonly BindableProperty SpacingProperty =
BindableProperty.Create
(
"Spacing",
typeof(double),
typeof(WrapLayout),
10.0,
propertyChanged: (bindable, oldvalue, newvalue) => ((WrapLayout)bindable).OnSizeChanged()
);
public double Spacing
{
get { return (double)GetValue(SpacingProperty); }
set { SetValue(SpacingProperty, value); }
}
private void OnSizeChanged()
{
this.ForceLayout();
}
protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint)
{
if (WidthRequest > 0)
widthConstraint = Math.Min(widthConstraint, WidthRequest);
if (HeightRequest > 0)
heightConstraint = Math.Min(heightConstraint, HeightRequest);
double internalWidth = double.IsPositiveInfinity(widthConstraint) ? double.PositiveInfinity : Math.Max(0, widthConstraint);
double internalHeight = double.IsPositiveInfinity(heightConstraint) ? double.PositiveInfinity : Math.Max(0, heightConstraint);
return DoHorizontalMeasure(internalWidth, internalHeight);
}
private SizeRequest DoHorizontalMeasure(double widthConstraint, double heightConstraint)
{
int rowCount = 1;
double width = 0;
double height = 0;
double minWidth = 0;
double minHeight = 0;
double widthUsed = 0;
foreach (var item in Children)
{
var size = item.Measure(widthConstraint, heightConstraint);
height = Math.Max(height, size.Request.Height);
var newWidth = width + size.Request.Width + Spacing;
if (newWidth > widthConstraint)
{
rowCount++;
widthUsed = Math.Max(width, widthUsed);
width = size.Request.Width;
}
else
width = newWidth;
minHeight = Math.Max(minHeight, size.Minimum.Height);
minWidth = Math.Max(minWidth, size.Minimum.Width);
}
if (rowCount > 1)
{
width = Math.Max(width, widthUsed);
height = (height + Spacing) * rowCount - Spacing; // via MitchMilam
}
return new SizeRequest(new Size(width, height), new Size(minWidth, minHeight));
}
protected override void LayoutChildren(double x, double y, double width, double height)
{
double rowHeight = 0;
double yPos = y, xPos = x;
foreach (var child in Children.Where(c => c.IsVisible))
{
var request = child.Measure(width, height);
double childWidth = request.Request.Width;
double childHeight = request.Request.Height;
rowHeight = Math.Max(rowHeight, childHeight);
if (xPos + childWidth > width)
{
xPos = x;
yPos += rowHeight + Spacing;
rowHeight = 0;
}
var region = new Rectangle(xPos, yPos, childWidth, childHeight);
LayoutChildIntoBoundingRegion(child, region);
xPos += region.Width + Spacing;
}
}
}
}
I implemented custom Windows.UI.Xaml.Controls.Panel for custom layout items in ListView and put it to <ItemsPanelTemplate>. It's works, but is not possible to scroll.
This answer not applicable because I'm using MVVM data binding.
How to make scrolling work?
My Panel implementation:
public class StaggeredWrapPanel : Panel
{
#region int ItemWidth Property
public int ItemWidth
{
get { return (int)GetValue(ItemWidthProperty); }
set { SetValue(ItemWidthProperty, value); }
}
public static readonly DependencyProperty ItemWidthProperty = DependencyProperty.Register("ItemWidth", typeof(int), typeof(StaggeredWrapPanel), new PropertyMetadata(300, OnItemWidthPropertyChanged));
private static void OnItemWidthPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
(source as StaggeredWrapPanel).InvalidateMeasure();
}
#endregion
protected override Size MeasureOverride(Size availableSize)
{
foreach (var item in Children)
{
item.Measure(new Size(ItemWidth, double.PositiveInfinity));
}
return base.MeasureOverride(availableSize);
}
protected override Size ArrangeOverride(Size finalSize)
{
int columnsCount = (int)(finalSize.Width / ItemWidth);
double[] columnsOffsetsY = new double[columnsCount];
double[] columnsOffsetsX = new double[columnsCount];
for (int i = 0; i < columnsOffsetsX.Length; i++)
{
columnsOffsetsX[i] = i * ItemWidth;
}
int currentColumn = 0;
foreach (var item in Children)
{
if (currentColumn != 0 && columnsOffsetsY[currentColumn] >= columnsOffsetsY[currentColumn - 1])
if (currentColumn == columnsCount - 1)
currentColumn = 0;
else currentColumn++;
Rect rect = new Rect(new Point(columnsOffsetsX[currentColumn], columnsOffsetsY[currentColumn]), new Size(item.DesiredSize.Width, item.DesiredSize.Height));
columnsOffsetsY[currentColumn] += item.DesiredSize.Height;
item.Arrange(rect);
if (currentColumn == 0)
currentColumn++;
if (currentColumn > columnsCount - 1)
currentColumn = 0;
}
return base.ArrangeOverride(finalSize);
}
}