如何绘制自定义滑块控件?
How to draw a custom slider control?
我创建了一个滑块用户控件,但是在 运行 时,当我向左或向右移动滑块时,为什么它没有到达终点或吞没?
在用户控件设计器中我添加了一个图片框控件:
然后在我做的代码中:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace Extract
{
public partial class Slider : UserControl
{
public float Height;
public float Min = 0.0f;
public float Max = 1.0f;
private float defaultValue = 0.1f;
public Slider()
{
InitializeComponent();
}
private void sliderControl_Paint(object sender, PaintEventArgs e)
{
float bar_size = 0.45f;
float x = Bar(defaultValue);
int y = (int)(sliderControl.Height * bar_size);
e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
e.Graphics.FillRectangle(Brushes.DimGray, 0, y, sliderControl.Width, y / 2);
e.Graphics.FillRectangle(Brushes.Red, 0, y, x, sliderControl.Height - 2 * y);
using (Pen pen = new Pen(Color.Black, 8))
{
e.Graphics.FillRectangle(Brushes.Red, 0, y, x, y / 2);
FillCircle(e.Graphics, Brushes.Red, x, y + y / 4, y / 2);
}
using (Pen pen = new Pen(Color.White, 5))
{
DrawCircle(e.Graphics, pen, x, y + y / 4, y/ 2);
}
}
public static void DrawCircle(Graphics g, Pen pen,
float centerX, float centerY, float radius)
{
g.DrawEllipse(pen, centerX - radius, centerY - radius,
radius + radius, radius + radius);
}
public static void FillCircle(Graphics g, Brush brush,
float centerX, float centerY, float radius)
{
g.FillEllipse(brush, centerX - radius, centerY - radius,
radius + radius, radius + radius);
}
private float Bar(float value)
{
return (sliderControl.Width - 24) * (value - Min) / (float)(Max - Min);
}
private void Thumb(float value)
{
if (value < Min) value = Min;
if (value > Max) value = Max;
defaultValue = value;
sliderControl.Refresh();
}
private float SliderWidth(int x)
{
return Min + (Max - Min) * x / (float)(sliderControl.Width);
}
protected override void OnResize(EventArgs e)
{
base.OnResize(e);
MaintainPictureBoxSize();
}
private void MaintainPictureBoxSize()
{
sliderControl.SizeMode = PictureBoxSizeMode.Normal;
sliderControl.Location = new Point();
sliderControl.Size = new Size();
var clientSize = this.ClientSize;
if (sliderControl.Image == null)
sliderControl.Size = clientSize;
else
{
Size s = sliderControl.Image.Size;
sliderControl.Size = new Size(
clientSize.Width > s.Width ? clientSize.Width : s.Width,
clientSize.Height > s.Height ? clientSize.Height : s.Height);
}
}
bool mouse = false;
private void sliderControl_MouseDown(object sender, MouseEventArgs e)
{
mouse = true;
Thumb(SliderWidth(e.X));
}
private void sliderControl_MouseMove(object sender, MouseEventArgs e)
{
if (!mouse) return;
Thumb(SliderWidth(e.X));
}
private void sliderControl_MouseUp(object sender, MouseEventArgs e)
{
mouse = false;
}
}
}
当我将控件拖到 form1 设计器然后 运行 应用程序然后当我将滑块拖动到左侧或右侧时,滑块的圆圈部分被吞没。
如果我将 form1 设计器中的控件调整得更小,然后 运行将应用程序设置为左侧,它会像以前一样吞下,但在右侧它根本不会结束。
最简单的解释方法是显示图像:
现在,在图片框内,想象拇指圆圈位于最左边和最右边的位置。这意味着条形必须从 x = 半径开始,并且条形的宽度必须是图片框的宽度减去半径的两倍。
所有内容都必须画在图片框内(虚线)。但这不需要在 PictureBox
上放置在 UserControl
上。让我们从 Control
派生滑块。
public class Slider : Control
{
...
}
现在,在第一次编译此代码后,此滑块会自动出现在工具箱中 window 并准备好放置在表单设计器中的表单上。
由于我们希望能够在属性中设置它的属性window并且我们希望能够在滑动后读取当前值,所以我们添加一个事件和一些属性。
public event EventHandler ValueChanged;
private float _min = 0.0f;
public float Min
{
get => _min;
set {
_min = value;
RecalculateParameters();
}
}
private float _max = 1.0f;
public float Max
{
get => _max;
set {
_max = value;
RecalculateParameters();
}
}
private float _value = 0.3f;
public float Value
{
get => _value;
set {
_value = value;
ValueChanged?.Invoke(this, EventArgs.Empty);
RecalculateParameters();
}
}
这需要一些字段和 RecalculateParameters
方法。
private float _radius;
private PointF _thumbPos;
private SizeF _barSize;
private PointF _barPos;
private void RecalculateParameters()
{
_radius = 0.5f * ClientSize.Height;
_barSize = new SizeF(ClientSize.Width - 2f * _radius, 0.5f * ClientSize.Height);
_barPos = new PointF(_radius, (ClientSize.Height - _barSize.Height) / 2);
_thumbPos = new PointF(
_barSize.Width / (Max - Min) * Value + _barPos.X,
_barPos.Y + 0.5f * _barSize.Height);
Invalidate();
}
在这个派生控件中,我们覆盖事件处理程序(On...
方法)而不是订阅事件:
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
e.Graphics.FillRectangle(Brushes.DimGray,
_barPos.X, _barPos.Y, _barSize.Width, _barSize.Height);
e.Graphics.FillRectangle(Brushes.Red,
_barPos.X, _barPos.Y, _thumbPos.X - _barPos.X, _barSize.Height);
e.Graphics.FillCircle(Brushes.White, _thumbPos.X, _thumbPos.Y, _radius);
e.Graphics.FillCircle(Brushes.Red, _thumbPos.X, _thumbPos.Y, 0.7f * _radius);
}
protected override void OnResize(EventArgs e)
{
base.OnResize(e);
RecalculateParameters();
}
现在让我们编译这段代码,并向窗体添加一个滑块。看看我们如何在设计器中调整它的大小。
另请注意,在属性 window 中,我们在“其他”部分看到了新的 Slider
属性 Max
、Min
和 Value
.我们可以在这里更改它们,拇指位置会自动更新。
我们仍然需要启用移动滑块的代码。当我们点击拇指时,我们点击的可能会偏离它的中心一点。移动鼠标时保持此偏移量感觉很自然。因此,我们将这个差异存储在变量_delta
.
中
bool _moving = false;
SizeF _delta;
protected override void OnMouseDown(MouseEventArgs e)
{
base.OnMouseDown(e);
// Difference between tumb and mouse position.
_delta = new SizeF(e.Location.X - _thumbPos.X, e.Location.Y - _thumbPos.Y);
if (_delta.Width * _delta.Width + _delta.Height * _delta.Height <= _radius * _radius) {
// Clicking inside thumb.
_moving = true;
}
}
我们还使用Pythagorean theorem计算OnMouseDown
中鼠标位置到拇指位置的距离。仅当鼠标在拇指内部时,我们通过设置 _moving = true;
开始移动拇指
在OnMouseMove
中我们计算并设置新的Value
。这会自动触发重新计算参数并重新绘制滑块。
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
if (_moving) {
float thumbX = e.Location.X - _delta.Width;
if (thumbX < _barPos.X) {
thumbX = _barPos.X;
} else if (thumbX > _barPos.X + _barSize.Width) {
thumbX = _barPos.X + _barSize.Width;
}
Value = (thumbX - _barPos.X) * (Max - Min) / _barSize.Width;
}
}
protected override void OnMouseUp(MouseEventArgs e)
{
base.OnMouseUp(e);
_moving = false;
}
我们可以通过向表单添加 TextBox
并响应 ValueChanged
事件来测试滑块。我们可以通过单击闪光符号将属性 window 切换为“事件”来添加事件处理程序,然后双击“其他”部分中的 ValueChanged
。
private void Slider1_ValueChanged(object sender, EventArgs e)
{
textBox1.Text = slider1.Value.ToString();
}
现在,当我们移动滑块时,文本框会显示值。
滑块的完整代码(使用 C# 10.0 文件范围命名空间):
using System.Drawing.Drawing2D;
namespace WinFormsSliderBar;
public class Slider : Control
{
private float _radius;
private PointF _thumbPos;
private SizeF _barSize;
private PointF _barPos;
public event EventHandler ValueChanged;
public Slider()
{
// This reduces flicker
DoubleBuffered = true;
}
private float _min = 0.0f;
public float Min
{
get => _min;
set {
_min = value;
RecalculateParameters();
}
}
private float _max = 1.0f;
public float Max
{
get => _max;
set {
_max = value;
RecalculateParameters();
}
}
private float _value = 0.3f;
public float Value
{
get => _value;
set {
_value = value;
ValueChanged?.Invoke(this, EventArgs.Empty);
RecalculateParameters();
}
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
e.Graphics.FillRectangle(Brushes.DimGray,
_barPos.X, _barPos.Y, _barSize.Width, _barSize.Height);
e.Graphics.FillRectangle(Brushes.Red,
_barPos.X, _barPos.Y, _thumbPos.X - _barPos.X, _barSize.Height);
e.Graphics.FillCircle(Brushes.White, _thumbPos.X, _thumbPos.Y, _radius);
e.Graphics.FillCircle(Brushes.Red, _thumbPos.X, _thumbPos.Y, 0.7f * _radius);
}
protected override void OnResize(EventArgs e)
{
base.OnResize(e);
RecalculateParameters();
}
private void RecalculateParameters()
{
_radius = 0.5f * ClientSize.Height;
_barSize = new SizeF(ClientSize.Width - 2f * _radius, 0.5f * ClientSize.Height);
_barPos = new PointF(_radius, (ClientSize.Height - _barSize.Height) / 2);
_thumbPos = new PointF(
_barSize.Width / (Max - Min) * Value + _barPos.X,
_barPos.Y + 0.5f * _barSize.Height);
Invalidate();
}
bool _moving = false;
SizeF _delta;
protected override void OnMouseDown(MouseEventArgs e)
{
base.OnMouseDown(e);
// Difference between tumb and mouse position.
_delta = new SizeF(e.Location.X - _thumbPos.X, e.Location.Y - _thumbPos.Y);
if (_delta.Width * _delta.Width + _delta.Height * _delta.Height <= _radius * _radius) {
// Clicking inside thumb.
_moving = true;
}
}
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
if (_moving) {
float thumbX = e.Location.X - _delta.Width;
if (thumbX < _barPos.X) {
thumbX = _barPos.X;
} else if (thumbX > _barPos.X + _barSize.Width) {
thumbX = _barPos.X + _barSize.Width;
}
Value = (thumbX - _barPos.X) * (Max - Min) / _barSize.Width;
}
}
protected override void OnMouseUp(MouseEventArgs e)
{
base.OnMouseUp(e);
_moving = false;
}
}
以及绘制圆圈的图形扩展:
namespace WinFormsSliderBar;
public static class GraphicsExtensions
{
public static void DrawCircle(this Graphics g, Pen pen,
float centerX, float centerY, float radius)
{
g.DrawEllipse(pen, centerX - radius, centerY - radius,
radius + radius, radius + radius);
}
public static void FillCircle(this Graphics g, Brush brush,
float centerX, float centerY, float radius)
{
g.FillEllipse(brush, centerX - radius, centerY - radius,
radius + radius, radius + radius);
}
}
我创建了一个滑块用户控件,但是在 运行 时,当我向左或向右移动滑块时,为什么它没有到达终点或吞没?
在用户控件设计器中我添加了一个图片框控件:
然后在我做的代码中:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace Extract
{
public partial class Slider : UserControl
{
public float Height;
public float Min = 0.0f;
public float Max = 1.0f;
private float defaultValue = 0.1f;
public Slider()
{
InitializeComponent();
}
private void sliderControl_Paint(object sender, PaintEventArgs e)
{
float bar_size = 0.45f;
float x = Bar(defaultValue);
int y = (int)(sliderControl.Height * bar_size);
e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
e.Graphics.FillRectangle(Brushes.DimGray, 0, y, sliderControl.Width, y / 2);
e.Graphics.FillRectangle(Brushes.Red, 0, y, x, sliderControl.Height - 2 * y);
using (Pen pen = new Pen(Color.Black, 8))
{
e.Graphics.FillRectangle(Brushes.Red, 0, y, x, y / 2);
FillCircle(e.Graphics, Brushes.Red, x, y + y / 4, y / 2);
}
using (Pen pen = new Pen(Color.White, 5))
{
DrawCircle(e.Graphics, pen, x, y + y / 4, y/ 2);
}
}
public static void DrawCircle(Graphics g, Pen pen,
float centerX, float centerY, float radius)
{
g.DrawEllipse(pen, centerX - radius, centerY - radius,
radius + radius, radius + radius);
}
public static void FillCircle(Graphics g, Brush brush,
float centerX, float centerY, float radius)
{
g.FillEllipse(brush, centerX - radius, centerY - radius,
radius + radius, radius + radius);
}
private float Bar(float value)
{
return (sliderControl.Width - 24) * (value - Min) / (float)(Max - Min);
}
private void Thumb(float value)
{
if (value < Min) value = Min;
if (value > Max) value = Max;
defaultValue = value;
sliderControl.Refresh();
}
private float SliderWidth(int x)
{
return Min + (Max - Min) * x / (float)(sliderControl.Width);
}
protected override void OnResize(EventArgs e)
{
base.OnResize(e);
MaintainPictureBoxSize();
}
private void MaintainPictureBoxSize()
{
sliderControl.SizeMode = PictureBoxSizeMode.Normal;
sliderControl.Location = new Point();
sliderControl.Size = new Size();
var clientSize = this.ClientSize;
if (sliderControl.Image == null)
sliderControl.Size = clientSize;
else
{
Size s = sliderControl.Image.Size;
sliderControl.Size = new Size(
clientSize.Width > s.Width ? clientSize.Width : s.Width,
clientSize.Height > s.Height ? clientSize.Height : s.Height);
}
}
bool mouse = false;
private void sliderControl_MouseDown(object sender, MouseEventArgs e)
{
mouse = true;
Thumb(SliderWidth(e.X));
}
private void sliderControl_MouseMove(object sender, MouseEventArgs e)
{
if (!mouse) return;
Thumb(SliderWidth(e.X));
}
private void sliderControl_MouseUp(object sender, MouseEventArgs e)
{
mouse = false;
}
}
}
当我将控件拖到 form1 设计器然后 运行 应用程序然后当我将滑块拖动到左侧或右侧时,滑块的圆圈部分被吞没。
如果我将 form1 设计器中的控件调整得更小,然后 运行将应用程序设置为左侧,它会像以前一样吞下,但在右侧它根本不会结束。
最简单的解释方法是显示图像:
现在,在图片框内,想象拇指圆圈位于最左边和最右边的位置。这意味着条形必须从 x = 半径开始,并且条形的宽度必须是图片框的宽度减去半径的两倍。
所有内容都必须画在图片框内(虚线)。但这不需要在 PictureBox
上放置在 UserControl
上。让我们从 Control
派生滑块。
public class Slider : Control
{
...
}
现在,在第一次编译此代码后,此滑块会自动出现在工具箱中 window 并准备好放置在表单设计器中的表单上。
由于我们希望能够在属性中设置它的属性window并且我们希望能够在滑动后读取当前值,所以我们添加一个事件和一些属性。
public event EventHandler ValueChanged;
private float _min = 0.0f;
public float Min
{
get => _min;
set {
_min = value;
RecalculateParameters();
}
}
private float _max = 1.0f;
public float Max
{
get => _max;
set {
_max = value;
RecalculateParameters();
}
}
private float _value = 0.3f;
public float Value
{
get => _value;
set {
_value = value;
ValueChanged?.Invoke(this, EventArgs.Empty);
RecalculateParameters();
}
}
这需要一些字段和 RecalculateParameters
方法。
private float _radius;
private PointF _thumbPos;
private SizeF _barSize;
private PointF _barPos;
private void RecalculateParameters()
{
_radius = 0.5f * ClientSize.Height;
_barSize = new SizeF(ClientSize.Width - 2f * _radius, 0.5f * ClientSize.Height);
_barPos = new PointF(_radius, (ClientSize.Height - _barSize.Height) / 2);
_thumbPos = new PointF(
_barSize.Width / (Max - Min) * Value + _barPos.X,
_barPos.Y + 0.5f * _barSize.Height);
Invalidate();
}
在这个派生控件中,我们覆盖事件处理程序(On...
方法)而不是订阅事件:
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
e.Graphics.FillRectangle(Brushes.DimGray,
_barPos.X, _barPos.Y, _barSize.Width, _barSize.Height);
e.Graphics.FillRectangle(Brushes.Red,
_barPos.X, _barPos.Y, _thumbPos.X - _barPos.X, _barSize.Height);
e.Graphics.FillCircle(Brushes.White, _thumbPos.X, _thumbPos.Y, _radius);
e.Graphics.FillCircle(Brushes.Red, _thumbPos.X, _thumbPos.Y, 0.7f * _radius);
}
protected override void OnResize(EventArgs e)
{
base.OnResize(e);
RecalculateParameters();
}
现在让我们编译这段代码,并向窗体添加一个滑块。看看我们如何在设计器中调整它的大小。
另请注意,在属性 window 中,我们在“其他”部分看到了新的 Slider
属性 Max
、Min
和 Value
.我们可以在这里更改它们,拇指位置会自动更新。
我们仍然需要启用移动滑块的代码。当我们点击拇指时,我们点击的可能会偏离它的中心一点。移动鼠标时保持此偏移量感觉很自然。因此,我们将这个差异存储在变量_delta
.
bool _moving = false;
SizeF _delta;
protected override void OnMouseDown(MouseEventArgs e)
{
base.OnMouseDown(e);
// Difference between tumb and mouse position.
_delta = new SizeF(e.Location.X - _thumbPos.X, e.Location.Y - _thumbPos.Y);
if (_delta.Width * _delta.Width + _delta.Height * _delta.Height <= _radius * _radius) {
// Clicking inside thumb.
_moving = true;
}
}
我们还使用Pythagorean theorem计算OnMouseDown
中鼠标位置到拇指位置的距离。仅当鼠标在拇指内部时,我们通过设置 _moving = true;
在OnMouseMove
中我们计算并设置新的Value
。这会自动触发重新计算参数并重新绘制滑块。
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
if (_moving) {
float thumbX = e.Location.X - _delta.Width;
if (thumbX < _barPos.X) {
thumbX = _barPos.X;
} else if (thumbX > _barPos.X + _barSize.Width) {
thumbX = _barPos.X + _barSize.Width;
}
Value = (thumbX - _barPos.X) * (Max - Min) / _barSize.Width;
}
}
protected override void OnMouseUp(MouseEventArgs e)
{
base.OnMouseUp(e);
_moving = false;
}
我们可以通过向表单添加 TextBox
并响应 ValueChanged
事件来测试滑块。我们可以通过单击闪光符号将属性 window 切换为“事件”来添加事件处理程序,然后双击“其他”部分中的 ValueChanged
。
private void Slider1_ValueChanged(object sender, EventArgs e)
{
textBox1.Text = slider1.Value.ToString();
}
现在,当我们移动滑块时,文本框会显示值。
滑块的完整代码(使用 C# 10.0 文件范围命名空间):
using System.Drawing.Drawing2D;
namespace WinFormsSliderBar;
public class Slider : Control
{
private float _radius;
private PointF _thumbPos;
private SizeF _barSize;
private PointF _barPos;
public event EventHandler ValueChanged;
public Slider()
{
// This reduces flicker
DoubleBuffered = true;
}
private float _min = 0.0f;
public float Min
{
get => _min;
set {
_min = value;
RecalculateParameters();
}
}
private float _max = 1.0f;
public float Max
{
get => _max;
set {
_max = value;
RecalculateParameters();
}
}
private float _value = 0.3f;
public float Value
{
get => _value;
set {
_value = value;
ValueChanged?.Invoke(this, EventArgs.Empty);
RecalculateParameters();
}
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
e.Graphics.FillRectangle(Brushes.DimGray,
_barPos.X, _barPos.Y, _barSize.Width, _barSize.Height);
e.Graphics.FillRectangle(Brushes.Red,
_barPos.X, _barPos.Y, _thumbPos.X - _barPos.X, _barSize.Height);
e.Graphics.FillCircle(Brushes.White, _thumbPos.X, _thumbPos.Y, _radius);
e.Graphics.FillCircle(Brushes.Red, _thumbPos.X, _thumbPos.Y, 0.7f * _radius);
}
protected override void OnResize(EventArgs e)
{
base.OnResize(e);
RecalculateParameters();
}
private void RecalculateParameters()
{
_radius = 0.5f * ClientSize.Height;
_barSize = new SizeF(ClientSize.Width - 2f * _radius, 0.5f * ClientSize.Height);
_barPos = new PointF(_radius, (ClientSize.Height - _barSize.Height) / 2);
_thumbPos = new PointF(
_barSize.Width / (Max - Min) * Value + _barPos.X,
_barPos.Y + 0.5f * _barSize.Height);
Invalidate();
}
bool _moving = false;
SizeF _delta;
protected override void OnMouseDown(MouseEventArgs e)
{
base.OnMouseDown(e);
// Difference between tumb and mouse position.
_delta = new SizeF(e.Location.X - _thumbPos.X, e.Location.Y - _thumbPos.Y);
if (_delta.Width * _delta.Width + _delta.Height * _delta.Height <= _radius * _radius) {
// Clicking inside thumb.
_moving = true;
}
}
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
if (_moving) {
float thumbX = e.Location.X - _delta.Width;
if (thumbX < _barPos.X) {
thumbX = _barPos.X;
} else if (thumbX > _barPos.X + _barSize.Width) {
thumbX = _barPos.X + _barSize.Width;
}
Value = (thumbX - _barPos.X) * (Max - Min) / _barSize.Width;
}
}
protected override void OnMouseUp(MouseEventArgs e)
{
base.OnMouseUp(e);
_moving = false;
}
}
以及绘制圆圈的图形扩展:
namespace WinFormsSliderBar;
public static class GraphicsExtensions
{
public static void DrawCircle(this Graphics g, Pen pen,
float centerX, float centerY, float radius)
{
g.DrawEllipse(pen, centerX - radius, centerY - radius,
radius + radius, radius + radius);
}
public static void FillCircle(this Graphics g, Brush brush,
float centerX, float centerY, float radius)
{
g.FillEllipse(brush, centerX - radius, centerY - radius,
radius + radius, radius + radius);
}
}