2

Здравствуйте, сделал элемент UserControl, который может иметь разные высоту и ширину. Необходимо создать алгоритм который будет при добавлении этого элемента в ScrollViewer(или другой элемент) располагать его в месте наиболее подходящем для него, т. е., например, вот тут:

пример замощения

было бы правильно, если бы "серый" элемент был между синим и зелёным. Ширина ScrollViewer фиксированная.

GetYouFun
  • 432

1 Answers1

3

У вас размещение элементов - часть бизнес-логики, поэтому алгоритм поместим в VM.

Я написал такой простой класс, представляющий ящик:

class BoxVm : Vm
{
    public int Width { get; }
    public int Height { get; }
    public BoxVm(int width, int height)
    {
        Width = width;
        Height = height;
    }
}

И класс, представляющий размещение ящика:

class PlaceVm : Vm
{
    public int X { get; }
    public int Y { get; }
    public BoxVm Box { get; }
    public PlaceVm(int x, int y, BoxVm box)
    {
        X = x;
        Y = y;
        Box = box;
    }
    public bool IsIntersects(PlaceVm place)
    {
        return !(X >= place.X + place.Box.Width
              || X + Box.Width <= place.X
              || Y >= place.Y + place.Box.Height
              || Y + Box.Height <= place.Y);
    }
}

Также в нем есть метод для определения пересекаются ли 2 размещения.

Теперь класс, представляющий склад:

class StorageVm : Vm
{
    public int Width { get; }
    public int Height { get; }
    public StorageVm(int width, int height)
    {
        Width = width;
        Height = height;
        Places = new ObservableCollection<PlaceVm>();
    }

    public ObservableCollection<PlaceVm> Places { get; }

    public PlaceVm AddBox(BoxVm box)
    {
        for (int y = 0; y <= Height - box.Height; ++y)
            for (int x = 0; x <= Width - box.Width; ++x)
            {
                var place = new PlaceVm(x, y, box);
                if (!Places.Any(p => p.IsIntersects(place)))
                {
                    Places.Add(place);
                    return place;
                }
            }
        throw new InvalidOperationException("Невозможно разместить!");
    }
}

Надеюсь, здесь вам всё понятно. Если у вас сетка не ограничена снизу, то просто замените цикл for по y на цикл while, ну и проверку, влазит ли элемент по ширине, надо будет вынести перед циклом и бросать исключение в ней, а не как у меня в конце.

Теперь главная VM:

class MainVm : Vm
{
    int width;
    public int Width
    {
        get => width;
        set => Set(ref width, value);
    }

    int height;
    public int Height
    {
        get => height;
        set => Set(ref height, value);
    }

    public StorageVm Storage { get; }
    public ICommand AddCommand { get; }

    public MainVm()
    {
        Storage = new StorageVm(10, 10);
        AddCommand = new DelegateCommand(_ => Storage.AddBox(new BoxVm(Width, Height)));
    }
}

Здесь тоже всё просто - один склад и одна команда для добавления ящика на склад.

Теперь GUI. Нам потребуется конвертер из ячеек в экранные размеры, он очень простой:

class ScaleConverter : IValueConverter
{
    public int Coeff { get; set; }

    public object Convert(object value, Type targetType,
        object parameter, CultureInfo culture)
    {
        return (int)value * Coeff;
    }

