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

为用户控件添加可设计边框

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.62/5 (20投票s)

2003年12月9日

8分钟阅读

viewsIcon

148432

downloadIcon

1993

关于为用户控件添加设计时启用的边框的教程。涵盖属性、互操作和自定义覆盖等主题

引言

本教程将引导您完成创建具有可在设计时更改边框的控件的过程。本教程假定您对使用 C# 编写控件相当熟悉,并且了解 Visual Studio .NET。本教程略有不同,因为边框是通过更改窗口的样式属性来处理的,这意味着您不必自己绘制它。

设置项目

创建一个名为 "BorderSample" 的新 C# Windows 应用程序项目。

现在再添加一个项目 – 这个项目将包含您要创建的新控件。在“文件”菜单中,选择 “添加项目 -> 新建项目”。选择 C# Windows 控件库作为项目类型,并将其命名为 "BorderControls"

此时,您正处于设计模式,看到一个空白的用户控件。将 UserControl1.cs 重命名为 SimpleControl.cs。在代码中,将类名和构造函数都更改为匹配。

如果您在设计模式下查看控件,您会注意到它没有边框。默认的用户控件没有边框,但添加起来相对简单。

创建控件时,Forms 库会使用控件的 CreateParams 属性来获取创建控件所需窗口的所有信息。CreateParams 是虚拟的,因此可以被重写。查看 System.Windows.Forms.CreateParams 类型(这是该属性返回的类)。它包含许多内容,但我们关心的是 StyleExStyle

如果您熟悉 Win32 编程,您会认出这些。StyleExStyle 包含控制窗口许多行为和视觉特征的标志。由于 UserControl 默认没有边框,我们可以假设没有与边框相关的样式被设置。在“Component Designer Generated Code”区域的正下方,键入此内容

override CreateParams

如果您在键入这两个词后立即按 Enter,Visual Studio 应该会为您填充以下内容

protected override CreateParams CreateParams
{
    get{ return base.CreateParams; } 
}

正如您所见,它只是从基类获取值并原封不动地传递。这为我们提供了更改值 G 的机会,但我们使用什么值呢?如果您查看有关 CreateWindowEx 的 Windows 帮助主题,您会找到一个名为 WS_EX_CLIENTEDGE 的扩展样式值,它为窗口提供了凹陷的 3D 边框。这就是我们想要的。在您的计算机上搜索名为 WinUser.h 的文件,该文件包含此常量(以及许多其他常量)。在头文件中查找该值,或者就相信我吧

// Constant taken from WinUser.h
private const int WS_EX_CLIENTEDGE = 0x00000200;

现在我们知道了值是什么,我们需要应用它

// Provide window style constants to enable control border
protected override CreateParams CreateParams
{
    get
    {
        // Take the default params from the base class
        CreateParams p = base.CreateParams;
        
        // Add the extended "3d sunken border" style
        p.ExStyle = p.ExStyle | WS_EX_CLIENTEDGE;
        return p;
    }
}

就是这样。回到“BorderSample”项目,并在设计器中打开“Form1.cs”。如果您查看工具箱中的“我的用户控件”选项卡,您应该会在其中看到您的“SimpleControl”。将其拖到窗体上。现在您有了一个带边框的控件。

创建可设计边框属性

您创建了一个带边框的用户控件,这很酷,但是如果您想让用户选择为控件提供哪种边框呢?这有点复杂,需要使用互操作,但并不困难。

您首先需要一个变量来保存您想要的边框类型。Forms 库中的控件使用 System.Drawing.Drawing2D.BorderStyle 类型,所以我们也使用它。将以下代码添加到 SimpleControl

private System.Windows.Forms.BorderStyle borderStyle = BorderStyle.Fixed3D;

此变量可以取三个值之一:NoneFixedSingleFixed3D。我们将默认值设置为 Fixed3D

现在您需要一种方法将这些值转换为适合 CreateParams 使用的窗口样式。None 相当简单——它根本没有边框。FixedSingle 是控件周围的单条黑线,它映射到基本窗口样式 WS_BORDERFixed3D 是凹陷的 3D 边框,映射到扩展窗口样式 WS_EX_CLIENTEDGE

通过查看“WinUser.h”,我们找到以下值

// These constants were taken from WinUser.h
private const int WS_BORDER  = 0x00800000;
private const int WS_EX_CLIENTEDGE = 0x00000200;

现在我们需要一个函数来将三个可能的 BorderStyle 值映射到用于窗口的样式和扩展样式值。它所做的就是获取现有的窗口样式,屏蔽掉 WS_BORDERWS_EX_CLIENTEDGE 样式,然后根据我们内部的 borderStyle 值应用适当的样式

