Панель виртуализации, которая не обрезает элементы

Таким образом, выглядит очень глупо иметь коллекцию без хрома, если элементы обрезаются/обрезаются в конце области прокрутки.

Я хочу создать панель виртуализации для коллекций (ItemsControl/ListBox), которая рисует только целые элементы, а не их части. Например:

 ______________
|              |
|______________|
 ______________
|              |
|______________|
 ______________
|              |

введите здесь описание изображения

Я не хочу, чтобы 3-й частичный контейнер отображался, если нет места для отображения ВСЕГО элемента/контейнера. В примерах третий элемент был обрезан из-за нехватки места.

Какие-либо предложения? Должен ли я попытаться изобрести велосипед (создать свой собственный VirtualizingWholeItemPanel)?

ИЗМЕНИТЬ:

Microsoft пояснила, что VirtualizingPanel.ScrollUnit вообще не предназначен для выполнения этой функции. Похоже, что VirtualizingPanel.ScrollUnit служит той же цели, что и старый CanContentScroll на ScrollViewer.


person Josh G    schedule 23.02.2012    source источник
comment
Вы меняете значение VirtualizingPanel.ScrollUnit? Потому что по умолчанию установлено значение Item, а это то, что вам нужно. Возможно, опубликуйте некоторый код и снимки экрана, и, поскольку это предварительная версия для разработчиков, вы могли бы отправить заявку на устранение неполадок в Microsoft Connect, если это действительно ошибка?   -  person myermian    schedule 23.02.2012
comment
Я явно устанавливаю ScrollUnit в Item. Я отредактирую и добавлю свой пример кода.   -  person Josh G    schedule 23.02.2012
comment
Насколько я знаю, это особенность виртуализации. Для скорости он не переоценивает ширину при повторном использовании контейнеров дисплея. Вы можете попробовать выравнивание по горизонтали = растяжение. Или вы можете перебрать список строк и явно установить ширину, чтобы вместить наибольшую. Или вы можете установить фиксированную ширину, которая, по вашему мнению, подойдет больше всего, и включить обтекание текстом.   -  person paparazzo    schedule 23.02.2012
comment
Я не беспокоюсь о ширине, а только о отображении целых / полных контейнеров. Эти элементы не обрезаются, потому что хост недостаточно широк. Они обрезаются ScrollViewer.   -  person Josh G    schedule 23.02.2012
comment
Их обрезают, потому что не хватает места для обоих. Конечно, это не тот эффект, который вам нужен, но попробуйте. Поместите явную ширину как в ListBox, так и в TextBox. Сделайте ListBox на 10 больше, чем TextBox. Вы также можете использовать конвертер, чтобы сделать TextBox на 10 меньше, чем ListBox.   -  person paparazzo    schedule 23.02.2012
comment
Попробуйте удалить оба прикрепленных свойства CacheLength... возможно, они мешают?   -  person myermian    schedule 23.02.2012
comment
Если вы установите видимость для вертикальной полосы прокрутки, то она, вероятно, освободит место для первого набора, но, возможно, не будет учитывать более крупный контент позже. Я часто получаю это с GridView, и я использую преобразователь, чтобы установить ширину одного столбца из фактической ширины GridView - ширина других столбцов - 10 для полосы прокрутки. Я мирюсь с этим, потому что GridView работает быстро.   -  person paparazzo    schedule 23.02.2012
comment
Я сделал все следующее: установил ширину ListBox на 280 и высоту на 180. Удалил свойства CacheLength и CacheLengthUnit. Добавлен ScrollViewer.VerticalScrollBarVisibility=Visible. Установите для сетки в DataTemplate значение Ширина = 180. Нет кости, тот же результат. Эта проблема не имеет ничего общего с шириной. Я действительно не хочу показывать полосу прокрутки, если прокрутка не требуется. Я создал отчет об ошибке в MicrosoftConnect.   -  person Josh G    schedule 23.02.2012
comment
В моем комментарии говорилось, что TextBlock не Grid. Попробуйте установить ширину TextBlock на 180 (не шаблон или сетку в шаблоне). Разве это не TextBlock, который обрезается?   -  person paparazzo    schedule 23.02.2012
comment
Нет, это ContentPresenter.   -  person Josh G    schedule 23.02.2012
comment
Понимаю. Я попробовал только текст и выключил виртуализацию, и он все равно обрезался. Я думал, что исправил это однажды. Извините, я в принципе ничем не помог.   -  person paparazzo    schedule 23.02.2012
comment
Возможно, вам следует дополнить свой вопрос новой информацией, которую Microsoft предоставила вам в вашем отчет об ошибке Microsoft Connect :)   -  person BoltClock    schedule 08.03.2012


Ответы (1)


У меня есть вспомогательный метод, который я использую, чтобы определить, частично или полностью виден элемент управления в родительском контейнере. Вероятно, вы можете использовать его с Converter для определения видимости элементов.

Ваш преобразователь должен либо вычислить родительский контейнер из элемента пользовательского интерфейса (в моем блоге есть набор Visual Tree Helpers, которые могут помочь с этим, если хотите), или это может быть MultiConverter, который принимает как элемент пользовательского интерфейса, так и родительский контейнер в качестве параметров.

ControlVisibility ctrlVisibility= 
    WPFHelpers.IsObjectVisibleInContainer(childControl, parentContainer);

if (ctrlVisibility == ControlVisibility.Full 
    || isVisible == ControlVisibility.FullHeightPartialWidth)
{
    return Visibility.Visible;
}
else
{
    return = Visibility.Hidden;
}

Код для определения видимости элемента управления в его родительском элементе выглядит следующим образом:

public enum ControlVisibility
{
    Hidden,
    Partial,
    Full,
    FullHeightPartialWidth,
    FullWidthPartialHeight
}


/// <summary>
/// Checks to see if an object is rendered visible within a parent container
/// </summary>
/// <param name="child">UI element of child object</param>
/// <param name="parent">UI Element of parent object</param>
/// <returns>ControlVisibility Enum</returns>
public static ControlVisibility IsObjectVisibleInContainer(
    FrameworkElement child, UIElement parent)
{
    GeneralTransform childTransform = child.TransformToAncestor(parent);
    Rect childSize = childTransform.TransformBounds(
        new Rect(new Point(0, 0), new Point(child.ActualWidth, child.ActualHeight)));

    Rect result = Rect.Intersect(
        new Rect(new Point(0, 0), parent.RenderSize), childSize);

    if (result == Rect.Empty)
    {
        return ControlVisibility.Hidden;
    }
    if (Math.Round(result.Height, 2) == childSize.Height 
        && Math.Round(result.Width, 2) == childSize.Width)
    {
        return ControlVisibility.Full;
    }
    if (result.Height == childSize.Height)
    {
        return ControlVisibility.FullHeightPartialWidth;
    }
    if (result.Width == childSize.Width)
    {
        return ControlVisibility.FullWidthPartialHeight;
    }
    return ControlVisibility.Partial;
}

Изменить

Провел некоторые тесты, и, по-видимому, конвертер запускается до того, как элементы управления фактически отображаются. Как хак, это будет работать, если вы используете MultiConverter и передаете ему ActualHeight элемента управления, что заставит преобразователь переоценить, когда элемент управления будет отрисован.

Вот конвертер, который я использовал:

public class TestConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        FrameworkElement child = values[0] as FrameworkElement;
        var parent = VisualTreeHelpers.FindAncestor<ListBox>(child);

        ControlVisibility ctrlVisibility =
            VisualTreeHelpers.IsObjectVisibleInContainer(child, parent);

        if (ctrlVisibility == ControlVisibility.Full
            || ctrlVisibility == ControlVisibility.FullHeightPartialWidth)
        {
            return Visibility.Visible;
        }
        else
        {
            return Visibility.Hidden;
        }
    }

    public object[] ConvertBack(object value, Type[] targetType, object parameter, CultureInfo culture)
    {
        return null;
    }
}

Я использовал XAML, который вы разместили в своем вопросе, и просто добавил неявный стиль для ListBoxItem в .Resources

<ListBox.Resources>
    <Style TargetType="{x:Type ListBoxItem}">
        <Setter Property="Visibility">
            <Setter.Value>
                <MultiBinding Converter="{StaticResource Converter}">
                    <Binding RelativeSource="{RelativeSource Self}" />
                    <Binding RelativeSource="{RelativeSource Self}" Path="ActualHeight" />
                </MultiBinding>
            </Setter.Value>
        </Setter>
    </Style>
</ListBox.Resources>
person Rachel    schedule 23.02.2012
comment
ControlVisibility можно превратить в перечисление [Flags]. Ничего не меняется в игре, просто предложение. - person myermian; 23.02.2012
comment
@ m-y: я не думаю, что есть какая-то причина делать это перечислением флагов. Значение никогда не должно быть равно более чем одной из возможностей. Насколько я понимаю, перечисления флагов используются для объединения нескольких значений вместе с логическими значениями &, |. - person Josh G; 23.02.2012
comment
Что ж, Full, FullHeightPartialWidth и FullWidthPartialHeight кажутся связанными значениями. - person myermian; 23.02.2012
comment
@m-y Первоначальной целью сценария была замена списков кнопок раскрывающимися меню, когда они превышали допустимую ширину, и некоторые шаблоны позволяли частично отображать элементы, а другие - нет. Я чувствовал, что перечисление более информативно, чем флаги, и Джош прав в том, что элемент управления никогда не вернет более одного значения ControlVisibility. - person Rachel; 23.02.2012
comment
Полдюжины или другой, это не было спросом. Это было просто предложение совпадать с тем, как ведет себя код .NET: например, посмотрите на CompareOptions, где он помечен Flags, даже несмотря на то, что некоторые комбинации могут не иметь смысла. Это будет работать в любом случае, точно так же, как передача 5 вместо (2 + 3) одинакова. - person myermian; 23.02.2012
comment
Итак, я попробовал это в своем сценарии, но, похоже, он не очень хорошо справляется с гибкими макетами. Ширина и высота дочернего элемента не заданы, поэтому функция IsObjectVisibleInContainer не работала должным образом. Я изменил его на ActualHeight/Width, а затем на DesiredSize и RenderSize, но ни один из них не был инициализирован. Кажется, что привязка инициализируется до того, как пройдет измерение/аранжировка, и после этого она не обновляется... - person Josh G; 23.02.2012
comment
@JoshG Странно, DataBind обрабатывается после Render, поэтому ActualHeight/ActualWidth должно иметь значение. В прошлом я всегда использовал его в событиях Loaded и SizeChanged объектов, но теперь мне любопытно, будет ли он работать в преобразователе. - person Rachel; 23.02.2012
comment
@JoshG Я обновил свой ответ с помощью работающего конвертера. Вы правы в том, что преобразователь оценивается до того, как объект будет визуализирован, поэтому, чтобы заставить его переоценить, я переключился на использование MultiConverter с ActualHeight в качестве одного из связанных значений. Я также переключил IsObjectVisibleInContainer на использование ActualHeight/ActualWidth вместо Height/Width. - person Rachel; 23.02.2012