C# WinForm编写一个六边形菜单

 更新时间:2023年09月14日 14:36:18   作者:开发者精选资讯  
这篇文章主要为大家详细介绍了如何利用C# WinForm编写一个六边形菜单,文中的示例代码讲解详细,具有一定的学习价值,有兴趣的可以参考下

背景

闲来无事逛群聊,看到一网友发来这么一个需求:

纯色图下面是一张六边形布局的背景图层,具体图片背景内容不便透露,所以这里采用色块形式模拟,ZF类的项目都喜欢这种给人新奇的东西,这每一个色块,都是一个可以点击进入的菜单。

分析

乍一看这图,就是三个正六边形嵌套。所以就根据公式 2 * Math.PI / 6计算出每个6边形每个顶点偏移角度,再根据公式计算出每个顶点的坐标位置。算法如下:

private static Point[] CalculateHexagonVertices(int sideLength, int offset = 0)
{
    Point[] vertices = new Point[6];
    double angle = 2 * Math.PI / 6;
    for (int i = 0; i < 6; i++)
    {
        int x = offset + (int)(sideLength * Math.Cos(i * angle));
        int y = offset + (int)(sideLength * Math.Sin(i * angle));
        vertices[i] = new Point(x, y);
    }
    return vertices;
}

首先根据 sideLength 边长,计算出最外层正六边形6个顶点的位置。注意这个时候因为顶点是从 (0,0) 计算的,所以这里要加入 offset 设置顶点位置,让每一顶点平移指定的像素。再计算中间那个小正六边形(包括蓝色间隙)的顶点位置。最后再计算最中间不包括间隙的紫色小正六边形顶点位置。这时候有人可能要问中间那些色块的位置怎么确定,这是个好问题,仔细看就知道了,这每一块都是一个等腰梯形,而且每个等腰梯形,都可以根据最外层的大正六边形中间那个小正六边形(包括蓝色间隙)顶点位置计算出来。

实现

先定义一个梯形类:

internal sealed class Trapezoid
{
    public Point TopLeft { get; set; }
    public Point TopRight { get; set; }
    public Point BottomLeft { get; set; }
    public Point BottomRight { get; set; }
    public Point[] Points
    {
        get
        {
            return new Point[] { TopLeft, TopRight, BottomRight, BottomLeft };
        }
    }
    public Trapezoid(Point topLeft, Point topRight, Point bottomLeft, Point bottomRight)
    {
        TopLeft = topLeft;
        TopRight = topRight;
        BottomLeft = bottomLeft;
        BottomRight = bottomRight;
    }
}

然后通过外层的两个正六边形顶点位置计算梯形位置,演示代码如下:

private static Trapezoid[] CalculateTrapezoids(Point[] hexagonVertices, Point[] smallHexagonVertices)
{
    Trapezoid[] trapezoids = new Trapezoid[6];
    for (int i = 0; i < 6; i++)
    {
        Point topLeft = hexagonVertices[i];
        Point topRight = hexagonVertices[(i + 1) % 6];
        Point bottomLeft = smallHexagonVertices[i];
        Point bottomRight = smallHexagonVertices[(i + 1) % 6];
        trapezoids[i] = new Trapezoid(topLeft, topRight, bottomLeft, bottomRight);
    }
    return trapezoids;
}

我写了一个继承自 PictureBox 的控件类,重写了 OnPaint 方法,实现了以上色块的展示。演示代码如下:

protected override void OnPaint(PaintEventArgs e)
{
    base.OnPaint(e);
    var g = e.Graphics;
    //centerHexagon 是中间最小的不包括间隙的紫色的六边形顶点位置
    g.FillPolygon(new SolidBrush(Color.FromArgb(180, Color.Red)), centerHexagon);
    for (int i = 0; i < trapezoids.Length; i++)
    {
        //trapezoids 是6个梯形位置
        var trapezoid = trapezoids[i];
        g.FillPolygon(new SolidBrush(Color.FromArgb(10 * (i + 1), Color.Yellow)), trapezoid.Points);
    }
}

