MDIClient 再探






4.53/5 (17投票s)
2004年10月13日
3分钟阅读

87176

2202
使用 100% 受管代码绘制 MDI 窗口的背景,包括设计时支持。
引言
我阅读了 Jacob Slusser 最近写的关于如何更改 MDI 窗体背景的文章。它非常翔实,但对我来说,对于它应该做的事情来说有点过于复杂。我不是 P/Invoke 调用的拥护者(例如,调用 SetWindowLong
函数),而是首先尝试受管方法和类,例如 SetStyle
来设置控件样式。他还询问了如何在设计时支持这一点。
我写了一个“简单”的窗体,它可以被视觉继承,并且几乎实现了 Jacob 所做的事情。但我采用了一种不同的方法,它 100% 是受管代码。它还提供设计时支持,可以直观地更改简单窗体或 MDI 窗体的背景颜色和图片。您可以根据我提供的示例应用程序中的基窗体的视觉继承,随心所欲地扩展它。
MDI 窗体设计时支持的问题在于,实际窗体和 MdiClient
控件在设计时和运行时行为不同。当 IsMdiContainer
属性设置为 true
时,MdiClient
的 Paint
事件在设计时不会被触发来绘制其背景,但在运行时会触发。相反,在运行时,实际窗体的 Paint
事件从未被触发,而是由 MdiClient
触发。
我在这里克服这个问题的做法是处理代码中的这两个事件来支持背景绘制(包括设计时)。我只需要重写基窗体的 IsMdiContainer
属性,挂钩 MdiClient
和实际窗体的 Paint
事件,然后进行一些绘制,就这样。
[DefaultValue(false)]
public new bool IsMdiContainer
{
get{ return base.IsMdiContainer; }
set
{
base.IsMdiContainer = value;
if( ! value) return;
for(int i = 0; i < this.Controls.Count; i++)
{
MdiClient mdiClient = this.Controls[i] as MdiClient;
if(mdiClient != null)
{
mdiClient.Paint +=new PaintEventHandler(this.MdiClient_Paint);
break;
}
}
}
}
protected override void OnPaint(PaintEventArgs e)
{
// In design time the MdiClient_Paint has not been
// called but OnPaint is called. Then...
PaintBackground(e.Graphics);
}
private void MdiClient_Paint(object sender, PaintEventArgs e)
{
PaintBackground(e.Graphics);
}
private void PaintBackground( Graphics g )
{
// Create a brush
Rectangle rect = this.ClientRectangle;
rect.Inflate(2,2);// to completely fill the client area
LinearGradientBrush filler = new LinearGradientBrush(
rect,
this._backColor1,
this._backColor2,
this._angle);
// Fill the client area
g.FillRectangle(filler,rect);
// Draw image centered,
// We use here forms "BackgroundImage" property. Nothing special...
if( this.BackgroundImage != null)
{
//Make it transparent if you like!!!
//((Bitmap)this.BackgroundImage).MakeTransparent();
int x= (this.ClientRectangle.Width/2) - (this.BackgroundImage.Width/2);
int y= (this.ClientRectangle.Height/2) - (this.BackgroundImage.Height/2);
g.DrawImageUnscaled(this.BackgroundImage, x, y);
}
filler.Dispose();
}
如果您使用上面的代码,您会发现在运行时将 IsMdiContainer
属性设置为 true
时,背景会出现闪烁。这是 MdiClient
控件一个恼人的设计缺陷。它基本上会从其父级(即实际窗体)复制所有样式,除了 ControlStyles.DoubleBuffer
。真奇怪!不是吗?我看不到任何合理的理由。为了克服这个问题,我使用了反射来设置 ControlStyles.DoubleBuffer
标志。要做到这一点,请像下面所示一样更改您的重写 IsMdiContainer
属性
[DefaultValue(false)]
public new bool IsMdiContainer
{
get{ return base.IsMdiContainer; }
set
{
base.IsMdiContainer = value;
if( ! value) return;
for(int i = 0; i < this.Controls.Count; i++)
{
MdiClient mdiClient = this.Controls[i] as MdiClient;
if(mdiClient != null)
{
ControlStyles styles = ControlStyles.DoubleBuffer;
try
{
// Prevent flickering, only if our assembly
// have reflection permission.
Type mdiType = typeof(MdiClient);
System.Reflection.BindingFlags flags =
System.Reflection.BindingFlags.NonPublic |
System.Reflection.BindingFlags.Instance;
System.Reflection.MethodInfo method =
mdiType.GetMethod("SetStyle",flags);
object[] param = {styles, true};
method.Invoke(mdiClient,param);
}
catch ( System.Security.SecurityException)
{
/*Don't do anything!!! This code is running under
partially trusted context*/
}
mdiClient.Paint += new PaintEventHandler(this.MdiClient_Paint);
break;
}
}
}
}
正如您所见,我的 catch
块不对异常做任何处理。原因是,如果我的控件没有足够的权限,它就不应该崩溃。如果它没有反射权限来防止这种情况,它只会闪烁。我认为这没问题,因为我的控件几乎可以在任何提供的安全上下文中运行。我们也可以将其设为可选,如果您要求在包含您的基窗体的程序集(在 AssemblyInfo.cs 中)中设置可选的反射权限,如下所示
[assembly: ReflectionPermission(SecurityAction.RequestOptional,
Unrestricted= true)]
完成此操作后,您会在设计时和运行时窗体左上角的系统菜单中看到一个信息(i)图像。当您启动窗体时,您将收到安全信息(作为工具提示),表明您的应用程序正在部分信任的安全上下文中运行。
结论
我认为这种方法比使用 NativeWindow
类子类化 MdiClient
控件要好,后者需要非托管代码权限才能运行。如果我们这样做,除了例如在完全信任上下文下运行窗体之外,将没有其他选择。这是一个很高的期望,从中可以使用我们的窗体。您需要问自己很多问题:您是否选择了正确的控件开发方法,用户从网络驱动器或 Internet 区域执行程序时,程序(使用您的控件)是否会运行,或者在什么情况下它会运行而不引发安全异常等等!使用这种方法,我的控件更简单、更兼容,我可以睡个好觉。
GradientForm 的完整源代码
using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Drawing.Drawing2D;
public class GradientForm : System.Windows.Forms.Form
{
private System.Drawing.Color _backColor1 = System.Drawing.Color.White;
private System.Drawing.Color _backColor2 = System.Drawing.Color.CornflowerBlue;
private int _angle = 0;
public GradientForm()
{
SetStyle(System.Windows.Forms.ControlStyles.DoubleBuffer, true);
SetStyle(System.Windows.Forms.ControlStyles.AllPaintingInWmPaint, true);
SetStyle(System.Windows.Forms.ControlStyles.ResizeRedraw, true);
SetStyle(System.Windows.Forms.ControlStyles.UserPaint, true);
}
[DefaultValue(false)]
public new bool IsMdiContainer
{
get{ return base.IsMdiContainer; }
set
{
base.IsMdiContainer = value;
if( ! value) return;
for(int i = 0; i < this.Controls.Count; i++)
{
MdiClient mdiClient = this.Controls[i] as MdiClient;
if(mdiClient != null)
{
ControlStyles styles = ControlStyles.DoubleBuffer;
try
{
// Prevent flickering, only if our assembly
// has reflection permission.
Type mdiType = typeof(MdiClient);
System.Reflection.BindingFlags flags =
System.Reflection.BindingFlags.NonPublic |
System.Reflection.BindingFlags.Instance;
System.Reflection.MethodInfo method
= mdiType.GetMethod("SetStyle",flags);
object[] param = {styles, true};
method.Invoke(mdiClient,param);
}
catch ( System.Security.SecurityException)
{
/*Don't do anything!!! This code is running under
partially trusted context*/
}
mdiClient.Paint +=new PaintEventHandler(this.MdiClient_Paint);
break;
}
}
}
}
[DefaultValue(typeof(Color),"White")]
[Category("Gradient")]
public System.Drawing.Color BackColor1
{
get
{
return _backColor1;
}
set
{
if( _backColor1 == value ) return;
_backColor1 = value;
this.Invalidate();
}
}
[DefaultValue(typeof(Color),"CornflowerBlue")]
[Category("Gradient")]
public System.Drawing.Color BackColor2
{
get
{
return _backColor2;
}
set
{
if( _backColor2 == value ) return;
_backColor2 = value;
this.Invalidate();
}
}
[DefaultValue(0)]
[Category("Gradient")]
public int Angle
{
get{ return _angle;}
set
{
if( _angle == value || value < 0 || _angle > 360) return;
_angle = value;
this.Invalidate();
}
}
protected override void OnPaint(PaintEventArgs e)
{
// In design time the MdiClient_Paint has not been
// called but OnPaint is called. Then...
PaintBackground(e.Graphics);
}
private void MdiClient_Paint(object sender, PaintEventArgs e)
{
PaintBackground(e.Graphics);
}
private void PaintBackground( Graphics g )
{
// Create a brush
Rectangle rect = this.ClientRectangle;
rect.Inflate(2,2);// to completely fill the client area
LinearGradientBrush filler = new LinearGradientBrush(
rect,
this._backColor1,
this._backColor2,
this._angle);
// Fill the client area
g.FillRectangle(filler,rect);
// Draw image centered,
// We use here forms "BackgroundImage" property. Nothing special...
if( this.BackgroundImage != null)
{
//Make it transparent if you like!!!
//((Bitmap)this.BackgroundImage).MakeTransparent();
int x= (this.ClientRectangle.Width/2) -
(this.BackgroundImage.Width/2);
int y= (this.ClientRectangle.Height/2) -
(this.BackgroundImage.Height/2);
g.DrawImageUnscaled(this.BackgroundImage, x, y);
}
filler.Dispose();
}
}