Не “проводник”, а визуальное пространство документов, что-то ближе к COSMOS (от Waves)-подходу, где файлы живут плитками, имеют вес, цвет, превью, теги, статус и историю.
Всем привет. :?*(*%:%=)
Ниже разбор, как мы шли от красивых, но тяжёлых плиток к более живому и быстрому интерфейсу на C#
И давайте сразу разберемся с плитками (Тут только о плитках)

Плитки в документном интерфейсе нужны не для красоты. Их смысл в том, чтобы пользователь быстрее понимал, что перед ним, не открывая файл.
Сразу скажу, не будет тут скриншотов, вот сразу 1 добавим, дальше просто по делу. Это крутая игровая C# Статья. Просто читайте, она простая.

Простой файловый лист хорошо работает, когда кто-то уже знает название документа. Но как только документов становится много, названия перестают помогать. scan_001.pdf, договор_final_2.docx, отчет_новый.xlsx, image_25.png выглядят одинаково бесполезно. Пользователь начинает открывать файлы один за другим, теряет контекст и время.
Плитка решает другую задачу: она превращает файл в визуальный объект. Документ становится не строкой в проводнике, а маленькой карточкой с признаками: превью, тип, дата, размер, теги, источник, статус, важность. Человек считывает не только имя файла, а сразу несколько сигналов.
Мы делаем не проводник, а визуальное пространство документов. Плитки позволяют показать документы как карту: где свежее, где старое, где PDF, где изображения, где подозрительные дубликаты, где то, что требует проверки.
Но у плиток есть опасность: если сделать их слишком большими, интерфейс превращается в витрину с пустым воздухом. Если сделать слишком маленькими, пропадает смысл превью. Поэтому плитка должна быть адаптивной. Большие карточки могут показывать больше информации: превью, название, путь, теги, дату, размер. Средние показывают главное. Маленькие оставляют только визуальный отпечаток, тип и короткое имя.
Так плитки становятся не просто способом отображения, а частью навигации. Размер, цвет, превью и положение помогают пользователю быстрее находить нужное. Хорошая плитка отвечает на вопрос “что это?” до клика. Отличная плитка отвечает ещё и на вопрос “почему мне это важно?”.
C# + WPF/XAML
Как бы показать плитки?
Первую версию плиток мы делали не как финальный дизайн, а как быстрый кликабельный прототип. Цель была простая: проверить, удобно ли воспринимать документы не списком, а визуальными карточками.
В WPF плитка удобно собирается из обычных XAML-блоков: Border, Grid, Image, TextBlock, ItemsControl. Получается карточка, где слева находится превью документа, справа название и короткая метаинформация.
<Border Padding="10" CornerRadius="8" BorderThickness="1" BorderBrush="#D9DEE6" Background="White"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="96" /> <ColumnDefinition Width="*" /> </Grid.ColumnDefinitions> <Border Width="88" Height="64" CornerRadius="6" Background="#EEF1F4"> <Image Source="{Binding PreviewImage}" Stretch="UniformToFill" /> </Border> <StackPanel Grid.Column="1" Margin="12,0,0,0"> <TextBlock Text="{Binding Name}" FontSize="16" FontWeight="SemiBold" TextTrimming="CharacterEllipsis" /> <TextBlock Text="{Binding MetaLine}" Margin="0,6,0,0" FontSize="13" Foreground="#687386" TextTrimming="CharacterEllipsis" /> </StackPanel> </Grid> </Border>
На стороне C# у плитки есть достаточно примитивная модель представления. Она берёт данные файла и готовит их в удобном для интерфейса виде: имя, тип, дату, размер, путь, теги и превью.
public sealed class DocumentCardViewModel { public string Name { get; } public string TypeLabel { get; } public string MetaLine { get; } public string FullPath { get; } public IReadOnlyList<string> Tags { get; } public ImageSource? PreviewImage { get; set; } public DocumentCardViewModel(DocumentFile document) { Name = document.Name; TypeLabel = document.Kind; FullPath = document.FullPath; Tags = document.Tags; MetaLine = $"{document.Kind} · {document.ModifiedAt:dd MMM} · {FormatSize(document.SizeBytes)}"; } private static string FormatSize(long bytes) { if (bytes < 1024) { return $"{bytes} B"; } if (bytes < 1024 * 1024) { return $"{bytes / 1024.0:0.#} KB"; } return $"{bytes / 1024.0 / 1024.0:0.#} MB"; } }
После этого мы добавили теги. На самом деле, это важно: плитка должна быть не просто красивым прямоугольником, а рабочим элементом навигации. Теги дают быстрый способ понять, что за документ перед нами.

