KaranReportTools.psm1
|
function Show-GridChartUI { [CmdletBinding()] param( [Parameter(ValueFromPipeline = $true)] $InputObject, [string]$Title = "Grid + Charts" ) begin { $items = [System.Collections.Generic.List[object]]::new() } process { if ($null -ne $InputObject) { [void]$items.Add($InputObject) } } end { # Assemblies for WinForms usage (NOT compiling C#) Add-Type -AssemblyName System.Windows.Forms -ErrorAction Stop Add-Type -AssemblyName System.Drawing -ErrorAction Stop Add-Type -AssemblyName System.Data -ErrorAction Stop # Optional charting $chartsAvailable = $true try { Add-Type -AssemblyName System.Windows.Forms.DataVisualization -ErrorAction Stop } catch { $chartsAvailable = $false } # Convert pipeline objects to DataTable $dt = New-Object System.Data.DataTable "Input" if ($items.Count -gt 0) { $props = $items[0].PSObject.Properties | Where-Object { $_.MemberType -in 'NoteProperty','Property' } foreach ($p in $props) { [void]$dt.Columns.Add($p.Name, [object]) } foreach ($obj in $items) { $row = $dt.NewRow() foreach ($p in $props) { $row[$p.Name] = $obj.$($p.Name) } [void]$dt.Rows.Add($row) } } # DataView for filtering $view = New-Object System.Data.DataView($dt) $bs = New-Object System.Windows.Forms.BindingSource $bs.DataSource = $view # ---------------- UI ---------------- $form = New-Object System.Windows.Forms.Form $form.Text = $Title $form.StartPosition = 'CenterScreen' $form.Width = 1350 $form.Height = 820 $form.KeyPreview = $true $main = New-Object System.Windows.Forms.SplitContainer $main.Dock = 'Fill' $main.Orientation = 'Vertical' $main.SplitterDistance = 980 $form.Controls.Add($main) # LEFT layout $left = New-Object System.Windows.Forms.TableLayoutPanel $left.Dock = 'Fill' $left.RowCount = 4 $left.ColumnCount = 1 [void]$left.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, 42))) [void]$left.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Percent, 55))) [void]$left.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, 44))) [void]$left.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Percent, 45))) $main.Panel1.Controls.Add($left) # Search panel $searchPanel = New-Object System.Windows.Forms.FlowLayoutPanel $searchPanel.Dock = 'Fill' $searchPanel.Padding = New-Object System.Windows.Forms.Padding(8,8,8,0) $searchPanel.WrapContents = $false $lblSearch = New-Object System.Windows.Forms.Label $lblSearch.Text = "Search:" $lblSearch.AutoSize = $true $lblSearch.Padding = New-Object System.Windows.Forms.Padding(0,6,6,0) $searchPanel.Controls.Add($lblSearch) $txtSearch = New-Object System.Windows.Forms.TextBox $txtSearch.Width = 260 $searchPanel.Controls.Add($txtSearch) $lblCol = New-Object System.Windows.Forms.Label $lblCol.Text = "Column:" $lblCol.AutoSize = $true $lblCol.Padding = New-Object System.Windows.Forms.Padding(12,6,6,0) $searchPanel.Controls.Add($lblCol) $cmbSearchCol = New-Object System.Windows.Forms.ComboBox $cmbSearchCol.Width = 220 $cmbSearchCol.DropDownStyle = 'DropDownList' $searchPanel.Controls.Add($cmbSearchCol) $btnClear = New-Object System.Windows.Forms.Button $btnClear.Text = "Clear" $btnClear.Width = 80 $btnClear.Height = 26 $btnClear.Margin = New-Object System.Windows.Forms.Padding(12,4,0,0) $searchPanel.Controls.Add($btnClear) # --- Row count label (Showing / Total / Filtered out) --- $lblCount = New-Object System.Windows.Forms.Label $lblCount.AutoSize = $true $lblCount.Padding = New-Object System.Windows.Forms.Padding(16,6,0,0) $lblCount.ForeColor = [System.Drawing.Color]::DimGray $lblCount.Text = "" $searchPanel.Controls.Add($lblCount) $left.Controls.Add($searchPanel, 0, 0) # Grid $grid = New-Object System.Windows.Forms.DataGridView $grid.Dock = 'Fill' $grid.ReadOnly = $true $grid.AllowUserToAddRows = $false $grid.AllowUserToDeleteRows = $false $grid.AutoGenerateColumns = $true $grid.AutoSizeColumnsMode = 'DisplayedCells' $grid.SelectionMode = 'FullRowSelect' $grid.MultiSelect = $false $grid.DataSource = $bs $left.Controls.Add($grid, 0, 1) # Export panel $exportPanel = New-Object System.Windows.Forms.FlowLayoutPanel $exportPanel.Dock = 'Fill' $exportPanel.Padding = New-Object System.Windows.Forms.Padding(8,6,8,0) $exportPanel.WrapContents = $false $btnExportCsv = New-Object System.Windows.Forms.Button $btnExportCsv.Text = "Export CSV (filtered)" $btnExportCsv.Width = 170 $exportPanel.Controls.Add($btnExportCsv) $btnExportXlsx = New-Object System.Windows.Forms.Button $btnExportXlsx.Text = "Export XLSX (requires Excel)" $btnExportXlsx.Width = 220 $btnExportXlsx.Margin = New-Object System.Windows.Forms.Padding(10,0,0,0) $exportPanel.Controls.Add($btnExportXlsx) $hint = New-Object System.Windows.Forms.Label $hint.Text = "CSV always works. XLSX needs Excel installed." $hint.AutoSize = $true $hint.Padding = New-Object System.Windows.Forms.Padding(14,6,0,0) $hint.ForeColor = [System.Drawing.Color]::DimGray $exportPanel.Controls.Add($hint) $left.Controls.Add($exportPanel, 0, 2) # Chart area $chartSplit = New-Object System.Windows.Forms.SplitContainer $chartSplit.Dock = 'Fill' $chartSplit.Orientation = 'Horizontal' $chartSplit.SplitterDistance = 270 $left.Controls.Add($chartSplit, 0, 3) $chart = $null $cmbChartType = $null $cmbAgg = $null $cmbX = $null $cmbY = $null $btnPlot = $null $btnClearChart = $null if ($chartsAvailable) { $chart = New-Object System.Windows.Forms.DataVisualization.Charting.Chart $chart.Dock = 'Fill' $chart.ChartAreas.Clear() [void]$chart.ChartAreas.Add("Main") $chartSplit.Panel1.Controls.Add($chart) # Minor default styling $chart.BackColor = [System.Drawing.Color]::White $chart.ChartAreas["Main"].BackColor = [System.Drawing.Color]::White $chart.ChartAreas["Main"].AxisX.MajorGrid.Enabled = $false $chart.ChartAreas["Main"].AxisY.MajorGrid.LineColor = [System.Drawing.Color]::Gainsboro $chart.ChartAreas["Main"].AxisX.Interval = 1 $chartControls = New-Object System.Windows.Forms.FlowLayoutPanel $chartControls.Dock = 'Fill' $chartControls.Padding = New-Object System.Windows.Forms.Padding(8) $chartControls.WrapContents = $true $chartSplit.Panel2.Controls.Add($chartControls) $cmbChartType = New-Object System.Windows.Forms.ComboBox $cmbChartType.DropDownStyle = 'DropDownList' $cmbChartType.Width = 130 [void]$cmbChartType.Items.AddRange(@("Column","Bar","Line","Pie","Area")) $cmbChartType.SelectedIndex = 0 $cmbAgg = New-Object System.Windows.Forms.ComboBox $cmbAgg.DropDownStyle = 'DropDownList' $cmbAgg.Width = 110 [void]$cmbAgg.Items.AddRange(@("Sum","Avg","Count","Raw")) $cmbAgg.SelectedIndex = 0 $cmbX = New-Object System.Windows.Forms.ComboBox $cmbX.DropDownStyle = 'DropDownList' $cmbX.Width = 200 $cmbY = New-Object System.Windows.Forms.ComboBox $cmbY.DropDownStyle = 'DropDownList' $cmbY.Width = 200 $btnPlot = New-Object System.Windows.Forms.Button $btnPlot.Text = "Plot" $btnPlot.Width = 90 $btnPlot.Margin = New-Object System.Windows.Forms.Padding(16,2,0,0) $btnClearChart = New-Object System.Windows.Forms.Button $btnClearChart.Text = "Clear chart" $btnClearChart.Width = 110 $btnClearChart.Margin = New-Object System.Windows.Forms.Padding(8,2,0,0) $chartControls.Controls.Add((New-Object System.Windows.Forms.Label -Property @{Text="Chart:"; AutoSize=$true; Padding=(New-Object System.Windows.Forms.Padding(0,6,6,0))})) $chartControls.Controls.Add($cmbChartType) $chartControls.Controls.Add((New-Object System.Windows.Forms.Label -Property @{Text="X:"; AutoSize=$true; Padding=(New-Object System.Windows.Forms.Padding(12,6,6,0))})) $chartControls.Controls.Add($cmbX) $chartControls.Controls.Add((New-Object System.Windows.Forms.Label -Property @{Text="Y:"; AutoSize=$true; Padding=(New-Object System.Windows.Forms.Padding(12,6,6,0))})) $chartControls.Controls.Add($cmbY) $chartControls.Controls.Add((New-Object System.Windows.Forms.Label -Property @{Text="Agg:"; AutoSize=$true; Padding=(New-Object System.Windows.Forms.Padding(12,6,6,0))})) $chartControls.Controls.Add($cmbAgg) $chartControls.Controls.Add($btnPlot) $chartControls.Controls.Add($btnClearChart) } else { $chartMsg = New-Object System.Windows.Forms.Label $chartMsg.Dock = 'Fill' $chartMsg.TextAlign = 'MiddleCenter' $chartMsg.ForeColor = [System.Drawing.Color]::DimGray $chartMsg.Text = "Charts not available (System.Windows.Forms.DataVisualization not found).`nYou can still export XLSX and chart in Excel." $chartSplit.Panel1.Controls.Add($chartMsg) } # RIGHT layout (columns + rename) $right = New-Object System.Windows.Forms.TableLayoutPanel $right.Dock = 'Fill' $right.RowCount = 4 $right.ColumnCount = 1 [void]$right.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, 28))) [void]$right.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Percent, 70))) [void]$right.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, 28))) [void]$right.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Absolute, 60))) $main.Panel2.Controls.Add($right) $right.Controls.Add((New-Object System.Windows.Forms.Label -Property @{ Text="Columns (toggle visibility):"; Dock='Fill'; Padding=(New-Object System.Windows.Forms.Padding(8,6,0,0)) }), 0, 0) $clbCols = New-Object System.Windows.Forms.CheckedListBox $clbCols.Dock = 'Fill' $clbCols.CheckOnClick = $true $right.Controls.Add($clbCols, 0, 1) $right.Controls.Add((New-Object System.Windows.Forms.Label -Property @{ Text="Rename selected column header:"; Dock='Fill'; Padding=(New-Object System.Windows.Forms.Padding(8,6,0,0)) }), 0, 2) $renamePanel = New-Object System.Windows.Forms.FlowLayoutPanel $renamePanel.Dock = 'Fill' $renamePanel.Padding = New-Object System.Windows.Forms.Padding(8) $renamePanel.WrapContents = $false $txtHeader = New-Object System.Windows.Forms.TextBox $txtHeader.Width = 210 $renamePanel.Controls.Add($txtHeader) $btnRename = New-Object System.Windows.Forms.Button $btnRename.Text = "Rename" $btnRename.Width = 90 $renamePanel.Controls.Add($btnRename) $right.Controls.Add($renamePanel, 0, 3) # ---------- Populate column UI ---------- $cmbSearchCol.Items.Clear() [void]$cmbSearchCol.Items.Add("(All columns)") $clbCols.Items.Clear() if ($chartsAvailable) { $cmbX.Items.Clear() $cmbY.Items.Clear() } foreach ($col in $dt.Columns) { [void]$cmbSearchCol.Items.Add($col.ColumnName) [void]$clbCols.Items.Add($col.ColumnName, $true) if ($chartsAvailable) { [void]$cmbX.Items.Add($col.ColumnName) [void]$cmbY.Items.Add($col.ColumnName) } } $cmbSearchCol.SelectedIndex = 0 if ($chartsAvailable -and $cmbX.Items.Count -gt 0) { $cmbX.SelectedIndex = 0 } if ($chartsAvailable -and $cmbY.Items.Count -gt 1) { $cmbY.SelectedIndex = 1 } elseif ($chartsAvailable -and $cmbY.Items.Count -gt 0) { $cmbY.SelectedIndex = 0 } # ---------- Event helpers ---------- # --- Updates the "Showing / Total" indicator and the window title --- $updateCounts = { $total = $dt.Rows.Count $shown = $bs.Count $filteredOut = $total - $shown if ($filteredOut -gt 0) { $lblCount.Text = "Showing $shown / $total (filtered out $filteredOut)" } else { $lblCount.Text = "Showing $shown / $total" } $form.Text = "$Title - $shown/$total rows" } $applyFilter = { $q = ($txtSearch.Text ?? "").Trim() if ([string]::IsNullOrEmpty($q)) { $view.RowFilter = "" & $updateCounts return } $q = $q.Replace("'", "''") # escape $selected = $null if ($cmbSearchCol.SelectedIndex -gt 0 -and $cmbSearchCol.SelectedItem) { $selected = $cmbSearchCol.SelectedItem.ToString() } if ($selected) { $view.RowFilter = "Convert([$selected], 'System.String') LIKE '%$q%'" & $updateCounts return } $parts = New-Object System.Collections.Generic.List[string] foreach ($c in $dt.Columns) { [void]$parts.Add("Convert([$($c.ColumnName)], 'System.String') LIKE '%$q%'") } $view.RowFilter = [string]::Join(" OR ", $parts.ToArray()) & $updateCounts } # Wire search/filter events $txtSearch.Add_TextChanged($applyFilter) $cmbSearchCol.Add_SelectedIndexChanged($applyFilter) $btnClear.Add_Click({ $txtSearch.Text = "" $cmbSearchCol.SelectedIndex = 0 & $updateCounts }) # Keep counts updated if the underlying binding list changes $bs.Add_ListChanged({ & $updateCounts }) # Initial counts & $updateCounts # ---- Column visibility toggle ---- $clbCols.Add_ItemCheck({ param($s, $e) $idx = $e.Index if ($idx -isnot [int] -or $idx -lt 0 -or $idx -ge $clbCols.Items.Count) { return } $item = $clbCols.Items[$idx] if ($null -eq $item) { return } $name = $item.ToString() if ([string]::IsNullOrWhiteSpace($name)) { return } if ($grid.Columns.Contains($name)) { $grid.Columns[$name].Visible = ($e.NewValue -eq [System.Windows.Forms.CheckState]::Checked) } }) $clbCols.Add_SelectedIndexChanged({ if ($clbCols.SelectedItem) { $name = $clbCols.SelectedItem.ToString() if ($grid.Columns.Contains($name)) { $txtHeader.Text = $grid.Columns[$name].HeaderText } } }) $btnRename.Add_Click({ if (-not $clbCols.SelectedItem) { return } $name = $clbCols.SelectedItem.ToString() $new = ($txtHeader.Text ?? "").Trim() if ($new.Length -eq 0) { return } if ($grid.Columns.Contains($name)) { $grid.Columns[$name].HeaderText = $new } }) $btnExportCsv.Add_Click({ $dlg = New-Object System.Windows.Forms.SaveFileDialog $dlg.Filter = "CSV (*.csv)|*.csv|All Files (*.*)|*.*" $dlg.Title = "Export filtered rows to CSV" if ($dlg.ShowDialog() -ne [System.Windows.Forms.DialogResult]::OK) { return } $visibleCols = @() foreach ($c in $grid.Columns) { if ($c.Visible) { $visibleCols += $c } } $out = foreach ($drv in $view) { $o = [ordered]@{} foreach ($c in $visibleCols) { $key = if ([string]::IsNullOrWhiteSpace($c.DataPropertyName)) { $c.Name } else { $c.DataPropertyName } $o[$c.HeaderText] = $drv.Row[$key] } [pscustomobject]$o } $out | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $dlg.FileName [System.Windows.Forms.MessageBox]::Show("Exported CSV:`n$($dlg.FileName)","Export", [System.Windows.Forms.MessageBoxButtons]::OK,[System.Windows.Forms.MessageBoxIcon]::Information) | Out-Null }) $btnExportXlsx.Add_Click({ $excelType = [Type]::GetTypeFromProgID("Excel.Application") if (-not $excelType) { [System.Windows.Forms.MessageBox]::Show("Excel is not installed. Use Export CSV instead.","XLSX Export", [System.Windows.Forms.MessageBoxButtons]::OK,[System.Windows.Forms.MessageBoxIcon]::Warning) | Out-Null return } $dlg = New-Object System.Windows.Forms.SaveFileDialog $dlg.Filter = "Excel Workbook (*.xlsx)|*.xlsx" $dlg.Title = "Export filtered rows to XLSX (requires Excel)" if ($dlg.ShowDialog() -ne [System.Windows.Forms.DialogResult]::OK) { return } $visibleCols = @() foreach ($c in $grid.Columns) { if ($c.Visible) { $visibleCols += $c } } $xl = $null; $wb = $null; $ws = $null try { $xl = New-Object -ComObject Excel.Application $xl.Visible = $false $xl.DisplayAlerts = $false $wb = $xl.Workbooks.Add() $ws = $wb.Worksheets.Item(1) for ($i=0; $i -lt $visibleCols.Count; $i++) { $ws.Cells.Item(1, $i+1).Value2 = $visibleCols[$i].HeaderText } $r = 2 foreach ($drv in $view) { for ($i=0; $i -lt $visibleCols.Count; $i++) { $c = $visibleCols[$i] $key = if ([string]::IsNullOrWhiteSpace($c.DataPropertyName)) { $c.Name } else { $c.DataPropertyName } $val = $drv.Row[$key] $ws.Cells.Item($r, $i+1).Value2 = if ($null -eq $val) { "" } else { "$val" } } $r++ } $wb.SaveAs($dlg.FileName, 51) # xlOpenXMLWorkbook $wb.Close() $xl.Quit() [System.Windows.Forms.MessageBox]::Show("Exported XLSX:`n$($dlg.FileName)","Export", [System.Windows.Forms.MessageBoxButtons]::OK,[System.Windows.Forms.MessageBoxIcon]::Information) | Out-Null } finally { foreach ($o in @($ws,$wb,$xl)) { try { if ($o) { [System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($o) | Out-Null } } catch {} } } }) if ($chartsAvailable) { $btnClearChart.Add_Click({ $chart.Series.Clear() $chart.Titles.Clear() $chart.Legends.Clear() }) $btnPlot.Add_Click({ # Basic validation if (-not $cmbX.SelectedItem -or -not $cmbY.SelectedItem) { return } if ($view.Count -eq 0) { [System.Windows.Forms.MessageBox]::Show( "No rows available to plot (check your filter).", "Plot", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Information ) | Out-Null return } $xCol = $cmbX.SelectedItem.ToString() $yCol = $cmbY.SelectedItem.ToString() $type = $cmbChartType.SelectedItem.ToString() $agg = $cmbAgg.SelectedItem.ToString() $points = @() # ---- Build points ---- if ($agg -eq 'Raw' -and $type -ne 'Pie') { # Raw: plot every numeric row foreach ($drv in $view) { $x = "$($drv.Row[$xCol])" $ys = "$($drv.Row[$yCol])" $y = 0.0 if ([double]::TryParse($ys, [ref]$y)) { $points += [pscustomobject]@{ X = $x; Y = [double]$y } } } } else { # Bucket by X and aggregate $buckets = @{} foreach ($drv in $view) { $x = "$($drv.Row[$xCol])" $ys = "$($drv.Row[$yCol])" if (-not $buckets.ContainsKey($x)) { $buckets[$x] = New-Object System.Collections.Generic.List[double] } if ($agg -eq 'Count') { $buckets[$x].Add(1.0) continue } $y = 0.0 if ([double]::TryParse($ys, [ref]$y)) { $buckets[$x].Add($y) } } foreach ($k in $buckets.Keys) { $list = $buckets[$k] if ($list.Count -eq 0) { continue } switch ($agg) { 'Avg' { $val = ($list | Measure-Object -Average).Average } 'Count' { $val = $list.Count } default { $val = ($list | Measure-Object -Sum).Sum } # Sum } $points += [pscustomobject]@{ X = $k; Y = [double]$val } } } # ---- No data guard ---- if (-not $points -or $points.Count -eq 0) { [System.Windows.Forms.MessageBox]::Show( "No numeric data available to plot for '$yCol' using aggregation '$agg'.", "Plot", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Information ) | Out-Null return } # ---- Sorting / Pie grouping ---- if ($type -eq 'Pie') { $sorted = $points | Sort-Object Y -Descending # Avoid unreadable pies: group small slices into "Other" if ($sorted.Count -gt 12) { $top = $sorted | Select-Object -First 11 $rest = $sorted | Select-Object -Skip 11 $otherSum = ($rest | Measure-Object -Property Y -Sum).Sum $points = @($top) + @([pscustomobject]@{ X = "Other"; Y = [double]$otherSum }) } else { $points = $sorted } } else { $points = $points | Sort-Object X } # ---- Reset chart ---- $chart.Series.Clear() $chart.Titles.Clear() $chart.Legends.Clear() # Build title before Add() $title = "{0}: {1} by {2} ({3})" -f $type, $yCol, $xCol, $agg [void]$chart.Titles.Add($title) # Add a legend (especially helpful for Pie) [void]$chart.Legends.Add((New-Object System.Windows.Forms.DataVisualization.Charting.Legend("Legend1"))) $chart.Legends["Legend1"].Docking = [System.Windows.Forms.DataVisualization.Charting.Docking]::Right $series = New-Object System.Windows.Forms.DataVisualization.Charting.Series("Series1") $series.IsValueShownAsLabel = $true $series.Legend = "Legend1" switch ($type) { 'Bar' { $series.ChartType = [System.Windows.Forms.DataVisualization.Charting.SeriesChartType]::Bar } 'Line' { $series.ChartType = [System.Windows.Forms.DataVisualization.Charting.SeriesChartType]::Line } 'Pie' { $series.ChartType = [System.Windows.Forms.DataVisualization.Charting.SeriesChartType]::Pie } 'Area' { $series.ChartType = [System.Windows.Forms.DataVisualization.Charting.SeriesChartType]::Area } default { $series.ChartType = [System.Windows.Forms.DataVisualization.Charting.SeriesChartType]::Column } } if ($type -eq 'Pie') { # Better Pie readability $series["PieLabelStyle"] = "Outside" $series["PieLineColor"] = "Gray" $series.Label = "#VALX: #PERCENT{P0}" $series.LegendText = "#VALX (#VALY)" } else { $series.Label = "#VALY" $series.LegendText = $yCol } foreach ($p in $points) { $dpIndex = $series.Points.AddXY($p.X, $p.Y) try { $series.Points[$dpIndex].ToolTip = "{0}: {1}" -f $p.X, $p.Y } catch {} } [void]$chart.Series.Add($series) }) } # Convenience hotkeys $form.Add_KeyDown({ param($s, $e) if ($e.Control -and $e.KeyCode -eq 'F') { $txtSearch.Focus(); $e.Handled = $true } if ($e.KeyCode -eq 'Escape') { $form.Close(); $e.Handled = $true } }) # Show dialog [void]$form.ShowDialog() } } Export-ModuleMember -Function Show-GridChartUI |