创建自定义形状的窗体而不是“默认”矩形?

我期待在c#中创建一个自定义形状的表单。

我有一个在某些地方透明的背景图像(png)。

无论如何使表单形状成为此图像的形状而不是“通常”的矩形?

我只想问这个,因为我希望为我的PC设计一个自定义皮肤(有点像雨量计/火箭岩组合,但是以’压缩’的方式)。

我听说过使用’透明度键’,但这会从背景中删除一种颜色(我将在后期使用颜色选择器,因此如果用户选择了特定颜色,则不会显示)。

一如既往,任何帮助将不胜感激。

在MSDN上讨论的TransparencyKey方法是最简单的方法。 您将窗体的BackgroundImage设置为图像蒙版。 图像蒙版具有透明的区域,填充了某种颜色 – 紫红色是一种流行的选择,因为没有人真正使用这种可怕的颜色。 然后将表单的TransparencyKey属性设置为此颜色,它基本上被屏蔽掉,将这些部分渲染为透明。

但我想在一个颜色选择器中,你想要紫红色作为一个选项,即使没有人选择它。 因此,您必须通过设置自定义区域来创建自定义形状的表单。 基本上,您创建一个Region对象 (基本上只是一个多边形)来描述表单的所需形状,然后将其分配给表单的Region属性 。

请注意,当您执行此操作时,您正在更改整个窗口的形状,而不仅仅是客户区,因此您的设计需要考虑到这一点。 此外,区域不能消除锯齿,因此如果您使用的是没有直边的形状,结果往往会非常难看。

另一个警告……我强烈建议要这样做。 要完成它需要相当多的工作,甚至一旦完成,结果通常是华而不实和用户敌意。 即使一切顺利,你也会得到一些看起来像这样的东西 – 没有人想要那样。 用户习惯于厌倦旧的矩形应用程序窗口。 应用程序不应该试图成为真实世界小部件的精确数字副本。 这似乎会使它们直观或易于使用,但实际上并非如此。 良好设计的关键是确定用户的应用程序的心智模型,并找出与目标窗口环境设置的标准进行网格划分的好方法。


