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

WPF 和 .NET 3.5 - 绘制自定义控件和自定义 UI 元素

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (21投票s)

2007年12月10日

CPOL

8分钟阅读

viewsIcon

166338

downloadIcon

3897

使用 Visual Studio 2008 对 WPF 和 .NET 3.5 进行自定义绘图;有趣的螺旋仪

Screenshot - windowsmall.jpg

目录

介绍

Visual Studio 2008 已发布,我可以肯定地说,我完全不知所措。截至本文撰写之时,我几乎没有时间深入研究 LINQ 等新语言功能,更不用说 WPF 这样重大的范式转变了。我将本文视为一个学习 WPF 的平台,希望您也能从中受益。我决定使用外摆线(epicycloid)的数学概念作为我第一次尝试 WPF 的主题。大多数人会记得外摆线是他们童年时期的 Spirograph。小时候,我对绘制 Spirograph 图案有着无尽的迷恋,所以用它来重温这段记忆,为这篇与图形相关的文章似乎很合适。

什么是 WPF?

定义

对于还不熟悉 Windows Presentation Foundation (WPF) 的朋友们来说,WPF 是微软用于构建 Windows 用户界面的下一代平台。WPF 的美妙之处在于,它首次将定义 Windows UI 的呈现和布局与代码分离开来。UI 使用 XAML 进行渲染,XAML 是 WPF 原生的、基于 XML 的标记语言,而代码则保留在 *.cs 文件中。

根本性的改变

最重要的是,WPF 应该能让大多数 UI 任务的难度呈指数级降低。我们将在本文中探讨这一说法。

XAML 和代码的分离,本质上与 HTML 标记和 ASP.NET 后台代码文件之间的分离相同。这与早期 VB 5 时代遗留下来的、视觉上有限的拖放界面截然不同。显而易见的好处是,借助 Expression 等工具,应用程序的 UI 可以由图形团队设计,而开发团队可以专注于开发。这对于像我这样在职业生涯中设计过一些糟糕 UI 的人来说尤其重要。

自定义布局

尽管我缺乏设计天赋,WPF 应该能提供巨大的 UI 优势。让我们尝试创建一个体面的界面。第一步是添加一个漂亮的背景。过去,开发人员需要订阅或重写窗体绘制事件,进行一些不太有趣的裁剪计算,然后手动绘制图像才能获得 WPF 仅用几行 XAML 就能提供的效果。这是平铺背景的 XAML 代码:

<grid>
        <grid.background>
            <imagebrush viewport="0,0,0.3,0.3" viewbox="0,0,1,1" stretch="None"
                tilemode="FlipXY" imagesource="background.jpg" />
        </grid.background>
</grid>

Screenshot - tile.jpg

XAML 几乎能自言自语,这不是很棒吗?我表单上的主(父)元素是 Grid。我在 Grid 的背景中添加了一个 ImageBrush。图像通过翻转每个交替的瓦片(横向)来平铺。Viewbox 属性简单地表示要显示的原始图像区域。Viewbox 属性是 Viewbox 将要显示的放置区域。换句话说,上面的 XAML 告诉 WPF 框架应该平铺图像的哪个部分。请注意,当您更改 XAML 中的 Viewport 值时,Visual Studio 2008 中的 UI 表示会动态更新!

好吧,背景用几行标记代码就轻松搞定了。现在我们需要一个通用的窗口布局。我打算分成两列,左边是控件,右边是 Spirograph 的绘图表面;大致如下:

Screenshot - layout.jpg

为了实现这一点,我结合使用了 GridStackPanelDockPanel 对象。DockPanel 是主要的容器。对我来说,这很简单,我想将控件停靠在左侧,让绘图表面填充窗口的其余部分。我发现的一个障碍是 WPF 中没有“Fill”属性,而我在 2.0 Framework 中一直依赖它。但是,我确实发现 DockPanel 控件有一个名为 LastChildFill 的便捷属性,它可以做我需要的一切。这是简化的 XAML:

<dockpanel lastchildfill="True" margin="20,20,5,5 " name="dockPanel1" >
    <stackpanel name="stackPanel1" width="200" dockpanel.dock="Left" >
    ...
    </stackpanel>
    <dockpanel lastchildfill="True" margin="20,0,15,15"
            name="stackPanel2" dockpanel.dock="Right" >
    ...
    </dockpanel>
</dockpanel>

Screenshot - box.jpg

又一次,非常简单。我们现在有了一个左侧停靠的面板和一个填充的右侧区域。

自定义控件

我希望控件能显示在半透明背景上。同样,如果不使用 WPF,这将是一项耗时的工作。再一次,WPF 能够轻松地完成 UI 设计。看看下面这段 XAML 代码,它会渲染一个带有圆角的半透明矩形:

<rectangle width="200" opacity="0.5" stroke="Silver"
    fill="White" radiusy="10" radiusx="10" height="260" />

就这些。一行代码。它仍然让我感到难以置信,一切都如此轻松地完成。请注意,在源代码中,您会在 Canvas 元素内看到这个 Rectangle 对象。快速解释一下,Canvas 元素允许控件相互重叠。

接下来,我想尝试自定义一个标准的按钮。我想要一个按钮,左侧边缘有一个 PNG 图像。这是 .NET 和 Visual Studio 早期版本中并不难完成的任务,所以让我们用 XAML 来测试一下,看看 WPF 是否更简单或更具可定制性。这是图像按钮的 XAML:

<button name="drawButton" margin="10,10,0,10" width="80" height="23"
        horizontalalignment="Right">
    <stackpanel width="Auto" height="Auto" horizontalalignment="Left"
            orientation="Horizontal">
        <img height="16" width="16" stretch="Fill" source="draw.png" />
        <textblock margin="10,0,50,0" text="Draw" fontsize="12"
                verticalalignment="Center" />
    </stackpanel>