// Convert borderStyle to Style and ExStyle values for Win32
private void BorderStyleToWindowStyle(ref int style, ref int exStyle)
{
    style &= ~WS_BORDER;
    exStyle &= ~WS_EX_CLIENTEDGE;
    switch(borderStyle)
    {
        case BorderStyle.Fixed3D:
            exStyle |= WS_EX_CLIENTEDGE;
            break;

        case BorderStyle.FixedSingle:
            style |= WS_BORDER;
            break;

        case BorderStyle.None:
            // No border style values
            break;
    }
}

现在可以修改 CreateParams 属性来使用这个新函数。将 CreateParams 属性更改为如下

// Provide window style constants to enable control border
protected override CreateParams CreateParams
{
    get
    {
        // Get the default values from the base class
        CreateParams p = base.CreateParams;

        // Store the Style and ExStyle values
        int style = p.Style;
        int exStyle = p.ExStyle;

        // Modify the values to match the desired border style
        BorderStyleToWindowStyle(ref style, ref exStyle);

        // Store the results back in the CreateParams class
        p.Style = style;
        p.ExStyle = exStyle;
        return p;
    }
}

这将使用 borderStyle 值来修改用于创建窗口的 CreateParams 的 Style 和 ExStyle 属性。当创建我们的控件时,边框将反映 borderStyle 变量选择的样式。

现在我们需要一个公共属性来允许用户(设计者)更改边框样式

/// <summary>
/// Gets or sets the border style of the tree view control.
/// </summary>
[Category("Appearance")]
[DescriptionAttribute("Border style of the control")]
[DefaultValue(typeof(System.Windows.Forms.BorderStyle), "Fixed3D")]
public BorderStyle BorderStyle
{
    get {return borderStyle;}
    set {borderStyle = value;}
}

这段代码的问题是它不能完全正常工作。窗口已经创建,其样式值已经设置。更改它们的唯一方法是通过 Win32 调用,特别是 SetWindowLong。您还必须告诉 Windows 您已更改样式值,这意味着还需要调用 SetWindowPos。幸运的是,这一点也不难,因为 C#(以及 .NET 总体而言)对调用 DLL 中的本机代码提供了良好的支持。像这样调用 Win32 API 函数是通过一种称为 **平台调用** 或简称 **P/Invoke** 的机制完成的。

使用 P/Invoke 设置窗口样式

如果您要使用本机 C++ 编写更改窗口边框样式的代码,它看起来会像这样

// Get style and exstyle values
int style = GetWindowLong(hWnd, GWL_STYLE);
int exStyle = GetWindowLong(hWnd, GWL_EXSTYLE);

// Modify existing style values
style = style & ~WS_BORDER;
exStyle = exStyle | WS_EX_CLIENTEDGE;

// Set new style values
SetWindowLong(hWnd, GWL_STYLE, style);
SetWindowLong(hWnd, GWL_EXSTYLE, exStyle);

// Tell windows that we changed the window frame style
SetWindowPos(hWnd, NULL, 0, 0, 0, 0,
    SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER |
    SWP_NOOWNERZORDER | SWP_FRAMECHANGED);

为了在 C# 中实现这一点,我们将需要以下内容

  • GetWindowLongSetWindowLong 函数
  • GWL_STYLEGWL_EXSTYLE 常量
  • SetWindowPos 函数
  • 所有 SWP_* 常量

所有这些内容都可以在 WinUser.h 中找到。首先我们处理常量。它们相当容易,因为我们所要做的就是将找到的 #define 值更改为整数常量,如下所示

// Constants from WinUser.h
const int GWL_STYLE = -16;
const int GWL_EXSTYLE = -20;

const uint SWP_NOSIZE   = 0x0001;
const uint SWP_NOMOVE   = 0x0002;
const uint SWP_NOZORDER   = 0x0004;
const uint SWP_NOREDRAW   = 0x0008;
const uint SWP_NOACTIVATE  = 0x0010;
const uint SWP_FRAMECHANGED  = 0x0020;
const uint SWP_SHOWWINDOW  = 0x0040;
const uint SWP_HIDEWINDOW  = 0x0080;
const uint SWP_NOCOPYBITS  = 0x0100;
const uint SWP_NOOWNERZORDER = 0x0200;
const uint SWP_NOSENDCHANGING = 0x0400;

