diff --git a/SheetReader.AppTest/MainWindow.xaml.cs b/SheetReader.AppTest/MainWindow.xaml.cs index a57aa12..8724e42 100644 --- a/SheetReader.AppTest/MainWindow.xaml.cs +++ b/SheetReader.AppTest/MainWindow.xaml.cs @@ -18,7 +18,7 @@ public MainWindow() tc.ItemsSource = Sheets; } - public ObservableCollection Sheets { get; } = new(); + public ObservableCollection Sheets { get; } = []; protected override void OnKeyDown(KeyEventArgs e) { @@ -28,7 +28,7 @@ protected override void OnKeyDown(KeyEventArgs e) } private void Exit_Click(object sender, RoutedEventArgs e) => Close(); - private void About_Click(object sender, RoutedEventArgs e) => MessageBox.Show(Assembly.GetEntryAssembly()!.GetCustomAttribute()!.Title + " - " + (IntPtr.Size == 4 ? "32" : "64") + "-bit" + Environment.NewLine + "Copyright (C) 2021-" + DateTime.Now.Year + " Simon Mourier. All rights reserved.", Assembly.GetEntryAssembly().GetCustomAttribute()!.Title, MessageBoxButton.OK, MessageBoxImage.Information); + private void About_Click(object sender, RoutedEventArgs e) => MessageBox.Show(Assembly.GetEntryAssembly()!.GetCustomAttribute()!.Title + " - " + (IntPtr.Size == 4 ? "32" : "64") + "-bit" + Environment.NewLine + "Copyright (C) 2021-" + DateTime.Now.Year + " Simon Mourier. All rights reserved.", Assembly.GetEntryAssembly()!.GetCustomAttribute()!.Title, MessageBoxButton.OK, MessageBoxImage.Information); private void Open_Click(object sender, RoutedEventArgs e) { var ofd = new OpenFileDialog @@ -49,30 +49,13 @@ private void Open_Click(object sender, RoutedEventArgs e) private void Load(string fileName) { Sheets.Clear(); - var book = new Book(); try { - foreach (var sheet in book.EnumerateSheets(fileName)) + var book = new BookDocument(); + book.Load(fileName); + foreach (var sheet in book.Sheets) { - // we need to load data for WPF data binding - var sheetData = new SheetData { Name = sheet.Name, IsHidden = !sheet.IsVisible }; - foreach (var row in sheet.EnumerateRows()) - { - var rowData = new RowData(row.Index, row.EnumerateCells().ToList()); - sheetData.Rows.Add(rowData); - } - - sheetData.Columns = sheet.EnumerateColumns().ToList(); - if (sheetData.Columns.Count > 0) - { - sheetData.LastColumnIndex = sheetData.Columns.Max(x => x.Index); - } - - if (sheetData.Rows.Count > 0) - { - sheetData.LastRowIndex = sheetData.Rows.Max(x => x.RowIndex); - } - Sheets.Add(sheetData); + Sheets.Add(sheet); } } catch (Exception ex) @@ -84,10 +67,11 @@ private void Load(string fileName) // not sure why, but with only 1 tab, binding doesn't work... if (Sheets.Count == 1) { - var dummy = new SheetData(); - Sheets.Add(dummy); - Sheets.Remove(dummy); + var first = Sheets[0]; + Sheets.Add(first); + Sheets.RemoveAt(1); } + Title = "Sheet Reader - " + Path.GetFileName(fileName); } diff --git a/SheetReader.AppTest/SheetControl.cs b/SheetReader.AppTest/SheetControl.cs index df0b3fd..b86f12d 100644 --- a/SheetReader.AppTest/SheetControl.cs +++ b/SheetReader.AppTest/SheetControl.cs @@ -1,5 +1,6 @@ using System; using System.Globalization; +using System.Linq; using System.Windows; using System.Windows.Media; @@ -7,7 +8,7 @@ namespace SheetReader.AppTest { public class SheetControl : FrameworkElement { - public static readonly DependencyProperty SheetProperty = DependencyProperty.Register(nameof(Sheet), typeof(SheetData), typeof(SheetControl), + public static readonly DependencyProperty SheetProperty = DependencyProperty.Register(nameof(Sheet), typeof(BookDocumentSheet), typeof(SheetControl), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender)); public static readonly DependencyProperty ColumnSizeProperty = DependencyProperty.Register(nameof(ColumnSize), typeof(double), typeof(SheetControl), @@ -16,7 +17,7 @@ public class SheetControl : FrameworkElement public static readonly DependencyProperty RowHeightProperty = DependencyProperty.Register(nameof(RowHeight), typeof(double), typeof(SheetControl), new FrameworkPropertyMetadata(20.0, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender)); - public SheetData Sheet { get { return (SheetData)GetValue(SheetProperty); } set { SetValue(SheetProperty, value); } } + public BookDocumentSheet Sheet { get { return (BookDocumentSheet)GetValue(SheetProperty); } set { SetValue(SheetProperty, value); } } public double ColumnSize { get { return (double)GetValue(ColumnSizeProperty); } set { SetValue(ColumnSizeProperty, value); } } public double RowHeight { get { return (double)GetValue(RowHeightProperty); } set { SetValue(RowHeightProperty, value); } } @@ -119,7 +120,7 @@ protected override void OnRender(DrawingContext drawingContext) } } - foreach (var row in Sheet.Rows) + foreach (var row in Sheet.Rows.Values.OrderBy(r => r.RowIndex)) { foreach (var cell in row.Cells) { diff --git a/SheetReader.AppTest/SheetData.cs b/SheetReader.AppTest/SheetData.cs deleted file mode 100644 index e2e042d..0000000 --- a/SheetReader.AppTest/SheetData.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Collections.Generic; - -namespace SheetReader.AppTest -{ - public class SheetData - { - public string? Name { get; set; } - public bool IsHidden { get; set; } - public List Rows { get; } = new List(); - public List? Columns { get; set; } - public int LastColumnIndex { get; set; } - public int LastRowIndex { get; set; } - - public override string ToString() => Name ?? string.Empty; - } - - public class RowData - { - public RowData(int index, List cells) - { - RowIndex = index; - Cells = cells; - } - - public int RowIndex { get; } - public List Cells { get; } - } -} diff --git a/SheetReader.Wpf.Test/GlobalSuppressions.cs b/SheetReader.Wpf.Test/GlobalSuppressions.cs new file mode 100644 index 0000000..9921f07 --- /dev/null +++ b/SheetReader.Wpf.Test/GlobalSuppressions.cs @@ -0,0 +1,3 @@ +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Interoperability", "SYSLIB1054:Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time", Justification = "Mostly useless")] diff --git a/SheetReader.Wpf.Test/MainWindow.xaml b/SheetReader.Wpf.Test/MainWindow.xaml index 30a63f6..f9dd4a0 100644 --- a/SheetReader.Wpf.Test/MainWindow.xaml +++ b/SheetReader.Wpf.Test/MainWindow.xaml @@ -30,7 +30,10 @@ Header="Open _Recent" IsEnabled="False"> - + EventProvider.Default.WriteMessageEvent(methodName + ":" + message); + private ScrollViewer? _scrollViewer; private SheetGrid? _grid; @@ -68,7 +71,6 @@ public override void OnApplyTemplate() private void OnScrollChanged(object sender, ScrollChangedEventArgs e) { - Trace.WriteLine("OnScrollChanged"); _grid?.InvalidateVisual(); } @@ -100,7 +102,6 @@ protected override Size MeasureCore(Size availableSize) protected override void OnRender(DrawingContext drawingContext) { - Trace.WriteLine("OnRender size:" + RenderSize); if (control.Sheet == null || control.Sheet.Columns.Count == 0 || control.Sheet.Rows.Count == 0) return; @@ -109,20 +110,33 @@ protected override void OnRender(DrawingContext drawingContext) var dpi = VisualTreeHelper.GetDpi(this); var colWidth = control.GetColWidth(); + var colFullWidth = colWidth + control.GridLineSize; var rowHeight = control.GetRowHeight(); - var fontSize = rowHeight * 0.6; + var rowFullHeight = rowHeight + control.GridLineSize; - var rowMargin = control.GetRowMargin(); - var headerHeight = rowHeight; + var rowsHeaderWidth = control.GetRowMargin(); + var rowsHeaderFullWidth = rowsHeaderWidth + control.GridLineSize; + var columnsHeaderHeight = rowHeight; + var columnsHeaderFullHeight = columnsHeaderHeight + control.GridLineSize; + + var offsetX = control._scrollViewer?.HorizontalOffset ?? 0; + var offsetY = control._scrollViewer?.VerticalOffset ?? 0; + var viewWidth = control._scrollViewer?.ViewportWidth ?? RenderSize.Width; + var viewHeight = control._scrollViewer?.ViewportHeight ?? RenderSize.Height; - var firstColumnIndex = 0; - var offsetX = control._scrollViewer?.ContentHorizontalOffset ?? 0; + var firstDrawnColumnIndex = Math.Max((int)((offsetX - rowsHeaderFullWidth) / colFullWidth), 0); + var lastDrawnColumnIndex = Math.Max(Math.Min((int)((offsetX - rowsHeaderFullWidth + viewWidth) / colFullWidth), control.Sheet.LastColumnIndex), firstDrawnColumnIndex); + + var firstDrawnRowIndex = Math.Max((int)((offsetY - columnsHeaderFullHeight) / rowFullHeight), 0); + var lastDrawnRowIndex = Math.Max(Math.Min((int)((offsetY - columnsHeaderFullHeight + viewHeight) / rowFullHeight), control.Sheet.LastRowIndex), firstDrawnRowIndex); + + Log("firstCol:" + firstDrawnColumnIndex + " lastCol:" + lastDrawnColumnIndex + " firstRow:" + firstDrawnRowIndex + " lastRow:" + lastDrawnRowIndex); // header backgrounds - drawingContext.DrawRectangle(Brushes.LightGray, null, new Rect(offsetX, headerHeight, rowMargin + offsetX, (control.Sheet.LastRowIndex + 1) * rowHeight)); - drawingContext.DrawRectangle(Brushes.LightGray, null, new Rect(rowMargin, 0, (control.Sheet.LastColumnIndex + 1) * colWidth, headerHeight)); + drawingContext.DrawRectangle(Brushes.LightGray, null, new Rect(offsetX, columnsHeaderHeight, rowsHeaderWidth, (control.Sheet.LastRowIndex + 1) * rowHeight)); + drawingContext.DrawRectangle(Brushes.LightGray, null, new Rect(rowsHeaderWidth, offsetY, (control.Sheet.LastColumnIndex + 1) * colWidth, columnsHeaderHeight)); - var maxWidth = Math.Min(colWidth * (control.Sheet.LastColumnIndex + 1) + rowMargin, RenderSize.Width); + var maxWidth = Math.Min(colWidth * (control.Sheet.LastColumnIndex + 1) + rowsHeaderWidth, RenderSize.Width); var maxHeight = Math.Min(rowHeight * (control.Sheet.LastRowIndex + 2), RenderSize.Height); // includes col header @@ -135,24 +149,24 @@ protected override void OnRender(DrawingContext drawingContext) } else { - x = colWidth * i - (colWidth - rowMargin); + x = colWidth * i - (colWidth - rowsHeaderWidth); } - drawingContext.DrawLine(pen, new Point(x, 0), new Point(x, maxHeight)); + drawingContext.DrawLine(pen, new Point(x, offsetY), new Point(x, maxHeight)); // draw col name if (i < control.Sheet.LastColumnIndex + 1) { var name = GetExcelColumnName(i); - var formatted = new FormattedText(name, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeFace, fontSize, Brushes.Black, dpi.PixelsPerDip) + var formatted = new FormattedText(name, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeFace, control.FontSize, control.Foreground, dpi.PixelsPerDip) { - MaxTextWidth = rowMargin, + MaxTextWidth = rowsHeaderWidth, MaxLineCount = 1 }; var xoffset = (colWidth - formatted.Width) / 2; // center horizontally - var yoffset = (headerHeight - formatted.Height) / 2; // center vertically - drawingContext.DrawText(formatted, new Point(xoffset + rowMargin + i * colWidth + pen.Thickness, yoffset + pen.Thickness)); + var yoffset = offsetY + (columnsHeaderHeight - formatted.Height) / 2; // center vertically + drawingContext.DrawText(formatted, new Point(xoffset + rowsHeaderWidth + i * colWidth + pen.Thickness, yoffset + pen.Thickness)); } } @@ -160,36 +174,41 @@ protected override void OnRender(DrawingContext drawingContext) for (var i = 0; i < control.Sheet.LastRowIndex + 2 + 1; i++) { var y = rowHeight * i; - drawingContext.DrawLine(pen, new Point(0, y), new Point(maxWidth, y)); + drawingContext.DrawLine(pen, new Point(offsetX, y), new Point(maxWidth, y)); // draw row # if (i < control.Sheet.LastRowIndex + 1) { - var formatted = new FormattedText((i + 1).ToString(), CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeFace, fontSize, Brushes.Black, dpi.PixelsPerDip) + var formatted = new FormattedText((i + 1).ToString(), CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeFace, control.FontSize, control.Foreground, dpi.PixelsPerDip) { - MaxTextWidth = rowMargin, + MaxTextWidth = rowsHeaderWidth, MaxLineCount = 1 }; - var xoffset = (rowMargin - formatted.Width) / 2; // center horizontally + var xoffset = offsetX + (rowsHeaderWidth - formatted.Width) / 2; // center horizontally var yoffset = (rowHeight - formatted.Height) / 2; // center vertically - drawingContext.DrawText(formatted, new Point(xoffset + pen.Thickness, headerHeight + rowHeight * i + yoffset + pen.Thickness)); + drawingContext.DrawText(formatted, new Point(xoffset + pen.Thickness, columnsHeaderHeight + rowHeight * i + yoffset + pen.Thickness)); } } - foreach (var row in control.Sheet.Rows) + for (var i = firstDrawnRowIndex; i <= lastDrawnRowIndex; i++) { - foreach (var cell in row.Cells) + var row = control.Sheet.Rows[i]; + for (var j = firstDrawnColumnIndex; j <= lastDrawnColumnIndex; j++) { + if (j >= row.Cells.Count) + continue; + + var cell = row.Cells[j]; var text = string.Format("{0}", cell.Value); - var formatted = new FormattedText(text, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeFace, fontSize, Brushes.Black, dpi.PixelsPerDip) + var formatted = new FormattedText(text, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeFace, control.FontSize, control.Foreground, dpi.PixelsPerDip) { MaxTextWidth = colWidth, MaxLineCount = 1 }; - var y = headerHeight + rowHeight * row.RowIndex; - var x = rowMargin + colWidth * cell.ColumnIndex; + var y = columnsHeaderHeight + rowHeight * row.RowIndex; + var x = rowsHeaderWidth + colWidth * cell.ColumnIndex; var yoffset = (rowHeight - formatted.Height) / 2; // center vertically drawingContext.DrawText(formatted, new Point(x + pen.Thickness, y + yoffset + pen.Thickness)); } diff --git a/SheetReader.Wpf/Themes/Generic.xaml b/SheetReader.Wpf/Themes/Generic.xaml index 39efc40..ca10cb0 100644 --- a/SheetReader.Wpf/Themes/Generic.xaml +++ b/SheetReader.Wpf/Themes/Generic.xaml @@ -14,6 +14,8 @@ x:Name="PART_ScrollViewer" Grid.Row="1" Grid.Column="1" + Background="{TemplateBinding Background}" + Foreground="{TemplateBinding Foreground}" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto" /> diff --git a/SheetReader.Wpf/Utilities/EventProvider.cs b/SheetReader.Wpf/Utilities/EventProvider.cs new file mode 100644 index 0000000..45a31ef --- /dev/null +++ b/SheetReader.Wpf/Utilities/EventProvider.cs @@ -0,0 +1,44 @@ +using System; +using System.ComponentModel; +using System.Runtime.InteropServices; +using System.Threading; + +namespace SheetReader.Wpf.Utilities +{ + public sealed class EventProvider : IDisposable + { + // use WpfTraceSpy to read this https://github.com/smourier/TraceSpy ("ETW messages support") + public static EventProvider Default { get; } = new(new Guid("964d4572-adb9-4f3a-8170-fcbecec27467")); + + private long _handle; + public Guid Id { get; } + + public EventProvider(Guid id) + { + Id = id; + var hr = EventRegister(id, IntPtr.Zero, IntPtr.Zero, out _handle); + if (hr != 0) + throw new Win32Exception(hr); + } + + public bool WriteMessageEvent(string text, byte level = 0, long keywords = 0) => EventWriteString(_handle, level, keywords, text) == 0; + + public void Dispose() + { + var handle = Interlocked.Exchange(ref _handle, 0); + if (handle != 0) + { + _ = EventUnregister(handle); + } + } + + [DllImport("advapi32")] + private static extern int EventRegister([MarshalAs(UnmanagedType.LPStruct)] Guid ProviderId, IntPtr EnableCallback, IntPtr CallbackContext, out long RegHandle); + + [DllImport("advapi32")] + private static extern int EventUnregister(long RegHandle); + + [DllImport("advapi32")] + private static extern int EventWriteString(long RegHandle, byte Level, long Keyword, [MarshalAs(UnmanagedType.LPWStr)] string String); + } +} diff --git a/SheetReader/Book.cs b/SheetReader/Book.cs index 8ab188f..94153ce 100644 --- a/SheetReader/Book.cs +++ b/SheetReader/Book.cs @@ -330,7 +330,7 @@ public XlsxSheet(XlsxBook book, XElement element, XmlReader reader) Book = book; Element = element; Reader = reader; - Name = element.Attribute("name")?.Value; + Name = element.Attribute("name")?.Value!; var state = element.Attribute("state")?.Value; if (state.EqualsIgnoreCase("hidden")) { diff --git a/SheetReader/BookDocument.cs b/SheetReader/BookDocument.cs new file mode 100644 index 0000000..bec39ab --- /dev/null +++ b/SheetReader/BookDocument.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace SheetReader +{ + // this class is for loading a workbook (stateful) vs enumerating it (stateless) + public sealed class BookDocument + { + private readonly List _sheets = []; + + public IReadOnlyList Sheets => _sheets; + + public void Load(string filePath, BookFormat? format = null) + { + ArgumentNullException.ThrowIfNull(filePath); + format ??= BookFormat.GetFromFileExtension(Path.GetExtension(filePath)); + ArgumentNullException.ThrowIfNull(format); + + using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + format.IsStreamOwned = true; + format.InputFilePath = filePath; + Load(stream, format); + } + + public void Load(Stream stream, BookFormat format) + { + ArgumentNullException.ThrowIfNull(stream); + _sheets.Clear(); + var book = new Book(); + foreach (var sheet in book.EnumerateSheets(stream, format)) + { + var docSheet = new BookDocumentSheet(sheet); + _sheets.Add(docSheet); + } + } + } +} diff --git a/SheetReader/BookDocumentRow.cs b/SheetReader/BookDocumentRow.cs new file mode 100644 index 0000000..35f80c0 --- /dev/null +++ b/SheetReader/BookDocumentRow.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Linq; + +namespace SheetReader +{ + public sealed class BookDocumentRow + { + internal BookDocumentRow(Row row) + { + RowIndex = row.Index; + IsHidden = !row.IsVisible; + Cells = row.EnumerateCells().ToList().AsReadOnly(); + } + + public int RowIndex { get; } + public bool IsHidden { get; } + public IReadOnlyList Cells { get; } + + public override string ToString() => RowIndex.ToString(); + } +} diff --git a/SheetReader/BookDocumentSheet.cs b/SheetReader/BookDocumentSheet.cs new file mode 100644 index 0000000..b1a8150 --- /dev/null +++ b/SheetReader/BookDocumentSheet.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; + +namespace SheetReader +{ + public sealed class BookDocumentSheet + { + private readonly Dictionary _rows = []; + private readonly Dictionary _columns = []; + + internal BookDocumentSheet(Sheet sheet) + { + Name = sheet.Name ?? string.Empty; + IsHidden = !sheet.IsVisible; + + foreach (var row in sheet.EnumerateRows()) + { + var rowData = new BookDocumentRow(row); + _rows[row.Index] = rowData; + + if (row.Index > LastRowIndex) + { + LastRowIndex = row.Index; + } + } + + foreach (var col in sheet.EnumerateColumns()) + { + _columns[col.Index] = col; + if (col.Index > LastColumnIndex) + { + LastColumnIndex = col.Index; + } + } + } + + public string Name { get; } + public bool IsHidden { get; } + public int LastColumnIndex { get; } + public int LastRowIndex { get; } + public IReadOnlyDictionary Rows => _rows; + public IReadOnlyDictionary Columns => _columns; + + public override string ToString() => Name; + } +} diff --git a/SheetReader/Row.cs b/SheetReader/Row.cs index 65ea799..9c5ed28 100644 --- a/SheetReader/Row.cs +++ b/SheetReader/Row.cs @@ -9,6 +9,19 @@ public abstract class Row public abstract IEnumerable EnumerateCells(); + public static string GetExcelColumnName(int index) + { + index++; + var name = string.Empty; + while (index > 0) + { + var mod = (index - 1) % 26; + name = (char)('A' + mod) + name; + index = (index - mod) / 26; + } + return name; + } + public override string ToString() => Index.ToString(); } }