    public object ConvertBack(object value, Type targetType,
        object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

Он просто умножает входное значение на коэффициент.

Теперь разметка:

<Grid Margin="5">
    <Grid.Resources>
        <c:ScaleConverter x:Key="conv" Coeff="20"/>
    </Grid.Resources>
    <Grid.ColumnDefinitions>
        <ColumnDefinition/>
        <ColumnDefinition Width="Auto"/>
    </Grid.ColumnDefinitions>

    <Viewbox>
        <ItemsControl ItemsSource="{Binding Storage.Places}">
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <Canvas IsItemsHost="True"
                            Height="{Binding Storage.Height, Converter={StaticResource conv}}"
                            Width="{Binding Storage.Width, Converter={StaticResource conv}}"/>
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
            <ItemsControl.ItemContainerStyle>
                <Style>
                    <Setter Property="Canvas.Top" Value="{Binding Y, Converter={StaticResource conv}}"/>
                    <Setter Property="Canvas.Left" Value="{Binding X, Converter={StaticResource conv}}"/>
                </Style>
            </ItemsControl.ItemContainerStyle>
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Border Height="{Binding Box.Height, Converter={StaticResource conv}}"
                            Width="{Binding Box.Width, Converter={StaticResource conv}}"
                            BorderThickness="1" BorderBrush="Black"/>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </Viewbox>

    <StackPanel Grid.Column="1" Margin="5,0,0,0">
        <TextBox Text="{Binding Width}"/>

        <TextBox Text="{Binding Height}"/>

        <Button VerticalAlignment="Top" Padding="10,2"
                Content="Add" Command="{Binding AddCommand}"/>
    </StackPanel>
</Grid>

Я разместил ItemsControl в ViewBox, чтобы он был растянут на всё доступное пространство, вы же можете поместить его в ScrollViewer.

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

  • Только эта штука не оптимизирует раскладку... – Qwertiy Apr 02 '18 at 14:20
  • @Qwertiy, она заполняет именно по тому алгоритму, что просил ТС – Андрей NOP Apr 02 '18 at 14:34
  • Ну, возможно... – Qwertiy Apr 02 '18 at 14:37
  • да всё классно, я позже попробую использовать это, и вопрос, что будет при удалении? я так понял, действия при удалении не описаны? – GetYouFun Apr 02 '18 at 14:43
  • Действия при удалении не описаны, да. Но, я надеюсь, вы справитесь. При удалении надо изъять также все элементы, которые находятся ниже удаляемого и добавить их заново с помощью AddBox – Андрей NOP Apr 02 '18 at 14:54
  • да, я когда буду пробовать, конечный вариант добавлю сюда – GetYouFun Apr 02 '18 at 15:11
  • будет ли пересчет при ресайзе? (код не смотрел) –  Apr 02 '18 at 15:33
  • @FoggyFinder, Viewbox же, но автору скорее всего пересчёт не нужен, он же хочет в ScrollViewer помещать – Андрей NOP Apr 02 '18 at 16:10
  • @АндрейNOP, ScrollViewer чтобы можно было скроллить.. сегодня-завтра отпишусь что у меня вышло. попробую добавить реакцию на удаление и прикрутить к своим элементам – GetYouFun Apr 02 '18 at 16:15
  • @GetYouFun, сначала добейтесь чтоб заработало в том виде, как у меня. Потом уже переносите с свой проект. – Андрей NOP Apr 02 '18 at 16:23
  • @АндрейNOP да, я понимаю – GetYouFun Apr 02 '18 at 17:03
  • @АндрейNOP, в какой студии вы писали? я в 2013 пишу (лицензионная потому-что.... и там нету класса DelegateCommand, но я его нашел.. но всё равно в других местах кучу ошибок лепит, я их пытался поправить, но оно не подтягивает.. сейчас попробую в 2017-ой..) – GetYouFun Apr 03 '18 at 12:40
  • VS2017. Недостающие классы: https://pastebin.com/gMXLCHnu – Андрей NOP Apr 03 '18 at 12:51
  • @АндрейNOP, хм.. я первый раз вижу такой способ добавления элементов... т.е. с Binding и не понимаю как это работает, т.е. ошибок в коде нету, я так понимаю что-то ещё нужно дописать? класс MainVm инициализировать(не помогло)? ну в целом я понял алгоритм, и позже попробую всё же к своему прикрутить, в вашем примере просто завязано на "крутом" способе.. в котором я пока не ведаю как делать. – GetYouFun Apr 03 '18 at 15:20
  • Необходимо установить экземпляр MainVm окну в качестве DataContext. Вы никогда не интересовались паттерном MVVM? Можете для начала в конструкторе окна перед InitializeComponent(); написать DataContext = new MainVm(); – Андрей NOP Apr 03 '18 at 16:36
  • @АндрейNOP, да не интересовался, только это назначения и не хватало, а я всё думал как же его назначить.. спасибо, вышло как у вас, теперь попробую использовать в своём. – GetYouFun Apr 03 '18 at 16:52
  • Раз всё получилось - можете пометить ответ галочкой. По MVVM можете посмотреть этот пример: https://ru.stackoverflow.com/a/616413/218063 Заодно увидите как правильно устанавливать DataContext окну (в App.xaml.cs) – Андрей NOP Apr 03 '18 at 17:08