65.9K
CodeProject 正在变化。 阅读更多。
Home

WinForm 扩展

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (44投票s)

2010 年 7 月 17 日

CPOL

11分钟阅读

viewsIcon

166695

downloadIcon

10015

一个扩展了 Microsoft 提供标准功能的 WinForm。

Screen_Shot.jpg

引言

在本文中,我将为大家介绍一个扩展版的 Windows Form。它比 .NET Framework 提供的常规 Form 具有更多功能。

最初,我根据 MSDN 用户的请求将这些功能单独实现。所以,我决定将它们整合在一起,构建一个 Form 来在本篇文章中展示。

FormEx 的附加功能包括:

  • 在标题栏和 Form 边框上绘图
  • 将 Form 附加到桌面,类似于 Windows Vista 和 7 的侧边栏
  • 将 Form 设置为全屏模式(覆盖包括任务栏在内的所有内容)
  • 使 Form 不可移动
  • 使 Form 不可调整大小(即使 `FormBorderStyle` 不是 `Fixed`)
  • 随时获取 `KeyState`(按下和抬起/切换和未切换状态)
  • 禁用窗口标题栏上的关闭按钮。

除获取按键状态外,所有这些功能都可通过设计器使用,因此非常易于使用,我将在下文进行解释。

Using the Code

正如我所提到的,使用这些功能非常简单。在提供的示例项目中,您可以看到所有这些功能,就像截图一样。我们将逐一介绍它们,但首先,让我们看看如何使用这个 Form。

您可以通过两种方式将新 Form 添加到您的解决方案中:

  • 将文章顶部的二进制文件添加到您的项目中。方法是:在解决方案资源管理器中 -> 您的项目 -> 右键单击“引用...” -> 添加引用... -> 浏览 -> 找到并选择 `FormEx.dll`。
  • 或者,您可以包含带有源代码的项目(也已提供),然后像上面一样添加引用,但选择“项目”而不是“浏览”。

完成这些操作后,您就可以开始编码了。首先,从您的项目中选择一个 Form 并编辑其源代码。您需要将其修改为继承自 `FormEx` 而不是 `Form`。

using FormExNS;

