Jak dokonać wyboru pola wyboru jednym kliknięciem w WPF DataGrid?

143

Mam DataGrid z pierwszą kolumną jako kolumną tekstową i drugą kolumną jako kolumną CheckBox. Chcę, jeśli kliknę pole wyboru. Powinien zostać sprawdzony.

Ale wybranie wymaga dwóch kliknięć, przy pierwszym kliknięciu komórka jest wybierana, przy drugich kliknięciach pole wyboru jest zaznaczane. Jak sprawić, by pole wyboru było zaznaczane / odznaczane za pomocą jednego kliknięcia.

Używam WPF 4.0. Kolumny w DataGrid są generowane automatycznie.

Książę Ashitaka
źródło
4
Duplikat: stackoverflow.com/questions/1225836/… , ale ten ma lepszy tytuł
surfuj

Odpowiedzi:

189

W przypadku pojedynczego kliknięcia pola wyboru DataGrid możesz po prostu umieścić zwykłą kontrolkę pola wyboru w środku DataGridTemplateColumni ustawić UpdateSourceTrigger=PropertyChanged.

<DataGridTemplateColumn.CellTemplate>
    <DataTemplate>
        <CheckBox IsChecked="{Binding Path=IsSelected, UpdateSourceTrigger=PropertyChanged}" />
    </DataTemplate>
</DataGridTemplateColumn.CellTemplate>
Konstantin Salavatov
źródło
4
WOW - cieszę się, że przeczytałem do końca. Działa to doskonale i jest znacznie mniej skomplikowane, IMO należy to zaznaczyć jako odpowiedź.
Tod
2
Działa to również w przypadku ComboBox. Tak jak w: sposób, WAY lepszy niż DataGridComboBoxColumn.
user1454265
2
Nie działa, gdy używam spacji, aby zaznaczyć / odznaczyć i strzałek, aby przejść do innej komórki.
Yola
1
Zinterpretowałem tę odpowiedź, że musisz powiązać „IsSelected”, ale to nieprawda! Można używać tylko DataGridTemplateColumn.CellTemplatez własnego oprawy i będzie działać !! Odpowiedź @ weidian-huang pomogła mi to zrozumieć, dzięki!
AstralisSomnium
62

Rozwiązałem to za pomocą następującego stylu:

<Style TargetType="DataGridCell">
     <Style.Triggers>
         <Trigger Property="IsMouseOver" Value="True">
             <Setter Property="IsEditing" Value="True" />
         </Trigger>
     </Style.Triggers>
 </Style>

Oczywiście możliwe jest dalsze dostosowanie tego do określonych kolumn ...

Jim Adorno
źródło
8
Miły. Zmieniłem to na MultiTrigger i dodałem warunek dla ReadOnly = False, ale podstawowe podejście działało w moim prostym przypadku, w którym nawigacja za pomocą klawiatury nie jest ważna.
MarcE
Dodanie tego stylu do mojej siatki spowoduje zgłoszenie wyjątku Operation nie jest poprawne, gdy ItemsSource jest używany. Zamiast tego uzyskuj dostęp do elementów i modyfikuj je za pomocą ItemsControl.ItemsSource.
Alkampfer,
1
To najczystszy sposób, jaki do tej pory widziałem! Miły! (dla IsReadOnly = "True", MultiTrigger wykona zadanie)
FooBarTheLittle
2
To rozwiązanie ma nieoczekiwane / niepożądane zachowanie. Zobacz stackoverflow.com/q/39004317/2881450
jHilscher
2
Aby powiązanie działało, będziesz potrzebować UpdateSourceTrigger = PropertyChanged
AQuirky Kwietnia
27

Po pierwsze, wiem, że to dość stare pytanie, ale nadal myślałem, że spróbuję na nie odpowiedzieć.

Kilka dni temu miałem ten sam problem i znalazłem zaskakująco krótkie rozwiązanie (patrz ten blog ). Zasadniczo wszystko, co musisz zrobić, to zastąpić DataGridCheckBoxColumndefinicję w XAML następującą:

<DataGridTemplateColumn Header="MyCheckBoxColumnHeader">
    <DataGridTemplateColumn.CellTemplate>
        <DataTemplate>
            <CheckBox HorizontalAlignment="Center" VerticalAlignment="Center" IsChecked="{Binding Path=MyViewModelProperty, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
        </DataTemplate>
    </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>

Zaleta tego rozwiązania jest oczywista - obsługuje tylko XAML; w ten sposób skutecznie powstrzymuje cię od obciążania twojego kodu z powrotem dodatkową logiką UI i pomaga ci utrzymać swój status w oczach zelotów MVVM;).

