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

WPF Amazon Explorer 使用 3D

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (49投票s)

2008 年 1 月 14 日

CPOL

18分钟阅读

viewsIcon

195763

downloadIcon

1918

WPF Amazon Explorer 使用 3D

目录

引言

在我最近的一篇 WPF 文章《我的朋友》(VS2008 竞赛获奖作品,你懂的?)中,我使用了一些 3D WPF 的内容,我必须说我非常喜欢它。别误会,我发现它很难,但看起来非常酷。总之,在我上一篇 WPF 文章之后,我在纽约度过了一个假期,在那里我遇到了 Josh Smith(如果你有兴趣,可以在这里阅读),并且被授予了 CodeProject MVP 和 Microsoft C# MVP 奖项。这真是个好消息,我对这两个奖项都感到非常激动……但我认为一个人应该继续做好工作才能保持这些奖项。所以我克服了度假的懒散,重新戴上 WPF 手套,思考了一下,想出了一个能够浏览亚马逊并以 3D 空间表示搜索查询结果的想法。其中每个 3D 项目都代表一个搜索项结果。这些搜索项可点击,以便深入检查相关的亚马逊数据。我还认为这个想法有很多扩展的空间。

所以,简而言之,这就是本文的主要内容。

本文内容

正如我所说,我将使用亚马逊网络服务 (AWS) 来收集与查询字符串匹配的结果,这将是一个基于 WPF 的解决方案,仅此而已。很简单,是吧?

然而,为了做到这一点,我希望使用 3D 视图(因为它看起来很漂亮),所以到本文结束时,我希望您能了解以下一些(或全部)内容:

  • WPF 中的 3D 基础知识
  • WPF 中的 3D 动画基础知识
  • 在 WPF 应用中使用第三方 Web 服务
  • 处理服务时的配置工具选项
  • 使用 FlowDocuments 创建可扩展的、布局多样的界面
  • 关于样式/模板的一些知识

演示应用程序

接下来的小节将介绍我认为是演示应用程序中重要部分的内容。

功能/外观

由于此应用程序主要基于 3D,因此我认为最好展示一个运行时的短视频。为此,您可以点击下面的视频,看看它的实际效果。

点击视频:来吧,勇敢一点,你知道你想看的

亚马逊网络服务

很久以前,我在coding4fun网站上看到一篇关于使用亚马逊网络服务 (AWS)和 C# 的文章,我觉得它很不错。从那时起,我一直在玩 WPF 和一些 WCF。我是亚马逊商店的忠实粉丝,所以我想为什么不尝试使用 Web 服务创建一个漂亮的 WPF 应用呢?于是我就这么做了,而本文就是关于这个的。

使用 AWS 的第一步是actually在亚马逊网站上注册:亚马逊网络服务 (AWS)。这允许您获取一个密钥以使用 Web 服务。但别担心,我已经注册并获取了一个密钥,我**将**在附带的演示应用程序中保留它,因此您无需向亚马逊注册。但请不要滥用我的密钥。

总之,一旦您获得了 AWS 密钥,您就可以尝试在自己的应用程序中使用 AWS 代码了。随着 WCF 和 VS2008 的发布,在服务的使用方式上发生了一些变化。变化不大,但足以让我觉得我应该写一点关于如何配置 AWS 以在您的应用程序中使用。

对于使用 WCF 或 Web 服务与 VS2008 的开发者来说,有几种选项/工具可用。VS2005 与以前一样,只需添加一个 Web 引用。但既然我现在使用 VS2008,我将重点关注它。

到目前为止,最容易上手的方法是简单地使用 VS2008,并在解决方案中使用“服务引用”项,然后选择“添加服务引用”。

从这里,您可以添加任何您想要的 Web 服务的 URL。AWS 的 URL 是http://soap.amazon.com/schemas3/AmazonWebServices.wsdl,所以您只需将其输入到向导地址中,然后,您就可以开始了。

到目前为止一切顺利,不是吗?所以我们已经添加了一个 AWS 的引用,但如何在代码中使用这个 AWS 引用呢?嗯,这其实非常简单。让我们来看看。

//make sure we have the correct using statment for the service
using AmazonService;
....
....
private Details[] doAmazonSearch(string keyword, string mode)
{
    try
    {
        KeywordRequest keywordReq = new KeywordRequest();
        keywordReq.locale = "us";
        keywordReq.type = "lite";
        keywordReq.sort = "reviewrank";
        keywordReq.mode = mode;
        keywordReq.keyword = keyword;
        keywordReq.tag = this.SubscriberID;
        keywordReq.devtag = this.SubscriberID;
        AmazonSearchPortClient ams = new AmazonSearchPortClient();
        ProductInfo productInfo = ams.KeywordSearchRequest(keywordReq);
        return productInfo.Details;
    }
    catch { return null; }
}