<ItemsControl ItemsSource="{Binding Tags}" Margin="0,10,0,0"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <WrapPanel /> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <ItemsControl.ItemTemplate> <DataTemplate> <Border Background="#F1F5F9" CornerRadius="10" Padding="8,3" Margin="0,0,6,6"> <TextBlock Text="{Binding}" FontSize="12" Foreground="#64748B" /> </Border> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl>
Т9 мог превратить слово «UI» (юай / интерфейс), «UWP» или «уме» (в моем уме / в коде) в слово «юмор».
Тут не смеемся)
Первая наивная версия могла выглядеть так: есть ItemsControl, внутри WrapPanel, а каждая карточка просто добавляется в общий поток.
<ItemsControl ItemsSource="{Binding Documents}"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <WrapPanel /> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> </ItemsControl>
Для прототипа это наверное гуд. Для зрелого приложения думаю не оч. WrapPanel не виртуализирует элементы: если документов тысяча, WPF пытается создать и измерить тысячу визуальных карточек. Даже если пользователь видит только 20.
Поэтому мы ушли от “просто накидать плитки в WrapPanel” к контролируемой раскладке. Вместо хаотичного потока документы группируются в строки, а интерфейс рисует только то, что нужно прямо сейчас.
Прикинул:
public sealed class DocumentRowViewModel { public IReadOnlyList<DocumentCardViewModel> Items { get; } public DocumentRowViewModel(IReadOnlyList<DocumentCardViewModel> items) { Items = items; } }
На стороне интерфейса мы показываем уже не бесконечную простыню плиток, а список строк:
<ListBox ItemsSource="{Binding FilteredRows}" ScrollViewer.CanContentScroll="True" VirtualizingPanel.IsVirtualizing="True" VirtualizingPanel.VirtualizationMode="Recycling"> <ListBox.ItemTemplate> <DataTemplate> <ItemsControl ItemsSource="{Binding Items}"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <UniformGrid Rows="1" /> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> </ItemsControl> </DataTemplate> </ListBox.ItemTemplate> </ListBox>
Это уже ближе к нормальной архитектуре. ListBox виртуализирует строки, а внутри строки лежит ограниченное количество плиток. То есть WPF больше не обязан одновременно держать в визуальном дереве все документы.
Дальше мы сделали сетку адаптивной. Количество плиток в строке зависит от ширины рабочей области. Если окно широкое, плиток больше. Если пользователь сжал правую или левую панель, сетка пересчтывается.
private int CalculateColumns(double availableWidth) { const double minTileWidth = 220; const int minColumns = 2; const int maxColumns = 8; var columns = (int)(availableWidth / minTileWidth); return Math.Clamp(columns, minColumns, maxColumns); }
После этого исчезает проблема, когда на большом экране видно только три колонки и половина пространства пустует. Плитки начинают занимать рабочую область плотнее.
Но плотность не должна убивать читаемость. Поэтому плитка не просто сжимается до бесконечности. У неё есть минимальный размер, а количество колонок ограничено. Если сделать слишком много колонок, карточки превратятся в шум.
Следующий шаг: разные режимы отображения.
public enum DocumentViewMode { Cosmos, Compact, List }
Cosmos нужен для визуального пространства: крупнее превью, больше контекста, удобнее узнавать документы глазами.
Compact нужен для плотной работы: меньше воздуха, больше документов на экране.
List нужен для точного сравнения: путь, дата, размер, тип, статусы.
Так плитки перестают быть единственным способом отображения. Они становятся одним из режимов, который хорош для визуального поиска. А когда пользователю нужна бухгалтерская точность, он переключается в список.

