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

可以在窗口的任何地方绘制的特殊“图形”对象

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (54投票s)

2008年9月17日

CPOL

9分钟阅读

viewsIcon

111527

downloadIcon

2718

创建特殊的“Graphics”对象,以便在窗口的任何位置进行绘制,包括非客户区

前言

大约四个月前,我在一个项目上工作时,想在窗口的标题栏上添加一些按钮,就在标准的最小化/最大化/关闭按钮附近。为了做到这一点,我必须在标题栏上绘制,也就是窗口的非客户区。虽然我在那个项目上的工作没有结果,只是浪费了时间,但至少我现在可以写这篇文章来介绍我的成果。:-)

在这篇文章中,我将向您展示如何在 C# 中创建特殊的 Graphics 对象,您可以使用这些对象在窗口的任何位置进行绘制。在我的下一篇文章中,我将解释如何向标题栏添加按钮。

背景

好了,正如您已经知道的,在 Microsoft .NET 中,当您想绘制某些内容时,就需要一个 Graphics 对象。Graphics 对象必须属于您想要在其上绘制的对象。通常,您有两种选择来获取一个控件(Form 或任何派生自 System.Windows.Forms.Control 类)的 Graphics。第一种方法是处理控件的 Paint 消息,第二种方法是调用对象的 CreateGraphics 方法。在前一种情况下,您的绘图将始终保留在对象上!这是因为当您的窗口的任何部分需要绘制时(即,它被无效化 - 在 C++ win32 编程的术语中),.NET 子系统(包装在 win32 WM_PAINT 消息中)总是会引发 Paint 事件。在后一种情况下,您可以随时在窗口上绘制,而不是在操作系统想要绘制时绘制。但是缺点是您的绘图会一直存在,直到有东西覆盖了您的窗口!

我认为我离题太远了。有关 .NET 和 Windows 上的绘图机制的更多信息,请参阅 MSDN 库(Pain 事件、CreateGraphics 方法,以及 Windows SDK 文档中 **Windows GDI** 的 **Painting and Drawing** 子部分)。

无论您使用哪种方法获取 graphics 对象,您只能在窗口的工作区(称为客户区)进行绘制,即除了窗口边框和标题栏之外的任何地方。

在这篇文章中,我将设计一个名为 WindowGraphics 的类,该类将为您的整个窗口创建一个 Graphics 对象,该对象包含窗口的客户区和非客户区;通过使用它,您可以在窗口的任何位置进行绘制。您还可以使用它来绘制您以前无法控制的某些控件的区域。例如,可以在 TextBox 的任何位置进行绘制(使用普通的 Graphics 对象,您无法在 TextBox 的边框上绘制)。
当您的窗体的 RightToLeftRightToLeftLayout 属性都为 true 时,该类还可以处理普通 Graphics 对象存在的问题。此问题的详细信息将在后面解释。

使用该类

下载文章附带的源代码后,您可以通过将 *WindowGraphics.cs* 文件包含到您的项目中来轻松开始使用 WindowGraphics 类。这非常简单。您首先创建一个 WindowGraphics 类的实例,将您要绘制的控件传递给构造函数。

// ...

// create a Graphics for entire form

WindowGraphics wg = new WindowGraphics( this ); // I assume that this line 
					  // is put somewhere in your form class,
					  // so the 'this' keyword refers 
                      // to an instance of the Form class.

然后,只需使用新创建对象的 Graphics 属性来执行您的绘图即可。

//.....
wg.Graphics.DrawLine( Pens.Blue, 0, 0, 100, 100 );

//.....

// or if you have to call many drawing functions, here is the way to reduce
// your typing. This is what I always do...
Graphics g = wg.Graphics;
g.DrawString( "I am on the title bar!", 
	new Font( "Tahoma", 10, FontStyle.Bold ), Brushes.Black, 0, 4 );
g.FillEllipse( Brushes.Black, this.Width - 40, this.Height - 40, 80, 80 );

// .... other drawing commands...

最后,调用对象的 Dispose 方法来释放它使用的任何资源。由于此类使用非托管资源,因此强烈建议您不要忘记调用 Dispose 方法!

