WPF 实现拟物音量控件

  • 框架使用.NET6
  • Visual Studio 2022;
  • 绘制使用了Canvas作为容器控件,DrawingContext上绘制水平线。
  • 当鼠标滚动滚轮时或按下鼠标向上向下拖动时计算角度偏移并更新图层。.
WPF 实现拟物音量控件

1)创建 Wheel.xaml 代码如下:

<UserControl x:Class="TopControl.Wheel"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:TopControl"
             mc:Ignorable="d" 
             Width="30" Height="180">
    <Grid>
        <!-- 进度 -->
        <Grid Width="2" Background="#0a0a0a" Height="180" HorizontalAlignment="Left" VerticalAlignment="Bottom"/>
        <Grid Width="2" x:Name="Grid_Value" Height="0" HorizontalAlignment="Left" VerticalAlignment="Bottom">
            <Grid Height="180" VerticalAlignment="Bottom">
                <Grid.Background>
                    <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
                        <GradientStop Offset="0" Color="#f0d967"/>
                        <GradientStop Offset="1" Color="#33b08d"/>
                    </LinearGradientBrush>
                </Grid.Background>
            </Grid>
        </Grid>
        <Grid Background="#0a0a0a" Width="26" HorizontalAlignment="Right" Height="180" Margin="2,0,0,0"/>
        <!-- 滚轮 -->
        <Grid x:Name="WheelArea" Height="176" Width="22" HorizontalAlignment="Right" Margin="0,0,2,0"
              MouseDown="WheelArea_MouseDown" MouseMove="WheelArea_MouseMove" MouseUp="WheelArea_MouseUp" MouseWheel="WheelArea_MouseWheel">
            <Grid.Background>
                <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
                    <GradientStop Color="#141414" Offset="0"/>
                    <GradientStop Color="#3c3c3c" Offset="0.5"/>
                    <GradientStop Color="#141414" Offset="1"/>
                </LinearGradientBrush>
            </Grid.Background>
            <Grid x:Name="LayerBox" IsHitTestVisible="False"/>
        </Grid>
    </Grid>
</UserControl>

2)创建 Wheel.xaml.cs 代码如下:

using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace TopControl
{
    public partial class Wheel : UserControl
    {
        public Wheel()
        {
            InitializeComponent();
            Loaded += Wheel_Loaded;
        }

        #region 属性

        public int MinValue { get; set; } = -720;

        public int MaxValue { get; set; } = 720;

        public int Value { get; set; } = -720;

        #endregion

        #region 控件事件

        private void Wheel_Loaded(object sender, RoutedEventArgs e)
        {
            InitLayer();
        }

        private void WheelArea_MouseDown(object sender, MouseButtonEventArgs e)
        {
            if (e.ChangedButton == MouseButton.Left)
            {
                BeginDrag();
            }
        }

        private void WheelArea_MouseMove(object sender, MouseEventArgs e)
        {
            if (_dragMode)
            {
                Drag();
            }
        }

        private void WheelArea_MouseUp(object sender, MouseButtonEventArgs e)
        {
            if (e.ChangedButton == MouseButton.Left && _dragMode)
            {
                EndDrag();
            }
        }

        private void WheelArea_MouseWheel(object sender, MouseWheelEventArgs e)
        {
            if (_dragMode) return;

            int offset = e.Delta / 120;
            if (offset < 0 && Value > MinValue)
            {
                Value += offset;
                UpdateProgress();
                _wheelLayer.AngleOffset -= offset * _wheelSpeed;
                _wheelLayer.UpdateLayer();
            }
            else if (offset > 0 && Value < MaxValue)
            {
                Value += offset;
                UpdateProgress();
                _wheelLayer.AngleOffset -= offset * _wheelSpeed;
                _wheelLayer.UpdateLayer();
            }
        }

        #endregion

        #region 鼠标操作

        private void BeginDrag()
        {
            _dragMode = true;
            WheelArea.CaptureMouse();

            _dragStart = Mouse.GetPosition(WheelArea);
            _angleStart = _wheelLayer.AngleOffset;

            _valueStart = Value;
            _offsetDown = Value - MinValue;
            _offsetUp = Value - MaxValue;
        }

        private void Drag()
        {
            double offset_y = Mouse.GetPosition(WheelArea).Y - _dragStart.Y;
            if (offset_y < _offsetUp) offset_y = _offsetUp;
            else if (offset_y > _offsetDown) offset_y = _offsetDown;

            double offset_angle = offset_y * _wheelSpeed;
            Value = _valueStart - (int)offset_y;
            _wheelLayer.AngleOffset = _angleStart + offset_angle;
            UpdateProgress();
            _wheelLayer.UpdateLayer();
        }

        private void EndDrag()
        {
            _dragMode = false;
            WheelArea.ReleaseMouseCapture();
        }

        #endregion

        #region 私有方法

        /// <summary>
        /// 初始化图层
        /// </summary>
        private void InitLayer()
        {
            _wheelLayer.Width = LayerBox.ActualWidth;
            _wheelLayer.Height = LayerBox.ActualHeight;
            LayerBox.Children.Add(_wheelLayer);
            _wheelLayer.Init();
            _wheelLayer.UpdateLayer();
        }

        /// <summary>
        /// 更新进度
        /// </summary>
        private void UpdateProgress()
        {
            Grid_Value.Height = (double)(Value - MinValue) / (MaxValue - MinValue) * 180;
        }

        #endregion

        #region 字段

        private readonly WheelLayer _wheelLayer = new WheelLayer();

        private Point _dragStart = new Point();
        private double _angleStart = 0;
        private int _valueStart = 0;
        private bool _dragMode = false;

        /// <summary>滚轮速度</summary>
        private readonly double _wheelSpeed = 0.7;

        /// <summary>最大向上偏移</summary>
        private double _offsetUp;
        /// <summary>最大向下偏移</summary>
        private double _offsetDown;

        #endregion
    }
}

