如何绘制自定义滑块控件?

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 属性 MaxMinValue .我们可以在这里更改它们,拇指位置会自动更新。

我们仍然需要启用移动滑块的代码。当我们点击拇指时,我们点击的可能会偏离它的中心一点。移动鼠标时保持此偏移量感觉很自然。因此,我们将这个差异存储在变量_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);
    }
}