....
wg.Dispose();
....

您也可以使用 C# 的 using 块,它会自动为您调用 Dispose 方法。这是推荐的方式。

using ( WindowGraphics wg = new WindowGraphics( this ) )
{
	Graphics g = wg.Graphics;

	// ...

	// do your drawing with 'g'
	// ...
}

Draw anywhere on your window

另外请注意,当您使用此 Graphics 时,原点是整个窗体矩形的左上角,而不是其客户区的左上角。

源代码中包含的示例项目是使用 Visual Studio 2008 创建的,但项目文件应该也可以在 Visual Studio 2005 中打开而没有任何问题,因为它被配置为使用 .NET Framework 2.0。

如果您对细节不感兴趣,可以到此为止,并在您的项目中使用该类。如果您感兴趣,请继续阅读。

它是如何工作的?

在 .NET 中创建这种 Graphics 对象并非易事。您必须使用本地方法和 win32 调用。

正如您中的一些人所知,最简单的方法是调用 GetWindowDC 函数,并将您的窗口句柄传递给它。GetWindowDC 函数属于 Windows 的 user32 库。您必须先导入此函数。

using System;
using System.Runtime.InteropServices;


//...

[DllImport( "user32" )]
private static extern IntPtr GetWindowDC( IntPtr hwnd );

// you also need ReleaseDC
[DllImport( "user32" )]
private static extern IntPtr ReleaseDC( IntPtr hwnd, IntPtr hdc );

然后,调用该方法创建一个用于整个窗口的**DC***,然后从**DC**创建一个 Graphics 对象。

* DC:设备上下文 - 在 GDI 世界中用于绘制事物的对象。某种程度上等同于 GDI+ 中的 Graphics

IntPtr hdc = GetWindowDC( this.Handle );
Graphics g = Graphics.FromHdc( hdc );

// ....
// do your drawing
// ....

// free the resources
g.Dispose();
ReleaseDC( this.Handle, hdc );

我最初在我的项目中使用了这种方法。现在的问题是……

RightToLeftLayout 问题

如果您永远不会使用从右到左的窗体,您可以跳过本节,但如果您为从右到左的语言(如我的情况)制作软件,或者对该主题感兴趣,请阅读本节。

尽管此问题与此种 Graphics 无关,但由于在此处遇到,我将对此进行解释。

您可以看到,当您将 RightToLeftLayoutRightToLeft 属性都设置为 true 时,顶层的整个坐标系统会被镜像。原点不再是窗体客户区的左上角,而是右上角。我说的是顶层,因为它只发生在窗体本身,而不是子控件。例如,当您在窗体中放置一个 Panel 时,在 Panel 内部,原点仍然是 Panel 的左上角。

好了,您可能猜到,当这种情况发生时,您为窗体构造的任何 Graphics 对象都必须被镜像。是的,这是真的,但并非完全如此!试试这个。

  • 在 Visual Studio 中创建一个新的**Windows Forms Application**。
  • 选择窗体,并将 RightToLeftRightToLeftLayout 属性设置为 true
  • 通过在**Properties**面板的**Events**选项卡中双击 Paint 事件来处理 Paint 事件。
  • 绘制一条简单的直线。
    private void Form1_Paint( object sender, PaintEventArgs e )
    {
        e.Graphics.DrawLine( Pens.Blue, 0,0, 100, 100 );
    }

运行项目后,您会看到直线从窗体的右上角开始绘制,正如预期的那样。

RightToLeft Form - Really right to left!

现在将另一个窗口移到窗体上方。您看到了什么?直线将从左上角开始绘制,就好像 Graphics 不是从右到左一样。

RightToLeft Form - Sometimes left to right!

现在,当窗体位于桌面上的另一个窗口后面时,单击任务栏中的其图标将其带到前面(或最小化窗体,然后恢复它)。直线又从右上角开始绘制了!