</button>

Screenshot - button.jpg

您是否注意到按钮可以作为子控件的宿主?这意味着无限可能!与标准的 WinForms 按钮不同,这个 WPF 按钮与 WPF 框架中的任何其他绘制元素一样功能丰富。您是否曾经想要一个带有集成下拉列表的按钮?我也没有,但这是可能的。希望到目前为止,您已经看到了如此少的标记中蕴含的巨大力量。

自定义绘图

现在到了有趣的部分。在这里,我们将构建一个自定义控件来绘制 Spirograph。事实证明,Canvas 元素拥有绘制或动画 Spirograph 所需的所有绘图方法和上下文。让我们看看如何扩展 Canvas 对象以进行自定义绘图。

public class GraphContext : Canvas
{
    protected override void OnRender(DrawingContext drawingContext){…}
    protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo){…}
}

这里没什么特别的,只是重写 OnRender 方法可以让我们访问 DrawingContext 对象。您可以将其类比为 .NET 早期版本的 Graphics 对象。我们将使用 DrawingContext 来向画布添加自定义形状,以实现自定义渲染和动画。

我不会详细介绍如何扩展控件;这只是标准的 OOP 实践。相反,让我们看看如何将这个自定义控件添加到我们的窗口中。再一次,这只需要一点 XAML。这次,然而,我们需要使用标准的 XML 命名空间对控件添加一些定义。

<window xmlns:codeprojectexample="clr-namespace:WpfApplication1"
    x:class="WpfApplication1.GraphWindow">

<graphcontext background="Black" x:name="graphSurface"></graphcontext>

这个 xmlns 声明告诉 WPF 使用 CodeProjectExample 前缀包含 WpfApplication1 namespace 。如果您不熟悉 XML,这有点像 ASP.NET 页面的某个部分。

引用自定义元素的 XAML 再简单不过了。它是我们定义的命名空间前缀和元素的类名的简单组合。任何附加属性都可以在内联定义。在这种情况下,我将绘图区域设置为黑色。

形状

在 WPF 中绘制形状不一定比 .NET 早期版本更容易,但在 WPF 框架中提供了一些不错的便利功能,即 Shape UI 元素。由于外摆线(像任何数学图表一样)本质上是一系列连接的 XY 点,我们可以绘制一系列线条来直观地表示图表。这是生成线段的方式:

private void DrawStaticGraph(DrawingContext drawingContext)
{
    // PathGeometry is a nice alternative to drawingContext.DrawLine(...) as it
    // allows the points to be rendered as an image that can be further manipulated
    PathGeometry geometry = new PathGeometry();

    // Add all points to the geometry
    foreach (Points pointXY in _points)
    {
        PathFigure figure = new PathFigure();
        figure.StartPoint = pointXY.FromPoint;
        figure.Segments.Add(new LineSegment(pointXY.ToPoint, true));
        geometry.Figures.Add(figure);
    }

    // Add the first point to close the gap from the graph's end point
    // to graph's start point
    PathFigure lastFigure = new PathFigure();
    lastFigure.StartPoint = _points[_points.Count - 1].FromPoint;
    lastFigure.Segments.Add(new LineSegment(_firstPoint, true));
    geometry.Figures.Add(lastFigure);

    // Create a new drawing and drawing group in order to apply
    // a custom drawing effect
    GeometryDrawing drawing = new GeometryDrawing(this.Pen.Brush, this.Pen, geometry);
    DrawingGroup drawingGroup = new DrawingGroup();
    drawingGroup.Children.Add(drawing);

    ...
}

让我花点时间解释一下,有几种绘制线段的方法(特别是 DrawingContext 对象上的 DrawLine 方法),但这段代码是有目的的。我们稍后会讲到。现在,让我们注意几点。请注意,我们有一个 PathGeometry 对象,我们向其中添加了一个 PathFigure 对象集合。最后,geometry 对象被添加到 DrawingGroup。字里行间可以看出,WPF 能够一次性渲染任意数量的形状或图形。我认为这令人印象深刻。

效果

正如我所说,上面冗长的代码是有目的的。我不仅想画一个外摆线,我还想让它看起来非常流畅。这才是 WPF 真正闪耀的地方。我甚至不想去想如何在 .NET 2.0 中为单个线段添加模糊效果。这需要大量的自定义代码。本文的主题是如何简单地通过 WPF 实现惊人的视觉效果。以下是柔化整个 Spirograph 所需的三行代码。

BlurBitmapEffect blurEffect = new BlurBitmapEffect();
blurEffect.Radius = Softness;
drawingGroup.BitmapEffect = blurEffect;

Screenshot - comparison.jpg

我希望有更多的代码可以展示这个片段,但就这些了。在我看来,太美妙了!WPF 框架中有几十种效果,它们都可以在 XAML 和代码中进行自定义。在大多数情况下,只需几行代码即可将效果应用于图像的一部分或全部。要渲染图表,只需调用 Add() 方法将绘图添加到我们的自定义画布即可。就这些!

关于动画的说明

WPF 拥有大量内置功能来支持动画。不幸的是,我还没有找到任何可以连续绘制一系列线条的动画。在我找到方法之前,本示例中的动画是通过标准计时器实现的。不过,我得说,能够在运行时添加和删除画布上的渲染元素,使得 Spirograph 的动画异常简单,而且需要缓冲和重绘的整体组件更少。我没有找到我想要的动画解决方案,但我仍然很佩服!

结论

我真的很喜欢 WPF 可以如此简单地创建自定义 UI。它是一个非常强大的框架,而我们才刚刚开始探索它。

© . All rights reserved.