namespace TestProject
{
    public partial class TestForm : FormEx
    { 

现在,进入功能部分。

1 - 在标题栏和 Form 边框上绘图

正如您在截图中看到的,我在标题栏上进行了绘制。为此,我实现了一个新事件:`PaintFrameArea`。您只需在设计器中转到“事件”,然后以与处理 `Paint` 方法相同的方式创建事件处理程序。它的工作方式完全相同,`Graphics` 对象设置为覆盖整个窗口,但不包括客户端区域。

此绘图是在 Windows 以其主题样式绘制窗口之后完成的,因此您在调整大小时可能会遇到一些闪烁。您也应该避免绘制在 `ControlBox` 上,因为这会覆盖最小化、最大化和关闭按钮(但并非永久,如果您将鼠标悬停在它们上面,它们会重新出现)。

2 - 将 Form 附加到桌面

我添加了一个名为 `DesktopAttached` 的新属性。通过将此属性设置为 `true`,窗口将“粘”到桌面上,并且不会显示在任务栏上。它将位于所有常规窗口下方,类似于 Windows 侧边栏。

3 - 全屏模式

我添加了一个名为 `FullScreen` 的新属性。通过将此属性设置为 `true`,窗口将占据当前显示器的全部区域,并且始终位于任务栏和任何其他非 `TopMost` 窗口之上。请注意,如果您将此功能与 `DesktopAttached` 结合使用,窗口将占据整个屏幕,但仍位于所有窗口和任务栏下方。另请注意,当您将 `FullScreen` 再次设置为 `false` 时,窗口会自动记住其之前的状态。

4 - 可移动

我添加了一个名为 `Movable` 的新属性。通过将此属性设置为 `false`,窗口将不再可移动。用户将无法通过拖动标题栏来移动窗口。当 Form 不可移动时,这也意味着它也不可调整大小。

5 - 可调整大小

我添加了一个名为 `Sizable` 的新属性。通过将此属性设置为 `false`,窗口将不再可调整大小,尽管其 `FormBorderStyle` 被设置为 `Sizable`。如果您想要可调整大小的 Form 外观,但又希望它是固定的,这可能很有用。此属性不会干扰 `Movable` 属性。

6 - 获取按键状态

我为 Form 添加了两个与 `KeyState` 相关的新方法。它们是:`

  • KeyState GetKeyState(Keys key) - 返回类型是一个 `enum`,有两个可能的值:0 - `KeyState.Up` 和 1 - `KeyState.Down`。参数是 Windows Forms 提供的标准 `Keys enum`。
  • KeyValue GetKeyValue(Keys key) - 返回类型是一个 `enum`,有两个可能的值:0 - `KeyValue.Untoggled` 和 1 - `KeyValue.Toggled`。参数是 Windows Forms 提供的标准 `Keys enum`。

7 - 关闭按钮

我添加了一个名为 `CloseButton` 的新属性。通过将此属性设置为 `false`,窗口的关闭按钮将变为灰色,用户将无法再关闭 Form。Form 仍然可以通过以下方式关闭:父 Form 关闭、任务管理器关闭应用程序、Windows 关机或调用 `Application.Exit()`。

代码工作原理

Windows 桌面开发者常常忽略的一件事是演示层是如何工作的。您是否曾考虑过 Windows 如何处理窗口的调整大小、单击、移动和绘图?

Windows 通过消息系统工作。每次窗口需要重绘时,都会向 Form 发送一个 `WM_PAINT`(客户端区域)或 `WM_NCPAINT`(边框区域)消息。当鼠标移过 Form、Form 调整大小或移动时,也会发送消息给 Form(有时每秒数百条消息)。因此,发生在 Form 上的几乎所有事情(以 .NET 中的事件形式)都通过消息传递。Windows Forms 框架中的所有控件和 Form 都实现了 `void WndProc(ref Message m)` 方法。所有消息都通过此方法发送,并在其中进行处理。功能 **1**、**4** 和 **5** 是通过重写此方法并处理正确的消息直接实现的。功能 **3** 也部分依赖于重写此方法。

在 `Message` 结构中,我使用了三个属性:

  • `Msg` - 这是发送到 Form 的实际窗口消息。
  • `WParam` - 消息的参数之一(W 代表单词,但只是历史原因,实际上是长整型)。
  • `LParam` - 消息的另一个参数(L 代表长整型)。

常量

//Parameters to EnableMenuItem Win32 function
private const int SC_CLOSE = 0xF060; //The Close Box identifier
private const int MF_ENABLED = 0x0;  //Enabled Value
private const int MF_DISABLED = 0x2; //Disabled Value

//Windows Messages
private const int WM_NCPAINT = 0x85;//Paint non client area message
private const int WM_PAINT = 0xF;//Paint client area message
private const int WM_SIZE = 0x5;//Resize the form message
private const int WM_IME_NOTIFY = 0x282;//Notify IME Window message
private const int WM_SETFOCUS = 0x0007;//Form.Activate message
private const int WM_SYSCOMMAND = 0x112; //SysCommand message
private const int WM_SIZING = 0x214; //Resize Message
private const int WM_NCLBUTTONDOWN = 0xA1; //L Mouse Btn on Non-Client Area is Down
private const int WM_NCACTIVATE = 0x86; //Message sent to the window when it's
                                        //activated or deactivated
                                        
//WM_SIZING WParams that stands for Hit Tests in the direction the form is resizing
private const int HHT_ONHEADER = 0x0002; 
private const int HT_TOPLEFT = 0XD; 
private const int HT_TOP = 0XC; 
private const int HT_TOPRIGHT = 0XE;
private const int HT_RIGHT = 0XB;
private const int HT_BOTTOMRIGHT = 0X11;
private const int HT_BOTTOM = 0XF;
private const int HT_BOTTOMLEFT = 0X10;
private const int HT_LEFT = 0XA;

//WM_SYSCOMMAND WParams that stands for which operation is being done
private const int SC_DRAGMOVE = 0xF012; //SysCommand Dragmove parameter
private const int SC_MOVE = 0xF010; //SysCommand Move with keyboard command

如果您查看 `FormEx` 类重写的 `WndProc` 方法,您会注意到我拦截了其中一些消息。

// Prevents moving or resizing through the task bar
if ((m.Msg == WM_SYSCOMMAND && (m.WParam == new IntPtr(SC_DRAGMOVE) 
    || m.WParam == new IntPtr(SC_MOVE))))
{
    if (m_FullScreen || !m_Movable)
        return;
}

// Prevents Resizing from dragging the borders
if (m.Msg == WM_SIZING || (m.Msg == WM_NCLBUTTONDOWN && 

    (m.WParam == new IntPtr(HT_TOPLEFT) || m.WParam == new IntPtr(HT_TOP)
    || m.WParam == new IntPtr(HT_TOPRIGHT) || m.WParam == new IntPtr(HT_RIGHT) 
    
    || m.WParam == new IntPtr(HT_BOTTOMRIGHT)
    || m.WParam == new IntPtr(HT_BOTTOM) 
    
    || m.WParam == new IntPtr(HT_BOTTOMLEFT) 
    || m.WParam == new IntPtr(HT_LEFT))))
{
    if (m_FullScreen || !m_Sizable || !m_Movable)
        return;
}

如上所示,我拦截了 `WM_SYSCOMMAND` 消息以防止窗口移动。我不能只拦截此消息,因为它用于窗口的其他功能,我还会检查 `WParam` 参数,以验证 `WM_SYSCOMMAND` 消息是否是试图移动窗口的类型。如果是,我将 `return`,消息将被丢弃,因此窗口不会被移动。

您可能会想,为什么我不能简单地保存窗口的位置,并在用户尝试移动 Form 时每次都将其设置回来。这不是一个好的解决方案,因为移动消息仍然会被发送,尽管移动回消息发送得非常快,但您可以看到 Form 在移动,而且看起来不好,Form 会一直跟着光标移动,并在您按住鼠标按钮时出现严重的闪烁。

另一方面,我拦截了 `WM_SIZING` 和 `WM_NCLBUTTONDOWN` 来防止 Form 调整大小。`WM_SIZING` 在用户尝试从点击 Form 图标时的下拉菜单中调整窗口大小时发送,而 `WM_NCLBUTTONDOWN` 在用户左键单击 Form 的非客户端区域时发送。此消息与其命中测试参数(`WParam`)一起,允许应用程序确定用户是否单击了 Form 的边缘,而这些边缘是可调整大小的。如果消息落在此条件下,我将 `return`,消息将被丢弃,从而阻止 Form 调整大小。

如果以上条件都不满足,我将消息转发给 `base Form`(`base.WndProc(ref m);`)并正常处理。这确保了窗口的其余行为不变。

最后但同样重要的是,处理非客户端区域的绘图。这发生在调用 `base.WndProc(ref m)` 之后,因此窗口有机会以自己的主题样式绘制自己的边框。之后,我还拦截消息,以便允许用户在原始绘图上进行自定义绘图。我拦截的消息是 `WM_NCPAINT`、`WM_IME_NOTIFY`、`WM_SIZE` 和 `WM_NCACTIVATE`。所有这些都会导致非客户端区域被重绘。`WM_NCACTIVATE` 消息在 Form 失去焦点并更改其活动状态时发送。

base.WndProc(ref m);

// Handles painting of the Non Client Area
if (m.Msg == WM_NCPAINT || m.Msg == WM_IME_NOTIFY || m.Msg == WM_SIZE 
    || m.Msg == 0x86)
{
    // To avoid unnecessary graphics recreation and thus improving performance
    if (m_GraphicsFrameArea == null || m.Msg == WM_SIZE)
    {
        ReleaseDC(this.Handle, m_WndHdc); //Release old handle
        m_WndHdc = GetWindowDC(this.Handle); //Get Graphics of full window area
        m_GraphicsFrameArea = Graphics.FromHdc(m_WndHdc);
        
        Rectangle clientRecToScreen = new Rectangle(
          this.PointToScreen(new Point(this.ClientRectangle.X, 
          this.ClientRectangle.Y)), new System.Drawing.Size(
          this.ClientRectangle.Width, this.ClientRectangle.Height));
          
        Rectangle clientRectangle = new Rectangle(clientRecToScreen.X - 
        this.Location.X, clientRecToScreen.Y - this.Location.Y, 
        clientRecToScreen.Width, clientRecToScreen.Height);
        
        m_GraphicsFrameArea.ExcludeClip(clientRectangle); //Remove client area
    }
    
    RectangleF recF = m_GraphicsFrameArea.VisibleClipBounds;
    
    PaintEventArgs pea = new PaintEventArgs(m_GraphicsFrameArea, new 
      Rectangle((int)recF.X, (int)recF.Y, (int)recF.Width, (int)recF.Height));
      
    OnPaintFrameArea(pea);
    CloseBoxEnable(m_EnableCloseButton);
    this.Refresh(); //Forces repainting of the client area to remove shadows
} 

基本上,在我意识到窗口需要重绘之后,我所做的是:

  1. 通过 `GetWindowDC` Win32 API 调用获取窗口的 `DeviceContext`。从设备上下文,我创建了 `Graphics` 对象,该对象将包含整个窗口,而不仅仅是常规 `Paint` 事件的客户端区域。
  2. 排除客户端区域的区域(`ExcludeClip`),我们只想在那里绘图。
  3. 创建我们窗口区域的 `PaintEventArgs` 和 `Graphics` 对象,以便我们可以将其传递给新事件处理程序。
  4. 调用 `OnPaintFrameArea` 来处理我们新的 `PaintFrameArea` 事件,传递新创建的 `PaintEventArgs` 变量。
  5. 刷新客户端区域,因为当用户调整 Form 大小时,绘图会保留在原位,并在 Form 的侧面绘图时产生“阴影效果”。

Aero Glass

在本文的当前版本中,不支持在 Windows 7 / Vista 的 Aero Glass 上绘图。与常规窗口不同,Aero 不是由 Form 绘制的,它使用 DWM[^] 进行绘制。因此,拦截 `WM_NCPAINT` 的绘图在不禁用 Aero 的情况下将不起作用。我计划很快扩展本文以涵盖 DWM。

其他功能呢?

那么功能 **2**、**3**、**6**、**7** 呢?它们都与 Windows 消息系统有关,但我没有通过处理消息直接改变行为,而是进行了操作系统 API 调用。

我看到许多日常开发者都忽略了的一点是,直接与操作系统进行交互的能力。大多数 Windows Forms 框架不过是对原生操作系统资源的包装。我在这里所做的基本上是相同的事情。我构建了一个包装器来调用那些在标准 `Form` 中未实现的 API。

下面是使功能 2、3、6 和 7 成为可能的所有 Win32 API 调用。代码中的注释具有自解释性。要使用它,您需要引用 `System.Runtime.InteropServices` 命名空间。只需在 CS 文件的标题中将其作为一个 `using` 语句。

//GetSystemMenu Win32 API Declaration (The Window Title bar is SystemMenu)
[DllImport("user32.dll")]
private static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert);

//EnableMenuItem Win32 API Declaration (Set the enabled values of the 
//title bar items)
[DllImport("user32.dll")]
private static extern int EnableMenuItem(IntPtr hMenu, int wIDEnable, int wValue);

//Get Desktop Window Handle
[DllImport("user32.dll")]
private static extern IntPtr GetDesktopWindow();

//Set Parent Window, used to set the desktop's parent as the window parent
[DllImport("user32.dll")]
private static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent);

//Find any Window in the OS. We will look for the parent of where the desktop is
[DllImport("User32.dll")]
public static extern IntPtr FindWindow(String lpClassName, String lpWindowName);

//Get the device component of the window to allow drawing on the title bar and 
//frame
[DllImport("User32.dll")]
public static extern IntPtr GetWindowDC(IntPtr hWnd);

//Releases the Device Component after it's been used
[DllImport("User32.dll")]
public static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);

正如您所看到的,所有 API 都可以实现我们认为不可能的许多事情。而且,通过这些调用确实可以做很多事情。如果您查看下面链接的 API 参考,您将能够找到本文中使用的所有方法。例如,在参考中,有 `ReleaseDC` 方法。

int ReleaseDC(
  __in  HWND hWnd,
  __in  HDC hDC
);

这在 C# 中将不起作用,因为它没有 `HWND` 或 `HDC` 类型。`__in` 告诉我们参数将由方法读取,不会被输出。类型是指向句柄的指针,因此我们可以简单地将它们替换为 .NET Framework 提供的 `IntPtr`。返回值不言而喻,是一个整数。

关注点

为了实现所有这些功能,我不得不多次调用 *user32.dll* 的 Win32 API。最烦人的是获取发送到 Form 的所有正确的窗口消息和常量值,因为 .NET Framework 没有这些消息的任何映射。

作为参考,我使用了 MSDN 的 Windows API 参考、Visual C++ 的 *winuser.h* 头文件以及 Visual Studio 的输出窗口(`System.Diagnostics.Debug.WriteLine`)来监视传入的消息,因为我在 Form 上进行了一些操作以使其重绘。例如,消息 `0x86`(`WM_NCACTIVATE`),在本文的早期版本中未标记,因为我找不到合适的文档(感谢 **Spectre2x**)。

希望您喜欢这段代码。请随时留下您的反馈。

历史

  • 2010 年 7 月 15 日 - 文章发布
  • 2010 年 7 月 20 日 - 更新以指出 0x86 消息标签。代码和文章的完整更新待定。
  • 2010 年 7 月 21 日 - 更新以标记项目和文章中的 0x86 消息。同时添加了关于 Aero 支持的注释。
© . All rights reserved.