但等等,这件事不像表面看起来那么简单,对吧?实际上,这件事确实比表面看起来要复杂得多,有两个至关重要的代码/配置部分,让我们能够简单地点击引用并开始使用 AWS。如果您不了解,您可能不知道这些细节,甚至不在乎。幸运的是,我是一个既想了解事物又关心细节的人。所以让我来告诉您更多关于这两个额外细节的信息。

它们实际上是

  • 服务配置:没有它,应用程序将不知道如何与 AWS 通信
  • 代理代码:这是我们的应用程序代码调用的

所以,我现在要做的是稍微谈谈这些项目以及它们的创建方式,以及创建这些项目的选项。

服务配置

为了让应用程序与 AWS 通信,我们需要在App.Config文件中添加一些条目。我们需要添加的配置部分与BindingEndpoint有关。在App.Config文件中创建这些部分有几种选项。这些选项的复杂程度各不相同。所以我将从最简单的开始。

选项 1:使用 VS2008

如果您使用的是 VS2008,一旦您成功添加了服务引用(如上所述),您就会(如果您查找的话)发现在App.Config中会添加两个新节(如果您还没有App.Config文件,甚至可能会创建一个全新的App.Config文件),以便应用程序能够与服务通信。让我们看看这些,好吗?

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
        <bindings>
            <basicHttpBinding>
                <binding name="AmazonSearchBinding" 
                    closeTimeout="00:01:00" 
                    openTimeout="00:01:00"
                    receiveTimeout="00:10:00" 
                    sendTimeout="00:01:00" 
                    allowCookies="false"
                    bypassProxyOnLocal="false" 
                    hostNameComparisonMode="StrongWildcard"
                    maxBufferSize="65536" 
                    maxBufferPoolSize="524288" 
                    maxReceivedMessageSize="65536"
                    messageEncoding="Text" 
                    textEncoding="utf-8" 
                    transferMode="Buffered"
                    useDefaultWebProxy="true">
                      <readerQuotas maxDepth="32" 
                        maxStringContentLength="8192" 
                        maxArrayLength="16384"
                        maxBytesPerRead="4096" 
                        maxNameTableCharCount="16384" />
                    <security mode="None">
                        <transport clientCredentialType="None" 
                            proxyCredentialType="None"
                            realm="" />
                        <message clientCredentialType="UserName" 
                          algorithmSuite="Default" />
                    </security>
                </binding>
            </basicHttpBinding>
        </bindings>
        <client>
            <endpoint address="http://soap.amazon.com/onca/soap3" 
                binding="basicHttpBinding"
                bindingConfiguration="AmazonSearchBinding" 
                contract="AmazonService.AmazonSearchPort"
                name="AmazonSearchPort" />
        </client>
    </system.serviceModel>
</configuration>

这很酷,VS2008 为我们创建了这些。很不错,是吧?但我们还有其他选择。

选项 2:使用 SVCConfig 编辑器

VS2008 中的一个新功能是 WCF 配置编辑器。好吧,它是为 WCF 而不是 Web 服务设计的,但最终,这个工具只是创建新文件或现有App.Config文件中的相关配置节。所以,我们可以利用它来配置我们的 Web 服务App.Config节。这个工具可以在 VS2008 的“工具 -> WCF SVCConfig 编辑器”菜单下找到。让我们看一些截图。

选项 3:使用 svcutil.exe

最后一个选项是使用命令行工具svcutil.exe,它能够从一个命令行生成完整的App.Config文件和代理类(下面会讨论)。使用的命令行如下。

要使用svcutil.exe完成此操作,我们可以简单地使用以下命令行:

svcutil.exe http://soap.amazon.com/schemas3/AmazonWebServices.wsdl /language:c#

代理代码

为了让我们能够与 AWS(或其他任何 Web 服务)通信,我们需要有一些代理代码,它知道如何序列化数据和调用。为此,C#|VB.NET 中的任何调用总是必须调用这个代理对象。代理只是接受我们的调用,并知道如何调用实际服务并获取正确的返回类型等。如果我们看看之前展示的生成的代理类的一小部分,例如AmazonSearchPortClient类,我们可以看到我们实际上在处理什么。