需要实现鼠标点击事件,我们需要定义一个事件,记录鼠标当前位置,然后在 OnMouseClick 方法中检测鼠标位置是否在某个梯形或者最中间位置,然后触发事件。演示代码如下:

//0-6 每个梯形位置,-1 中间位置
//          4 
//     3         5
//         -1
//     2         0
//          1
/// <summary>
/// 菜单点击事件处理
/// </summary>
public event Action<object, int> OnMenuClicked;
/// <summary>
/// 鼠标当前位置
/// </summary>
private Point? mouseHoverLocation = null;
/// <summary>
/// 鼠标滑过的时候,重绘界面,然后设置鼠标位置
/// </summary>
/// <param name="e"></param>
protected override void OnMouseMove(MouseEventArgs e)
{
    base.OnMouseMove(e);
    mouseHoverLocation = e.Location;
    Invalidate();
}
/// <summary>
/// 鼠标移除
/// </summary>
/// <param name="e"></param>
protected override void OnMouseLeave(EventArgs e)
{
    base.OnMouseLeave(e);
    mouseHoverLocation = null;
}
/// <summary>
/// 菜单点击事件
/// </summary>
/// <param name="e"></param>
protected override void OnMouseClick(MouseEventArgs e)
{
    var mouseLocation = e.Location;
    //检测鼠标是否在中间的六边形中:
    if (IsPointInPolygon(mouseLocation, centerHexagon))
    {
        OnMenuClicked?.Invoke(this, -1);
        return;
    }
    //检测是否在某个梯形内部
    for (int i = 0; i < trapezoids.Length; i++)
    {
        var trapezoid = trapezoids[i];
        if (IsPointInTrapezoid(mouseLocation, trapezoid))
        {
            OnMenuClicked?.Invoke(this, i);
            return;
        }
    }
}

里面有一个 IsPointInPolygon 方法,用于检测某一点是否在某个多边形内,这里的算法抄袭了某N上的代码,如下:

/// <summary>
/// 判断点是否在多边形内.
/// 来源:https://blog.csdn.net/xxdddail/article/details/49093635
/// ----------原理----------
/// 注意到如果从P作水平向左的射线的话,如果P在多边形内部,那么这条射线与多边形的交点必为奇数,
/// 如果P在多边形外部,则交点个数必为偶数(0也在内)。
/// </summary>
/// <param name="checkPoint">要判断的点</param>
/// <param name="polygonPoints">多边形的顶点</param>
/// <returns></returns>
private static bool IsPointInPolygon(Point checkPoint, Point[] polygonPoints)
{
    bool inside = false;
    int pointCount = polygonPoints.Length;
    Point p1, p2;
    for (int i = 0, j = pointCount - 1; i < pointCount; j = i, i++)//第一个点和最后一个点作为第一条线,之后是第一个点和第二个点作为第二条线,之后是第二个点与第三个点,第三个点与第四个点...
    {
        p1 = polygonPoints[i];
        p2 = polygonPoints[j];
        if (checkPoint.Y < p2.Y)
        {//p2在射线之上
            if (p1.Y <= checkPoint.Y)
            {//p1正好在射线中或者射线下方
                if ((checkPoint.Y - p1.Y) * (p2.X - p1.X) > (checkPoint.X - p1.X) * (p2.Y - p1.Y))//斜率判断,在P1和P2之间且在P1P2右侧
                {
                    //射线与多边形交点为奇数时则在多边形之内,若为偶数个交点时则在多边形之外。
                    //由于inside初始值为false,即交点数为零。所以当有第一个交点时,则必为奇数,则在内部,此时为inside=(!inside)
                    //所以当有第二个交点时,则必为偶数,则在外部,此时为inside=(!inside)
                    inside = (!inside);
                }
            }
        }
        else if (checkPoint.Y < p1.Y)
        {
            //p2正好在射线中或者在射线下方,p1在射线上
            if ((checkPoint.Y - p1.Y) * (p2.X - p1.X) < (checkPoint.X - p1.X) * (p2.Y - p1.Y))//斜率判断,在P1和P2之间且在P1P2右侧
            {
                inside = (!inside);
            }
        }
    }
    return inside;
}

