WPF Grid Layout

Michele Santucci 1 Punto di reputazione
2025-01-02T10:38:02.5933333+00:00

I'm writing a simple WPF .Net 4.8 application, this application should list a set of pictures (as thumbnails) as a multiple columns grid and:

  • load a set of pictures from a startup folder;
  • allow to drag and drop new pictures onto the list;
  • reorder images into the list;
  • delete pictures.

Of course the number of images it's not predictable since the startup folder could also be empty. I fixed the number of columns (4) and the size (100x150) of thumbnails.

Since I'm not an expert in WPF I tried with different approaches as long as I learn/drill into WPF details.

This's my first very basic attempt:

<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        Title="Thumbnail Grid Viewer" 
        Height="450" 
        Width="800"
        AllowDrop="True" 
        Drop="Window_Drop" 
        DragOver="Window_DragOver">
    <Grid Name="MainGrid">
        <ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
            <UniformGrid Name="ThumbnailGrid" 
                         Columns="4"
                         AllowDrop="True"
                         Drop="ThumbnailGrid_Drop"
                         PreviewMouseLeftButtonDown="ThumbnailGrid_PreviewMouseLeftButtonDown"
                         DragOver="ThumbnailGrid_DragOver" />
        </ScrollViewer>
    </Grid>
</Window>
using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace WpfApp1
{
    public partial class MainWindow : Window
    {
        private ObservableCollection<ImageItem> _imageItems;
        private ImageItem _draggedItem;
        private Point _dragStartPoint;

        public MainWindow()
        {
            InitializeComponent();
            _imageItems = new ObservableCollection<ImageItem>();
            LoadImagesFromFolder(Properties.Settings.Default.ImagesFolder); // Sostituisci con il percorso della tua cartella
            PopulateGrid();
        }

        private void LoadImagesFromFolder(string folderPath)
        {
            if (Directory.Exists(folderPath))
            {
                var imageFiles = Directory.GetFiles(folderPath);
                foreach (var imagePath in imageFiles)
                {
                    if (IsImageFile(imagePath))
                    {
                        _imageItems.Add(new ImageItem { ImagePath = imagePath });
                    }
                }
            }
        }

        private bool IsImageFile(string filePath)
        {
            string[] validExtensions = { ".jpg", ".jpeg", ".png", ".bmp", ".gif", ".tiff" };
            string fileExtension = Path.GetExtension(filePath).ToLower();
            return Array.Exists(validExtensions, ext => ext == fileExtension);
        }

        private void PopulateGrid()
        {
            ThumbnailGrid.Children.Clear();
            
             foreach (var imageItem in _imageItems)
            {
                var stackPanel = new StackPanel
                {
                    Orientation = Orientation.Vertical
                };

                var imageControl = new Image
                {
                    Source = new BitmapImage(new Uri(imageItem.ImagePath)),
                    Width = 100,
                    Height = 100,
                    Margin = new Thickness(5)
                };
                imageControl.MouseLeftButtonDown += ImageControl_MouseLeftButtonDown;

                var deleteButton = new Button
                {
                    Content = "Delete",
                    Margin = new Thickness(5)
                };
                deleteButton.Click += (s, e) => DeleteImage(imageItem);

                stackPanel.Children.Add(imageControl);
                stackPanel.Children.Add(deleteButton);

                ThumbnailGrid.Children.Add(stackPanel);
            }
        }

        private void DeleteImage(ImageItem imageItem)
        {
            _imageItems.Remove(imageItem);
            PopulateGrid();
        }

        private void ImageControl_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            if (sender is Image image && image.Source is BitmapImage bitmapImage)
            {
                _dragStartPoint = e.GetPosition(null);
                _draggedItem = new ImageItem { ImagePath = bitmapImage.UriSource.ToString() };
                DragDrop.DoDragDrop(image, _draggedItem, DragDropEffects.Move);
            }
        }

        private void ThumbnailGrid_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            _dragStartPoint = e.GetPosition(null);
        }

        private void ThumbnailGrid_Drop(object sender, DragEventArgs e)
        {
            HandleDrop(e);
        }

        private void Window_Drop(object sender, DragEventArgs e)
        {
            HandleDrop(e);
        }

        private void HandleDrop(DragEventArgs e)
        {
            if (e.Data.GetDataPresent(DataFormats.FileDrop))
            {
                string[] droppedFiles = (string[])e.Data.GetData(DataFormats.FileDrop);
                foreach (string filePath in droppedFiles)
                {
                    if (IsImageFile(filePath))
                    {
                        _imageItems.Add(new ImageItem { ImagePath = filePath });
                    }
                }
                PopulateGrid();
            }
            else if (e.Data.GetDataPresent(typeof(ImageItem)))
            {
                var droppedItem = (ImageItem)e.Data.GetData(typeof(ImageItem));
                var dropPosition = e.GetPosition(ThumbnailGrid);
                var targetIndex = GetIndexFromPoint(dropPosition);
                if (_draggedItem != null)
                {
                    _imageItems.Remove(_draggedItem);
                    _imageItems.Insert(targetIndex, _draggedItem);
                    PopulateGrid();
                }
            }
        }

        private void Window_DragOver(object sender, DragEventArgs e)
        {
            e.Effects = DragDropEffects.Move;
        }

        private void ThumbnailGrid_DragOver(object sender, DragEventArgs e)
        {
            e.Effects = DragDropEffects.Move;
        }

        private int GetIndexFromPoint(Point point)
        {
            for (int i = 0; i < ThumbnailGrid.Children.Count; i++)
            {
                var element = ThumbnailGrid.Children[i] as UIElement;
                if (element != null)
                {
                    var bounds = VisualTreeHelper.GetDescendantBounds(element);
                    var mousePosition = element.TranslatePoint(point, ThumbnailGrid);
                    if (bounds.Contains(mousePosition))
                    {
                        return i;
                    }
                }
            }
            return ThumbnailGrid.Children.Count;
        }
    }
}