Как лечь под плитку?
Плитки в нашем прототипе родились как быстрый способ проверить идею: документы должны восприниматься не как строки в проводнике, а как визуальное пространство. Поэтому первую версию мы набросали на C# + WPF/XAML: это удобно для быстрого Windows-прототипа, можно быстро собрать карточки, фильтры, боковые панели, превью и проверить механику интерфейса.
Упрощённая плитка выглядела примерно так:
<Border Padding="10" CornerRadius="8" BorderThickness="1" BorderBrush="#D9DEE6" Background="White"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="96" /> <ColumnDefinition Width="*" /> </Grid.ColumnDefinitions> <Image Source="{Binding PreviewImage}" Width="88" Height="64" Stretch="UniformToFill" /> <StackPanel Grid.Column="1" Margin="12,0,0,0"> <TextBlock Text="{Binding Name}" FontSize="16" FontWeight="SemiBold" TextTrimming="CharacterEllipsis" /> <TextBlock Text="{Binding MetaLine}" FontSize="13" Foreground="#687386" /> </StackPanel> </Grid> </Border>
А данные для неё собирались в простой ViewModel:
public sealed class DocumentCardViewModel { public string Name { get; } public string MetaLine { get; } public string FullPath { get; } public ImageSource? PreviewImage { get; set; } public DocumentCardViewModel(DocumentFile document) { Name = document.Name; FullPath = document.FullPath; MetaLine = $"{document.Kind} · {document.ModifiedAt:dd MMM} · {FormatSize(document.SizeBytes)}"; } private static string FormatSize(long bytes) { if (bytes < 1024) { return $"{bytes} B"; } if (bytes < 1024 * 1024) { return $"{bytes / 1024.0:0.#} KB"; } return $"{bytes / 1024.0 / 1024.0:0.#} MB"; } }
Но это был именно прототип, а не финальная архитектура.
Проблема C# + WPF в такой задаче не в том, что на нём невозможно сделать красиво. Возможно. Проблема в другом: когда начинаешь делать “COSMOS для документов”, быстро упираешься в производительность, контроль рендера и предсказуемость поведения интерфейса.
Плитки должны быть очень быстрыми. Скролл не должен ждать превью. Наведение не должно подвешивать UI. Перетаскивание таймлайна не должно пересчитывать весь экран на каждом пикселе. Добавление источника не должно замораживать приложение. Если пользователь листает тысячи документов, интерфейс должен оставаться живым.
В WPF для этого приходится постоянно договариваться с фреймворком: виртуализация, recycling, ленивые превью, Dispatcher.Yield, кэширование изображений, ограничение количества визуальных элементов, осторожная работа с биндингами. Это работает, но ощущается как борьба с системой.
Поэтому финальная версия для меня логичнее на Rust + egui и своей библиотеке плиток.
Там плитка становится не XAML-объектом с большим визуальным деревом, а лёгкой структурой данных и прямым отрисованным элементом:
pub struct DocumentTile { pub id: DocumentId, pub name: String, pub kind: DocumentKind, pub modified_at: DateTime<Utc>, pub size_bytes: u64, pub tags: Vec<String>, pub preview: Option<TextureId>, }
Рендер можно держать под своим контролем:
fn draw_tile(ui: &mut egui::Ui, tile: &DocumentTile, rect: egui::Rect) -> egui::Response { let response = ui.allocate_rect(rect, egui::Sense::click()); let painter = ui.painter(); painter.rect_filled(rect, 8.0, egui::Color32::from_rgb(255, 255, 255)); painter.rect_stroke( rect, 8.0, egui::Stroke::new(1.0, egui::Color32::from_rgb(217, 222, 230)), ); if let Some(texture) = tile.preview { let preview_rect = egui::Rect::from_min_size( rect.min + egui::vec2(10.0, 10.0), egui::vec2(96.0, 72.0), ); painter.image( texture, preview_rect, egui::Rect::from_min_max(egui::Pos2::ZERO, egui::pos2(1.0, 1.0)), egui::Color32::WHITE, ); } painter.text( rect.min + egui::vec2(118.0, 14.0), egui::Align2::LEFT_TOP, &tile.name, egui::FontId::proportional(16.0), egui::Color32::from_rgb(31, 39, 51), ); response }
И дальше уже строить свою библиотеку плиток: с виртуальным скроллом, собственным layout engine, кэшем превью, разными размерами карточек, плотной сеткой, правым кликом, hover-состояниями, таймлайном и быстрым пересчётом видимой области.
То есть WPF был полезен как быстрый черновик: он помог понять, какие плитки нужны, как они должны вести себя, где появляется пустое пространство, где тормозит скролл, где нужны превью, теги и контекстные действия.
Но финальная мысль такая: если интерфейс должен ощущаться как отдельный быстрый документный космос, а не как улучшенный проводник, лучше не пытаться бесконечно приручать чужую модель рендера. Лучше сделать свой слой плиток: Rust + egui + собственная библиотека, где каждый пиксель, каждый hover и каждый кадр скролла находятся под контролем приложения.
Я ненавижу шарп.
Эта статья не про то, что C# плохой. И даже не про то, что WPF плохой. Для прототипа они оказались полезны: можно быстро накидать окно, плитки, фильтры, боковые панели, превью и проверить, есть ли вообще жизнь в идее “COSMOS для документов”.
Но лично я C# не люблю.
Не потому что на нём нельзя писать рабочие приложения. Можно (мало того, он один из лучших). Просто в задачах, где интерфейс должен быть очень быстрым, плотным и предсказуемым, я не хочу постоянно спорить с фреймворком. Я не хочу думать, почему сегодня виртуализация сработала, а завтра нет. Почему скролл подвисает из-за визуального дерева. Почему биндинг потянул за собой лишний пересчёт. Почему простая плитка внезапно становится тяжёлым объектом. (Я Зануда ребят, мне нужно каждый software кадр вылезать)
В этой статье C# был нужен как строительные леса. Мы использовали его, чтобы быстро понять форму продукта: какие должны быть плитки, как показывать превью, зачем нужны теги, почему важна плотность, как должен работать таймлайн, где появляются тормоза и что бесит в реальном использовании.
В целом забавная статья, про-то как сегодня можно разобрать плитку, разрабатывая коммерческий продукт.
C# помог быстро нащупать идею.
Мой совет для разрабов, C# позволяет стилить качественную плитку, разумеется сложно но научившись вы будете лучшими.
Честно говоря, я не люблю C# и вообще с подозрением отношусь к технологиям, которые не причиняют достаточно боли. Но приходится признать: сегодня это один из флагманских инструментов для Windows-разработки, с сильными фреймворками и, если не трогать macOS, пожалуй, одними из лучших возможностей для реализации плиточных интерфейсов.
Если статья оказалась полезной, буду рад обсудить детали в комментариях.
Спасибо, что дочитали.
