From 8ca07e584ab581b7328b228778f01c0e7af8ffcd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 02:38:19 +0000 Subject: [PATCH 01/15] Add DataGridView control with renderer, columns, rows, cells, and ControlGallery demo panel Agent-Logs-Url: https://github.com/modern-forms/Modern.Forms/sessions/a8e2d3dd-1cb3-4e19-bb8a-bd53fbff1d30 Co-authored-by: jpobst <179295+jpobst@users.noreply.github.com> --- samples/ControlGallery/MainForm.cs | 3 + .../Panels/DataGridViewPanel.cs | 66 ++ src/Modern.Forms/DataGridView.cs | 653 ++++++++++++++++++ src/Modern.Forms/DataGridViewCell.cs | 81 +++ .../DataGridViewCellCollection.cs | 65 ++ src/Modern.Forms/DataGridViewColumn.cs | 121 ++++ .../DataGridViewColumnCollection.cs | 75 ++ src/Modern.Forms/DataGridViewRow.cs | 69 ++ src/Modern.Forms/DataGridViewRowCollection.cs | 69 ++ .../Renderers/DataGridViewRenderer.cs | 208 ++++++ src/Modern.Forms/Renderers/RenderManager.cs | 1 + 11 files changed, 1411 insertions(+) create mode 100644 samples/ControlGallery/Panels/DataGridViewPanel.cs create mode 100644 src/Modern.Forms/DataGridView.cs create mode 100644 src/Modern.Forms/DataGridViewCell.cs create mode 100644 src/Modern.Forms/DataGridViewCellCollection.cs create mode 100644 src/Modern.Forms/DataGridViewColumn.cs create mode 100644 src/Modern.Forms/DataGridViewColumnCollection.cs create mode 100644 src/Modern.Forms/DataGridViewRow.cs create mode 100644 src/Modern.Forms/DataGridViewRowCollection.cs create mode 100644 src/Modern.Forms/Renderers/DataGridViewRenderer.cs diff --git a/samples/ControlGallery/MainForm.cs b/samples/ControlGallery/MainForm.cs index e6dbad6..2957d95 100644 --- a/samples/ControlGallery/MainForm.cs +++ b/samples/ControlGallery/MainForm.cs @@ -22,6 +22,7 @@ public MainForm () tree.Items.Add ("Button", ImageLoader.Get ("button.png")); tree.Items.Add ("CheckBox", ImageLoader.Get ("button.png")); tree.Items.Add ("ComboBox", ImageLoader.Get ("button.png")); + tree.Items.Add ("DataGridView", ImageLoader.Get ("button.png")); tree.Items.Add ("Dialogs", ImageLoader.Get ("button.png")); tree.Items.Add ("FileDialogs", ImageLoader.Get ("button.png")); tree.Items.Add ("FlowLayoutPanel", ImageLoader.Get ("button.png")); @@ -90,6 +91,8 @@ private void Tree_ItemSelected (object? sender, EventArgs e) return new CheckBoxPanel (); case "ComboBox": return new ComboBoxPanel (); + case "DataGridView": + return new DataGridViewPanel (); case "Dialogs": return new DialogPanel (); case "FileDialogs": diff --git a/samples/ControlGallery/Panels/DataGridViewPanel.cs b/samples/ControlGallery/Panels/DataGridViewPanel.cs new file mode 100644 index 0000000..4134056 --- /dev/null +++ b/samples/ControlGallery/Panels/DataGridViewPanel.cs @@ -0,0 +1,66 @@ +using Modern.Forms; + +namespace ControlGallery.Panels +{ + public class DataGridViewPanel : BasePanel + { + public DataGridViewPanel () + { + Controls.Add (new Label { Text = "DataGridView - Row Selection", Left = 10, Top = 10, Width = 300 }); + + var dgv1 = new DataGridView { + Left = 10, + Top = 30, + Width = 700, + Height = 250 + }; + + dgv1.Columns.Add ("Name", 150); + dgv1.Columns.Add ("Age", 60); + dgv1.Columns.Add ("City", 120); + dgv1.Columns.Add ("Occupation", 150); + dgv1.Columns.Add ("Email", 200); + + dgv1.Rows.Add ("Alice Johnson", "32", "New York", "Engineer", "alice@example.com"); + dgv1.Rows.Add ("Bob Smith", "45", "Los Angeles", "Designer", "bob@example.com"); + dgv1.Rows.Add ("Carol Williams", "28", "Chicago", "Teacher", "carol@example.com"); + dgv1.Rows.Add ("David Brown", "51", "Houston", "Doctor", "david@example.com"); + dgv1.Rows.Add ("Eve Davis", "39", "Phoenix", "Lawyer", "eve@example.com"); + dgv1.Rows.Add ("Frank Miller", "22", "Philadelphia", "Student", "frank@example.com"); + dgv1.Rows.Add ("Grace Wilson", "36", "San Antonio", "Architect", "grace@example.com"); + dgv1.Rows.Add ("Henry Moore", "48", "San Diego", "Manager", "henry@example.com"); + dgv1.Rows.Add ("Ivy Taylor", "31", "Dallas", "Analyst", "ivy@example.com"); + dgv1.Rows.Add ("Jack Anderson", "55", "San Jose", "Director", "jack@example.com"); + dgv1.Rows.Add ("Karen Thomas", "42", "Austin", "Consultant", "karen@example.com"); + dgv1.Rows.Add ("Leo Jackson", "27", "Jacksonville", "Developer", "leo@example.com"); + dgv1.Rows.Add ("Mia White", "34", "Fort Worth", "Scientist", "mia@example.com"); + dgv1.Rows.Add ("Noah Harris", "60", "Columbus", "Professor", "noah@example.com"); + dgv1.Rows.Add ("Olivia Martin", "25", "Charlotte", "Intern", "olivia@example.com"); + + dgv1.SelectedRowIndex = 2; + + Controls.Add (dgv1); + + Controls.Add (new Label { Text = "DataGridView - Cell Selection, No Headers", Left = 10, Top = 300, Width = 300 }); + + var dgv2 = new DataGridView { + Left = 10, + Top = 320, + Width = 500, + Height = 200, + RowSelectionMode = false, + ColumnHeadersVisible = false + }; + + dgv2.Columns.Add ("Column 1", 120); + dgv2.Columns.Add ("Column 2", 120); + dgv2.Columns.Add ("Column 3", 120); + dgv2.Columns.Add ("Column 4", 120); + + for (var i = 0; i < 10; i++) + dgv2.Rows.Add ($"Cell {i},0", $"Cell {i},1", $"Cell {i},2", $"Cell {i},3"); + + Controls.Add (dgv2); + } + } +} diff --git a/src/Modern.Forms/DataGridView.cs b/src/Modern.Forms/DataGridView.cs new file mode 100644 index 0000000..fa2ad7a --- /dev/null +++ b/src/Modern.Forms/DataGridView.cs @@ -0,0 +1,653 @@ +using System.Drawing; +using Modern.Forms.Renderers; + +namespace Modern.Forms +{ + /// + /// Represents a DataGridView control for displaying tabular data. + /// + public class DataGridView : Control + { + private int header_height = 30; + private int row_height = 25; + private int top_index; + private int horizontal_scroll_offset; + private int selected_row_index = -1; + private int selected_column_index = -1; + private int hovered_row_index = -1; + private int resize_column_index = -1; + private int resize_start_x; + private int resize_start_width; + private bool is_resizing; + private bool column_headers_visible = true; + private bool row_selection_mode = true; + + private readonly VerticalScrollBar vscrollbar; + private readonly HorizontalScrollBar hscrollbar; + + /// + /// Initializes a new instance of the DataGridView class. + /// + public DataGridView () + { + Columns = new DataGridViewColumnCollection (this); + Rows = new DataGridViewRowCollection (this); + + vscrollbar = new VerticalScrollBar { + Minimum = 0, + Maximum = 0, + SmallChange = 1, + LargeChange = 1, + Visible = false, + Dock = DockStyle.Right + }; + + vscrollbar.ValueChanged += (o, e) => { + top_index = Math.Max (vscrollbar.Value, 0); + Invalidate (); + }; + + hscrollbar = new HorizontalScrollBar { + Minimum = 0, + Maximum = 0, + SmallChange = 10, + LargeChange = 50, + Visible = false, + Dock = DockStyle.Bottom + }; + + hscrollbar.ValueChanged += (o, e) => { + horizontal_scroll_offset = Math.Max (hscrollbar.Value, 0); + Invalidate (); + }; + + Controls.AddImplicitControl (vscrollbar); + Controls.AddImplicitControl (hscrollbar); + } + + /// + /// Gets or sets whether column headers are visible. + /// + public bool ColumnHeadersVisible { + get => column_headers_visible; + set { + if (column_headers_visible != value) { + column_headers_visible = value; + Invalidate (); + } + } + } + + /// + /// Gets the collection of columns in the DataGridView. + /// + public DataGridViewColumnCollection Columns { get; } + + /// + protected override Size DefaultSize => new Size (450, 300); + + /// + public new static readonly ControlStyle DefaultStyle = new ControlStyle (Control.DefaultStyle, + (style) => { + style.BackgroundColor = Theme.ControlLowColor; + style.Border.Width = 1; + }); + + /// + /// Gets the first visible row index. + /// + public int FirstVisibleIndex { + get => top_index; + set { + if (top_index == value) + return; + + if (value < 0 || value >= Rows.Count) + return; + + vscrollbar.Value = Math.Min (value, vscrollbar.Maximum); + } + } + + /// + /// Gets the bounding rectangle for a cell. + /// + public Rectangle GetCellBounds (int rowIndex, int columnIndex) + { + if (rowIndex < 0 || rowIndex >= Rows.Count || columnIndex < 0 || columnIndex >= Columns.Count) + return Rectangle.Empty; + + var client = GetContentArea (); + var row_top = client.Top + (ColumnHeadersVisible ? ScaledHeaderHeight : 0); + var visible_row = rowIndex - top_index; + + if (visible_row < 0 || visible_row >= VisibleRowCount) + return Rectangle.Empty; + + var y = row_top + visible_row * ScaledRowHeight; + var x = client.Left - horizontal_scroll_offset; + + for (var i = 0; i < columnIndex; i++) { + if (Columns[i].Visible) + x += LogicalToDeviceUnits (Columns[i].Width); + } + + var col_width = LogicalToDeviceUnits (Columns[columnIndex].Width); + return new Rectangle (x, y, col_width, ScaledRowHeight); + } + + /// + /// Gets the content area, accounting for scrollbars. + /// + internal Rectangle GetContentArea () + { + var client = ClientRectangle; + var w = client.Width - (vscrollbar.Visible ? vscrollbar.ScaledWidth : 0); + var h = client.Height - (hscrollbar.Visible ? hscrollbar.ScaledHeight : 0); + return new Rectangle (client.Left, client.Top, w, h); + } + + /// + /// Gets the column index at the specified location. + /// + internal int GetColumnAtLocation (Point location) + { + var client = GetContentArea (); + var x = client.Left - horizontal_scroll_offset; + + for (var i = 0; i < Columns.Count; i++) { + if (!Columns[i].Visible) + continue; + + var col_width = LogicalToDeviceUnits (Columns[i].Width); + + if (location.X >= x && location.X < x + col_width) + return i; + + x += col_width; + } + + return -1; + } + + /// + /// Gets the resize column index if the mouse is near a column border. + /// + private int GetResizeColumnAtLocation (Point location) + { + var client = GetContentArea (); + var header_rect = new Rectangle (client.Left, client.Top, client.Width, ScaledHeaderHeight); + + if (!header_rect.Contains (location)) + return -1; + + var x = client.Left - horizontal_scroll_offset; + var resize_zone = LogicalToDeviceUnits (4); + + for (var i = 0; i < Columns.Count; i++) { + if (!Columns[i].Visible) + continue; + + x += LogicalToDeviceUnits (Columns[i].Width); + + if (Math.Abs (location.X - x) <= resize_zone) + return i; + } + + return -1; + } + + /// + /// Gets the row index at the specified location. + /// + internal int GetRowAtLocation (Point location) + { + var client = GetContentArea (); + var row_top = client.Top + (ColumnHeadersVisible ? ScaledHeaderHeight : 0); + + if (location.Y < row_top) + return -1; + + var relative_y = location.Y - row_top; + var row_index = relative_y / ScaledRowHeight + top_index; + + if (row_index >= 0 && row_index < Rows.Count) + return row_index; + + return -1; + } + + /// + /// Gets or sets the height, in pixels, of the column headers row. + /// + public int HeaderHeight { + get => header_height; + set { + if (header_height != value) { + header_height = Math.Max (value, 10); + Invalidate (); + } + } + } + + /// + /// Gets or sets the horizontal scroll offset. + /// + internal int HorizontalScrollOffset => horizontal_scroll_offset; + + /// + /// Gets or sets the index of the currently hovered row. + /// + internal int HoveredRowIndex { + get => hovered_row_index; + set { + if (hovered_row_index != value) { + hovered_row_index = value; + Invalidate (); + } + } + } + + /// + protected override void OnMouseDown (MouseEventArgs e) + { + base.OnMouseDown (e); + + if (!Enabled || !e.Button.HasFlag (MouseButtons.Left)) + return; + + // Check for column resize + if (ColumnHeadersVisible) { + var resize_col = GetResizeColumnAtLocation (e.Location); + + if (resize_col >= 0) { + is_resizing = true; + resize_column_index = resize_col; + resize_start_x = e.Location.X; + resize_start_width = LogicalToDeviceUnits (Columns[resize_col].Width); + return; + } + } + + // Check for header click (sorting) + if (ColumnHeadersVisible) { + var client = GetContentArea (); + var header_rect = new Rectangle (client.Left, client.Top, client.Width, ScaledHeaderHeight); + + if (header_rect.Contains (e.Location)) { + var col = GetColumnAtLocation (e.Location); + + if (col >= 0 && Columns[col].Sortable) + OnColumnHeaderClick (col); + + return; + } + } + + // Select row/cell + var row = GetRowAtLocation (e.Location); + + if (row >= 0) { + if (row_selection_mode) { + SelectedRowIndex = row; + } else { + var col = GetColumnAtLocation (e.Location); + SelectedRowIndex = row; + SelectedColumnIndex = col; + } + } + } + + /// + protected override void OnMouseLeave (EventArgs e) + { + base.OnMouseLeave (e); + HoveredRowIndex = -1; + + if (!is_resizing) + Cursor = Cursors.Arrow; + } + + /// + protected override void OnMouseMove (MouseEventArgs e) + { + base.OnMouseMove (e); + + if (is_resizing) { + var delta = e.Location.X - resize_start_x; + var new_width = DeviceToLogicalUnits (resize_start_width + delta); + Columns[resize_column_index].Width = new_width; + UpdateScrollBars (); + return; + } + + // Update cursor for column resize zones + if (ColumnHeadersVisible) { + var resize_col = GetResizeColumnAtLocation (e.Location); + Cursor = resize_col >= 0 ? Cursors.SizeWestEast : Cursors.Arrow; + } + + // Update hovered row + var row = GetRowAtLocation (e.Location); + HoveredRowIndex = row; + } + + /// + protected override void OnMouseUp (MouseEventArgs e) + { + base.OnMouseUp (e); + + if (is_resizing) { + is_resizing = false; + resize_column_index = -1; + Cursor = Cursors.Arrow; + } + } + + /// + protected override void OnMouseWheel (MouseEventArgs e) + { + base.OnMouseWheel (e); + + if (vscrollbar.Visible) + vscrollbar.RaiseMouseWheel (e); + } + + /// + protected override void OnPaint (PaintEventArgs e) + { + base.OnPaint (e); + + RenderManager.Render (this, e); + } + + /// + /// Handles a column header click for sorting. + /// + private void OnColumnHeaderClick (int columnIndex) + { + var column = Columns[columnIndex]; + + // Toggle sort order + var new_order = column.SortOrder == SortOrder.Ascending + ? SortOrder.Descending + : SortOrder.Ascending; + + // Reset all other columns + foreach (var col in Columns) + col.SortOrder = SortOrder.None; + + column.SortOrder = new_order; + + // Raise the event + ColumnHeaderClick?.Invoke (this, new EventArgs (column)); + + Invalidate (); + } + + /// + /// Raised when a column header is clicked. + /// + public event EventHandler>? ColumnHeaderClick; + + /// + protected override void OnKeyUp (KeyEventArgs e) + { + if (e.KeyCode == Keys.Down) { + if (selected_row_index < Rows.Count - 1) { + SelectedRowIndex = selected_row_index + 1; + EnsureRowVisible (selected_row_index); + e.Handled = true; + return; + } + } + + if (e.KeyCode == Keys.Up) { + if (selected_row_index > 0) { + SelectedRowIndex = selected_row_index - 1; + EnsureRowVisible (selected_row_index); + e.Handled = true; + return; + } + } + + if (e.KeyCode == Keys.PageDown) { + var new_index = Math.Min (selected_row_index + VisibleRowCount, Rows.Count - 1); + SelectedRowIndex = new_index; + EnsureRowVisible (new_index); + e.Handled = true; + return; + } + + if (e.KeyCode == Keys.PageUp) { + var new_index = Math.Max (selected_row_index - VisibleRowCount, 0); + SelectedRowIndex = new_index; + EnsureRowVisible (new_index); + e.Handled = true; + return; + } + + if (e.KeyCode == Keys.Home) { + SelectedRowIndex = 0; + EnsureRowVisible (0); + e.Handled = true; + return; + } + + if (e.KeyCode == Keys.End) { + SelectedRowIndex = Rows.Count - 1; + EnsureRowVisible (Rows.Count - 1); + e.Handled = true; + return; + } + + if (!row_selection_mode) { + if (e.KeyCode == Keys.Left && selected_column_index > 0) { + SelectedColumnIndex = selected_column_index - 1; + e.Handled = true; + return; + } + + if (e.KeyCode == Keys.Right && selected_column_index < Columns.Count - 1) { + SelectedColumnIndex = selected_column_index + 1; + e.Handled = true; + return; + } + } + + base.OnKeyUp (e); + } + + /// + /// Called when the row collection changes. + /// + internal void OnRowsChanged () + { + UpdateScrollBars (); + Invalidate (); + } + + /// + /// Gets the collection of rows in the DataGridView. + /// + public DataGridViewRowCollection Rows { get; } + + /// + /// Gets or sets the default height, in pixels, of each row. + /// + public int RowHeight { + get => row_height; + set { + if (row_height != value) { + row_height = Math.Max (value, 10); + UpdateScrollBars (); + Invalidate (); + } + } + } + + /// + /// Gets or sets whether entire rows are selected when a cell is clicked. + /// + public bool RowSelectionMode { + get => row_selection_mode; + set { + if (row_selection_mode != value) { + row_selection_mode = value; + Invalidate (); + } + } + } + + /// + /// Gets the scaled height of the header row. + /// + public int ScaledHeaderHeight => LogicalToDeviceUnits (header_height); + + /// + /// Gets the scaled height of each data row. + /// + public int ScaledRowHeight => LogicalToDeviceUnits (row_height); + + /// + /// Gets or sets the index of the currently selected column. + /// + public int SelectedColumnIndex { + get => selected_column_index; + set { + if (selected_column_index != value) { + selected_column_index = value; + OnSelectionChanged (EventArgs.Empty); + Invalidate (); + } + } + } + + /// + /// Gets or sets the index of the currently selected row. + /// + public int SelectedRowIndex { + get => selected_row_index; + set { + if (selected_row_index != value) { + // Deselect old row + if (selected_row_index >= 0 && selected_row_index < Rows.Count) + Rows[selected_row_index].Selected = false; + + selected_row_index = value; + + // Select new row + if (selected_row_index >= 0 && selected_row_index < Rows.Count) + Rows[selected_row_index].Selected = true; + + OnSelectionChanged (EventArgs.Empty); + Invalidate (); + } + } + } + + /// + /// Raises the SelectionChanged event. + /// + protected virtual void OnSelectionChanged (EventArgs e) => SelectionChanged?.Invoke (this, e); + + /// + /// Raised when the selection changes. + /// + public event EventHandler? SelectionChanged; + + /// + protected override void SetBoundsCore (int x, int y, int width, int height, BoundsSpecified specified) + { + base.SetBoundsCore (x, y, width, height, specified); + + UpdateScrollBars (); + } + + /// + public override ControlStyle Style { get; } = new ControlStyle (DefaultStyle); + + /// + /// Gets the total width needed to display all columns. + /// + internal int TotalColumnsWidth { + get { + var total = 0; + + foreach (var col in Columns) + if (col.Visible) + total += LogicalToDeviceUnits (col.Width); + + return total; + } + } + + /// + /// Updates the scrollbars based on the current content. + /// + private void UpdateScrollBars () + { + var client = ClientRectangle; + var content_height = client.Height - (ColumnHeadersVisible ? ScaledHeaderHeight : 0); + var visible_rows = content_height / ScaledRowHeight; + + // Vertical scrollbar + if (Rows.Count > visible_rows) { + vscrollbar.Visible = true; + vscrollbar.Maximum = Rows.Count - visible_rows; + vscrollbar.LargeChange = Math.Max (0, visible_rows); + } else { + vscrollbar.Visible = false; + vscrollbar.Value = 0; + top_index = 0; + } + + // Horizontal scrollbar + var available_width = client.Width - (vscrollbar.Visible ? vscrollbar.ScaledWidth : 0); + + if (TotalColumnsWidth > available_width) { + hscrollbar.Visible = true; + hscrollbar.Maximum = TotalColumnsWidth - available_width; + hscrollbar.LargeChange = Math.Max (0, available_width); + } else { + hscrollbar.Visible = false; + hscrollbar.Value = 0; + horizontal_scroll_offset = 0; + } + } + + /// + /// Gets the number of full rows that can be displayed at a time. + /// + public int VisibleRowCount { + get { + var content = GetContentArea (); + var available = content.Height - (ColumnHeadersVisible ? ScaledHeaderHeight : 0); + return Math.Max (0, available / ScaledRowHeight); + } + } + + /// + /// Ensures the specified row is visible by scrolling if necessary. + /// + private void EnsureRowVisible (int index) + { + if (VisibleRowCount >= Rows.Count) + return; + + if (index < top_index) + FirstVisibleIndex = index; + else if (index >= top_index + VisibleRowCount) + FirstVisibleIndex = index - VisibleRowCount + 1; + } + + /// + /// Converts device units to logical units. + /// + internal int DeviceToLogicalUnits (int value) + { + var factor = Scaling; + return factor > 0 ? (int)(value / factor) : value; + } + } +} diff --git a/src/Modern.Forms/DataGridViewCell.cs b/src/Modern.Forms/DataGridViewCell.cs new file mode 100644 index 0000000..21e6429 --- /dev/null +++ b/src/Modern.Forms/DataGridViewCell.cs @@ -0,0 +1,81 @@ +using System.Drawing; + +namespace Modern.Forms +{ + /// + /// Represents a cell in a DataGridView control. + /// + public class DataGridViewCell + { + private string value = string.Empty; + private DataGridViewRow? owner; + + /// + /// Initializes a new instance of the DataGridViewCell class. + /// + public DataGridViewCell () + { + } + + /// + /// Initializes a new instance of the DataGridViewCell class with the specified value. + /// + public DataGridViewCell (string value) + { + this.value = value; + } + + /// + /// Gets the bounding rectangle of the cell. + /// + internal Rectangle Bounds { get; set; } + + /// + /// Gets the column index of this cell. + /// + public int ColumnIndex => owner?.Cells.IndexOf (this) ?? -1; + + /// + /// Gets the DataGridView that contains this cell. + /// + public DataGridView? DataGridView => owner?.DataGridView; + + /// + /// Gets the row that contains this cell. + /// + public DataGridViewRow? OwningRow => owner; + + /// + /// Gets the row index of this cell. + /// + public int RowIndex => owner?.Index ?? -1; + + /// + /// Gets or sets whether this cell is selected. + /// + public bool Selected { get; set; } + + /// + /// Gets or sets an object that contains data to associate with the cell. + /// + public object? Tag { get; set; } + + /// + /// Gets or sets the value of this cell. + /// + public string Value { + get => value; + set { + if (this.value != value) { + this.value = value; + owner?.DataGridView?.Invalidate (); + } + } + } + + /// + /// Sets the owning row. + /// + internal void SetOwner (DataGridViewRow? row) => owner = row; + } +} diff --git a/src/Modern.Forms/DataGridViewCellCollection.cs b/src/Modern.Forms/DataGridViewCellCollection.cs new file mode 100644 index 0000000..9523be0 --- /dev/null +++ b/src/Modern.Forms/DataGridViewCellCollection.cs @@ -0,0 +1,65 @@ +using System.Collections.ObjectModel; + +namespace Modern.Forms +{ + /// + /// Represents a collection of DataGridViewCell objects in a DataGridViewRow. + /// + public class DataGridViewCellCollection : Collection + { + private readonly DataGridViewRow owner; + + /// + /// Initializes a new instance of the DataGridViewCellCollection class. + /// + internal DataGridViewCellCollection (DataGridViewRow owner) + { + this.owner = owner; + } + + /// + /// Adds a cell with the specified value to the collection. + /// + public DataGridViewCell Add (string value) + { + var cell = new DataGridViewCell (value); + Add (cell); + return cell; + } + + /// + protected override void ClearItems () + { + foreach (var cell in this) + cell.SetOwner (null); + + base.ClearItems (); + owner.DataGridView?.Invalidate (); + } + + /// + protected override void InsertItem (int index, DataGridViewCell item) + { + item.SetOwner (owner); + base.InsertItem (index, item); + owner.DataGridView?.Invalidate (); + } + + /// + protected override void RemoveItem (int index) + { + this[index].SetOwner (null); + base.RemoveItem (index); + owner.DataGridView?.Invalidate (); + } + + /// + protected override void SetItem (int index, DataGridViewCell item) + { + this[index].SetOwner (null); + item.SetOwner (owner); + base.SetItem (index, item); + owner.DataGridView?.Invalidate (); + } + } +} diff --git a/src/Modern.Forms/DataGridViewColumn.cs b/src/Modern.Forms/DataGridViewColumn.cs new file mode 100644 index 0000000..e1a91e9 --- /dev/null +++ b/src/Modern.Forms/DataGridViewColumn.cs @@ -0,0 +1,121 @@ +using System.Drawing; + +namespace Modern.Forms +{ + /// + /// Represents a column in a DataGridView control. + /// + public class DataGridViewColumn + { + private string header_text = string.Empty; + private int width = 100; + private DataGridView? owner; + + /// + /// Initializes a new instance of the DataGridViewColumn class. + /// + public DataGridViewColumn () + { + } + + /// + /// Initializes a new instance of the DataGridViewColumn class with the specified header text. + /// + public DataGridViewColumn (string headerText) + { + header_text = headerText; + } + + /// + /// Gets the bounding rectangle of the column header. + /// + internal Rectangle HeaderBounds { get; set; } + + /// + /// Gets or sets the header text for this column. + /// + public string HeaderText { + get => header_text; + set { + if (header_text != value) { + header_text = value; + owner?.Invalidate (); + } + } + } + + /// + /// Gets the index of this column in the DataGridView. + /// + public int Index => owner?.Columns.IndexOf (this) ?? -1; + + /// + /// Gets or sets the minimum width, in pixels, of the column. + /// + public int MinimumWidth { get; set; } = 30; + + /// + /// Gets the DataGridView control that contains this column. + /// + public DataGridView? DataGridView => owner; + + /// + /// Gets or sets a value indicating whether the column is sortable. + /// + public bool Sortable { get; set; } = true; + + /// + /// Gets or sets the sort order for this column. + /// + public SortOrder SortOrder { get; set; } = SortOrder.None; + + /// + /// Gets or sets an object that contains data to associate with the column. + /// + public object? Tag { get; set; } + + /// + /// Gets or sets whether the column is visible. + /// + public bool Visible { get; set; } = true; + + /// + /// Gets or sets the width, in pixels, of the column. + /// + public int Width { + get => width; + set { + value = Math.Max (value, MinimumWidth); + + if (width != value) { + width = value; + owner?.Invalidate (); + } + } + } + + /// + /// Sets the owning DataGridView. + /// + internal void SetOwner (DataGridView? dataGridView) => owner = dataGridView; + } + + /// + /// Specifies the sort order for a column. + /// + public enum SortOrder + { + /// + /// No sort order. + /// + None, + /// + /// Items are sorted in ascending order. + /// + Ascending, + /// + /// Items are sorted in descending order. + /// + Descending + } +} diff --git a/src/Modern.Forms/DataGridViewColumnCollection.cs b/src/Modern.Forms/DataGridViewColumnCollection.cs new file mode 100644 index 0000000..256aa46 --- /dev/null +++ b/src/Modern.Forms/DataGridViewColumnCollection.cs @@ -0,0 +1,75 @@ +using System.Collections.ObjectModel; + +namespace Modern.Forms +{ + /// + /// Represents a collection of DataGridViewColumn objects in a DataGridView control. + /// + public class DataGridViewColumnCollection : Collection + { + private readonly DataGridView owner; + + /// + /// Initializes a new instance of the DataGridViewColumnCollection class. + /// + internal DataGridViewColumnCollection (DataGridView owner) + { + this.owner = owner; + } + + /// + /// Adds a column with the specified header text to the collection. + /// + public DataGridViewColumn Add (string headerText) + { + var column = new DataGridViewColumn (headerText); + Add (column); + return column; + } + + /// + /// Adds a column with the specified header text and width to the collection. + /// + public DataGridViewColumn Add (string headerText, int width) + { + var column = new DataGridViewColumn (headerText) { Width = width }; + Add (column); + return column; + } + + /// + protected override void ClearItems () + { + foreach (var column in this) + column.SetOwner (null); + + base.ClearItems (); + owner.Invalidate (); + } + + /// + protected override void InsertItem (int index, DataGridViewColumn item) + { + item.SetOwner (owner); + base.InsertItem (index, item); + owner.Invalidate (); + } + + /// + protected override void RemoveItem (int index) + { + this[index].SetOwner (null); + base.RemoveItem (index); + owner.Invalidate (); + } + + /// + protected override void SetItem (int index, DataGridViewColumn item) + { + this[index].SetOwner (null); + item.SetOwner (owner); + base.SetItem (index, item); + owner.Invalidate (); + } + } +} diff --git a/src/Modern.Forms/DataGridViewRow.cs b/src/Modern.Forms/DataGridViewRow.cs new file mode 100644 index 0000000..ca9ba7a --- /dev/null +++ b/src/Modern.Forms/DataGridViewRow.cs @@ -0,0 +1,69 @@ +using System.Drawing; + +namespace Modern.Forms +{ + /// + /// Represents a row in a DataGridView control. + /// + public class DataGridViewRow + { + private int height = 25; + private DataGridView? owner; + + /// + /// Initializes a new instance of the DataGridViewRow class. + /// + public DataGridViewRow () + { + Cells = new DataGridViewCellCollection (this); + } + + /// + /// Gets the bounding rectangle of the row. + /// + internal Rectangle Bounds { get; set; } + + /// + /// Gets the collection of cells in this row. + /// + public DataGridViewCellCollection Cells { get; } + + /// + /// Gets the DataGridView that contains this row. + /// + public DataGridView? DataGridView => owner; + + /// + /// Gets or sets the height, in pixels, of the row. + /// + public int Height { + get => height; + set { + if (height != value) { + height = Math.Max (value, 10); + owner?.Invalidate (); + } + } + } + + /// + /// Gets the index of this row in the DataGridView. + /// + public int Index => owner?.Rows.IndexOf (this) ?? -1; + + /// + /// Gets or sets whether this row is selected. + /// + public bool Selected { get; set; } + + /// + /// Gets or sets an object that contains data to associate with the row. + /// + public object? Tag { get; set; } + + /// + /// Sets the owning DataGridView. + /// + internal void SetOwner (DataGridView? dataGridView) => owner = dataGridView; + } +} diff --git a/src/Modern.Forms/DataGridViewRowCollection.cs b/src/Modern.Forms/DataGridViewRowCollection.cs new file mode 100644 index 0000000..2353be6 --- /dev/null +++ b/src/Modern.Forms/DataGridViewRowCollection.cs @@ -0,0 +1,69 @@ +using System.Collections.ObjectModel; + +namespace Modern.Forms +{ + /// + /// Represents a collection of DataGridViewRow objects in a DataGridView control. + /// + public class DataGridViewRowCollection : Collection + { + private readonly DataGridView owner; + + /// + /// Initializes a new instance of the DataGridViewRowCollection class. + /// + internal DataGridViewRowCollection (DataGridView owner) + { + this.owner = owner; + } + + /// + /// Adds a new row with the specified cell values. + /// + public DataGridViewRow Add (params string[] values) + { + var row = new DataGridViewRow (); + + foreach (var value in values) + row.Cells.Add (value); + + Add (row); + return row; + } + + /// + protected override void ClearItems () + { + foreach (var row in this) + row.SetOwner (null); + + base.ClearItems (); + owner.OnRowsChanged (); + } + + /// + protected override void InsertItem (int index, DataGridViewRow item) + { + item.SetOwner (owner); + base.InsertItem (index, item); + owner.OnRowsChanged (); + } + + /// + protected override void RemoveItem (int index) + { + this[index].SetOwner (null); + base.RemoveItem (index); + owner.OnRowsChanged (); + } + + /// + protected override void SetItem (int index, DataGridViewRow item) + { + this[index].SetOwner (null); + item.SetOwner (owner); + base.SetItem (index, item); + owner.OnRowsChanged (); + } + } +} diff --git a/src/Modern.Forms/Renderers/DataGridViewRenderer.cs b/src/Modern.Forms/Renderers/DataGridViewRenderer.cs new file mode 100644 index 0000000..0b2ffcc --- /dev/null +++ b/src/Modern.Forms/Renderers/DataGridViewRenderer.cs @@ -0,0 +1,208 @@ +using System.Drawing; +using SkiaSharp; + +namespace Modern.Forms.Renderers +{ + /// + /// Represents a class that can render a DataGridView. + /// + public class DataGridViewRenderer : Renderer + { + /// + protected override void Render (DataGridView control, PaintEventArgs e) + { + var content = control.GetContentArea (); + + e.Canvas.Save (); + e.Canvas.Clip (content); + + // Draw column headers + if (control.ColumnHeadersVisible) + RenderColumnHeaders (control, e, content); + + // Draw rows + RenderRows (control, e, content); + + e.Canvas.Restore (); + } + + /// + /// Renders the column headers. + /// + protected virtual void RenderColumnHeaders (DataGridView control, PaintEventArgs e, Rectangle contentArea) + { + var header_height = control.ScaledHeaderHeight; + var x = contentArea.Left - control.HorizontalScrollOffset; + var y = contentArea.Top; + + // Draw header background + var header_rect = new Rectangle (contentArea.Left, y, contentArea.Width, header_height); + e.Canvas.FillRectangle (header_rect, Theme.ControlMidColor); + + for (var i = 0; i < control.Columns.Count; i++) { + var column = control.Columns[i]; + + if (!column.Visible) + continue; + + var col_width = control.LogicalToDeviceUnits (column.Width); + var cell_rect = new Rectangle (x, y, col_width, header_height); + + column.HeaderBounds = cell_rect; + + RenderColumnHeader (control, column, i, cell_rect, e); + + x += col_width; + } + + // Draw header bottom border + e.Canvas.DrawLine (contentArea.Left, y + header_height - 1, contentArea.Right, y + header_height - 1, Theme.BorderMidColor); + } + + /// + /// Renders a single column header. + /// + protected virtual void RenderColumnHeader (DataGridView control, DataGridViewColumn column, int columnIndex, Rectangle bounds, PaintEventArgs e) + { + // Draw right border + e.Canvas.DrawLine (bounds.Right - 1, bounds.Top, bounds.Right - 1, bounds.Bottom, Theme.BorderLowColor); + + // Draw text + var text_bounds = bounds; + text_bounds.Inflate (-6, 0); + + e.Canvas.DrawText (column.HeaderText, Theme.UIFontBold, control.LogicalToDeviceUnits (Theme.ItemFontSize), text_bounds, Theme.ForegroundColor, ContentAlignment.MiddleLeft, maxLines: 1); + + // Draw sort indicator + if (column.SortOrder != SortOrder.None) + RenderSortGlyph (e, bounds, column.SortOrder); + } + + /// + /// Renders the sort direction glyph. + /// + protected virtual void RenderSortGlyph (PaintEventArgs e, Rectangle bounds, SortOrder sortOrder) + { + var glyph_size = 6; + var glyph_x = bounds.Right - glyph_size - 8; + var glyph_y = bounds.Top + (bounds.Height - glyph_size) / 2; + + using var path = new SKPath (); + + if (sortOrder == SortOrder.Ascending) { + path.MoveTo (glyph_x, glyph_y + glyph_size); + path.LineTo (glyph_x + glyph_size / 2, glyph_y); + path.LineTo (glyph_x + glyph_size, glyph_y + glyph_size); + path.Close (); + } else { + path.MoveTo (glyph_x, glyph_y); + path.LineTo (glyph_x + glyph_size / 2, glyph_y + glyph_size); + path.LineTo (glyph_x + glyph_size, glyph_y); + path.Close (); + } + + using var paint = new SKPaint { Color = Theme.ForegroundColor, IsAntialias = true }; + e.Canvas.DrawPath (path, paint); + } + + /// + /// Renders the data rows. + /// + protected virtual void RenderRows (DataGridView control, PaintEventArgs e, Rectangle contentArea) + { + var row_height = control.ScaledRowHeight; + var header_offset = control.ColumnHeadersVisible ? control.ScaledHeaderHeight : 0; + var y = contentArea.Top + header_offset; + + for (var i = control.FirstVisibleIndex; i < control.Rows.Count; i++) { + if (y >= contentArea.Bottom) + break; + + var row = control.Rows[i]; + var row_rect = new Rectangle (contentArea.Left, y, contentArea.Width, Math.Min (row_height, contentArea.Bottom - y)); + + row.Bounds = row_rect; + + RenderRow (control, row, i, row_rect, e); + + y += row_height; + } + } + + /// + /// Renders a single row. + /// + protected virtual void RenderRow (DataGridView control, DataGridViewRow row, int rowIndex, Rectangle bounds, PaintEventArgs e) + { + // Draw selection background + if (control.SelectedRowIndex == rowIndex) + e.Canvas.FillRectangle (bounds, Theme.ControlHighlightLowColor); + // Draw hover background + else if (control.HoveredRowIndex == rowIndex) + e.Canvas.FillRectangle (bounds, Theme.ControlMidColor); + // Draw alternating row background + else if (rowIndex % 2 == 1) + e.Canvas.FillRectangle (bounds, AlternatingRowColor ()); + + // Draw cells + var x = bounds.Left - control.HorizontalScrollOffset; + + for (var i = 0; i < control.Columns.Count; i++) { + var column = control.Columns[i]; + + if (!column.Visible) + continue; + + var col_width = control.LogicalToDeviceUnits (column.Width); + var cell_rect = new Rectangle (x, bounds.Top, col_width, bounds.Height); + + var cell_value = i < row.Cells.Count ? row.Cells[i].Value : string.Empty; + + if (i < row.Cells.Count) + row.Cells[i].Bounds = cell_rect; + + RenderCell (control, cell_value, rowIndex, i, cell_rect, e); + + x += col_width; + } + + // Draw row bottom border + e.Canvas.DrawLine (bounds.Left, bounds.Bottom - 1, bounds.Right, bounds.Bottom - 1, Theme.BorderLowColor); + } + + /// + /// Renders a single cell. + /// + protected virtual void RenderCell (DataGridView control, string value, int rowIndex, int columnIndex, Rectangle bounds, PaintEventArgs e) + { + // Draw cell right border + e.Canvas.DrawLine (bounds.Right - 1, bounds.Top, bounds.Right - 1, bounds.Bottom, Theme.BorderLowColor); + + // Draw cell selection for cell mode + if (!control.RowSelectionMode && control.SelectedRowIndex == rowIndex && control.SelectedColumnIndex == columnIndex) + e.Canvas.DrawRectangle (bounds, Theme.AccentColor, 2); + + // Draw text + var text_bounds = bounds; + text_bounds.Inflate (-4, 0); + text_bounds.Height = control.ScaledRowHeight; // Ensure consistent text positioning + + e.Canvas.DrawText (value, Theme.UIFont, control.LogicalToDeviceUnits (Theme.ItemFontSize), text_bounds, Theme.ForegroundColor, ContentAlignment.MiddleLeft, maxLines: 1); + } + + /// + /// Gets the alternating row background color. + /// + private static SKColor AlternatingRowColor () + { + // Slightly different from the default background + var bg = Theme.ControlLowColor; + return new SKColor ( + (byte)Math.Max (0, bg.Red - 5), + (byte)Math.Max (0, bg.Green - 5), + (byte)Math.Max (0, bg.Blue - 5), + bg.Alpha + ); + } + } +} diff --git a/src/Modern.Forms/Renderers/RenderManager.cs b/src/Modern.Forms/Renderers/RenderManager.cs index 1f42164..10ff64e 100644 --- a/src/Modern.Forms/Renderers/RenderManager.cs +++ b/src/Modern.Forms/Renderers/RenderManager.cs @@ -12,6 +12,7 @@ static RenderManager () SetRenderer