Gdy aplikacja desktopowa ma pokazać pliki, użytkowników, produkty albo wyniki wyszukiwania, zwykła lista szybko przestaje wystarczać. Kontrolka ListView w C# pozwala wyświetlać elementy jako prostą listę, tabelę z kolumnami lub widok z ikonami, dlatego dobrze sprawdza się w narzędziach Windows Forms i WPF. Pokażę, kiedy jej używać, jak ją skonfigurować, jak obsługiwać zaznaczenie oraz kiedy lepiej wybrać DataGridView albo inną kontrolkę.
Najważniejsze informacje o kontrolce ListView w C#
- Windows Forms oferuje widoki listy, szczegółów, dużych ikon i małych ikon.
- WPF wykorzystuje ListView głównie z wiązaniem danych i szablonami XAML.
- Widok Details pozwala budować tabelę z kolumnami i dodatkowymi informacjami.
- Przy dużych zbiorach danych lepiej użyć DataGridView, wirtualizacji albo stronicowania.
- Najczęstsze błędy dotyczą braku konfiguracji widoku, niepoprawnego wiązania danych i obsługi zaznaczenia.
Do czego służy ListView w aplikacjach Windows
ListView służy do prezentowania kolekcji obiektów w formie, którą użytkownik może szybko przeglądać. W Windows Forms kontrolka przypomina rozwiązania znane z Eksploratora plików. Jeden element może mieć tekst, ikonę, stan zaznaczenia, pola dodatkowe oraz dane przechowywane w kolekcji SubItems.
W praktyce używam jej wtedy, gdy użytkownik ma nie tylko zobaczyć dane, lecz także wybrać konkretny element, otworzyć szczegóły, zaznaczyć kilka pozycji albo przełączyć sposób prezentacji. Dobrym przykładem jest lista plików, gdzie jedna kolumna pokazuje nazwę, druga rozmiar, a trzecia datę modyfikacji.
Trzeba jednak rozróżnić dwie kontrolki o tej samej nazwie. Windows Forms ListView jest nastawiony na ręczne zarządzanie elementami i klasyczny interfejs desktopowy. WPF ListView znacznie częściej działa z wiązaniem danych, szablonami oraz językiem XAML.
| Technologia | Najczęstszy sposób pracy | Dobre zastosowanie |
|---|---|---|
| Windows Forms | Dodawanie ListViewItem w kodzie C# | Listy plików, proste panele administracyjne, starsze aplikacje firmowe |
| WPF | ItemsSource, DataTemplate i binding | Nowoczesne interfejsy, widoki MVVM, elastyczne układy danych |
| DataGridView | Siatka danych z edycją i sortowaniem | Duże tabele, formularze biznesowe, dane wymagające edycji |
Moja praktyczna zasada jest prosta. Jeżeli ważny jest wybór elementu i sposób jego prezentacji, ListView będzie dobrym kandydatem. Jeżeli najważniejsza jest masowa edycja danych, sortowanie wielu kolumn i praca jak w arkuszu, zwykle szybciej osiągnę dobry efekt za pomocą DataGridView.
Jak skonfigurować ListView w Windows Forms
W projekcie Windows Forms dodaj kontrolkę ListView z przybornika Visual Studio albo utwórz ją w kodzie. Najważniejszą właściwością jest View, ponieważ określa sposób prezentacji danych. Dla układu tabelarycznego ustawiam wartość View.Details, a następnie dodaję kolumny.
using System;
using System.Drawing;
using System.Windows.Forms;
public partial class MainForm : Form
{
private readonly ListView ordersListView = new();
public MainForm()
{
InitializeComponent();
ConfigureListView();
LoadOrders();
}
private void ConfigureListView()
{
ordersListView.Dock = DockStyle.Fill;
ordersListView.View = View.Details;
ordersListView.FullRowSelect = true;
ordersListView.GridLines = true;
ordersListView.MultiSelect = false;
ordersListView.HideSelection = false;
ordersListView.Columns.Add("Numer", 90);
ordersListView.Columns.Add("Klient", 180);
ordersListView.Columns.Add("Status", 120);
ordersListView.Columns.Add("Data", 120);
ordersListView.SelectedIndexChanged += OrdersListView_SelectedIndexChanged;
Controls.Add(ordersListView);
}
private void LoadOrders()
{
var firstOrder = new ListViewItem("1001");
firstOrder.SubItems.Add("Anna Kowalska");
firstOrder.SubItems.Add("Nowe");
firstOrder.SubItems.Add("2026-09-16");
var secondOrder = new ListViewItem("1002");
secondOrder.SubItems.Add("Piotr Nowak");
secondOrder.SubItems.Add("Wysłane");
secondOrder.SubItems.Add("2026-09-15");
ordersListView.Items.Add(firstOrder);
ordersListView.Items.Add(secondOrder);
}
private void OrdersListView_SelectedIndexChanged(
object? sender,
EventArgs e)
{
if (ordersListView.SelectedItems.Count == 0)
{
return;
}
ListViewItem selectedItem = ordersListView.SelectedItems[0];
string orderNumber = selectedItem.Text;
Text = $"Wybrane zamówienie: {orderNumber}";
}
}Pierwszy tekst przekazany do konstruktora ListViewItem trafia do właściwości Text, czyli pierwszej kolumny. Każdy kolejny element dodany przez SubItems.Add odpowiada następnej kolumnie. To drobny szczegół, ale właśnie tutaj początkujący często próbują tworzyć osobne obiekty dla każdej komórki, choć w tym modelu komórki są częścią jednego elementu.
Właściwość FullRowSelect sprawia, że zaznaczenie obejmuje cały wiersz. HideSelection = false pozwala zachować widoczne zaznaczenie po przejściu fokusu na inną kontrolkę. W aplikacji z panelem szczegółów ustawienie obu właściwości zwykle wyraźnie poprawia czytelność.
Najważniejsze tryby widoku
-
View.Listpokazuje prostą listę tekstową. -
View.Detailstworzy widok kolumnowy z nagłówkami. -
View.LargeIconprezentuje duże ikony i podpisy. -
View.SmallIconwykorzystuje mniejsze ikony, dzięki czemu mieści więcej elementów. -
View.Tilepozwala pokazać element w formie kafelka z dodatkowymi informacjami.
Do danych biznesowych najczęściej wybieram View.Details. Widoki ikon mają sens przy plikach, zdjęciach, skrótach lub typach dokumentów. Nie stosowałbym ich do długiej listy zamówień, ponieważ użytkownik traci wtedy możliwość szybkiego porównania wartości w kolumnach.
Jak dodawać dane, ikony i reakcję na zaznaczenie
Najprostsze listy można wypełnić ręcznie, ale w prawdziwej aplikacji dane zwykle pochodzą z bazy, pliku JSON albo wywołania API. W takim przypadku warto oddzielić pobieranie danych od ich prezentacji. Dzięki temu kontrolka odpowiada za interfejs, a kod domenowy za przygotowanie informacji.
Przy większej liczbie elementów dodaję je partiami przez BeginUpdate i EndUpdate. Zapobiega to odświeżaniu interfejsu po każdym dodanym wierszu i ogranicza migotanie kontrolki.
private void DisplayProducts(IEnumerable products)
{
ordersListView.BeginUpdate();
try
{
ordersListView.Items.Clear();
foreach (Product product in products)
{
var item = new ListViewItem(product.Code);
item.SubItems.Add(product.Name);
item.SubItems.Add(product.Price.ToString("C"));
item.SubItems.Add(product.Stock.ToString());
item.Tag = product;
ordersListView.Items.Add(item);
}
}
finally
{
ordersListView.EndUpdate();
}
}
private sealed class Product
{
public string Code { get; init; } = string.Empty;
public string Name { get; init; } = string.Empty;
public decimal Price { get; init; }
public int Stock { get; init; }
} Właściwość Tag jest bardzo użyteczna, bo pozwala przechować cały obiekt powiązany z wierszem. Dzięki temu po kliknięciu nie muszę odtwarzać produktu na podstawie tekstu w kolumnach. Pobieram go bezpośrednio z SelectedItems[0].Tag, co jest bezpieczniejsze i wygodniejsze przy dalszej rozbudowie aplikacji.
Ikony wymagają użycia ImageList. Do dużych i małych widoków można przypisać osobne listy obrazów, a konkretny element wskazuje ikonę przez ImageIndex albo ImageKey.
var icons = new ImageList
{
ImageSize = new Size(20, 20)
};
icons.Images.Add("available", Properties.Resources.Available);
icons.Images.Add("unavailable", Properties.Resources.Unavailable);
ordersListView.SmallImageList = icons;
var item = new ListViewItem("Produkt A")
{
ImageKey = "available"
};
item.SubItems.Add("Dostępny");
ordersListView.Items.Add(item);Ikona powinna wspierać informację tekstową, a nie ją zastępować. Sam kolor lub symbol nie wystarczy, gdy użytkownik ma rozpoznać status zamówienia, szczególnie przy ustawieniach systemowych zwiększających kontrast.
Obsługa kliknięcia i dwukliku
Do zwykłego reagowania na zmianę zaznaczenia używam SelectedIndexChanged. Do otwierania formularza szczegółów wygodniejszy jest DoubleClick albo zdarzenie ItemActivate. Przed odczytem danych zawsze sprawdzam, czy element faktycznie został zaznaczony.
private void OrdersListView_ItemActivate(
object? sender,
EventArgs e)
{
if (ordersListView.SelectedItems.Count == 0)
{
return;
}
if (ordersListView.SelectedItems[0].Tag is Product product)
{
using var detailsForm = new ProductDetailsForm(product);
detailsForm.ShowDialog(this);
}
}Takie sprawdzenie ma znaczenie także przy usuwaniu elementu, zmianie filtrów i odświeżaniu listy. Zdarzenie może wywołać się w momencie, gdy zaznaczenie właśnie zniknęło, więc odwołanie do indeksu bez kontroli często kończy się wyjątkiem.
ListView w WPF i wiązanie danych
WPF korzysta z tej samej nazwy kontrolki, ale podejście jest inne. Zamiast tworzyć każdy wiersz ręcznie, zwykle ustawiam ItemsSource i pozwalam, aby XAML opisał sposób prezentacji obiektu. Taki model dobrze pasuje do architektury MVVM, w której widok nie powinien znać szczegółów pobierania danych.
W tym przykładzie GridView odpowiada za kolumny, a właściwości obiektu są wskazywane przez binding. Jeśli zmieni się kolekcja ObservableCollection
public partial class ProductsViewModel : ObservableObject
{
public ObservableCollection Products { get; } = new();
[ObservableProperty]
private Product? selectedProduct;
public ProductsViewModel()
{
Products.Add(new Product
{
Code = "A-100",
Name = "Klawiatura",
Price = 199.00m,
Stock = 14
});
}
} WPF daje większą swobodę wyglądu. Za pomocą ItemTemplate można zbudować kartę produktu z ikoną, dwoma opisami i przyciskiem akcji. Z drugiej strony oznacza to więcej decyzji dotyczących bindingu, stylów i szablonów. Jeśli tworzę mały, klasyczny formularz, Windows Forms bywa po prostu szybsze.
Przeczytaj również: var w JavaScripcie - zakres, hoisting i pułapki starego kodu
Windows Forms czy WPF
Nie traktuję jednego podejścia jako zawsze lepszego. Wybór zależy od tego, czy ważniejsza jest szybkość zbudowania prostego narzędzia, czy elastyczność interfejsu i rozdzielenie warstw.
| Kryterium | Windows Forms ListView | WPF ListView |
|---|---|---|
| Próg wejścia | Niższy przy prostych formularzach | Wyższy przez XAML i binding |
| Układ tabelaryczny | Wbudowany widok Details | GridView w XAML |
| Personalizacja wyglądu | Ograniczona, ale szybka | Bardzo duża dzięki szablonom |
| Architektura MVVM | Możliwa, lecz mniej naturalna | Dobrze wspierana |
| Ręczne zarządzanie elementami | Proste i bezpośrednie | Zwykle zastępowane bindingiem |
Kiedy ListView nie jest najlepszym wyborem
ListView dobrze radzi sobie z prezentacją, ale nie jest uniwersalną tabelą. Gdy użytkownik ma edytować wiele komórek, sortować dane po kilku kolumnach, filtrować tysiące rekordów i korzystać z walidacji, DataGridView w Windows Forms będzie zwykle praktyczniejszy.
WPF-owy ListView także nie rozwiązuje automatycznie problemu dużych kolekcji. Przy kilku tysiącach elementów trzeba sprawdzić wirtualizację, sposób generowania kontenerów oraz koszt szablonów. Ciężki ItemTemplate z wieloma obrazami może spowolnić przewijanie bardziej niż sama liczba rekordów.
| Potrzeba | Najlepszy pierwszy wybór | Dlaczego |
|---|---|---|
| Lista plików z ikonami | ListView | Obsługa ikon i kilku trybów widoku |
| Tabela z edycją komórek | DataGridView | Gotowe mechanizmy edycji, sortowania i walidacji |
| Prosta lista wyboru jednej wartości | ListBox lub ComboBox | Mniejszy narzut i prostszy interfejs |
| Hierarchia folderów i kategorii | TreeView | Naturalna prezentacja relacji rodzic-dziecko |
| Karty produktów i niestandardowy wygląd | WPF ListView | Szablony pozwalają zaprojektować własny układ |
Najczęstszy błąd polega na wyborze ListView tylko dlatego, że potrafi pokazać kolumny. Sama obecność kolumn nie czyni z niego pełnoprawnego edytora danych. Zanim dodam kontrolkę, spisuję trzy podstawowe akcje użytkownika: czyta, wybiera czy edytuje. Jeśli odpowiedź brzmi głównie „edytuje”, zaczynam od DataGridView albo innego komponentu tabelarycznego.
Typowe błędy i sposoby ich uniknięcia
Najbardziej widoczny błąd w Windows Forms to dodanie kolumn bez ustawienia View.Details. Kolumny istnieją w kolekcji, ale użytkownik ich nie zobaczy, jeśli kontrolka pracuje w trybie List albo LargeIcon. Zawsze konfiguruję widok razem z kolumnami, najlepiej w jednym miejscu.
Drugi problem to traktowanie tekstu z kolumn jako głównego źródła danych. Gdy format ceny, daty albo statusu się zmieni, kod zaczyna parsować tekst interfejsu. Przechowywanie obiektu w Tag w Windows Forms lub użycie bindingu w WPF utrzymuje dane i prezentację w osobnych rolach.
- Nie dodawaj tysięcy elementów pojedynczo bez
BeginUpdateiEndUpdate. - Nie odwołuj się do
SelectedItems[0]bez sprawdzenia, czy lista zawiera zaznaczenie. - Nie ustawiaj na sztywno zbyt wąskich kolumn, jeśli tekst pochodzi od użytkownika.
- Nie używaj ikon jako jedynego oznaczenia statusu.
- Nie pobieraj danych z bazy synchronicznie w wątku interfejsu, gdy lista może zawierać wiele rekordów.
- Nie mieszaj ręcznego dodawania elementów z bindingiem w jednym widoku bez wyraźnego powodu.
Przy odświeżaniu danych szczególnie uważałbym na utratę zaznaczenia. Wyczyszczenie kolekcji i ponowne dodanie elementów może sprawić, że panel szczegółów nagle stanie się pusty. W aplikacjach, w których użytkownik często filtruje listę, przechowuję identyfikator wybranego obiektu i próbuję przywrócić zaznaczenie po zakończeniu odświeżania.
Kolumny powinny mieć sensowne szerokości, ale nie muszą być idealnie dopasowane do każdego tekstu. Funkcja automatycznego dopasowania do zawartości może być kosztowna przy dużych zbiorach, dlatego przy dynamicznych danych lepiej ustalić rozsądne minimum i pozwolić użytkownikowi zmienić szerokość.
Jak podjąć dobrą decyzję w konkretnym projekcie
Jeśli buduję panel z listą plików, ikonami, zaznaczeniem i dwuklikiem otwierającym szczegóły, wybieram ListView bez większych obaw. W Windows Forms konfiguruję widok Details, dodaję kolumny i przechowuję obiekt domenowy w Tag. W WPF zaczynam od modelu danych, ObservableCollection i ItemsSource.
Jeśli aplikacja ma prezentować kilkanaście tysięcy rekordów, od razu planuję filtrowanie po stronie źródła, stronicowanie albo wirtualizację. Sama kontrolka nie sprawi, że duży zbiór stanie się lekki. Największą różnicę robi sposób pobierania i aktualizowania danych, a dopiero później wybór konkretnego widoku.
Ostatecznie ListView jest dobrym narzędziem do list, które mają być czytelne, wybieralne i czasem wzbogacone o ikony lub dodatkowe kolumny. Wybieram je świadomie wtedy, gdy interfejs ma przede wszystkim prezentować dane. Gdy pojawia się intensywna edycja, rozbudowane sortowanie albo bardzo duża tabela, sięgam po kontrolkę zaprojektowaną dokładnie do tych zadań.
Najbezpieczniejszy pierwszy krok to przygotowanie małego prototypu z kilkunastoma prawdziwymi rekordami. Sprawdź w nim zaznaczanie, filtrowanie, zmianę rozmiaru okna i odświeżanie danych. Taki test szybko pokaże, czy ListView pasuje do aplikacji, zanim logika interfejsu zdąży rozrosnąć się wokół niewłaściwego wyboru.
