Melhorias na usabilidade e interação com o GridView

Substituído duplo-clique por botão para abrir pastas,
adicionada nova coluna com ícone de pasta e handler
`OpenFolderButton_Click`. Células agora usam `TextBox`
readonly para permitir seleção de texto. Melhorado o
comando de cópia (`Ctrl+C`) com tratamento de exceções
e mensagens de erro detalhadas.

Refatorado hit-test para suportar a nova estrutura de
células e adicionado fallback para capturar texto de
outras fontes. Ajustado evento `KeyDown` para abrir
pastas com validações adicionais. Melhorias gerais na
robustez e mensagens informativas ao usuário.
This commit is contained in:
Giuliano Paschoalino 2025-09-30 17:23:44 -03:00
parent 6b791aa3d5
commit 2f1c28e482
2 changed files with 288 additions and 41 deletions

View File

@ -88,7 +88,9 @@
Grid.Column="2" Grid.Column="2"
Text="{Binding SearchUnidadeText, UpdateSourceTrigger=PropertyChanged}" /> Text="{Binding SearchUnidadeText, UpdateSourceTrigger=PropertyChanged}" />
</Grid> </Grid>
<TextBlock Text="Unidades da empresa selecionada (Duplo-clique para abrir pasta):" Margin="10,0,10,0" FontWeight="Bold" Grid.Row="1"/> <!-- texto atualizado: remove menção ao duplo-clique -->
<TextBlock Text="Unidades da empresa selecionada (Clique no ícone da pasta para abrir o caminho):" Margin="10,0,10,0" FontWeight="Bold" Grid.Row="1"/>
<ListView ItemsSource="{Binding UnidadesSelecionadas}" Margin="10" Grid.Row="2" <ListView ItemsSource="{Binding UnidadesSelecionadas}" Margin="10" Grid.Row="2"
x:Name="UnidadesListView" x:Name="UnidadesListView"
PreviewMouseLeftButtonDown="UnidadesListView_PreviewMouseLeftButtonDown" PreviewMouseLeftButtonDown="UnidadesListView_PreviewMouseLeftButtonDown"
@ -104,21 +106,107 @@
<MenuItem Header="Copiar coluna" Click="CopyMenu_Click"/> <MenuItem Header="Copiar coluna" Click="CopyMenu_Click"/>
</ContextMenu> </ContextMenu>
</ListView.ContextMenu> </ListView.ContextMenu>
<ListView.View> <ListView.View>
<GridView> <GridView>
<GridViewColumn Header="Unidade" DisplayMemberBinding="{Binding Unidade}" Width="200" /> <!-- Coluna do ícone de pasta (pequena) -->
<GridViewColumn Header="Instalação" DisplayMemberBinding="{Binding Codigo_Instalacao}" Width="120" /> <GridViewColumn Width="36" Header="">
<GridViewColumn Header="CNPJ" DisplayMemberBinding="{Binding CNPJ_CPF}" Width="120" /> <GridViewColumn.CellTemplate>
<GridViewColumn Header="Razão Social" DisplayMemberBinding="{Binding Razao_Social}" Width="Auto" /> <DataTemplate>
<!-- Botão com ícone (emoji) — simples e confiável.
Pode trocar por uma imagem se preferir (Image/Path). -->
<Button Click="OpenFolderButton_Click"
ToolTip="Abrir pasta da unidade"
Padding="2"
Margin="2"
BorderThickness="0"
Background="Transparent"
Cursor="Hand"
HorizontalAlignment="Center"
VerticalAlignment="Center"
DataContext="{Binding}">
<TextBlock Text="📁" FontSize="14" VerticalAlignment="Center" HorizontalAlignment="Center"/>
</Button>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<!-- Demais colunas (TextBox readonly para seleção) -->
<GridViewColumn Header="Unidade" Width="200">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding Unidade}"
IsReadOnly="True"
BorderThickness="0"
Background="Transparent"
Padding="0"
VerticalAlignment="Center"
HorizontalAlignment="Left"
Cursor="IBeam"
Focusable="True"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Instalação" Width="120">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding Codigo_Instalacao}"
IsReadOnly="True"
BorderThickness="0"
Background="Transparent"
Padding="0"
VerticalAlignment="Center"
HorizontalAlignment="Left"
Cursor="IBeam"
Focusable="True"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="CNPJ" Width="120">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding CNPJ_CPF}"
IsReadOnly="True"
BorderThickness="0"
Background="Transparent"
Padding="0"
VerticalAlignment="Center"
HorizontalAlignment="Left"
Cursor="IBeam"
Focusable="True"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Razão Social" Width="Auto">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding Razao_Social}"
IsReadOnly="True"
BorderThickness="0"
Background="Transparent"
Padding="0"
VerticalAlignment="Center"
HorizontalAlignment="Left"
Cursor="IBeam"
Focusable="True"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView> </GridView>
</ListView.View> </ListView.View>
<ListView.ItemContainerStyle> <ListView.ItemContainerStyle>
<Style TargetType="ListViewItem"> <Style TargetType="ListViewItem">
<EventSetter Event="MouseDoubleClick" Handler="UnidadeListView_MouseDoubleClick" /> <!-- removido MouseDoubleClick -->
<!--<EventSetter Event="MouseDoubleClick" Handler="UnidadeListView_MouseDoubleClick" />-->
<EventSetter Event="KeyDown" Handler="UnidadeListView_EnterKeyDown" /> <EventSetter Event="KeyDown" Handler="UnidadeListView_EnterKeyDown" />
</Style> </Style>
</ListView.ItemContainerStyle> </ListView.ItemContainerStyle>
</ListView> </ListView>
</Grid> </Grid>
</Grid> </Grid>
</Window> </Window>

