C#发现之旅 --- WinForm.NET中开发具有固定背景图片的可滚动控件
摘要
在本文章中笔者使用WinForm.NET2.0开发出一个具有固定背景图片的带滚动条的容器控件。点击下载本文章配套的演示程序 /Files/xdesigner/FixedBackground.zip。
在WinForm.NET开发中,可以使用一个Panel或UserControl作为一个带滚动条的容器放置图形或其他控件。我们可以设置控件的BackgroundImage属性来设置控件的背景图片,但这个背景图片是会随着控件内容的滚动而滚动的,而且还出现背景破碎的不良效果。现笔者在开发实践中遇到控件的背景图片不随着控件的滚动而滚动。
在B/S开发中,开发者可以使用“background-attachment:fixed”的CSS样式来固定HTML文档的背景图片,使之不随着内容的滚动而滚动。但在WinForm.NET开发中却没这个功能。
于是袁某人又开始路漫漫其修远兮,到处上下而求索,居然得出了一个解决方法,在此使用发现问题,分析问题和解决问题的步骤来一一道来,希望能为遇到相同问题的人一点启发。
发现问题
首先说说WinForm.NET滚动时背景也随之滚动的原理。如下图所示,笔者在一个窗体上放置一个Panel控件,设置了一个尺寸较大的背景图片,然后设置控件的AuotScroll值为true,设置控件的AutoScrollMinSize属性值为背景图片的大小,则这个控件就会如下图所示的显示滚动条。[作者袁永福 http://www.xdesigner.cn/ ]
笔者向下拖拽控件的垂直滚动条,使得控件的内容发生滚动。在默认情况下,Windows操作系统会自动实现控件客户区视图的滚动,例如滚动操作导致了100个像素的滚动距离,Windows会自动的将控件客户区显示的内容向上平移100个像素,于是控件下方新腾出来100个像素高度的客户区,这个客户区就是控件的无效矩形,Windows操作系统会向控件发送WM_PAINT消息,导致触发控件的Paint事件,从而调用程序代码来重新绘制这100个像素高度的区域。[作者袁永福 http://www.xdesigner.cn/ ]
Panel控件内部处理Paint事件来绘制背景图片,此时即使控件的内容发生滚动,但绘制图形使用的XY坐标系仍然是以控件的左上角为原点的。而且由于无效矩形只有控件客户区最下面的100个像素的高度,而无效矩形之上的部分是不会重新绘制的,于是控件重新绘制了一部分的背景图片,于是导致如下的用户界面。这个用户界面是破碎的,是不能见人的。
笔者让其他窗体完全覆盖掉这个控件后关闭覆盖窗体,则控件的所有的客户区都是无效区域,Windows操作系统会向控件发送WM_PAINT消息来触发控件的Paint事件,而控件内容会自动处理Paint事件并重新完整的绘制背景,从而形成如下效果。
分析问题
根据上述观察到的现象可以知道WinForm.NET控件天生具有固定背景的功能,其背景图片是不随控件的滚动而滚动。但Windows的默认滚动图形的操作却破坏了这个功能,从而造成了控件滚动时背景破碎的现象。
根据上述的原理,笔者可以得出,只要阻止Windows默认的滚动图形的操作即可保护WinForm.NET控件的天生的固定背景的功能,从而实现固定背景的带滚动的控件。
WinForm.NET并没有提供任何禁止Windows默认滚动图形的功能,于是笔者使用到了一个强大的Win32API函数,那就是LockWindowUpdate。[作者袁永福 http://www.xdesigner.cn/ ]
这个API函数在C#中的声明形式如下
external static bool LockWindowUpdate( IntPtr hWndLock );
这个函数能允许或禁止指定窗体的绘制操作,在任何时刻,整个操作系统中只能有一个窗体的绘图操作被禁止掉。
这个函数的参数是窗体句柄,若参数为0表示用户界面被锁定的窗体重新释放而能绘制用户界面。
只要在控件发生滚动时程序调用LockWindowUpdate函数,则控件的内容被锁定了,不能反映任何图形操作,Windows默认的滚动图形的操作就没有效果。当控件的滚动操作完成调用LockWindowUpdate函数来重新释放窗体并强制重新绘制控件的所有内容,则就能实现固定背景的效果。
根据上述分析,笔者只要处理控件的滚动事件,当控件内容发生滚动时调用LockWindowUpdate函数锁定控件用户界面,而滚动完毕后又调用LockWindowUpdate函数解除锁定并重新绘制控件所有的内容则就可以让控件发送滚动时不背景图片不随之滚动。
在WinForm.NET2.0中,支持滚动的控件都是从System.Windows.Forms.ScrollableControl派生的,这些控件都提供一个Scroll事件。这个事件的参数是一个System.Windows.Forms.ScrollEventArgs类型的对象,该参数有一个Type属性值,是System.Windows.Forms.ScrollEventType类型,用于表示滚动事件的类型。WinForm.NET2.0中支持的滚动事件类型有以下几种。[作者袁永福 http://www.xdesigner.cn/ ]
滚动事件类型 |
说明 |
SmallDecrement |
滚动框移动了较短的距离。用户单击了左(水平)或上(垂直)滚动箭头,或者按了向上键。 |
SmallIncrement |
滚动框移动了较短的距离。用户单击了右(水平)或下(垂直)滚动箭头,或者按了向下键。 |
LargeDecrement |
滚动框移动了较长的距离。用户在滚动条上单击了滚动框左侧(水平)或上方(垂直),或者按了 Page Up 键。 |
LargeIncrement |
滚动框移动了较长的距离。用户在滚动条上单击了滚动框右侧(水平)或下方(垂直),或者按了 Page Down 键。 |
ThumbPosition |
滚动框被移动。 |
ThumbTrack |
滚动框当前正在移动。 |
First |
滚动框被移动到 System.Windows.Forms.ScrollBar.Minimum 位置。 |
Last |
滚动框被移动到 System.Windows.Forms.ScrollBar.Maximum 位置。 |
EndScroll |
滚动框已停止移动。 |
一般的用户在进行滚动操作时会触发一个或多个不同类型的滚动事件,而且这些事件的发生过程和Windows操作系统的“拖动时显示窗口内容”设置有关。
笔者在控制面板中运行“显示”项目,显示出“显示属性”对话框,切换到“外观”标签页,点击“效果”按钮,弹出的“效果”对话框中就能设置“拖动时显示窗口内容”的操作系统配置项了。这个选项对控件的滚动行为影响很大。
在Windows操作系统中,鼠标右击控件的滚动条,通常会弹出如下的快捷菜单。点击这些快捷菜单就会设置滚动条的位置,而且会触发不同类型的滚动事件
经过试验,在WindowsXP SP2的环境下,点击这些菜单项目而触发的滚动事件有
滚动至此 |
当Windows系统设置了“拖动时显示窗口内容”时 ThumbTrack ThumbPosition ThumbTrack ThumbPosition 当没有设置“拖动时显示窗口内容”时 ThumbTrack ThumbTrack ThumbPosition |
顶部 |
First 0 |
底部 |
Last |
向上翻页 |
LargeDecrement |
向下翻页 |
LargeIncrement |
向上滚动 |
SmallDecrment |
向下滚动 |
SmallIncrement |
当用户用鼠标拖拽操作直接拖动滚动条时,控件触发的滚动事件过程如下:
1. 当用户在滚动条上按下鼠标左键,开始拖拽在时,控件触发ThumbTrack类型的滚动事件。[作者袁永福 http://www.xdesigner.cn/ ]
2. 当用户移动鼠标时,每一移动都会让控件触发ThumbTrack类型的滚动事件。当Windows系统设置了“拖动时显示窗口内容”时,还会触发控件重绘事件,当没有设置“拖动时显示窗口内容”时不会触发控件重绘事件。
3. 当用户松开鼠标按键,结束拖拽操作时触发ThumbPosition事件。
此外当程序自己使用代码设置控件的AutoScrollPosition属性来自行滚动时不会触发任何控件滚动事件,鼠标滚轮操作也不会触发滚动事件。
以上是在笔者的WindowsXP SP2的系统中的实验效果,相信对其他Windows操作系统也一样吧。
根据上述实验结果,笔者重点处理ThumbTrack和ThumbPosition类型的滚动事件,由于存在“拖动时显示窗口内容”的设置,笔者会重载处理控件的Windows消息处理方法,当Windows没有设置“拖动时显示窗口内容”时,对每一个ThumbTrack事件消息都额外的处理一个ThumbPosition消息,这样就将两种情况统一起来。[作者袁永福 http://www.xdesigner.cn/ ]
解决问题
根据所上述分析,笔者开始创建一种固定背景的可滚动的控件了,原理上面讲的比较清楚,因此编写代码时不再多说了。笔者首先使用VS.NET2005建立一个名为“FixedBackground”的WinForm的C#工程。然后创建一个名为FixedBackgroundControl的类型,该类型是从System.Windows.Forms.UserControl类型派生的。
笔者建立一个FixedBackground的属性用于指定是否启动固定背景的功能,其代码如下
/// <summary>
/// 固定背景
/// </summary>
[System.ComponentModel.Category("Appearance")]
[System.ComponentModel.DefaultValue(false)]
public bool FixedBackground
{
get
{
return bolFixedBackground;
}
set
{
bolFixedBackground = value;
}
}
然后定义一个名为LogonImage的属性用于设置在控件客户区右下角显示的图标,其代码如下
/// <summary>
/// 标志图片
/// </summary>
[System.ComponentModel.Category("Appearance")]
public System.Drawing.Image LogonImage
{
get
{
return myLogonImage;
}
set
{
myLogonImage = value;
}
}
然后笔者重载控件的OnPaintBackground方法用于自定义绘制背景,其代码如下。
/// 自定义绘制控件背景
/// </summary>
/// <param name="e"></param>
protected override void OnPaintBackground(PaintEventArgs e)
{
base.OnPaintBackground(e);
if (myLogonImage != null)
{
// 在控件客户区的右下角绘制标志图片
int x = this.ClientSize.Width - myLogonImage.Width;
int y = this.ClientSize.Height - myLogonImage.Height;
if (e.ClipRectangle.IntersectsWith(
new Rectangle(
x,
y,
myLogonImage.Width,
myLogonImage.Height)))
{
e.Graphics.DrawImage(
myLogonImage,
x,
y,
myLogonImage.Width ,
myLogonImage.Height );
}
}
}
接着要处理控件的滚动事件了,首先笔者导入Win32API函数LockWindowUpdate,其代码如下
private static extern bool LockWindowUpdate(IntPtr hWnd);
笔者重载控件的OnScroll方法,其代码如下
/// 处理滚动条事件
/// </summary>
/// <param name="se">事件参数</param>
protected override void OnScroll(ScrollEventArgs se)
{
if (bolFixedBackground)
{
// 执行固定背景的操作
if (se.Type == ScrollEventType.ThumbTrack)
{
// 若滚动框正在移动,解除对控件用户界面的锁定
LockWindowUpdate(IntPtr.Zero);
// 立即重新绘制控件所有的用户界面
this.Refresh();
// 锁定控件的用户界面
LockWindowUpdate(this.Handle);
}
else
{
// 解除对控件用户界面的锁定
LockWindowUpdate(IntPtr.Zero);
// 声明控件的所有的内容无效,但不立即重新绘制
this.Invalidate();
}
}
base.OnScroll(se);
}
在这里可以看到程序处理滚动事件时调用了Refresh或Invalidate函数,这将导致控件所有的内容都重新绘制,当控件比较大,内容比较复杂时会导致绘制控件图形的任务很重,从而导致明显的闪烁,这是不可避免的难于优化的过程,因此笔者使用双缓冲技术来解决闪烁文件,笔者在控件的构造函数中添加以下代码即可启用控件的双缓冲设置
this.DoubleBuffered = true;
双缓冲能避免闪烁,但拖累的软件的性能,因此本控件是一个为了美观而降低性能的典型,因此建议本技术不要大量采用。[作者袁永福 http://www.xdesigner.cn/ ]
接着笔者要重载控件的Windows消息处理方法了,由于要事先知道Windows操作系统的“拖动时显示窗口内容”的设置。因此使用了下述代码
/// Windows操作系统是否设置为拖动时显示窗口内容
/// </summary>
private bool bolDragFullWindows = false;
/// <summary>
/// 当创建控件Windows句柄时的处理,调用SystemParametersInfoGetBool API函数
/// 判断操作系统是否设置为拖动时显示窗口内容。
/// </summary>
/// <param name="e">参数</param>
protected override void OnHandleCreated(EventArgs e)
{
bolDragFullWindows = false;
if (SystemParametersInfoGetBool(SPI_GETDRAGFULLWINDOWS, 0, ref bolDragFullWindows, 0) == false)
{
bolDragFullWindows = false;
}
base.OnHandleCreated(e);
}
private const int SPI_GETDRAGFULLWINDOWS = 0x0026;
[System.Runtime.InteropServices.DllImport(
"user32.dll",
EntryPoint = "SystemParametersInfo",
SetLastError = true)]
private static extern bool SystemParametersInfoGetBool(
int action,
uint param,
ref bool vparam,
uint init);
在这段代码中,bolDragFullWindows全局变量用于指明是否系统是否启用了“拖动时显示窗口内容”。笔者重载了OnHandleCreated方法,在这个方法中调用Win32API函数SystemParametersInfoGetBool来获得这个系统级设置。
SystemParametersInfo函数是一个很强大的Win32API函数,用于获得各种操作系统级设置,能实现的功能点很多,具体可参考MSDN中关于该函数的详细说明。
经过试验,控件在创建后修改了“滚动时显示窗口内容”后,对控件的滚动行为没有发生任何影响。因此实时的检测“滚动时显示窗口内容”的设置是不合适的,应当在控件句柄创建时才检查该设置,因此笔者重载了控件的OnHandleCreated函数来检测该系统级设置并保存在一个全局变量中。[作者袁永福 http://www.xdesigner.cn/ ]
笔者重载了控件的WndProc方法来处理控件接受的Windows底层消息,其代码如下
/// 处理底层Windows消息处理方法
/// </summary>
/// <param name="m">Windows消息对象</param>
protected override void WndProc(ref Message m)
{
if (bolFixedBackground)
{
if (m.HWnd == this.Handle)
{
// 当前消息是横向滚动条或纵向滚动条事件
if (m.Msg == 0x0114 // WM_HSCROLL
|| m.Msg == 0x0115)// WM_VSCROLL )
{
int v = m.WParam.ToInt32();
if ((v & 0xf) == 5)
{
// 滚动消息是 THUMBTRACK 类型
base.WndProc(ref m);
if (bolDragFullWindows == false)
{
// Windows操作系统没有设置为拖动时显示窗口内容
// 则重复执行 THUMBPOSITION 类型的滚动消息
v = v - 1;
m.WParam = new IntPtr(v);
base.WndProc(ref m);
}
}
else
{
base.WndProc(ref m);
}
return;
}
}
}
base.WndProc(ref m);
}
在这段代码中,若消息类型是ThumbTrack类型的滚动消息,则执行控件的默认处理方法,然后将消息类型修改为ThumbPosition类型的滚动消息,然后再次执行控件默认的消息处理方法,这样就使得控件每接受到一个ThumbTrack类型的滚动消息就处理了ThumbTrack和ThumbPosition两个消息。这就统一了有和没有“拖动时显示窗口内容”两种情况。
由于用户进行鼠标滚轮操作时不会触发滚动事件,因此还需要处理控件的鼠标滚轮事件来,其代码如下
/// 处理鼠标滚轮事件
/// </summary>
/// <param name="e"></param>
protected override void OnMouseWheel(MouseEventArgs e)
{
if (bolFixedBackground)
{
LockWindowUpdate(this.Handle);
base.OnMouseWheel(e);
LockWindowUpdate(IntPtr.Zero);
this.Invalidate();
}
else
{
base.OnMouseWheel(e);
}
}
笔者还进行了一些其他非关键的代码的编写,这样,一个具有固定背景图片的可滚动的控件开发完毕。[作者袁永福 http://www.xdesigner.cn/ ]
测试
这个控件开发完毕后,笔者就可以测试了。笔者在项目中新增一个窗体,打开窗体设计器,可以在工具箱上看到“FixedBackgroundControl”的项目,笔者点击该项目即可在窗体上画出一个固定背景图片的控件,则窗体的设计样式如下图所示
笔者对该控件进行以下属性设置
AutoScroll |
True |
AutoScrollMinSize |
2000,2000 |
FixedBackground |
True |
LogonImage |
这样笔者就可以运行这个窗体来检查控件的运行时效果了。[作者袁永福 http://www.xdesigner.cn/ ]
小结
在这篇文章中,笔者使用了WinForm.NET2.0来实现一个具有固定背景图片的带滚动的控件,从而实现了类似“background-attachment:fixed”的CSS样式的用户界面,这个过程是比较复杂的,需要了解WIN32编程的一些知识。相对于常规ASP.NET开发,C#图形编程比较复杂的,应用广泛,对于软件技术爱好者来说是一片广阔的天空。