专注收集记录技术开发学习笔记、技术难点、解决方案
网站信息搜索 >> 请输入关键词:
您当前的位置: 首页 > WinRT Metro

博客园客户端(Universal App)开发漫笔 - App的精灵:自定义控件

发布时间:2011-06-23 13:56:39 文章来源:www.iduyao.cn 采编人员:星星草
博客园客户端(Universal App)开发随笔 -- App的精灵:自定义控件

前言

拿到一个App的需求后,对于前端工程师来说,第一步要干什么?做Navigation规划!第二步要干什么?做页面分解!页面分解如何做?首先要确定UI Element的容器,其次要抽象UI Element本身,也就是要做一堆自定义控件,最终组成整个页面。今天我们就说说自定义控件如何实现吧。

感性认识

在我们的博客园UAP的Windows Phone的版本中,一个最重要的自定义控件就是PostControl,它的样子如下图中红色矩形内所示。

image

这个控件在无数页面中都要用到,而且有几种变种。上面看到的是在主页/热门/精华中所展示的样子,是界面元素最全的,包括标题,作者,发布时间,阅读状态(朕已阅),摘要,属性(最下方的三组数字),还有最下方的横线(可不要忽视它哟,它是整体页面设计的重要组成部分)。

 

第二个变种,是在博客列表中,如下图所示。细心的人可以发现这个变种中没有显示作者,因为这是在博主页面,上下文中有MS-UAP的作者名称了,所以没必要再显示了,否则会显得很自恋。

image

 

第三个变种,在分类博客列表里面,最下方的属性没有显示。由于服务器端返回的数据中,推荐/阅读/评论次数都是0,所以弄3个0在那里感觉很傻,所以可以不显示了,这样会觉得自己智商有所提升。

image

 

第四个变种,是在所有列表中都有,就是不显示阅读状态(标题下面的“朕已阅”没有),表示这是一篇新博客,你还没有来得及看。

image

 

第五个变种,是不显示摘要和属性,如下图所示。因为这篇博客你已经读过了(朕已阅),没必要再把摘要显示出来占据有限的屏幕空间了,留着地方显示那些没读过的博客。老话儿说,吃肉别吧唧嘴,让人家没吃到肉的人听着难受,显示咱们有教养。

当然,你如果想看摘要的话,点击一下标题,摘要就自动优雅地展开了(有个小动画);如果想看正文,就点击一下摘要部分,进入到阅读页面。

image

 

以上这些变种的逻辑,包括动画,都是在自定义控件中来实现的,很强大吧?下面让我看看如何实现它吧。

 

两种自定义控件的选择

WinRT SDK有两种用户自定义控件的实现方式,一种是User Control, 另一种是 Template Control。在WPF/ASP.NET/WindowsForm中都有这两个概念,只不过后者可能叫做Custom Control。总之这是一个很古老的概念了。

如何选择这两种控件呢?说实话,不知道!但是我们强烈建议你使用Template Control, 因为我们还没发现它有什么缺点,但是发现User Control有缺点。

 

在Visual Studio 2013中,在你的Project上点击鼠标右键,Add New Item:

image

注意几个选择点,下面写PostControl.cs, 就可以轻轻点击Add按钮了。请不要猛击该按钮,注意咱们开发人员的素质。

如果一切正常的话,你的项目文件中会出现下面两个东西:

image

上面那个是PostControl.cs, 我后来把它移到Controls folder下面的,为了好管理。下面那个是Themes/Generic.xaml, 是系统帮你生成好的,别动它的位置,否则后果自负。这里有个bug,如果你是第二次添加自定义控件,很有可能出现了.cs文件后,在Generic.xaml中没有新控件的style。此时你可以用仇恨的笔写一封email发给有关部门控诉这个bug,然后乖乖的在Generic.xaml中自己添加。添加什么东西呢?后面会说到。

 

Generic.xaml

