WinForm 扩展






4.94/5 (44投票s)
一个扩展了 Microsoft 提供标准功能的 WinForm。

引言
在本文中,我将为大家介绍一个扩展版的 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
}
基本上,在我意识到窗口需要重绘之后,我所做的是:
- 通过 `GetWindowDC` Win32 API 调用获取窗口的 `DeviceContext`。从设备上下文,我创建了 `Graphics` 对象,该对象将包含整个窗口,而不仅仅是常规 `Paint` 事件的客户端区域。
- 排除客户端区域的区域(`ExcludeClip`),我们只想在那里绘图。
- 创建我们窗口区域的 `PaintEventArgs` 和 `Graphics` 对象,以便我们可以将其传递给新事件处理程序。
- 调用 `OnPaintFrameArea` 来处理我们新的 `PaintFrameArea` 事件,传递新创建的 `PaintEventArgs` 变量。
- 刷新客户端区域,因为当用户调整 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 支持的注释。