且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

如何在窗口的标题栏中添加一个额外的按钮?

更新时间:2023-01-05 10:07:38

UPDATE:添加了一个解决方案,该解决方案适用于为 Windows Vista 和 Windows 7 启用 Aero 的情况

UPDATE: Added a solution that will work with Aero enabled for Windows Vista and Windows 7

窗口交互的非客户区由一系列非客户特定的消息管理.例如WM_NCPAINT消息被发送到窗口过程来绘制非客户区.

The non-client area of a window interaction is managed by a series of non-client specfic messages. For example WM_NCPAINT message is sent to the window procedure to paint the non-client area.

我从来没有在 .NET 上做过这个,但我怀疑你可以覆盖 WndProc 并处理 WM_NC* 消息来实现你想要的.

I have never done this from .NET, but I suspect you can overide the WndProc and handle the WM_NC* messages to achieve what you want.

更新:因为我从来没有在 .NET 上尝试过这个,所以我有几分钟的时间并想我会快速尝试一下.

Update: Since I never tried this from .NET I got a few minutes and thought I would give it a quick try.

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

Trying this on Windows 7, I found that I needed to disable the Themes for the Window if I wanted to OS to do the base rendering of the non-client area. So here is a short test. I used GetWindowDC to get the DC of the entire window rather than GetDCEx, that was just because I could interop that from memory and did not have lookup all the flag constants for GetDcEx. And of course the code could do with more error checking.

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,如果应用程序响应 Windows 消息的时间过长,这将阻止操作系统绘制非客户区.如果您不这样做,则在某些情况下将呈现边框但不会显示您的装饰.所以这取决于您的要求,它是否适合您.

Btw. I called DisableProcessWindowsGhosting, this will stop the OS from drawing the non-client area if the application takes too long to respond to windows messages. If you do not do this, then in some situations the border will be renderd but your adornments will not be shown. So that depends on your requirements it that is right for you or not.

受到@TheCodeKing 评论的提示,我想我会再看看这个.事实证明,这可以在支持 Aero 的同时以完整记录的方式完成.但它不适合胆小的人.我不会在这里提供一个完整的解决方案,仍然有一些问题需要锻炼,但它可以解决基础问题.

Prompted by the comment from @TheCodeKing, I thought I would take another look at this. It turns out this can be done in a fully documented way while supporting Aero. But it is not for the faint of heart. I will not provide a complete solution here, there are still some kinks to workout, but it does the basics.

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

This code/solution is based off the Win32 example which can be found at the following location http://msdn.microsoft.com/en-us/library/bb688195(VS.85).aspx

原则上你需要做的是以下.

In principal what you need to do is the following.

  • 扩展窗口的客户区以覆盖框架.这是通过处理 WM_NCCALCSIZE 消息并返回 0 来完成的.这使非客户区的大小为 0,因此客户区现在覆盖了整个窗口.
  • 使用 DwmExtendFrameIntoClientArea 将框架扩展到客户区.这让操作系统在客户区渲染框架.
  • Extend the client area of the window to cover the Frame. This is done by handling the WM_NCCALCSIZE message and returning 0. This gives the Non-Client area a size of 0 and therefore the client area now covers the entire window.
  • Extend the Frame into the client area using DwmExtendFrameIntoClientArea. This gets the OS to render the Frame over the client area.

以上步骤将为您提供一个标准玻璃框架的窗口,不包括系统菜单(窗口图标)和标题.最小化、最大化和关闭按钮仍将被绘制并起作用.你不能做的是拖动或调整窗口大小,这是因为框架并不存在,记住客户区覆盖整个窗口,我们刚刚要求操作系统将框架绘制到客户区.

The above steps will give you a windows with the standard glass frame excluding the system menu (Window Icon) and the title. The minimize, maximize and close buttons will still be drawn and will work. What you will not be able to do is drag or resize the window, this is because the frame is not really there, remember the client area covers the whole window, we have just asked the OS to draw the frame onto the client area.

现在您可以照常在窗口上绘图,甚至可以在框架的顶部.您甚至可以在标题区域放置控件.

Now you can draw on the window as normal, even on top of the frame. You can even put controls in the caption area.

最后,通过调用 DwmDefWindowProc 来自您的 WndProc(在您处理它之前).它返回一个布尔值,指示 DWM 是否为您处理了消息.

Finally, allow the DWM to handle hit-testing for you, by calling DwmDefWindowProc from your WndProc (before you've processed it). It returns a boolean indicating whether the DWM handled the message for you.