Страницы

Поиск по вопросам

четверг, 5 декабря 2019 г.

Игра на WPF, переносим код WinForms в MVVM

#c_sharp #wpf


Хочу реализовать:
Button расположены квадратом (как 2-мерный массив NxN). 
При клике на кнопку поворачиваются все кнопки в одной строке и в одном столбце. 
Число N настраиваемое.
Начал сначала всё делать в MainWindow.xaml.cs, посоветовали всё сделать нормально
и использовать MVVM. Код MainWindow.xaml.cs:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private Button[,] CreateButtons(int quantity)
    {
        Form.Rows = quantity;
        Form.Columns = quantity;
        Button[,] buttons = new Button[quantity, quantity];
        for (int i = 0; i < quantity; i++)
        {
            for (int j = 0; j < quantity; j++)
            {
                buttons[i, j] = new Button();
                buttons[i, j].Width = 100;
                buttons[i, j].Height = 20;
                buttons[i, j].Margin = new Thickness(5,80,0,0);
                buttons[i, j].Click += new RoutedEventHandler(new_button_click);
            }
        }
        return buttons;
    }

    void new_button_click(object sender, RoutedEventArgs e)
    {
        Button btn = sender as Button;
        if (btn != null)
        {
            var rotateTransform = btn.RenderTransform as RotateTransform;
            var transform = new RotateTransform(90 + (rotateTransform == null ? 0
: rotateTransform.Angle));
            transform.CenterX = 50;
            transform.CenterY = 10;
            btn.RenderTransform = transform;
        }
    }

    private void AddToWrapPanel(int quantity, Button[,] buttons)
    {
        for (int i = 0; i < quantity; i++)
            for (int j = 0; j < quantity; j++)
            {
                Form.Children.Add(buttons[i, j]);
            }
    }

    private int GetQuantityButtons()
    {
        ComboBoxItem item = (ComboBoxItem)comboBox1.SelectedItem;
        int count = int.Parse((string)item.Content);
        return count;
    }

    private void СreateButton_Click(object sender, RoutedEventArgs e)
    {
        if (Form.Children.Count > 0)
            Form.Children.Clear();
        int count = GetQuantityButtons();
        Button[,] buttons = CreateButtons(count);
        AddToWrapPanel(count, buttons);
    }
}


Теперь начинаю всё переносить.

XAML:



    


    
        
        
    
    


Ответы

Ответ 1



