精华内容
下载资源
问答
  • wpf自定义控件库,包含列表,按钮,文本框,滚动条,进度条,消息窗体,气泡组件等多种自定义控件样式。
  • WPF自定义控件库

    千次阅读 2018-07-10 12:48:21
    物尽其用,今天跟大家分享一下自己写的一个WPF控件库。大家可以看以下几张图片效果,都是可以实现的(歌曲列表等少部分不在此库中)。基本控件实现的方式可以看代码,如果有问题可以评论留言,互相交流。1. 音乐...

           2018年,为了大学能够顺利毕业,自己仿照QQ音乐做了一套包含Windows桌面客户端与Web后台管理的系统。物尽其用,今天跟大家分享一下自己写的一个WPF控件库。

           大家可以看以下几张图片效果,都是可以实现的(歌曲列表等少部分不在此库中)。基本控件实现的方式可以看代码,如果有问题可以评论留言,互相交流。

    1. 音乐播放器主界面

    2. 搜索框

    3. 皮肤管理


    有兴趣的同学可以下载源代码,交流学习。

    https://download.csdn.net/download/qq_31606375/10532332

    展开全文
  • [WPF自定义控件库] 自定义控件的代码如何与ControlTemplate交互 原文:[WPF自定义控件库] 自定义控件的代码如何与ControlTemplate交互1. 前言 WPF有一个灵活的UI框架,用户可以轻松地使用代码控制控件...
    原文: [WPF自定义控件库] 自定义控件的代码如何与ControlTemplate交互

    1. 前言

    WPF有一个灵活的UI框架,用户可以轻松地使用代码控制控件的外观。例设我需要一个控件在鼠标进入的时候背景变成蓝色,我可以用下面这段代码实现:

    protected override void OnMouseEnter(MouseEventArgs e)
    {
        base.OnMouseEnter(e);
        Background = new SolidColorBrush(Colors.Blue);
    }

    但一般没人会这么做,因为这样做代码和UI过于耦合,难以扩展。正确的做法应该是使用代码告诉ControlTemplate去改变外观,或者控制ControlTemplate中可用的元素进入某个状态。

    这篇文章介绍自定义控件的代码如何和ControlTemplate交互,涉及的知识包括RelativeSource、Trigger、TemplatePart和VisualState。

    2. 简单的Expander

    本文使用一个简单的Expander介绍UI和ControlTemplate交互的几种技术,它的代码如下:

    public class MyExpander : HeaderedContentControl
    {
        public MyExpander()
        {
            DefaultStyleKey = typeof(MyExpander);
        }
    
        public bool IsExpanded
        {
            get => (bool)GetValue(IsExpandedProperty);
            set => SetValue(IsExpandedProperty, value);
        }
    
        public static readonly DependencyProperty IsExpandedProperty =
            DependencyProperty.Register(nameof(IsExpanded), typeof(bool), typeof(MyExpander), new PropertyMetadata(default(bool), OnIsExpandedChanged));
    
        private static void OnIsExpandedChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
        {
            var oldValue = (bool)args.OldValue;
            var newValue = (bool)args.NewValue;
            if (oldValue == newValue)
                return;
    
            var target = obj as MyExpander;
            target?.OnIsExpandedChanged(oldValue, newValue);
        }
    
        protected virtual void OnIsExpandedChanged(bool oldValue, bool newValue)
        {
            if (newValue)
                OnExpanded();
            else
                OnCollapsed();
        }
    
        protected virtual void OnCollapsed()
        {
        }
    
        protected virtual void OnExpanded()
        {
        }
    }
    
    <Style TargetType="{x:Type local:MyExpander}">
        <Setter Property="HorizontalContentAlignment"
                Value="Stretch" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:MyExpander}">
                    <Border Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}">
                        <StackPanel>
                            <ToggleButton x:Name="ExpanderToggleButton"
                                          Content="{TemplateBinding Header}"
                                          IsChecked="{Binding IsExpanded,RelativeSource={RelativeSource Mode=TemplatedParent},Mode=TwoWay}" />
                            <ContentPresenter Grid.Row="1"
                                              x:Name="ContentPresenter"
                                              HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                              VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                                              Visibility="Collapsed" />
                        </StackPanel>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

    MyExpander是一个HeaderedContentControl,它包含一个IsExpanded用于指示当前是展开还是折叠。ControlTemplate中包含ExpanderToggleButton及ContentPresenter两个元素。

    3. 使用RelativeSource

    之前已经介绍过TemplateBinding,通常ControlTemplate中元素都通过TemplateBinding获取控件的属性值。但需要双向绑定的话,就是RelativeSource出场的时候了。

    RelativeSource有几种模式,分别是:

    • FindAncestor,引用数据绑定元素的父链中的上级。 这可用于绑定到特定类型的上级或其子类。
    • PreviousData,允许在当前显示的数据项列表中绑定上一个数据项(不是包含数据项的控件)。
    • Self,引用正在其上设置绑定的元素,并允许你将该元素的一个属性绑定到同一元素的其他属性上。
    • TemplatedParent,引用应用了模板的元素,其中此模板中存在数据绑定元素。。

    ControlTemplate中主要使用RelativeSource Mode=TemplatedParent的Binding,它相当于TemplateBinding的双向绑定版本。,主要是为了可以和控件本身进行双向绑定。ExpanderToggleButton.IsChecked使用这种绑定与Expander的IsExpanded关联,当Expander.IsChecked为True时ExpanderToggleButton处于选中的状态。

    IsChecked="{Binding IsExpanded,RelativeSource={RelativeSource Mode=TemplatedParent},Mode=TwoWay}" 

    接下来分别用几种技术实现Expander.IsChecked为True时显示ContentPresenter。

    4. 使用Trigger

    <ControlTemplate TargetType="{x:Type local:ExpanderUsingTrigger}">
        <Border Background="{TemplateBinding Background}">
            ......
        </Border>
        <ControlTemplate.Triggers>
            <Trigger Property="IsExpanded"
                     Value="True">
                <Setter Property="Visibility"
                        TargetName="ContentPresenter"
                        Value="Visible" />
            </Trigger>
        </ControlTemplate.Triggers>
    </ControlTemplate>

    可以为ControlTemplate添加Triggers,内容为TriggerEventTrigger的集合,Triggers通过响应属性值变更或事件更改控件的外观。

    大部分情况下Trigger简单好用,但滥用或错误使用将使ControlTemplate的各个状态之间变得很混乱。例如当可以影响外观的属性超过一定数量,并且这些属性可以组成不同的组合,Trigger将要处理无数种情况。

    5. 使用TemplatePart

    TemplatePart(部件)是指ControlTemplate中的命名元素(如上面XAML中的“HeaderElement”)。控件逻辑预期这些部分存在于ControlTemplate中,控件在加载ControlTemplate后会调用OnApplyTemplate,可以在这个函数中调用protected DependencyObject GetTemplateChild(String childName)获取模板中指定名字的部件。

    [TemplatePart(Name =ContentPresenterName,Type =typeof(UIElement))]
    public class ExpanderUsingPart : MyExpander
    {
        private const string ContentPresenterName = "ContentPresenter";
    
        protected UIElement ContentPresenter { get; private set; }
    
        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            ContentPresenter = GetTemplateChild(ContentPresenterName) as UIElement;
            UpdateContentPresenter();
        }
    
        protected override void OnIsExpandedChanged(bool oldValue, bool newValue)
        {
            base.OnIsExpandedChanged(oldValue, newValue);
            UpdateContentPresenter();
        }
    
        private void UpdateContentPresenter()
        {
            if (ContentPresenter == null)
                return;
    
            ContentPresenter.Visibility = IsExpanded ? Visibility.Visible : Visibility.Collapsed;
        }
    }

    上面的代码实现了获取HeaderElement并为它订阅鼠标点击事件。由于Template可能多次加载,或者不能正确获取TemplatePart,所以使用TemplatePart前应该先判断是否为空;如果要订阅事件,应该先取消订阅。

    注意:不要在Loaded事件中尝试调用GetTemplateChild,因为Loaded的时候OnApplyTemplate不一定已经被调用,而且Loaded更容易被多次触发。

    TemplatePartAttribute协定

    有时,为了表明控件期待在ControlTemplate存在某个特定部件,防止编辑ControlTemplate的开发人员删除它,控件上会添加添加TemplatePartAttribute协定。上面代码中即包含这个协定:

    [TemplatePart(Name =ContentPresenterName,Type =typeof(UIElement))]

    这段代码的意思是期待在ControlTemplate中存在名称为 "ContentPresenterName",类型为UIElement的部件。

    TemplatePartAttribute在UWP中的作用好像被弱化了,不止在UWP原生控件中见不到TemplatePartAttribute,甚至在Blend中“部件”窗口也消失了。可能UWP更加建议使用VisualState。

    使用TemplatePart需要遵循以下原则:

    • 尽可能减少TemplarePartAttribute协定。
    • 在使用TemplatePart之前检查其是否为Null。
    • 如果ControlTemplate没有遵循TemplatePartAttribute协定也不应该抛出异常,缺少部分功能可以接受,但要确保程序不会报错。

    6. 使用VisualState

    VisualState 指定控件处于特定状态时的外观。控件的代码使用VisualStateManager.GoToState(Control control, string stateName,bool useTransitions)指定控件处于何种VisualState,控件的ControlTemplate中根节点使用VisualStateManager.VisualStateGroups附加属性,并在其中确定各个VisualState的外观。

    [TemplateVisualState(Name = StateExpanded, GroupName = GroupExpansion)]
    [TemplateVisualState(Name = StateCollapsed, GroupName = GroupExpansion)]
    public class ExpanderUsingState : MyExpander
    {
        public const string GroupExpansion = "ExpansionStates";
    
        public const string StateExpanded = "Expanded";
    
        public const string StateCollapsed = "Collapsed";
    
        public ExpanderUsingState()
        {
            DefaultStyleKey = typeof(ExpanderUsingState);
        }
    
        protected override void OnIsExpandedChanged(bool oldValue, bool newValue)
        {
            base.OnIsExpandedChanged(oldValue, newValue);
            UpdateVisualStates(true);
        }
    
        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            UpdateVisualStates(false);
        }
    
        protected virtual void UpdateVisualStates(bool useTransitions)
        {
            VisualStateManager.GoToState(this, IsExpanded ? StateExpanded : StateCollapsed, useTransitions);
        }
    
    }
    <ControlTemplate TargetType="{x:Type local:ExpanderUsingState}">
        <Border Background="{TemplateBinding Background}"
                BorderBrush="{TemplateBinding BorderBrush}"
                BorderThickness="{TemplateBinding BorderThickness}">
            <VisualStateManager.VisualStateGroups>
                <VisualStateGroup x:Name="ExpansionStates">
                    <VisualState x:Name="Expanded">
                        <Storyboard>
                            <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)"
                                                           Storyboard.TargetName="ContentPresenter">
                                <DiscreteObjectKeyFrame KeyTime="0"
                                                        Value="{x:Static Visibility.Visible}" />
                            </ObjectAnimationUsingKeyFrames>
                        </Storyboard>
                    </VisualState>
                    <VisualState x:Name="Collapsed" />
                </VisualStateGroup>
            </VisualStateManager.VisualStateGroups>
          ......
        </Border>
    </ControlTemplate>
    

    上面的代码演示了如何通过控件的IsExpanded 属性进入不同的VisualState。ExpansionStates是VisualStateGroup,它包含Expanded和Collapsed两个互斥的状态,控件使用VisualStateManager.GoToState(Control control, string stateName,bool useTransitions)更新VisualState。useTransitions这个参数指示是否使用 VisualTransition 进行状态过渡,简单来说即是VisualState之间切换时用不用VisualTransition里面定义的动画。请注意我在OnApplyTemplate()中使用了 UpdateVisualStates(false),这是因为这时候控件还没在UI上呈现,这时候使用动画毫无意义。

    使用VisualState的最佳实践

    使用属性控制状态,并创建一个方法帮助状态间的转换。如上面的UpdateVisualStates(bool useTransitions)。当属性值改变或其它有可能影响VisualState的事件发生都可以调用这个方法,由它统一管理控件的VisualState。注意一个控件应该最多只有几种VisualStateGroup,有限的状态才容易管理。

    TemplateVisualStateAttribute协定

    自定义控件可以使用TemplateVisualStateAttribute协定声明它的VisualState,用于通知控件的使用者有这些VisualState可用。这很好用,尤其是对于复杂的控件来说。上面代码也包含了这个协定:

    [TemplateVisualState(Name = StateExpanded, GroupName = GroupExpansion)]
    [TemplateVisualState(Name = StateCollapsed, GroupName = GroupExpansion)]

    TemplateVisualStateAttribute是可选的,而且就算控件声明了这些VisualState,ControlTemplate也可以不包含它们中的任何一个,并且不会引发异常。

    7. Trigger、TemplatePart及VisualState之间的选择

    正如Expander所示,Trigger、TemplatePart及VisualState都可以实现类似的功能,像这种三种方式都可以实现同一个功能的情况很常见。

    在过去版本的Blend中,编辑ControlTemplate可以看到“状态(States)”、“触发器(Triggers)”、“部件(Parts)”三个面板,现在“部件”面板已经消失了,而“触发器”从Silverlight开始就不再支持,以后也应该不会回归(xaml standard在github上有这方面的讨论(Add Triggers, DataTrigger, EventTrigger,___) [and-or] VisualState · Issue #195 · Microsoft-xaml-standard · GitHub[https://github.com/Microsoft/xaml-standard/issues/195])。现在看起来是VisualState的胜利,其实在Silverlight和UWP中TemplatePart仍是个十分常用的技术,而在WPF中Trigger也工作得很出色。

    38937-20190115215547501-1389131607.png

    38937-20190115215558840-878012282.png

    如果某个功能三种方案都可以实现,我的选择原则是这样:

    • 需要向控件发出命令的,如响应点击事件,就用TemplatePart;
    • 简单的UI,如隐藏/显示某个元素就用Trigger;
    • 如果要有动画,并且代码量和使用Trigger的话,我会选择用VisualState;

    几乎所有WPF的原生控件都提供了VisualState支持,例如Button虽然使用ButtonChrome实现外观,但同时也可以使用VisualState定义外观。有时做自定义控件的时候要考虑为常用的VisualState提供支持。

    8. 结语

    VisualState是个比较复杂的话题,可以通过我的另一篇文章理解ControlTemplate中的VisualTransition更深入地理解它的用法(虽然是UWP的内容,但对WPF也同样适用)。

    即使不自定义控件,学会使用ControlTemplate也是一件好事,下面给出一些有用的参考链接。

    9. 参考

    创建具有可自定义外观的控件 Microsoft Docs
    通过创建 ControlTemplate 自定义现有控件的外观 Microsoft Docs
    Control Customization Microsoft Docs
    ControlTemplate Class (System_Windows_Controls) Microsoft Docs

    posted on 2019-05-22 09:11 NET未来之路 阅读( ...) 评论( ...) 编辑 收藏

    转载于:https://www.cnblogs.com/lonelyxmas/p/10903848.html

    展开全文
  • [WPF自定义控件库]了解如何自定义ItemsControl 原文:[WPF自定义控件库]了解如何自定义ItemsControl1. 前言 对WPF来说ContentControl和ItemsControl是最重要的两个控件。 顾名思义,ItemsControl表示...
    原文: [WPF自定义控件库]了解如何自定义ItemsControl

    1. 前言

    对WPF来说ContentControl和ItemsControl是最重要的两个控件。

    顾名思义,ItemsControl表示可用于呈现一组Item的控件。大部分时候我们并不需要自定义ItemsControl,因为WPF提供了一大堆ItemsControl的派生类:HeaderedItemsControl、TreeView、Menu、StatusBar、ListBox、ListView、ComboBox;而且配合Style或DataTemplate足以完成大部分的定制化工作,可以说ItemsControl是XAML系统灵活性的最佳代表。不过,既然它是最常用的控件,那么掌握一些它的原理对所有WPF开发者都有好处。

    我以前写过一篇文章介绍如何模仿ItemsControl,并且博客园也已经很多文章深入介绍ItemsControl的原理,所以这篇文章只介绍简单的自定义ItemsControl知识,通过重写GetContainerForItemOverride和IsItemItsOwnContainerOverride、PrepareContainerForItemOverride函数并使用ItemContainerGenerator等自定义一个简单的IItemsControl控件。

    2. 介绍作为例子的Repeater

    作为教学我创建了一个继承自ItemsControl的控件Repeater(虽然简单,用来展示资料的话好像还真的有点用)。它的基本用法如下:

    <local:Repeater>
        <local:RepeaterItem Content="1234999"
                            Label="Product ID" />
        <local:RepeaterItem Content="Power Projector 4713"
                            Label="IGNORE" />
        <local:RepeaterItem Content="Projector (PR)"
                            Label="Category" />
        <local:RepeaterItem Content="A very powerful projector with special features for Internet usability, USB"
                            Label="Description" />
    </local:Repeater>

    也可以不直接使用Items,而是绑定ItemsSource并指定DisplayMemberPath和LabelMemberPath。

    public class Product
    {
        public string Key { get; set; }
    
        public string Value { get; set; }
    
        public static IEnumerable<Product> Products
        {
            get
            {
                return new List<Product>
                {
                    new Product{Key="Product ID",Value="1234999" },
                    new Product{Key="IGNORE",Value="Power Projector 4713" },
                    new Product{Key="Category",Value="Projector (PR)" },
                    new Product{Key="Description",Value="A very powerful projector with special features for Internet usability, USB" },
                    new Product{Key="Price",Value="856.49 EUR" },
                };
    
            }
        }
    }
    
    <local:Repeater ItemsSource="{x:Static local:Product.Products}"
                    DisplayMemberPath="Value"
                    LabelMemberPath="Key"/>

    运行结果如下图:

    38937-20190117164839949-571721109.png

    3. 实现

    确定好需要实现的ItemsControl后,通常我大致会使用三步完成这个ItemsControl:

    1. 定义ItemContainer
    2. 关联ItemContainer和ItemsControl
    3. 实现ItemsControl的逻辑

    3.1 定义ItemContainer

    派生自ItemsControl的控件通常都会有匹配的子元素控件,如ListBox对应ListBoxItem,ComboBox对应ComboBoxItem。如果ItemsControl的Items内容不是对应的子元素控件,ItemsControl会创建对应的子元素控件作为容器再把Item放进去。

    <ListBox>
        <system:String>Item1</system:String>
        <system:String>Item2</system:String>
    </ListBox>

    38937-20190117164740994-1401346920.png

    例如这段XAML中,Item1和Item2是ListBox的LogicalChildren,而它们会被ListBox封装到ListBoxItem,ListBoxItem才是ListBox的VisualChildren。在这个例子中,ListBoxItem可以称作ItemContainer

    ItemsControl派生类的ItemContainer控件要使用父元素名称做前缀、-Item做后缀,例如ComboBox的子元素ComboBoxItem,这是WPF约定俗成的做法(不过也有TabControl和TabItem这种例外)。Repeater也派生自ItemsControl,Repeatertem即为Repeater的ItemContainer控件。

    public RepeaterItem()
    {
        DefaultStyleKey = typeof(RepeaterItem);
    }
    
    public object Label
    {
        get => GetValue(LabelProperty);
        set => SetValue(LabelProperty, value);
    }
    
    public DataTemplate LabelTemplate
    {
        get => (DataTemplate)GetValue(LabelTemplateProperty);
        set => SetValue(LabelTemplateProperty, value);
    }
    <Style TargetType="local:RepeaterItem">
        <Setter Property="Padding"
                Value="8" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:RepeaterItem">
                    <Border BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}"
                            Background="{TemplateBinding Background}">
                        <StackPanel Margin="{TemplateBinding Padding}">
                            <ContentPresenter Content="{TemplateBinding Label}"
                                              ContentTemplate="{TemplateBinding LabelTemplate}"
                                              VerticalAlignment="Center"
                                              TextBlock.Foreground="#FF777777" />
                            <ContentPresenter x:Name="ContentPresenter" />
                        </StackPanel>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

    上面是RepeaterItem的代码和DefaultStyle。RepeaterItem继承ContentControl并提供Label、LabelTemplate。DefaultStyle的做法参考ContentControl。

    3.2 关联ItemContainer和ItemsControl

    <Style TargetType="{x:Type local:Repeater}">
        <Setter Property="ScrollViewer.VerticalScrollBarVisibility"
                Value="Auto" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:Repeater}">
                    <Border BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}"
                            Background="{TemplateBinding Background}">
                        <ScrollViewer Padding="{TemplateBinding Padding}">
                            <ItemsPresenter />
                        </ScrollViewer>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

    如上面XAML所示,Repeater的ControlTemplate中需要提供一个ItemsPresenter,用于指定ItemsControl中的各Item摆放的位置。

    [StyleTypedProperty(Property = "ItemContainerStyle", StyleTargetType = typeof(RepeaterItem))]
    public class Repeater : ItemsControl
    {
        public Repeater()
        {
            DefaultStyleKey = typeof(Repeater);
        }
    
        protected override bool IsItemItsOwnContainerOverride(object item)
        {
            return item is RepeaterItem;
        }
    
        protected override DependencyObject GetContainerForItemOverride()
        {
            var item = new RepeaterItem();
            return item;
        }
    }

    Repeater的基本代码如上所示。要将Repeater和RepeaterItem关联起来,除了使用约定俗成的命名方式告诉用户,还需要使用下面两步:

    重写 GetContainerForItemOverride
    protected virtual DependencyObject GetContainerForItemOverride () 用于返回Item的Container。Repeater返回的是RepeaterItem。

    重写 IsItemItsOwnContainer
    protected virtual bool IsItemItsOwnContainerOverride (object item),确定Item是否是(或者是否可以作为)其自己的Container。在Repeater中,只有RepeaterItem返回True,即如果Item的类型不是RepeaterItem,就将它作使用RepeaterItem包装起来。

    完成上面几步后,为Repeater设置ItemsSource的话Repeater将会创建对应的RepeaterItem并添加到自己的VisualTree下面。

    使用 StyleTypedPropertyAttribute

    最后可以在Repeater上添加StyleTypedPropertyAttribute,指定ItemContainerStyle的类型为RepeaterItem。添加这个Attribute后在Blend中选择“编辑生成项目的容器(ItemContainerStyle)”就会默认使用RepeaterItem的样式。

    38937-20190117164925650-90988219.png

    3.3 实现ItemsControl的逻辑

    public string LabelMemberPath
    {
        get => (string)GetValue(LabelMemberPathProperty);
        set => SetValue(LabelMemberPathProperty, value);
    }
    
    /*LabelMemberPathProperty Code...*/
    
    protected virtual void OnLabelMemberPathChanged(string oldValue, string newValue)
    {
        // refresh the label member template.
        _labelMemberTemplate = null;
        var newTemplate = LabelMemberPath;
    
        int count = Items.Count;
        for (int i = 0; i < count; i++)
        {
            if (ItemContainerGenerator.ContainerFromIndex(i) is RepeaterItem RepeaterItem)
                PrepareRepeaterItem(RepeaterItem, Items[i]);
        }
    }
    
    private DataTemplate _labelMemberTemplate;
    
    private DataTemplate LabelMemberTemplate
    {
        get
        {
            if (_labelMemberTemplate == null)
            {
                _labelMemberTemplate = (DataTemplate)XamlReader.Parse(@"
                <DataTemplate xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""
                            xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml"">
                        <TextBlock Text=""{Binding " + LabelMemberPath + @"}"" VerticalAlignment=""Center""/>
                </DataTemplate>");
            }
    
            return _labelMemberTemplate;
        }
    }
    
    protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
    {
        base.PrepareContainerForItemOverride(element, item);
    
        if (element is RepeaterItem RepeaterItem )
        {
            PrepareRepeaterItem(RepeaterItem,item);
        }
    }
    
    private void PrepareRepeaterItem(RepeaterItem RepeaterItem, object item)
    {
        if (RepeaterItem == item)
            return;
    
        RepeaterItem.LabelTemplate = LabelMemberTemplate;
        RepeaterItem.Label = item;
    }

    Repeater本身没什么复杂的逻辑,只是模仿DisplayMemberPath添加了LabelMemberPathLabelMemberTemplate属性,并把这个属性和RepeaterItem的Label和'LabelTemplate'属性关联起来,上面的代码即用于实现这个功能。

    LabelMemberPath和LabelMemberTemplate
    Repeater动态地创建一个内容为TextBlock的DataTemplate,这个TextBlock的Text绑定到LabelMemberPath

    XamlReader相关的技术我在如何使用代码创建DataTemplate这篇文章里讲解了。

    ItemContainerGenerator.ContainerFromIndex
    ItemContainerGenerator.ContainerFromIndex(Int32)返回ItemsControl中指定索引处的Item,当Repeater的LabelMemberPath改变时,Repeater首先强制更新了LabelMemberTemplate,然后用ItemContainerGenerator.ContainerFromIndex找到所有的RepeaterItem并更新它们的Label和LabelTemplate。

    PrepareContainerForItemOverride
    protected virtual void PrepareContainerForItemOverride (DependencyObject element, object item) 用于在RepeaterItem添加到UI前为其做些准备工作,其实也就是为RepeaterItem设置LabelLabelTemplate而已。

    4. 结语

    实际上WPF的ItemsControl很强大也很复杂,源码很长,对初学者来说我推荐参考Moonlight中的实现(Moonlight, an open source implementation of Silverlight for Unix systems),上面LabelMemberTemplate的实现就是抄Moonlight的。Silverlight是WPF的简化版,Moonlight则是很久没维护的Silverlight的简陋版,这使得Moonlight反而成了很优秀的WPF教学材料。

    当然,也可以参考Silverlight的实现,使用JustDecompile可以轻松获取Silverlight的源码,这也是很好的学习材料。不过ItemsControl的实现比Moonlight多了将近一倍的代码。

    38937-20190117164947920-320249099.png

    5. 参考

    ItemsControl Class (System.Windows.Controls) Microsoft Docs
    moon_ItemsControl.cs at master
    ItemContainer Control Pattern - Windows applications _ Microsoft Docs

    posted on 2019-05-20 15:10 NET未来之路 阅读( ...) 评论( ...) 编辑 收藏

    转载于:https://www.cnblogs.com/lonelyxmas/p/10894245.html

    展开全文
  • [WPF自定义控件库]使用WindowChrome自定义RibbonWindow 原文:[WPF自定义控件库]使用WindowChrome自定义RibbonWindow1. 为什么要自定义RibbonWindow 自定义Window有可能是设计或功能上的要求,可以...
    原文: [WPF自定义控件库]使用WindowChrome自定义RibbonWindow

    1. 为什么要自定义RibbonWindow

    自定义Window有可能是设计或功能上的要求,可以是非必要的,而自定义RibbonWindow则不一样:

    • 如果程序使用了自定义样式的Window,为了统一外观需要把RibbonWindow一起修改样式。
    • 为了解决RibbonWindow的BUG。

    1.png

    如上图所示,在Windows 10 上运行打开RibbonWindow,可以看到标题栏的内容(包括分隔符)没有居中对齐,缺少下边框。

    2.png

    在最大化的时候标题栏内容甚至超出屏幕范围。

    WPF提供的Ribbon是个很古老很古老的控件,附带的RibbonWindow也十分古老。RibbonWindow在以前应该可以运行良好,但多年没有更新,在.NET 4.5(或者说是WIN7平台,我没仔细考究)后就出现了这个问题。作为专业软件这可能没法接受,而这个问题微软好像也没打算修复。以前的做法通常是使用Fluent.Ribbon之类的第三方组件,因为我已经在Kino.Toolkit.Wpf中提供了使用WindowChrome自定义的Window,为了统一外观于是顺手自定义一个ExtendedRibbonWindow

    2. 问题产生的原因

    RibbonWindow是派生自Window,并使用了WindowChrome,它的ControlTemplate大概是这样:

    <Grid>
        <Border Name="PART_ClientAreaBorder"
                Background="{TemplateBinding Control.Background}"
                BorderBrush="{TemplateBinding Control.BorderBrush}"
                BorderThickness="{TemplateBinding Control.BorderThickness}"
                Margin="{Binding Path=(SystemParameters.WindowNonClientFrameThickness)}" />
        <Border BorderThickness="{Binding Path=(WindowChrome.WindowChrome).ResizeBorderThickness, RelativeSource={RelativeSource TemplatedParent}}">
            <Grid>
                <Image Name="PART_Icon"
                       WindowChrome.IsHitTestVisibleInChrome="True"
                       HorizontalAlignment="Left"
                       VerticalAlignment="Top"
                       Width="{Binding Path=(SystemParameters.SmallIconWidth)}"
                       Height="{Binding Path=(SystemParameters.SmallIconHeight)}" />
                <AdornerDecorator>
                    <ContentPresenter Name="PART_RootContentPresenter" />
                </AdornerDecorator>
                <ResizeGrip Name="WindowResizeGrip"
                            WindowChrome.ResizeGripDirection="BottomRight"
                            HorizontalAlignment="Right"
                            VerticalAlignment="Bottom"
                            Visibility="Collapsed"
                            IsTabStop="False" />
            </Grid>
        </Border>
    </Grid>

    Ribbon的Chrome部分完全依赖于WindowChrome,PART_ClientAreaBorder负责为ClientArea提供背景颜色。在PART_ClientAreaBorder后面的另一个Border才是真正的ClientArea部分,它用于放置Ribbon。因为Ribbon的一些按钮位于标题栏,所以Ribbon必须占用标题栏的位置,并且由Ribbon显示原本应该由Window显示的标题。WindowChrome的标题栏高度是SystemParameters.WindowNonClientFrameThickness.Top,在Windows 10,100% DPI的情况下为27像素。而Ribbon标题栏部分使用了SystemParameters.WindowCaptionHeight作为高度,这个属性的值为23,所以才会出现对不齐的问题。

    <DockPanel Grid.Column="0" Grid.ColumnSpan="3" LastChildFill="True" Height="{Binding Path=(SystemParameters.WindowCaptionHeight)}">

    而最大化的时候完全没有调整Ribbon的Margin,并且WindowChrome本身在最大化就会有问题。所以不能直接使用WindowChrome,而应该使用自定义的UI覆盖WindowChrome的内容。

    3. 自定义RibbonWindow

    3.png

    我在Kino.Toolkit.Wpf提供了一个自定义RibbonWindow,基本上代码和ControlTempalte与自定义Window一样,运行效果如上图所示。在自定义RibbonWindow里我添加了RibbonStyle属性,默认值是一个解决Ribbon标题栏问题的Ribbon样式,里面使用SystemParameters.WindowNonClientFrameThickness作为标题的高度。

    <DockPanel Grid.Column="0"
               Grid.ColumnSpan="3"
               Margin="0,-1,0,0"
               Height="{Binding Path=(SystemParameters.WindowNonClientFrameThickness).Top}"
               LastChildFill="True">

    RibbonWindow还添加了一个StyleTypedProperty:

    [StyleTypedProperty(Property = nameof(RibbonStyle), StyleTargetType = typeof(Ribbon))]

    StyleTypedProperty 应用于类定义并确定类型为 TargetType 的属性的 Style。使用了这个属性的控件可以在Blend中使用 "右键"->"编辑其他模板"->"编辑RibbonSytle" 创建Ribbon的Style。

    4.png

    不过虽然我这么贴心地加上这个Attribute,但我的Blend复制Ribbon模板总是报错。

    4. 结语

    我也见过一些很专业的软件没处理RibbonWindow,反正外观上的问题忍一忍就过去了,实在受不了可以买一个有现代化风格的控件库,只是为了标题栏对不齐这种小事比较难说服上面同意引入一个新的组件。除了使用我提供的解决方案,stackoverflow也由不少关于这个问题的讨论及解决方案可供参考,例如这个:

    c# - WPF RibbonWindow + Ribbon = Title outside screen - Stack Overflow

    顺便一提,ExtendedRibbonWindow需要继承RibbonWindow,所以没法直接集成ExtendedWindow。因为ExtendedWindow很多功能都试用附加属性和控件代码分离,所以ExtendedRibbonWindow需要重复的代码不会太多。

    5. 参考

    RibbonWindow Class (System.Windows.Controls.Ribbon) Microsoft Docs

    Ribbon Class (System.Windows.Controls.Ribbon) Microsoft Docs

    WindowChrome Class (System.Windows.Shell) Microsoft Docs

    SystemParameters Class (System.Windows) Microsoft Docs

    StyleTypedPropertyAttribute Class (System.Windows) Microsoft Docs

    6. 源码

    Kino.Toolkit.Wpf_Window at master

    posted on 2019-06-13 13:06 NET未来之路 阅读( ...) 评论( ...) 编辑 收藏

    转载于:https://www.cnblogs.com/lonelyxmas/p/11015950.html

    展开全文
  • WPF自定义美化控件,用于界面的美化。可直接在提供的源码基础上开发出属于自己的一套控件库,便于后续系统开发等
  • 在我们平时的项目中,我们经常需要一套自己的自定义控件库,这个特别是在Prism这种框架下面进行开发的时候,每个人都... 1 在我们的项目中,在解决方案右键-》新建项目,添加“WPF自定义控件库”。 2 在默认生成...
  • [WPF自定义控件库]了解WPF的布局过程,并利用Measure为Expander添加动画 原文:[WPF自定义控件库]了解WPF的布局过程,并利用Measure为Expander添加动画1. 前言 这篇文章介绍WPF UI元素的两步布局过程...
  • 主要包含以下要素: 1.WPF常用控件的重写 2.可以用来实现QQ音乐播放器界面 3.控件内事件的实现 4.毛玻璃效果 另外,该可以直接通过引用使用,有兴趣的同学可以下载学习
  • 1. 前言 上一篇文章介绍了使用Resizer实现Expander简单的...这篇继续Measure的话题,自定义了一个带有动画的ExtendedExpander。 2. ExtendedExpander的需求 使用Resizer实现的简易Expander没办法在折叠时做淡出动画...
  • 上一篇文章介绍了使用WindowChrome自定义Window,实际使用下来总有各种各样的问题,这些问题大部分都不影响使用,可能正是因为不影响使用所以一直没得到修复(也有可能别人根本不觉得这些是问题)。 这篇文章我总结了...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 15,323
精华内容 6,129
关键字:

wpf自定义控件库