我注意到这个标签仍然打开,有一些空闲的时刻,所以我试着快速抽样。 我制作的“forms”由两个随机大小的圆圈组成,只是为了强调自定义形状效果和透明度 – 不要在设计中阅读任何内容或获取任何疯狂的想法! 这就是我想出的:

 using System; using System.Drawing; using System.Drawing.Drawing2D; using System.Windows.Forms; public class MyCrazyForm : Form { private Size szFormSize = new Size(600, 600); private Size szCaptionButton = SystemInformation.CaptionButtonSize; private Rectangle rcMinimizeButton = new Rectangle(new Point(330, 130), szCaptionButton); private Rectangle rcCloseButton = new Rectangle(new Point(rcMinimizeButton.X + szCaptionButton.Width + 3, rcMinimizeButton.Y), SystemInformation.CaptionButtonSize); public MyCrazyForm() { // Not necessary in this sample: the designer was not used. //InitializeComponent(); // Force the form's size, and do not let it be changed. this.Size = szFormSize; this.MinimumSize = szFormSize; this.MaximumSize = szFormSize; // Do not show a standard title bar (since we can't see it anyway)! this.FormBorderStyle = FormBorderStyle.None; // Set up the irregular shape of the form. using (GraphicsPath path = new GraphicsPath()) { path.AddEllipse(0, 0, 200, 200); path.AddEllipse(120, 120, 475, 475); this.Region = new Region(path); } } protected override void OnActivated(EventArgs e) { base.OnActivated(e); // Force a repaint on activation. this.Invalidate(); } protected override void OnDeactivate(EventArgs e) { base.OnDeactivate(e); // Force a repaint on deactivation. this.Invalidate(); } protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); // Draw the custom title bar ornamentation. if (this.Focused) { ControlPaint.DrawCaptionButton(e.Graphics, rcMinimizeButton, CaptionButton.Minimize, ButtonState.Normal); ControlPaint.DrawCaptionButton(e.Graphics, rcCloseButton, CaptionButton.Close, ButtonState.Normal); } else { ControlPaint.DrawCaptionButton(e.Graphics, rcMinimizeButton, CaptionButton.Minimize, ButtonState.Inactive); ControlPaint.DrawCaptionButton(e.Graphics, rcCloseButton, CaptionButton.Close, ButtonState.Inactive); } } private Point GetPointFromLParam(IntPtr lParam) { // Handle 64-bit builds, which we detect based on the size of a pointer. // Otherwise, this is functionally equivalent to the Win32 MAKEPOINTS macro. uint dw = unchecked(IntPtr.Size == 8 ? (uint)lParam.ToInt64() : (uint)lParam.ToInt32()); return new Point(unchecked((short)dw), unchecked((short)(dw >> 16))); } protected override void WndProc(ref Message m) { const int WM_SYSCOMMAND = 0x112; const int WM_NCHITTEST = 0x84; const int WM_NCLBUTTONDOWN = 0xA1; const int HTCLIENT = 1; const int HTCAPTION = 2; const int HTMINBUTTON = 8; const int HTCLOSE = 20; // Provide additional handling for some important messages. switch (m.Msg) { case WM_NCHITTEST: { base.WndProc(ref m); Point ptClient = PointToClient(GetPointFromLParam(m.LParam)); if (rcMinimizeButton.Contains(ptClient)) { m.Result = new IntPtr(HTMINBUTTON); } else if (rcCloseButton.Contains(ptClient)) { m.Result = new IntPtr(HTCLOSE); } else if (m.Result.ToInt32() == HTCLIENT) { // Make the rest of the form's entire client area draggable // by having it report itself as part of the caption region. m.Result = new IntPtr(HTCAPTION); } return; } case WM_NCLBUTTONDOWN: { base.WndProc(ref m); if (m.WParam.ToInt32() == HTMINBUTTON) { this.WindowState = FormWindowState.Minimized; m.Result = IntPtr.Zero; } else if (m.WParam.ToInt32() == HTCLOSE) { this.Close(); m.Result = IntPtr.Zero; } return; } case WM_SYSCOMMAND: { // Setting the form's MaximizeBox property to false does *not* disable maximization // behavior when the caption area is double-clicked. // Since this window is fixed-size and does not support a "maximized" mode, and the // entire client area is treated as part of the caption to enable dragging, we also // need to ensure that double-click-to-maximize is disabled. // NOTE: See documentation for WM_SYSCOMMAND for explanation of the magic value 0xFFF0! const int SC_MAXIMIZE = 0xF030; if ((m.WParam.ToInt32() & 0xFFF0) == SC_MAXIMIZE) { m.Result = IntPtr.Zero; } else { base.WndProc(ref m); } return; } } base.WndProc(ref m); } } 

它在Windows XP和7上并行运行:

呼! 它确实有效,但距离完成还有很长的路要走。 还有很多小事需要做。 例如:

  • 单击时,标题按钮不会“按下”。 有一个可以与DrawCaptionButton方法一起使用的内置状态,但是你需要在单击其中一个按钮时强制重绘,或者直接在表单上直接重新绘制。
  • 它不支持视觉样式。 这是ControlPaint类的限制; 它是在Visual Styles发明之前编写的。 实现对此的支持将是更多的工作,但有一个WinForms包装器 。 您必须确保编写回退代码以处理禁用可视样式的情况。
  • 标题按钮实际上并不居中 – 我只是用它来表示。 即使你的眼球比我的好,这仍然是一个糟糕的方法,因为字幕按钮可以是不同的大小,这取决于系统设置和你正在运行的操作系统版本(Vista改变了按钮形状)。
  • 当鼠标在标题栏按钮上方时,其他窗口会调用操作。 但是当您尝试使用WM_NCLBUTTONUP (而不是WM_NCLBUTTONDOWN )时,您必须双击标题按钮才能使它们正常工作。 这是因为非客户区正在捕获鼠标。 我确信有一个解决方案,但在我发现它之前我没有耐心。
  • 当窗口最小化(或恢复)时,您不会获得漂亮的动画效果,也没有为标题按钮hover发光。 使用默认样式可以免费获得大量的视觉细节,这些样式在这里缺失。 通过编写更多代码可以轻松添加其中一些代码,但对于您编写的每一行,维护负担都会急剧增加 – 较新版本的Windows可能会破坏内容。 更糟糕的是,有些事情实现起来并不容易,所以它甚至可能都不值得。 所有这些努力再次成为什么?
  • 激活/停用重绘整个表单只是为了更新标题按钮可能是一个坏主意。 如果你在表单上绘制更复杂的东西,这可能会减慢整个系统的速度。
  • 一旦开始向表单添加控件,您可能会遇到问题。 例如,即使使用Label控件,也无法通过单击并按住Label控件顶部来拖动表单。 标签控件不会返回HTTRANSPARENT以响应WM_NCHITTEST消息,因此消息不会传递给父窗体。 您可以将Label子类化,然后使用您的子类。
  • 由于我没有副本,因此代码完全未经Windows 8测试。 自定义非客户端区域往往会爆炸新的操作系统更新,这些更新会改变非客户区域的呈现方式,因此您可以自行调整相应的代码。 即使它工作,它肯定不会有正确的Windows 8外观。
  • 等等等等

您还可以看到,就像我上面提到的那样,圆形边框没有消除锯齿,所以它看起来像是锯齿状。 不幸的是,这是不可修复的。