View File

@ -47,19 +47,27 @@ namespace BD_empresa
if (sender is ListView lv) if (sender is ListView lv)
{ {
var pt = e.GetPosition(lv); var pt = e.GetPosition(lv);
// Se clicou em um ListViewItem, seleciona-o (comportamento útil para context menu)
var hit = VisualTreeHelper.HitTest(lv, pt); var hit = VisualTreeHelper.HitTest(lv, pt);
var dep = hit?.VisualHit; var dep = hit?.VisualHit;
if (dep == null) return;
// Seleciona o item clicado
var lvi = FindAncestor<ListViewItem>(dep); var lvi = FindAncestor<ListViewItem>(dep);
if (lvi != null) if (lvi != null) lvi.IsSelected = true;
{
lvi.IsSelected = true; // Se certo elemento TextBox foi clicado, foca ele (para permitir seleção/Ctrl+C)
} var tbx = FindAncestor<TextBox>(dep) ?? FindDescendant<TextBox>(dep);
tbx?.Focus();
RecordCellTextFromPoint(lv, pt); RecordCellTextFromPoint(lv, pt);
} }
} }
private void UnidadesListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
_lastClickedCellText = null;
}
/// <summary> /// <summary>
/// Executado quando o usuário pressiona Ctrl+C /// Executado quando o usuário pressiona Ctrl+C
/// </summary> /// </summary>
@ -87,8 +95,15 @@ namespace BD_empresa
// 1) se o usuário clicou previamente em uma célula (direito/esquerdo), usamos esse texto // 1) se o usuário clicou previamente em uma célula (direito/esquerdo), usamos esse texto
if (!string.IsNullOrEmpty(_lastClickedCellText)) if (!string.IsNullOrEmpty(_lastClickedCellText))
{
try
{ {
Clipboard.SetText(_lastClickedCellText); Clipboard.SetText(_lastClickedCellText);
}
catch (Exception ex)
{
MessageBox.Show($"Erro ao copiar para o clipboard: {ex.Message}", "Erro", MessageBoxButton.OK, MessageBoxImage.Error);
}
return; return;
} }
@ -106,7 +121,14 @@ namespace BD_empresa
if (prop != null) if (prop != null)
{ {
var value = prop.GetValue(unidade)?.ToString() ?? string.Empty; var value = prop.GetValue(unidade)?.ToString() ?? string.Empty;
try
{
Clipboard.SetText(value); Clipboard.SetText(value);
}
catch (Exception ex)
{
MessageBox.Show($"Erro ao copiar para o clipboard: {ex.Message}", "Erro", MessageBoxButton.OK, MessageBoxImage.Error);
}
return; return;
} }
} }
@ -114,15 +136,29 @@ namespace BD_empresa
var lvi = listView.ItemContainerGenerator.ContainerFromItem(unidade) as ListViewItem; var lvi = listView.ItemContainerGenerator.ContainerFromItem(unidade) as ListViewItem;
var tb = FindDescendant<TextBlock>(lvi); var tb = FindDescendant<TextBlock>(lvi);
if (tb != null) if (tb != null)
{
try
{ {
Clipboard.SetText(tb.Text); Clipboard.SetText(tb.Text);
}
catch (Exception ex)
{
MessageBox.Show($"Erro ao copiar para o clipboard: {ex.Message}", "Erro", MessageBoxButton.OK, MessageBoxImage.Error);
}
return; return;
} }
} }
// 3) fallback final: ToString do objeto // 3) fallback final: ToString do objeto
try
{
Clipboard.SetText(unidade?.ToString() ?? string.Empty); Clipboard.SetText(unidade?.ToString() ?? string.Empty);
} }
catch (Exception ex)
{
MessageBox.Show($"Erro ao copiar para o clipboard: {ex.Message}", "Erro", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
} }
/// <summary> /// <summary>
@ -136,31 +172,68 @@ namespace BD_empresa
var dep = hit?.VisualHit; var dep = hit?.VisualHit;
if (dep == null) return; if (dep == null) return;
// sobe a árvore procurando TextBlock (geralmente GridView cria TextBlock) // 1) Procura TextBox (nossa célula agora é TextBox readonly)
var tb = FindAncestor<TextBlock>(dep); var tbx = FindAncestor<TextBox>(dep) ?? FindDescendant<TextBox>(dep);
if (tb != null) if (tbx != null)
{ {
_lastClickedCellText = tb.Text; // se houver seleção, prioriza a seleção
if (!string.IsNullOrEmpty(tbx.SelectedText))
_lastClickedCellText = tbx.SelectedText;
else
_lastClickedCellText = tbx.Text;
return; return;
} }
// às vezes o TextBlock está abaixo de um Border/ContentPresenter // 2) Procura TextBlock (fallback)
var tblock = FindAncestor<TextBlock>(dep) ?? FindDescendant<TextBlock>(dep);
if (tblock != null)
{
_lastClickedCellText = tblock.Text;
return;
}
// 3) Fallback: procura container e, dentro dele, tenta TextBox primeiro, depois TextBlock
DependencyObject? container = FindAncestor<ContentPresenter>(dep); DependencyObject? container = FindAncestor<ContentPresenter>(dep);
container ??= FindAncestor<GridViewRowPresenter>(dep); container ??= FindAncestor<GridViewRowPresenter>(dep);
if (container != null) if (container != null)
{ {
var tb2 = FindDescendant<TextBlock>(container); var innerTbx = FindDescendant<TextBox>(container);
if (tb2 != null) if (innerTbx != null)
{ {
_lastClickedCellText = tb2.Text; _lastClickedCellText = !string.IsNullOrEmpty(innerTbx.SelectedText) ? innerTbx.SelectedText : innerTbx.Text;
return;
}
var innerTblock = FindDescendant<TextBlock>(container);
if (innerTblock != null)
{
_lastClickedCellText = innerTblock.Text;
return;
}
}
// 4) último recurso: se um ListViewItem foi selecionado, tenta pegar a primeira coluna via binding (fallback anterior)
if (listView.SelectedItem is Data.UnidadeSmart unidade)
{
var gv = listView.View as GridView;
if (gv?.Columns.Count > 0)
{
var firstCol = gv.Columns[0];
if (firstCol.DisplayMemberBinding is System.Windows.Data.Binding b && !string.IsNullOrEmpty(b.Path?.Path))
{
var prop = unidade.GetType().GetProperty(b.Path.Path, BindingFlags.Public | BindingFlags.Instance);
if (prop != null)
{
var value = prop.GetValue(unidade)?.ToString() ?? string.Empty;
_lastClickedCellText = value;
}
}
} }
} }
} }
/// <summary> // Helpers (coloque-os se já não existirem):
/// Encontra primeiro ancestral do tipo T.
/// </summary>
private static T? FindAncestor<T>(DependencyObject? current) where T : DependencyObject private static T? FindAncestor<T>(DependencyObject? current) where T : DependencyObject
{ {
while (current != null) while (current != null)
@ -171,9 +244,6 @@ namespace BD_empresa
return null; return null;
} }
/// <summary>
/// Encontra primeiro descendente do tipo T usando DFS (útil para achar TextBlock dentro de um visual container)
/// </summary>
private static T? FindDescendant<T>(DependencyObject? root) where T : DependencyObject private static T? FindDescendant<T>(DependencyObject? root) where T : DependencyObject
{ {
if (root == null) return null; if (root == null) return null;
@ -192,6 +262,7 @@ namespace BD_empresa
} }
return null; return null;
} }
private void Window_Loaded(object sender, RoutedEventArgs e) private void Window_Loaded(object sender, RoutedEventArgs e)
{ {
txtEmpresaSearch.Focus(); txtEmpresaSearch.Focus();
@ -217,23 +288,111 @@ namespace BD_empresa
} }
} }
private void UnidadeListView_EnterKeyDown(object sender, KeyEventArgs e) private void UnidadeListView_EnterKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter && sender is ListViewItem listViewItem && listViewItem.Content is Data.UnidadeSmart unidade && !string.IsNullOrWhiteSpace(unidade.Caminho_NFs))
{
string? parentDirectory = System.IO.Path.GetDirectoryName(unidade.Caminho_NFs);
if (!string.IsNullOrWhiteSpace(parentDirectory))
{ {
try try
{ {
System.Diagnostics.Process.Start("explorer.exe", unidade.Caminho_NFs); if (e.Key != Key.Enter) { return; }
// DataContext do Button será a UnidadeSmart relacionada à linha
var lvi = sender as ListViewItem;
if (lvi?.Content is not Data.UnidadeSmart unidade)
{
MessageBox.Show("Não foi possível identificar a unidade.", "Erro", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
var caminho = unidade.Caminho_NFs;
if (string.IsNullOrWhiteSpace(caminho))
{
MessageBox.Show("Não há caminho definido para essa unidade.", "Aviso", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
string folderToOpen;
// Se o caminho for um arquivo (provavelmente um arquivo NFe), abre o diretório pai.
if (System.IO.File.Exists(caminho))
{
folderToOpen = System.IO.Path.GetDirectoryName(caminho) ?? caminho;
}
else
{
// tenta obter diretório do caminho (caso usuário tenha colocado um arquivo inexistente ou caminho parcial)
var parent = System.IO.Path.GetDirectoryName(caminho);
if (!string.IsNullOrWhiteSpace(parent) && System.IO.Directory.Exists(parent))
folderToOpen = parent;
else
{
MessageBox.Show($"O caminho informado não existe: {caminho}", "Erro", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
}
// Abre o Explorer na pasta encontrada (UseShellExecute = true para abrir paths corretamente)
var psi = new System.Diagnostics.ProcessStartInfo
{
FileName = "explorer.exe",
Arguments = $"\"{folderToOpen}\"",
UseShellExecute = true
};
System.Diagnostics.Process.Start(psi);
} }
catch (System.Exception ex) catch (System.Exception ex)
{ {
MessageBox.Show($"Não foi possível abrir a pasta: {ex.Message}", "Erro", MessageBoxButton.OK, MessageBoxImage.Error); MessageBox.Show($"Não foi possível abrir a pasta: {ex.Message}", "Erro", MessageBoxButton.OK, MessageBoxImage.Error);
} }
} }
private void OpenFolderButton_Click(object sender, RoutedEventArgs e)
{
try
{
// DataContext do Button será a UnidadeSmart relacionada à linha
var btn = sender as Button;
if (btn?.DataContext is not Data.UnidadeSmart unidade)
{
MessageBox.Show("Não foi possível identificar a unidade.", "Erro", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
var caminho = unidade.Caminho_NFs;
if (string.IsNullOrWhiteSpace(caminho))
{
MessageBox.Show("Não há caminho definido para essa unidade.", "Aviso", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
string folderToOpen;
// Se o caminho for um arquivo (provavelmente um arquivo NFe), abre o diretório pai.
if (System.IO.File.Exists(caminho))
{
folderToOpen = System.IO.Path.GetDirectoryName(caminho) ?? caminho;
}
else
{
// tenta obter diretório do caminho (caso usuário tenha colocado um arquivo inexistente ou caminho parcial)
var parent = System.IO.Path.GetDirectoryName(caminho);
if (!string.IsNullOrWhiteSpace(parent) && System.IO.Directory.Exists(parent))
folderToOpen = parent;
else
{
MessageBox.Show($"O caminho informado não existe: {caminho}", "Erro", MessageBoxButton.OK, MessageBoxImage.Error);
return;
} }
} }
// Abre o Explorer na pasta encontrada (UseShellExecute = true para abrir paths corretamente)
var psi = new System.Diagnostics.ProcessStartInfo
{
FileName = "explorer.exe",
Arguments = $"\"{folderToOpen}\"",
UseShellExecute = true
};
System.Diagnostics.Process.Start(psi);
}
catch (System.Exception ex)
{
MessageBox.Show($"Não foi possível abrir a pasta: {ex.Message}", "Erro", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
} }
} }