如何向窗口标题栏添加额外按钮?

45

我看到有些应用程序(也许不是 .NET 应用程序)在窗体标题栏的最小化按钮左边有一个额外的按钮。我该如何在 C# 中实现这个功能?


2
请注意不要这样做:http://msdn.microsoft.com/en-us/library/aa974173(v=MSDN.10).aspx - "不要将控件添加到窗口框架中,而是将控件放到窗口内部。" - Joey
3
@Johannes,我承认有时这样做的应用程序可能很烦人,但有些应用程序做得非常好。比如Ultramon,它在框架上放置了一个按钮来交换显示器,在Windows 7中看起来非常本地化。因此,有一些应用程序做得很好看。 - Josh
3
@Josh:Office 2007也这样做了(但这并不是Office团队无视任何Windows UX准则的第一次)。尽管如此,那个文件是由UX专业人员撰写的,而仅仅是开发人员(或代码猴子)却努力忽略它们。为了用户的利益,我认为他们应该跟进,除非他们能带来经验和知识、最重要的是可用性测试,以证明相反的观点。 - Joey
3
这就是为什么它们被称为指南而不是法律。如果你读完整个页面,你会看到许多情况,其中微软自己的设计指南曾经被认为是正确的方法,现在却被认为是错误的。 - Josh
6
我喜欢文章,当微软忽略他们自己的指南时。https://s3.amazonaws.com/github-images/blog/2012/gh4w/makingof/Windows8-vs.png - Dave
3个回答

44

更新: 添加了一种适用于启用Aero的 Windows Vista 和 Windows 7 的解决方案


***无Aero解决方案***

窗口的非客户端区域交互是由一系列非客户端特定消息管理的。例如,WM_NCPAINT消息被发送到窗口过程以绘制非客户端区域。

我从未在.NET中完成过这个操作,但我认为您可以重写 WndProc 并处理 WM_NC* 消息以实现您想要的效果。

更新:由于我从未在.NET中尝试过这个操作,所以我抽出了几分钟时间,尝试了一下。

在 Windows 7 上进行测试时,我发现如果我想让操作系统执行非客户端区域的基本渲染,则需要为窗口禁用主题。所以这是一个简短的测试。我使用 GetWindowDC 获取整个窗口的 DC 而不是 GetDCEx,那只是因为我可以从内存中进行Interop而不必查找所有的GetDcEx标志常量。当然,代码还可以进行更多的错误检查。

using System;
using System.Drawing;
using System.Windows.Forms;
using System.Runtime.InteropServices;

namespace WindowsFormsApplication1
{
  public partial class CustomBorderForm : Form
  {
    const int WM_NCPAINT = 0x85;

    [DllImport("user32.dll", SetLastError = true)]
    public static extern IntPtr GetWindowDC(IntPtr hwnd);

    [DllImport("user32.dll", SetLastError = true)]
    public static extern int ReleaseDC(IntPtr hwnd, IntPtr hdc);

    [DllImport("user32.dll", SetLastError = true)]
    public static extern void DisableProcessWindowsGhosting();

    [DllImport("UxTheme.dll", SetLastError = true, CharSet = CharSet.Unicode)]
    public static extern IntPtr SetWindowTheme(IntPtr hwnd, string pszSubAppName, string pszSubIdList);

    public CustomBorderForm()
    {
      // This could be called from main.
      DisableProcessWindowsGhosting();

      InitializeComponent();
    }

    protected override void OnHandleCreated(EventArgs e)
    {
      SetWindowTheme(this.Handle, "", "");
      base.OnHandleCreated(e);
    }

    protected override void WndProc(ref Message m)
    {
      base.WndProc(ref m);
      
      switch (m.Msg)
      {
        case WM_NCPAINT:
          {
            IntPtr hdc = GetWindowDC(m.HWnd);
            using (Graphics g = Graphics.FromHdc(hdc))
            {
              g.FillEllipse(Brushes.Red, new Rectangle((Width-20)/2, 8, 20, 20));
            }
            ReleaseDC(m.HWnd, hdc);
          }
          break;
      }
    }
  }
}

顺便说一下,我调用了DisableProcessWindowsGhosting,这将阻止操作系统在应用程序响应窗口消息的时间过长时绘制非客户区域。如果你不这样做,在某些情况下边框会被渲染但装饰不会显示。所以要根据你的需求来决定是否适合使用此方法。


