在 .NET 应用程序中添加气球窗口






4.94/5 (118投票s)
2002 年 12 月 28 日
6分钟阅读

480780

3500
介绍 BalloonWindow 类,该类允许 .NET 应用程序实现类似于 Windows XP 中可用的气球窗口。完全自定义允许配置外观和形状,以及投射 Alpha 混合阴影。
引言
本文介绍了
BalloonWindow
类。BalloonWindow
的设计目标是允许任何 .NET 应用程序显示功能与操作系统内置的气球(如图 2 所示)类似的气球。
BalloonWindow
以 C# 编写,公开了完全自定义气球外观所需的函数。自定义示例包括设置背景样式或颜色;通过调整锚点位置、圆角曲率或阴影效果来定义气球布局;以及像所有其他 Form 类一样,在气球内放置控件。
计算形状
气球的形状由 GraphicsPath
对象维护,并通过 RecalcLayout
方法计算。
private GraphicsPath RecalcLayout(Rectangle rect, Point target)
{
GraphicsPath gp = new GraphicsPath();
两个独立的组件控制锚点的形状和位置:锚点象限指示四个侧面之一,锚点偏移量指示沿指定锚点象限的增加距离。图 3 显示了这两个组件与整个气球之间的关系。
一个重要的设计考虑是使它们独立,因为如图 4 所示,存在十二种锚点排列方式。沿任何象限,锚点的位置可以是前中心、中心或后中心。中心是最小和最大偏移量之间的确切位置。当锚点居中时,其扫描角度为 90 度,而所有其他位置的角度为 45 度,且垂直边缘始终背离中心。
下面的代码片段计算了锚点象限所需的调整。计算象限很简单,只需要使用 matrix
和 anchorFlipped
变量,稍后将进行解释。此代码基于从气球“顶部”的旋转来确定其他任何锚点的位置,即,如果需要在三个“顶部”锚点位置之一放置锚点,则不进行旋转;而如果需要在气球的右侧或左侧放置锚点,则分别使用 90 度或 -90 度的旋转。
switch(anchorQuadrant)
{
case AnchorQuadrant.Top:
break;
case AnchorQuadrant.Bottom:
matrix.Translate(balloonBounds.Width, balloonBounds.Height);
matrix.Rotate(180);
anchorFlipped = true;
break;
case AnchorQuadrant.Left:
balloonBounds.Size =
new Size(balloonBounds.Height, balloonBounds.Width);
matrix.Translate(0, balloonBounds.Width);
matrix.Rotate(-90);
anchorFlipped = true;
break;
case AnchorQuadrant.Right:
balloonBounds.Size =
new Size(balloonBounds.Height, balloonBounds.Width);
matrix.Translate(balloonBounds.Height, 0);
matrix.Rotate(90);
break;
}
如前所述,在构建路径时,所有计算都假定锚点位于顶部象限。这样做是为了使计算简单且可预测。路径完成后,使用
matrix
变量进行调整,该变量旋转气球,使锚点位于正确的象限。然而,使用顶部作为基本锚点象限会带来一个有趣的问题。当气球旋转 180 度或 270 度时,锚点偏移量会出现在与所需位置相反的中心侧。图 5 演示了这种情况。anchorFlipped
变量标记此条件,允许像下面那样重新计算锚点偏移量。从图 5 可以看出,当锚点旋转 180 度时,锚点会被移到“右下角”。在将锚点放置在正确位置(在此情况下是“左下角”)之前,需要进行一次后续的移动。
if(anchorFlipped)
anchorOffset =
(int)balloonBounds.Width-(offsetFromEdge*2)-anchorOffset;
无论偏移量是前中心、中心还是后中心,锚点始终有三个点。以下代码计算这三个点。
if(anchorOffset < balloonEdgeCenter)
{
anchorPoints[0] =
new Point(anchorOffset+offsetFromEdge,
(int)balloonBounds.Y+anchorMargin);
anchorPoints[1] =
new Point(anchorOffset+offsetFromEdge,
(int)balloonBounds.Y);
anchorPoints[2] =
new Point(anchorOffset+anchorMargin+offsetFromEdge,
(int)balloonBounds.Y+anchorMargin);
}
else if(anchorOffset > balloonEdgeCenter)
{
anchorPoints[0] =
new Point(anchorOffset-anchorMargin+offsetFromEdge,
(int)balloonBounds.Y+anchorMargin);
anchorPoints[1] =
new Point(anchorOffset+offsetFromEdge,
(int)balloonBounds.Y);
anchorPoints[2] =
new Point(anchorOffset+offsetFromEdge,
(int)balloonBounds.Y+anchorMargin);
}
else
{
anchorPoints[0] =
new Point(anchorOffset-anchorMargin+offsetFromEdge,
(int)balloonBounds.Y+anchorMargin);
anchorPoints[1] =
new Point(anchorOffset+offsetFromEdge,
(int)balloonBounds.Y);
anchorPoints[2] =
new Point(anchorOffset+anchorMargin+offsetFromEdge,
(int)balloonBounds.Y+anchorMargin);
}
计算完成后,可以按如下所示构建路径。
gp.AddArc(balloonBounds.Left, balloonBounds.Top+anchorMargin,
cornerDiameter, cornerDiameter,180, 90);
gp.AddLine(anchorPoints[0], anchorPoints[1]);
gp.AddLine(anchorPoints[1], anchorPoints[2]);
gp.AddArc(balloonBounds.Width-cornerDiameter,
balloonBounds.Top+anchorMargin,
cornerDiameter, cornerDiameter, -90, 90);
gp.AddArc(balloonBounds.Width-cornerDiameter,
balloonBounds.Bottom-cornerDiameter,
cornerDiameter, cornerDiameter, 0, 90);
gp.AddArc(balloonBounds.Left, balloonBounds.Bottom-cornerDiameter,
cornerDiameter, cornerDiameter, 90, 90);
最后还有一个问题;调整路径,使锚点位于正确的象限。
gp.Transform(matrix);
约束区域
GraphicsPathWindow
类支持非标准窗口形状。该类本身不了解窗口的形状;而是在需要时调用虚拟 PreparePath
方法,如下所示。此处 GetPath
方法检查路径是否已缓存,如果未缓存,则请求派生类提供路径。
public GraphicsPath GetPath()
{
GraphicsPath gp = __graphicsPath;
if(gp == null) gp = PreparePath();
SetPath(gp);
return gp;
}
这为所有派生类提供了根据需要定义路径的能力。下面是 BalloonWindow
用于定义其路径的代码。
protected override GraphicsPath PreparePath()
{
return __layout.Path;
}
窗口定义一个区域,指示操作系统仅在该区域内绘制。以下代码约束了区域,并确保窗口看起来像一个气球。当从 GraphicsPath
对象定义 Region
时,该区域被定义为路径的内部区域。下面的代码包括了包含路径边框的调整。
private Region RegionFromPath(GraphicsPath gp)
{
if(gp == null) throw(new ArgumentNullException("gp"));
Region region = new Region(gp);
float inflateBy = 1F+2F/(float)Width;
Matrix matrix = new Matrix();
matrix.Scale(inflateBy, inflateBy);
matrix.Translate(-1, -1);
region.Transform(matrix);
return region;
}
投射阴影
Windows 2000 引入了分层窗口。分层窗口允许操作系统将窗口内容与背景进行 Alpha 混合。一个例子是 Windows 2000 和 XP 中可用的气球窗口投射的阴影。
BalloonWindow
的一个设计目标是支持相同的阴影效果。经过研究了几个设计考虑因素,最终的最佳方法是将阴影实现在一个独立窗口中,该窗口位于内容窗口的后面(如图 6 所示)。
ShadowedWindow
类负责维护 BalloonWindow
继承的阴影效果。当阴影首次显示时,CreateShadowProjection
会创建一个 Projection
对象。请记住,ShadowedWindow
本身不是阴影。阴影窗口本身由 ShadowedWindow
维护的 Projection
对象封装。
private Projection CreateShadowProjection()
{
Projection shadow = new Projection(this);
shadow.BackColor = Color.White;
BindShadowToOwner(shadow, this);
return shadow;
}
投射的阴影始终位于内容窗口后面,并具有相同的尺寸。因此,阴影需要稍微偏移以使其可见。
public void ShowShadow()
{
GraphicsPathWindow shadow = __shadow;
if(shadow == null) shadow = __shadow = CreateShadowProjection();
int shadowMargin = ShadowMargin;
Point shadowLocation =
new Point(Location.X+shadowMargin, Location.Y+shadowMargin);
Size shadowSize = Size;
shadow.Location = shadowLocation;
shadow.Size = shadowSize;
shadow.Show();
}
阴影是在离屏位图上创建的,并使用渐变图案渲染。
Bitmap img = new Bitmap(Width, Height);
GraphicsPath path = GetPath();
Graphics grx = Graphics.FromImage(img);
float scaleFactor = 1F-((float)__owner.ShadowMargin*2/(float)Width);
PathGradientBrush backStyle = new PathGradientBrush(path);
backStyle.CenterPoint = new Point(0, 0);
backStyle.CenterColor = __owner.ShadowColor;
backStyle.FocusScales =
new PointF(scaleFactor, scaleFactor);
backStyle.SurroundColors =
new Color[]{Color.Transparent};
Region region = new Region(path);
region.Translate(-__owner.ShadowMargin, -__owner.ShadowMargin);
grx.SetClip(region, CombineMode.Xor);
grx.FillPath(backStyle, path);
SetBitmap 方法使用 Rui Godinho Lopes 提供的代码初始化和更新分层窗口,该代码在其文档“C# 中的逐像素 Alpha 混合”中有详细介绍。
SetBitmap(img);
结论
BalloonWindow 是一个强大的库,用于生成简单的 UI 元素。
类结构
BalloonWindow 被设计成任何应用程序健壮且必不可少组件。图 7 显示了 BalloonWindow 可用的公共对象模型。
构建历史
有关每个版本的详细信息,请参阅 BalloonWindow Library for .NET 门户。版权
版权所有 © 2002-2003 Peter Rilling
源代码文件和二进制文件可以以任何方式不受修改地重新分发,前提是它们不以盈利为目的出售,并且作者已明确书面同意,并且此通知以及作者姓名和所有版权声明保持不变。
以任何形式(无论是否修改)使用软件的源代码或二进制形式,都必须在用户文档(“关于”框和打印文档)和代码的内部注释中包含以下用户通知:
"部分版权所有 © 2002-2003 Peter Rilling"
一封告知您正在使用它的电子邮件也会很好。考虑到为此付出的工作量,这不算太多要求。
本软件按“原样”提供,不提供任何明示或暗示的保证。请自行承担使用风险。对于本产品可能造成的任何数据损坏/丢失,作者概不负责。