Давайте пойдём сначала. Построим VM. Нам пригодится базовый класс для VM, в котором будет имплементация INotifyPropertyChanged: class VM : INotifyPropertyChanged { protected bool Set(ref T field, T value, [CallerMemberName] string propertyName = null) { if (EqualityComparer.Default.Equals(field, value)) return false; field = value; RaisePropertyChanged(propertyName); return true; } protected void RaisePropertyChanged([CallerMemberName] string propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); public event PropertyChangedEventHandler PropertyChanged; } (Если вы пользуетесь каким-нибудь MVVM-фреймворком, то аналогичный базовый класс у вас уже может быть определён.) Теперь VM для одной клетки. Что нам нужно знать? Угол поворота — сделаем из него свойство с INPC. Строку и столбец — эти свойства неизменяемые. И команда, которая будет вызываться при активации клетки. (Она тоже неизменяемая.) Действие, которое будет выполняться при нажатии на клетку, сама клетка выполнить не может, так как поворот происходит у многих клеток. Поэтому реакцию на действие передадим «сверху» в качестве параметра. Получаем такой вот код: class CellVM : VM { public CellVM(int row, int column, Action onActivate) { Row = row; Column = column; Activate = new RelayCommand(() => onActivate(row, column)); } double rotationAngle = 0; public double RotationAngle { get { return rotationAngle; } set { Set(ref rotationAngle, value); } } public int Row { get; } public int Column { get; } public ICommand Activate { get; } } Следующая VM — вся доска. Ей придётся и принимать решение о вращении клеток. Какие нам тут нужны данные? Ширина и высота нужны, и при изменении нужно пересоздать массив клеток. Нужны сами клетки, и поскольку клетки у нас будут подменяться только как целое, берём не ObservableCollection, а просто IEnumerable. Наружу выставлять квадратный массив нельзя, никто не умеет к нему привязываться. Поэтому выставим все клетки, «слитые» в одну общую последовательность. class BoardVM : VM { int width; public int Width { get { return width; } set { if (Set(ref width, value)) { GenerateCells(); } } } int height; public int Height { get { return height; } set { if (Set(ref height, value)) { GenerateCells(); } } } CellVM[,] cells; public IEnumerable AllCells => cells.Cast(); Далее, при изменении количества строк или столбцов нам нужно перегенерировать клетки. void GenerateCells() { var cells = new CellVM[width, height]; for (int row = 0; row < height; row++) for (int column = 0; column < width; column++) cells[column, row] = new CellVM(row, column, OnCellActivate); ShuffleAngles(cells); // отбрасываем существующие клетки this.cells = cells; RaisePropertyChanged(nameof(AllCells)); } ... и установить им случайный начальный угол: static Random random = new Random(); void ShuffleAngles(CellVM[,] cells) { for (int y = 0; y < height; y++) for (int x = 0; x < width; x++) cells[x, y].RotationAngle = random.Next(4) * 90; } Теперь функция, которая вызывается при активации клетки. Нам нужно повернуть все клетки в том же столбце и той же строке. Во втором цикле пропускем уже один раз повёрнутую клетку. void OnCellActivate(int row0, int column0) { for (int row = 0; row < height; row++) Rotate(cells[column0, row]); for (int column = 0; column < width; column++) if (column != column0) Rotate(cells[column, row0]); } void Rotate(CellVM cellVM) { cellVM.RotationAngle = (cellVM.RotationAngle + 90) % 360; } } Окей, с VM более-менее ясно. Переходим к View. Нам понадобится ItemsControl, т. к. мы хотим показать последовательность элементов. У нас последовательность элементов содержится в свойстве AllCells. Далее, нам нужно, чтобы клетки укладывались в UniformGrid. Выберем в качестве носителя UniformGrid, заодно привяжем количество строк и столбцов: Дальше, как показывать отдельную клетку? Вы хотите Button, пускай. Пишем DataTemplate. Вроде бы всё. Теперь нужно ещё задать размеры поля. Для этого, по-хорошему, нужно завести другое окно (и показывать его только в самом начале игры), потому что изменять размер поля во время игры как-то неправильно. Но в нашем быстром прототипе мы закроем на это глаза. (А вам потом придётся таки переделать.) Итак, нам нужна информация о том, сколько у нас возможно строк и столбцов. Возвращаемся в VM и заводим класс: static class GameInfo { static public IEnumerable PossibleColumnNumber { get; } = new[] { 3, 4, 5, 6 }; static public IEnumerable PossibleRowNumber { get; } = new[] { 3, 4, 5, 6 }; } Для красоты, нам нужно инициализировать значения в BoardVM валидным числом. Находим и меняем строки: int width = GameInfo.PossibleColumnNumber.Min(); и int height = GameInfo.PossibleRowNumber.Min(); Теперь View. Дописываем два комбобокса и метки к ним: Весь MainWindow.xaml: Теперь нам нужно прикрепить VM к View. Лучше всего делать это не в XAML, а в App.xaml.cs (смотрите тут). Пишем: public partial class App : Application { BoardVM boardVM = new BoardVM(); protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); new MainWindow() { DataContext = boardVM }.Show(); } } и убираем из App.xaml StartupUri. Компилируем, запускаем. Сразу видим пустое поле. Недоработка, мы ж не сгенерировали поле в конструкторе BoardVM! Исправляем: public BoardVM() { GenerateCells(); } Вот что у меня получилось:

Комментариев нет:

Отправить комментарий