首先我们看这个文件中的模样,一堆xaml语法而已:

<Style TargetType="local:PostControl">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:PostControl">
                    <Border>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

最开始时是上面那个样子,空白的模板,你要把它写成你自己想要的样子。此时你的头脑中要有PostControl控件的具体样式,因为这个编辑器不能所见即所得。我最后把他改成了以下样子:

<Style TargetType="local:PostControl">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:PostControl">
                    <Border BorderThickness="0,0,0,1" BorderBrush="{ThemeResource CNBlogsLineColor}">
                        <Grid Margin="15">
                            <Grid.RowDefinitions>
                                <RowDefinition/>
                                <RowDefinition/>
                                <RowDefinition/>
                                <RowDefinition/>
                            </Grid.RowDefinitions>
                            <TextBlock x:Name="tb_Title" Grid.Row="0" Text="{Binding Title}" Style="{StaticResource PostTitleFont}"/>
                            <Grid Grid.Row="1" Margin="0,5,0,0">
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width="Auto"/>
                                    <ColumnDefinition Width="Auto"/>
                                    <ColumnDefinition Width="*"/>
                                </Grid.ColumnDefinitions>
                                <local:AuthorControl Grid.Column ="0" Visibility="{TemplateBinding AuthorVisible}" NameFontSize="20" NameColor="{ThemeResource CNBlogsAttributionColor}" AvatarHeight="25" Margin="0,0,10,0" />
                                <TextBlock Grid.Column="1" Text="{Binding PublishTime, Converter={StaticResource TimeCountDownConverter}}" Style="{StaticResource PublishTimeFont}" VerticalAlignment="Center"/>
                                <TextBlock x:Name="tb_Status" Grid.Column="2" Text="{Binding Status, Converter={StaticResource PostStatusConverter}}" FontFamily="Segoe UI Symbol" FontSize="14" HorizontalAlignment="Right" VerticalAlignment="Center"/>
                            </Grid>

                            <!-- used for tapped anywhere on title and attribution -->
                            <Rectangle x:Name="rect_Header" Grid.RowSpan="2" Fill="Transparent"/>

                            <TextBlock x:Name="tb_Summary" Grid.Row="2" Margin="0,5" TextTrimming="CharacterEllipsis" MaxLines="4" FontSize="20" FontFamily="Segoe WP" Foreground="{ThemeResource CNBlogsSummaryColor}" TextWrapping="Wrap" Visibility="Collapsed">
                                <Run Text="{Binding Summary}"/>
                                <Run Text="..."/>
                                <TextBlock.Resources>
                                    <Storyboard x:Name="sb_Summary">
                                        <DoubleAnimation Storyboard.TargetName="tb_Summary" Storyboard.TargetProperty="Opacity" From="0" To="1" Duration="0:0:1"/>
                                    </Storyboard>
                                </TextBlock.Resources>
                            </TextBlock>
                            <local:AttributionControl x:Name="control_Attribution" Grid.Row="3" HorizontalAlignment="Right" Visibility="{TemplateBinding AttributionVisible}" FontFamily="Global User Interface"/>
                        </Grid>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

最外面有个Border,它的好处是把边界设置成{0,0,0,1}就会在最下面显示分割线。如果你不想在最下面显示一条横线来分割两个博客,可以把这层border去掉。

里面是个Grid,定义了四行:第一行是标题;第二行是作者/发布时间/阅读状态。这里作者又是另一个自定义控件,所以写成了local:AuthorControl;第三行是摘要;第四行是属性,也是一个自定义控件。

说明几个刁(雕)民(虫)小技:

1)控件可以套控件,比如AuthorControl和AttributionControl在PostControl里面。

2)阅读状态的HorizontalAlignment=Right,就是右侧对齐,这个必须在Grid里才有用,在StackPanel里好像做不到。也许我不够刁,反正在Windows Phone上没试出来。