I had some issues with reoder management but the layout is correct:

Grid layout Since I was not sure that reordering issue was fixable into the first approach I tried another attempt (this time a little smarter):

<Window x:Class="WpfApp2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        Title="Image Gallery" 
        Height="450" 
        Width="800" 
        AllowDrop="True" 
        Drop="Image_Drop">
    <Grid Name="MainGrid">
        <ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
            <UniformGrid Columns="3" >
                <ItemsControl ItemsSource="{Binding Images}">
                    <ItemsControl.ItemTemplate>
                        <DataTemplate>
                            <Grid>
                                <Image Source="{Binding}" Width="100" Height="100" Stretch="UniformToFill"
                                       PreviewMouseMove="Image_PreviewMouseMove"
                                       Drop="Image_DropReorder"
                                       DragOver="Image_DragOver"/>
                                <Button Content="X" HorizontalAlignment="Right" VerticalAlignment="Top"
                                        Width="20" Height="20" Click="DeleteImage"/>
                            </Grid>
                        </DataTemplate>
                    </ItemsControl.ItemTemplate>
                </ItemsControl>
            </UniformGrid>
        </ScrollViewer>
    </Grid>
</Window>
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace WpfApp2
{
    public partial class MainWindow : Window
    {
        public ObservableCollection<string> Images { get; set; }
        public MainWindow()
        {
            InitializeComponent();
            Images = new ObservableCollection<string>();
            DataContext = this;
            LoadImagesFromFolder(WpfApp2.Properties.Settings.Default.ImagesFolder);
        }
        private void LoadImagesFromFolder(string folderPath)
        {
            if (Directory.Exists(folderPath))
            {
                var imageFiles = Directory.GetFiles(folderPath)
                                          .Where(IsImageFile);
                foreach (var file in imageFiles)
                {
                    Images.Add(file);
                }
            }
            else
            {
                MessageBox.Show("The specified folder does not exist.", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
            }
        }
        private void Image_Drop(object sender, DragEventArgs e)
        {
            if (e.Data.GetDataPresent(DataFormats.FileDrop))
            {
                string[] files = (string[])e.Data.GetData(DataFormats.FileDrop);
                foreach (var file in files)
                {
                    if (IsImageFile(file))
                    {
                        Images.Add(file);
                    }
                }
            }
        }
        private bool IsImageFile(string filePath)
        {
            string[] validExtensions = { ".jpg", ".jpeg", ".png", ".bmp", ".gif" };
            string extension = Path.GetExtension(filePath)?.ToLower();
            return validExtensions.Contains(extension);
        }
        private void DeleteImage(object sender, RoutedEventArgs e)
        {
            if (sender is Button button && button.DataContext is string imagePath)
            {
                Images.Remove(imagePath);
            }
        }
        private void Image_PreviewMouseMove(object sender, MouseEventArgs e)
        {
            if (e.LeftButton == MouseButtonState.Pressed && sender is Image image && image.DataContext is string imagePath)
            {
                DragDrop.DoDragDrop(image, imagePath, DragDropEffects.Move);
            }
        }
        private void Image_DragOver(object sender, DragEventArgs e)
        {
            e.Effects = DragDropEffects.Move;
            e.Handled = true;
        }
        private void Image_DropReorder(object sender, DragEventArgs e)
        {
            if (e.Data.GetDataPresent(DataFormats.StringFormat))
            {
                string droppedImage = (string)e.Data.GetData(DataFormats.StringFormat);
                if (sender is Image targetImage && targetImage.DataContext is string targetPath)
                {
                    int oldIndex = Images.IndexOf(droppedImage);
                    int newIndex = Images.IndexOf(targetPath);
                    if (oldIndex >= 0 && newIndex >= 0 && oldIndex != newIndex)
                    {
                        Images.Move(oldIndex, newIndex);
                    }
                }
            }
        }
    }
}

The second approach is fully functional and I like to proceed this way but the layout it's not what I was expecting:

Listview like layout

I would like to understand what differences between the two xaml layouts lead to such a difference in rendering and of course I would like to know if it is possibile to achieve the correct grid layout within the second approach?

If yes : what changes are needed?

I tought the problem was ItemsControl size but if I drill into the xaml hierarchy within Visual Studio down to the ItemsControl element it seems to be correctly (automatically) horizzontally sized to 200 (800/4) then it should not be the problem. It seems that there's just some hidden detail that implies that ItemsControls inside UniformGrids are placed in different rows.

I tried with ListView, GridView without much luck unless an overcomplicate xaml which was impossibile to reorder.

The only fully working result I got was using Win UI 3 instead WPF (because GridView there natively allows reordering) but this requires .NET instead .NET Framework (unluckly I need to use an assembly not compatible with .NET xx).

C#
C#
Un linguaggio di programmazione orientato agli oggetti e indipendente dai tipi che ha le sue radici nella famiglia di linguaggi C e include il supporto per la programmazione orientata ai componenti.
10 domande
0 commenti Nessun commento
{count} voti

Risposta

Le risposte possono essere contrassegnate come risposte accettate dall'autore della domanda. Ciò consente agli utenti di sapere che la risposta ha risolto il problema dell'autore.