Host PlotView in MainWindow.xaml (Phase 5b step 5)

PlotView now has a parameterless ctor + AttachViewModel(vm) so it can be instantiated by the XAML loader. The context flyout is built lazily in AttachViewModel since it captures live ViewModel state. Class is now public sealed (XAML codegen requirement). MainWindow's BuildPlotPane is gone; the plot pane (toolbar + <controls:PlotView x:Name=PlotControl/>) lives in XAML and the ctor just calls PlotControl.AttachViewModel(ViewModel).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-06-02 13:36:20 -05:00
co-authored by Copilot
parent fd55a89e22
commit 35f730c89a
3 changed files with 79 additions and 94 deletions
@@ -21,7 +21,7 @@ using Microsoft.UI.Xaml.Shapes;
namespace LibreHardwareMonitor.Windows.WinUI.Controls;
internal sealed class PlotView : Grid
public sealed class PlotView : Grid
{
internal static readonly (string Label, int Value)[] PlotTimeWindowOptions =
[
@@ -47,14 +47,12 @@ internal sealed class PlotView : Grid
private const double PlotRightMargin = 12;
private const double PlotTopMargin = 10;
private readonly MainWindowViewModel _viewModel;
private MainWindowViewModel? _viewModel;
private readonly Canvas _canvas;
private double _plotValueZoomFactor = 1;
public PlotView(MainWindowViewModel viewModel)
public PlotView()
{
_viewModel = viewModel ?? throw new ArgumentNullException(nameof(viewModel));
_canvas = new Canvas
{
MinWidth = 320,
@@ -63,12 +61,24 @@ internal sealed class PlotView : Grid
VerticalAlignment = VerticalAlignment.Stretch,
Background = (Brush)Application.Current.Resources["SystemControlBackgroundAltHighBrush"]
};
_canvas.ContextFlyout = BuildPlotContextMenu();
_canvas.SizeChanged += (_, _) => Redraw();
_canvas.PointerWheelChanged += PlotCanvas_PointerWheelChanged;
Children.Add(_canvas);
}
public PlotView(MainWindowViewModel viewModel) : this()
{
AttachViewModel(viewModel);
}
public void AttachViewModel(MainWindowViewModel viewModel)
{
_viewModel = viewModel ?? throw new ArgumentNullException(nameof(viewModel));
_canvas.ContextFlyout = BuildPlotContextMenu();
}
private MainWindowViewModel VM => _viewModel ?? throw new InvalidOperationException("PlotView used before AttachViewModel.");
public void ApplyTheme(AppThemeMode themeMode)
{
RequestedTheme = themeMode switch
@@ -93,9 +103,12 @@ internal sealed class PlotView : Grid
public void Redraw()
{
if (_viewModel == null)
return;
_canvas.Children.Clear();
_canvas.Background = new SolidColorBrush(GetPlotBackgroundColor());
if (!_viewModel.ShowPlot)
if (!VM.ShowPlot)
return;
double width = GetPlotCanvasWidth(_canvas);
@@ -106,12 +119,12 @@ internal sealed class PlotView : Grid
PlotBounds bounds = GetPlotBounds(width, height);
DrawPlotFrame(_canvas, bounds);
PlotSeriesViewModel[] plotSeriesWithPoints = _viewModel.PlotSeries
PlotSeriesViewModel[] plotSeriesWithPoints = VM.PlotSeries
.Where(plotSeries => plotSeries.Points.Count > 0)
.ToArray();
if (plotSeriesWithPoints.Length == 0)
{
DrawPlotMessage(_canvas, bounds, _viewModel.PlotSeries.Count == 0 ? "No sensors selected for plot" : "Waiting for sensor samples...");
DrawPlotMessage(_canvas, bounds, VM.PlotSeries.Count == 0 ? "No sensors selected for plot" : "Waiting for sensor samples...");
return;
}
@@ -130,8 +143,8 @@ internal sealed class PlotView : Grid
if (minTimestamp == DateTime.MaxValue || maxTimestamp == DateTime.MinValue)
return;
if (_viewModel.PlotTimeWindow.HasValue)
minTimestamp = maxTimestamp - _viewModel.PlotTimeWindow.Value;
if (VM.PlotTimeWindow.HasValue)
minTimestamp = maxTimestamp - VM.PlotTimeWindow.Value;
if (maxTimestamp <= minTimestamp)
maxTimestamp = minTimestamp.AddSeconds(1);
@@ -173,7 +186,7 @@ internal sealed class PlotView : Grid
.OrderBy(group => group.Key)
.ToArray();
double axisHeight = _viewModel.PlotStackedAxes ? bounds.Height / groups.Length : bounds.Height;
double axisHeight = VM.PlotStackedAxes ? bounds.Height / groups.Length : bounds.Height;
for (int i = 0; i < groups.Length; i++)
{
IGrouping<SensorType, PlotSeriesSample> group = groups[i];
@@ -185,12 +198,12 @@ internal sealed class PlotView : Grid
PlotAxisLayout axis = new(
group.Key,
samples.First().Series.Unit,
_viewModel.PlotStackedAxes ? bounds.Top + axisHeight * i : bounds.Top,
VM.PlotStackedAxes ? bounds.Top + axisHeight * i : bounds.Top,
axisHeight,
minValue,
maxValue);
DrawValueAxis(_canvas, bounds, axis, drawGrid: _viewModel.PlotStackedAxes || i == 0, titleIndex: i);
DrawValueAxis(_canvas, bounds, axis, drawGrid: VM.PlotStackedAxes || i == 0, titleIndex: i);
foreach (PlotSeriesSample sample in samples)
DrawPlotSeries(_canvas, bounds, axis, minTimestamp, maxTimestamp, sample);
}
@@ -199,23 +212,23 @@ internal sealed class PlotView : Grid
private MenuFlyout BuildPlotContextMenu()
{
MenuFlyout flyout = new();
flyout.Items.Add(CreateToggleSettingItem("Stacked Axes", () => _viewModel.PlotStackedAxes, value => _viewModel.PlotStackedAxes = value));
flyout.Items.Add(CreateToggleSettingItem("Show Axes Labels", () => _viewModel.ShowPlotAxisLabels, value => _viewModel.ShowPlotAxisLabels = value));
flyout.Items.Add(CreateToggleSettingItem("Stacked Axes", () => VM.PlotStackedAxes, value => VM.PlotStackedAxes = value));
flyout.Items.Add(CreateToggleSettingItem("Show Axes Labels", () => VM.ShowPlotAxisLabels, value => VM.ShowPlotAxisLabels = value));
MenuFlyoutSubItem timeAxis = new() { Text = "Time Axis" };
timeAxis.Items.Add(CreateToggleSettingItem("Enable Zoom", () => _viewModel.PlotTimeAxisZoomEnabled, value => _viewModel.PlotTimeAxisZoomEnabled = value));
timeAxis.Items.Add(CreateToggleSettingItem("Enable Zoom", () => VM.PlotTimeAxisZoomEnabled, value => VM.PlotTimeAxisZoomEnabled = value));
timeAxis.Items.Add(new MenuFlyoutSeparator());
foreach ((string label, int value) in PlotTimeWindowOptions)
{
ToggleMenuFlyoutItem item = new()
{
Text = label,
IsChecked = _viewModel.PlotTimeWindowIndex == value,
IsChecked = VM.PlotTimeWindowIndex == value,
Tag = value
};
item.Click += (_, _) =>
{
_viewModel.PlotTimeWindowIndex = value;
VM.PlotTimeWindowIndex = value;
foreach (ToggleMenuFlyoutItem sibling in timeAxis.Items.OfType<ToggleMenuFlyoutItem>())
sibling.IsChecked = Equals(sibling.Tag, value);
};
@@ -225,7 +238,7 @@ internal sealed class PlotView : Grid
flyout.Items.Add(timeAxis);
MenuFlyoutSubItem valueAxes = new() { Text = "Value Axes" };
valueAxes.Items.Add(CreateToggleSettingItem("Enable Zoom", () => _viewModel.PlotValueAxesZoomEnabled, value => _viewModel.PlotValueAxesZoomEnabled = value));
valueAxes.Items.Add(CreateToggleSettingItem("Enable Zoom", () => VM.PlotValueAxesZoomEnabled, value => VM.PlotValueAxesZoomEnabled = value));
valueAxes.Items.Add(CreateMenuItem("Autoscale All", (_, _) => ResetZoom()));
flyout.Items.Add(valueAxes);
@@ -261,14 +274,14 @@ internal sealed class PlotView : Grid
return;
PlotBounds bounds = GetPlotBounds(GetPlotCanvasWidth(canvas), GetPlotCanvasHeight(canvas));
if (_viewModel.PlotValueAxesZoomEnabled && ShouldDrawPlotAxisLabels(bounds) && pointerPoint.Position.X <= bounds.Left)
if (VM.PlotValueAxesZoomEnabled && ShouldDrawPlotAxisLabels(bounds) && pointerPoint.Position.X <= bounds.Left)
{
ZoomPlotValueAxes(wheelDelta);
e.Handled = true;
return;
}
if (_viewModel.PlotTimeAxisZoomEnabled)
if (VM.PlotTimeAxisZoomEnabled)
{
ZoomPlotTimeAxis(wheelDelta);
e.Handled = true;
@@ -284,13 +297,13 @@ internal sealed class PlotView : Grid
private void ZoomPlotTimeAxis(int wheelDelta)
{
int lastIndex = PlotTimeWindowOptions[^1].Value;
int index = _viewModel.PlotTimeWindowIndex;
int index = VM.PlotTimeWindowIndex;
if (wheelDelta > 0)
index = index == 0 ? lastIndex : Math.Max(1, index - 1);
else
index = index >= lastIndex ? 0 : index + 1;
_viewModel.PlotTimeWindowIndex = index;
VM.PlotTimeWindowIndex = index;
}
private static double GetPlotCanvasWidth(Canvas canvas)
@@ -305,10 +318,10 @@ internal sealed class PlotView : Grid
private PlotBounds GetPlotBounds(double width, double height)
{
double left = _viewModel.ShowPlotAxisLabels ? PlotLeftMargin : 0;
double top = _viewModel.ShowPlotAxisLabels ? PlotTopMargin : 0;
double right = _viewModel.ShowPlotAxisLabels ? PlotRightMargin : 0;
double bottom = _viewModel.ShowPlotAxisLabels ? PlotBottomMargin : 0;
double left = VM.ShowPlotAxisLabels ? PlotLeftMargin : 0;
double top = VM.ShowPlotAxisLabels ? PlotTopMargin : 0;
double right = VM.ShowPlotAxisLabels ? PlotRightMargin : 0;
double bottom = VM.ShowPlotAxisLabels ? PlotBottomMargin : 0;
if (width <= left + right + 32 || height <= top + bottom + 24)
return new PlotBounds(0, 0, Math.Max(1, width), Math.Max(1, height));
@@ -358,13 +371,13 @@ internal sealed class PlotView : Grid
}
}
if (_viewModel.PlotStackedAxes)
if (VM.PlotStackedAxes)
DrawLine(canvas, bounds.Left, axis.Top, bounds.Right, axis.Top, borderBrush, 1);
if (drawLabels)
{
string unit = string.IsNullOrWhiteSpace(axis.Unit) ? "" : $" ({axis.Unit})";
AddPlotLabel(canvas, $"{SensorTypeDisplay.GetText(axis.SensorType)}{unit}", 4, axis.Top + 2 + (_viewModel.PlotStackedAxes ? 0 : titleIndex * 15), textBrush, PlotAxisLabelFontSize, 600);
AddPlotLabel(canvas, $"{SensorTypeDisplay.GetText(axis.SensorType)}{unit}", 4, axis.Top + 2 + (VM.PlotStackedAxes ? 0 : titleIndex * 15), textBrush, PlotAxisLabelFontSize, 600);
}
}
@@ -421,7 +434,7 @@ internal sealed class PlotView : Grid
private bool ShouldDrawPlotAxisLabels(PlotBounds bounds)
{
return _viewModel.ShowPlotAxisLabels && bounds.Left >= PlotLeftMargin && bounds.Height > 24 && bounds.Width > 32;
return VM.ShowPlotAxisLabels && bounds.Left >= PlotLeftMargin && bounds.Height > 24 && bounds.Width > 32;
}
private void DrawPlotSeries(
@@ -437,7 +450,7 @@ internal sealed class PlotView : Grid
Polyline line = new()
{
Stroke = stroke,
StrokeThickness = _viewModel.PlotStrokeThickness,
StrokeThickness = VM.PlotStrokeThickness,
StrokeLineJoin = PenLineJoin.Round
};
@@ -565,7 +578,7 @@ internal sealed class PlotView : Grid
private global::Windows.UI.Color GetPlotBackgroundColor()
{
return _viewModel.ThemeMode switch
return VM.ThemeMode switch
{
AppThemeMode.Black => Colors.Black,
AppThemeMode.Dark => global::Windows.UI.Color.FromArgb(255, 24, 24, 24),
@@ -609,7 +622,7 @@ internal sealed class PlotView : Grid
private bool IsDarkPlotTheme()
{
return _viewModel.ThemeMode switch
return VM.ThemeMode switch
{
AppThemeMode.Black or AppThemeMode.Dark => true,
AppThemeMode.Light => false,
@@ -34,8 +34,6 @@ public sealed partial class MainWindow : Window
private readonly WindowChromeManager _chromeManager;
private readonly WindowPlacementService _placementService;
private readonly DispatcherQueueTimer _timer;
private readonly PlotView _plotView;
private readonly Grid _plotPane;
private readonly WinUiStartupTrace? _startupTrace;
private readonly TrayIconService _trayIconService;
private readonly DialogService _dialogService;
@@ -95,17 +93,7 @@ public sealed partial class MainWindow : Window
MeasureStartup("MainWindow.PopulateMenuSubmenus", PopulateMenuSubmenus);
MeasureStartup("MainWindow.PopulateSensorHeader", PopulateSensorHeader);
PlotView? builtPlot = null;
Grid plotPane = MeasureStartup("MainWindow.BuildPlotPane", () =>
{
Grid pane = BuildPlotPane(out PlotView pv);
builtPlot = pv;
return pane;
});
_plotPane = plotPane;
_plotView = builtPlot!;
ContentGrid.Children.Add(plotPane);
MeasureStartup("MainWindow.AttachPlotView", () => PlotControl.AttachViewModel(ViewModel));
MeasureStartup("MainWindow.RestoreWindowBounds", _placementService.Restore);
MeasureStartup("MainWindow.MaximizeWindow", _placementService.Maximize);
@@ -371,43 +359,6 @@ public sealed partial class MainWindow : Window
SensorHeaderHost.Children.Add(header);
}
private Grid BuildPlotPane(out PlotView plotView)
{
Grid pane = new()
{
Padding = new Thickness(8),
BorderBrush = (Brush)Application.Current.Resources["SystemControlForegroundBaseLowBrush"],
BorderThickness = new Thickness(1, 0, 0, 0),
RowDefinitions =
{
new RowDefinition { Height = GridLength.Auto },
new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }
}
};
Grid toolbar = new()
{
ColumnDefinitions =
{
new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) },
new ColumnDefinition { Width = GridLength.Auto }
}
};
toolbar.Children.Add(new TextBlock { Text = "Plot", FontWeight = new global::Windows.UI.Text.FontWeight { Weight = 600 }, VerticalAlignment = VerticalAlignment.Center });
Button reset = new() { Content = "Reset" };
reset.Click += (_, _) => ResetPlot();
Grid.SetColumn(reset, 1);
toolbar.Children.Add(reset);
pane.Children.Add(toolbar);
PlotView plotViewLocal = new(ViewModel);
Grid.SetRow(plotViewLocal, 1);
pane.Children.Add(plotViewLocal);
plotView = plotViewLocal;
return pane;
}
private void RootItems_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
QueueSensorTreeRebuild();
@@ -809,16 +760,16 @@ public sealed partial class MainWindow : Window
private void ResetPlot()
{
ViewModel.ResetPlot();
_plotView.ResetZoom();
PlotControl.ResetZoom();
_secondaryWindows.RedrawPlot();
}
private void DrawPlot()
{
if (ViewModel.PlotLocation == PlotLocation.Window)
_plotView.Clear();
PlotControl.Clear();
else
_plotView.Redraw();
PlotControl.Redraw();
_secondaryWindows.RedrawPlot();
}
@@ -828,11 +779,11 @@ public sealed partial class MainWindow : Window
ContentGrid.ColumnDefinitions[1].Width = ViewModel.ShowPlot && ViewModel.PlotLocation == PlotLocation.Right ? new GridLength(320) : new GridLength(0);
ContentGrid.RowDefinitions[1].Height = ViewModel.ShowPlot && ViewModel.PlotLocation == PlotLocation.Bottom ? new GridLength(220) : new GridLength(0);
_plotPane.Visibility = ViewModel.PlotVisibility;
Grid.SetRow(_plotPane, ViewModel.PlotGridRow);
Grid.SetColumn(_plotPane, ViewModel.PlotGridColumn);
Grid.SetRowSpan(_plotPane, ViewModel.PlotGridRowSpan);
Grid.SetColumnSpan(_plotPane, ViewModel.PlotGridColumnSpan);
PlotPane.Visibility = ViewModel.PlotVisibility;
Grid.SetRow(PlotPane, ViewModel.PlotGridRow);
Grid.SetColumn(PlotPane, ViewModel.PlotGridColumn);
Grid.SetRowSpan(PlotPane, ViewModel.PlotGridRowSpan);
Grid.SetColumnSpan(PlotPane, ViewModel.PlotGridColumnSpan);
Grid.SetRowSpan(SensorPane, ViewModel.SensorGridRowSpan);
Grid.SetColumnSpan(SensorPane, ViewModel.SensorGridColumnSpan);
UpdatePlotWindowVisibility();
@@ -855,7 +806,7 @@ public sealed partial class MainWindow : Window
_ => (Brush)Application.Current.Resources["ApplicationPageBackgroundThemeBrush"]
};
_plotView.ApplyTheme(ViewModel.ThemeMode);
PlotControl.ApplyTheme(ViewModel.ThemeMode);
_secondaryWindows.ApplyTheme(ViewModel.ThemeMode);
}
@@ -5,7 +5,8 @@
x:Class="LibreHardwareMonitor.Windows.WinUI.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:LibreHardwareMonitor.Windows.WinUI">
xmlns:local="using:LibreHardwareMonitor.Windows.WinUI"
xmlns:controls="using:LibreHardwareMonitor.Windows.WinUI.Controls">
<Grid x:Name="RootGrid">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
@@ -105,6 +106,26 @@
</TreeView>
</Grid>
</Grid>
<Grid x:Name="PlotPane"
Grid.Column="1"
Grid.Row="0"
Padding="8"
BorderThickness="1,0,0,0"
BorderBrush="{ThemeResource SystemControlForegroundBaseLowBrush}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Text="Plot" FontWeight="SemiBold" VerticalAlignment="Center" />
<Button Grid.Column="1" Content="Reset" Click="OnResetPlot" />
</Grid>
<controls:PlotView x:Name="PlotControl" Grid.Row="1" />
</Grid>
</Grid>
<TextBlock x:Name="StatusText"
Grid.Row="2"