3)必须要知道屏幕的宽度,否则如果显示内容不够一屏宽,右对齐就不是真正的右对齐了,这个你自己试试就知道了。如何知道屏幕宽度呢?在PostControl.cs里:

public PostControl()
        {
            this.DefaultStyleKey = typeof(PostControl);
            this.DataContextChanged += PostControl_DataContextChanged;
            this.Width = CNBlogs.DataHelper.Helper.Functions.GetWindowsWidth();
        }

上面那个this.Width,就是根据屏幕宽度指定控件本身的宽度。如果你要两边留白,可以想别的办法得到该控件所属的容器的宽度,比如listview的宽度。

4)不要在最外层设置Margin,就是Border那一层,比如Margin=20. 如果设置了,你麻烦大了!别紧张,你唯一的麻烦是在调整个页面的样子时,遇到一些奇怪的空白,找了一圈才知道是控件自己内部有空白。如果想设置空白,在使用这个控件的XAML里设置,比如ListView的ItemsPanel里。

5)大家可能看到这个:<Rectangle x:Name="rect_Header" Grid.RowSpan="2" Fill="Transparent"/>。挺奇怪的,但作用很大。因为我们设计在点击上面两行(标题和作者)时隐藏或显示摘要,但是这两行并不会充满了字,肯定有很多空白。如果你纤细的手指点击到了空白处,不会触发任何内部点击事件。如果加了一层透明的Rectangle,可以点击任何位置了。

6)里面可以发现有用到TemplateBinding语法的,这个对应的属性要在PostControl.cs里注册(下面有说)。

7)写完所有style后,仔细看一遍,不要有多余的Border, Grid, StackPanel等容器。尤其是在修改了style后,这种情况很有可能发生。其坏处就是让别人觉得你的程序员素质不高啊微笑

8)可以在style里面定义动画,就像summary里的Storyboard那样,然后在PostControl.cs里调用。

9)TextBlock是个好东东,要妥善使用。比如<Run Text/>语法,可以用来拼接字符串,还可以指定不同的字体字号字色,但不建议这样做,会毁坏的你的UI,让别人觉得你的素质不高啊(又来了)。

 

PostControl.cs

自定义属性

如果想从外面(使用时)控制某些内容,比如显示或不显示作者,需要自定义属性如下:

public Visibility AuthorVisible
        {
            get { return (Visibility)GetValue(AuthorVisibleProperty); }
            set { SetValue(AuthorVisibleProperty, value); }
        }

        // Using a DependencyProperty as the backing store for AuthorVisiable.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty AuthorVisibleProperty =
            DependencyProperty.Register("AuthorVisible", typeof(Visibility), typeof(PostControl), new PropertyMetadata(Visibility.Visible));

这个语法太难记住了,你可以每次copy/paste/modify,有一个简单的方式是在空白处键入propdp,然后按Tab键,会自动生成这些东东,然后再手工改一些关键的西西,就是你想要的东西了。有了这个属性后,在Style里面(Generic.xaml),可以这样写:

<local:AuthorControl Grid.Column ="0" Visibility="{TemplateBinding AuthorVisible}" />

表明这个AuthorControl的显示与否可以在使用时控制。

然后你在使用这个PostControl的page.xaml中这样写:

<ListView x:Name="lv_AuthorPosts" Grid.Row="1" Background="{ThemeResource CNBlogsBackColor}" Loaded="lv_AuthorPosts_Loaded">
            <ListView.Header>
            <ListView.ItemTemplate>
                <DataTemplate>
                    <local:PostControl AuthorVisible="Collapsed" Tapped="PostControl_Tapped"/>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>

PostControl的AuthorVisible=Collapsed,于是乎作者不显示了,显示的是我们开发者的情怀(扯远了)。

 

事件注册

在构造函数中,可以注册你想要的事件,如:

