在WinRT/UWP中创建自定义虚拟化控件
本文关键字:自定义 虚拟化 控件 创建 WinRT UWP | 更新日期: 2023-09-27 18:24:58
在WPF中,FrameworkElement
派生类可以通过AddVisualChild
提供自己的子类。通过这种方式,可以实现您自己的虚拟化控件,这些控件只生成可见的子控件。此外,您还可以在没有后台集合的情况下生成子项。
我想使用这种技术将几个控件从WPF移植到Windows10UWP,但不清楚如何在WinRTUI中正确实现虚拟化。因为在我对这个问题的原始版本的评论中,有人说,对于Stack Overflow来说,询问实现技术太笼统了。我创建了一个极简主义的例子来解释我试图涵盖的关键功能,它们是
- 从数据模型动态生成子控件
- 为生成的子控件执行自定义布局逻辑
我做了以下考虑:
- 据我所见,自定义控件不可能像WPF中那样管理自己的子控件
- 我排除了
Panel
子类,因为当我的自定义控件被(其他人)使用时,很容易出错。面板子级应该由包含XAML的控制,而不是由面板控制 - 我排除了
ItemsControl
子类,因为它不可能提供备份集合(数据虚拟化是一项要求)
(注意,排除它们可能是一个错误,所以如果是,请指出。)
下面的WPF代码创建了一个无限滚动的日期带,但只具体化了当前可见的单元格。我有意保持它尽可能的极简主义,所以它没有多大意义,但它确实提供了我上面提到的两个关键功能,我需要了解如何在WinRT中实现这些功能。
所以我的问题是:有可能在WinRT中创建这样一个控件,动态构建其子控件以显示无限滚动带吗?请记住,它需要是自包含的,以便放置在任意页面上,而不必在页面中包含额外的代码(否则它就不是一个可重用的控件)。
如果您已经知道如何实现虚拟化,并且可以给我一些提示,那么我认为这足以作为一个答案来概述如何在WinRT中实现它。
WPF来源:
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace Sandbox
{
public class DateBand : FrameworkElement
{
public static readonly DependencyProperty ScrollOffsetProperty = DependencyProperty.Register(
nameof(ScrollOffset), typeof(double), typeof(DateBand), new FrameworkPropertyMetadata {
AffectsMeasure = true,
});
public double ScrollOffset
{
get { return (double)GetValue(ScrollOffsetProperty); }
set { SetValue(ScrollOffsetProperty, value); }
}
public static readonly DependencyProperty CellTemplateProperty = DependencyProperty.Register(
nameof(CellTemplate), typeof(DataTemplate), typeof(DateBand), new FrameworkPropertyMetadata {
AffectsMeasure = true,
});
public DataTemplate CellTemplate
{
get { return (DataTemplate)GetValue(CellTemplateProperty); }
set { SetValue(CellTemplateProperty, value); }
}
private List<DateCell> _cells = new List<DateCell>();
private DateTime _startDate = new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1);
private const double cSlotWidth = 5;
private const double cSlotHeight = 20;
protected override int VisualChildrenCount => _cells.Count;
protected override Visual GetVisualChild(int index) => _cells[index];
protected override Size MeasureOverride(Size availableSize)
{
int usedCells = 0;
double desiredWidth = 0;
double desiredHeight = 0;
if (!double.IsPositiveInfinity(availableSize.Height))
{
var index = (int)Math.Floor(ScrollOffset);
var offset = (index - ScrollOffset) * cSlotHeight;
while (offset < availableSize.Height)
{
DateCell cell;
if (usedCells < _cells.Count)
{
cell = _cells[usedCells];
}
else
{
cell = new DateCell();
AddVisualChild(cell);
_cells.Add(cell);
}
usedCells++;
var cellValue = _startDate.AddMonths(index);
cell._offset = offset;
cell._width = DateTime.DaysInMonth(cellValue.Year, cellValue.Month) * cSlotWidth;
cell.Content = cellValue;
cell.ContentTemplate = CellTemplate;
cell.Measure(new Size(cell._width, cSlotHeight));
offset += cSlotHeight;
index++;
desiredHeight = Math.Max(desiredHeight, offset);
desiredWidth = Math.Max(desiredWidth, cell._width);
}
}
if (usedCells < _cells.Count)
{
for (int i = usedCells; i < _cells.Count; i++)
RemoveVisualChild(_cells[i]);
_cells.RemoveRange(usedCells, _cells.Count - usedCells);
}
return new Size(desiredWidth, desiredHeight);
}
protected override Size ArrangeOverride(Size finalSize)
{
foreach (var cell in _cells)
cell.Arrange(new Rect(0, cell._offset, cell._width, cell.DesiredSize.Height));
return finalSize;
}
}
public class DateCell : ContentControl
{
internal double _offset;
internal double _width;
}
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void Window_MouseWheel(object sender, MouseWheelEventArgs e)
{
Band.SetCurrentValue(DateBand.ScrollOffsetProperty, Band.ScrollOffset - e.Delta / Mouse.MouseWheelDeltaForOneLine);
}
}
}
WPF XAML:
<Window x:Class="Sandbox.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Sandbox"
MouseWheel="Window_MouseWheel">
<DockPanel>
<ScrollBar x:Name="Scroll" Orientation="Vertical" Minimum="-24" Maximum="+24" ViewportSize="6"/>
<local:DateBand x:Name="Band" ScrollOffset="{Binding ElementName=Scroll, Path=Value, Mode=OneWay}">
<local:DateBand.CellTemplate>
<DataTemplate>
<Border BorderBrush="Black" BorderThickness="1" Padding="5,2">
<TextBlock Text="{Binding StringFormat='yyyy - MMMM'}"/>
</Border>
</DataTemplate>
</local:DateBand.CellTemplate>
</local:DateBand>
</DockPanel>
</Window>
根据评论中的要求,我发布了我最终得到的解决方案。我只找到了使用某种Panel子类的解决方案,所以我想出了将控件分为两部分的折衷方案,以避免控件的用户意外地干扰子集合。
因此,我实际上有两个主要类,一个Control子类公开公共API(如依赖属性)并支持主题化,另一个Panel子类实现实际的虚拟化。两者都通过XAML模板链接,如果有人在预期控件之外使用Panel子类,则Panel子类将拒绝执行任何工作。
完成后,虚拟化非常简单,与WPF中的方法没有太大区别——只需修改面板的子项,例如在MeasureOverride中。
为了便于说明,我将问题中的代码移植到UWP,如下所示:
UWP来源:
[TemplatePart(Name = PanelPartName, Type = typeof(DateBandPanel))]
public class DateBand : Control
{
private const string PanelPartName = "CellPanel";
public static readonly DependencyProperty ScrollOffsetProperty = DependencyProperty.Register(
nameof(ScrollOffset), typeof(double), typeof(DateBand), new PropertyMetadata(
(double)0, new PropertyChangedCallback((d, e) => ((DateBand)d).HandleScrollOffsetChanged(e))));
private void HandleScrollOffsetChanged(DependencyPropertyChangedEventArgs e)
{
_panel?.InvalidateMeasure();
}
public double ScrollOffset
{
get { return (double)GetValue(ScrollOffsetProperty); }
set { SetValue(ScrollOffsetProperty, value); }
}
public static readonly DependencyProperty CellTemplateProperty = DependencyProperty.Register(
nameof(CellTemplate), typeof(DataTemplate), typeof(DateBand), new PropertyMetadata(
null, new PropertyChangedCallback((d, e) => ((DateBand)d).HandleCellTemplateChanged(e))));
private void HandleCellTemplateChanged(DependencyPropertyChangedEventArgs e)
{
_panel?.InvalidateMeasure();
}
public DataTemplate CellTemplate
{
get { return (DataTemplate)GetValue(CellTemplateProperty); }
set { SetValue(CellTemplateProperty, value); }
}
private DateBandPanel _panel;
public DateBand()
{
this.DefaultStyleKey = typeof(DateBand);
}
protected override void OnApplyTemplate()
{
if (_panel != null)
_panel._band = null;
base.OnApplyTemplate();
_panel = GetTemplateChild(PanelPartName) as DateBandPanel;
if (_panel != null)
_panel._band = this;
}
}
public class DateBandPanel : Panel
{
internal DateBand _band;
private List<DateCell> _cells = new List<DateCell>();
private DateTime _startDate = new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1);
private const double cSlotWidth = 5;
private const double cSlotHeight = 26;
protected override Size MeasureOverride(Size availableSize)
{
int usedCells = 0;
double desiredWidth = 0;
double desiredHeight = 0;
if (!double.IsPositiveInfinity(availableSize.Height) && _band != null)
{
var index = (int)Math.Floor(_band.ScrollOffset);
var offset = (index - _band.ScrollOffset) * cSlotHeight;
while (offset < availableSize.Height)
{
DateCell cell;
if (usedCells < _cells.Count)
{
cell = _cells[usedCells];
}
else
{
cell = new DateCell();
Children.Add(cell);
_cells.Add(cell);
}
usedCells++;
var cellValue = _startDate.AddMonths(index);
cell._offset = offset;
cell._width = DateTime.DaysInMonth(cellValue.Year, cellValue.Month) * cSlotWidth;
cell.Content = new CellData(cellValue);
cell.ContentTemplate = _band.CellTemplate;
cell.Measure(new Size(cell._width, cSlotHeight));
offset += cSlotHeight;
index++;
desiredHeight = Math.Max(desiredHeight, offset);
desiredWidth = Math.Max(desiredWidth, cell._width);
}
}
if (usedCells < _cells.Count)
{
for (int i = usedCells; i < _cells.Count; i++)
Children.Remove(_cells[i]);
_cells.RemoveRange(usedCells, _cells.Count - usedCells);
}
return new Size(desiredWidth, desiredHeight);
}
protected override Size ArrangeOverride(Size finalSize)
{
foreach (var cell in _cells)
cell.Arrange(new Rect(0, cell._offset, cell._width, cell.DesiredSize.Height));
return finalSize;
}
}
public class CellData
{
public DateTime Date { get; }
public CellData(DateTime date) { this.Date = date; }
}
public class DateCell : ContentControl
{
internal double _offset;
internal double _width;
}
public class FormattingConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value == null)
return null;
if (parameter == null)
return value.ToString();
return ((IFormattable)value).ToString((string)parameter, CultureInfo.CurrentCulture);
}
object IValueConverter.ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotSupportedException();
}
}
public sealed partial class MainPage : Page
{
public MainPage()
{
this.InitializeComponent();
}
private void Page_PointerWheelChanged(object sender, PointerRoutedEventArgs e)
{
Scroll.Value -= e.GetCurrentPoint(this).Properties.MouseWheelDelta / 120.0;
}
}
UWP XAML页面:
<Page x:Class="Sandbox.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Sandbox"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
PointerWheelChanged="Page_PointerWheelChanged">
<Page.Resources>
<local:FormattingConverter x:Key="FormattingConverter"/>
</Page.Resources>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<ScrollBar x:Name="Scroll" Grid.Column="0" Orientation="Vertical" IndicatorMode="MouseIndicator" Minimum="-24" Maximum="+24" ViewportSize="6"/>
<local:DateBand x:Name="Band" Grid.Column="1" ScrollOffset="{Binding ElementName=Scroll, Path=Value, Mode=OneWay}">
<local:DateBand.CellTemplate>
<DataTemplate x:DataType="local:CellData">
<Border BorderBrush="Black" BorderThickness="1" Padding="5,2">
<TextBlock Text="{x:Bind Path=Date, Converter={StaticResource FormattingConverter}, ConverterParameter='yyyy - MMMM'}"/>
</Border>
</DataTemplate>
</local:DateBand.CellTemplate>
</local:DateBand>
</Grid>
</Page>
UWP XAML主题:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:local="using:Sandbox">
<Style TargetType="local:DateBand" >
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:DateBand">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<local:DateBandPanel Name="CellPanel"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
我会选择TemplatedControl,在那里你可以在XAML中定义你想要的控件的结构,这会容易得多,
当我在Codeplex 上构建可视化图表库时,我就是这么做的