大致就是这样,再就是缩放窗体时自动计算位置的细节处理,这里先贴一下全部代码:

using System;
using System.Drawing;
using System.Windows.Forms;
namespace HexagonButton
{
    public partial class HButton : PictureBox
    {
        //0-6 每个梯形位置,-1 中间位置
        //          4 
        //     3         5
        //         -1
        //     2         0
        //          1
        /// <summary>
        /// 菜单点击事件处理
        /// </summary>
        public event Action<object, int> OnMenuClicked;
        /// <summary>
        /// 每个梯形位置
        /// </summary>
        private Trapezoid[] trapezoids = new Trapezoid[6];
        /// <summary>
        /// 中间的小正六边形位置
        /// </summary>
        private Point[] centerHexagon = new Point[6];
        /// <summary>
        /// 鼠标当前位置
        /// </summary>
        private Point? mouseHoverLocation = null;
        /// <summary>
        /// 鼠标滑过时的层背景
        /// </summary>
        private SolidBrush mouseHoverLayerBrush = new SolidBrush(Color.FromArgb(50, Color.White));
        public HButton()
        {
            InitializeComponent();
            DoubleBuffered = true;
        }
        /// <summary>
        /// 缩放窗体时(调用),自动修正位置
        /// </summary>
        /// <param name="formWidth"></param>
        /// <param name="formHeight"></param>
        public void ResetSizeByForm(int formWidth, int formHeight)
        {
            var hHeight = (int)(formHeight * 0.8);
            var hWidth = hHeight;
            var hLeft = (formWidth - hWidth) / 2;
            var hTop = (formHeight - hHeight) / 2;
            this.Location = new Point(hLeft, hTop);
            this.Width = hWidth;
            this.Height = hHeight;
        }
        private void InitHexagonMenus()
        {
            this.BackColor = Color.Transparent;
            //this.Image = Properties.Resources.button_bg;
            this.SizeMode = PictureBoxSizeMode.Zoom;
            this.Cursor = Cursors.Hand;
            //计算图片缩放级别
            //var scale = Properties.Resources.button_bg.Width / this.Width;
            //计算原始图片高度和宽度之差,因为该背景非正六边形,所以计算一下宽度和高度之差,用以计算正确得位置
            //var diffOfImageSize = (Properties.Resources.button_bg.Width - Properties.Resources.button_bg.Height) / scale;
            var diffOfImageSize = 0;
            var sideWidth = (this.Width - diffOfImageSize) / 2;
            var offset = this.Width / 2;
            //计算最外层大六边形顶点位置
            var big = CalculateHexagonVertices((this.Width + diffOfImageSize / 2) / 2, offset);
            //计算内部小六边形顶点位置
            var small = CalculateHexagonVertices(sideWidth / 2, offset);
            //计算两个六边形相交之后,形成得六边形环,分割为6个等腰梯形,用以检测点击事件
            trapezoids = CalculateTrapezoids(big, small);
            //计算内部小的正六边形,用以检测点击事件
            centerHexagon = CalculateHexagonVertices(sideWidth / 2 - 20, offset);
        }
        /// <summary>
        /// 鼠标滑过的时候,重绘界面,然后设置鼠标位置
        /// </summary>
        /// <param name="e"></param>
        protected override void OnMouseMove(MouseEventArgs e)
        {
            base.OnMouseMove(e);
            mouseHoverLocation = e.Location;
            Invalidate();
        }
        /// <summary>
        /// 鼠标移除
        /// </summary>
        /// <param name="e"></param>
        protected override void OnMouseLeave(EventArgs e)
        {
            base.OnMouseLeave(e);
            mouseHoverLocation = null;
        }
        /// <summary>
        /// 菜单点击事件
        /// </summary>
        /// <param name="e"></param>
        protected override void OnMouseClick(MouseEventArgs e)
        {
            var mouseLocation = e.Location;
            //检测鼠标是否在中间的六边形中:
            if (IsPointInPolygon(mouseLocation, centerHexagon))
            {
                OnMenuClicked?.Invoke(this, -1);
                return;
            }
            //检测是否在某个梯形内部
            for (int i = 0; i < trapezoids.Length; i++)
            {
                var trapezoid = trapezoids[i];
                if (IsPointInTrapezoid(mouseLocation, trapezoid))
                {
                    OnMenuClicked?.Invoke(this, i);
                    return;
                }
            }
        }
        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);
            var g = e.Graphics;
#if DEBUG
            //以下的代码可以直接删除,这里是作为标识多边形位置
            g.FillPolygon(new SolidBrush(Color.FromArgb(180, Color.Red)), centerHexagon);
            for (int i = 0; i < trapezoids.Length; i++)
            {
                var trapezoid = trapezoids[i];
                g.FillPolygon(new SolidBrush(Color.FromArgb(10 * (i + 1), Color.Yellow)), trapezoid.Points);
            }
#endif
            if (mouseHoverLocation == null)
            {
                return;
            }
            //检测鼠标是否在中间的六边形中:
            if (IsPointInPolygon(mouseHoverLocation.Value, centerHexagon))
            {
                g.FillPolygon(mouseHoverLayerBrush, centerHexagon);
                return;
            }
            //检测是否在某个梯形内部
            for (int i = 0; i < trapezoids.Length; i++)
            {
                var trapezoid = trapezoids[i];
                if (IsPointInTrapezoid(mouseHoverLocation.Value, trapezoid))
                {
                    g.FillPolygon(mouseHoverLayerBrush, trapezoid.Points);
                    return;
                }
            }
        }
        /// <summary>
        /// 计算正六边形的顶点坐标
        /// </summary>
        /// <param name="sideLength"></param>
        /// <param name="offset"></param>
        /// <returns></returns>
        private static Point[] CalculateHexagonVertices(int sideLength, int offset = 0)
        {
            Point[] vertices = new Point[6];
            double angle = 2 * Math.PI / 6;
            for (int i = 0; i < 6; i++)
            {
                int x = offset + (int)(sideLength * Math.Cos(i * angle));
                int y = offset + (int)(sideLength * Math.Sin(i * angle));
                vertices[i] = new Point(x, y);
            }
            return vertices;
        }
        /// <summary>
        /// 计算每个梯形的坐标
        /// </summary>
        /// <param name="hexagonVertices"></param>
        /// <param name="smallHexagonVertices"></param>
        /// <returns></returns>
        private static Trapezoid[] CalculateTrapezoids(Point[] hexagonVertices, Point[] smallHexagonVertices)
        {
            Trapezoid[] trapezoids = new Trapezoid[6];
            for (int i = 0; i < 6; i++)
            {
                Point topLeft = hexagonVertices[i];
                Point topRight = hexagonVertices[(i + 1) % 6];
                Point bottomLeft = smallHexagonVertices[i];
                Point bottomRight = smallHexagonVertices[(i + 1) % 6];
                trapezoids[i] = new Trapezoid(topLeft, topRight, bottomLeft, bottomRight)
                {
                    Index = i
                };
            }
            return trapezoids;
        }
        /// <summary>
        /// 判断点是否在梯形内
        /// </summary>
        /// <param name="checkPoint"></param>
        /// <param name="trapezoid"></param>
        /// <returns></returns>
        private static bool IsPointInTrapezoid(Point checkPoint, Trapezoid trapezoid)
        {
            return IsPointInPolygon(checkPoint, trapezoid.Points);
        }
        /// <summary>
        /// 判断点是否在多边形内.
        /// 来源:https://blog.csdn.net/xxdddail/article/details/49093635
        /// ----------原理----------
        /// 注意到如果从P作水平向左的射线的话,如果P在多边形内部,那么这条射线与多边形的交点必为奇数,
        /// 如果P在多边形外部,则交点个数必为偶数(0也在内)。
        /// </summary>
        /// <param name="checkPoint">要判断的点</param>
        /// <param name="polygonPoints">多边形的顶点</param>
        /// <returns></returns>
        private static bool IsPointInPolygon(Point checkPoint, Point[] polygonPoints)
        {
            bool inside = false;
            int pointCount = polygonPoints.Length;
            Point p1, p2;
            for (int i = 0, j = pointCount - 1; i < pointCount; j = i, i++)//第一个点和最后一个点作为第一条线,之后是第一个点和第二个点作为第二条线,之后是第二个点与第三个点,第三个点与第四个点...
            {
                p1 = polygonPoints[i];
                p2 = polygonPoints[j];
                if (checkPoint.Y < p2.Y)
                {//p2在射线之上
                    if (p1.Y <= checkPoint.Y)
                    {//p1正好在射线中或者射线下方
                        if ((checkPoint.Y - p1.Y) * (p2.X - p1.X) > (checkPoint.X - p1.X) * (p2.Y - p1.Y))//斜率判断,在P1和P2之间且在P1P2右侧
                        {
                            //射线与多边形交点为奇数时则在多边形之内,若为偶数个交点时则在多边形之外。
                            //由于inside初始值为false,即交点数为零。所以当有第一个交点时,则必为奇数,则在内部,此时为inside=(!inside)
                            //所以当有第二个交点时,则必为偶数,则在外部,此时为inside=(!inside)
                            inside = (!inside);
                        }
                    }
                }
                else if (checkPoint.Y < p1.Y)
                {
                    //p2正好在射线中或者在射线下方,p1在射线上
                    if ((checkPoint.Y - p1.Y) * (p2.X - p1.X) < (checkPoint.X - p1.X) * (p2.Y - p1.Y))//斜率判断,在P1和P2之间且在P1P2右侧
                    {
                        inside = (!inside);
                    }
                }
            }
            return inside;
        }
        /// <summary>
        /// 当窗体改变时,自动计算大小
        /// </summary>
        /// <param name="e"></param>
        protected override void OnClientSizeChanged(EventArgs e)
        {
            base.OnClientSizeChanged(e);
            InitHexagonMenus();
        }
    }
    /// <summary>
    /// 梯形类
    /// </summary>
    internal sealed class Trapezoid
    {
        public int Index { get; set; }
        public Point TopLeft { get; set; }
        public Point TopRight { get; set; }
        public Point BottomLeft { get; set; }
        public Point BottomRight { get; set; }
        public Point[] Points
        {
            get
            {
                return new Point[] { TopLeft, TopRight, BottomRight, BottomLeft };
            }
        }
        public Trapezoid(Point topLeft, Point topRight, Point bottomLeft, Point bottomRight)
        {
            TopLeft = topLeft;
            TopRight = topRight;
            BottomLeft = bottomLeft;
            BottomRight = bottomRight;
        }
        public override string ToString()
        {
            return $"Trapezoid {{ Index={Index}, TopLeft={TopLeft}, TopRight={TopRight}, BottomLeft={BottomLeft}, BottomRight={BottomRight} }}";
        }
    }
}

