using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.Drawing; using System.Windows.Forms; #nullable enable namespace LibationWinForms.ProcessQueue { internal partial class VirtualFlowControl : UserControl { /// /// Triggered when one of the 's buttons has been clicked /// public event EventHandler? ButtonClicked; public IList? Items { get; private set; } private object? m_OldContext; protected override void OnDataContextChanged(EventArgs e) { if (m_OldContext is INotifyCollectionChanged oldNotify) oldNotify.CollectionChanged -= Items_CollectionChanged; if (DataContext is INotifyCollectionChanged newNotify) { m_OldContext = newNotify; newNotify.CollectionChanged += Items_CollectionChanged; } Items = DataContext as IList; base.OnDataContextChanged(e); } private void Items_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { RefreshDisplay(); } public void RefreshDisplay() { AdjustScrollBar(); DoVirtualScroll(); } #region Dynamic Properties /// /// The number of virtual s in the /// public int VirtualControlCount => Items?.Count ?? 0; int ScrollValue => Math.Max(vScrollBar1.Value, 0); /// /// Amount the control moves with a small scroll change /// private int SmallScrollChange => VirtualControlHeight * SMALL_SCROLL_CHANGE_MULTIPLE; /// /// Amount the control moves with a large scroll change. Equal to the number of whole s in the panel, less 1. /// private int LargeScrollChange => Math.Max(DisplayHeight / VirtualControlHeight - 1, SMALL_SCROLL_CHANGE_MULTIPLE) * VirtualControlHeight; /// /// Virtual height of all virtual controls within this /// private int VirtualHeight => (VirtualControlCount + NUM_BLANK_SPACES_AT_BOTTOM) * VirtualControlHeight - DisplayHeight + 2 * TopMargin; /// /// Index of the first virtual /// private int FirstVisibleVirtualIndex => ScrollValue / VirtualControlHeight; /// /// The display height of this /// private int DisplayHeight => DisplayRectangle.Height; #endregion #region Instance variables /// /// The total height, including margins, of the repeated /// private readonly int VirtualControlHeight; /// /// Margin between the top and the top of the Panel, and the bottom and the bottom of the panel /// private readonly int TopMargin; private readonly List BookControls = new(); #endregion #region Global behavior settings /// /// Total number of actual controls added to the panel. 23 is sufficient up to a 4k monitor height. /// private const int NUM_ACTUAL_CONTROLS = 23; /// /// Multiple of that is moved for each small scroll change /// private const int SMALL_SCROLL_CHANGE_MULTIPLE = 1; /// /// Amount of space at the bottom of the , in multiples of /// private const int NUM_BLANK_SPACES_AT_BOTTOM = 2; #endregion public VirtualFlowControl() { InitializeComponent(); panel1.Resize += (_, _) => RefreshDisplay(); var control = InitControl(0); VirtualControlHeight = this.DpiUnscale(control.Height + control.Margin.Top + control.Margin.Bottom); TopMargin = control.Margin.Top; BookControls.Add(control); panel1.Controls.Add(control); for (int i = 1; i < NUM_ACTUAL_CONTROLS; i++) { control = InitControl(VirtualControlHeight * i); BookControls.Add(control); panel1.Controls.Add(control); } vScrollBar1.Scroll += (_, s) => SetScrollPosition(s.NewValue); vScrollBar1.SmallChange = SmallScrollChange; panel1.Height += this.DpiScale(NUM_BLANK_SPACES_AT_BOTTOM * VirtualControlHeight); } private ProcessBookControl InitControl(int locationY) { var control = new ProcessBookControl(); control.Location = new Point(control.Margin.Left, locationY + control.Margin.Top); control.Width = panel1.ClientRectangle.Width - control.Margin.Left - control.Margin.Right; control.Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Top; control.cancelBtn.Click += ControlButton_Click; control.moveFirstBtn.Click += ControlButton_Click; control.moveUpBtn.Click += ControlButton_Click; control.moveDownBtn.Click += ControlButton_Click; control.moveLastBtn.Click += ControlButton_Click; return control; } /// /// Handles all button clicks from all , detects which one sent the click, and fires to notify the model of the click /// private void ControlButton_Click(object? sender, EventArgs e) { Control? button = sender as Control; Control? form = button?.Parent; while (form is not null and not ProcessBookControl) form = form?.Parent; if (form is not null && button?.Name is string buttonText) ButtonClicked?.Invoke(form, buttonText); } /// /// Adjusts the max width and enabled status based on the and the /// private void AdjustScrollBar() { int maxFullVisible = DisplayHeight / VirtualControlHeight; if (VirtualControlCount <= maxFullVisible) { vScrollBar1.Enabled = false; vScrollBar1.Value = 0; for (int i = VirtualControlCount; i < NUM_ACTUAL_CONTROLS; i++) BookControls[i].Visible = false; } else { vScrollBar1.Enabled = true; //https://stackoverflow.com/a/2882878/3335599 int newMaximum = VirtualHeight + LargeScrollChange - 1; if (newMaximum < vScrollBar1.Maximum) vScrollBar1.Value = Math.Max(vScrollBar1.Value - (vScrollBar1.Maximum - newMaximum), 0); vScrollBar1.Maximum = newMaximum; vScrollBar1.LargeChange = LargeScrollChange; } } /// /// Calculated the virtual controls that are in view at the current scroll position and windows size, /// positions to simulate scroll activity, then fires updates the controls with /// the context corresponding to the virtual scroll position /// private void DoVirtualScroll() { int firstVisible = FirstVisibleVirtualIndex; int position = ScrollValue % VirtualControlHeight; panel1.Location = new Point(0, -position); int numVisible = DisplayHeight / VirtualControlHeight; if (DisplayHeight % VirtualControlHeight != 0) numVisible++; numVisible = Math.Min(numVisible, VirtualControlCount); numVisible = Math.Min(numVisible, VirtualControlCount - firstVisible); if (Items is IList items) { for (int i = 0; i < numVisible; i++) BookControls[i].DataContext = items[firstVisible + i]; } for (int i = 0; i < BookControls.Count; i++) { BookControls[i].Visible = i < numVisible; } } /// /// Set scroll value to an integral multiple of /// private void SetScrollPosition(int value) { if (!vScrollBar1.Enabled) return; int newPos = (int)Math.Round((double)value / SmallScrollChange) * SmallScrollChange; if (vScrollBar1.Value != newPos) { //https://stackoverflow.com/a/2882878/3335599 vScrollBar1.Value = Math.Min(newPos, vScrollBar1.Maximum - vScrollBar1.LargeChange + 1); DoVirtualScroll(); } } private const int WM_MOUSEWHEEL = 522; private const int WHEEL_DELTA = 120; protected override void WndProc(ref Message m) { //Capture mouse wheel movement and interpret it as a scroll event if (m.Msg == WM_MOUSEWHEEL) { //https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-mousewheel int wheelDelta = -(short)(((ulong)m.WParam) >> 16 & ushort.MaxValue); int numSmallPositionMoves = Math.Abs(wheelDelta) / WHEEL_DELTA; int scrollDelta = Math.Sign(wheelDelta) * numSmallPositionMoves * SmallScrollChange; int newScrollPosition; if (scrollDelta > 0) newScrollPosition = Math.Min(vScrollBar1.Value + scrollDelta, vScrollBar1.Maximum); else newScrollPosition = Math.Max(vScrollBar1.Value + scrollDelta, vScrollBar1.Minimum); SetScrollPosition(newScrollPosition); } base.WndProc(ref m); } } }