public PostControl()
        {
            this.DefaultStyleKey = typeof(PostControl);
            this.DataContextChanged += PostControl_DataContextChanged;
            this.Width = CNBlogs.DataHelper.Helper.Functions.GetWindowsWidth();
        }

此处注册了DataContextChanged事件,并在后面要响应这个事件。

还有些事件是不需注册的,比如OnApplyTemplate(), 表现为一个方法,直接override即可。

 

显示控制

如果在一个ListView中显示一串博文,而且这些博文的状态可能不一样,比如有的是”朕已阅“不显示摘要,有的是要显示摘要,我们需要在OnApplyTemplate()中控制这个显示行为。

protected override void OnApplyTemplate()
        {
            this.UpdateUI(false);
        }

注意啦!这里有个问题,你实际运行就知道了,由于ListView有一些”智能“显示控制,它只会对前几个博文执行OnApplyTemplate()方法,具体几个呢?依赖于你的屏幕的高度能显示几个博文。对于后面所有的博文,都会无视这个方法,这样当你卷滚ListView至下方时,悲剧发生了,没有按照你的意思控制显示(该隐藏的摘要没有隐藏)。

怎么办?再次拿起仇恨的笔写一封控告信,然后默默地烧掉它。幸好我们注册了DataContextChanged事件,于是轻松地写下如下代码:

void PostControl_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
{
        this.UpdateUI(false);
}

搞定啦!后面的博文也能按照你的逻辑显示了。具体原因不讲了,你自己领会吧。

 

事件响应

OnTapped事件是我们必须要用到的,处理当用户点击此控件的任何一个部分时,你要响应的逻辑。

/// <summary>
        /// if user click title, control will collapse summary, set status = Skip, next time: show title only
        /// if user click summary, goto reading page, set status = Read, next time: show title only
        /// if user click favorite in reading page, set status = Favorite, next time: show title only
        /// </summary>
        /// <param name="e"></param>
        protected override void OnTapped(TappedRoutedEventArgs e)
        {
            CNBlogs.DataHelper.DataModel.Post post = this.DataContext as CNBlogs.DataHelper.DataModel.Post;
            if (post == null)
            {
                return;
            }

            // click on the title
            if (e.OriginalSource is Windows.UI.Xaml.Shapes.Rectangle)
            {
                this.GotoReadingPage = false;
                if (this.showSummary) // show summary
                {
                    this.HideSummary();
                    this.SetNewStatus(post, DataHelper.DataModel.PostStatus.Skip);
                }
                else // hide summary
                {
                    this.ShowSummary(true);
                }
            }
            else // click on the summary
            {
                TextBlock tbSource = e.OriginalSource as TextBlock;
                if (tbSource.Name == "tb_Summary")
                {
                    // don't navigate to target page here(in control), need do that in page's viewmodel (.cs)
                    this.GotoReadingPage = true;
                    this.SetNewStatus(post, DataHelper.DataModel.PostStatus.Read);
                }
            }

            base.OnTapped(e);
        }

我把最上端的注释也保留了,for your eyes only.

这里特别强调一点,很重要:其实这个点击事件可以在三个地方响应:

1)在这里的code中响应

2)在ListView的Control中响应(PostControl_Tapped)

<ListView x:Name="lv_BestPosts" Grid.Row="1" Background="{ThemeResource CNBlogsBackColor}">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <local:PostControl Tapped="PostControl_Tapped"/>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>

3)在ListView的ItemClicked事件中响应。

有何区别呢?建议如下:

1)在PostControl.cs中响应事件时,只关注控件本身的样式变化,比如隐藏摘要,不要做别的事情,否则就会超出你的职责范围了,让上层事件无法处理。

2)在控件的PostControl_Tapped中,你可以做上层逻辑了,比如直接显示博文阅读页面。但是PostControl实例是从sender里得到的:

private void PostControl_Tapped(object sender, TappedRoutedEventArgs e)
        {
            PostControl postControl = sender as PostControl;
            if (postControl.GotoReadingPage)
            {
                Post post = postControl.DataContext as Post;
                this.Frame.Navigate(typeof(PostReadingPage), post);
            }
            else
            {

            }

3)在ListView的ItemClicked事件中(如果有的话),也同样可以做上层逻辑。记得上面那个透明的Rectangle吧,如果没有它,ItemClicked事件也会响应,但是底层那两个事件不一定会响应(如果手指头太细点在了空白处)。而且对应的类实例是从click事件中得到的,而不是sender:

private void lv_Category_ItemClick(object sender, ItemClickEventArgs e)
        {
            Category category = e.ClickedItem as Category;
            this.Frame.Navigate(typeof(SubCategoriesPage), category);
        }
如上面代码中的Category实例,是从e.ClickedItem中得到的。

 

小结

写累了,休息一下,我们已经完成了一个自定义控制的样式定义和逻辑定义,用几次就熟悉了。某些粗糙的App, 直接用一个TextBlock显示内容,不加任何修饰,体现不出我程序员们的素质,建议稍微讲究一些,用个template control。就拿这个PostControl来说,你尽可以把它拿去稍微修改一下,就可以适应所有阅读类的需求了。真的,不信你试试,我反正已经用这个Control做了三个App了。

在Windows 8.1上,同样的道理,可以使用自定义control。但是以博客园为例,由于UI design相差太大,无法复用样式,但可以部分复用逻辑,所以建议不要把这些control放在Shared里面,而是放在各自的Project内部。

比较Windows 8.1和Windows Phone 8.1的自定义控件,在Windows上,由于显示面积大,控件要设计得大气,别扣扣嗦嗦的,可以色彩鲜明些,显示充分些;但是在Windows Phone上,显示面积小,要讲究精巧,比如隐藏摘要这件事,很适合Windows Phone,但是不适合Windows。

 

分享代码,改变世界!

 

Windows Phone Store App link:

http://www.windowsphone.com/zh-cn/store/app/博客园-uap/500f08f0-5be8-4723-aff9-a397beee52fc (明天会有一个更新)

Windows Store App link:

http://apps.microsoft.com/windows/zh-cn/app/c76b99a0-9abd-4a4e-86f0-b29bfcc51059

(明天会有一个更新)

GitHub open source link:

https://github.com/MS-UAP/cnblogs-UAP

MSDN Sample Code:

https://code.msdn.microsoft.com/CNBlogs-Client-Universal-9c9692d1

6楼yonghu86
好家伙,此文必火。
5楼h82258652
赞,这次文章的风格有点调皮^-^,很好地说了ItemClick和Tapped的应该何时使用的问题。,,不过有个问题想问问,为什么在这个自定义模板控件的构造函数里设置宽度而不在Listview里设置宽度:,lt;ListViewgt; lt;ListView.ItemContainerStylegt; lt;Style TargetType=quot;ListViewItemquot;gt; lt;Setter Property=quot;HorizontalContentAlignmentquot; Value=quot;Stretchquot; /gt; lt;/Stylegt; lt;/ListView.ItemContainerStylegt;lt;/ListViewgt;,我那里是ListViewItem,你这里就是PostControl啦。,,构造函数里设置宽度的话,那么如果发生重绘的话,宽度就没法变了(例如ListView宽度变小了,当然现时这设计下不会出这情况)
4楼Phoozyan
牛逼!
3楼MS-UAP
Hi h82258652,,,Thanks for your suggestion! We failed to find the HorizontalContentAlignment property in document. We will try your method and change our code if it works, and then publish the code again.,,Thanks again! Hope you can give us more suggestion!
2楼bunny_gg
做app的都应该看一看。
1楼葡萄城控件技术团队
赞一个。
友情提示:
信息收集于互联网,如果您发现错误或造成侵权,请及时通知本站更正或删除,具体联系方式见页面底部联系我们,谢谢。

其他相似内容:

热门推荐: