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

Flexibox – Silverlight 的 Lightbox 替代品

starIconstarIconstarIconstarIconemptyStarIcon

4.00/5 (4投票s)

2010年3月10日

Ms-PL

8分钟阅读

viewsIcon

33392

downloadIcon

216

Flexibox 是 Lightbox 的替代品,无需弹出层即可显示图像的多种分辨率。Flexibox 展示了 Silverlight 应用程序如何随页面自身调整大小。

引言

精美的照片需要大尺寸才能得到最佳欣赏,但较小的图像尺寸最适合放在文本块中或作为缩略图集合。为了解决这个设计难题,缩略图通常包含一个链接,用于查看同一图像的较大版本。如果这会将用户带到另一个页面,则很不方便,因为用户必须返回到原始页面才能继续。

一个流行的替代方案是使用 JavaScript 库(如 Lightbox)在当前页面上显示一个覆盖图像。这在初次使用时会给人留下深刻的印象。但是,覆盖层会隐藏页面的其余部分。分散注意力和花费的时间会阻止用户查看图像的较大版本。

Flexibox 像 <img> 标签一样,在 HTML 页面中显示单个图像。Flexibox 可以更改其显示的图像为不同尺寸的图像,并自动在页面内调整自身大小。这使用户无需离开页面即可从缩略图切换到高分辨率大图像。

Flexibox 在网页上显示就像任何缩略图一样,但在右上角覆盖了一个放大按钮,以指示用户存在一个更大的版本。

当用户单击放大按钮时,Flexibox 会立即放大,导致周围的页面元素重新格式化以环绕现在更大的控件。

要实时查看 Flexibox,请访问 http://ithinkly.com/demo/flexibox/

代码

该代码演示了许多有用的 Silverlight 技术

  • 将参数从 HTML 页面传递到 Silverlight
  • 将 CSS 颜色字符串转换为 Silverlight Color 结构
  • 加载相对于托管页面的内容
  • 异步加载和缓存图像
  • 调整页面内 Silverlight 控件的大小

对于一个看似非常简单的应用程序,Flexibox 包含的代码量比这里介绍的要多得多,因此只介绍以上列出的技术。提供了完整的源代码,以便您可以完整地研究所有细节。该项目是使用 VS 2010 开发的,目标是 Silverlight 3。

HTML

首先要研究的是 Flexibox 控件的 HTML 与标准 HTML 的不同之处。

<div style="margin-right: 8px; margin-bottom: 3px;float: left;">
 <object data="data:application/x-silverlight-2," 
        type="application/x-silverlight-2" 
        width="240px" height="161px">
  <param name="source" value="ClientBin/FlexiBox.xap"/>
  <param name="onError" value="onSilverlightError" />
  <param name="background" value="white" />
  <param name="minRuntimeVersion" value="3.0.40818.0" />
  <param name="autoUpgrade" value="true" />
  <param name="initParams" 
    value="border=2,border_color=#444,preload=true,mode=small,
            thumb_url=images/s1_100.jpg,medium_url=images/s1_500.jpg,
            large_url=images/s1_1024.jpg"  />
  <a href="http://go.microsoft.com/fwlink/?LinkID=149156&v=3.0.40818.0" 
                   style="text-decoration:none">
    <img src="http://go.microsoft.com/fwlink/?LinkId=161376" 
         alt="Get Microsoft Silverlight" style="border-style:none"/>
  </a>
</object><iframe id="_sl_historyFrame" 
  style="visibility:hidden;height:0px;width:0px;border:0px"></iframe></div>

object 标签以固定的像素宽度和高度给出。这些属性稍后将在 Silverlight 中更改。环绕 object 标签的 div 标签使用内联样式设置边距并将控件浮动到周围文本的左侧。您可以在此处使用几乎任何样式,但宽度或高度除外。此外,请勿为 div 标签设置 ID 或类样式,因为默认的 Visual Studio 生成页面会这样做。

initParams param 标签包含要传递给 Silverlight 控件的参数。上面的示例设置了边框的宽度和颜色,并传递了四张图片。图像 URL 相对于页面,就像在 img 标签中使用一样。mode 设置为 small,因此首先显示小图像。

Flexibox XAML

应用程序中只有一个控件,定义如下

<Border BorderBrush="{Binding Path=BorderBrush}" 
        BorderThickness="{Binding Path=BorderThickness}" Name="border1"  >
    <Grid x:Name="LayoutRoot" >
        <Image  Name="image1" Stretch="None" Source="{Binding Path=DisplayedImage}" />
        <controlsToolkit:BusyIndicator Height="59" 
             HorizontalAlignment="Center"  Name="busyIndicator1" 
             VerticalAlignment="Center" Width="151" 
             IsBusy="{Binding Path=IsBusy}" BusyContent="{Binding Path=BusyStatus}" 
             DisplayAfter="00:00:02.1000000" IsEnabled="True" />
        <StackPanel Height="25" HorizontalAlignment="Right" 
                Margin="0,2,2,0" Name="stackPanelButtons" 
                VerticalAlignment="Top"  Orientation="Horizontal">
            <Button Style="{StaticResource ButtonIcon}" 
                Click="Contract_Button_Click" Name="ContractButton" 
                RenderTransform="{StaticResource ButtonBottomLeft}" 
                Visibility="{Binding Path=ContractVisible}" />
            <Button Style="{StaticResource ButtonIcon}" 
               Click="Expand_Button_Click" Name="ExpandButton" 
               RenderTransform="{StaticResource ButtonTopRight}" 
               Visibility="{Binding Path=ExpandVisible}" />
        </StackPanel>
    </Grid>
</Border>

正如您所预料的那样,有一个 Border,一个 Image 控件,以及一个用于加载第一张图像时的忙碌指示器。StackPanel 用于对齐两个按钮,这两个按钮仅在可用时才会显示。按钮只是重新设置了样式。有关详细信息,请参阅 styles.xaml 文件。所有动态元素都通过数据绑定到模型属性来控制。

代码隐藏文件

public partial class MainPage : UserControl
{
    FlexiBoxViewModel viewModel = new FlexiBoxViewModel();

    public MainPage()
    {
        InitializeComponent();
        DataContext = viewModel;
    }
    private void Contract_Button_Click(object sender, RoutedEventArgs e)
    {
        viewModel.Contract();
    }
    private void Expand_Button_Click(object sender, RoutedEventArgs e)
    {
        viewModel.Expand();
    }
}

可以看出,所有有趣的东西都在 viewModel 中。我们需要处理按钮点击的事件处理程序,因为我们使用的是 Silverlight 3。在 Silverlight 4 中,我们可以删除这些处理程序,直接在 XAML 中使用命令。

App.xaml.cs 文件仅包含生成的代码,因此无需查看。

从 HTML 页面读取参数

foreach (string key in Application.Current.Host.InitParams.Keys)
    ParseExternalParam(key, Application.Current.Host.InitParams[key]);

应用程序宿主包含传递给控件的所有参数的字典,可以逐个处理。

private void ParseExternalParam(string key, string value)
{
    try
    {
    ...
        else if (String.Compare("thumb_url", key, 
                 StringComparison.InvariantCultureIgnoreCase) == 0)
        {
            images[(int)FlexiImageModel.Size.thumb].Url = value;
        }
        else if (String.Compare("border", key, 
                 StringComparison.InvariantCultureIgnoreCase) == 0)
        {
            borderSize = Double.Parse(value);
        }
        else if (String.Compare("border_color", key, 
                 StringComparison.InvariantCultureIgnoreCase) == 0)
        {
            borderColor = value.ToColor();
        }
    }
    catch
    {
        // contain any exceptions thrown by invalid params
    }
}

检查每个参数键是否匹配,然后将值转换为适当的类型并存储以供以后使用。与代码中的其他地方一样,会捕获异常并安全地忽略它们。如果不这样做,为边框参数输入无效数字会导致向用户显示一个恼人的错误消息。这可能就是您想要的,在这种情况下,请删除 try-catch,但我更喜欢应用程序静默失败并继续,就像 XAML 找到错误时一样。

与 .NET 的完整版本不同,Silverlight 中没有内置的方法可以将颜色的字符串表示转换为颜色对象。在过去的两年里,许多人都必须解决这个问题,并且网上有很多例子,但大多数只处理一种格式的十六进制字符串。我想要一个可以处理所有 CSS 颜色格式的颜色字符串转换器。例如:'#FFFFFF'、'#FFF'、'white'。