[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")]
public partial class AmazonSearchPortClient : 
    System.ServiceModel.ClientBase<AmazonService.AmazonSearchPor>, 
    AmazonService.AmazonSearchPort {
        
    public AmazonSearchPortClient() {
    }
    
    public AmazonSearchPortClient(string endpointConfigurationName) : 
            base(endpointConfigurationName) {
    }
    
    public AmazonSearchPortClient(string endpointConfigurationName, 
            string remoteAddress) : 
            base(endpointConfigurationName, remoteAddress) {
    }
    
    public AmazonSearchPortClient(string endpointConfigurationName, 
            System.ServiceModel.EndpointAddress remoteAddress) : 
            base(endpointConfigurationName, remoteAddress) {
    }
    
    public AmazonSearchPortClient(System.ServiceModel.Channels.Binding binding, 
            System.ServiceModel.EndpointAddress remoteAddress) : 
            base(binding, remoteAddress) {
    }
    
    public AmazonService.ProductInfo KeywordSearchRequest(
            AmazonService.KeywordRequest KeywordSearchRequest1) {
        return base.Channel.KeywordSearchRequest(KeywordSearchRequest1);
    }
    
    public AmazonService.ProductInfo TextStreamSearchRequest(
            AmazonService.TextStreamRequest TextStreamSearchRequest1) {
        return base.Channel.TextStreamSearchRequest(TextStreamSearchRequest1);
    }
    
    .....
    .....
    .....

但我们如何才能获得一个这样的代理对象呢?

嗯,如果您使用的是 VS2008,一旦您添加了服务引用(如上所述),您就会(如果您查找的话)发现在您的<your project>Service References\<your service ref name>\文件夹中有一个代理类。下面的屏幕截图显示了附加项目的代理。VS2008 会自动创建这个。

但是如果我们没有 VS2008 怎么办?我们可以做什么?和以前一样,svcutil.exe是生成代理类的重要工具,就像它对于配置文件一样。

要使用svcutil.exe完成此操作,我们可以简单地使用以下命令行:

svcutil.exe http://soap.amazon.com/schemas3/AmazonWebServices.wsdl /language:c#

这将生成两个项目:代理代码和一个示例App.Config(如前所述)。本质上,svcutil.exe所做的事情与 VS2008 相同,除了它不是一个友好的 GUI 界面,而是一个命令行工具。我将把哪个工具最好的争论留给您。我只是想让您知道,即使没有 VS2008 也可以做到。

如果您对svcutil.exe如何与 WCF 一起使用感兴趣,您可能还会喜欢阅读我的另一篇WPF/WCF 聊天文章

对一切的不信任

由于我正在使用第三方 Web 服务(记住,是 AWS),所以我有点谨慎。基本上,我的意思是,如果我调用亚马逊网络服务,我真的知道它将如何以及何时给我一个结果吗?我对这个问题回答“**否**”。为此,我思考了一下这个问题,并将其形式化为这个需求:“我希望能够异步调用 AWS,并在预定义的超时时间过后,我得到一个有效的结果并使用 AWS 结果,或者我向用户发出超时警告”。

我觉得这大致涵盖了我想要做的事情。这是一个相当简单的需求。让我们看看这如何转化为代码,好吗?

//delegate that will be called asynchronously to fetch Amazon Details
internal Details[] FetchAmazonDetailDelegate(string searchword, string category);
...
...
//Method called by aysychronous delegate to fetch Amazon Details
private Details[] FetchAmazonDetail(string searchword, string category)
{
    return doAmazonSearch(searchword, category);
}
...
...
try
{
    //Fetch the details asynchronously. Basically assume we will not get results quickly
    FetchAmazonDetailDelegate fetchDetails = FetchAmazonDetail;
    IAsyncResult asynchResult = fetchDetails.BeginInvoke(searchword, category,null, null);
    //wait for the results from the asynch call.
    while (!asynchResult.AsyncWaitHandle.WaitOne(5000, false))
    {
        //waiting for result for exactly 5 seconds
    }
    //get the results of the asynch call
    details = fetchDetails.EndInvoke(asynchResult);
    if (details != null)
    {
        //use the Amazon Details gathered
    }
}
//As its more than likely the web service, not much I can do about it, 
//just catch the Exception
catch
{
    MessageBox.Show("Error obtaining amazon results");
}

我认为代码相当直观。它基本上是一个异步调用,使用WaitHandle等待 5 秒钟后完成异步调用。但是,这一切都需要用try-catch块包围,因为我们可能会遇到Exception,或者EndInvoke可能会导致一些奇怪的事情发生。

3D

我希望用这个应用程序做的主要事情之一是让它看起来很酷。为此,我认为附带的演示应用程序做得很好。基本上,当输入一个新的亚马逊搜索时,会创建一些 3D 网格,并将它们随机放置在 3D 空间中。然后,对于每个网格,会保存Point3D。然后,对于每个保存的点,都会创建一个新的Point3DAnimation,用于动画化PerspectiveCamera。它非常有效。同样,一个简单的截图无法充分展示这一点。您需要查看功能/外观部分以获得更好的演示。这(部分)是基于 Lee Brimelow 的一个移动图像动画,他现在在 Adobe 工作,所以不再支持他的 WPF 网站。这很可惜,因为那位伙计懂 WPF。

总之,值得注意的重要代码如下:

private void createViewPortCamera()
{
    cam = new PerspectiveCamera();
    cam.Position = new Point3D(0,0,10);
    cam.FarPlaneDistance = 600;
    cam.NearPlaneDistance = 0.1;
    cam.FieldOfView = 90;
    cam.LookDirection = new Vector3D(0,0,-1);
    view3D.Camera = cam;
}

private void createViewPortLight()
{
    model = new ModelVisual3D();
    model.Content = new AmbientLight();
    view3D.Children.Add(model);
}
....
....
p3s = new Point3D[details.Length];
//for each Detail obtained for current search,
//create a new 3d Mesh and store
//its Point3D. To allow camera to be animated to later
for (int i = 0; i < details.Length; i++)
{
    MeshGeometry3D plMesh = 
      this.TryFindResource("planeMesh") as MeshGeometry3D;
    InteractiveVisual3D mv = new InteractiveVisual3D();
    mv.IsBackVisible = true;
    mv.Geometry = plMesh;
    mv.Visual = createAmazonDetail(details[i]);
    view3D.Children.Add(mv);
    //position item randomly in 3D space, but always ensure Z is (-)
    Matrix3D trix = new Matrix3D();
    double x = ran.NextDouble() * 50 - 50;
    double y = ran.NextDouble() * 2 - 2;
    double z = -i * 10;
    p3s[i] = new Point3D(x, y, z);
    trix.Append(new TranslateTransform3D(x, y, z).Value);
    mv.Transform = new MatrixTransform3D(trix);
}
//create animation, and bring item 1 into view
pa = new Point3DAnimation(p3s[0], TimeSpan.FromMilliseconds(300));
pa.AccelerationRatio = 0.3;
pa.DecelerationRatio = 0.3;
pa.Completed += new EventHandler(pa_Completed);
cam.BeginAnimation(PerspectiveCamera.PositionProperty, pa);
fetching = false;
dt.Tick += new EventHandler(dt_Tick);
....
....
private void dt_Tick(object sender, EventArgs e)
{

    dt.Stop();
    if (count == detailsCount-1) count = 0;
    else count++;
    pa = new Point3DAnimation(new Point3D(p3s[count].X, 
    p3s[count].Y + 0.5, p3s[count].Z + 2), TimeSpan.FromMilliseconds(500));
    pa.AccelerationRatio = 0.3;
    pa.DecelerationRatio = 0.3;
    pa.Completed += new EventHandler(pa_Completed);
    cam.BeginAnimation(PerspectiveCamera.PositionProperty, pa);
}

private void pa_Completed(object sender, EventArgs e)
{
    //need to try and catch as user may some times change the search 1/2 way 
    //through an animation and the current animation has not yet completed. 
    try
    {
        pa = new Point3DAnimation(new Point3D(p3s[count].X, 
        p3s[count].Y + 0.5, p3s[count].Z + 1.6), TimeSpan.FromMilliseconds(3100));
        pa.Completed += new EventHandler(dt_Tick);
        cam.BeginAnimation(PerspectiveCamera.PositionProperty, pa);
    }
    catch { }
}

3D 工具

正如我在本文开头提到的,我过去曾见过一个用 C# 编写的亚马逊搜索应用程序。但那是 WinForms,而这是 WPF。所以我想为什么不全力以赴。为此,我不仅使用了上面描述的 3D,而且还允许 3D 视口进行以下操作:

  • 轨迹球 - 倾斜/缩放
  • 3D 表面的 2D 对象交互

这两个很棒的功能都得益于出色的3dTools.Dll,它就在这里,并且是由 WPF 3D 团队创建的。

轨迹球

轨迹球非常棒;您只需将其包裹在您的ViewPort3D控件周围……基本上,就是托管 3D 模型的地方。

<!-- 3D Viewport, wrapped up in some of the nice 3DTools classes 
     to allow 2D UI elements on 3D and allow trackball functions
     in 3D space -->
<inter3D:TrackballDecorator x:Name="inter3d" 
         DockPanel.Dock="Bottom" Height="Auto">
    <inter3D:Interactive3DDecorator>
        <Viewport3D x:Name="view3D"/>
    </inter3D:Interactive3DDecorator>
</inter3D:TrackballDecorator>

通过使用这几行代码,您可以倾斜和缩放 3D 视口。左键执行倾斜,右键执行缩放。我在这里放了一张截图,但这真的不足以展示其全部效果。您应该查看功能/外观部分中的视频以获得更好的演示。

3D 表面的 2D 对象交互

将 2D 控件放置在 3D 表面上的能力对我来说非常有吸引力。这意味着我可以(并且确实如此)通过一个 3D 网格的 3D 视口来动画化一个摄像机,其中每个网格都包含一个标准的 2D UIElement,例如用户可以与之交互的 StackPanel。这正是我所做的。我允许用户点击 3D 网格中的 2D UIElement,然后会启动详细信息窗口(FlowDocumentWindow),该窗口包含接下来将要讨论的 FlowDocument。让我们看一些代码。

MeshGeometry3D plMesh = this.TryFindResource("planeMesh") as MeshGeometry3D;
InteractiveVisual3D mv = new InteractiveVisual3D();
mv.IsBackVisible = true;
mv.Geometry = plMesh;
mv.Visual = createAmazonDetail(details[i]);
view3D.Children.Add(mv);
....
....
....
private StackPanel createAmazonDetail(Details amazonDetail)
{
    StackPanel sp = new StackPanel();
    sp.Background = Brushes.Transparent;
    AmazonItem item = new AmazonItem(amazonDetail);
    item.ItemClicked += 
      new AmazonItem.AmazonItemClickedEventHandler(item_ItemClicked);
    sp.Children.Add(item);
    return sp;
}

很简单,对吧?我们现在在 3D 网格上拥有了一个 2D 可交互的 2D UIElement。很棒。不过,我还可以使用另一种方法,那就是使用新的 .NET 3.5 Viewport2DVisual3D 类。这个新类允许您创建一个 3D 模型,但它也允许您在 3D Material 上托管一个 2D UIElement,并且 2D UIElement 是完全可交互的。所以,它的功能与3dTools.dll类相同,但用户需要安装 .NET 3.5。我选择了3dTools.dll类,因为它是我以前没用过的,我想尝试一下。如果您想了解更多关于 .NET 3.5 Viewport2DVisual3D 类的信息,我创建了一个小型演示应用程序,可以从我的博客这里获取。

LINQ 和反射

正如我在本文中所述,每次对 AWS 代理的搜索都会产生一个搜索结果,形式为 Details[] 对象数组。可能不明显的一点是,演示应用程序允许执行三种不同的搜索类型:书籍/DVD/视频。因此,填充有效数据的公共属性可能会有所不同,具体取决于正在执行的搜索类型。例如,DVD 不会有作者。而且,由于我只想显示当前查看的亚马逊详细信息对象具有有效数据的属性,因此我需要一种通用的方法来仅获取具有数据的属性。我无法在循环中迭代字段集合,因为我不知道哪些字段适用于哪种搜索类型,而且我也无法保证哪些属性实际上会包含值。

所以我思考了一下;当然,我可以使用一些 LINQ,不是吗?LINQ 用于查询内联集合。Details[]对象数组,正是所需。但由于这是 AWS,没有 LINQ 扩展方法可用。总之,即使有,LINQ 也只能帮我完成一半的任务。它存在与使用循环相同的固有问题;我需要知道要选择哪些属性,而我只想要那些非 null 或非空的值。

所以我又思考了一下……反射来帮忙。我可以简单地结合使用 LINQ/反射来查询声明Type上那些不为 null 或为空的属性。搞定。

这是代码片段:

Details det = AmazonDetail;
Type amazonType = det.GetType();
//get all public and instance fields only
PropertyInfo[] props = amazonType.GetProperties(
    BindingFlags.Public | BindingFlags.Instance);
//now use some LINQ with some added reflection for good measure
//to obtain the fields from the Amazon details that arent null
var nonNullProps  =     
  (   from prop in props where
            prop.GetValue(det, null) != null &&
            !prop.GetValue(det, null).ToString().EndsWith("[]")
            select new AmazonParameterDetail
                 {
                     PropertyName = prop.Name,
                     PropertyValue = prop.GetValue(det, null).ToString()
                 }
        );
....
....
//Now I can use a loop, safe in the knowledge
//I only have those properties for the current
//Amazon Detail that aren't null or empty
foreach (AmazonParameterDetail nonNullprop in nonNullProps)
{
    ....
    ....
}

FlowDocument

System.Windows.Documents.FlowDocument 是一个用户控件,可以放置在Window中的多个FlowDocument读取器控件内。FlowDocument控件本身相当整洁,允许开发者创建类似 HTML 的设计布局。例如,我们可以创建一个FlowDocument,其中可以包含表格/图片/段落/超链接等。

有几种 WPF 容器控件可以托管FlowDocument。这些 WPF 容器控件提供的功能各不相同。让我们看看它们之间的区别,好吗?

  • FlowDocumentScrollViewer:简单地显示整个文档并提供滚动条。像网页一样。
  • FlowDocumentPageViewer:将文档显示为单个页面,并允许用户调整缩放级别。
  • FlowDocumentReader:将FlowDocumentScrollViewerFlowDocumentPageViewer合并到一个控件中,并提供文本搜索功能。

例如,FlowDocumentPageViewer如下所示:

对于那些没有接触过FlowDocument的人来说,这里列出了一些可以用它做的事情:

  • 允许分段
  • 允许锚定图像
  • 允许超链接
  • 允许文本块
  • 允许表格
  • 允许下标/上标文本
  • 允许UIElements(例如Button等)
  • 允许文本效果

FlowDocuments想象成一个迷你桌面排版类型的界面。不过,我相信像 Quark 这样的工具会提供更多的灵活性。尽管如此,FlowDocuments 的结果可以被认为是能够创建那种页面发布类型的布局。我现在要做的是向您展示如何在 XAML 和代码中创建几个不同的FlowDocument元素,因为它们实际上有点不同。

段落

在 XAML 中
<Paragraph FontSize="11">
    This page is a simple FlowDocument that is part 
    of the Windows.Document namespace, and it
    has been included in this application, simply 
    to show how easy it is to create simple Documents
    which have Paragrpahs/Links/Images and can be scaled 
    up/down using the FlowDocumentReader control.
    This only really touches the surface of what you 
    can do with FlowDocument(s) in WPF, you can also
    use all sort of text effects, like subscript/superscript/underline. 
    You can also use tables. In fact
    with FlowDocument(s) you can acheive some pretty slick 
    looking documents. At least this should give you a 
    a flavour of what can be done. I hope.
</Paragraph>
在 C# 后台代码中
Paragraph paraHeader = new Paragraph();
paraHeader.FontSize = 12;
paraHeader.Foreground = headerBrsh;
paraHeader.FontWeight = FontWeights.Bold;
paraHeader.Inlines.Add(new Run("Paragraph Text"));
flowDoc.Blocks.Add(paraHeader);

超链接

在 XAML 中
<Paragraph FontSize="11">
     <Hyperlink  Click="hl_Click" 
       NavigateUri="www.google.com">Click Here</Hyperlink>
</Paragraph>
在 C# 后台代码中
Paragraph paraValue = new Paragraph();
Hyperlink hl =new Hyperlink(new Run("click here"));
hl.Click += new RoutedEventHandler(hl_Click);
paraValue.Inlines.Add(hl);
flowDoc.Blocks.Add(paraValue);

嵌入 UI 元素

在 XAML 中
<BlockUIContainer>
    <Button Width="60" Height="60" Click="Button_Click">
        Click me
    </Button>
</BlockUIContainer>
在 C# 后台代码中
BlockUIContainer uiCont = new BlockUIContainer();
Button b = new Button();
b.Width = b.Height = 60;
b.Click += new RoutedEventHandler(Button_Click);
b.Content ="Click me"
uiCont.Child = b; 
flowDoc.Blocks.Add(uiCont);

在演示应用程序中,我构建了一个WindowFlowDocumentWindow),其中嵌入了一个FlowDocument,该FlowDocument在用户点击主WindowExplorer3D)中的一个 3D 亚马逊项目时显示。上面提到的 LINQ/反射用于确保只显示当前亚马逊Details对象中适用、非空或 null 的属性。

