Języki programowania na platformie .NET / Programowanie .NET
Rok akademicki: 2026/27
Instrukcja laboratoryjna nr 3: Eksploracja Systemu Plików (System.IO), Generowanie Raportów OpenXML (EPPlus) oraz Czysta Architektura z DI
Prowadzący: Tomasz Goluch
Wersja: 1.2
1. Wprowadzenie: Standard OpenXML i Generowanie Raportów w .NET
Cel: Zrozumienie budowy dokumentów Office Open XML
(.xlsx), porównanie podejść do programowego generowania
arkuszy kalkulacyjnych na platformie .NET oraz poznanie modelu
licencyjnego i architektury biblioteki EPPlus.
W aplikacjach biznesowych i analitycznych jednym z najczęstszych
wymagań jest eksport przetworzonych danych oraz statystyk do postaci
skoroszytów programu Microsoft Excel. Współczesny format
.xlsx (wprowadzony w standardzie
Office Open XML – OOXML, ECMA-376 / ISO/IEC 29500) nie
jest monolitycznym plikiem binarnym (jak historyczny format
.xls), lecz skompresowanym archiwum ZIP
zawierającym hierarchię dokumentów XML opisujących strukturę danych,
style, relacje oraz elementy graficzne:
[ Plik raportu: Raport.xlsx (Archiwum ZIP) ]
├── [Content_Types].xml (Rejestr typów MIME wszystkich części pakietu)
├── _rels/.rels (Główne powiązania wewnątrz pakietu)
├── docProps/core.xml (Metadane: Autor, Tytuł, Firma, Data utworzenia)
└── xl/
├── workbook.xml (Definicja skoroszytu i lista arkuszy)
├── styles.xml (Czcionki, kolory tła, obramowania, formaty liczb)
├── sharedStrings.xml (Słownik unikalnych ciągów tekstowych – deduplikacja)
├── worksheets/
│ ├── sheet1.xml (Komórki, wartości, formuły i konspekt arkusza 1)
│ └── sheet2.xml (Komórki i dane arkusza 2)
└── drawings/
└── charts/chart1.xml (Definicje wykresów i serii danych)
Porównanie technologii obsługi plików Excel w ekosystemie .NET
| Cecha | Microsoft Office Interop (COM) | Open XML SDK
(DocumentFormat.OpenXml) |
Biblioteka EPPlus
(OfficeOpenXml) |
|---|---|---|---|
| Wymagania systemowe | Wymaga zainstalowanego programu MS Excel i systemu Windows | Brak (czysta biblioteka .NET) | Brak (wieloplatformowa biblioteka .NET 8/9+) |
| Wydajność i stabilność | Bardzo niska (uruchamia proces
EXCEL.EXE w tle, ryzyko wycieków COM) |
Bardzo wysoka (szczególnie w trybie strumieniowym SAX) | Wysoka (optymalizowana pamięć z
RecyclableMemoryStream) |
| Poziom abstrakcji API | Wysoki, ale przestarzały model obiektowy COM | Bardzo niski (ręczne operacje na węzłach drzewa XML) | Wysoki i intuicyjny
(Cells["A1"], stylizacja, wykresy, tabele przestawne) |
| Zastosowanie na serwerze / w Dockerze | Niewspierane przez Microsoft | Wspierane | W pełni wspierane (Linux / Windows / macOS) |
🧠 Zrozumieć ewolucję licencjonowania EPPlus (Wersje 4 vs 5–7 vs 8+):
- EPPlus 4.x: Ostatnia wersja wydana na otwartej licencji LGPL, nieaktualizowana od lat i niewspierająca nowoczesnych funkcji platformy .NET.
- EPPlus 5.x – 7.x: Przejście na model Dual Licensing (komercyjny oraz PolyForm Noncommercial 1.0.0 dla zastosowań edukacyjnych, osobistych i non-profit). Wymagało ustawienia właściwości
ExcelPackage.LicenseContext = LicenseContext.NonCommercial;.- EPPlus 8.x+ (Obecny standard): Właściwość
LicenseContextzostała wycofana (Obsolete) i zastąpiona nową właściwością statycznąExcelPackage.License. Przed utworzeniem pierwszej instancji klasyExcelPackagenależy jawnie zadeklarować kontekst licencji, np. wywołującExcelPackage.License.SetNonCommercialPersonal("Imię Nazwisko"). Brak tej deklaracji skutkuje rzuceniem wyjątkuLicenseNotSetException.
2. Kompilacja Biblioteki EPPlus ze Źródeł i Zarządzanie Referencjami
Cel: Opanowanie pracy z zewnętrznym kodem
otwartoźródłowym, dostosowanie docelowych frameworków (Multi-Targeting),
uruchamianie i debugowanie testów jednostkowych biblioteki oraz
dołączanie lokalnie zbudowanych zestawów .dll.
W codziennej pracy najprostszym sposobem instalacji biblioteki jest użycie menedżera pakietów NuGet:
dotnet add package EPPlus
(lub w Konsoli Menedżera Pakietów Visual Studio:
Install-Package EPPlus -Project <NazwaProjektu>).
W celach inżynierskich i diagnostycznych ogromną wartość daje jednak pobranie i samodzielna kompilacja kodu źródłowego biblioteki. Pozwala to na pracę z najnowszą wersją kodu, analizę architektury testów jednostkowych twórców oraz bezpośrednie wchodzenie debugerem (F11) do wnętrza metod biblioteki.
1. Pobranie repozytorium i konfiguracja docelowych frameworków
- Sklonuj oficjalne repozytorium biblioteki EPPlus z serwisu GitHub:
git clone [https://github.com/EPPlusSoftware/EPPlus.git](https://github.com/EPPlusSoftware/EPPlus.git) - Otwórz rozwiązanie znajdujące się w podkatalogu
src\EPPlus.sln. - Projekty biblioteki oraz testów w oficjalnej solucji wykorzystują
mechanizm Multi-Targeting (kompilację na wiele wersji platformy
.NET jednocześnie, w tym historyczne wersje .NET Framework):
Jeśli na stacji roboczej nie zainstalowano starszych pakietów SDK (Targeting Packs dla<TargetFrameworks>net8.0;net6.0;netstandard2.1;netstandard2.0;net462;net35</TargetFrameworks>.NET Framework 3.5 / 4.6.2), kompilacja wszystkich celów zakończy się błędem. - Aby przyspieszyć kompilację i uniknąć błędów brakujących SDK, w
plikach projektów (
EPPlus.csprojoraz projekcie testowymEPPlusTest.csproj) ogranicz listę docelowych frameworków do wersji używanej w laboratorium (np.net8.0lubnet9.0):<TargetFrameworks>net8.0</TargetFrameworks>
2. Budowanie biblioteki i debugowanie testów jednostkowych
Po zbudowaniu biblioteki (dotnet build lub
Ctrl+Shift+B w Visual Studio) możesz zweryfikować
poprawność jej działania, uruchamiając zestaw testów jednostkowych.
- Aby zbadać działanie wybranej funkcji EPPlus od środka, otwórz plik
z testami jednostkowymi w projekcie
EPPlusTest, postaw pułapkę debugera (F9) w wybranej metodzie oznaczonej atrybutem[TestMethod]i wybierz z menu kontekstowego opcję Debuguj testy (Ctrl+R, Ctrl+T). - Pozwala to na krokowe śledzenie wykonania kodu biblioteki oraz inspekcję stanu wewnętrznego obiektów.
3. Dołączanie zbudowanej biblioteki do własnego projektu
Aby wykorzystać samodzielnie skompilowaną bibliotekę w nowym projekcie (tworzonym w osobnej solucji):
- Dodaj w swoim projekcie bezpośrednią referencję do skompilowanego
pliku
EPPlus.dllorazEPPlus.Interfaces.dll(znajdujących się w katalogusrc\EPPlus\bin\Debug\net8.0\):- W Visual Studio: Kliknij prawym przyciskiem myszy
na węzeł
Zależności (Dependencies)→Dodaj odwołanie do projektu (Add Project Reference...)→ zakładkaPrzeglądaj (Browse)→Przeglądaj...i wskaż plikiEPPlus.dllorazEPPlus.Interfaces.dll. - Bezpośrednio w pliku
.csproj:<ItemGroup> <Reference Include="EPPlus"> <HintPath>..\..\EPPlus\src\EPPlus\bin\Debug\net8.0\EPPlus.dll</HintPath> </Reference> <Reference Include="EPPlus.Interfaces"> <HintPath>..\..\EPPlus\src\EPPlus\bin\Debug\net8.0\EPPlus.Interfaces.dll</HintPath> </Reference> </ItemGroup>
- W Visual Studio: Kliknij prawym przyciskiem myszy
na węzeł
- Nie instaluj pakietu
EPPlusprzez NuGet w tym samym projekcie – doprowadziłoby to do konfliktu i niejednoznaczności typów w przestrzeni nazwOfficeOpenXml. - Ponieważ ręczne dodanie referencji do pliku
.dllnie przenosi automatycznie zależności przechodnich (transitive dependencies) zadeklarowanych w NuGet, środowisko może zgłosić brak bibliotek pomocniczych w czasie uruchomienia. Należy je wówczas doinstalować w swoim projekcie za pomocą NuGet CLI:dotnet add package Microsoft.IO.RecyclableMemoryStream dotnet add package Microsoft.Extensions.Configuration.Json
💡 Wyjaśnienie: Jak działa debugowanie kodu zewnętrznej biblioteki .dll i jak automatycznie wyeksportować wszystkie jej zależności? (Kliknij, aby rozwinąć/zwinąć)
1. Rola plików
.pdb przy referencji binarnej
Gdy dodajesz referencję bezpośrednio do pliku EPPlus.dll
skompilowanego w konfiguracji Debug, w tym samym folderze
znajduje się wygenerowany plik bazy symboli
EPPlus.pdb. Plik ten zawiera pełne
mapowanie instrukcji MSIL na ścieżki plików .cs z Twojego
dysku.
Dopóki nie usuniesz sklonowanego folderu ze źródłami EPPlus,
naciśnięcie klawisza F11 (Step Into) na
wywołaniu np. ws.Cells.AutoFitColumns() w Twojej aplikacji
sprawi, że Visual Studio automatycznie otworzy odpowiedni plik źródłowy
.cs biblioteki EPPlus i pozwoli na debugowanie jej
algorytmów linijka po linijce!
2.
Automatyczne kopiowanie EPPlus.Interfaces.dll podczas
kompilacji
W najnowszych wersjach EPPlus pakiet EPPlus.Interfaces
jest pobierany jako zależność NuGet podczas budowania projektu
EPPlus.csproj. Domyślnie .NET SDK nie kopiuje
bibliotek z pakietów NuGet do folderu bin\Debug\net8.0 dla
projektów będących bibliotekami klas (Class Library).
Aby wymusić skopiowanie pliku EPPlus.Interfaces.dll
(oraz pozostałych zależności) bezpośrednio obok pliku
EPPlus.dll, wystarczy dodać do pliku
src\EPPlus\EPPlus.csproj w sekcji
<PropertyGroup> właściwość:
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
lub zbudować projekt poleceniem
dotnet publish -c Debug.
3.
Operacje na Plikach i Katalogach (System.IO) oraz Odporna
Rekurencja
Cel: Zapoznanie z klasami przestrzeni nazw
System.IO służącymi do eksploracji systemu plików oraz
implementacja bezpiecznego algorytmu rekurencyjnego odpornego na błędy
uprawnień.
Platforma .NET udostępnia w przestrzeni System.IO dwie
grupy klas do pracy z systemem plików:
- Klasy statyczne (
Directory,File,Path): Przeznaczone do pojedynczych, szybkich operacji (każde wywołanie wykonuje osobną weryfikację bezpieczeństwa w systemie operacyjnym). - Klasy instancyjne (
DirectoryInfo,FileInfodziedziczące poFileSystemInfo): Przeznaczone do wielokrotnego odczytu metadanych oraz nawigacji po drzewie katalogów.
1.
Podstawowe operacje na DirectoryInfo oraz
FileInfo
using System.IO;
// Inicjalizacja obiektu reprezentującego katalog
var dir = new DirectoryInfo(@"C:\Projekty");
if (dir.Exists)
{
// Przeglądanie bezpośrednich podkatalogów
foreach (DirectoryInfo subDir in dir.GetDirectories())
{
Console.WriteLine($"[KATALOG] {subDir.Name} | Atrybuty: {subDir.Attributes}");
}
// Przeglądanie plików w bieżącym katalogu
foreach (FileInfo file in dir.GetFiles())
{
string ext = file.Extension; // Rozszerzenie pliku (np. ".cs", ".xlsx")
long sizeBytes = file.Length; // Rozmiar pliku w bajtach
FileAttributes attr = file.Attributes; // Flagi bitowe (np. ReadOnly, Hidden, Archive)
Console.WriteLine($"[PLIK] {file.Name} ({ext}) - {sizeBytes} B [{attr}]");
}
}
⚠️ Pułapka
SearchOption.AllDirectoriesw systemach produkcyjnych:
Metodadir.GetFiles("*", SearchOption.AllDirectories)wydaje się najprostszym sposobem na pobranie wszystkich plików z podkatalogów. Posiada jednak krytyczną wadę: jeśli natrafi na choćby jeden katalog chroniony przez system operacyjny (np.C:\$RECYCLE.BINlubC:\System Volume Information), rzuca wyjątekUnauthorizedAccessExceptioni natychmiast przerywa całe przeszukiwanie, nie zwracając żadnych wyników!
Ponadto nie pozwala na kontrolowanie maksymalnej głębokości zagnieżdżenia (maxDepth).
2. Wzorzec odpornej rekurencji z kontrolą głębokości
Aby aplikacja działała niezawodnie, należy zaimplementować własną
rekurencję (lub iterację ze stosem), przechwytując wyjątki
UnauthorizedAccessException oraz IOException
na poziomie pojedynczego podkatalogu:
public void ScanDirectorySafe(DirectoryInfo currentDir, int currentDepth, int maxDepth, IList<string> errorLog)
{
if (currentDepth > maxDepth)
return;
FileInfo[] files = Array.Empty<FileInfo>();
try
{
files = currentDir.GetFiles();
}
catch (UnauthorizedAccessException ex)
{
errorLog.Add($"Brak uprawnień do odczytu plików w '{currentDir.FullName}': {ex.Message}");
return; // Pomijamy niedostępny katalog, ale kontynuujemy działanie programu!
}
catch (IOException ex)
{
errorLog.Add($"Błąd We/Wy podczas odczytu '{currentDir.FullName}': {ex.Message}");
return;
}
foreach (FileInfo file in files)
{
// Przetwarzanie metadanych pliku (file.Name, file.Extension, file.Length, file.Attributes)...
}
if (currentDepth == maxDepth)
return;
DirectoryInfo[] subDirs = Array.Empty<DirectoryInfo>();
try
{
subDirs = currentDir.GetDirectories();
}
catch (UnauthorizedAccessException ex)
{
errorLog.Add($"Brak dostępu do podkatalogów '{currentDir.FullName}': {ex.Message}");
return;
}
catch (IOException ex)
{
errorLog.Add($"Błąd We/Wy katalogu '{currentDir.FullName}': {ex.Message}");
return;
}
foreach (DirectoryInfo subDir in subDirs)
{
ScanDirectorySafe(subDir, currentDepth + 1, maxDepth, errorLog);
}
}
4. Biblioteka EPPlus: Skoroszyty, Arkusze, Adresowanie i Konspekty Danych
Cel: Opanowanie API biblioteki EPPlus
(OfficeOpenXml) do tworzenia skoroszytów, manipulacji
komórkami, definiowania formuł oraz grupowania wierszy w konspekty
(Outlines).
1.
Konfiguracja licencji i inicjalizacja pakietu
ExcelPackage
Wszystkie główne klasy biblioteki znajdują się w przestrzeni nazw
OfficeOpenXml:
using OfficeOpenXml;
// 1. Deklaracja użycia w kontekście niekomercyjnym (PolyForm Noncommercial) - wymagane w EPPlus 8+
ExcelPackage.License.SetNonCommercialPersonal("Jan Kowalski");
// 2. Określenie ścieżki docelowej pliku (jeśli plik istnieje, usuwamy go przed wygenerowaniem nowego)
var file = new FileInfo("labEpp.xlsx");
if (file.Exists)
{
file.Delete();
}
// 3. Utworzenie pakietu ExcelPackage (implementuje IDisposable)
using var ep = new ExcelPackage(file);
// 4. Ustawienie metadanych dokumentu (właściwości skoroszytu)
ep.Workbook.Properties.Title = "Raport struktury katalogu";
ep.Workbook.Properties.Author = "Jan Kowalski";
ep.Workbook.Properties.Comments = "Wygenerowano automatycznie w .NET";
ep.Workbook.Properties.Company = "Politechnika Gdańska";
ep.Workbook.Properties.SetCustomPropertyValue("WersjaGeneratora", "1.2");
2.
Zarządzanie arkuszami (ExcelWorksheet) i zapis pliku
// Dodanie nowego arkusza do skoroszytu (nazwa arkusza musi być unikalna)
ExcelWorksheet ws = ep.Workbook.Worksheets.Add("Struktura katalogu");
// Pobranie istniejącego arkusza po nazwie lub po indeksie (indeksowanie od 0 w .NET Core / .NET 8+!)
ExcelWorksheet wsByName = ep.Workbook.Worksheets["Struktura katalogu"];
ExcelWorksheet wsByIndex = ep.Workbook.Worksheets[0];
// Zapisanie pakietu na dysk (wymaga obecności przynajmniej jednego arkusza w skoroszycie)
ep.Save();
3. Adresowanie komórek, zakresy i formuły
⚠️ KRYTYCZNA ZASADA – Indeksowanie komórek od 1 (1-based):
W przeciwieństwie do tablic w języku C#, współrzędne komórekws.Cells[wiersz, kolumna], wierszyws.Row(wiersz)oraz kolumnws.Column(kolumna)w EPPlus są indeksowane od 1!
- Pierwszy parametr to numer wiersza (od 1), a drugi to numer kolumny (od 1, gdzie 1 = A, 2 = B, 3 = C...).
- Z kolei w tekstowym adresowaniu Excela (notacja
A1) najpierw podaje się literę kolumny, a następnie numer wiersza.- Tym samym odwołanie
ws.Cells[1, 2]wskazuje dokładnie tę samą komórkę cows.Cells["B1"](wiersz 1, kolumna B).
// Dostęp do pojedynczej komórki po współrzędnych [wiersz, kolumna] oraz po adresie tekstowym:
ws.Cells[1, 2].Value = 14.5; // Komórka B1
ws.Cells["A2"].Formula = "$E5*G$12"; // Komórka A2 z formułą w notacji A1 (bez znaku '=' na początku!)
// Dostęp do prostokątnego zakresu komórek [OdWiersza, OdKolumny, DoWiersza, DoKolumny]:
ws.Cells[5, 7, 9, 10].Value = 14.5; // Zakres G5:J9
ws.Cells["B1:D14"].Formula = "$E$5*G$12"; // Formuła w klasycznej notacji A1 (odwołania bezwzględne/mieszane z '$')
// Formuła w notacji względnej R1C1 (np. wartość z wiersza powyżej pomnożona przez kolumnę po lewej):
ws.Cells["E2:E4"].FormulaR1C1 = "R[-1]C*RC[-1]";
// Wykorzystanie klasy pomocniczej ExcelAddress do dynamicznego budowania formuł:
string adr = new ExcelAddress(4, 4, 6, 8).Address; // Zwróci ciąg "D4:H6"
using (ExcelRange range = ws.Cells[1, 1, 2, 1])
{
range.Formula = $"AVERAGE({adr})";
}
4.
Automatyczne wczytywanie danych z kolekcji
(LoadFromCollection)
Metoda LoadFromCollection<T> pozwala błyskawicznie
wypełnić arkusz danymi z kolekcji obiektów lub rekordów. Kolejne kolumny
odpowiadają publicznym właściwościom klasy, a wiersze – kolejnym
elementom kolekcji. Ustawienie parametru PrintHeaders: true
automatycznie wygeneruje wiersz nagłówkowy z nazwami właściwości:
public record FileStatItem(string Name, string Extension, long SizeBytes);
var topFiles = new List<FileStatItem>
{
new("baza.bak", ".bak", 104857600),
new("wideo.mp4", ".mp4", 52428800)
};
// Wpisanie tabeli począwszy od komórki A2 wraz z nagłówkami kolumn:
ws.Cells["A2"].LoadFromCollection(topFiles, PrintHeaders: true);
5. Konspekt danych (Grupowanie / Outline), kolumny i scalanie komórek
Mechanizm konspektu (Outline) pozwala na tworzenie zwijanych
i rozwijanych grup wierszy lub kolumn (widocznych w Excelu jako
przyciski [+] oraz [-] na lewym/górnym
marginesie):
// WAŻNE dla drzewa katalogów: Domyślnie Excel zakłada, że wiersz podsumowujący znajduje się POD grupą.
// Aby nagłówek katalogu nadrzędnego znajdował się NAD jego zawartością, ustawiamy:
ws.OutLineSummaryBelow = false;
ws.OutLineSummaryRight = false;
// Przypisanie poziomu zagnieżdżenia konspektu (wartości od 1 do maksymalnie 7):
ws.Row(2).OutlineLevel = 1; // Poziom 1 (np. zawartość katalogu głównego)
ws.Row(3).OutlineLevel = 2; // Poziom 2 (np. zawartość podkatalogu)
ws.Column(4).OutlineLevel = 1;
// Domyślne zwinięcie/ukrycie zgrupowanej kolumny lub wiersza:
ws.Column(1).Collapsed = true;
// Ustalanie szerokości kolumn i autodopasowanie:
ws.Column(1).Width = 25.0;
ws.Column(1).AutoFit(); // Dopasowanie pojedynczej kolumny do zawartości
ws.Cells.AutoFitColumns(0); // Autodopasowanie wszystkich kolumn w arkuszu (z podaniem szerokości min.)
// Wyłączenie wyświetlania linii siatki i scalanie komórek:
ws.View.ShowGridLines = false;
ws.Cells[5, 6, 5, 9].Merge = true;
ws.Cells["A5:E5"].Merge = true;
⚠️ Ograniczenie formatu Excel dla
OutlineLevel:
Specyfikacja Microsoft Excel pozwala na maksymalnie 7 poziomów grupowania konspektu (OutlineLevelz przedziału0..7). Jeśli głębokość rekurencji katalogów przekroczy 7, przed przypisaniem wartości należy ją ograniczyć:ws.Row(r).OutlineLevel = Math.Min(depth, 7);.
5. Stylizacja Komórek, Nagłówki Wydruku oraz Wykresy Kołowe
Cel: Formatowanie wizualne arkuszy, stosowanie masek formatowania liczb i dat oraz osadzanie wykresów 3D opartych na seriach danych.
1.
Formatowanie wyglądu komórek (Style) oraz maski liczbowe
(Numberformat)
using System.Drawing;
using OfficeOpenXml.Style;
using (ExcelRange range = ws.Cells[1, 1, 1, 4])
{
range.Style.Font.Bold = true;
range.Style.Font.Italic = true;
range.Style.Font.Color.SetColor(Color.FromArgb(0, 0, 255));
range.Style.Fill.PatternType = ExcelFillStyle.Solid; // lub np. ExcelFillStyle.DarkUp
range.Style.Fill.BackgroundColor.SetColor(Color.Aquamarine);
range.Style.Border.Top.Style = ExcelBorderStyle.Medium;
range.Style.HorizontalAlignment = ExcelHorizontalAlignment.Right;
}
// Przykłady określania formatu liczbowego oraz daty (Numberformat.Format):
ws.Cells["B2:B10"].Style.Numberformat.Format = "#####"; // 123 -> 123
ws.Cells["B2:B10"].Style.Numberformat.Format = "00000"; // 123 -> 00123
ws.Cells["B2:B10"].Style.Numberformat.Format = "#,#"; // 1234567890 -> 1,234,567,890
ws.Cells["B2:B10"].Style.Numberformat.Format = "00.00"; // 1.2 -> 01.20
ws.Cells["B2:B10"].Style.Numberformat.Format = "#,##0.00"; // 1234567890 -> 1,234,567,890.00
ws.Cells["C2:C10"].Style.Numberformat.Format = "#0.##%"; // 0.092 -> 9.2%
ws.Cells["D2:D10"].Style.Numberformat.Format = "m/d/yyyy"; // Format daty
ws.Cells["A2:A10"].Style.Numberformat.Format = "@"; // Format tekstowy
2. Nagłówki i stopki
strony (HeaderFooter)
// Nagłówek dla stron nieparzystych (czcionka Times New Roman, rozmiar 22, podkreślona):
ws.HeaderFooter.OddHeader.CenteredText = "&22&U&\"Times New Roman,Regular Bold\" Struktura Katalogu";
// Stopka: numer bieżącej strony i całkowita liczba stron (po prawej), nazwa arkusza (w środku), ścieżka (po lewej):
ws.HeaderFooter.OddFooter.RightAlignedText = $"Page {ExcelHeaderFooter.PageNumber} of {ExcelHeaderFooter.NumberOfPages}";
ws.HeaderFooter.OddFooter.CenteredText = ExcelHeaderFooter.SheetName;
ws.HeaderFooter.OddFooter.LeftAlignedText = ExcelHeaderFooter.FilePath + ExcelHeaderFooter.FileName;
3.
Tworzenie i konfiguracja wykresów kołowych
(ExcelPieChart)
Aby utworzyć wykres w przestrzeni
OfficeOpenXml.Drawing.Chart, w arkuszu muszą najpierw
znajdować się komórki zawierające etykiety kategorii (np. nazwy
rozszerzeń w E2:E6) oraz odpowiadające im wartości liczbowe
(np. liczba plików w F2:F6):
using OfficeOpenXml.Drawing;
using OfficeOpenXml.Drawing.Chart;
// 1. Dodanie wykresu kołowego 3D do kolekcji rysunków arkusza (nazwa wykresu musi być unikalna):
var chart = ws.Drawings.AddChart("PieChartExtensionsCount", eChartType.Pie3D) as ExcelPieChart;
chart!.Title.Text = "Udział ilościowy rozszerzeń plików";
// 2. Ustalenie pozycji wykresu: SetPosition(WierszOd0, OffsetWierszaPx, KolumnaOd0, OffsetKolumnyPx)
// UWAGA: W metodzie SetPosition wiersz i kolumna są indeksowane od 0! (0 = wiersz 1, 5 = kolumna F)
chart.SetPosition(Row: 0, RowOffsetPixels: 5, Column: 7, ColumnOffsetPixels: 5);
chart.SetSize(PixelWidth: 600, PixelHeight: 350);
// 3. Podpięcie serii danych: Series.Add(AdresWartości, AdresKategorii)
ExcelAddress valAddr = new ExcelAddress(fromRow: 2, fromCol: 6, toRow: 6, toCol: 6); // F2:F6 (Wartości)
ExcelAddress catAddr = new ExcelAddress(fromRow: 2, fromCol: 5, toRow: 6, toCol: 5); // E2:E6 (Etykiety rozszerzeń)
var serie = chart.Series.Add(valAddr.Address, catAddr.Address) as ExcelPieChartSerie;
// 4. Włączenie wyświetlania nazw kategorii oraz wartości procentowych na wycinkach koła:
chart.DataLabel.ShowCategory = true;
chart.DataLabel.ShowPercent = true;
// 5. Formatowanie obramowania i tła legendy:
chart.Legend.Border.LineStyle = eLineStyle.Solid;
chart.Legend.Border.Fill.Style = eFillStyle.SolidFill;
chart.Legend.Border.Fill.Color = Color.DarkBlue;
6.
Czysta Architektura (Clean Architecture), Kontener DI i Interfejs
Terminal.Gui
Cel: Zastosowanie zasady Odwrócenia Zależności (DIP)
oraz wbudowanego kontenera
Microsoft.Extensions.DependencyInjection do odseparowania
logiki analizy plików od biblioteki EPPlus.
W dobrze zaprojektowanej aplikacji zgodnej z Clean
Architecture logika biznesowa (skanowanie drzewa plików,
obliczanie rozmiarów katalogów, wyznaczanie 10 największych plików oraz
agregacja statystyk rozszerzeń) nie powinna bezpośrednio zależeć
od klas konkretnej biblioteki raportującej
(ExcelPackage).
[ Warstwa Prezentacji (Program.cs / Terminal.Gui) ]
│
▼ (Wstrzyknięcie przez DI)
[ Usługa Aplikacji: ReportService(IDirectoryScanner, IExcelReportGenerator) ]
│ │
▼ zależy od abstrakcji ▼ zależy od abstrakcji
[ Interfejs: IDirectoryScanner ] [ Interfejs: IExcelReportGenerator ]
▲ ▲
│ implementuje (System.IO) │ implementuje (EPPlus)
[ FileSystemDirectoryScanner ] [ EpplusReportGenerator ]
1.
Definiowanie kontraktów i rejestracja w
Microsoft.Extensions.DependencyInjection
using Microsoft.Extensions.DependencyInjection;
// Kontrakty (Interfejsy warstwy domeny/aplikacji):
public interface IDirectoryScanner
{
DirectoryScanResult Scan(string rootPath, int maxDepth);
}
public interface IExcelReportGenerator
{
void GenerateReport(DirectoryScanResult scanResult, string outputFilePath);
}
// Główna usługa aplikacyjna (programuje wyłącznie do interfejsów!):
public class ReportService(IDirectoryScanner scanner, IExcelReportGenerator reportGenerator)
{
public DirectoryScanResult CreateDirectoryReport(string sourceDir, int maxDepth, string targetExcelPath)
{
var result = scanner.Scan(sourceDir, maxDepth);
reportGenerator.GenerateReport(result, targetExcelPath);
return result;
}
}
Konfiguracja kontenera DI w Program.cs (z użyciem
Top-level statements):
var services = new ServiceCollection();
services.AddTransient<IDirectoryScanner, FileSystemDirectoryScanner>();
services.AddTransient<IExcelReportGenerator, EpplusReportGenerator>();
services.AddTransient<ReportService>();
using ServiceProvider serviceProvider = services.BuildServiceProvider();
var reportService = serviceProvider.GetRequiredService<ReportService>();
reportService.CreateDirectoryReport(@"D:\", maxDepth: 3, @"D:\scanDir.xlsx");
💡 Tworzenie tekstowego interfejsu użytkownika (TUI) z pakietem Terminal.Gui (Kliknij, aby rozwinąć/zwinąć)
Biblioteka Terminal.Gui pozwala na
tworzenie okienkowych interfejsów użytkownika bezpośrednio w konsoli
tekstowej (obsługujących mysz, skróty klawiszowe, pola tekstowe i
przyciski).
Instalacja pakietu (wersja stabilna gałęzi 1.x):
dotnet add package Terminal.Gui --version 1.17.1
Przykładowa implementacja formularza parametryzującego skanowanie:
using Terminal.Gui;
Application.Init();
try
{
var top = Application.Top;
var win = new Window("Pobierz katalog plików - naciśnij Ctrl+Q, aby zamknąć")
{
X = 0, Y = 0,
Width = Dim.Fill(), Height = Dim.Fill()
};
top.Add(win);
var lblOut = new Label("Ścieżka zapisu:") { X = 2, Y = 2 };
var txtOut = new TextField(@"d:\scanDir.xlsx") { X = 28, Y = 2, Width = 40 };
var lblDir = new Label("Ścieżka do przeszukania:") { X = 2, Y = 4 };
var txtDir = new TextField(@"d:\") { X = 28, Y = 4, Width = 40 };
var lblDepth = new Label("Głębokość przeszukiwania:") { X = 2, Y = 6 };
var txtDepth = new TextField("3") { X = 28, Y = 6, Width = 40 };
var btnRun = new Button("Generuj raport") { X = 28, Y = 8 };
var txtLog = new TextView()
{
X = 2, Y = 10,
Width = Dim.Fill() - 4,
Height = Dim.Fill() - 2,
ReadOnly = true
};
btnRun.Clicked += () =>
{
string outPath = txtOut.Text.ToString() ?? "";
string dirPath = txtDir.Text.ToString() ?? "";
if (!int.TryParse(txtDepth.Text.ToString(), out int depth) || depth < 0)
{
MessageBox.ErrorQuery("Błąd walidacji", "Głębokość musi być liczbą nieujemną!", "OK");
return;
}
try
{
var result = reportService.CreateDirectoryReport(dirPath, depth, outPath);
txtLog.Text = string.Join(Environment.NewLine, result.Errors)
+ $"{Environment.NewLine}Pomyślnie utworzono plik!{Environment.NewLine}Znajduje się on w pliku: {outPath}";
}
catch (Exception ex)
{
txtLog.Text = $"Błąd krytyczny: {ex.Message}";
}
};
win.Add(lblOut, txtOut, lblDir, txtDir, lblDepth, txtDepth, btnRun, txtLog);
Application.Run();
}
finally
{
Application.Shutdown();
}
Zadanie Laboratoryjne do wykonania (100 pkt)
Cel zadania: Stworzenie aplikacji konsolowej w architekturze Clean Architecture z wykorzystaniem lokalnie skompilowanej biblioteki EPPlus, która skanuje wskazaną strukturę katalogów do zadanej głębokości i generuje wieloarkuszowy raport Excel (
.xlsx) zawierający drzewo plików ze zwijanym konspektem (Outline), ranking 10 największych plików oraz dwa wykresy kołowe 3D prezentujące statystyki rozszerzeń.
Część 1: Pobranie, Kompilacja i Uruchomienie Testów Biblioteki EPPlus (10 pkt)
- Pobierz kod źródłowy biblioteki EPPlus za pomocą
git clonez oficjalnego repozytorium:git clone [https://github.com/EPPlusSoftware/EPPlus.git](https://github.com/EPPlusSoftware/EPPlus.git) - Otwórz solucję
src\EPPlus.sln, dostosuj w razie potrzeby<TargetFrameworks>do zainstalowanego środowiska (.NET 8lub.NET 9), skompiluj bibliotekę i uruchom wybrane testy jednostkowe (zademonstruj możliwość ich debugowania). - Utwórz nową solucję wraz z projektem konsolowym
(np.
ExcelDirScan) – nie dodawaj własnego projektu do solucji bibliotekisrc\EPPlus.sln! - Dodaj w swoim projekcie bezpośrednią referencję binarną do
zbudowanych w podpunkcie 2 bibliotek
EPPlus.dllorazEPPlus.Interfaces.dll(nie instaluj pakietu EPPlus przez NuGet!). - Doinstaluj za pomocą menedżera pakietów NuGet wymagane zależności
przechodnie (np.
Microsoft.IO.RecyclableMemoryStream,Microsoft.Extensions.Configuration.Json), jeśli środowisko zgłosi ich brak podczas uruchomienia.
Część 2: Arkusz „Struktura katalogu”, Metadane i Konspekt Danych (45 pkt)
- (20 pkt) Eksploracja katalogu z limitem głębokości:
- Skonfiguruj licencję niekomercyjną EPPlus
(
ExcelPackage.License.SetNonCommercialPersonal(...)) oraz właściwości dokumentu (Title,Author,Company). - Utwórz nowy dokument Excela i dodaj w nim arkusz o nazwie „Struktura katalogu”.
- Wpisz do kolejnych wierszy zawartość katalogu (zarówno pliki, jak i
podkatalogi) przekazanego jako pierwszy parametr wywołania metody.
Drugim parametrem metody jest liczba całkowita określająca maksymalną
głębokość rekurencyjnego przeszukiwania (
maxDepth).
- Skonfiguruj licencję niekomercyjną EPPlus
(
- (15 pkt) Metadane elementów systemu plików:
- W kolejnych kolumnach arkusza wyświetl szczegółowe informacje o
elementach: Rozszerzenie, Rozmiar (w
bajtach, z odpowiednim formatowaniem liczbowym) oraz
Atrybuty (
FileAttributes).
- W kolejnych kolumnach arkusza wyświetl szczegółowe informacje o
elementach: Rozszerzenie, Rozmiar (w
bajtach, z odpowiednim formatowaniem liczbowym) oraz
Atrybuty (
- (10 pkt) Grupowanie danych (Konspekt / Outline):
- Dodaj możliwość zwijania i rozwijania zawartości poszczególnych
podkatalogów za pomocą mechanizmu konspektu danych
(
ws.Row(r).OutlineLevel). - Pamiętaj o ustawieniu
ws.OutLineSummaryBelow = false;, aby podkatalogi zwijały się do swojego wiersza nagłówkowego, oraz o dopasowaniu szerokości kolumn (AutoFitColumns).
- Dodaj możliwość zwijania i rozwijania zawartości poszczególnych
podkatalogów za pomocą mechanizmu konspektu danych
(
Część 3: Arkusz „Statystyki” – Top 10 Plików oraz Wykresy Kołowe (25 pkt)
- (15 pkt) Ranking największych plików:
- Dodaj do tego samego skoroszytu drugi arkusz o nazwie „Statystyki”.
- Umieść w nim sformatowaną tabelę zawierającą listę 10
największych plików znalezionych podczas skanowania,
posortowaną malejąco według rozmiaru (możesz
wykorzystać LINQ:
.OrderByDescending(f => f.Size).Take(10)oraz metodęLoadFromCollection).
- (10 pkt) Wizualizacja udziału rozszerzeń na wykresach
kołowych:
- Wygeneruj w arkuszu „Statystyki” zestawienie danych pogrupowanych według rozszerzeń plików (liczba wystąpień każdego rozszerzenia oraz łączna zajmowana powierzchnia).
- Dodaj do arkusza „Statystyki” dwa wykresy kołowe
(
eChartType.Pie3DlubPie) prezentujące:- Procentowy udział plików o danym rozszerzeniu pod względem ilościowym (liczby plików).
- Procentowy udział plików o danym rozszerzeniu pod względem zajmowanej powierzchni (rozmiaru na dysku).
- Skonfiguruj na obu wykresach wyświetlanie kategorii oraz wartości
procentowych (
ShowCategory = true,ShowPercent = true). - (Wskazówka: Jeśli wykresy 3D są niewidoczne w pakiecie LibreOffice Calc, można zweryfikować ich poprawne wygenerowanie w przeglądarce online: https://onlinedocumentviewer.com/Viewer/).
Część 4: Czysta Architektura i Wstrzykiwanie Zależności (DI) (10 pkt)
- Zaprojektuj aplikację zgodnie z zasadami Clean
Architecture oraz zasadą Odwrócenia Zależności
(DIP):
- Wykorzystaj kontener DI z pakietu NuGet
Microsoft.Extensions.DependencyInjection. - Wydziel interfejsy abstrakcji dla operacji wejścia/wyjścia i
generowania raportu (np.
IDirectoryScannerorazIExcelReportGenerator/IWorkbookBuilder). - Zamknij główną logikę orkiestracji w usłudze (np.
ReportService), która przyjmuje wymagane abstrakcje przez konstruktor, oddzielając logikę domenową i wyliczanie statystyk od bezpośrednich wywołań klas implementacyjnych EPPlus.
- Wykorzystaj kontener DI z pakietu NuGet
Część 5: Odporność na Błędy i Interfejs Użytkownika (10 pkt)
- Odporność na błędy systemu plików:
- Aplikacja musi być odporna na niepoprawne ścieżki wejściowe oraz
brak uprawnień do odczytu katalogów systemowych (np.
$RECYCLE.BIN,System Volume Information). - Sytuacje wyjątkowe należy obsłużyć w dedykowanych blokach
catch(UnauthorizedAccessException,DirectoryNotFoundException,IOException), wypisując czytelny komunikat diagnostyczny. - Brak dostępu do pojedynczego podfolderu nie może przerywać skanowania pozostałej części drzewa katalogów!
- Aplikacja musi być odporna na niepoprawne ścieżki wejściowe oraz
brak uprawnień do odczytu katalogów systemowych (np.
- Punkt wejścia i interfejs użytkownika:
- Punkt startowy aplikacji (
Program.cs) zaimplementuj z wykorzystaniem instrukcji najwyższego poziomu (Top-level statements). - Opcjonalnie (rekomendowane): Wykorzystaj bibliotekę
Terminal.Guido stworzenia tekstowego menu pozwalającego na podanie ścieżki zapisu pliku.xlsx, ścieżki przeszukiwanego katalogu oraz głębokości skanowania, wraz z walidacją parametrów i wyświetleniem logu pominiętych katalogów.
- Punkt startowy aplikacji (
Zestawienie Oceny (Suma: 100 pkt)
| Nr | Zakres modułu | Weryfikacja | Punkty |
|---|---|---|---|
| 1 | Kompilacja EPPlus ze źródeł i referencje | Sklonowanie repozytorium, kompilacja,
uruchomienie testów, referencja do EPPlus.dll i
EPPlus.Interfaces.dll bez NuGet. |
10 pkt |
| 2 | Arkusz „Struktura katalogu” i rekurencja | Skanowanie plików i podkatalogów do zadanej głębokości, zapis w arkuszu. | 20 pkt |
| 3 | Metadane plików i formatowanie | Wyświetlenie rozszerzenia, rozmiaru i
atrybutów (FileAttributes), stylizacja komórek. |
15 pkt |
| 4 | Konspekt danych (Outline) | Poprawne grupowanie wierszy podkatalogów
(OutlineLevel,
OutLineSummaryBelow = false). |
10 pkt |
| 5 | Arkusz „Statystyki” – Top 10 plików | Lista 10 największych plików posortowana malejąco w drugim arkuszu. | 15 pkt |
| 6 | Wykresy kołowe rozszerzeń | Dwa wykresy kołowe (ilościowy i rozmiarowy) z etykietami kategorii i procentów. | 10 pkt |
| 7 | Clean Architecture i Kontener DI | Podział na interfejsy, usługa
ReportService,
Microsoft.Extensions.DependencyInjection. |
10 pkt |
| 8 | Odporność na błędy i UI
(Terminal.Gui) |
Osobna obsługa
UnauthorizedAccessException i IOException bez
przerywania skanowania, Top-level statements /
Terminal.Gui. |
10 pkt |
| SUMA | 100 pkt |
Przykładowe pytania na obronie ustnej:
- Dlaczego standardowa metoda
DirectoryInfo.GetFiles("*", SearchOption.AllDirectories)nie sprawdza się podczas skanowania dysku systemowego i jak należy zaimplementować bezpieczną eksplorację drzewa katalogów? - Jakie indeksowanie (od 0 czy od 1) stosuje biblioteka EPPlus w
przypadku kolekcji arkuszy
Worksheets[i], komórekCells[row, col]oraz metody pozycjonowania wykresuchart.SetPosition(...)? - Dlaczego podczas tworzenia konspektu danych
(
OutlineLevel) dla drzewa katalogów konieczne jest ustawienie właściwościws.OutLineSummaryBelow = false? - Czym różni się dodanie do projektu bezpośredniej referencji binarnej
(
<Reference Include="EPPlus.dll">) od referencji pakietu NuGet (<PackageReference>) w kontekście rozwiązywania zależności przechodnich i debugowania kodu? - W jaki sposób oddzielenie interfejsów
IDirectoryScannerorazIExcelReportGeneratorod usługiReportServiceułatwia napisanie testów jednostkowych logiki raportowania bez konieczności fizycznego tworzenia plików na dysku?