***支持Aero的解决方案***

受到@TheCodeKing的评论的启发,我认为我应该再次看看这个问题。事实证明,这可以在完全记录的方式下支持Aero完成。但这不是针对新手的。我不会在这里提供完整的解决方案,还有一些细节需要处理,但它可以做到基本功能。

这段代码/解决方案基于Win32示例,可以在以下位置找到http://msdn.microsoft.com/en-us/library/bb688195(VS.85).aspx

原则上,您需要执行以下操作。

  • 通过处理WM_NCCALCSIZE消息并返回0,扩展窗口的客户区以覆盖Frame。这使得非客户区域的大小为0,因此客户区现在覆盖整个窗口。
  • 使用DwmExtendFrameIntoClientArea将帧扩展到客户区域。这使操作系统可以在客户区域上绘制框架。

上述步骤将为您提供具有标准玻璃框架(不包括系统菜单(窗口图标)和标题)的窗口。最小化,最大化和关闭按钮仍将绘制并可用。您无法拖动或调整窗口大小,这是因为框架实际上不存在,记住客户区域覆盖整个窗口,我们只是要求操作系统将框架绘制到客户区域上。

现在您可以像往常一样在窗口上绘制,甚至在框架顶部绘制。您甚至可以在标题区域放置控件。

最后,通过从WndProc中调用DwmDefWindowProc(在处理它之前),允许DWM为您处理命中测试。它返回一个布尔值,指示DWM是否为您处理了消息。


1
@TheCodeKing,我已经更新了答案以回答你的问题。我还在.NET中进行了原型设计,以确认它可以正常工作。这是我参考的MSDN文章http://msdn.microsoft.com/en-us/library/bb688195(VS.85).aspx - Chris Taylor
其实这让我想起来了,我知道如何做到这一点,我有一些代码在某个地方,只是它在设计模式下没有呈现出运行时的样子。你有一个C#的例子可以描述你所说的技术吗?我已经尝试过大多数方法,但它们都不起作用。 - TheCodeKing
2
我有一个非常基本(有限)的样例,是为了测试这个概念而原型化的。你可以从我的skydrive获取代码。这绝不是完整的,只是一个概念验证!https://skydrive.live.com/?cid=5bb13275dc6b8248&sc=documents&uc=1&id=5BB13275DC6B8248%21162# - Chris Taylor
@ChrisTaylor:我尝试使用这些代码通过g.DrawString()改变标题的字体,但是我遇到了一些问题:如果我使用 SetWindowTheme(this.Handle, "", "");,标题会正常显示,但窗体的边框将会是灰色的。如果我使用 SetWindowTheme(this.Handle, "explorer", null);,标题就不会显示(在标题栏后面),但窗体的边框颜色不会变化(默认颜色)。我该怎么做才能以默认颜色显示标题呢?谢谢! - GSP
@Scratte - 谢谢,我没有注意到这个编辑,感谢你提醒我。 - Chris Taylor
显示剩余4条评论

6
简单解决方案:
步骤1:创建一个Windows窗体(这将成为您的自定义标题栏)
-Set Form Border Style to None
-Add whatever controls you would like to this
-I will name this custom form "TitleBarButtons"

第2步。在您想要使用此自定义控件的表单中添加

titleBarBtn = new TitleBarButtons();
titleBarBtn.Location = new Point(this.Location.X + 100, this.Location.Y+5);
titleBarBtn.Show();
titleBarBtn.Owner = this;

对于你的构造函数...你可以调整偏移量,这样就能为我的应用程序找到一个合适的位置。
第三步:将Move事件添加到你的主窗体中。
private void Form14_Move(object sender, EventArgs e)
{
    titleBarBtn.Location = new Point(this.Location.X + 100, this.Location.Y+5);
}

请告诉我,如果您对上述代码的任何部分需要更好的解释。

什么是TitleBarButtons?似乎找不到这个类的任何参考资料。 - Borik
很抱歉,我意识到我表达不够清晰。我相信“TitleBarButtons”是第一步中创建的自定义表单。我会修改我的回答,使其更加明确。 - hrh
@hrh 虽然这是一个相对较旧的帖子,但在WPF中也可以实现将窗口添加到表单的功能。 - murmansk

4
我认为实现这个的一种方法是处理WM_NCPAINT消息(非客户端绘制)来绘制按钮,并处理非客户端鼠标点击以了解某人是否单击了“按钮”。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接