当然,这只是FlowDocument功能的冰山一角。但这让您对 WPF 文档格式化的灵活性有了初步了解。

收藏夹

从演示应用程序中使用的FlowDocument,可以向收藏夹ItemsControl(其模板稍后讨论)添加一个项目。工作方式是,当点击FlowDocument上的收藏夹(星形)Button时,一个新的AmazonParameterDetail对象(包含亚马逊Details)会被添加到声明在应用程序主WindowExplorerWindow)中的内部ObservableCollection<AmazonFavourite>favouriteDataItems字段中。由于该集合是ObservableCollection,因此一旦集合发生更改,收藏夹ItemsControl就会更新。

收藏夹项目显示在一个可滚动的ItemsControl中,当鼠标悬停在主窗口标题区域下方的灰色条上时,它就会显示出来。

可以看到,ScrollViewer不是标准的ScrollViewer。这是因为应用了自定义Style。您可以在下方阅读有关此内容的信息,或者查看我的博客文章以获取更多详细信息:ScrollViewer 样式

样式 / 模板

在这个应用程序中,我只在几个地方使用了样式/模板。以下列表概述了这些区域:

  • 收藏夹 Items Control DataTemplate
  • 收藏夹 Items OrangeGelButton ControlTemplate
  • 收藏夹 Item CloseButton ControlTemplate
  • 收藏夹区域 ScrollViewer 控件 Style

