diff --git a/.gitignore b/.gitignore
index 461d4c3..4183425 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,8 +20,11 @@ publish/
# Stato locale degli strumenti agentici
.claude/
-# File Excel/XML della Distinta Base (esportati dal gestionale) e il CSV generato
+# File Excel/XML della Distinta Base (esportati dal gestionale), il CSV generato
+# e la cartella con le copie dei file CAD trovati
/*.xls
/*.xlsx
/*.xls.xml
/*.csv
+/raw/
+/step/
diff --git a/CadSearchGui/Form1.cs b/CadSearchGui/Form1.cs
index 99cb14c..d34e7a6 100644
--- a/CadSearchGui/Form1.cs
+++ b/CadSearchGui/Form1.cs
@@ -7,15 +7,19 @@ public partial class Form1 : Form
{
private readonly Button _btnApriDb = new();
private readonly Button _btnAggiorna = new();
+ private readonly Button _btnCreaRaw = new();
private readonly Label _lblStatus = new();
private readonly Label _lblDb = new();
private readonly DataGridView _gridImport = new();
private readonly Label _lblImportStatus = new();
+ private readonly ToolTip _tipImportStatus = new();
private static readonly string[] TipiPredefiniti = { "prt", "asm" };
private const string RadiceDiscoDefault = @"X:\";
private string? _dbPath;
+ private string? _percorsoInputCorrente;
+ private List<(string Componente, string Descrizione)>? _righeCorrenti;
public Form1()
{
@@ -24,84 +28,72 @@ public partial class Form1 : Form
Load += (_, _) => InizializzaDatabase();
}
+ // Palette: blu primario per l'azione principale del flusso ("Crea cartella raw"),
+ // il resto resta ai colori di sistema per restare coerente col tema Windows dell'utente.
+ private static readonly Color ColorePrimario = Color.FromArgb(0, 99, 177);
+
private void BuildUi()
{
AllowDrop = true;
Text = "Distinta Base -> CSV con percorsi CAD";
+ MinimumSize = new Size(760, 480);
+ Font = new Font("Segoe UI", 9.5f);
- var topPanel = new TableLayoutPanel
+ var contenutoTop = new TableLayoutPanel
{
Dock = DockStyle.Top,
- Height = 108,
- ColumnCount = 3,
- RowCount = 3,
- Padding = new Padding(8),
+ AutoSize = true,
+ AutoSizeMode = AutoSizeMode.GrowAndShrink,
+ ColumnCount = 1,
+ Padding = new Padding(12, 12, 12, 0),
};
- topPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
- topPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 130));
- topPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 130));
-
- var lblIstruzioni = new Label
- {
- Dock = DockStyle.Fill,
- AutoSize = false,
- Text = "Trascina qui il file .xls/.xml esportato dal gestionale (foglio 'Distinta "
- + "Base'), oppure scegli il file col pulsante 'Scegli file...'. Viene generato "
- + "un CSV con Componente/Descrizione e, per ogni componente, il percorso del "
- + "file CAD corrispondente trovato nell'indice.",
- };
- topPanel.SetColumnSpan(lblIstruzioni, 3);
- topPanel.Controls.Add(lblIstruzioni, 0, 0);
-
- var btnSfoglia = new Button { Text = "Scegli file distinta...", Dock = DockStyle.Fill };
- btnSfoglia.Click += (_, _) => ScegliFileDistinta();
- topPanel.Controls.Add(btnSfoglia, 0, 1);
-
- _btnApriDb.Text = "Apri DB...";
- _btnApriDb.Dock = DockStyle.Fill;
- _btnApriDb.Click += (_, _) => ScegliDatabase();
- topPanel.Controls.Add(_btnApriDb, 1, 1);
-
- _btnAggiorna.Text = "Aggiorna indice";
- _btnAggiorna.Dock = DockStyle.Fill;
- _btnAggiorna.Click += (_, _) => AggiornaIndice();
- topPanel.Controls.Add(_btnAggiorna, 2, 1);
-
- _lblDb.Dock = DockStyle.Fill;
- _lblDb.AutoSize = false;
- _lblDb.ForeColor = SystemColors.GrayText;
- _lblDb.TextAlign = ContentAlignment.MiddleLeft;
- topPanel.SetColumnSpan(_lblDb, 3);
- topPanel.Controls.Add(_lblDb, 0, 2);
+ contenutoTop.Controls.Add(BuildGruppoDatabase());
+ contenutoTop.Controls.Add(BuildGruppoImporta());
_lblImportStatus.Dock = DockStyle.Top;
- _lblImportStatus.Height = 24;
- _lblImportStatus.Padding = new Padding(8, 4, 8, 4);
+ _lblImportStatus.AutoSize = false;
+ _lblImportStatus.AutoEllipsis = true;
+ _lblImportStatus.Height = 28;
+ _lblImportStatus.Padding = new Padding(12, 6, 12, 6);
_lblImportStatus.ForeColor = SystemColors.GrayText;
_gridImport.Dock = DockStyle.Fill;
_gridImport.ReadOnly = true;
+ _gridImport.BorderStyle = BorderStyle.None;
+ _gridImport.BackgroundColor = SystemColors.Window;
+ _gridImport.RowHeadersVisible = false;
_gridImport.AllowUserToAddRows = false;
_gridImport.AllowUserToDeleteRows = false;
+ _gridImport.AllowUserToResizeRows = false;
_gridImport.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
_gridImport.MultiSelect = false;
_gridImport.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill;
+ _gridImport.ColumnHeadersHeight = 32;
+ _gridImport.ColumnHeadersDefaultCellStyle.Font = new Font(Font, FontStyle.Bold);
+ _gridImport.ColumnHeadersDefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleLeft;
+ _gridImport.RowTemplate.Height = 26;
+ _gridImport.AlternatingRowsDefaultCellStyle.BackColor = Color.FromArgb(245, 247, 250);
+ _gridImport.DefaultCellStyle.SelectionBackColor = ColorePrimario;
+ _gridImport.DefaultCellStyle.Padding = new Padding(4, 0, 4, 0);
_gridImport.Columns.Add("componente", "Componente");
_gridImport.Columns.Add("descrizione", "Descrizione");
_gridImport.Columns.Add("percorso", "Percorso CAD");
+ _gridImport.Columns["componente"]!.FillWeight = 20;
+ _gridImport.Columns["descrizione"]!.FillWeight = 40;
+ _gridImport.Columns["percorso"]!.FillWeight = 40;
_gridImport.CellDoubleClick += (_, _) => ApriCartellaFileSelezionato();
- var statusPanel = new Panel { Dock = DockStyle.Bottom, Height = 28 };
+ var statusPanel = new Panel { Dock = DockStyle.Bottom, Height = 28, BackColor = SystemColors.ControlLight };
_lblStatus.Dock = DockStyle.Fill;
_lblStatus.TextAlign = ContentAlignment.MiddleLeft;
- _lblStatus.Padding = new Padding(8, 0, 0, 0);
+ _lblStatus.Padding = new Padding(12, 0, 0, 0);
_lblStatus.Text = "Pronto.";
statusPanel.Controls.Add(_lblStatus);
Controls.Add(_gridImport);
Controls.Add(_lblImportStatus);
Controls.Add(statusPanel);
- Controls.Add(topPanel);
+ Controls.Add(contenutoTop);
DragEnter += (_, e) =>
e.Effect = e.Data?.GetDataPresent(DataFormats.FileDrop) == true
@@ -114,6 +106,112 @@ public partial class Form1 : Form
};
}
+ private GroupBox BuildGruppoDatabase()
+ {
+ var contenuto = new TableLayoutPanel
+ {
+ Dock = DockStyle.Top,
+ AutoSize = true,
+ AutoSizeMode = AutoSizeMode.GrowAndShrink,
+ ColumnCount = 1,
+ };
+
+ _lblDb.AutoSize = true;
+ _lblDb.AutoEllipsis = true;
+ _lblDb.MaximumSize = new Size(900, 0);
+ _lblDb.ForeColor = SystemColors.GrayText;
+ _lblDb.Margin = new Padding(0, 0, 0, 8);
+ contenuto.Controls.Add(_lblDb);
+
+ var bottoni = new FlowLayoutPanel { AutoSize = true, FlowDirection = FlowDirection.LeftToRight };
+
+ _btnApriDb.Text = "Apri DB...";
+ _btnApriDb.Size = new Size(120, 32);
+ _btnApriDb.Margin = new Padding(0, 0, 8, 0);
+ _btnApriDb.Click += (_, _) => ScegliDatabase();
+ bottoni.Controls.Add(_btnApriDb);
+
+ _btnAggiorna.Text = "Aggiorna indice";
+ _btnAggiorna.Size = new Size(120, 32);
+ _btnAggiorna.Click += (_, _) => AggiornaIndice();
+ bottoni.Controls.Add(_btnAggiorna);
+
+ contenuto.Controls.Add(bottoni);
+
+ return new GroupBox
+ {
+ Text = "1. Indice CAD (caricato automaticamente all'avvio)",
+ Dock = DockStyle.Top,
+ AutoSize = true,
+ AutoSizeMode = AutoSizeMode.GrowAndShrink,
+ Padding = new Padding(12, 8, 12, 12),
+ Margin = new Padding(0, 0, 0, 10),
+ Controls = { contenuto },
+ };
+ }
+
+ private GroupBox BuildGruppoImporta()
+ {
+ var contenuto = new TableLayoutPanel
+ {
+ Dock = DockStyle.Top,
+ AutoSize = true,
+ AutoSizeMode = AutoSizeMode.GrowAndShrink,
+ ColumnCount = 1,
+ };
+
+ var lblIstruzioni1 = new Label
+ {
+ AutoSize = true,
+ ForeColor = SystemColors.GrayText,
+ Margin = new Padding(0, 0, 0, 0),
+ Text = "2. Trascina qui il file .xls/.xml esportato dal gestionale (foglio 'Distinta "
+ + "Base'), oppure scegli il file col pulsante sotto: genera un CSV con Componente "
+ + "e Descrizione, in anteprima nella griglia.",
+ };
+ contenuto.Controls.Add(lblIstruzioni1);
+
+ var btnSfoglia = new Button
+ {
+ Text = "Scegli file distinta...",
+ Size = new Size(220, 32),
+ Margin = new Padding(0, 8, 0, 16),
+ };
+ btnSfoglia.Click += (_, _) => ScegliFileDistinta();
+ contenuto.Controls.Add(btnSfoglia);
+
+ var lblIstruzioni2 = new Label
+ {
+ AutoSize = true,
+ ForeColor = SystemColors.GrayText,
+ Text = "3. Premi qui sotto per cercare il percorso CAD di ogni componente "
+ + "nell'indice e copiare i file trovati in una cartella 'raw'.",
+ };
+ contenuto.Controls.Add(lblIstruzioni2);
+
+ _btnCreaRaw.Text = "Crea cartella raw";
+ _btnCreaRaw.Size = new Size(220, 36);
+ _btnCreaRaw.Margin = new Padding(0, 8, 0, 0);
+ _btnCreaRaw.Enabled = false;
+ _btnCreaRaw.FlatStyle = FlatStyle.Flat;
+ _btnCreaRaw.FlatAppearance.BorderSize = 0;
+ _btnCreaRaw.BackColor = ColorePrimario;
+ _btnCreaRaw.ForeColor = Color.White;
+ _btnCreaRaw.Font = new Font(Font, FontStyle.Bold);
+ _btnCreaRaw.Click += (_, _) => CreaCartellaRaw();
+ contenuto.Controls.Add(_btnCreaRaw);
+
+ return new GroupBox
+ {
+ Text = "2-3. Importa Distinta Base e crea la cartella raw",
+ Dock = DockStyle.Top,
+ AutoSize = true,
+ AutoSizeMode = AutoSizeMode.GrowAndShrink,
+ Padding = new Padding(12, 8, 12, 12),
+ Controls = { contenuto },
+ };
+ }
+
private void ScegliFileDistinta()
{
using var dialog = new OpenFileDialog
@@ -130,31 +228,82 @@ public partial class Form1 : Form
try
{
var (csv, righe) = DistintaBaseImporter.ConvertiInCsv(percorso);
- var percorsiTrovati = TrovaPercorsiCad(righe.Select(r => r.Componente));
+ _percorsoInputCorrente = percorso;
+ _righeCorrenti = righe;
_gridImport.Rows.Clear();
- int trovati = 0;
foreach (var (componente, descrizione) in righe)
{
- string percorsoCad = percorsiTrovati.TryGetValue(componente, out var lista)
- ? string.Join("; ", lista)
- : "";
- if (percorsoCad.Length > 0)
- trovati++;
- _gridImport.Rows.Add(componente, descrizione, percorsoCad);
+ _gridImport.Rows.Add(componente, descrizione, "");
}
+ _btnCreaRaw.Enabled = righe.Count > 0;
- _lblImportStatus.ForeColor = Color.DarkGreen;
- _lblImportStatus.Text = $"CSV generato ({righe.Count} righe, {trovati} con percorso CAD trovato): {csv}";
+ ImpostaStatoImport($"CSV generato ({righe.Count} righe): {csv}", Color.DarkGreen);
}
catch (Exception ex)
{
+ _percorsoInputCorrente = null;
+ _righeCorrenti = null;
+ _btnCreaRaw.Enabled = false;
_gridImport.Rows.Clear();
- _lblImportStatus.ForeColor = Color.DarkRed;
- _lblImportStatus.Text = $"Errore durante la conversione: {ex.Message}";
+ ImpostaStatoImport($"Errore durante la conversione: {ex.Message}", Color.DarkRed);
}
}
+ /// Imposta testo/colore dello stato import e il tooltip con il testo completo
+ /// (la label tronca con "..." quando il percorso non ci sta in una riga).
+ private void ImpostaStatoImport(string testo, Color colore)
+ {
+ _lblImportStatus.ForeColor = colore;
+ _lblImportStatus.Text = testo;
+ _tipImportStatus.SetToolTip(_lblImportStatus, testo);
+ }
+
+ private void CreaCartellaRaw()
+ {
+ if (_percorsoInputCorrente is null || _righeCorrenti is null)
+ return;
+
+ var percorsiTrovati = TrovaPercorsiCad(_righeCorrenti.Select(r => r.Componente));
+
+ for (int i = 0; i < _righeCorrenti.Count; i++)
+ {
+ string percorsoCad = percorsiTrovati.TryGetValue(_righeCorrenti[i].Componente, out var lista)
+ ? string.Join("; ", lista)
+ : "";
+ _gridImport.Rows[i].Cells[2].Value = percorsoCad;
+ }
+
+ var cartellaRaw = Path.Combine(Path.GetDirectoryName(Path.GetFullPath(_percorsoInputCorrente))!, "raw");
+ int copiati = CopiaFileTrovati(percorsiTrovati.Values.SelectMany(p => p).Distinct(), cartellaRaw);
+
+ ImpostaStatoImport(
+ $"{percorsiTrovati.Count} componenti con percorso CAD trovato su {_righeCorrenti.Count}. "
+ + $"{copiati} file copiati in {cartellaRaw}",
+ Color.DarkGreen);
+ }
+
+ /// Copia i file trovati nella cartella "raw" indicata, creandola se necessario.
+ /// Ritorna il numero di file copiati con successo.
+ private static int CopiaFileTrovati(IEnumerable percorsi, string cartellaRaw)
+ {
+ int copiati = 0;
+ foreach (var percorso in percorsi)
+ {
+ try
+ {
+ Directory.CreateDirectory(cartellaRaw);
+ File.Copy(percorso, Path.Combine(cartellaRaw, Path.GetFileName(percorso)), overwrite: true);
+ copiati++;
+ }
+ catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
+ {
+ // ponytail: file non raggiungibile (share offline, permessi) -> lo si salta, non blocca gli altri
+ }
+ }
+ return copiati;
+ }
+
/// Cerca, per ogni codice componente, i percorsi dei file CAD (ultima versione,
/// solo prt/asm) corrispondenti nell'indice.
private Dictionary> TrovaPercorsiCad(IEnumerable codici)
@@ -195,19 +344,23 @@ public partial class Form1 : Form
return risultato;
}
+ private static readonly string PercorsoImpostazioni = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "CadSearchGui", "ultimo_db.txt");
+
private void InizializzaDatabase()
{
var candidati = new[]
{
+ LeggiUltimoDbUsato(),
Path.Combine(AppContext.BaseDirectory, "cad_index.db"),
Path.Combine(Directory.GetCurrentDirectory(), "cad_index.db"),
};
- _dbPath = candidati.FirstOrDefault(File.Exists);
+ _dbPath = candidati.FirstOrDefault(p => p is not null && File.Exists(p));
if (_dbPath is null)
{
- _lblStatus.Text = "Database non trovato. Usa 'Apri DB...' per selezionarlo.";
- _lblDb.Text = "Nessun database caricato.";
+ _lblStatus.Text = "Database non trovato. Usa 'Apri DB...' per selezionarlo oppure 'Aggiorna indice' per crearlo.";
+ _lblDb.Text = "Nessun database caricato. Premi 'Aggiorna indice' per crearlo da X:\\, oppure 'Apri DB...' se ne hai gi\u00e0 uno.";
return;
}
@@ -239,6 +392,7 @@ public partial class Form1 : Form
_dbPath = path;
_lblDb.Text = $"Database: {path} ({totale:N0} file indicizzati)";
_lblStatus.Text = "Pronto.";
+ SalvaUltimoDbUsato(path);
}
catch (Exception ex)
{
@@ -247,6 +401,33 @@ public partial class Form1 : Form
}
}
+ /// Ricorda l'ultimo DB caricato con successo, così al prossimo avvio si
+ /// ricarica automaticamente anche se l'eseguibile gira da una cartella diversa.
+ private static string? LeggiUltimoDbUsato()
+ {
+ try
+ {
+ return File.Exists(PercorsoImpostazioni) ? File.ReadAllText(PercorsoImpostazioni).Trim() : null;
+ }
+ catch (IOException)
+ {
+ return null;
+ }
+ }
+
+ private static void SalvaUltimoDbUsato(string path)
+ {
+ try
+ {
+ Directory.CreateDirectory(Path.GetDirectoryName(PercorsoImpostazioni)!);
+ File.WriteAllText(PercorsoImpostazioni, path);
+ }
+ catch (IOException)
+ {
+ // ponytail: non critico se non si riesce a salvare, si torna a chiedere "Apri DB..." la prossima volta
+ }
+ }
+
private async void AggiornaIndice()
{
string dbPath = _dbPath ?? Path.Combine(AppContext.BaseDirectory, "cad_index.db");
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..60c6c42
--- /dev/null
+++ b/README.md
@@ -0,0 +1,81 @@
+# CadSearchGui
+
+App Windows Forms (.NET 10) per indicizzare, cercare e recuperare i file CAD
+Pro-ENGINEER/Creo di un disco/share di rete, e per trasformare l'estrazione
+"Distinta Base" del gestionale (file `.xls`/`.xml`) in un CSV con il percorso
+CAD di ogni componente.
+
+## Funzionalità
+
+1. **Indice CAD** — all'avvio carica automaticamente `cad_index.db` (se
+ presente accanto all'eseguibile). Se manca, si può selezionare un DB
+ esistente con **Apri DB...** o generarne uno nuovo con **Aggiorna
+ indice**, che scansiona il disco (di default `X:\`, cartelle
+ `__Accessori`, `__B.end`, `__Corpi`, `__Fondi`) e indicizza tutti i file.
+2. **Importa Distinta Base** — trascina (o scegli da file) l'estrazione
+ `.xls`/`.xml` del gestionale (foglio "Distinta Base", colonne
+ Componente/Descrizione): viene generato un CSV con anteprima in griglia.
+3. **Crea cartella raw** — cerca nell'indice il percorso CAD di ogni
+ componente importato e copia i file trovati in una cartella `raw/`
+ accanto al file di distinta originale.
+
+## Requisiti
+
+- Windows
+- [.NET 10 SDK](https://dotnet.microsoft.com/download) (per build/run da sorgente)
+- Accesso alla share di rete con i file CAD (`X:\` di default) per
+ l'indicizzazione
+
+## Build
+
+```powershell
+cd CadSearchGui
+dotnet build -c Release
+```
+
+## Avvio
+
+Da sorgente:
+
+```powershell
+cd CadSearchGui
+dotnet run -c Release
+```
+
+Oppure l'eseguibile già compilato:
+
+```powershell
+cd CadSearchGui\bin\Release\net10.0-windows
+.\CadSearchGui.exe
+```
+
+## Pubblicazione (eseguibile standalone)
+
+Genera un `.exe` self-contained per Windows x64, senza bisogno del runtime
+.NET installato sulla macchina di destinazione:
+
+```powershell
+cd CadSearchGui
+dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -o publish
+```
+
+L'eseguibile risultante è in `CadSearchGui\publish\CadSearchGui.exe`.
+
+## Struttura del progetto
+
+Tutto il codice è in `CadSearchGui/` (unico progetto nella solution, GUI e
+indicizzazione integrate):
+
+- `Program.cs` — entry point.
+- `Form1.cs` — finestra principale (indice CAD, import Distinta Base, griglia
+ risultati); UI costruita interamente in codice, nessun designer visuale.
+- `Indexer.cs` — scansione del disco e scrittura/aggiornamento del DB SQLite.
+- `DistintaBaseImporter.cs` — parsing del file Distinta Base (SpreadsheetML
+ XML o Excel binario/OpenXML tramite ExcelDataReader) e generazione CSV.
+- `ProgressForm.cs` — dialog di avanzamento durante l'aggiornamento indice.
+
+Il database generato (`cad_index.db`), i file Excel/CSV della distinta
+trascinati nella root e la cartella `raw/` sono rigenerabili e non sono
+tracciati in git (vedi `.gitignore`).
+
+Non ci sono test automatizzati nel repo.