【问题标题】:Create a custom shaped windows form instead of 'default' rectangle?创建自定义形状的窗口窗体而不是“默认”矩形?
【发布时间】:2014-08-14 12:21:10
【问题描述】:

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

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

有没有办法让表单形状成为这个图像的形状,而不是“通常”的矩形?

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

我听说过使用“透明键”,但这会从背景中移除颜色(我将在稍后阶段使用颜色选择器,因此如果用户选择了该特定颜色,它将不会显示)。

一如既往,我们将不胜感激。

【问题讨论】:

  • 是的,它告诉你使用他不想使用的 TransparencyKey 属性。
  • @DylanCorriveau 正在使用透明密钥,我在我的问题中提到了它为什么不合适:(
  • 如果您担心使用透明度键会丢失颜色,您可以随时在使用颜色选择器时更改它...
  • 还有这一点:“尽管设置了 TransparencyKey 属性,但设置为大于 24 位颜色深度的显示器可能会出现显示问题,因为表单的某些部分不透明。为了避免这个问题,请确保在显示控制面板中将显示器的颜色深度设置为小于 24 位。在开发具有这种透明度的应用程序时,请记住,您必须让您的用户意识到这个问题。”跨度>

标签: c# winforms customization


【解决方案1】:

here on MSDN 讨论的 TransparencyKey 方法是最简单的方法。您将表单的 BackgroundImage 设置为图像掩码。图像蒙版具有填充某种颜色的透明区域——紫红色是一种流行的选择,因为实际上没有人使用这种可怕的颜色。然后将表单的TransparencyKey 属性设置为这种颜色,它基本上被屏蔽掉,将这些部分呈现为透明。

但我想在颜色选择器中,您希望紫红色作为一个选项可用,即使没有人选择它。因此,您必须以另一种方式创建自定义形状的表单——通过设置自定义区域。基本上,您创建一个Region object(基本上只是一个多边形)来描述表单的所需形状,然后将其分配给表单的Region property

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

还有一个警告……我强烈建议不要这样做。完成它需要相当多的工作,即使你完成了,结果通常也是华而不实和用户敌对的。即使一切顺利,您最终也会得到看起来像this 的东西——没有人想要这样。用户非常习惯于无聊的旧矩形应用程序窗口。应用程序不应试图成为现实世界小部件的精确数字复制品。 似乎这样会使它们直观或易于使用,但实际上并非如此。良好设计的关键是为您的应用程序确定用户的心智模型,并找出一种将其与目标窗口环境设置的标准相结合的好方法。


我注意到这个标签仍然打开并且有一些空闲时间,所以我尝试快速制作一个示例。我让“表格”由两个随机大小的圆圈组成,只是为了强调自定义形状效果和透明度——不要在设计中读到任何东西或得到任何疯狂的想法!这是我想出的:

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 类的限制;它是在视觉样式发明之前编写的。实现对此的支持需要做更多的工作,但有a WinForms wrapper。您必须确保编写后备代码来处理视觉样式也被禁用的情况。
  • 字幕按钮实际上并没有居中——我只是盯着它看。即使你的眼球比我的好,这仍然是一个糟糕的方法,因为标题按钮可以有不同的大小,这取决于系统设置和你运行的操作系统版本(Vista 改变了按钮的形状)。李>
  • 当鼠标向上 移到标题栏按钮上时,其他窗口会调用这些操作。但是当您尝试使用WM_NCLBUTTONUP(而不是WM_NCLBUTTONDOWN)时,您必须双击标题按钮才能使它们起作用。这是因为非客户区正在捕获鼠标。我确信有一个解决方案,但在我发现它之前我已经没有耐心了。
  • 当窗口最小化(或恢复)时,您不会获得漂亮的动画效果,字幕按钮也不会在悬停时发光。您可以免费获得大量视觉细节,而这些默认样式在此处未使用。其中一些可以通过编写更多代码轻松添加,但是对于您编写的每一行代码,维护负担都会猛增——较新版本的 Windows 可能会破坏一些东西。更糟糕的是,有些事情实现起来,所以它可能甚至不值得。而所有这些努力又是为了什么?
  • 在激活/停用时重新绘制整个表单只是为了更新标题按钮可能是个坏主意。如果您在表单上绘制其他更复杂的内容,这可能会降低整个系统的速度。
  • 一旦开始向表单添加控件,您可能会遇到问题。例如,即使使用 Label 控件,您也无法通过单击并按住该 Label 控件的顶部来拖动表单。标签控件不返回 HTTRANSPARENT 以响应 WM_NCHITTEST 消息,因此消息不会传递到父窗体。您可以继承 Label 来这样做并使用您的子类。
  • 代码完全未经 Windows 8 测试,因为我没有副本。自定义非客户区往往会因新的操作系统更新而崩溃,这些更新会改变非客户区的呈现方式,因此您需要自己相应地调整代码。即使它有效,它也肯定不会具有正确的 Windows 8 外观。
  • 等等等等

您还可以看到,就像我在上面警告的那样,圆形边框没有抗锯齿,因此看起来参差不齐。不幸的是,这是无法修复的。

【讨论】:

  • 我只会使用直边@Cody。我只将它用于自定义“外壳” - 删除任务栏/等。并用这个代替它。我也在查看一些 Rainmeter 应用程序,并希望(也许)将其放入我的项目中。只是想知道 - (1) 你可以在同一个项目中有多个区域吗?即屏幕底部的一个“部分”,屏幕顶部的“第二个”? (2) 如果我让它可拖动,该区域是否也必须是“可拖动的”?
  • 与项目无关。您为每个 form 设置区域。区域只是像画笔或钢笔这样的图形对象,Windows 允许您为屏幕上的每个窗口设置自定义区域。是的,您可以实现拖动,但您可能必须编写代码来手动处理它。它涉及继承 Form 类,覆盖其 WndProc 方法,并处理 WM_NCHITTEST message 以在光标位于表单的某个部分上时返回适当的值。
  • 听起来这个项目变得更有趣了!我会调查这个@Cody,谢谢你的帮助!这纯粹是因为我有一个雨量计皮肤 ATM,它使用了大量的 CPU,我决定承担这个任务!再次感谢您的澄清,我会进一步调查!
  • @jbutler483 可能很有趣,甚至可能具有教育意义,但绝非易事!我整天都在编写这种代码,但我仍然花费了比预期更长的时间来获得示例的详细信息。 (当然,区域部分很简单——困难的部分是绘制一个等效的自定义标题栏。)我用我想出的代码更新了我的答案,如果你遇到困难,也许这会对你有所帮助。 (另外,不要指望这在 CPU/GPU 消耗方面会特别少。这些效果往往很费力。)
猜你喜欢
  • 1970-01-01
  • 2011-04-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-02-02
相关资源
最近更新 更多