我知道这可能代码量很大,但我会列出所有的DataTemplate/ControlTemplateStyle,这样人们就能知道它们分别是哪些。有时在 XAML 中很难看清楚所有这些。

收藏夹区域 ScrollViewer 控件样式

整个收藏夹ItemsControl被包裹在一个ScrollViewer控件中,不久前,我看了 Infragistics WPF 展品Tangerine,我非常羡慕他们使用的滚动条。我的意思是,样式化一个按钮是一回事,但滚动条由许多棘手的不同控件部分(Part_XXX 元素)组成。但总之,我决定尝试一下。代码当然包含了完成这项工作所需的所有 XAML。在这里列出太多了,但如果您对这个Style确实感兴趣,您可以在我的博客文章中阅读更多内容:ScrollViewer 样式。总之,基本思想是它改变了滚动条的外观。

应用Style后的结果如下所示;左侧是新的ScrollViewer外观。

收藏夹 Items Control DataTemplate

看起来是这样的:

<!-- This is where the look and feel of the items is defined for the
     icFavourites ItemsControl -->
<DataTemplate x:Key="favItemsTemplate">
    <Button Content="{Binding Price}" 
           VerticalAlignment="Top" HorizontalAlignment="Left"
           Padding="3" Width="130" 
           Height="30" FontFamily="Arial Rounded MT" 
           FontSize="12" FontWeight="Normal" 
           Foreground="#FFEF3800"
           Template="{StaticResource OrangeGelButton}"
           Margin="5,5,5,5"
           Click="btnFavMain_Click">
        <Button.ToolTip>
            <Border Background="White" 
                    CornerRadius="5,5,5,5" Width="200">
                <DockPanel Width="Auto" 
                        Height="Auto" LastChildFill="True">
                    <Label Margin="2,2,2,2" 
                       VerticalAlignment="Top" 
                       Width="Auto" Height="Auto" 
                       Content="Amazon Favourite" 
                       Background="#FF000000" 
                       FontFamily="Arial Rounded MT" 
                       FontSize="14" Foreground="#FFFFFFFF" 
                       DockPanel.Dock="Top"/>
                    <TextBlock Margin="2,2,2,2" 
                          Width="Auto" Height="Auto" 
                          TextWrapping="Wrap">
                        <Run Language="en-gb">You have saved this Amazon 
                            item as a favourite. You can click it to re open it, 
                            or click on the close button to delete it from the 
                            favourites list</Run>
                    </TextBlock>
                </DockPanel>
            </Border>
        </Button.ToolTip>
    </Button>