现在是激动人心的部分:调用函数。调用外部 DLL 中的函数非常容易。您告诉 C# 函数是什么样的(原型),参数和返回值是什么,以及在哪里找到它。C# 会处理其余的事情。GetWindowLong 函数在 User32.dll 中,看起来是这样的

int GetWindowLong(HWND hWnd, DWORD Index);

首先,您需要在文件的顶部添加这一行

using System.Runtime.InteropServices; // Needed for DLLImport

Interop 是 Inter-operation 的缩写,这是 Platform Invoke 的另一个名称。添加到我们的类中,GetWindowLong 的 P/Invoke 规范如下所示

[DllImport("User32", CharSet=CharSet.Auto)]
private static extern int GetWindowLong(IntPtr hWnd, int Index);

DllImport 属性告诉 C# 在哪里找到函数(User32.dll)。

CharSet=CharSet.Auto 属性很重要,因为 SetWindowLongGetWindowLong 是 **Unicode 感知** 函数。也就是说,它们既有 Ansi 版本,也有 Unicode 版本,根据您的代码是否启用了 Unicode,后缀为 A(Ansi)或 W(Wide)。添加 CharSet=CharSet.Auto 属性告诉 C# 它可以决定使用哪个版本,通常是 Unicode 版本。有关更完整的描述,请查看 .NET Framework 帮助文件中的“DllImport Attribute”主题。

实际的函数原型看起来像一个静态成员函数,除了它上面有 extern 关键字,它告诉 C# 该函数是在外部库中实现的。IntPtr 是 CLR 用于表示窗口句柄的类型。有关 CLR 类型和 Win32 类型之间的其他映射,请参阅 DllImport 文档。

另外两个函数与第一个函数以相同的方式处理

[DllImport("User32", CharSet=CharSet.Auto)]
private static extern int SetWindowLong(IntPtr hWnd, int Index, int Value);

[DllImport("User32", ExactSpelling=true)]
private static extern int SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, 
    int x, int y, int cx, int cy, uint uFlags);

这里有一个区别——SetWindowPos 从不处理字符串,所以它只有一个版本,不像其他两个那样有 Unicode 和 Ansi 版本。这意味着函数名没有后缀 A 或 W,因此我们省略了 CharSet 属性,并用 ExactSpelling=true 替换它。

将常量和函数原型添加到控件类后,它们就可以使用了。

BorderStyle 属性更改为如下
/// <summary>
/// Gets or sets the border style of the tree view control.
/// </summary>
[Category("Appearance")]
[Description ("Border style of the control")]
[DefaultValue(typeof(System.Windows.Forms.BorderStyle), "Fixed3D")]
public BorderStyle BorderStyle
{
    get {return borderStyle;}
    set
    {
        borderStyle = value;
        // Get Styles using Win32 calls
        int style = GetWindowLong(Handle, GWL_STYLE);
        int exStyle = GetWindowLong(Handle, GWL_EXSTYLE);

        // Modify Styles to match the selected border style
        BorderStyleToWindowStyle(ref style, ref exStyle);

        // Set Styles using Win32 calls
        SetWindowLong(Handle, GWL_STYLE, style);
        SetWindowLong(Handle, GWL_EXSTYLE, exStyle);

        // Tell Windows that the frame changed
        SetWindowPos(Handle, IntPtr.Zero, 0, 0, 0, 0,
            SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE |
            SWP_NOZORDER | SWP_NOOWNERZORDER |
            SWP_FRAMECHANGED);
    }
}

由于我们已经使用 interop 定义了所需的函数,因此它们作为我们类的一部分出现,并且可以像任何其他函数一样调用。BorderStyle 属性已完成,编译您的代码,然后再次返回到 BorderSample 项目。如果您查看控件的属性,您应该会看到一个名为 BorderStyle 的属性。更改它,控件边框会立即更新以反映更改。

有必要对应用于 BorderStyle 属性的属性进行简要解释。Category 属性指定该属性在设计器中的哪个标题下显示。如果您在 Forms 设计器中查看控件的属性,您会看到 BorderStyle 出现在 Appearance 下。您还会注意到,当您选择 BorderStyle 属性时,帮助框中显示的文本与 Description 属性中的文本匹配。最后,指定 DefaultValue 属性会告诉 Forms 设计器,当它编写创建我们控件的代码时,如果用户为边框样式选择了 Fixed3D 值,则无需编写代码,因为这是我们的默认值。没有这一行,即使不是默认值,也会编写代码来设置该属性,这会使代码膨胀,如果您在控件中有大量属性。

在属性值上使用属性是提供有关您希望属性如何呈现给用户以及您的控件如何在代码中持久化的简单方法。

© . All rights reserved.