我进行了大量调查,并尝试了您能想到的一切来找出原因,但我没有发现任何东西!这可能是 .NET Framework 或 Windows 中的一个 bug。在撰写本文时,我使用的是最新版本的 .NET Framework 和 Windows XP(XP 的 Service Pack 3,以及包含 .NET 2 Service Pack 2 的 .NET 3.5 SP1),这个问题仍然存在。它甚至发生在 Windows Vista 上。所以,我决定做些别的事情。这是我的解决方案。

再次,它是如何工作的?

我试图从一个永远不会从右到左的东西创建 Graphics —— 整个桌面。

首先,我获取整个屏幕的 **DC**,然后进行所需的变换和裁剪,以使 **DC** 适应窗口的可见区域。下面是分步说明:

IntPtr hdc = GetDC( IntPtr.Zero ); 	    // get DC for the entire screen

通过将零传递给 GetDC 函数作为窗口句柄,我们可以获取整个屏幕的 **DC**。

IntPtr hrgn = GetVisibleRgn( hWnd );	// obtain visible clipping region for the window
SelectClipRgn( hdc, hrgn ); 	        // clip the DC with the region

我们必须检索窗体的剪裁区域,并用该区域裁剪 **DC**。不这样做,我们可能会绘制到其他窗口上——那些不属于我们的地方。
GetVisibleRgn 方法是我们的类的一个 private 方法。它返回一个句柄,指向窗口当前被剪裁到的区域。我稍后会解释该方法。SelectClipRgn 是一个 Win32 API 函数。它将给定的 DC 裁剪到给定的区域。

现在 DC 的原点是屏幕的左上角,但我们希望它是我们窗体的左上角。所以我们必须移动原点。

Rect rect = new Rect();
GetWindowRect( hWnd, rect );

// move the origin from upper left corner of the screen,
// to the upper left corner of the form

SetWindowOrgEx( hdc, -rect.left, -rect.top, IntPtr.Zero );

最后,从 **DC** 创建您的 Graphics

Graphics graphics = Graphics.FromHdc( hdc );

您完成了。现在是 GetVisibleRgn 方法。

private IntPtr GetVisibleRgn( IntPtr hWnd )
{
    IntPtr hrgn, hdc;
    hrgn = CreateRectRgn( 0, 0, 0, 0 );
    hdc = GetWindowDC( hWnd );
    int res = GetRandomRgn( hdc, hrgn, 4 ); 	// the value of SYSRGN is 4. 
					                            // Refer to Windows SDK Documentation.

    ReleaseDC( hWnd, hdc );
    return hrgn;
}

我们创建一个空区域,获取窗口的 **DC**,将 **DC** 传递给一个特殊函数来检索与窗口相关的可见区域,然后释放 **DC**。这个特殊函数是 GetRandomRgn。我不知道这个函数名称背后的哲学是什么,但我想它最初是为了做比检索窗口剪裁区域多得多的事情。顺便说一下,它对我们有帮助。

最后一件事情是将所有内容封装在一个可重用的类中。您可以在文章附带的代码中看到最终的类。

我为什么写这篇文章,以及您可以从中学习到什么?

对我来说,第一个原因是我想写点东西!我发现这个类足够简单并且设计良好。我希望您除了能学到一些关于 Windows GDI 的知识外,还能学到一些关于面向对象设计的技巧。

这是我在 CodeProject 上的第一篇文章。我一直想写关于我的项目(将近几年了!)。我认为我有很多很好的经验可以与他人分享。这是我的第一步,我希望它能顺利进行。我不知道何时能写下一篇文章,但我想它将是关于标题栏上的按钮。

如果您喜欢这篇文章,请投票支持它,并留下一些评论。这样我就可以了解优点并纠正我的错误。

历史

  • 2008年9月18日 - 首次发布
  • 2008年9月19日 - 修复了一个非常小的 bug!源代码已更新
    • WindowGraphics.cs 的第 79 行,将 hWnd 改为 IntPtr.Zero
      这是因为在检索 **DC** 时,我们将 IntPtr.Zero 传递给 GetWindowDC 以获取整个屏幕的 **DC**,而在释放 **DC** 时也应传递 IntPtr.Zero
  • 2008年9月21日 - 在 CreateGraphics 中,用 GetDC 替换了 GetWindowDC,以兼容 Windows Vista
© . All rights reserved.