</DataTemplate>

其结果如下(请注意,此DataTemplate还使用了OrangeGelButtonControlTemplate)。

收藏夹 Item OrangeGelButton ControlTemplate

看起来是这样的:

<!-- Gel Button Template For Amazon Favourite -->
<ControlTemplate x:Key="OrangeGelButton" TargetType="Button">
    <Grid Background="#00FFFFFF">
        <Border BorderBrush="#FF000000" CornerRadius="6,6,6,6" 
            BorderThickness="1,1,0,0" Opacity="0.9">
            <Border.BitmapEffect>
                <BlurBitmapEffect Radius="1" />
            </Border.BitmapEffect>
        </Border>
        <Border BorderBrush="#FFFFFFFF" CornerRadius="6,6,6,6" 
            BorderThickness="0,0,0.6,0.6" Opacity="0.7" />
        <Border Margin="1,1,1,1" CornerRadius="6,6,6,6" Name="background">
            <Border.Background>
                <LinearGradientBrush EndPoint="0,1" StartPoint="0,0">
                    <LinearGradientBrush.GradientStops>
                        <GradientStop Offset="0" Color="#FFFBD19E" />
                        <GradientStop Offset="1" Color="#FFF68F15" />
                    </LinearGradientBrush.GradientStops>
                </LinearGradientBrush>
            </Border.Background>
            <Grid Margin="1,1,1,1" ClipToBounds="True">
                <Grid.RowDefinitions>
                    <RowDefinition Height="*" />
                    <RowDefinition Height="*" />
                </Grid.RowDefinitions>
                <Rectangle Width="{TemplateBinding FrameworkElement.Width}" 
                    Fill="#FFFFFFFF" Opacity="0.34" 
                    Grid.Row="0" />
            </Grid>
        </Border>
        <Border Margin="1,1,1,1" 
                BorderBrush="#FFFFFFFF" CornerRadius="6,6,6,6" 
                BorderThickness="0,0,0,0" Opacity="0.3">
            <Border.BitmapEffect>
                <BlurBitmapEffect Radius="1" />
            </Border.BitmapEffect>
        </Border>
        <Border Margin="1,1,1,1" 
                BorderBrush="#FF000000" CornerRadius="6,6,6,6" 
                BorderThickness="0,0,0.6,0.6" Opacity="1">
            <Border.BitmapEffect>
                <BlurBitmapEffect Radius="1" />
            </Border.BitmapEffect>
        </Border>
        <Image Source="resources/Amazon.png" 
            Width="60" Height="11" Stretch="Fill" 
            HorizontalAlignment="Right" 
            VerticalAlignment="Top" Margin="5,5,50,5"/>
        <ContentPresenter Margin="5,13,5,5" 
            HorizontalAlignment="Left" 
            VerticalAlignment="Top" 
            ContentTemplate="{TemplateBinding ContentControl.ContentTemplate}" 
            Content="{TemplateBinding ContentControl.Content}" />
        <Button x:Name="btnSub" 
            Click="btnDeleteFavourite_Click" 
            HorizontalAlignment="Right" 
            Content="X" Margin="0,0,5,0" 
            Width="20" Height="20" 
            FontFamily="Arial Rounded MT" 
            FontSize="11" FontWeight="Normal" 
            Template="{StaticResource CloseButton}" />
    </Grid>
    <ControlTemplate.Triggers>
        <Trigger Property="UIElement.IsMouseOver" Value="True">
            <Trigger.ExitActions>
                <BeginStoryboard>
                    <Storyboard>
                        <Storyboard.Children>
                            <ColorAnimation To="#FFFBD19E" 
                               FillBehavior="HoldEnd" 
                               Duration="00:00:00.4000000" 
                               Storyboard.TargetName="background" 
                               Storyboard.TargetProperty=
                                 "(Panel.Background).(GradientBrush.
                                  GradientStops).[0].(GradientStop.Color)" />
                            <ColorAnimation To="#FFF68F15" 
                               FillBehavior="HoldEnd" 
                               Duration="00:00:00.4000000" 
                               Storyboard.TargetName="background" 
                               Storyboard.TargetProperty=
                                 "(Panel.Background).(GradientBrush.
                                  GradientStops).[1].(GradientStop.Color)" />
                        </Storyboard.Children>
                    </Storyboard>
                </BeginStoryboard>
            </Trigger.ExitActions>
            <Trigger.EnterActions>
                <BeginStoryboard>
                    <Storyboard>
                        <Storyboard.Children>
                            <ColorAnimation To="#FFFAF688" 
                              FillBehavior="HoldEnd" 
                              Duration="00:00:00.2000000" 
                              Storyboard.TargetName="background" 
                              Storyboard.TargetProperty=
                                "(Panel.Background).(GradientBrush.
                                 GradientStops).[0].(GradientStop.Color)" />
                            <ColorAnimation To="#FFF6D415" 
                              FillBehavior="HoldEnd" 
                              Duration="00:00:00.2000000" 
                              Storyboard.TargetName="background" 
                              Storyboard.TargetProperty=
                                "(Panel.Background).(GradientBrush.
                                 GradientStops).[1].(GradientStop.Color)" />
                        </Storyboard.Children>
                    </Storyboard>
                </BeginStoryboard>
            </Trigger.EnterActions>
        </Trigger>
        <Trigger Property="ButtonBase.IsPressed" Value="True">
            <Trigger.ExitActions>
                <BeginStoryboard>
                    <Storyboard>
                        <Storyboard.Children>
                            <ColorAnimation To="#FFFAF688" 
                              FillBehavior="Stop" 
                              Duration="00:00:00.4000000" 
                              Storyboard.TargetName="background" 
                              Storyboard.TargetProperty=
                                "(Panel.Background).(GradientBrush.
                                 GradientStops).[0].(GradientStop.Color)" />
                            <ColorAnimation To="#FFF6D415" 
                              FillBehavior="Stop" 
                              Duration="00:00:00.4000000" 
                              Storyboard.TargetName="background" 
                              Storyboard.TargetProperty=
                                "(Panel.Background).(GradientBrush.
                                 GradientStops).[1].(GradientStop.Color)" />
                        </Storyboard.Children>
                    </Storyboard>
                </BeginStoryboard>
            </Trigger.ExitActions>
            <Trigger.EnterActions>
                <BeginStoryboard>
                    <Storyboard>
                        <Storyboard.Children>
                            <ColorAnimation To="#FFFAA182" 
                              FillBehavior="HoldEnd" 
                              Duration="00:00:00.2000000" 
                              Storyboard.TargetName="background" 
                              Storyboard.TargetProperty=
                                "(Panel.Background).(GradientBrush.
                                 GradientStops).[0].(GradientStop.Color)" />
                            <ColorAnimation To="#FFFD6420" 
                              FillBehavior="HoldEnd" 
                              Duration="00:00:00.2000000" 
                              Storyboard.TargetName="background" 
                              Storyboard.TargetProperty=
                                "(Panel.Background).(GradientBrush.
                                 GradientStops).[1].(GradientStop.Color)" />
                        </Storyboard.Children>
                    </Storyboard>
                </BeginStoryboard>
            </Trigger.EnterActions>
        </Trigger>
    </ControlTemplate.Triggers>