3)创建 WheelLayer.cs 代码如下:

using MathUtil;
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Media;
using WpfUtil;

namespace TopControl
{
    public class WheelLayer : SingleLayer
    {
        #region 属性

        /// <summary>槽高度:180px - 上下边框(4px)</summary>
        public int GrooveHeight { get; set; } = 176;

        /// <summary>角度:圆弧切线与槽边的夹角</summary>
        public int Angle { get; set; } = 90;

        /// <summary>刻度线数量</summary>
        public int LineCount { get; set; } = 90;

        /// <summary>角度偏移</summary>
        public double AngleOffset { get; set; } = 0;

        #endregion

        #region 公开方法

        public override void Init()
        {
            // 起点、终点、中点
            Point2D startPoint = new Point2D(0, 0);
            Point2D endPoint = new Point2D(GrooveHeight, 0);
            Point2D centerPoint = startPoint.CenterWith(endPoint);
            // 向量:中点 -> 起点
            Vector2 centerToStart = new Vector2(centerPoint, startPoint);
            centerToStart.Rotate(-90);
            // 向量:终点 -> 中点
            Vector2 endToCenter = new Vector2(endPoint, centerPoint);
            endToCenter.Rotate(-90 + Angle);
            // 圆心
            _circleCenter = centerToStart.IntersectionWith(endToCenter);
            // 向量:圆心 -> 起点
            Vector2 vector = new Vector2(_circleCenter, startPoint);
            _radius = vector.Distance;
            _anglePerLine = 360.0 / LineCount;
        }

        protected override void OnUpdate()
        {
            // 最高点
            Point2D top = new Point2D(_circleCenter.X, _circleCenter.Y - _radius);
            // 向量:圆心 -> 最高点
            Vector2 vector = new Vector2(_circleCenter, top);
            double max = Math.Abs(vector.Target.Y);
            // 偏移角度
            vector.Rotate(AngleOffset);
            // 开始旋转计算刻度位置
            List<Point2D> pointList = new List<Point2D>();
            for (int counter = 0; counter < LineCount; counter++)
            {
                if (vector.Target.Y < 0) pointList.Add(vector.Target);
                vector.Rotate(-_anglePerLine);
            }

            // 绘制刻度线
            foreach (var item in pointList)
                DrawHorizontalLine(item.X, Math.Abs(item.Y) / max);
        }

        #endregion

        #region 私有方法

        /// <summary>
        /// 绘制水平线
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private void DrawHorizontalLine(double y, double opacity)
        {
            Pen pen = new Pen(new SolidColorBrush(Color.FromArgb((byte)(opacity * 255), 32, 32, 32)), 1);
            Pen pen2 = new Pen(new SolidColorBrush(Color.FromArgb((byte)(opacity * 255), 64, 64, 64)), 1);
            _dc.DrawLine(pen, new Point(2, y - 0.5), new Point(Width - 2, y - 0.5));
            _dc.DrawLine(pen2, new Point(2, y + 0.5), new Point(Width - 2, y + 0.5));
        }

        #endregion

        #region 字段

        /// <summary>圆心</summary>
        private Point2D _circleCenter = new Point2D(0, 0);
        /// <summary>半径</summary>
        private double _radius = 0;
        /// <summary>刻度线之间的夹角</summary>
        private double _anglePerLine = 0;

        #endregion
    }
}

4)创建 WheelExample.xaml 代码如下:

<wd:Window x:Class="TopControl.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:TopControl"
        xmlns:wd="https://github.com/WPFDevelopersOrg/WPFDevelopers"
        mc:Ignorable="d" Background="#1e1e1e"
        Title="https://github.com/WPFDevelopersOrg/SimulationControl - 俞宏伟" Height="450" Width="800">
    <Grid>
        <local:Wheel HorizontalAlignment="Left" VerticalAlignment="Top" Margin="50"/>
    </Grid>
</wd:Window>

编辑:驚鏵

更多代码请访问GitHub源码地址

WPF 实现拟物音量控件