Priidu Neemre
źródło
1
Jest to podobne do odpowiedzi Konstantina Salavatova i ta zadziałała dla mnie. +1 za dołączenie próbki kodu tam, gdzie jego nie było. Dzięki za dobrą odpowiedź na stare pytanie.
Don Herod,
1
Problem polega na tym, że jeśli zrobisz to z kolumnami combobox, mały przycisk rozwijany będzie widoczny dla wszystkich komórek w tej kolumnie przez cały czas. Nie tylko po kliknięciu komórki.
user3690202
18

Aby odpowiedź Konstantina Salavatova działała z AutoGenerateColumns, dodaj procedurę obsługi zdarzeń do DataGrid's AutoGeneratingColumnz następującym kodem:

if (e.Column is DataGridCheckBoxColumn && !e.Column.IsReadOnly)
{
    var checkboxFactory = new FrameworkElementFactory(typeof(CheckBox));
    checkboxFactory.SetValue(FrameworkElement.HorizontalAlignmentProperty, HorizontalAlignment.Center);
    checkboxFactory.SetValue(FrameworkElement.VerticalAlignmentProperty, VerticalAlignment.Center);
    checkboxFactory.SetBinding(ToggleButton.IsCheckedProperty, new Binding(e.PropertyName) { UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged });

    e.Column = new DataGridTemplateColumn
        {
            Header = e.Column.Header,
            CellTemplate = new DataTemplate { VisualTree = checkboxFactory },
            SortMemberPath = e.Column.SortMemberPath
        };
}

Dzięki temu wszystkie DataGridautomatycznie generowane kolumny pól wyboru będą dostępne do edycji „jednym kliknięciem”.

Allon Guralnek
źródło
Dzięki za wypełnienie podejścia do kolumny generowanej automatycznie, to zręcznie wskazuje mi odpowiedni kierunek.
el2iot2
17

Na podstawie bloga, do którego odwołuje się odpowiedź Goblin, ale zmodyfikowany do pracy w .NET 4.0 i trybie zaznaczania wierszy.

Zauważ, że przyspiesza również edycję DataGridComboBoxColumn - poprzez wejście w tryb edycji i wyświetlenie listy rozwijanej po jednym kliknięciu lub wprowadzeniu tekstu.

XAML:

        <Style TargetType="{x:Type DataGridCell}">
            <EventSetter Event="PreviewMouseLeftButtonDown" Handler="DataGridCell_PreviewMouseLeftButtonDown" />
            <EventSetter Event="PreviewTextInput" Handler="DataGridCell_PreviewTextInput" />
        </Style>

Za kodem:

    private void DataGridCell_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        DataGridCell cell = sender as DataGridCell;
        GridColumnFastEdit(cell, e);
    }

    private void DataGridCell_PreviewTextInput(object sender, TextCompositionEventArgs e)
    {
        DataGridCell cell = sender as DataGridCell;
        GridColumnFastEdit(cell, e);
    }

    private static void GridColumnFastEdit(DataGridCell cell, RoutedEventArgs e)
    {
        if (cell == null || cell.IsEditing || cell.IsReadOnly)
            return;

        DataGrid dataGrid = FindVisualParent<DataGrid>(cell);
        if (dataGrid == null)
            return;

        if (!cell.IsFocused)
        {
            cell.Focus();
        }

        if (cell.Content is CheckBox)
        {
            if (dataGrid.SelectionUnit != DataGridSelectionUnit.FullRow)
            {
                if (!cell.IsSelected)
                    cell.IsSelected = true;
            }
            else
            {
                DataGridRow row = FindVisualParent<DataGridRow>(cell);
                if (row != null && !row.IsSelected)
                {
                    row.IsSelected = true;
                }
            }
        }
        else
        {
            ComboBox cb = cell.Content as ComboBox;
            if (cb != null)
            {
                //DataGrid dataGrid = FindVisualParent<DataGrid>(cell);
                dataGrid.BeginEdit(e);
                cell.Dispatcher.Invoke(
                 DispatcherPriority.Background,
                 new Action(delegate { }));
                cb.IsDropDownOpen = true;
            }
        }
    }


    private static T FindVisualParent<T>(UIElement element) where T : UIElement
    {
        UIElement parent = element;
        while (parent != null)
        {
            T correctlyTyped = parent as T;
            if (correctlyTyped != null)
            {
                return correctlyTyped;
            }

            parent = VisualTreeHelper.GetParent(parent) as UIElement;
        }
        return null;
    }