</ControlTemplate>

其结果如下(请注意,此DataTemplate还使用了CloseButtonControlTemplate)。

收藏夹 Item CloseButton ControlTemplate

看起来是这样的:

<!-- Close Button Used As Part Of Gel Button Template For Amazon Favourite -->    
<ControlTemplate x:Key="CloseButton" TargetType="Button">

    <Border Opacity="0.5" Name="bord" Margin="0" 
                    Width="{TemplateBinding Width}" 
                    Height="{TemplateBinding Height}"  
                    BorderBrush="#FF000000" 
                    BorderThickness="2,2,2,2" 
                    CornerRadius="5,5,5,5" 
                    Padding="0,0,0,0" 
                    Background="#FFFFFFFF">
        <ContentPresenter Margin="{TemplateBinding Control.Padding}" 
                    HorizontalAlignment="Center" 
                    VerticalAlignment="Center" 
                    ContentTemplate=
                      "{TemplateBinding ContentControl.ContentTemplate}" 
                    Content="{TemplateBinding ContentControl.Content}" />
    </Border>

    <ControlTemplate.Triggers>
        <Trigger Property="IsMouseOver" Value="True">
            <Setter TargetName="bord" 
              Property="Background" Value="#FFFC0C0C"/>
            <Setter TargetName="bord" 
              Property="Opacity" Value="1.0"/>
        </Trigger>
    </ControlTemplate.Triggers>

</ControlTemplate>

其结果如下:

已知问题

偶尔会因空引用而崩溃,但在 VS2008 中没有抛出异常,并且InnerExceptionnull;没有任何消息表明错误是如何、在哪里或如何引发的。因此,我无法追踪它。所以如果有人找到了这个罪魁祸首,请告诉我,我会修复代码。

历史

  • v1.1:2008/02/27:样式中的代码更改。
  • v1.0:2008/01/12:初始发布。
© . All rights reserved.