窗体代码:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
namespace HexagonButton
{
    public partial class Form1 : Form
    {
        private HButton HButton;
        public Form1()
        {
            InitializeComponent();
            this.WindowState = FormWindowState.Maximized;
            //这个窗体里面把那个纯色的背景图放进去
            this.BackColor = Color.FromArgb(9, 56, 128);
        }
        protected override void OnSizeChanged(EventArgs e)
        {
            base.OnSizeChanged(e);
            if (HButton == null)
            {
                HButton = new HButton();
                HButton.OnMenuClicked += (s, index) =>
                {
                    MessageBox.Show($"点击了菜单:#{index}");
                };
                this.Controls.Add(HButton);
            }
            else
            {
                HButton.ResetSizeByForm(this.Width, this.Height);
            }
        }
    }
}

就是在窗体缩放时,把六边形菜单动态加进去。

结局

代码发给网友之后,网友表示很满意,然后经过我的指导,然后自由发挥改成了7边形,然后我又扩展了下面这一版,更加智能了。截一些图片在这里:

最终版演示 DEMO:

做着做着,就一发不可收拾了,做的越来越顺眼了... 最后,该菜单通过增加了一些配置,使得可以自定义边数,是否包含中间菜单等功能,功能更加强大了一些。该项目已全部开源,开源地址:https://github.com/mrhuo/polymenuNUGET 安装:

