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菜单的资料请关注脚本之家其它相关文章!