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






4.94/5 (54投票s)
创建特殊的“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
的边框上绘制)。
当您的窗体的 RightToLeft
和 RightToLeftLayout
属性都为 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'
// ...
}
另外请注意,当您使用此 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
无关,但由于在此处遇到,我将对此进行解释。
您可以看到,当您将 RightToLeftLayout
和 RightToLeft
属性都设置为 true
时,顶层的整个坐标系统会被镜像。原点不再是窗体客户区的左上角,而是右上角。我说的是顶层,因为它只发生在窗体本身,而不是子控件。例如,当您在窗体中放置一个 Panel
时,在 Panel
内部,原点仍然是 Panel 的左上角。
好了,您可能猜到,当这种情况发生时,您为窗体构造的任何 Graphics
对象都必须被镜像。是的,这是真的,但并非完全如此!试试这个。
- 在 Visual Studio 中创建一个新的**Windows Forms Application**。
- 选择窗体,并将
RightToLeft
和RightToLeftLayout
属性设置为true
。 - 通过在**Properties**面板的**Events**选项卡中双击
Paint
事件来处理Paint
事件。 - 绘制一条简单的直线。
private void Form1_Paint( object sender, PaintEventArgs e ) { e.Graphics.DrawLine( Pens.Blue, 0,0, 100, 100 ); }
运行项目后,您会看到直线从窗体的右上角开始绘制,正如预期的那样。
现在将另一个窗口移到窗体上方。您看到了什么?直线将从左上角开始绘制,就好像 Graphics
不是从右到左一样。
现在,当窗体位于桌面上的另一个窗口后面时,单击任务栏中的其图标将其带到前面(或最小化窗体,然后恢复它)。直线又从右上角开始绘制了!
我进行了大量调查,并尝试了您能想到的一切来找出原因,但我没有发现任何东西!这可能是 .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
。
- WindowGraphics.cs 的第 79 行,将
- 2008年9月21日 - 在
CreateGraphics
中,用GetDC
替换了GetWindowDC
,以兼容 Windows Vista