所以我写了以下实用类

public static class TWSilverlightUtilities
{
    // Converts a CSS style string into Color structure.
    // Returns black if the input is invalid.
    // The color string to convert. May be in one
    // of the following formats; #FFFFFFFF, #FFFFFF, #FFF, white.
    public static Color ToColor(this string str)
    {
        Color rv = Color.FromArgb(0xFF,0,0,0);

        try
        {
            rv = str.ToColorEx();
        }
        catch
        {
        }

        return rv;
    }

    public static Color ToColorEx(this string str)
    {
        // empty string check
        if ((str == null) || (str.Length == 0))
            throw new Exception("empty color string");

        // This is the only way to access the colors that XAML knows about!
        String xamlString = "<Canvas xmlns=\"http://schemas." + 
                            "microsoft.com/winfx/2006/xaml/" + 
                            "presentation\" Background=\"" + 
                            str + "\"/>";
        Canvas c = (Canvas)System.Windows.Markup.XamlReader.Load(xamlString);
        SolidColorBrush brush = (SolidColorBrush)c.Background;

        return brush.Color;
    }
}

我真的很想把它写成 Color 的扩展方法,这样它就可以很好地匹配 ToString 方法。不幸的是,Color 是一个结构体,扩展方法在结构体上无法正常工作,因为 'this' 参数只能按值传递而不能按引用传递,这使得它对结构体来说是无用的。另外,您不能定义一个静态扩展方法,只能实例化一个。在我看来,这是语言中的两个严重疏漏。

加载相对于托管页面的内容

要显示的图像的 URL 可以是绝对的,也可以是相对于网页的。Silverlight 控件可能托管在完全不同的位置,因此必须使用正确的地址,该地址是从 HtmlPage.Document.DocumentUri 获取的。然后,只需剥离页面名称、查询和片段,并附加相对图像 URL,即可获得所需的图像的绝对 URL。

private Uri GetImageUri(FlexiImageModel.Size size)
{
    Uri imgUri = new Uri(images[(int)size].Url, 
                     UriKind.RelativeOrAbsolute);

    if (imgUri.IsAbsoluteUri)
        return imgUri;

    // Make an absolute URL relative to the document we are in
    UriBuilder pageUri = new UriBuilder(HtmlPage.Document.DocumentUri);
    pageUri.Fragment = null;
    pageUri.Query = null;
    int n = pageUri.Path.LastIndexOf('/');
    if (n > 0)
        pageUri.Path = pageUri.Path.Substring(0, n + 1);

    return new Uri(pageUri.Uri, imgUri);
}

异步加载和缓存图像

每个 Flexibox 最多可以容纳四张图像:缩略图、小图、中图和大图。这些图像可以按需一张一张地加载,或者全部预加载。预加载功能使用户可以即时访问不同尺寸的图像,但会消耗更多带宽,因此每种选项都有其用途。预加载时,最重要的是首先加载要显示的图像。只有当该图像下载完成后,才应下载并缓存其他图像。用户可能在下载下一个图像时单击展开按钮,代码需要能够应对这种情况。图像的缓存是纯 C#,而不是 Silverlight 技术,因此此处不作详细介绍,但包含在附带的源代码中。

private void RetrieveImage(FlexiImageModel.Size size)
{
    try
    {
        // Dont start a second retrieve of the same image
        if (!images[(int)size].IsRetrieving)
        {
            Uri uri = GetImageUri(size);
            images[(int)size].IsRetrieving = true;
            WebClient webClient = new WebClient();
            webClient.OpenReadCompleted += 
              new OpenReadCompletedEventHandler(webClient_OpenReadCompleted);
            webClient.OpenReadAsync(uri, size);
        }
    }
    catch (Exception ex)
    {
        images[(int)size].IsError = true;
    }
}

要下载图像,首先按照上述方法获取其完整 URL,然后使用 WebClient 异步启动下载,以免阻塞用户界面。开始操作时,我们会传入图像的大小,以便在完成时知道哪个图像已下载。

