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

标签 c# winforms customization

我正在寻找在C#中创建自定义形状的表单。

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

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

我只是问这个问题,因为我希望为我的PC设计一个自定义外观(有点像雨量计/火箭坞相结合,但采用“压缩”方式)。

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

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

最佳答案

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上并行运行:



ew!它确实可以工作,但是距离完成还有很长的路要走。还有很多小事情需要完成。例如:


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


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

关于c# - 创建自定义形状的Windows窗体而不是“默认”矩形?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/25307970/

相关文章:

c# - 动态地从动态对象中获取值

c# - 是否可以使用 MVP 方法在 Main 中实例化演示者?

vb.net - 如何在 VS 2010 项目中定义 'Start external program' 的相对路径?

ruby - IRb:如何使用预加载类启动交互式 ruby​​ session

java - 将自定义TensorFlowLite模型添加到就绪的Android应用程序时出现问题

c# - 多重性与参照约束冲突

c# - 使用 Kentico API 9 创建多元文化产品

C# .NET SqlDependency

c# - 从 Windows API 返回的消息

android - 在自定义键盘中添加下一个按钮