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.
Related
I have the following custom framework element (which just draws a rectangle and some ellipses) with nested "visuals". Now I want to change some colors when the mouse enters or leaves the control. The code will be called but the color won't change even with InvalidVisual. Any idea what's wrong here:
public class Dummy : FrameworkElement
{
#region Fields
private Ellipse _bottomLeft = new Ellipse();
private Ellipse _bottomRight = new Ellipse();
private ContainerVisual _containerVisual = new ContainerVisual();
private Rectangle _rect = new Rectangle();
private Ellipse _topLeft = new Ellipse();
private Ellipse _topRight = new Ellipse();
#endregion Fields
#region Constructors
public Dummy()
{
Initialize();
_containerVisual.Children.Add(_rect);
_containerVisual.Children.Add(_topLeft);
_containerVisual.Children.Add(_topRight);
_containerVisual.Children.Add(_bottomLeft);
_containerVisual.Children.Add(_bottomRight);
}
#endregion Constructors
#region Properties
protected override int VisualChildrenCount
{
get { return _containerVisual == null ? 0 : 1; }
}
#endregion Properties
#region Methods
private void Initialize()
{
// Rectangle
_rect.Stroke = Brushes.Red;
_rect.StrokeThickness = 1;
_rect.Fill = new SolidColorBrush(Colors.Transparent);
// Ellipses
_topLeft.StrokeThickness = 1;
_topLeft.Stroke = Brushes.Red;
_topLeft.Fill = new SolidColorBrush(Colors.Orange);
//----------------
_topRight.StrokeThickness = 1;
_topRight.Stroke = Brushes.Red;
_topRight.Fill = new SolidColorBrush(Colors.Orange);
//----------------
_bottomLeft.StrokeThickness = 1;
_bottomLeft.Stroke = Brushes.Red;
_bottomLeft.Fill = new SolidColorBrush(Colors.Orange);
//----------------
_bottomRight.StrokeThickness = 1;
_bottomRight.Stroke = Brushes.Red;
_bottomRight.Fill = new SolidColorBrush(Colors.Orange);
}
protected override Size ArrangeOverride(Size finalSize)
{
var diameter = 6;
var radius = diameter / 2;
var rectThicknessOffset = _rect.StrokeThickness / 2;
var rect = new Rect(new Point(0, 0), finalSize);
_rect.Arrange(rect);
_topLeft.Arrange(new Rect(rect.TopLeft.X - radius + rectThicknessOffset,
rect.TopLeft.Y - radius + rectThicknessOffset, diameter, diameter));
_topRight.Arrange(new Rect(rect.TopRight.X - radius - rectThicknessOffset,
rect.TopRight.Y - radius + rectThicknessOffset, diameter, diameter));
_bottomRight.Arrange(new Rect(rect.BottomRight.X - radius - rectThicknessOffset,
rect.BottomRight.Y - radius - rectThicknessOffset, diameter, diameter));
_bottomLeft.Arrange(new Rect(rect.BottomLeft.X - radius + rectThicknessOffset,
rect.BottomLeft.Y - radius - rectThicknessOffset, diameter, diameter));
return base.ArrangeOverride(finalSize);
}
// Provide a required override for the GetVisualChild method.
protected override Visual GetVisualChild(int index)
{
if (_containerVisual == null)
{
throw new ArgumentOutOfRangeException();
}
return _containerVisual;
}
protected override void OnMouseEnter(MouseEventArgs e)
{
base.OnMouseEnter(e);
_rect.Stroke = new SolidColorBrush(Colors.Blue);
InvalidateVisual();
Debug.WriteLine("Mouse Enter");
}
protected override void OnMouseLeave(MouseEventArgs e)
{
base.OnMouseLeave(e);
_rect.Stroke = new SolidColorBrush(Colors.Green);
InvalidateVisual();
Debug.WriteLine("Mouse Leave");
}
protected override HitTestResult HitTestCore(PointHitTestParameters hitTestParameters)
{
return new PointHitTestResult(this, hitTestParameters.HitPoint);
}
#endregion Methods
}
Try to replace the call to InvalidateVisual with calls to Measure and Arrange:
protected override void OnMouseLeave(MouseEventArgs e)
{
base.OnMouseLeave(e);
_rect.Stroke = new SolidColorBrush(Colors.Green);
Measure(Size.Empty);
Arrange(new Rect(DesiredSize));
Debug.WriteLine("Mouse Leave");
}
I am trying to add a close button on the tab pages of TabControl and
change the color of the close button from light gray to black when
mouse hovers over it. However, the color never changes.
The DrawEventArgsCustom class is created to indicate that the mouse is
hovering over the close button. When it's true, the statement to
change the color is executed but color never changes.
private void tabControl1_DrawItem(object sender, DrawItemEventArgs e)
{
try
{
Rectangle r = e.Bounds;
r = this.tabControl1.GetTabRect(e.Index);
r.Offset(2, 2);
Brush TitleBrush = new SolidBrush(Color.Black);
Brush CloseBrush = new SolidBrush(Color.Gray);
Brush CloseBrushSelected = new SolidBrush(Color.Black);
Font f = this.Font;
string title = this.tabControl1.TabPages[e.Index].Text;
e.Graphics.DrawString(title, f, TitleBrush, new PointF(r.X, r.Y));
if (e is DrawEventArgsCustom)
{
if (((DrawEventArgsCustom)e) != null && ((DrawEventArgsCustom)e).HoverTrue == true)
e.Graphics.DrawString("x", f, CloseBrushSelected, new PointF
(r.X + (this.tabControl1.GetTabRect(e.Index).Width - _imageLocation.X), _imageLocation.Y));
}
e.Graphics.DrawString("x", f, CloseBrush, new PointF
(r.X + (this.tabControl1.GetTabRect(e.Index).Width - _imageLocation.X), _imageLocation.Y));
}
catch (Exception ex)
{
}
}
private void tabControl1_MouseMove(object sender, MouseEventArgs e)
{
Rectangle mouseRect = new Rectangle(e.X, e.Y, 1, 1);
Graphics graphics = CreateGraphics();
for (int i = 0; i < tabControl1.TabCount; i++)
{
if (tabControl1.GetTabRect(i).IntersectsWith(mouseRect))
{
tabControl1_DrawItem(this, new DrawEventArgsCustom(hoverTrue: true, graphics, this.Font, mouseRect, i, DrawItemState.Focus));
}
}
}
class DrawEventArgsCustom : DrawItemEventArgs
{
public DrawEventArgsCustom(bool hoverTrue, Graphics graphics, Font font, Rectangle rect, int index, DrawItemState drawItemState)
: base(graphics, font, rect, index, drawItemState)
{
this.HoverTrue = hoverTrue;
this.Graph = graphics;
this.Fnt = font;
this.Rect = rect;
this.ind = index;
this.drawItemSt = drawItemState;
}
public bool HoverTrue { get; private set; }
public Graphics Graph { get; private set; }
public Font Fnt { get; private set; }
public Rectangle Rect { get; private set; }
public int ind { get; private set; }
public DrawItemState drawItemSt { get; private set; }
}
No need to create new Graphics objects like that, you should do all the drawings in the DrawItem event. For example in this context:
//a class level variable.
private int HoverIndex = -1;
private void tabControl1_DrawItem(object sender, DrawItemEventArgs e)
{
var g = e.Graphics;
var tp = tabControl1.TabPages[e.Index];
var rt = e.Bounds;
var rx = new Rectangle(rt.Right - 20, (rt.Y + (rt.Height - 12)) / 2 + 1, 12, 12);
if ((e.State & DrawItemState.Selected) != DrawItemState.Selected)
{
rx.Offset(0, 2);
}
rt.Inflate(-rx.Width, 0);
rt.Offset(-(rx.Width / 2), 0);
using (Font f = new Font("Marlett", 8f))
using (StringFormat sf = new StringFormat()
{
Alignment = StringAlignment.Center,
LineAlignment = StringAlignment.Center,
Trimming = StringTrimming.EllipsisCharacter,
FormatFlags = StringFormatFlags.NoWrap,
})
{
g.DrawString(tp.Text, tp.Font ?? Font, Brushes.Black, rt, sf);
g.DrawString("r", f, HoverIndex == e.Index ? Brushes.Black : Brushes.LightGray, rx, sf);
}
tp.Tag = rx;
}
Note that, now the Tag property of each TabPage control holds a rectangle for the x button.
In the MouseMove event iterate through the TabPages, cast the x rectangle from the Tag property, check if the x rectangle contains the current e.Location, and call Invalidate(); method of the TabControl to update the drawing:
private void tabControl1_MouseMove(object sender, MouseEventArgs e)
{
for (int i = 0; i < tabControl1.TabCount; i++)
{
var rx =(Rectangle)tabControl1.TabPages[i].Tag;
if (rx.Contains(e.Location))
{
//To avoid the redundant calls.
if (HoverIndex != i)
{
HoverIndex = i;
tabControl1.Invalidate();
}
return;
}
}
//To avoid the redundant calls.
if (HoverIndex != -1)
{
HoverIndex = -1;
tabControl1.Invalidate();
}
}
In the MouseLeave event invalidate if necessary:
private void tabControl1_MouseLeave(object sender, EventArgs e)
{
if (HoverIndex != -1)
{
HoverIndex = -1;
tabControl1.Invalidate();
}
}
And to close/dispose a page, handle the MouseUp event:
private void tabControl1_MouseUp(object sender, MouseEventArgs e)
{
for(int i = 0; i < tabControl1.TabCount; i++)
{
var rx = (Rectangle)tabControl1.TabPages[i].Tag;
if (rx.Contains(rx.Location)) //changed e.Location to rx.Location
{
tabControl1.TabPages[i].Dispose();
return;
}
}
}
Related Posts
TabControl with Close and Add Button
I am trying to simulate a LED display board with c# . I need a control which contains 1536 clickable controls to simulate LEDs (96 in width and 16 in Height). I used a panel named pnlContainer for this and user will add 1536 tiny customized panels at runtime. These customized panels should change their color by click event at runtime. Everything works . But adding this number of tiny panels to the container takes long time ( about 10 secs). What is your suggestion to solve this issue? Any tips are appreciated.
this is my custome panel:
public partial class LedPanel : Panel
{
public LedPanel()
{
InitializeComponent();
}
protected override void OnPaint(PaintEventArgs pe)
{
base.OnPaint(pe);
}
protected override void OnMouseDown(MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
if (this.BackColor == Color.Black)
{
this.BackColor = Color.Red;
}
else
{
this.BackColor = Color.Black;
}
}
}
}
and this is piece of code which adds tiny panels to the pnlContainer :
private void getPixels(Bitmap img2)
{
pnlContainer.Controls.Clear();
for (int i = 0; i < 96; i++)
{
for (int j = 0; j < 16; j++)
{
Custom_Controls.LedPanel led = new Custom_Controls.LedPanel();
led.Name = i.ToString() + j.ToString();
int lWidth = (int)(pnlContainer.Width / 96);
led.Left = i * lWidth;
led.Top = j * lWidth;
led.Width = led.Height = lWidth;
if (img2.GetPixel(i, j).R>numClear.Value)
{
led.BackColor = Color.Red;
}
else
{
led.BackColor = Color.Black;
}
led.BorderStyle = BorderStyle.FixedSingle;
pnlContainer.Controls.Add(led);
}
}
}
Is there any better approach or better control instead of panelto do this?
I agree with what #TaW recommends. Don't put 1000+ controls on a form. Use some sort of data structure, like an array to keep track of which LEDs need to be lit and then draw them in the Paint event of a Panel.
Here's an example. Put a Panel on a form and name it ledPanel. Then use code similar to the following. I just randomly set the values of the boolean array. You would need to set them appropriately in response to a click of the mouse. I didn't include that code, but basically you need to take the location of the mouse click, determine which array entry needs to be set (or unset) and then invalidate the panel so it will redraw itself.
public partial class Form1 : Form
{
//set these variables appropriately
int matrixWidth = 96;
int matrixHeight = 16;
//An array to hold which LEDs must be lit
bool[,] ledMatrix = null;
//Used to randomly populate the LED array
Random rnd = new Random();
public Form1()
{
InitializeComponent();
ledPanel.BackColor = Color.Black;
ledPanel.Resize += LedPanel_Resize;
//clear the array by initializing a new one
ledMatrix = new bool[matrixWidth, matrixHeight];
//Force the panel to repaint itself
ledPanel.Invalidate();
}
private void LedPanel_Resize(object sender, EventArgs e)
{
//If the panel resizes, then repaint.
ledPanel.Invalidate();
}
private void button1_Click(object sender, EventArgs e)
{
//clear the array by initializing a new one
ledMatrix = new bool[matrixWidth, matrixHeight];
//Randomly set 250 of the 'LEDs';
for (int i = 0; i < 250; i++)
{
ledMatrix[rnd.Next(0, matrixWidth), rnd.Next(0, matrixHeight)] = true;
}
//Make the panel repaint itself
ledPanel.Invalidate();
}
private void ledPanel_Paint(object sender, PaintEventArgs e)
{
//Calculate the width and height of each LED based on the panel width
//and height and allowing for a line between each LED
int cellWidth = (ledPanel.Width - 1) / (matrixWidth + 1);
int cellHeight = (ledPanel.Height - 1) / (matrixHeight + 1);
//Loop through the boolean array and draw a filled rectangle
//for each one that is set to true
for (int i = 0; i < matrixWidth; i++)
{
for (int j = 0; j < matrixHeight; j++)
{
if (ledMatrix != null)
{
//I created a custom brush here for the 'off' LEDs because none
//of the built in colors were dark enough for me. I created it
//in a using block because custom brushes need to be disposed.
using (var b = new SolidBrush(Color.FromArgb(64, 0, 0)))
{
//Determine which brush to use depending on if the LED is lit
Brush ledBrush = ledMatrix[i, j] ? Brushes.Red : b;
//Calculate the top left corner of the rectangle to draw
var x = (i * (cellWidth + 1)) + 1;
var y = (j * (cellHeight + 1) + 1);
//Draw a filled rectangle
e.Graphics.FillRectangle(ledBrush, x, y, cellWidth, cellHeight);
}
}
}
}
}
private void ledPanel_MouseUp(object sender, MouseEventArgs e)
{
//Get the cell width and height
int cellWidth = (ledPanel.Width - 1) / (matrixWidth + 1);
int cellHeight = (ledPanel.Height - 1) / (matrixHeight + 1);
//Calculate which LED needs to be turned on or off
int x = e.Location.X / (cellWidth + 1);
int y = e.Location.Y / (cellHeight + 1);
//Toggle that LED. If it's off, then turn it on and if it's on,
//turn it off
ledMatrix[x, y] = !ledMatrix[x, y];
//Force the panel to update itself.
ledPanel.Invalidate();
}
}
I'm sure there can be many improvements to this code, but it should give you an idea on how to do it.
#Chris and #user10112654 are right.
here is a code similar to #Chris but isolates the displaying logic in a separate class. (#Chris answered your question when I was writing the code :))))
just create a 2D array to initialize the class and pass it to the Initialize method.
public class LedDisplayer
{
public LedDisplayer(Control control)
{
_control = control;
_control.MouseDown += MouseDown;
_control.Paint += Control_Paint;
// width and height of your tiny boxes
_width = 5;
_height = 5;
// margin between tiny boxes
_margin = 1;
}
private readonly Control _control;
private readonly int _width;
private readonly int _height;
private readonly int _margin;
private bool[,] _values;
// call this method first of all to initialize the Displayer
public void Initialize(bool[,] values)
{
_values = values;
_control.Invalidate();
}
private void MouseDown(object sender, MouseEventArgs e)
{
var firstIndex = e.X / OuterWidth();
var secondIndex = e.Y / OuterHeight();
_values[firstIndex, secondIndex] = !_values[firstIndex, secondIndex];
_control.Invalidate(); // you can use other overloads of Invalidate method for the blink problem
}
private void Control_Paint(object sender, PaintEventArgs e)
{
if (_values == null)
return;
e.Graphics.Clear(_control.BackColor);
for (int i = 0; i < _values.GetLength(0); i++)
for (int j = 0; j < _values.GetLength(1); j++)
Rectangle(i, j).Paint(e.Graphics);
}
private RectangleInfo Rectangle(int firstIndex, int secondIndex)
{
var x = firstIndex * OuterWidth();
var y = secondIndex * OuterHeight();
var rectangle = new Rectangle(x, y, _width, _height);
if (_values[firstIndex, secondIndex])
return new RectangleInfo(rectangle, Brushes.Red);
return new RectangleInfo(rectangle, Brushes.Black);
}
private int OuterWidth()
{
return _width + _margin;
}
private int OuterHeight()
{
return _height + _margin;
}
}
public class RectangleInfo
{
public RectangleInfo(Rectangle rectangle, Brush brush)
{
Rectangle = rectangle;
Brush = brush;
}
public Rectangle Rectangle { get; }
public Brush Brush { get; }
public void Paint(Graphics graphics)
{
graphics.FillRectangle(Brush, Rectangle);
}
}
this is how it's used in the form:
private void button2_Click(object sender, EventArgs e)
{
// define the displayer class
var displayer = new LedDisplayer(panel1);
// define the array to initilize the displayer
var display = new bool[,]
{
{true, false, false, true },
{false, true, false, false },
{false, false, true, false },
{true, false, false, false }
};
// and finally
displayer.Initialize(display);
}
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 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