Install-Package MrHuo.PolyMenu -Version 1.0.23.913

以上就是C# WinForm编写一个六边形菜单的详细内容,更多关于C# WinForm菜单的资料请关注脚本之家其它相关文章!

相关文章

  • 解析StreamReader与文件乱码问题的解决方法

    解析StreamReader与文件乱码问题的解决方法

    本篇文章是对StreamReader与文件乱码问题的解决方法进行了详细的分析介绍,需要的朋友参考下
    2013-05-05
  • C#操作数据库中存取图片文件的方法

    C#操作数据库中存取图片文件的方法

    这篇文章主要介绍了C#操作数据库中存取图片文件的方法,以实例形式分析了C#将图片存入数据库及从数据库读取图片文件的相关技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-10-10
  • WPF利用DrawingContext实现绘制温度计

    WPF利用DrawingContext实现绘制温度计

    这篇文章主要为大家详细介绍了如何利用WPF和DrawingContext实现绘制温度计,文中的示例代码讲解详细,对我们学习或工作有一定帮助,感兴趣的小伙伴可以了解一下
    2022-09-09
  • C#和Java中二维数组区别分析

    C#和Java中二维数组区别分析

    这篇文章主要介绍了C#和Java中二维数组区别分析,主要讲述了二维数组在C#和Java中定义及应用的区别,非常实用,需要的朋友可以参考下
    2014-10-10
  • 基于WPF实现弹幕效果的示例代码

    基于WPF实现弹幕效果的示例代码

    这篇文章主要为大家详细介绍了如何利用WPF实现弹幕效果,文中的示例代码讲解详细,对我们学习或工作有一定帮助,感兴趣的小伙伴可以了解一下
    2022-09-09
  • C#实现DataList里面嵌套DataList的折叠菜单

    C#实现DataList里面嵌套DataList的折叠菜单

    这篇文章主要介绍了C#实现DataList里面嵌套DataList的折叠菜单,以实例形式详细分析了DataList嵌套实现折叠菜单所涉及的JavaScript、HTML与C#相关使用技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-11-11
  • Unity实现角色受击身体边缘发光特效

    Unity实现角色受击身体边缘发光特效

    这篇文章主要为大家详细介绍了Unity实现角色受击身体边缘发光特效,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-04-04
  • C#中的ComboBox控件详细使用方法

    C#中的ComboBox控件详细使用方法

    这篇文章主要给大家介绍了关于C#中ComboBox控件详细使用的相关资料,ComboBox控件是一个很容易使用出错的控件,在此将常用的操作写下来,以备后用,需要的朋友可以参考下
    2023-09-09
  • C#实现判断文件夹存在与否并创建文件夹的方法

    C#实现判断文件夹存在与否并创建文件夹的方法

    这篇文章主要介绍了C#实现判断文件夹存在与否并创建文件夹的方法,涉及C#针对文件及目录的判断与创建操作相关技巧,需要的朋友可以参考下
    2017-02-02
  • C# 7.0中解构功能详解

    C# 7.0中解构功能详解

    这篇文章主要为大家详细介绍了C# 7.0中的解构功能,解构元组、解构对象等,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-08-08

最新评论