void webClient_OpenReadCompleted(object sender, OpenReadCompletedEventArgs e)
{
    FlexiImageModel.Size size = newSizeMode;
    if (e.UserState is FlexiImageModel.Size)
        size = (FlexiImageModel.Size)e.UserState;

    try
    {
        if ((e.Cancelled) || (e.Error != null))
            throw new Exception("Error downloading image");

        BitmapImage bitmap = new BitmapImage();
        bitmap.SetSource(e.Result);
        e.Result.Close();
        images[(int)size].Bitmap = bitmap;
        images[(int)size].IsError = false;
    }
    catch
    {
        images[(int)size].IsError = true;
        BusyStatus = "Error loading image";
    }

    images[(int)size].IsRetrieving = false;

    LoadImage();

    RetrieveNextImage();
}

WebClient 在如何接收下载数据方面有几个选项。通过使用 OpenReadAsync,它在 e.Result 中提供了一个打开的流,非常适合设置为新 BitmapImage 对象的源。当然,事情可能会出错,任何错误都会被捕获,图像会在缓存中标记为错误。

一旦图像被缓存,Flexibox 就可以将其加载到用户控件中,然后开始下载下一个图像。

将图像加载到用户界面

private void LoadImage()
{
    if ( ((imageLoaded) && (sizeMode == newSizeMode)) ||
        (images[(int)newSizeMode].IsError) || (!images[(int)newSizeMode].IsLoaded))
        return; // nothing to do

    sizeMode = newSizeMode;
    imageLoaded = true;

    ContractVisible = (sizeMode != NextSmallerImage(sizeMode)) ? 
                       Visibility.Visible : Visibility.Collapsed;
    ExpandVisible = (sizeMode != NextBiggerImage(sizeMode)) ? 
                     Visibility.Visible : Visibility.Collapsed;

    ResizeControl();

    NotifyPropertyChanged("DisplayedImage");
    IsBusy = false;
}

该方法首先检查错误,以及所需图像是否已加载,然后再继续执行其余操作。

“展开”和“收起”按钮需要根据是否有更大的或更小的图像可用来打开或关闭。

public Visibility ExpandVisible { get { return expandVisible; } 
       set { expandVisible = value; NotifyPropertyChanged("ExpandVisible"); } }
public Visibility ContractVisible { get { return contractVisible; } 
       set { contractVisible = value; NotifyPropertyChanged("ContractVisible"); } }

这是通过设置 XAML 定义的按钮绑定的两个公共属性来完成的。

public BitmapImage DisplayedImage { get { return images[(int)sizeMode].Bitmap; } }

XAML 定义的图像绑定到 DisplayedImage 属性,该属性直接从缓存中检索图像。但是,图像不会更新,因为它需要被告知选定的图像已更改。这是通过在确切需要的时间调用 NotifyPropertyChanged 来完成的,即在控件首次调整大小以适应新图像之后。

调整页面内 Silverlight 控件的大小

现在是最后一种技术,也是这个控件真正重点所在的技术:调整页面内 Silverlight 控件的大小。我要将这项技术的全部功劳归功于 Charles Petzold

private void ResizeControl()
{
    if (!images[(int)sizeMode].IsLoaded)
        return;

    double height = Math.Max(20,(2 * borderSize) + 
                    images[(int)sizeMode].Bitmap.PixelHeight);
    double width = Math.Max(20,(2 * borderSize) + 
                   images[(int)sizeMode].Bitmap.PixelWidth);

    if (controllWidth != width)
    {
        controllWidth = width;
        SetDimensionPixelValue("width", controllWidth);
    }

    if (controllHeight != height)
    {
        controllHeight = height;
        SetDimensionPixelValue("height", controllHeight);
    }
}

每个位图图像都会暴露其尺寸,因此我们可以通过将这些尺寸加上边框粗细来计算控件的新宽度和高度。然后,使用此方法设置宽度和高度即可。

void SetDimensionPixelValue(string style, double value)
{
    HtmlPage.Plugin.SetAttribute(style, 
           ((int)Math.Round(value)).ToString() + "px");
}

HtmlPage.Plugin 提供了对 object 标签的 HtmlElement 的访问。然后,只需将宽度或高度属性设置为度量的文本定义即可。为了使其正常工作,重要的是页面不包含会影响控件本身或其某个 div 容器的其他宽度和高度设置。默认情况下,这是通过生成代码中的样式完成的。

结论

除了解释一些基本的 Silverlight 技术外,我希望这个控件本身能够展示 Silverlight 如何成为网页设计不可或缺的一部分。

© . All rights reserved.