WPF Grid Layout
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:
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:
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).