surfować
źródło
To rozwiązanie działało najlepiej dla mnie. Mój powiązany ViewModel nie aktualizował się za pomocą innych rozwiązań.
BrokeMyLegBiking
@surfen, czy muszę umieścić powyższy styl i kod na każdej stronie i za jej kodem, jeśli mam wiele stron, które mają w sobie datagrid.Czy jest możliwe użycie stylu i kodu we wspólnym miejscu zamiast tworzenia go w każda strona
Anioł
Dlaczego musisz wysłać pustą akcję?
user3690202
@ user3690202 To jak DoEvents w Windows.Forms. Po wywołaniu BeginEdit musisz poczekać, aż komórka faktycznie przejdzie w tryb edycji.
Jiří Skála
@ JiříSkála - Nie przypominam sobie, żeby kiedykolwiek potrzebowałem tego robić w moich rozwiązaniach tego problemu, ale rozumiem, co mówisz - dzięki!
user3690202
10

Wypróbowałem te sugestie i wiele innych, które znalazłem na innych stronach, ale żadna z nich nie działała dla mnie. W końcu stworzyłem następujące rozwiązanie.

Utworzyłem własną kontrolkę odziedziczoną po DataGrid i po prostu dodałem do niej następujący kod:

public class DataGridWithNavigation : Microsoft.Windows.Controls.DataGrid
{
    public DataGridWithNavigation()
    {
        EventManager.RegisterClassHandler(typeof(DataGridCell), 
            DataGridCell.PreviewMouseLeftButtonDownEvent,
            new RoutedEventHandler(this.OnPreviewMouseLeftButtonDown));
    }


    private void OnPreviewMouseLeftButtonDown(object sender, RoutedEventArgs e)
    {
        DataGridCell cell = sender as DataGridCell;
        if (cell != null && !cell.IsEditing && !cell.IsReadOnly)
        {
          DependencyObject obj = FindFirstControlInChildren(cell, "CheckBox");
            if (obj != null)
            {
                System.Windows.Controls.CheckBox cb = (System.Windows.Controls.CheckBox)obj;
                cb.Focus();
                cb.IsChecked = !cb.IsChecked;
            }
        }
    }

    public DependencyObject FindFirstControlInChildren(DependencyObject obj, string controlType)
    {
        if (obj == null)
            return null;

        // Get a list of all occurrences of a particular type of control (eg "CheckBox") 
        IEnumerable<DependencyObject> ctrls = FindInVisualTreeDown(obj, controlType);
        if (ctrls.Count() == 0)
            return null;

        return ctrls.First();
    }

    public IEnumerable<DependencyObject> FindInVisualTreeDown(DependencyObject obj, string type)
    {
        if (obj != null)
        {
            if (obj.GetType().ToString().EndsWith(type))
            {
                yield return obj;
            }

            for (var i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
            {
                foreach (var child in FindInVisualTreeDown(VisualTreeHelper.GetChild(obj, i), type))
                {
                    if (child != null)
                    {
                        yield return child;
                    }
                }
            }
        }
        yield break;
    }
}

Co to wszystko robi?

Cóż, za każdym razem, gdy klikamy dowolną komórkę w naszym DataGrid, sprawdzamy, czy komórka zawiera w sobie kontrolkę CheckBox. Jeśli to robi , wtedy będziemy ustawić ostrość na tym CheckBox i przełączania jego wartość .

Wydaje mi się, że to działa i jest przyjemnym rozwiązaniem, które można łatwo wykorzystać ponownie.

To rozczarowujące, że my potrzebujemy do zapisu kodu to zrobić chociaż. Wyjaśnienie, że pierwsze kliknięcie myszą (na polu CheckBox DataGrid) jest „ignorowane”, ponieważ WPF używa go do przestawienia wiersza w tryb edycji, może wydawać się logiczne, ale w rzeczywistości jest to sprzeczne ze sposobem działania każdej prawdziwej aplikacji.

Jeśli użytkownik widzi pole wyboru na swoim ekranie, powinien móc je raz kliknąć, aby zaznaczyć / odznaczyć. Koniec opowieści.

Mike Gledhill
źródło
1
Dzięki, wypróbowałem kilka „rozwiązań”, ale to jest pierwsze, które wydaje się naprawdę działać za każdym razem. I pięknie pasuje do mojej architektury aplikacji.
Guge
To rozwiązanie powoduje problemy z aktualizacją wiązania, podczas gdy ten tutaj: wpf.codeplex.com/wikipage?title=Single-Click%20Editing nie.
Justin Simon
2
zbyt skomplikowane. zobacz moją odpowiedź. :)
Konstantin Salavatov
1
Po 5 latach ten kod wciąż oszczędza czas na życie towarzyskie :) Do prostych wymagań wystarczy rozwiązanie @KonstantinSalavatov. W moim przypadku wymieszałem swój kod z rozwiązaniem Mike'a, aby uzyskać dynamiczne skojarzenie zdarzeń z programami obsługi, moja siatka ma dynamiczną liczbę kolumn, za pomocą jednego kliknięcia w określoną komórkę musi przechowywać zmiany w bazie danych.
Fer R
8

