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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (118投票s)

2002 年 12 月 28 日

6分钟阅读

viewsIcon

480780

downloadIcon

3500

介绍 BalloonWindow 类,该类允许 .NET 应用程序实现类似于 Windows XP 中可用的气球窗口。完全自定义允许配置外观和形状,以及投射 Alpha 混合阴影。

Sample screen demonstrating the BalloonWindow class.

引言

固定在通知图标上的操作系统气球窗口。本文介绍了 BalloonWindow 类。BalloonWindow 的设计目标是允许任何 .NET 应用程序显示功能与操作系统内置的气球(如图 2 所示)类似的气球。

BalloonWindow 以 C# 编写,公开了完全自定义气球外观所需的函数。自定义示例包括设置背景样式或颜色;通过调整锚点位置、圆角曲率或阴影效果来定义气球布局;以及像所有其他 Form 类一样,在气球内放置控件。

计算形状

气球的形状由 GraphicsPath 对象维护,并通过 RecalcLayout 方法计算。

private GraphicsPath RecalcLayout(Rectangle rect, Point target)
{
   GraphicsPath gp = new GraphicsPath();

锚点象限和偏移量。两个独立的组件控制锚点的形状和位置:锚点象限指示四个侧面之一,锚点偏移量指示沿指定锚点象限的增加距离。图 3 显示了这两个组件与整个气球之间的关系。


锚点的外观根据象限和偏移量而变化。一个重要的设计考虑是使它们独立,因为如图 4 所示,存在十二种锚点排列方式。沿任何象限,锚点的位置可以是前中心、中心或后中心。中心是最小和最大偏移量之间的确切位置。当锚点居中时,其扫描角度为 90 度,而所有其他位置的角度为 45 度,且垂直边缘始终背离中心。

下面的代码片段计算了锚点象限所需的调整。计算象限很简单,只需要使用 matrixanchorFlipped 变量,稍后将进行解释。此代码基于从气球“顶部”的旋转来确定其他任何锚点的位置,即,如果需要在三个“顶部”锚点位置之一放置锚点,则不进行旋转;而如果需要在气球的右侧或左侧放置锚点,则分别使用 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 可用的公共对象模型。

Object model for the BalloonWindow class structure.

构建历史

有关每个版本的详细信息,请参阅 BalloonWindow Library for .NET 门户。

版权

版权所有 © 2002-2003 Peter Rilling

源代码文件和二进制文件可以以任何方式不受修改地重新分发,前提是它们不以盈利为目的出售,并且作者已明确书面同意,并且此通知以及作者姓名和所有版权声明保持不变。

以任何形式(无论是否修改)使用软件的源代码或二进制形式,都必须在用户文档(“关于”框和打印文档)和代码的内部注释中包含以下用户通知:

"部分版权所有 © 2002-2003 Peter Rilling"

一封告知您正在使用它的电子邮件也会很好。考虑到为此付出的工作量,这不算太多要求。

本软件按“原样”提供,不提供任何明示或暗示的保证。请自行承担使用风险。对于本产品可能造成的任何数据损坏/丢失,作者概不负责。

    © . All rights reserved.