Jest tutaj znacznie prostsze rozwiązanie.

          <DataGridTemplateColumn MinWidth="20" >
                <DataGridTemplateColumn.CellTemplate>
                    <DataTemplate>
                        <Grid>
                            <CheckBox VerticalAlignment="Center" HorizontalAlignment="Center"/>
                        </Grid>
                    </DataTemplate>
                </DataGridTemplateColumn.CellTemplate>
            </DataGridTemplateColumn>

Jeśli używasz DataGridCheckBoxColumndo implementacji, pierwsze kliknięcie służy do skupienia, a drugie do sprawdzenia.

Ale użycie DataGridTemplateColumndo wdrożenia wymaga tylko jednego kliknięcia.

Różnica między użyciem DataGridComboboxBoxColumna implementacją przez DataGridTemplateColumnjest również podobna.

Weidian Huang
źródło
Dobre wyjaśnienie dla mnie i zadziałało natychmiast, dzięki!
AstralisSomnium
8

Rozwiązałem to:

<DataGridTemplateColumn>
    <DataGridTemplateColumn.CellTemplate>
        <DataTemplate>
            <Viewbox Height="25">
                <CheckBox IsChecked="{Binding TheProperty, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"/>
            </Viewbox>
        </DataTemplate>
    </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>

Pole wyboru aktywne po jednym kliknięciu!

Darlan Dieterich
źródło
2
Nie musiałem zawijać pola wyboru w ViewBox, ale ta odpowiedź działała dla mnie.
JGeerWM
3
Jest to dla mnie znacznie czystsze rozwiązanie niż przyjęta odpowiedź. Nie potrzebujesz też Viewbox. Zabawne, jak to działa lepiej niż zdefiniowana kolumna Checkbox.
kenjara
6

Bazując na odpowiedzi Jima Adorno i komentarzach do jego postu, jest to rozwiązanie z MultiTrigger:

<Style TargetType="DataGridCell">
  <Style.Triggers>
    <MultiTrigger>
      <MultiTrigger.Conditions>
    <Condition Property="IsReadOnly" Value="False" />
    <Condition Property="IsMouseOver" Value="True" />
      </MultiTrigger.Conditions>
      <Setter Property="IsEditing" Value="True" />
    </MultiTrigger>
  </Style.Triggers>
</Style>
Rafal Spacjer
źródło
5

Jeszcze innym prostym rozwiązaniem jest dodanie tego stylu do kolumny DataGridColumn. Treść stylu może być pusta.

<DataGridCheckBoxColumn>
     <DataGridCheckBoxColumn.ElementStyle>
          <Style TargetType="CheckBox">
           </Style>
     </DataGridCheckBoxColumn.ElementStyle>
</DataGridCheckBoxColumn>
AmirHossein Rezaei
źródło
2
Naciśnięcie spacji, aby zaznaczyć / odznaczyć, przesunie CheckBox z lewej strony na środek. Dodanie <Setter Property = "HorizontalAlignment" Value = "Center" /> w stylu zapobiegnie przesuwaniu się CheckBox.
YantingChen
1
<Style x:Key="StilCelula" TargetType="DataGridCell"> 
<Style.Triggers>
 <Trigger Property="IsMouseOver" Value="True">
   <Setter Property="IsEditing" 
     Value="{Binding RelativeSource={x:Static RelativeSource.Self}, 
     Converter={StaticResource CheckBoxColumnToEditingConvertor}}" />
 </Trigger>
</Style.Triggers>
<Style>
Imports System.Globalization
Public Class CheckBoxColumnToEditingConvertor
    Implements IValueConverter
    Public Function Convert(ByVal value As Object, ByVal targetType As Type, ByVal parameter As Object, ByVal culture As CultureInfo) As Object Implements IValueConverter.Convert
        Try

            Return TypeOf TryCast(value, DataGridCell).Column Is DataGridCheckBoxColumn
        Catch ex As Exception
            Return Visibility.Collapsed
        End Try
    End Function

    Public Function ConvertBack(ByVal value As Object, ByVal targetType As Type, ByVal parameter As Object, ByVal culture As CultureInfo) As Object Implements IValueConverter.ConvertBack
        Throw New NotImplementedException()
    End Function
End Class
TotPeRo
źródło